defmodule GitGud.Workflows.JobTokenGrants do
@moduledoc """
The job-token allowlist: which other repositories' CI jobs a repository
lets read its images and packages, or dispatch its workflows.
See `GitGud.Workflows.JobTokenGrant`. Checked by
`GitGud.Packages.Access` (reads) and the workflow dispatch API.
"""
import Ecto.Query
alias GitGud.Organizations
alias GitGud.Organizations.Organization
alias GitGud.Repo
alias GitGud.Repositories
alias GitGud.Repositories.Repository
alias GitGud.Workflows.JobTokenGrant
@doc """
Does `repo` let jobs of the repository `source_repo_id` do `action`
(`:read` or `:dispatch`)? A repository always may, to itself.
"""
@spec allows?(Repository.t(), integer(), :read | :dispatch) :: boolean()
def allows?(%Repository{id: id}, id, _action), do: true
def allows?(%Repository{id: repo_id}, source_repo_id, action)
when is_integer(source_repo_id) and action in [:read, :dispatch] do
source_org_id =
Repo.one(from r in Repository, where: r.id == ^source_repo_id, select: r.organization_id)
query =
from g in JobTokenGrant,
where: g.repository_id == ^repo_id,
where: field(g, ^action) == true
query =
if source_org_id do
where(
query,
[g],
g.source_repository_id == ^source_repo_id or g.source_organization_id == ^source_org_id
)
else
where(query, [g], g.source_repository_id == ^source_repo_id)
end
Repo.exists?(query)
end
def allows?(_repo, _source, _action), do: false
@doc "The grants on `repo`, sources preloaded, oldest first."
def list(%Repository{id: repo_id}) do
Repo.all(
from g in JobTokenGrant,
where: g.repository_id == ^repo_id,
order_by: [asc: g.inserted_at, asc: g.id],
preload: [source_repository: [:owner, :organization], source_organization: []]
)
end
@doc """
Grant `source` — `"owner/name"` for one repository, or an organization's
handle for all of its repositories — on `repo`. `attrs` takes `read` and
`dispatch` booleans.
"""
def create(%Repository{} = repo, source, attrs \\ %{}) when is_binary(source) do
case resolve_source(String.trim(source)) do
{:ok, source_attrs} ->
%JobTokenGrant{}
|> JobTokenGrant.changeset(
attrs
|> Map.new(fn {k, v} -> {to_string(k), v} end)
|> Map.merge(source_attrs)
|> Map.put("repository_id", repo.id)
)
|> Repo.insert()
:error ->
{:error, :unknown_source}
end
end
defp resolve_source(source) do
case String.split(source, "/") do
[owner, name] when owner != "" and name != "" ->
try do
%Repository{id: id} = Repositories.get_repository_by_path!(owner, name)
{:ok, %{"source_repository_id" => id}}
rescue
Ecto.NoResultsError -> :error
end
[handle] when handle != "" ->
case Organizations.get_organization_by_handle(String.downcase(handle)) do
%Organization{id: id} -> {:ok, %{"source_organization_id" => id}}
_ -> :error
end
_ ->
:error
end
end
@doc "Remove grant `id` from `repo`. Grants on other repositories are left alone."
def delete(%Repository{id: repo_id}, id) do
case Repo.get_by(JobTokenGrant, id: id, repository_id: repo_id) do
nil -> {:error, :not_found}
grant -> Repo.delete(grant)
end
end
@doc "`owner/name` or the organization handle a grant names."
def source_label(%JobTokenGrant{source_repository: %Repository{} = repo}),
do: Repositories.Storage.repo_handle(repo) <> "/" <> repo.name
def source_label(%JobTokenGrant{source_organization: %Organization{handle: handle}}),
do: handle <> " (all repositories)"
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
3.7 KiB · text
History
b868907