defmodule GitGud.Packages.Access do
@moduledoc """
Who may do what to a repository's packages and container images.
One decision for both registries. The OCI token endpoint
(`GitGudWeb.RegistryController`) and the package registries
(`GitGud.Packages.Auth`) used to answer this separately, and they
disagreed: the container registry let anonymous callers pull
*internal* images and ignored org admins and teams, while the package
registry ignored what a personal token was scoped to. Both now ask
here, and a user's access goes through `GitGud.Organizations.can?/3` —
the same rule the web UI applies.
## Principals
* `{:user, user, scopes}` — a forge account. `scopes` is `:all` for a
password (or a signed-in browser session) and the token's scopes for
a personal access token (`ggp_…`), which limit what the account's
own permission would otherwise allow.
* `{:token, repository_id, scopes}` — a repo-scoped registry token
(`ggrt_…`), `pull` and/or `push`.
* `{:job, repository_id, perm}` — a running CI job's
`${{ github.token }}`, with the `perm` claim its workflow's
`permissions:` gave it (`GitGud.Workflows.Permissions`; nil for a
token minted before tokens carried one).
* `:anonymous`
## Actions
`:read` (pull, download), `:write` (push, publish) and `:delete`
(removing image manifests, which only repository admins may do).
## Personal token scopes
Reading wants `packages:read`, `packages:write`, `repo` or `repo:read`;
writing wants `packages:write` or `repo`; deleting wants `repo`. `repo`
predates the finer scopes and keeps meaning everything.
"""
alias GitGud.Accounts.User
alias GitGud.Organizations
alias GitGud.Repositories
alias GitGud.Repositories.Repository
alias GitGud.Workflows.JobTokenGrants
alias GitGud.Workflows.Permissions
@type principal ::
{:user, User.t(), :all | [String.t()]}
| {:token, integer(), [String.t()]}
| {:job, integer(), map() | nil}
| :anonymous
@type action :: :read | :write | :delete
@read_scopes ~w(packages:read packages:write repo repo:read)
@write_scopes ~w(packages:write repo)
@delete_scopes ~w(repo)
@doc "May `principal` perform `action` on `repo`'s packages and images?"
@spec allowed?(principal(), Repository.t(), action()) :: boolean()
def allowed?({:token, repo_id, scopes}, %Repository{id: repo_id}, action) do
case action do
:read -> "pull" in scopes
:write -> "push" in scopes
:delete -> false
end
end
# A registry token minted for a different repository is worth no more
# than an anonymous request.
def allowed?({:token, _other, _scopes}, %Repository{} = repo, action),
do: allowed?(:anonymous, repo, action)
# A job token carries the forge's own judgement that the job belongs to
# its repository, so it reads and writes that repository's packages with
# no stored credential — as far as its `packages:` permission goes.
# Elsewhere it reads only what the other repository has granted it, and
# only with `packages: read`; what anyone may read it reads regardless.
def allowed?({:job, repo_id, perm}, %Repository{id: repo_id} = repo, action)
when action in [:read, :write] do
Permissions.permits?(perm, "packages", action) or
(action == :read and allowed?(:anonymous, repo, :read))
end
def allowed?({:job, source_id, perm}, %Repository{} = repo, :read) do
(Permissions.permits?(perm, "packages", :read) and
JobTokenGrants.allows?(repo, source_id, :read)) or allowed?(:anonymous, repo, :read)
end
def allowed?({:job, _source_id, _perm}, %Repository{}, _action), do: false
def allowed?({:user, %User{} = user, scopes}, %Repository{} = repo, action) do
scope_permits?(scopes, action) and can?(user, repo, permission(action))
end
def allowed?(:anonymous, %Repository{} = repo, :read), do: can?(nil, repo, :read)
def allowed?(:anonymous, %Repository{}, _action), do: false
# `Organizations.can?/3` answers nil for "no permission at all".
defp can?(user, repo, permission), do: Organizations.can?(user, repo, permission) == true
defp permission(:read), do: :read
defp permission(:write), do: :write
defp permission(:delete), do: :admin
defp scope_permits?(:all, _action), do: true
defp scope_permits?(scopes, :read), do: Enum.any?(scopes, &(&1 in @read_scopes))
defp scope_permits?(scopes, :write), do: Enum.any?(scopes, &(&1 in @write_scopes))
defp scope_permits?(scopes, :delete), do: Enum.any?(scopes, &(&1 in @delete_scopes))
@doc """
The repository a registry image name belongs to.
An image is named for its repository, `owner/name`, or for something
under it — `owner/name/server`, `owner/name/web/assets` — so a
repository can publish several images, as on GitLab. Whatever applies
to the repository applies to every image beneath it.
"""
@spec repository_for_image(String.t()) :: {:ok, Repository.t()} | :error
def repository_for_image(image) when is_binary(image) do
case String.split(image, "/") do
[owner, name | rest] when owner != "" and name != "" ->
if Enum.all?(rest, &(&1 != "")) do
try do
{:ok, Repositories.get_repository_by_path!(owner, name)}
rescue
Ecto.NoResultsError -> :error
end
else
:error
end
_ ->
:error
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
5.3 KiB · text
History
b868907