The Bodyguard Policy module for project members' roles.
Two steps, deliberately separate.
Lightning.Scope establishes the facts: which project, and what
standing this actor has in it, including whether the project is scheduled
for deletion.
authorize/3 on a %Scope{} makes the judgement, in the order
Lightning.Policies.Decision sets. It never mentions
Project.scheduled_deletion or Project.requires_mfa. Both reach it as
facts on the scope.
Adding an action means adding an atom to @admin_actions or
@editor_actions. It inherits the guards; there is nothing to remember.
We deny by default: an action in none of the lists is refused.
Summary
Functions
Every action this policy decides. Read by the recurrence test, so a new action is covered the day it is added rather than the day someone remembers.
Whether the caller may perform action, where the subject is anything that
identifies a project — see Lightning.Scope.subject/0.
Whether this actor would have standing on the project if not for the project's MFA requirement.
Decide an action against an already-resolved %Scope{}, as a boolean.
Types
@type actions() ::
:access_project
| :list_project_credentials
| :delete_project
| :publish_template
| :run_workflow
| :edit_workflow
| :delete_workflow
| :create_workflow
| :create_project_credential
| :initiate_github_sync
| :create_channel
| :delete_channel
| :update_channel
| :edit_project
| :edit_data_retention
| :edit_run_settings
| :add_project_user
| :remove_project_user
| :write_webhook_auth_method
| :write_github_connection
| :edit_digest_alerts
| :edit_failure_alerts
Functions
@spec actions() :: [actions()]
Every action this policy decides. Read by the recurrence test, so a new action is covered the day it is added rather than the day someone remembers.
@spec authorize( actions(), Lightning.Scope.t() | Lightning.Scope.actor(), Lightning.Scope.subject() ) :: Lightning.Policies.Decision.answer()
Whether the caller may perform action, where the subject is anything that
identifies a project — see Lightning.Scope.subject/0.
Given a %Scope{}, the subject must be the scope's own project or one of
its membership rows.
@spec blocked_by_mfa?(Lightning.Scope.t()) :: boolean()
Whether this actor would have standing on the project if not for the project's MFA requirement.
Lets a caller distinguish "no access at all" from "access except MFA" —
LightningWeb.Hooks sends the first to not-found and the second to
/mfa_required — without duplicating what counts as standing. It cannot ask
permitted?(:access_project, ...) for that, since that answers false for
both cases. A project scheduled for deletion is not-found for everyone, so
it is never "blocked by MFA".
@spec permitted?(actions(), Lightning.Scope.t()) :: boolean()
Decide an action against an already-resolved %Scope{}, as a boolean.
Public so a caller needing several answers about one project resolves once
rather than once per question. It is false when the scope lacks the
action's grant, and on a project scheduled for deletion.