defmodule GitGudWeb.JobTokenLive.Index do
@moduledoc """
Repo settings page for the job-token allowlist: which other
repositories' CI jobs may read this repository's images and packages,
or dispatch its workflows. See `GitGud.Workflows.JobTokenGrant`.
"""
use GitGudWeb, :live_view
alias GitGud.Repositories
alias GitGud.Repositories.Storage
alias GitGud.Workflows.JobTokenGrants
@impl true
def mount(%{"owner" => owner, "name" => name}, _session, socket) do
repo =
Repositories.get_repository_by_path!(owner, name)
|> GitGud.Repo.preload([:owner, :organization])
# Repo admins only, like registry tokens: the list says which other
# repositories can read this one's packages or start its workflows.
if GitGudWeb.RepoAdmin.admin?(socket.assigns.current_scope.user, repo) do
{:ok,
socket
|> assign(:repo, repo)
|> assign(:handle, Storage.repo_handle(repo))
|> GitGudWeb.RepoLive.Header.assign_chrome(repo)
|> load()
|> assign(:page_title, "Job token access — #{owner}/#{name}")}
else
{:ok, GitGudWeb.RepoAdmin.deny(socket, repo)}
end
end
# Which repository or organization the grant lets in, as the page
# shows it.
defp grant_label(grant) do
grant
|> GitGud.Repo.preload(source_repository: [:owner, :organization], source_organization: [])
|> JobTokenGrants.source_label()
end
defp load(socket), do: assign(socket, :grants, JobTokenGrants.list(socket.assigns.repo))
@impl true
def handle_event("create_grant", params, socket) do
source = String.trim(params["source"] || "")
attrs = %{
read: params["read"] == "true",
dispatch: params["dispatch"] == "true"
}
cond do
# Asked again, not read from mount: the socket can outlive the role.
not GitGudWeb.RepoAdmin.admin_now?(socket) ->
{:noreply, put_flash(socket, :error, "Not allowed.")}
source == "" ->
{:noreply,
put_flash(socket, :error, "Name a repository (owner/name) or an organization.")}
true ->
repo = socket.assigns.repo
audited =
GitGud.Audit.transact(
socket.assigns.current_scope,
"job_token_grant.created",
fn -> JobTokenGrants.create(repo, source, attrs) end,
fn grant ->
%{
repository: repo,
target_id: grant.id,
target_label: grant_label(grant),
metadata: %{read: grant.read, dispatch: grant.dispatch}
}
end
)
case audited do
{:ok, _grant} ->
{:noreply, socket |> load() |> put_flash(:info, "Granted #{source}.")}
{:error, :unknown_source} ->
{:noreply,
put_flash(socket, :error, "No repository or organization called #{source}.")}
{:error, cs} ->
{:noreply, put_flash(socket, :error, error_message(cs))}
end
end
end
def handle_event("delete_grant", %{"id" => id}, socket) do
if GitGudWeb.RepoAdmin.admin_now?(socket) do
repo = socket.assigns.repo
if id = GitGudWeb.RepoAdmin.parse_id(id) do
GitGud.Audit.transact(
socket.assigns.current_scope,
"job_token_grant.deleted",
fn -> JobTokenGrants.delete(repo, id) end,
fn grant ->
%{repository: repo, target_id: grant.id, target_label: grant_label(grant)}
end
)
end
{:noreply, socket |> load() |> put_flash(:info, "Grant removed.")}
else
{:noreply, put_flash(socket, :error, "Not allowed.")}
end
end
defp error_message(%Ecto.Changeset{} = cs) do
cs
|> Ecto.Changeset.traverse_errors(fn {m, _} -> m end)
|> Enum.map_join("; ", fn {k, v} -> "#{k} #{Enum.join(v, ", ")}" end)
end
defp permissions(grant) do
[grant.read && "read images & packages", grant.dispatch && "dispatch workflows"]
|> Enum.filter(& &1)
|> Enum.join(", ")
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="space-y-8">
<GitGudWeb.RepoLive.Header.header
repo={@repo}
handle={@handle}
current_scope={@current_scope}
has_packages?={@has_packages?}
can_admin?={@can_admin?}
latest_run={@latest_run}
open_pulls={@open_pulls}
security_alerts={assigns[:security_alerts]}
/>
<.repo_settings_header handle={@handle} repo={@repo} current={:job_tokens} />
<section class="max-w-2xl space-y-4">
<div>
<h2 class="font-semibold">Job token access</h2>
<p class="text-sm opacity-70">
A CI job's token (<code>{"${{ github.token }}"}</code>) reaches only its own repository.
Let another repository's jobs — or every repository in an organization — read this one's
container images and packages, or dispatch its workflows.
</p>
</div>
<form :if={@can_admin?} phx-submit="create_grant" class="flex flex-wrap items-end gap-3">
<label class="form-control">
<span class="text-xs opacity-70">Repository or organization</span>
<input
name="source"
placeholder="owner/repo or org"
class="input input-sm input-bordered font-mono"
/>
</label>
<label class="label cursor-pointer gap-1 text-sm">
<input type="checkbox" name="read" value="true" checked class="checkbox checkbox-sm" />
<span>read</span>
</label>
<label class="label cursor-pointer gap-1 text-sm">
<input type="checkbox" name="dispatch" value="true" class="checkbox checkbox-sm" />
<span>dispatch</span>
</label>
<button class="btn btn-sm btn-primary">Grant</button>
</form>
<table class="table table-sm">
<thead>
<tr>
<th>Jobs from</th>
<th>May</th>
<th></th>
</tr>
</thead>
<tbody>
<tr :for={g <- @grants}>
<td class="font-mono">{JobTokenGrants.source_label(g)}</td>
<td class="text-xs">{permissions(g)}</td>
<td class="text-right">
<button
:if={@can_admin?}
phx-click="delete_grant"
phx-value-id={g.id}
data-confirm={"Remove #{JobTokenGrants.source_label(g)}'s access?"}
class="btn btn-xs btn-ghost text-error"
>
Remove
</button>
</td>
</tr>
<tr :if={@grants == []}>
<td colspan="3" class="text-sm opacity-50">
No other repository's jobs have access.
</td>
</tr>
</tbody>
</table>
<div class="text-xs opacity-70 space-y-1">
<p>Dispatching from a granted repository's workflow:</p>
<pre class="bg-base-200 p-2 rounded overflow-auto select-all">curl -fsS -X POST -H "Authorization: token $TOKEN" \
-H "Content-Type: application/json" -d '{"ref":"{@repo.default_branch || "main"}"}' \
"$CI_FORGE_URL/api/v1/repos/{@handle}/{@repo.name}/actions/workflows/<file>.yml/dispatches"</pre>
<p>
with <code>TOKEN: {"${{ github.token }}"}</code>; the workflow needs <code>on: workflow_dispatch</code>.
</p>
</div>
</section>
</div>
</Layouts.app>
"""
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
7.6 KiB · text
History
b868907