Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
164 lines
5.6 KiB
Elixir
164 lines
5.6 KiB
Elixir
defmodule Tarakan.Accounts.Scope do
|
|
@moduledoc """
|
|
An authorization snapshot for the caller behind a request.
|
|
|
|
The account remains available for compatibility with Phoenix's generated
|
|
authentication code. Authorization code should prefer the copied standing,
|
|
role, trust, repository relationship, and credential fields on the scope.
|
|
"""
|
|
|
|
alias Tarakan.Accounts.Account
|
|
|
|
defstruct account: nil,
|
|
account_id: nil,
|
|
account_state: nil,
|
|
platform_role: nil,
|
|
trust_tier: nil,
|
|
repository_memberships: %{},
|
|
token_id: nil,
|
|
token_scopes: nil,
|
|
token_repository_id: nil,
|
|
authentication_method: nil
|
|
|
|
@type t :: %__MODULE__{
|
|
account: Account.t() | nil,
|
|
account_id: integer() | nil,
|
|
account_state: String.t() | nil,
|
|
platform_role: String.t() | nil,
|
|
trust_tier: String.t() | nil,
|
|
repository_memberships: %{optional(integer()) => map()},
|
|
token_id: integer() | nil,
|
|
token_scopes: MapSet.t(String.t()) | nil,
|
|
token_repository_id: integer() | nil,
|
|
authentication_method: atom() | String.t() | nil
|
|
}
|
|
|
|
@doc """
|
|
Creates a scope for an account.
|
|
|
|
`token_scopes: nil` represents a browser session or trusted system scope.
|
|
API credential scopes fail closed when the grant list is absent. Supplying an
|
|
empty list represents a credential with no grants.
|
|
Repository memberships may be a list of membership structs or a map keyed
|
|
by repository id.
|
|
|
|
Returns nil if no account is given.
|
|
"""
|
|
def for_account(account, opts \\ [])
|
|
|
|
def for_account(%Account{} = account, opts) do
|
|
%__MODULE__{
|
|
account: account,
|
|
account_id: account.id,
|
|
account_state: account.state,
|
|
platform_role: account.platform_role,
|
|
trust_tier: account.trust_tier,
|
|
repository_memberships:
|
|
opts |> option(:repository_memberships, []) |> normalize_memberships(),
|
|
token_id: option(opts, :token_id),
|
|
token_scopes: opts |> option(:token_scopes) |> normalize_token_scopes(),
|
|
token_repository_id: option(opts, :token_repository_id),
|
|
authentication_method: option(opts, :authentication_method, :session)
|
|
}
|
|
end
|
|
|
|
def for_account(nil, _opts), do: nil
|
|
|
|
@doc "Creates an explicit scope for trusted background maintenance."
|
|
def for_system(opts \\ []) do
|
|
%__MODULE__{
|
|
account_state: "active",
|
|
platform_role: "system",
|
|
trust_tier: "reviewer",
|
|
token_scopes: nil,
|
|
token_repository_id: option(opts, :token_repository_id),
|
|
authentication_method: :system
|
|
}
|
|
end
|
|
|
|
@doc "Adds repository relationships to an existing scope."
|
|
def put_repository_memberships(%__MODULE__{} = scope, memberships) do
|
|
%{scope | repository_memberships: normalize_memberships(memberships)}
|
|
end
|
|
|
|
@doc "Returns the relationship snapshot for a repository, if one exists."
|
|
def repository_membership(%__MODULE__{} = scope, repository_or_id) do
|
|
with repository_id when is_integer(repository_id) <- repository_id(repository_or_id) do
|
|
Map.get(scope.repository_memberships, repository_id)
|
|
else
|
|
_other -> nil
|
|
end
|
|
end
|
|
|
|
def repository_membership(_scope, _repository_or_id), do: nil
|
|
|
|
@doc "Whether the caller has a verified repository role."
|
|
def repository_role?(scope, repository_or_id, roles) do
|
|
roles = List.wrap(roles) |> Enum.map(&to_string/1)
|
|
|
|
case repository_membership(scope, repository_or_id) do
|
|
%{status: "verified", role: role} -> role in roles
|
|
%{"status" => "verified", "role" => role} -> role in roles
|
|
_other -> false
|
|
end
|
|
end
|
|
|
|
@doc "Whether an explicitly scoped credential carries at least one grant."
|
|
def token_scope?(
|
|
%__MODULE__{token_scopes: nil, authentication_method: method},
|
|
_required
|
|
)
|
|
when method in [:session, :system, :ssh_key, "session", "system", "ssh_key"],
|
|
do: true
|
|
|
|
def token_scope?(%__MODULE__{token_scopes: nil}, _required), do: false
|
|
|
|
def token_scope?(%__MODULE__{token_scopes: scopes}, required) do
|
|
required = List.wrap(required) |> Enum.map(&to_string/1)
|
|
Enum.any?(required, &MapSet.member?(scopes, &1))
|
|
end
|
|
|
|
def token_scope?(_scope, _required), do: false
|
|
|
|
defp normalize_memberships(memberships)
|
|
when is_map(memberships) and not is_struct(memberships) do
|
|
memberships
|
|
end
|
|
|
|
defp normalize_memberships(memberships) do
|
|
memberships
|
|
|> List.wrap()
|
|
|> Enum.reduce(%{}, fn membership, acc ->
|
|
case membership_repository_id(membership) do
|
|
repository_id when is_integer(repository_id) -> Map.put(acc, repository_id, membership)
|
|
_other -> acc
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp normalize_token_scopes(nil), do: nil
|
|
defp normalize_token_scopes(%MapSet{} = scopes), do: scopes
|
|
|
|
defp normalize_token_scopes(scopes) do
|
|
scopes
|
|
|> List.wrap()
|
|
|> Enum.map(&to_string/1)
|
|
|> MapSet.new()
|
|
end
|
|
|
|
defp membership_repository_id(%{repository_id: repository_id}), do: repository_id
|
|
defp membership_repository_id(%{"repository_id" => repository_id}), do: repository_id
|
|
defp membership_repository_id(_membership), do: nil
|
|
|
|
defp repository_id(repository_id) when is_integer(repository_id), do: repository_id
|
|
defp repository_id(%{id: repository_id}) when is_integer(repository_id), do: repository_id
|
|
|
|
defp repository_id(%{repository_id: repository_id}) when is_integer(repository_id),
|
|
do: repository_id
|
|
|
|
defp repository_id(_repository), do: nil
|
|
|
|
defp option(opts, key, default \\ nil)
|
|
defp option(opts, key, default) when is_list(opts), do: Keyword.get(opts, key, default)
|
|
defp option(opts, key, default) when is_map(opts), do: Map.get(opts, key, default)
|
|
end
|