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 projectSo {: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{}.
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
@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.
@type error() :: :no_such_project | :invalid_id | :no_standing
@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{}.
@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
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.
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.
@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.
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.
No project means no MFA requirement to meet, so mfa_satisfied? is true.
Whether the caller acts for the instance on every project.
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.