Authorization
Lean CMS uses two layers of authorization, deliberately. They solve different problems and don’t substitute for each other.
| Layer | Used for | Provided by |
|---|---|---|
| Area gates | “Is this user allowed in /lean-cms at all? In the blog editor? In settings?” | LeanCms::Authorization controller concern — simple before_actions that check permission flags on the user. |
| Per-record decisions | “Can this admin modify that super-admin? Can this editor delete a post they didn’t author?” | Pundit policies — LeanCms::PostPolicy, LeanCms::UserPolicy, etc. |
Both layers are already wired into the gem’s CMS controllers. You only need to think about them when you build your own controllers that interact with Lean CMS-managed records or live inside the admin area.
The permission flags
The User model carries these boolean columns (see Installation step 4):
| Flag | Powers |
|---|---|
is_super_admin |
Implicit true for every permission below. |
can_edit_pages |
/lean-cms/page-contents/*, inline-edit endpoints. |
can_edit_blog |
/lean-cms/posts/* (blog + portfolio). |
can_manage_users |
/lean-cms/users/*. |
can_access_settings |
/lean-cms/settings/*, /lean-cms/notification_settings/*. |
The corresponding User predicates (can_edit_pages?, etc.) must return
true whenever the user is a super admin. The recommended pattern:
def can_edit_pages?
is_super_admin? || can_edit_pages
end
has_any_cms_permission? is the master gate — return true if the user has
any of the four area permissions or is a super admin.
Layer 1: Area gates (LeanCms::Authorization)
LeanCms::Authorization is the controller concern included automatically by
LeanCms::ApplicationController. It installs:
before_action :require_cms_access
…which redirects to / with a flash alert if current_user&.has_any_cms_permission? is false. Inside the gem, specific areas tighten this with:
before_action :require_page_editing # in LeanCms::PageContentsController
before_action :require_blog_editing # in LeanCms::PostsController
before_action :require_user_management # in LeanCms::UsersController
before_action :require_settings_access # in LeanCms::SettingsController
Using area gates in your own controllers
If you build a custom controller that should only be reachable by users with
a specific CMS permission, include the concern and add the right
before_action:
class Admin::ReportsController < ApplicationController
include LeanCms::Authorization
before_action :require_settings_access
end
Available before-action helpers: require_cms_access, require_page_editing,
require_blog_editing, require_user_management, require_settings_access.
Layer 2: Per-record decisions (Pundit)
Within an area the user is allowed into, finer questions still need answers:
- “Editors can edit any post, but only super admins can edit posts other editors wrote.”
- “A user manager can deactivate any user — except themselves, and except super admins.”
- “A non-author can read drafts only if they have super-admin rights.”
These are record-level decisions, and they live in Pundit policies under
app/policies/lean_cms/. Example:
class LeanCms::PostPolicy < LeanCms::ApplicationPolicy
def update?
return false unless user.can_edit_blog?
user.is_super_admin? || record.author_id == user.id
end
def destroy? = update?
end
Inside any Lean CMS admin controller, you can call:
authorize @post # runs PostPolicy#update? (or whatever action maps)
policy_scope(LeanCms::Post) # returns the subset the user is allowed to see
policy(@post).update? # boolean check, no exception
Using Pundit in your own controllers and views
Pundit is already a dependency of Lean CMS and the gem’s policies are
namespaced. If you have your own record types that should respect CMS
permissions, drop a policy under app/policies/:
class TestimonialPolicy < ApplicationPolicy
def update?
user.can_edit_pages? # reuse a Lean CMS permission flag
end
end
…and in your controller:
class TestimonialsController < ApplicationController
include LeanCms::Authorization
before_action :require_page_editing # area gate
before_action :set_testimonial, only: %i[ edit update ]
before_action -> { authorize @testimonial }, only: %i[ edit update ] # per-record
end
When to use which
| Question | Answer | Use |
|---|---|---|
| “Can this user see this area at all?” | Yes/no based on permission flags. | LeanCms::Authorization before_action. |
| “Can this user perform this action on this record?” | Depends on record state, ownership, or the user’s relationship to it. | Pundit policy. |
| “Should I show or hide this UI element for this user?” | A view question. | policy(record).update? in the view. |
| “Which records should be in this list?” | A scoping question. | policy_scope(Model) — the policy’s Scope#resolve. |
If you find yourself reaching for Pundit to express “this user has the
permission to edit pages,” that’s an area gate, not a record decision —
prefer require_page_editing. If you find yourself reaching for a permission
flag to express “but only on posts they wrote,” that’s a per-record
decision — write a policy.
Why the hybrid
A Pundit policy that ignores record to answer “does this user have
permission X?” is policy busywork. A before_action that has to fan-thread
record ownership through controller code is controller busywork. Keeping
the two layers separate keeps each one declarative.