Skip to content

Permitted roles for restricted pages - #4112

Merged
tvdeyen merged 8 commits into
mainfrom
permitted-roles-for-restricted-pages
Aug 7, 2026
Merged

Permitted roles for restricted pages#4112
tvdeyen merged 8 commits into
mainfrom
permitted-roles-for-restricted-pages

Conversation

@sascha-karnatz

@sascha-karnatz sascha-karnatz commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Note

There was a previous PR #4102 that used restricted_roles instead of permitted_roles as column.

What is this pull request for?

Add a permitted_roles column to the Page model to extend the restricted page behavior. This way it is possible restrict the access to pages only for a smaller set of users with a given role.

Notable changes (remove if none)

There is an addition to the MemberUser permission that is now evaluating also the permitted_roles column on the page. The default value is "member" and it should behave same way as before.

Screenshots

CleanShot 2026-07-29 at 11 52 13@2x

Checklist

  • I have followed Pull Request guidelines
  • I have added a detailed description into each commit message
  • I have added tests to cover this change

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.28%. Comparing base (28f8ed4) to head (d36d599).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4112   +/-   ##
=======================================
  Coverage   98.27%   98.28%           
=======================================
  Files         351      352    +1     
  Lines        9239     9276   +37     
=======================================
+ Hits         9080     9117   +37     
  Misses        159      159           
Flag Coverage Δ
mariadb 96.83% <100.00%> (+0.01%) ⬆️
postgresql 95.70% <100.00%> (+0.01%) ⬆️
sqlite 95.70% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tvdeyen tvdeyen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice feature.

Comment thread config/alchemy/config.yml Outdated
Comment thread spec/dummy/config/initializers/alchemy.rb Outdated
Comment thread app/models/alchemy/page.rb Outdated
Comment thread app/models/alchemy/page.rb Outdated
Comment thread app/javascript/alchemy_admin/components/conditional_field.js
Comment thread app/components/alchemy/admin/page_permitted_roles_select.rb Outdated
@tvdeyen tvdeyen added the enhancement New feature or enhancement label Jul 29, 2026
@tvdeyen tvdeyen added this to the 8.4 milestone Jul 29, 2026
@sascha-karnatz
sascha-karnatz force-pushed the permitted-roles-for-restricted-pages branch from f87322a to 89fe4b0 Compare July 30, 2026 07:49
sascha-karnatz added a commit to AlchemyCMS/alchemy_i18n that referenced this pull request Jul 30, 2026
These translations all translated by Claude Opus 5.

Ref: AlchemyCMS/alchemy_cms#4112
@sascha-karnatz
sascha-karnatz force-pushed the permitted-roles-for-restricted-pages branch from 89fe4b0 to db959bc Compare August 4, 2026 07:34
@sascha-karnatz
sascha-karnatz changed the base branch from main to conditional-field-component August 4, 2026 07:36
Base automatically changed from conditional-field-component to main August 4, 2026 08:06
@tvdeyen
tvdeyen force-pushed the permitted-roles-for-restricted-pages branch from db959bc to cc39279 Compare August 4, 2026 08:06

@tvdeyen tvdeyen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to adjust the permission to use a scope as well. There are likely more places that we need to check

Comment thread app/models/alchemy/permissions.rb Outdated
Comment thread app/models/alchemy/permissions.rb Outdated
@tvdeyen tvdeyen self-assigned this Aug 4, 2026
@tvdeyen
tvdeyen requested a review from mamhoff August 6, 2026 14:18
@tvdeyen tvdeyen removed their assignment Aug 6, 2026
@tvdeyen
tvdeyen force-pushed the permitted-roles-for-restricted-pages branch 2 times, most recently from a63c78c to c1f44cc Compare August 7, 2026 09:32
sascha-karnatz and others added 7 commits August 7, 2026 13:25
Add a new permitted_roles column with a space separated list of roles, that can access a page. The default values is "member" to support the same behavior as before.
...to test that feature better in the dummy app.
Add getter and setter to the page model to split and join the group collection. Extend the set_restrictions_to_child_pages behavior to update permitted_roles as well. It was necessary to move from before_save to after_save to update restricted and permitted_roles of child pages. Otherwise inherit_restricted_status would read old data from the database.
Test if the user has the correct role to read the page. This change only affects the MemberUser permission. GuestUser can't see restricted pages and AuthorUser includes the MemberUser module. If Alchemy will be extended by other roles (like restricted_test in the Dummy app), it is necessary to create new abilities to prevent misconfigurations (like to many redirects).
Provide a new PagePermittedRolesSelect view component to render a select with all configured permitted_roles. The select is only visible if the restricted checkbox is enabled.
Remove the width for the input-column class. Without the width this class can also be used for the PagePermittedRolesSelect and the missing width does not have a visual difference in all other place where the class is used (ElementScheduleTimestamps and PagePublicationFields).
The member rule only checked `permitted_roles` inside the CanCanCan block,
so `can?` denied a role-restricted page while `accessible_by` still returned
it. Every query-based consumer (the pages API index, the page tree preloader)
therefore leaked pages a member was not allowed to read.

Fixing this needs the roles to be filterable in SQL. The space separated
string had no portable, collision-safe way to match a single role, so the
column now stores a JSON array in a plain text column, byte identical on every
supported database, and a new `readable_by` scope mirrors the `readable_by?`
predicate for a whole relation, matching a quoted role token with a portable
LIKE. Wiring that scope into the member rule brings `accessible_by` back in
line with the per-instance check.

Because the match is a LIKE, any wildcard in a role name is escaped with
`sanitize_sql_like` so a role cannot smuggle in a `%` or `_`. Assigned roles
are validated against `Alchemy.config.user_roles`, so an unknown role is
rejected with an error instead of being silently stored, and the admin select
offers the configured roles only.
The member node rule mirrored the old binary restriction: `available_to_members`
returned nodes to every published page and the block only checked `public?`.
Once page access became role based, a member without the required role could
still read a node pointing at a page they cannot open, and because the API
nodes index uses `accessible_by`, that node leaked the restricted page's name
and URL.

A new `Node.available_to(user)` scope mirrors `Page#readable_by?` for a whole
relation, so a node is only offered when the user may read its page, and the
member rule checks `readable_by?` in the block as well. For a page permitted to
the default "member" role this reproduces the previous behavior exactly; only
pages restricted to a role the member lacks are now hidden. The former
`available_to_guests` and `available_to_members` scopes stay as they were.
@tvdeyen
tvdeyen force-pushed the permitted-roles-for-restricted-pages branch from c1f44cc to d475c2d Compare August 7, 2026 11:25
Node.available_to_members is deprecated and will be removed in Alchemy 9.0. Use Node.available_to(user) instead.
@tvdeyen
tvdeyen merged commit 307dea5 into main Aug 7, 2026
28 checks passed
@tvdeyen
tvdeyen deleted the permitted-roles-for-restricted-pages branch August 7, 2026 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants