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 Apr 19, 2023
1 parent 3616f9f commit b083ff9
Show file tree
Hide file tree
Showing 16 changed files with 335 additions and 91 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 @@ -67,9 +67,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 @@ -511,15 +511,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.collect(&: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
85 changes: 51 additions & 34 deletions lib/alchemy/permissions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ class Permissions
def initialize(user)
set_action_aliases
@user ||= user
@user ? user_role_rules : alchemy_guest_user_rules
if @user
@user = Alchemy::UserWithLanguages.new(@user)
user_role_rules
else
alchemy_guest_user_rules
end
end

module GuestUser
Expand Down Expand Up @@ -105,11 +110,11 @@ def alchemy_author_rules
can :manage, Alchemy::Ingredient
can [:crop], Alchemy::Ingredients::Picture
can :manage, Alchemy::LegacyPageUrl
can :manage, Alchemy::Node
can :manage, Alchemy::Node, language_id: @user.accessible_language_ids
can [:read, :url], Alchemy::Picture
can [:read, :autocomplete], Alchemy::Tag
can :edit_content, Alchemy::Page, Alchemy::Page.all do |page|
page.editable_by?(@user)
page.editable_by?(@user.user)
end
end
end
Expand All @@ -124,43 +129,50 @@ 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,
], Alchemy::Page

# Resources which may be locked via template permissions
#
# # config/alchemy/page_layouts.yml
# - name: contact
# editable_by:
# - freelancer
# - admin
#
can([
:create,
:destroy,
], Alchemy::Page) { |p| p.editable_by?(@user) }

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

can :manage, Alchemy::Picture
can :manage, Alchemy::Attachment
can :manage, Alchemy::Tag
can :index, Alchemy::Language
can :switch, Alchemy::Language
end
end

Expand All @@ -175,14 +187,19 @@ def alchemy_admin_rules
alchemy_editor_rules

# Navigation
can :index, [:alchemy_admin_sites, :alchemy_admin_styleguide]
can :index, :alchemy_admin_styleguide
can :index, :alchemy_admin_sites
# can :index, @user.accessible_sites

# Controller actions
can [:info, :update_check], :alchemy_admin_dashboard

# Resources
can :manage, Alchemy::Language
can :manage, Alchemy::Site
can :manage, Alchemy::Language, id: @user.accessible_language_ids
can :manage, Alchemy::Site do |site|
# Only allow to fully manage if the user has access to all of the languages of the site.
(site.language_ids - @user.accessible_language_ids).empty?
end
end
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

0 comments on commit b083ff9

Please sign in to comment.