Lightning.Scope (Lightning v2.20.1)

Copy Markdown View Source

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.

Summary

Types

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

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()

Functions

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

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

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.

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.

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.

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.

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.

The facts about the caller alone, with no project.

Whether the caller acts for the instance on every project.

Whether the actor holds one of roles in the project.

Types

actor()

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

error()

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

subject()

@type subject() ::
  Lightning.Projects.Project.t()
  | Ecto.UUID.t()
  | %{:project_id => Ecto.UUID.t(), optional(any()) => 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()

@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()
}

Functions

allows?(scope, grant)

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

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

describes?(scope, subject)

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

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

fetch(scope, subject)

@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(scope, project_ids)

@spec fetch_all(t() | actor(), [Ecto.UUID.t()]) :: %{required(Ecto.UUID.t()) => 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?(scope)

@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?(scope, roles)

@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?(scope)

@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(user)

@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?(scope)

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

Whether the caller acts for the instance on every project.

role_in?(actor, subject, roles)

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