LightningWeb.API.Target (Lightning v2.20.1)

Copy Markdown View Source

Resolves the project an API action works on, asks the policy whether the caller may act there, and only then looks up the record inside it. It takes the Lightning.Scope that LightningWeb.Plugs.AuthorizeAction assigned, and every refusal it returns is one LightningWeb.API.FallbackController answers.

A caller the policy refuses gets the same 403 whether the record exists, is missing or has an id that isn't a UUID, and only a caller it permits learns which.

Summary

Types

Looks a record up by id inside a project, given the project's id and the record's.

What identifies a project: an id the request sent, as a LightningWeb.API.Request.param/0, or a record loaded from the database, a %Project{} or anything carrying its project_id. A bare id is not accepted, so an id that isn't a UUID is always refused naming where the request sent it.

Functions

The record id_param names in the project subject identifies, once the caller may apply the permission to that project. The id is checked as a UUID only then, and get is given the project's id and the record's.

The record id_param names on a route whose parent is the instance, such as /api/jobs/:id, once the caller may apply the permission to the project the record sits in. locate answers the id of that project, and the record is then looked up in it with get, as in_project/5 does on the nested route.

The project subject identifies, once the caller may apply the permission to it.

The project subject names, as project/3 answers it, or a new, unsaved %Project{} when no project has that id, or subject carries no id, and the caller holds create_permission.

The record param names on a route whose parent is the instance and whose record sits in no project, such as /api/credentials/:id, once the caller may apply the permission to it. get is given the id once it parses as a UUID and answers {:ok, record} or {:error, :not_found}.

Types

get(record)

@type get(record) :: (Ecto.UUID.t(), Ecto.UUID.t() ->
                  {:ok, record} | {:error, :not_found})

Looks a record up by id inside a project, given the project's id and the record's.

permission()

@type permission() :: {policy :: atom(), action :: atom()}

subject()

@type subject() ::
  LightningWeb.API.Request.param()
  | Lightning.Projects.Project.t()
  | %{:project_id => Ecto.UUID.t(), optional(any()) => any()}

What identifies a project: an id the request sent, as a LightningWeb.API.Request.param/0, or a record loaded from the database, a %Project{} or anything carrying its project_id. A bare id is not accepted, so an id that isn't a UUID is always refused naming where the request sent it.

Functions

in_project(scope, subject, permission, id_param, get)

@spec in_project(
  Lightning.Scope.t() | Lightning.Scope.actor(),
  subject(),
  permission(),
  LightningWeb.API.Request.param(),
  get(record)
) :: {:ok, record} | {:error, term()}
when record: term()

The record id_param names in the project subject identifies, once the caller may apply the permission to that project. The id is checked as a UUID only then, and get is given the project's id and the record's.

Target.in_project(scope, Request.param(conn, "project_id"),
  {:project_users, :edit_workflow},
  Request.param(conn, "id"),
  &Workflows.get_workflow_for_project/2
)

in_project_of(scope, id_param, permission, locate, get)

@spec in_project_of(
  Lightning.Scope.t(),
  LightningWeb.API.Request.param(),
  permission(),
  (Ecto.UUID.t() -> {:ok, Ecto.UUID.t()} | {:error, :not_found}),
  get(record)
) :: {:ok, record} | {:error, term()}
when record: term()

The record id_param names on a route whose parent is the instance, such as /api/jobs/:id, once the caller may apply the permission to the project the record sits in. locate answers the id of that project, and the record is then looked up in it with get, as in_project/5 does on the nested route.

Only an operator learns that an id names no record ({:error, :not_found}) or isn't a UUID ({:error, {:invalid_id, location}}). Anyone else gets {:error, :no_standing} for both. A record in a project the caller may not apply the permission to is {:error, :no_standing} or {:error, :forbidden}. All three answer 403 forbidden.

A project deleted between locating it and reading it is {:error, :no_such_project} to a caller who may learn which projects exist, as Lightning.Scope.fetch/2 answers it.

Target.in_project_of(scope, Request.param(conn, "id"),
  {:project_users, :access_project},
  &Jobs.get_project_id/1,
  &Jobs.get_job_for_project/2
)

project(scope, subject, arg)

@spec project(Lightning.Scope.t() | Lightning.Scope.actor(), subject(), permission()) ::
  {:ok, Lightning.Projects.Project.t()} | {:error, term()}

The project subject identifies, once the caller may apply the permission to it.

A project subject doesn't identify is refused as Lightning.Scope.fetch/2 refuses it.

Target.project(conn.assigns.scope, Request.param(conn, "project_id"),
  {:project_users, :access_project}
)

project_or_new(scope, subject, permission, create_permission)

The project subject names, as project/3 answers it, or a new, unsaved %Project{} when no project has that id, or subject carries no id, and the caller holds create_permission.

An id that isn't a UUID is {:error, {:invalid_id, location}} to a caller who holds create_permission. An id held by a project the caller may not apply permission to is that refusal, never a new project.

record(scope, param, arg, get)

@spec record(
  Lightning.Scope.t(),
  LightningWeb.API.Request.param(),
  permission(),
  (Ecto.UUID.t() -> {:ok, record} | {:error, :not_found})
) :: {:ok, record} | {:error, term()}
when record: term()

The record param names on a route whose parent is the instance and whose record sits in no project, such as /api/credentials/:id, once the caller may apply the permission to it. get is given the id once it parses as a UUID and answers {:ok, record} or {:error, :not_found}.

Only a caller who may apply the permission to the instance, asked of the policy with a nil subject, learns that an id names nothing ({:error, :not_found}) or isn't a UUID ({:error, {:invalid_id, location}}). Anyone else gets {:error, :no_standing}, as for a record they can't see.

Target.record(scope, Request.param(conn, "id"),
  {:credentials, :see_credential},
  &fetch_credential/1
)