defmodule GitGudWeb.RegistryControllerTest do
@moduledoc """
The OCI token endpoint: what each kind of credential is granted, on a
repository's own image and on images beneath it (`owner/repo/server`).
"""
use GitGudWeb.ConnCase, async: false
import GitGud.AccountsFixtures
import GitGud.ForgeFixtures
alias GitGud.Accounts
alias GitGud.Organizations
alias GitGud.Packages
alias GitGud.Repositories
alias GitGud.Repositories.Storage
alias GitGud.Workflows.JobTokenGrants
alias GitGud.Workflows.RunnerJwt
alias GitGud.Workflows.WorkflowJob
alias GitGud.Workflows.WorkflowRun
@moduletag :git_required
setup do
if System.find_executable("git") == nil do
{:skip, "git not on PATH"}
else
previous = Application.get_env(:git_gud, Packages.Token)
{private_pem, _public_pem} = Packages.Token.generate_keypair(2048)
Application.put_env(:git_gud, Packages.Token, private_key_pem: private_pem)
on_exit(fn ->
if previous,
do: Application.put_env(:git_gud, Packages.Token, previous),
else: Application.delete_env(:git_gud, Packages.Token)
end)
:ok
end
end
# The actions granted on `image` for a token request with `auth`.
defp granted(conn, auth, image, actions \\ "pull,push") do
conn =
if auth,
do: put_req_header(conn, "authorization", "Basic " <> Base.encode64(auth)),
else: conn
body = conn |> get("/v2/token", scope: "repository:#{image}:#{actions}") |> json_response(200)
{:ok, claims} = Joken.peek_claims(body["token"])
case Enum.find(claims["access"], &(&1["name"] == image)) do
%{"actions" => granted} -> granted
nil -> []
end
end
defp path(repo) do
repo = GitGud.Repo.preload(repo, [:owner, :organization], force: true)
Storage.repo_handle(repo) <> "/" <> repo.name
end
defp pat(user, scopes) do
{:ok, _token, plaintext} = Accounts.create_personal_access_token(user, "t", scopes)
"x:" <> plaintext
end
defp job_token(repo, perms \\ GitGud.Workflows.Permissions.default()) do
run =
%WorkflowRun{repository_id: repo.id}
|> WorkflowRun.create_changeset(%{
number: System.unique_integer([:positive]),
workflow_path: ".forgejo/workflows/ci.yml",
trigger: "push",
head_sha: :crypto.strong_rand_bytes(20)
})
|> GitGud.Repo.insert!()
job =
%WorkflowJob{workflow_run_id: run.id}
|> WorkflowJob.create_changeset(%{job_id: "build", definition: %{}})
# Claimed: a job token is only good while its job runs.
|> Ecto.Changeset.change(status: "running", started_at: DateTime.utc_now(:second))
|> GitGud.Repo.insert!()
"gitgud-actions:" <> RunnerJwt.mint(job, run, perms)
end
defp org_repo(visibility \\ "private") do
owner = user_fixture()
handle = "org#{System.unique_integer([:positive])}"
{:ok, org} = Organizations.create_organization(owner, %{"handle" => handle, "name" => handle})
{:ok, repo} =
Repositories.create_repository_for_org(org, owner, %{
"name" => unique_repo_name(),
"visibility" => visibility
})
%{owner: owner, org: org, repo: repo}
end
describe "images under a repository" do
test "the owner may push the repository's image and any beneath it", %{conn: conn} do
{owner, repo} = repository_fixture()
owner = set_password(owner)
auth = owner.email <> ":" <> valid_user_password()
assert granted(conn, auth, path(repo)) == ["pull", "push"]
assert granted(conn, auth, path(repo) <> "/login") == ["pull", "push"]
assert granted(conn, auth, path(repo) <> "/web/assets") == ["pull", "push"]
end
test "scopes it doesn't understand are dropped, not signed as asked", %{conn: conn} do
{_owner, repo} = repository_fixture(%{visibility: "public"})
for scope <- [
"registry:catalog:*",
"repository(plugin):#{path(repo)}:pull",
"registry:catalog:* repository:#{path(repo)}:pull"
] do
body = conn |> get("/v2/token", scope: scope) |> json_response(200)
{:ok, claims} = Joken.peek_claims(body["token"])
assert Enum.all?(claims["access"], &(&1["type"] == "repository")), scope
refute Enum.any?(claims["access"], &(&1["name"] == "catalog")), scope
end
# The repository half of a space-separated request is still granted.
body =
conn
|> get("/v2/token", scope: "registry:catalog:* repository:#{path(repo)}:pull")
|> json_response(200)
{:ok, claims} = Joken.peek_claims(body["token"])
assert [%{"name" => name, "actions" => ["pull"]}] = claims["access"]
assert name == path(repo)
end
test "names that aren't a repository grant nothing", %{conn: conn} do
{owner, repo} = repository_fixture()
auth = pat(owner, ["repo"])
assert granted(conn, auth, "nobody-here/" <> repo.name <> "/login") == []
assert granted(conn, auth, path(repo) <> "//login") == []
assert granted(conn, auth, repo.name) == []
end
end
describe "job tokens" do
test "push and pull their own repository's images, sub-paths included", %{conn: conn} do
{_owner, repo} = repository_fixture()
auth = job_token(repo)
assert granted(conn, auth, path(repo)) == ["pull", "push"]
assert granted(conn, auth, path(repo) <> "/tower-demo") == ["pull", "push"]
end
test "get nothing from another private repository until it grants read", %{conn: conn} do
{_, source} = repository_fixture()
{_, target} = repository_fixture()
auth = job_token(source)
assert granted(conn, auth, path(target) <> "/tower-demo") == []
{:ok, _} = JobTokenGrants.create(target, path(source), %{read: true})
# Read, never write: a grant doesn't let another repository publish here.
assert granted(conn, auth, path(target) <> "/tower-demo") == ["pull"]
end
test "an organization-wide grant covers every repository in it", %{conn: conn} do
%{org: org, repo: source} = org_repo()
{_, target} = repository_fixture()
{:ok, _} = JobTokenGrants.create(target, org.handle, %{read: true})
assert granted(conn, job_token(source), path(target)) == ["pull"]
end
test "a dispatch-only grant doesn't open reads", %{conn: conn} do
{_, source} = repository_fixture()
{_, target} = repository_fixture()
{:ok, _} = JobTokenGrants.create(target, path(source), %{read: false, dispatch: true})
assert granted(conn, job_token(source), path(target)) == []
end
test "follow the workflow's packages: permission", %{conn: conn} do
{_, repo} = repository_fixture()
read = job_token(repo, %{"contents" => :read, "packages" => :read, "actions" => :none})
none = job_token(repo, %{"contents" => :read, "packages" => :none, "actions" => :none})
assert granted(conn, read, path(repo)) == ["pull"]
assert granted(conn, none, path(repo)) == []
end
test "a grant is read only with packages: read", %{conn: conn} do
{_, source} = repository_fixture()
{_, target} = repository_fixture()
{:ok, _} = JobTokenGrants.create(target, path(source), %{read: true})
auth = job_token(source, %{"contents" => :read, "packages" => :none, "actions" => :none})
assert granted(conn, auth, path(target)) == []
end
end
describe "registry tokens" do
test "grant their scopes on their repository and the images beneath it", %{conn: conn} do
{_owner, repo} = repository_fixture()
{:ok, _, plaintext} = Packages.create_registry_token(repo, "argo", ["pull"])
auth = "deploy:" <> plaintext
assert granted(conn, auth, path(repo)) == ["pull"]
assert granted(conn, auth, path(repo) <> "/login") == ["pull"]
{_, other} = repository_fixture()
assert granted(conn, auth, path(other) <> "/login") == []
end
end
describe "visibility (anonymous)" do
test "public images are pullable", %{conn: conn} do
{_, repo} = repository_fixture(%{visibility: "public"})
assert granted(conn, nil, path(repo) <> "/web") == ["pull"]
end
test "internal images are not: internal means signed-in users", %{conn: conn} do
{_, repo} = repository_fixture(%{visibility: "internal"})
assert granted(conn, nil, path(repo)) == []
reader = user_fixture()
assert granted(conn, pat(reader, ["packages:read"]), path(repo)) == ["pull"]
end
test "private images are not", %{conn: conn} do
{_, repo} = repository_fixture()
assert granted(conn, nil, path(repo)) == []
end
end
describe "organizations" do
test "an org admin who isn't the repository's owner may push", %{conn: conn} do
%{org: org, repo: repo} = org_repo()
admin = user_fixture()
{:ok, _} = Organizations.add_member(org, admin, "admin")
assert granted(conn, pat(admin, ["repo"]), path(repo) <> "/login") == ["pull", "push"]
end
test "a team with write may push; read may only pull", %{conn: conn} do
%{org: org, repo: repo} = org_repo()
writer = user_fixture()
reader = user_fixture()
for {user, permission} <- [{writer, "write"}, {reader, "read"}] do
{:ok, _} = Organizations.add_member(org, user)
{:ok, team} =
Organizations.create_team(org, %{"name" => "t#{System.unique_integer([:positive])}"})
{:ok, _} = Organizations.add_team_member(team, user)
{:ok, _} = Organizations.grant_team_repo(team, repo, permission)
end
assert granted(conn, pat(writer, ["repo"]), path(repo)) == ["pull", "push"]
assert granted(conn, pat(reader, ["repo"]), path(repo)) == ["pull"]
end
test "a plain member gets nothing from a private repository", %{conn: conn} do
%{org: org, repo: repo} = org_repo()
member = user_fixture()
{:ok, _} = Organizations.add_member(org, member)
assert granted(conn, pat(member, ["repo"]), path(repo)) == []
end
test "deleting takes an admin", %{conn: conn} do
%{owner: owner, org: org, repo: repo} = org_repo()
writer = user_fixture()
{:ok, _} = Organizations.add_member(org, writer)
{:ok, team} =
Organizations.create_team(org, %{"name" => "w#{System.unique_integer([:positive])}"})
{:ok, _} = Organizations.add_team_member(team, writer)
{:ok, _} = Organizations.grant_team_repo(team, repo, "write")
assert granted(conn, pat(owner, ["repo"]), path(repo), "delete") == ["delete"]
assert granted(conn, pat(writer, ["repo"]), path(repo), "delete") == []
end
end
describe "personal access tokens keep to their scopes" do
setup do
{owner, repo} = repository_fixture()
%{owner: owner, repo: repo}
end
test "packages:write pushes", %{conn: conn, owner: owner, repo: repo} do
assert granted(conn, pat(owner, ["packages:write"]), path(repo)) == ["pull", "push"]
end
test "packages:read only pulls", %{conn: conn, owner: owner, repo: repo} do
assert granted(conn, pat(owner, ["packages:read"]), path(repo)) == ["pull"]
end
test "a token without a package or repo scope gets nothing", %{
conn: conn,
owner: owner,
repo: repo
} do
assert granted(conn, pat(owner, ["ci:read", "ci:write"]), path(repo)) == []
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
11.2 KiB · text
History
b868907