# `Lightning.Policies.ProjectUsers`
[🔗](https://github.com/OpenFn/lightning/blob/main/lib/lightning/policies/project_users.ex#L1)

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.

# `actions`

```elixir
@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
```

# `actions`

```elixir
@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.

# `authorize`

```elixir
@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 `t:Lightning.Scope.subject/0`.

Given a `%Scope{}`, the subject must be the scope's own project or one of
its membership rows.

# `blocked_by_mfa?`

```elixir
@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".

# `permitted?`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
