Module: Seams::Permissions
- Defined in:
- lib/seams/permissions.rb
Overview
Permissions module — public API for authorisation across engines.
Engines register the ability codes they understand at boot through Seams::PermissionRegistry, mirroring the event bus. Authorisation is resolved through Seams::Permissions.can?, which answers from a code-defined role hierarchy and a host-configurable grant map. Nothing here enforces permissions on a request; it is the machinery that the request and controller layers build on in later phases.
Defined Under Namespace
Classes: DuplicateAbilityError, Error, InvalidAbilityNameError, UnregisteredAbilityError
Constant Summary collapse
- NAME_PATTERN =
Three dot-separated segments, lowercase, snake_case allowed. Matches the event name convention so abilities and events read alike.
/\A[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*\z/- ROLE_HIERARCHY =
Roles ordered from most to least privileged. Each role inherits every ability granted to the roles below it (owner ⊇ admin ⊇ member).
%w[owner admin member].freeze
- SYSTEM_ROLE =
A pseudo-role that bypasses every check — used by trusted internal callers such as background jobs and system actors.
"system"- DEFAULT_GRANTS =
Sensible default grant map so
can?allows things out of the box, keyed by role.membermay read;adminmay manage.ownerinherits everything granted toadmin(and through itmember) via ROLE_HIERARCHY, andsystembypasses checks entirely — so neither needs its own entry here. Hosts override the whole map throughSeams.configuration.permission_grants. The codes mirror what each canonical engine registers from its engine.rb. { "member" => %w[ identity.read.auth invoice.read.billing subscription.read.billing team.read.teams account.read.accounts membership.read.accounts notification.read.notifications ].freeze, "admin" => %w[ identity.manage.auth invoice.manage.billing subscription.manage.billing plan.manage.billing lifetime.manage.billing team.manage.teams member.manage.teams invitation.manage.teams account.manage.accounts membership.manage.accounts notification.manage.notifications preference.manage.notifications ].freeze }.freeze
Class Method Summary collapse
- .assert_valid_name!(name) ⇒ Object
-
.can?(role:, ability:) ⇒ Boolean
Resolves whether a role is allowed an ability.
-
.granted_abilities_for(role) ⇒ Object
Abilities granted directly to a single role in the configured map, tolerating string or symbol keys and values.
-
.roles_for(role) ⇒ Object
The role itself plus every role it inherits from, lowest first.
- .valid_name?(name) ⇒ Boolean
Class Method Details
.assert_valid_name!(name) ⇒ Object
75 76 77 78 79 80 81 |
# File 'lib/seams/permissions.rb', line 75 def self.assert_valid_name!(name) return if valid_name?(name) raise InvalidAbilityNameError, "Ability name #{name.inspect} must follow resource.action.engine " \ "(e.g. invoice.read.billing)" end |
.can?(role:, ability:) ⇒ Boolean
Resolves whether a role is allowed an ability. The role string is one of owner/admin/member/system; request-layer bypasses (e.g. staff?) are handled by callers, not here. Raises if the ability was never registered, matching the event bus's loud-failure behaviour.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
# File 'lib/seams/permissions.rb', line 87 def self.can?(role:, ability:) ability = ability.to_s unless PermissionRegistry.registered?(ability) raise UnregisteredAbilityError, "Ability #{ability.inspect} is not registered; register it with " \ "Seams::PermissionRegistry.register before checking it" end role = role.to_s return true if role == SYSTEM_ROLE roles_for(role).any? { |r| granted_abilities_for(r).include?(ability) } end |
.granted_abilities_for(role) ⇒ Object
Abilities granted directly to a single role in the configured map, tolerating string or symbol keys and values.
114 115 116 117 118 |
# File 'lib/seams/permissions.rb', line 114 def self.granted_abilities_for(role) grants = Seams.configuration. list = grants[role.to_s] || grants[role.to_sym] Array(list).map(&:to_s) end |
.roles_for(role) ⇒ Object
The role itself plus every role it inherits from, lowest first. An unknown role resolves to just itself (and therefore no grants).
104 105 106 107 108 109 110 |
# File 'lib/seams/permissions.rb', line 104 def self.roles_for(role) role = role.to_s index = ROLE_HIERARCHY.index(role) return [role] unless index ROLE_HIERARCHY[index..] end |
.valid_name?(name) ⇒ Boolean
71 72 73 |
# File 'lib/seams/permissions.rb', line 71 def self.valid_name?(name) NAME_PATTERN.match?(name.to_s) end |