# `Lightning.Scope`
[🔗](https://github.com/OpenFn/lightning/blob/main/lib/lightning/scope.ex#L1)

The facts about whoever is calling, and their standing in a project right now.
Policies decide from these facts. A Scope neither authenticates the caller nor
decides what it may do.

`new/1` gives the facts about the caller alone: the `grants` its token
carries (`:all` for a person), its `instance_role`, MFA status, and whether it
can own a credential (only a person can). `fetch/2` adds one project's facts,
including whether it is scheduled for deletion, and `fetch_all/2` does the
same for several projects in one query. On the API,
`LightningWeb.Plugs.AuthorizeAction` builds the first and
`LightningWeb.API.Target` the second.

    {:ok, %Scope{role: :editor}}
    {:ok, %Scope{role: nil}}                      no membership row
    {:ok, %Scope{scheduled_for_deletion?: true}}  scheduled for deletion
    {:error, :no_such_project}                    no such project, to an
                                                  operator or a superuser
    {:error, :invalid_id}                         an id that isn't a UUID, to
                                                  an operator or a superuser
    {:error, :no_standing}                        no such project or an id
                                                  that isn't a UUID, to anyone
                                                  else, or a repo connection
                                                  naming another project

So `{:ok, _}` is not permission. A non-member of a live project gets
`role: nil`, which support access can still make meaningful. Whether the
facts are enough for an action is the policy's judgement, through
`Lightning.Policies.Decision`.

The one refusal `fetch/2` makes itself is for a `%ProjectRepoConnection{}`.
It has no membership row, so its standing is which project it belongs to, and
its `role` is always `nil`. Ownership is checked before anything else is read,
so a connection learns nothing about a project that is not its own, and hears
the same `:no_standing` a policy gives a user with no membership row.

A `%ServiceAccount{}` has no membership row either and is not tied to one
project, so its `role` is always `nil`. Its `instance_role` is `:operator`,
which `holds_role?/2` treats as holding every project role.

Two support fields, deliberately. `support_user?` is the account flag.
`support?` also requires the project's `allow_support_access`. Anything
granting access to a customer's project wants `support?`.

Scheduling deletion removes no membership rows and revokes no token, so
during the purge window only the policies' check of `scheduled_for_deletion?`
keeps members out. Policies read `scheduled_for_deletion?` and
`mfa_satisfied?` here, never `Project.scheduled_deletion`, `requires_mfa` or
`mfa_enabled`. The MFA rule is written once, privately, in this module. The
exception is `Lightning.Policies.Sandboxes` `:cancel_scheduled_deletion`,
which reads its subject's role directly, since its subject is scheduled by
definition.

# `actor`

```elixir
@type actor() ::
  Lightning.Actor.t() | Lightning.VersionControl.ProjectRepoConnection.t()
```

Whoever is asking. A `%User{}` may hold a role; a `%ProjectRepoConnection{}`
and a `%ServiceAccount{}` never do.

# `error`

```elixir
@type error() :: :no_such_project | :invalid_id | :no_standing
```

# `subject`

```elixir
@type subject() ::
  Lightning.Projects.Project.t()
  | Ecto.UUID.t()
  | %{:project_id =&gt; Ecto.UUID.t(), optional(any()) =&gt; any()}
  | nil
```

Anything that identifies a project: a loaded `%Project{}`, a project id, or
any struct or map carrying a `:project_id` — a `%Dataclip{}`, a
`%Collection{}`, a `%Projects.File{}`, a `%ProjectUser{}`.

# `t`

```elixir
@type t() :: %Lightning.Scope{
  actor: actor(),
  can_own_credentials?: boolean(),
  grants: :all | MapSet.t(String.t()),
  instance_role: :superuser | :operator | nil,
  mfa_satisfied?: boolean(),
  project: Lightning.Projects.Project.t() | nil,
  project_user: Lightning.Projects.ProjectUser.t() | nil,
  role: :owner | :admin | :editor | :viewer | nil,
  scheduled_for_deletion?: boolean(),
  support?: boolean(),
  support_user?: boolean()
}
```

# `allows?`

```elixir
@spec allows?(t(), String.t()) :: boolean()
```

Whether the caller's token carries `grant`. A person holds every grant.

# `describes?`

```elixir
@spec describes?(t(), term()) :: boolean()
```

Whether `subject` names this scope's project: the project itself, its id, or
anything carrying its `project_id`.

# `fetch`

```elixir
@spec fetch(t() | actor(), subject()) :: {:ok, t()} | {:error, error()}
```

The actor's standing in the project. Given a `%Scope{}` from `new/1`, adds
the project's facts to it rather than building it again.

When the subject does not identify a project that exists, a caller with
`instance_wide?/1` access gets `{:error, :invalid_id}` for an id that isn't a
UUID, or a value that isn't an id at all, and `{:error, :no_such_project}`
for any other. Anyone else gets `{:error, :no_standing}`, the same answer as
for a project they can't see.

Returns `{:error, :no_standing}` when a `%ProjectRepoConnection{}` actor names
a project other than the connection's own. A project scheduled for deletion
resolves, with `scheduled_for_deletion?` set; refusing it is the policy's
call.

A `%ServiceAccount{}` resolves any project, with no role.

# `fetch_all`

```elixir
@spec fetch_all(t() | actor(), [Ecto.UUID.t()]) :: %{required(Ecto.UUID.t()) =&gt; t()}
```

The actor's standing in each of several projects, read in one query, as a
map from project id to the scope `fetch/2` would give.

An id that is malformed, names no project, or names a project a
`%ProjectRepoConnection{}` actor does not belong to is absent from the map.

# `has_standing?`

```elixir
@spec has_standing?(t()) :: boolean()
```

Whether the caller has any standing in the scope's project at all: a role,
or support access. This is the one definition of standing, so reading a
project and being blocked by MFA can't disagree about it.

# `holds_role?`

```elixir
@spec holds_role?(t(), [atom()]) :: boolean()
```

Whether the caller stands in one of `roles` in the scope's project. An
operator stands in every role. Standing only: it does not consult MFA or
whether the project is being deleted.

# `instance_wide?`

```elixir
@spec instance_wide?(t()) :: boolean()
```

Whether the caller may learn which projects exist: an operator or a
superuser. Anyone else is told nothing about a project they have no standing
on, including that it doesn't exist.

# `new`

```elixir
@spec new(actor()) :: t()
```

The facts about the caller alone, with no project.

No project means no MFA requirement to meet, so `mfa_satisfied?` is `true`.

# `operator?`

```elixir
@spec operator?(t()) :: boolean()
```

Whether the caller acts for the instance on every project.

# `role_in?`

```elixir
@spec role_in?(actor(), subject(), [atom()]) :: boolean()
```

Whether the actor holds one of `roles` in the project.

`false` for a project that does not exist or is scheduled for deletion,
`false` for a user with no membership row, and `false` for a user who has
not met the project's MFA requirement. It never consults `support?`, so a
rule that admits a support user has to decide on the `%Scope{}` itself.

---

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