Skip to content

Commit 67071d8

Browse files
committed
feature: restrict access to sites by user roles
This introduces role-based access control for Alchemy::Site objects. Sites can now define an accessible_by whitelist in config/alchemy/site_layouts.yml, limiting which user roles may access and edit content for each site. If accessible_by is not set, all roles retain access (backward compatible). Access checks integrate with CanCanCan and restrict site selection and page/content management accordingly.
1 parent 7711d16 commit 67071d8

5 files changed

Lines changed: 31 additions & 2 deletions

File tree

app/controllers/alchemy/admin/base_controller.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,18 @@ def load_locked_pages
164164
end
165165

166166
# Returns the current site for admin controllers.
167+
# Raises CanCan::AccessDenied if the user is not allowed to access the site.
167168
#
168169
def current_alchemy_site
169170
@current_alchemy_site ||= begin
170171
site_id = params[:site_id] || session[:alchemy_site_id]
171172
site = Site.find_by(id: site_id) || super
173+
174+
authorize! :access, site if site
175+
rescue CanCan::AccessDenied
176+
site = Site.accessible_by(current_alchemy_user).first
177+
raise
178+
ensure
172179
session[:alchemy_site_id] = site&.id
173180
site
174181
end

app/helpers/alchemy/admin/base_helper.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def alchemy_admin_js_translations(locale = ::I18n.locale)
7575

7676
# Used for site selector in Alchemy cockpit.
7777
def sites_for_select
78-
Alchemy::Site.all.map do |site|
78+
@_sites_for_select ||= Alchemy::Site.accessible_by(current_alchemy_user).map do |site|
7979
[site.name, site.id]
8080
end
8181
end

app/models/alchemy/permissions.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ def alchemy_author_rules
111111
can :edit_content, Alchemy::Page, Alchemy::Page.all do |page|
112112
page.editable_by?(@user)
113113
end
114+
115+
can :switch, Alchemy::Site
116+
can(:access, Alchemy::Site) do |site|
117+
site.accessible_by?(@user)
118+
end
114119
end
115120
end
116121

app/models/alchemy/site.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ def default_language
5151
languages.find_by(default: true)
5252
end
5353

54+
# Returns true if the given user has access to this site.
55+
# A site is accessible by all users if no roles are specified.
56+
#
57+
def accessible_by?(user)
58+
(accessible_by & user.alchemy_roles).any? || accessible_by.empty?
59+
end
60+
61+
# Returns an array of role names that are allowed to access this site.
62+
#
63+
def accessible_by
64+
definition.fetch("accessible_by", [])
65+
end
66+
5467
class << self
5568
def find_for_host(host)
5669
# These are split up into two separate queries in order to run the
@@ -66,6 +79,10 @@ def find_in_aliases(host)
6679
site.aliases.split.include?(host) if site.aliases.present?
6780
end
6881
end
82+
83+
def accessible_by(user)
84+
all.select { |site| site.accessible_by?(user) }
85+
end
6986
end
7087
end
7188
end

app/views/alchemy/admin/partials/_site_select.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<%- if multi_site? -%>
1+
<%- if can?(:switch, Alchemy::Site) && sites_for_select.size > 1 -%>
22
<div class="toolbar_button">
33
<sl-tooltip content="<%= Alchemy.t("Current site") %>">
44
<%= select_tag 'change_site',

0 commit comments

Comments
 (0)