Skip to content

Commit

Permalink
Add ability to restrict users to specific languages
Browse files Browse the repository at this point in the history
  • Loading branch information
dbwinger committed Nov 29, 2023
1 parent dfbc336 commit 9318a2a
Show file tree
Hide file tree
Showing 17 changed files with 302 additions and 64 deletions.
18 changes: 18 additions & 0 deletions app/controllers/concerns/alchemy/admin/current_language.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,29 @@ module CurrentLanguage
extend ActiveSupport::Concern

included do
prepend_before_action :redirect_to_accessible_site_language, only: :index
before_action :load_current_language
end

private

def current_alchemy_user_with_languages
UserWithLanguages.new(current_alchemy_user)
end

# If the current alchemy user has not been given access to the current site/language, change them to ones the user has access to.
def redirect_to_accessible_site_language
if Alchemy::Language.current
if current_alchemy_user_with_languages.accessible_sites.exclude? Alchemy::Site.current
set_alchemy_language current_alchemy_user_with_languages.accessible_languages.first
@current_alchemy_site = Language.current.site
set_current_alchemy_site
elsif current_alchemy_user_with_languages.accessible_languages.exclude? Alchemy::Language.current
set_alchemy_language current_alchemy_user_with_languages.accessible_languages.on_current_site.first
end
end
end

def load_current_language
@current_language = Alchemy::Language.current
if @current_language.nil?
Expand Down
36 changes: 36 additions & 0 deletions app/decorators/alchemy/user_with_languages.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Alchemy
class UserWithLanguages < SimpleDelegator
alias_method :user, :__getobj__

def language_restriction_implemented?
user.respond_to? :languages
end

def accessible_languages
language_restriction_implemented? ? super : Alchemy::Language.all
end

def accessible_language_ids
accessible_languages.pluck(:id)
end

def languages_restricted?
!language_restriction_implemented? ||
accessible_languages != Alchemy::Language.all
end

def accessible_site_ids
accessible_languages.map(&:site_id).uniq
end

def accessible_sites
Alchemy::Site.where(id: accessible_site_ids).order(:id)
end

def can_access_language?(language)
accessible_languages.where(id: language.id).any?
end
end
end
4 changes: 1 addition & 3 deletions app/helpers/alchemy/admin/base_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,7 @@ def translations_for_select

# Used for site selector in Alchemy cockpit.
def sites_for_select
Alchemy::Site.all.map do |site|
[site.name, site.id]
end
UserWithLanguages.new(current_alchemy_user).accessible_sites.pluck(:name, :id)
end

# Returns a javascript driven live filter for lists.
Expand Down
1 change: 1 addition & 0 deletions app/models/alchemy/language.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Language < BaseRecord
belongs_to :site
has_many :pages, inverse_of: :language
has_many :nodes, inverse_of: :language
has_and_belongs_to_many :users, class_name: Alchemy.user_class_name, join_table: :alchemy_users_languages

before_validation :set_locale, if: -> { locale.blank? }

Expand Down
14 changes: 9 additions & 5 deletions app/models/alchemy/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -471,15 +471,19 @@ def attribute_fixed?(name)
fixed_attributes.fixed?(name)
end

# Checks the current page's list of editors, if defined.
# Checks the current page's list of editors, if defined, and the user's accessible languages, if they are restricted
#
# This allows us to pass in a user and see if any of their roles are enable
# them to make edits
# This allows us to pass in a user and see if any of their roles/languages enable them to make edits
#
def editable_by?(user)
return true unless has_limited_editors?
user = UserWithLanguages.new(user)

(editor_roles & user.alchemy_roles).any?
if has_limited_editors? || user.languages_restricted?
(!has_limited_editors? || (editor_roles & user.alchemy_roles).any?) &&
user.accessible_languages.include?(language)
else
true
end
end

# Returns the value of +public_on+ attribute from public version
Expand Down
4 changes: 1 addition & 3 deletions app/models/alchemy/page/page_natures.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ def has_limited_editors?
end

def editor_roles
return unless has_limited_editors?

definition["editable_by"]
has_limited_editors? ? definition["editable_by"] : []
end

# True if page locked_at timestamp and locked_by id are set
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<% languages = Alchemy::Language.on_current_site %>
<% if can?(:switch_language, Alchemy::Page) && languages.many? %>
<% languages = Alchemy::Language.accessible_by(Alchemy::Permissions.new(current_alchemy_user), :switch_language).on_current_site %>
<% if can?(:switch_language, Alchemy::Page) && Alchemy::Language.current.site.languages.many? %>
<div class="button_with_label">
<%= form_tag switch_admin_languages_path, method: 'get' do %>
<%= select_tag(
Expand Down
4 changes: 2 additions & 2 deletions app/views/alchemy/admin/pictures/_infos.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
<li>
<h3>
<%= render_icon 'file-alt' %>
<p><%= link_to page.name, edit_admin_page_path(page) %></p>
<p><%= link_to_if can?(:edit, page), page.name, edit_admin_page_path(page) %></p>
</h3>
<ul class="list">
<% picture_ingredients.group_by(&:element).each do |element, picture_ingredients| %>
<li class="<%= cycle('even', 'odd') %>">
<% page_link = link_to element.display_name_with_preview_text,
<% page_link = link_to_if can?(:edit, page), element.display_name_with_preview_text,
edit_admin_page_path(page, anchor: "element_#{element.id}") %>
<% ingredients = picture_ingredients.map { |p| Alchemy::IngredientEditor.new(p).translated_role }.to_sentence %>
<% if element.public? %>
Expand Down
13 changes: 13 additions & 0 deletions db/migrate/20230407153522_create_alchemy_users_languages.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

class CreateAlchemyUsersLanguages < ActiveRecord::Migration[6.1]
def change
create_table :alchemy_users_languages do |t|
t.bigint :user_id, null: false
t.bigint :language_id, full: false

t.index :user_id
t.index :language_id
end
end
end
59 changes: 44 additions & 15 deletions lib/alchemy/permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ def alchemy_guest_user_rules
e.public? && !e.restricted?
end

can :read, Alchemy::Page, Alchemy::Page.published.not_restricted.from_current_site do |p|
p.public? && !p.restricted? && p.site == Alchemy::Site.current
can :read, Alchemy::Page, Alchemy::Page.published.not_restricted do |p|
p.public? && !p.restricted?
end
end
end
Expand All @@ -64,8 +64,8 @@ def alchemy_member_rules
e.public?
end

can :read, Alchemy::Page, Alchemy::Page.published.from_current_site do |p|
p.public? && p.site == Alchemy::Site.current
can :read, Alchemy::Page, Alchemy::Page.published do |p|
p.public?
end
end
end
Expand All @@ -89,7 +89,7 @@ def alchemy_author_rules
:alchemy_admin_pages,
:alchemy_admin_pictures,
:alchemy_admin_tags,
:alchemy_admin_users
:alchemy_admin_users,
]

# Controller actions
Expand Down Expand Up @@ -124,19 +124,46 @@ module EditorUser
def alchemy_editor_rules
alchemy_author_rules

# Navigation
can :index, [
:alchemy_admin_languages,
:alchemy_admin_users
]

# Resources
if @user.accessible_languages.any?
# Navigation
# Allow to view (but not edit) all languages in sites for which the user has access to at least one language
can :index, :alchemy_admin_languages
can :index, Alchemy::Language, site_id: @user.accessible_site_ids

# Resources
can [
:copy,
:copy_language_tree,
:flush,
:order,
:switch_language,
], Alchemy::Page, Alchemy::Page.where(language: @user.accessible_languages) do |page|
@user.can_access_language? page.language
end

# Resources which may be locked via template permissions
#
# # config/alchemy/page_layouts.yml
# - name: contact
# editable_by:
# - freelancer
# - admin
#
can :publish, Alchemy::Page, Alchemy::Page.where(language: { id: @user.accessible_languages, public: true }) do |page|
page.language.public? && page.editable_by?(@user.user)
end
can :switch, Alchemy::Language
elsif Alchemy::Language.none?
can :index, Alchemy::Language
end

can [
:copy,
:copy_language_tree,
:flush,
:order,
:switch_language
:switch_language,
], Alchemy::Page

# Resources which may be locked via template permissions
Expand All @@ -149,10 +176,12 @@ def alchemy_editor_rules
#
can([
:create,
:destroy
], Alchemy::Page) { |p| p.editable_by?(@user) }
:destroy,
], Alchemy::Page, Alchemy::Page.where(language: @user.accessible_languages) do |page|
page.editable_by?(@user.user)
end

can(:publish, Alchemy::Page) do |page|
can(:publish, Alchemy::Page, Alchemy::Page.where(language: @user.accessible_languages)) do |page|
page.language.public? && page.editable_by?(@user)
end

Expand Down
10 changes: 10 additions & 0 deletions spec/dummy/app/models/dummy_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

class DummyUser < ActiveRecord::Base
has_many :folded_pages, class_name: "Alchemy::FoldedPage"
has_and_belongs_to_many :languages, class_name: "Alchemy::Language", foreign_key: :user_id, join_table: :alchemy_users_languages

attr_writer :alchemy_roles, :name

def self.logged_in
Expand All @@ -23,4 +25,12 @@ def name
def human_roles_string
alchemy_roles.map(&:humanize)
end

# Languages this user is allowed to access
#
# An empty collection means allow all languages
#
def accessible_languages
languages.any? ? languages : Alchemy::Language.all
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# frozen_string_literal: true

# This migration comes from alchemy (originally 20230407153522)
class CreateAlchemyUsersLanguages < ActiveRecord::Migration[6.1]
def change
create_table :alchemy_users_languages do |t|
t.bigint :user_id, null: false
t.bigint :language_id, full: false

t.index :user_id
t.index :language_id
end
end
end
9 changes: 0 additions & 9 deletions spec/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,6 @@
t.index ["updater_id"], name: "index_alchemy_nodes_on_updater_id"
end

create_table "alchemy_page_mutexes", force: :cascade do |t|
t.integer "page_id", null: false
t.datetime "created_at"
t.index ["page_id"], name: "index_alchemy_page_mutexes_on_page_id", unique: true
end

create_table "alchemy_page_versions", force: :cascade do |t|
t.integer "page_id", null: false
t.datetime "public_on", precision: nil
Expand Down Expand Up @@ -208,8 +202,6 @@
t.integer "image_file_size"
t.string "image_file_format"
t.index ["creator_id"], name: "index_alchemy_pictures_on_creator_id"
t.index ["image_file_name"], name: "index_alchemy_pictures_on_image_file_name"
t.index ["name"], name: "index_alchemy_pictures_on_name"
t.index ["updater_id"], name: "index_alchemy_pictures_on_updater_id"
end

Expand Down Expand Up @@ -289,7 +281,6 @@
add_foreign_key "alchemy_languages", "alchemy_sites", column: "site_id"
add_foreign_key "alchemy_nodes", "alchemy_languages", column: "language_id"
add_foreign_key "alchemy_nodes", "alchemy_pages", column: "page_id", on_delete: :restrict
add_foreign_key "alchemy_page_mutexes", "alchemy_pages", column: "page_id"
add_foreign_key "alchemy_page_versions", "alchemy_pages", column: "page_id", on_delete: :cascade
add_foreign_key "alchemy_pages", "alchemy_languages", column: "language_id"
add_foreign_key "alchemy_picture_thumbs", "alchemy_pictures", column: "picture_id"
Expand Down
1 change: 1 addition & 0 deletions spec/features/admin/navigation_feature_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

context "editor users" do
let!(:default_site) { create(:alchemy_site) }
let!(:default_language) { create(:alchemy_language) }
before { authorize_user(:as_editor) }

it "can access the languages page" do
Expand Down
2 changes: 2 additions & 0 deletions spec/features/admin/site_select_feature_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

context "with multiple sites" do
let!(:default_site) { create(:alchemy_site, :default) }
let!(:default_language) { create(:alchemy_language) }
let!(:a_site) { create(:alchemy_site) }
let!(:a_language) { create(:alchemy_language, site: a_site) }

context "not on pages or languages module" do
it "does not display the site select" do
Expand Down

0 comments on commit 9318a2a

Please sign in to comment.