The approval queue of API write operations (the approval path of the confirmation flow):
every write call of a key marked "requires approval", dangerous or not, waits here for a
person. A read or a dry_run of such a key is never held.
Life of an approval
pending -> approved -> executing -> executed | failed
pending -> rejected | stale | expiredrequest/2 creates the pending row (the danger gate calls it with the preview the
handler produced in dry-run mode). An agent follows its approval with
system.get_approval (get_for_key/2 and status_view/1).
Rules
- An identical pending request (same key, tool, arguments) returns the existing approval.
- A key holds at most
api_approval_pending_maxpending approvals; one more is{:error, :pending_cap}. Both rules run under a per-key advisory lock. inputis the arguments the approval will run with; at most 1 MB; cleared when the approval is decided or expires.- Who may decide: an Owner, or a person who holds
api, created the key and holds the tool'spermission(may_decide?/2). - An approved operation is run once: only an
approvedrow starts, and it moves toexecutingfirst. If it runs for more than 15 minutes without callingctx.progress, the stuck-operation sweep marks itfailed(interrupted) although the process is still working; when the handler then succeeds, the row is corrected toexecuted(with anote). Until then, and if the process really died, the row staysfailedand part of the operation may have been applied: an agent must check the data and not queue the operation again before it has.
Changes are announced on the PubSub topic "phoenix_kit:api:approvals" as
{:api_approval_changed, approval_uuid}, which the approvals page listens to.
Summary
Functions
Approves approval_id and runs it now, in the calling process: the decision, the checks
of the request path repeated against the key as it stands, the values fingerprint, then
the operation. On {:ok, row} the row is final: executed, failed or stale.
The active people who may decide approval: the Owners and the key's creator, if they qualify.
The approval approval_id if it belongs to the key key_uuid, else nil (an unknown
id, a malformed id, another key's approval and a confirmation code all read the same).
A pending approval past its time reads as expired.
Whether the site uses approvals at all: a key that still works (Keys.dead_reason/1 is
nil: a revocation or expiry in the future has not been reached) is marked "requires
approval", or an approval of any status is still kept (a pending one outlives the flag
being switched off, and the decided ones are the page's history). The admin menu offers
the Approvals entry only then; the page itself answers either way.
The pending approvals scope may decide, oldest first, with their keys.
The latest decided approvals scope may see, newest change first.
Whether the person behind scope may approve or reject approval: an Owner, or a
holder of api who created the key and holds the tool's permission.
How many pending approvals scope may decide: what list_pending/2 would list, counted
without reading the rows (the menu draws it on every render). An Owner's is one count;
anyone else's is a count per tool of the approvals of their keys, kept for the tools they
hold the permission of.
Rejects approval_id: nothing runs and its input is discarded.
Queues an approval. args is %{actor, tool, input, input_hash, preview, fingerprint, context}: the key's actor, the tool name, the validated arguments without the control
fields, their canonical hash, the preview and fingerprint of the dry run, and the
request context (language currency hosts base_url request_id) the operation will
run with.
The JSON-able view system.get_approval answers: id, status, tool, preview, times,
and by status the result, or the error and the partial results. Never the input.
The PubSub topic of approval changes.
Functions
@spec approve(term(), PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) :: {:ok, PhoenixKit.API.Confirm.Confirmation.t()} | {:error, :not_found | :forbidden | :not_pending | :expired}
Approves approval_id and runs it now, in the calling process: the decision, the checks
of the request path repeated against the key as it stands, the values fingerprint, then
the operation. On {:ok, row} the row is final: executed, failed or stale.
@spec deciders(PhoenixKit.API.Confirm.Confirmation.t()) :: [ PhoenixKit.Users.Auth.User.t() ]
The active people who may decide approval: the Owners and the key's creator, if they qualify.
The approval approval_id if it belongs to the key key_uuid, else nil (an unknown
id, a malformed id, another key's approval and a confirmation code all read the same).
A pending approval past its time reads as expired.
@spec in_use?() :: boolean()
Whether the site uses approvals at all: a key that still works (Keys.dead_reason/1 is
nil: a revocation or expiry in the future has not been reached) is marked "requires
approval", or an approval of any status is still kept (a pending one outlives the flag
being switched off, and the decided ones are the page's history). The admin menu offers
the Approvals entry only then; the page itself answers either way.
@spec list_pending(PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) :: [ %{ approval: PhoenixKit.API.Confirm.Confirmation.t(), key: PhoenixKit.API.Keys.Key.t() } ]
The pending approvals scope may decide, oldest first, with their keys.
@spec list_recent(PhoenixKit.Users.Auth.Scope.t(), pos_integer()) :: [ %{ approval: PhoenixKit.API.Confirm.Confirmation.t(), key: PhoenixKit.API.Keys.Key.t() } ]
The latest decided approvals scope may see, newest change first.
@spec may_decide?( PhoenixKit.Users.Auth.Scope.t(), PhoenixKit.API.Confirm.Confirmation.t() ) :: boolean()
Whether the person behind scope may approve or reject approval: an Owner, or a
holder of api who created the key and holds the tool's permission.
@spec pending_count(PhoenixKit.Users.Auth.Scope.t()) :: non_neg_integer()
How many pending approvals scope may decide: what list_pending/2 would list, counted
without reading the rows (the menu draws it on every render). An Owner's is one count;
anyone else's is a count per tool of the approvals of their keys, kept for the tools they
hold the permission of.
@spec reject(term(), PhoenixKit.Users.Auth.Scope.t(), DateTime.t()) :: {:ok, PhoenixKit.API.Confirm.Confirmation.t()} | {:error, :not_found | :forbidden | :not_pending | :expired}
Rejects approval_id: nothing runs and its input is discarded.
@spec request(map(), DateTime.t()) :: {:ok, PhoenixKit.API.Confirm.Confirmation.t(), :created | :existing} | {:error, :pending_cap | :input_too_large | Ecto.Changeset.t()}
Queues an approval. args is %{actor, tool, input, input_hash, preview, fingerprint, context}: the key's actor, the tool name, the validated arguments without the control
fields, their canonical hash, the preview and fingerprint of the dry run, and the
request context (language currency hosts base_url request_id) the operation will
run with.
@spec status_view(PhoenixKit.API.Confirm.Confirmation.t()) :: map()
The JSON-able view system.get_approval answers: id, status, tool, preview, times,
and by status the result, or the error and the partial results. Never the input.
@spec topic() :: String.t()
The PubSub topic of approval changes.