Merge pull request #5057 from consuldemocracy/only_manage_tenants

Add an option to enable the "Multitenancy management mode"
This commit is contained in:
Sebastia
2024-11-06 14:59:50 +01:00
committed by GitHub
36 changed files with 671 additions and 403 deletions

View File

@@ -109,6 +109,14 @@
&.ml-link { &.ml-link {
@include icon(brain, solid); @include icon(brain, solid);
} }
&.administrators-link {
@include icon(user, solid);
}
&.tenants-link {
@include icon(building, regular);
}
} }
li { li {

View File

@@ -3,6 +3,16 @@ class Admin::MenuComponent < ApplicationComponent
use_helpers :can? use_helpers :can?
def links def links
if Rails.application.multitenancy_management_mode?
multitenancy_management_links
else
default_links
end
end
private
def default_links
[ [
(proposals_link if feature?(:proposals)), (proposals_link if feature?(:proposals)),
(debates_link if feature?(:debates)), (debates_link if feature?(:debates)),
@@ -23,7 +33,9 @@ class Admin::MenuComponent < ApplicationComponent
] ]
end end
private def multitenancy_management_links
[tenants_link, administrators_link]
end
def moderated_content? def moderated_content?
moderated_sections.include?(controller_name) && controller.class.module_parent != Admin::Legislation moderated_sections.include?(controller_name) && controller.class.module_parent != Admin::Legislation
@@ -395,7 +407,8 @@ class Admin::MenuComponent < ApplicationComponent
[ [
t("admin.menu.administrators"), t("admin.menu.administrators"),
admin_administrators_path, admin_administrators_path,
controller_name == "administrators" controller_name == "administrators",
class: "administrators-link"
] ]
end end
@@ -482,7 +495,8 @@ class Admin::MenuComponent < ApplicationComponent
[ [
t("admin.menu.multitenancy"), t("admin.menu.multitenancy"),
admin_tenants_path, admin_tenants_path,
controller_name == "tenants" controller_name == "tenants",
class: "tenants-link"
] ]
end end
end end

View File

@@ -2,9 +2,11 @@
<div class="top-links"> <div class="top-links">
<%= render Layout::LocaleSwitcherComponent.new %> <%= render Layout::LocaleSwitcherComponent.new %>
<% if show_link_to_root_path? %>
<%= link_to root_path do %> <%= link_to root_path do %>
<%= t("admin.dashboard.index.back", org: setting["org_name"]) %> <%= t("admin.dashboard.index.back", org: setting["org_name"]) %>
<% end %> <% end %>
<% end %>
</div> </div>
<div class="top-bar"> <div class="top-bar">

View File

@@ -29,4 +29,8 @@ class Layout::AdminHeaderComponent < ApplicationComponent
def show_account_menu? def show_account_menu?
show_admin_menu?(user) || namespace != "management" show_admin_menu?(user) || namespace != "management"
end end
def show_link_to_root_path?
!Rails.application.multitenancy_management_mode?
end
end end

View File

@@ -7,7 +7,7 @@ class Layout::AdminLoginItemsComponent < ApplicationComponent
end end
def render? def render?
show_admin_menu?(user) show_admin_menu?(user) && !Rails.application.multitenancy_management_mode?
end end
private private

View File

@@ -1,6 +1,10 @@
class Layout::FooterComponent < ApplicationComponent class Layout::FooterComponent < ApplicationComponent
use_helpers :content_block use_helpers :content_block
def render?
!Rails.application.multitenancy_management_mode?
end
def footer_legal_content_block def footer_legal_content_block
content_block("footer_legal") content_block("footer_legal")
end end

View File

@@ -1,4 +1,5 @@
<% if user %> <% if user %>
<% if show_my_activity_link? %>
<li> <li>
<%= layout_menu_link_to t("layouts.header.my_activity_link"), <%= layout_menu_link_to t("layouts.header.my_activity_link"),
user_path(user), user_path(user),
@@ -7,6 +8,7 @@
title: t("shared.go_to_page") + title: t("shared.go_to_page") +
t("layouts.header.my_activity_link") %> t("layouts.header.my_activity_link") %>
</li> </li>
<% end %>
<li> <li>
<%= layout_menu_link_to t("layouts.header.my_account_link"), <%= layout_menu_link_to t("layouts.header.my_account_link"),
account_path, account_path,

View File

@@ -5,4 +5,10 @@ class Layout::LoginItemsComponent < ApplicationComponent
def initialize(user) def initialize(user)
@user = user @user = user
end end
private
def show_my_activity_link?
!Rails.application.multitenancy_management_mode?
end
end end

View File

@@ -1,4 +1,3 @@
<% if user %>
<li id="notifications"> <li id="notifications">
<%= link_to notifications_path, rel: "nofollow", <%= link_to notifications_path, rel: "nofollow",
title: text, title: text,
@@ -9,4 +8,3 @@
<span class="show-for-small-only"><%= text %></span> <span class="show-for-small-only"><%= text %></span>
<% end %> <% end %>
</li> </li>
<% end %>

View File

@@ -5,6 +5,10 @@ class Layout::NotificationItemComponent < ApplicationComponent
@user = user @user = user
end end
def render?
user.present? && !Rails.application.multitenancy_management_mode?
end
private private
def text def text

View File

@@ -1,3 +1,7 @@
class Layout::SubnavigationComponent < ApplicationComponent class Layout::SubnavigationComponent < ApplicationComponent
use_helpers :content_block, :layout_menu_link_to use_helpers :content_block, :layout_menu_link_to
def render?
!Rails.application.multitenancy_management_mode?
end
end end

View File

@@ -4,7 +4,13 @@ module AccessDeniedHandler
included do included do
rescue_from CanCan::AccessDenied do |exception| rescue_from CanCan::AccessDenied do |exception|
respond_to do |format| respond_to do |format|
format.html { redirect_to main_app.root_path, alert: exception.message } format.html do
if Rails.application.multitenancy_management_mode?
redirect_to main_app.account_path, alert: exception.message
else
redirect_to main_app.root_path, alert: exception.message
end
end
format.json { render json: { error: exception.message }, status: :forbidden } format.json { render json: { error: exception.message }, status: :forbidden }
end end
end end

View File

@@ -7,7 +7,9 @@ class Users::SessionsController < Devise::SessionsController
private private
def after_sign_in_path_for(resource) def after_sign_in_path_for(resource)
if !verifying_via_email? && resource.show_welcome_screen? if Rails.application.multitenancy_management_mode? && !resource.administrator?
account_path
elsif !verifying_via_email? && resource.show_welcome_screen?
welcome_path welcome_path
else else
super super

View File

@@ -159,6 +159,12 @@ module Consul
# Set to true to enable managing different tenants using the same application # Set to true to enable managing different tenants using the same application
config.multitenancy = Rails.application.secrets.multitenancy config.multitenancy = Rails.application.secrets.multitenancy
# Set to true if you want that the default tenant only to be used to manage other tenants
config.multitenancy_management_mode = Rails.application.secrets.multitenancy_management_mode
def multitenancy_management_mode?
config.multitenancy && Tenant.default? && config.multitenancy_management_mode
end
end end
end end

View File

@@ -6,11 +6,17 @@ Rails.application.routes.draw do
draw :account draw :account
draw :admin draw :admin
draw :devise
constraints lambda { |request| Rails.application.multitenancy_management_mode? } do
get "/", to: "admin/tenants#index"
end
constraints lambda { |request| !Rails.application.multitenancy_management_mode? } do
draw :budget draw :budget
draw :comment draw :comment
draw :community draw :community
draw :debate draw :debate
draw :devise
draw :direct_upload draw :direct_upload
draw :document draw :document
draw :graphql draw :graphql
@@ -47,3 +53,35 @@ Rails.application.routes.draw do
# Static pages # Static pages
resources :pages, path: "/", only: [:show] resources :pages, path: "/", only: [:show]
end end
resolve "Budget::Investment" do |investment, options|
[investment.budget, :investment, options.merge(id: investment)]
end
resolve("Topic") { |topic, options| [topic.community, topic, options] }
resolve "Legislation::Proposal" do |proposal, options|
[proposal.process, :proposal, options.merge(id: proposal)]
end
resolve "Vote" do |vote, options|
[*resource_hierarchy_for(vote.votable), vote, options]
end
resolve "Legislation::Question" do |question, options|
[question.process, :question, options.merge(id: question)]
end
resolve "Legislation::Annotation" do |annotation, options|
[annotation.draft_version.process, :draft_version, :annotation,
options.merge(draft_version_id: annotation.draft_version, id: annotation)]
end
resolve "Poll::Question" do |question, options|
[:question, options.merge(id: question)]
end
resolve "SDG::LocalTarget" do |target, options|
[:local_target, options.merge(id: target)]
end
end

View File

@@ -1,5 +1,18 @@
namespace :admin do namespace :admin do
root to: "dashboard#index" root to: "dashboard#index"
resources :administrators, only: [:index, :create, :destroy, :edit, :update] do
get :search, on: :collection
end
resources :tenants, except: [:show, :destroy] do
member do
put :hide
put :restore
end
end
constraints lambda { |request| !Rails.application.multitenancy_management_mode? } do
resources :organizations, only: :index do resources :organizations, only: :index do
get :search, on: :collection get :search, on: :collection
member do member do
@@ -147,10 +160,6 @@ namespace :admin do
resources :managers, only: [:index, :create, :destroy] resources :managers, only: [:index, :create, :destroy]
end end
resources :administrators, only: [:index, :create, :destroy, :edit, :update] do
get :search, on: :collection
end
resources :users, only: [:index, :show] resources :users, only: [:index, :show]
scope module: :poll do scope module: :poll do
@@ -295,12 +304,6 @@ namespace :admin do
post :execute, on: :collection post :execute, on: :collection
delete :cancel, on: :collection delete :cancel, on: :collection
end end
resources :tenants, except: [:show, :destroy] do
member do
put :hide
put :restore
end
end end
end end

View File

@@ -19,7 +19,3 @@ resources :budgets, only: [:show, :index] do
resource :stats, only: :show, controller: "budgets/stats" resource :stats, only: :show, controller: "budgets/stats"
resource :executions, only: :show, controller: "budgets/executions" resource :executions, only: :show, controller: "budgets/executions"
end end
resolve "Budget::Investment" do |investment, options|
[investment.budget, :investment, options.merge(id: investment)]
end

View File

@@ -1,5 +1,3 @@
resources :communities, only: [:show] do resources :communities, only: [:show] do
resources :topics resources :topics
end end
resolve("Topic") { |topic, options| [topic.community, topic, options] }

View File

@@ -26,3 +26,12 @@
# over the default routes. So, if you define a route for `/proposals`, # over the default routes. So, if you define a route for `/proposals`,
# the default action for `/proposals` will not be used and the one you # the default action for `/proposals` will not be used and the one you
# define will be used instead. # define will be used instead.
constraints lambda { |request| !Rails.application.multitenancy_management_mode? } do
# The routes defined within this block will not be accessible if multitenancy
# management mode is enabled. If you need these routes to be accessible when
# using multitenancy management mode, you should define them outside of this block.
#
# If multitenancy management mode is not being used, routes can be included within
# this block and will still be accessible.
end

View File

@@ -39,20 +39,3 @@ namespace :legislation do
end end
end end
end end
resolve "Legislation::Proposal" do |proposal, options|
[proposal.process, :proposal, options.merge(id: proposal)]
end
resolve "Vote" do |vote, options|
[*resource_hierarchy_for(vote.votable), vote, options]
end
resolve "Legislation::Question" do |question, options|
[question.process, :question, options.merge(id: question)]
end
resolve "Legislation::Annotation" do |annotation, options|
[annotation.draft_version.process, :draft_version, :annotation,
options.merge(draft_version_id: annotation.draft_version, id: annotation)]
end

View File

@@ -8,7 +8,3 @@ resources :polls, only: [:show, :index] do
resources :answers, controller: "polls/answers", only: [:create, :destroy], shallow: false resources :answers, controller: "polls/answers", only: [:create, :destroy], shallow: false
end end
end end
resolve "Poll::Question" do |question, options|
[:question, options.merge(id: question)]
end

View File

@@ -24,7 +24,3 @@ namespace :sdg_management do
get "#{type}/:id/edit", to: "relations#edit", as: "edit_#{type.singularize}" get "#{type}/:id/edit", to: "relations#edit", as: "edit_#{type.singularize}"
end end
end end
resolve "SDG::LocalTarget" do |target, options|
[:local_target, options.merge(id: target)]
end

View File

@@ -22,6 +22,7 @@ development:
authentication_logs: false authentication_logs: false
devise_lockable: false devise_lockable: false
multitenancy: false multitenancy: false
multitenancy_management_mode: false
security: security:
# allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"] # allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"]
last_sign_in: false last_sign_in: false
@@ -64,6 +65,7 @@ staging:
managers_url: "" managers_url: ""
managers_application_key: "" managers_application_key: ""
multitenancy: false multitenancy: false
multitenancy_management_mode: false
security: security:
# allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"] # allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"]
last_sign_in: false last_sign_in: false
@@ -119,6 +121,7 @@ preproduction:
managers_url: "" managers_url: ""
managers_application_key: "" managers_application_key: ""
multitenancy: false multitenancy: false
multitenancy_management_mode: false
security: security:
# allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"] # allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"]
last_sign_in: false last_sign_in: false
@@ -173,6 +176,7 @@ production:
managers_url: "" managers_url: ""
managers_application_key: "" managers_application_key: ""
multitenancy: false multitenancy: false
multitenancy_management_mode: false
security: security:
# allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"] # allowed_admin_ips: ["123.45.67.89", "192.168.1.0/24"]
last_sign_in: false last_sign_in: false

View File

@@ -78,6 +78,21 @@ Note that, if you use a different domain for a tenant, you'll have to configure
When adding a new tenant, an admin user **copying the same login data as the administrator creating the tenant** will be automatically created. Note this user is stored in the database schema of the new tenant, so changing their password in one tenant won't change their password in any other tenants. When adding a new tenant, an admin user **copying the same login data as the administrator creating the tenant** will be automatically created. Note this user is stored in the database schema of the new tenant, so changing their password in one tenant won't change their password in any other tenants.
### Multitenancy management mode
The `multitenancy_management_mode` setting allows using the main tenant solely for managing other tenants and admin users, hiding any other admin panel functionality or public content.
There are two possible ways to enable multitenancy management mode:
* Adding `config.multitenancy_management_mode = true` inside the `class Application < Rails::Application` class in the `config/application_custom.rb` file
* Replacing the line `multitenancy_management_mode: false` with `multitenancy_management_mode: true` (or adding it if it isn't already there) in the `config/secrets.yml` file
We recommend using the same method that has been used to enable the multitenancy functionality in the [Common step for all Consul Democracy installations](#common-step-for-all-consul-democracy-installations) section.
After enabling this option, restart the application and you will see the administration panel as follows:
![The administration panel only contains links to multitenancy and administrators](../../img/multitenancy/management-mode-en.png)
## Steps to take after adding a tenant ## Steps to take after adding a tenant
### SSL certificates ### SSL certificates

View File

@@ -78,6 +78,21 @@ Nótese que, si estás usando un dominio distinto para una entidad, tendrás que
Al añadir una nueva entidad, se creará automáticamente un usuario con permiso de administrador para esta nueva entidad **cuyos datos de acceso serán una copia de los del administrador que crea la entidad**. Este usuario se almacenará en el esquema de base de datos de la nueva entidad, con lo que cambiar su contraseña en una entidad no cambiará su contraseña en otras entidades. Al añadir una nueva entidad, se creará automáticamente un usuario con permiso de administrador para esta nueva entidad **cuyos datos de acceso serán una copia de los del administrador que crea la entidad**. Este usuario se almacenará en el esquema de base de datos de la nueva entidad, con lo que cambiar su contraseña en una entidad no cambiará su contraseña en otras entidades.
### Modo de gestión de multientidad
La configuración `multitenancy_management_mode` permite utilizar la entidad principal únicamente para gestionar otras entidades y usuarios administradores, ocultando cualquier otra funcionalidad del panel de administración o contenido público.
Existen dos posibles maneras de habilitar este modo de gestión de multientidad:
* Añadiendo `config.multitenancy_management_mode = true` dentro de la clase `class Application < Rails::Application` del fichero `config/application_custom.rb`
* Cambiando la línea `multitenancy_management_mode: false` por `multitenancy_management_mode: true` (o añadiéndola si no está ya ahí) en el fichero `config/secrets.yml`
Recomendamos utilizar el mismo método que se ha utilizado para habilitar la funcionalidad de multientidad en la sección [Paso común a todas las instalaciones de Consul Democracy](#paso-común-a-todas-las-instalaciones-de-consul-democracy).
Tras habilitar esta opción, reinicia la aplicación y podrás ver el panel de administración de la siguiente manera:
![El panel de administración sólo contiene enlaces a multientidad y administradores](../../img/multitenancy/management-mode-es.png)
## Pasos a realizar tras añadir una entidad ## Pasos a realizar tras añadir una entidad
### Certificados SSL ### Certificados SSL

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

View File

@@ -1,6 +1,6 @@
require "rails_helper" require "rails_helper"
describe Admin::MenuComponent, controller: Admin::NewslettersController do describe Admin::MenuComponent, :admin, controller: Admin::NewslettersController do
it "disables all buttons when JavaScript isn't available" do it "disables all buttons when JavaScript isn't available" do
render_inline Admin::MenuComponent.new render_inline Admin::MenuComponent.new
@@ -20,6 +20,17 @@ describe Admin::MenuComponent, controller: Admin::NewslettersController do
expect(page).to have_css "button[aria-expanded='false']", exact_text: "Settings" expect(page).to have_css "button[aria-expanded='false']", exact_text: "Settings"
end end
it "only renders the multitenancy and administrators sections in multitenancy management mode" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
render_inline Admin::MenuComponent.new
expect(page).to have_css "#admin_menu"
expect(page).to have_link "Multitenancy"
expect(page).to have_link "Administrators"
expect(page).to have_link count: 2
end
describe "#polls_link" do describe "#polls_link" do
it "is marked as current when managing poll options", it "is marked as current when managing poll options",
controller: Admin::Poll::Questions::OptionsController do controller: Admin::Poll::Questions::OptionsController do

View File

@@ -35,4 +35,13 @@ describe Layout::AdminHeaderComponent do
expect(page).not_to have_css "[data-toggle]" expect(page).not_to have_css "[data-toggle]"
end end
end end
it "does not show link to root path when multitenancy_management_mode is enabled" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
create(:administrator, user: user)
render_inline Layout::AdminHeaderComponent.new(user)
expect(page).not_to have_link "Go back to CONSUL"
end
end end

View File

@@ -15,6 +15,15 @@ describe Layout::AdminLoginItemsComponent do
expect(page).not_to be_rendered expect(page).not_to be_rendered
end end
it "is not rendered when multitenancy_management_mode is enabled" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
user = create(:administrator).user
render_inline Layout::AdminLoginItemsComponent.new(user)
expect(page).not_to be_rendered
end
it "shows access to all places except officing to administrators" do it "shows access to all places except officing to administrators" do
user = create(:administrator).user user = create(:administrator).user

View File

@@ -13,4 +13,11 @@ describe Layout::FooterComponent do
end end
end end
end end
it "is not rendered when multitenancy_management_mode is enabled" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
render_inline Layout::FooterComponent.new
expect(page).not_to be_rendered
end
end end

View File

@@ -0,0 +1,11 @@
require "rails_helper"
describe Layout::LoginItemsComponent do
it "does not show the my activity link when multitenancy_management_mode is enabled" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
render_inline Layout::LoginItemsComponent.new(create(:user))
expect(page).not_to have_content "My content"
end
end

View File

@@ -0,0 +1,22 @@
require "rails_helper"
describe Layout::NotificationItemComponent do
it "is not rendered for anonymous users" do
render_inline Layout::NotificationItemComponent.new(nil)
expect(page).not_to be_rendered
end
it "is rendered for identified users" do
render_inline Layout::NotificationItemComponent.new(create(:user))
expect(page).to be_rendered
end
it "is not rendered when multitenancy_management_mode is enabled" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
render_inline Layout::NotificationItemComponent.new(create(:user))
expect(page).not_to be_rendered
end
end

View File

@@ -0,0 +1,10 @@
require "rails_helper"
describe Layout::SubnavigationComponent do
it "is not rendered when multitenancy_management_mode is enabled" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
render_inline Layout::SubnavigationComponent.new
expect(page).not_to be_rendered
end
end

View File

@@ -69,4 +69,22 @@ describe Users::SessionsController do
end end
end end
end end
describe "after_sign_in_path_for" do
it "redirects to account path when multitenancy_management_mode is enabled and user is not an admin" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
post :create, params: { user: { login: "citizen@consul.org", password: "12345678" }}
expect(response).to redirect_to account_path
end
it "redirects to welcome path when multitenancy_management_mode is disabled" do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(false)
post :create, params: { user: { login: "citizen@consul.org", password: "12345678" }}
expect(response).to redirect_to welcome_path
end
end
end end

View File

@@ -0,0 +1,58 @@
require "rails_helper"
describe "Multitenancy management mode", :admin do
before do
allow(Rails.application.config).to receive(:multitenancy_management_mode).and_return(true)
Setting["org_name"] = "CONSUL"
end
scenario "renders expected content for multitenancy manage mode in admin section" do
visit admin_root_path
within ".top-links" do
expect(page).not_to have_content "Go back to CONSUL"
end
within ".top-bar" do
expect(page).to have_css "li", count: 2
expect(page).to have_content "My account"
expect(page).to have_content "Sign out"
end
within "#admin_menu" do
expect(page).to have_content "Multitenancy"
expect(page).to have_content "Administrators"
expect(page).to have_css "li", count: 2
end
end
scenario "redirects root path requests to the admin tenants path" do
visit root_path
expect(page).to have_content "CONSUL ADMINISTRATION", normalize_ws: true
expect(page).to have_content "Multitenancy"
expect(page).not_to have_content "Most active proposals"
end
scenario "does not redirect other tenants when visiting the root path", :seed_tenants do
create(:tenant, schema: "mars")
with_subdomain("mars") do
visit root_path
expect(page).to have_content "Most active proposals"
expect(page).not_to have_content "Multitenancy"
expect(page).not_to have_content "CONSUL ADMINISTRATION", normalize_ws: true
end
end
scenario "redirects to account path when regular users try to access the admin section" do
logout
login_as(create(:user))
visit admin_root_path
expect(page).to have_current_path account_path
expect(page).to have_content "You do not have permission to access this page."
end
end