Module: Rhino::HasPermissions
- Extended by:
- ActiveSupport::Concern
- Defined in:
- lib/rhino/concerns/has_permissions.rb
Overview
Permission checking concern for the User model. Mirrors the Laravel HasPermissions trait.
Usage:
class User < ApplicationRecord
include Rhino::HasPermissions
has_many :user_roles
end
Permission format: ‘slug.action’ (e.g., ‘posts.index’, ‘blogs.store’) Wildcard support on every layer:
- '*' grants/denies everything
- 'posts.*' grants/denies all actions on posts
── Layered resolution (organization context) ────────────────────────────The effective decision for an org-scoped check is:
effective = (role ∪ granted) − denied (deny always wins)
- role → org_role_permissions[(organization, role)].permissions
The shared "role layer" an org manages once per role.
- granted → user_roles.granted_permissions (per-user additive delta)
- denied → user_roles.denied_permissions (per-user subtractive delta)
- legacy → user_roles.permissions (kept in the allow set)
Deny is checked first and overrides everything — even a role ‘*’. This is intentionally deny-overrides (not most-specific-wins).
Backward compatibility:
- The global roles.permissions column is preserved as a FALLBACK: it is
consulted only when the primary union (legacy ∪ granted ∪ org role layer)
is empty — exactly the pre-layer "fall back to role.permissions when
user_role.permissions is empty" behavior.
- When org_role_permissions has no row and the granted/denied columns are
absent, resolution reduces to the previous behavior byte-for-byte.
Sources:
1. organization provided (tenant route group) → user_roles layers above.
2. no organization (non-tenant route group) → users.permissions (with
optional user-level granted/denied if those columns exist; deny wins).
Instance Method Summary collapse
-
#explain_permission(permission, organization = nil, route_group: nil) ⇒ Hash
Explain a permission decision — returns the deciding layer.
-
#has_permission?(permission, organization = nil, route_group: nil) ⇒ Boolean
Check if the user has a specific permission.
-
#role_slug_for_validation(organization = nil) ⇒ String?
Get the role slug for validation purposes.
Instance Method Details
#explain_permission(permission, organization = nil, route_group: nil) ⇒ Hash
Explain a permission decision — returns the deciding layer.
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/rhino/concerns/has_permissions.rb', line 83 def (, organization = nil, route_group: nil) return { granted: false, reason: "default-deny" } if .blank? if group_membership_enforced? membership = Rhino::GroupMembership.matching_membership(self, route_group, organization) return decide_for_record(, membership, organization, explain: true) end if organization return decide_for_record(, find_user_role(organization), organization, explain: true) end deny = (safe_attr(self, :denied_permissions)) return { granted: false, reason: "denied" } if (, deny) user = (safe_attr(self, :permissions)) granted = (safe_attr(self, :granted_permissions)) return { granted: true, reason: "granted" } if (, granted) return { granted: true, reason: "user" } if (, user) { granted: false, reason: "default-deny" } end |
#has_permission?(permission, organization = nil, route_group: nil) ⇒ Boolean
Check if the user has a specific permission.
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/rhino/concerns/has_permissions.rb', line 54 def (, organization = nil, route_group: nil) return false if .blank? # Group-aware permission resolution (GROUP_AUTH_DESIGN.md §6). Only active # when enforce_group_membership is on. Permissions then resolve from the # membership row matching (route_group, organization). if group_membership_enforced? membership = Rhino::GroupMembership.matching_membership(self, route_group, organization) return decide_for_record(, membership, organization) end if organization # Tenant route group: layered resolution from the user_role for this org. return decide_for_record(, find_user_role(organization), organization) end # Non-tenant route group: users.permissions (+ optional user-level deltas). deny = (safe_attr(self, :denied_permissions)) return false if (, deny) allow = (safe_attr(self, :permissions)) + (safe_attr(self, :granted_permissions)) (, allow) end |
#role_slug_for_validation(organization = nil) ⇒ String?
Get the role slug for validation purposes.
110 111 112 113 114 115 116 117 118 |
# File 'lib/rhino/concerns/has_permissions.rb', line 110 def role_slug_for_validation(organization = nil) user_role = find_user_role(organization) return nil unless user_role role = user_role.respond_to?(:role) ? user_role.role : nil return nil unless role role.respond_to?(:slug) ? role.slug : nil end |