Authorization
Entitlement gating versus record-level Pundit policies, and how they compose.
One Shot answers two different authorization questions, and it answers them with two different mechanisms on purpose: whether a workspace is allowed to use a paid feature at all, and whether this specific user can act on this specific record. Conflating the two either lets a free account touch paid functionality, or makes every feature-gate check reimplement record-level scoping. Keep them separate.
Entitlement: the coarse paywall
RequireEntitlement answers "may this workspace use the paid surface at all?" Include it in any
controller whose feature is paid:
class ProjectsController < ApplicationController
include RequireEntitlement
# ...
end
It checks Billing.for(Current.account).entitled? on every action, and redirects to the pricing
page when the account isn't entitled. Admins are exempt, so an admin account can always reach a
paid controller regardless of subscription status. Drop the include entirely for a feature that
should be free.
See Gating a Feature for the full walkthrough of adding this to a new resource.
Record-level authorization: Pundit
Entitlement answers "is this workspace allowed in here," not "which rows can it see." That's
Pundit's job. Every resource gets a policy in app/policies/, subclassing ApplicationPolicy, and
two safety defaults apply everywhere:
- Account-scoped by default.
ApplicationPolicy::Scopefilters any relation carrying anaccount_idcolumn down to the acting user's account. Callpolicy_scope(SomeModel)instead ofSomeModel.all, and a request for another tenant's record can't leak through, even if a developer forgets to scope a specific query by hand. - Deny by default. The base policy returns
falsefor every action. A resource's own policy has to opt in per action; nothing is implicitly allowed just because a policy class exists.
Controllers call authorize @record before acting on it, and policy_scope(Relation) when listing
records. pundit_user resolves to Current.user (guests get nil, so a policy can distinguish
signed-in from anonymous without a separate check). ProjectPolicy is the reference
implementation, and spec/policies/project_policy_spec.rb is the reference spec, including the
tenant-isolation test: requesting a record that belongs to a different account by id should behave
like the record doesn't exist, not like it's forbidden. A 404, not a 403, is what stops a URL from
confirming that another tenant's data exists at all.
Roles
User#role is either member or admin. There's no admin UI in v1: an admin manages data through
bin/rails console, and the admin? check exists mainly to exempt admin accounts from entitlement
checks. You can use the same flag to gate an admin area you build yourself later.
Next
See how billing and entitlement fit together: How Billing Works.