defmodule GitGudWeb.AptNamespaceTest do
@moduledoc """
The namespace-wide apt repository at `_all`.
A namespace is usually the unit a machine wants: five repositories
under one organization are five `deb` lines for what an administrator
thinks of as one publisher. These assert that the aggregate is a real
apt repository — signed, complete, and filtered by who is asking.
Organizations and users are the same code path; both are namespaces,
and both are covered here.
"""
use GitGudWeb.ConnCase, async: false
alias GitGud.Packages
alias GitGud.Packages.Apt.Native
alias GitGud.Repositories
alias GitGud.Repositories.Storage
import GitGud.AccountsFixtures
import GitGud.ForgeFixtures
# A .deb: an ar archive of debian-binary, control.tar.gz and
# data.tar.gz, in that order.
defp deb(fields) do
control = Enum.map_join(fields, fn {k, v} -> "#{k}: #{v}\n" end)
ar([
{"debian-binary", "2.0\n"},
{"control.tar.gz", :zlib.gzip(tar([{~c"./control", control}]))},
{"data.tar.gz", :zlib.gzip(tar([{~c"./usr/bin/thing", "#!/bin/sh\n"}]))}
])
end
defp tar(members) do
path = Path.join(System.tmp_dir!(), "t-#{System.unique_integer([:positive])}.tar")
:ok = :erl_tar.create(String.to_charlist(path), members, [])
File.read!(path)
end
defp ar(members) do
body =
Enum.map_join(members, fn {name, data} ->
header =
String.pad_trailing(name, 16) <>
String.pad_trailing("0", 12) <>
String.pad_trailing("0", 6) <>
String.pad_trailing("0", 6) <>
String.pad_trailing("100644", 8) <>
String.pad_trailing(Integer.to_string(byte_size(data)), 10) <>
<<0x60, 0x0A>>
header <> data <> if(rem(byte_size(data), 2) == 1, do: "\n", else: "")
end)
"!<arch>\n" <> body
end
defp publish(repo, package, opts \\ []) do
fields = [
Package: package,
Version: Keyword.get(opts, :version, "1.0.0-1"),
Architecture: Keyword.get(opts, :arch, "amd64"),
Maintainer: "Someone <someone@example.com>",
Description: "the #{package} package"
]
body = deb(fields)
{:ok, _p, _v, _f} =
Packages.publish_file(repo, "deb", package, Keyword.get(opts, :version, "1.0.0-1"), %{
filename:
"#{package}_#{Keyword.get(opts, :version, "1.0.0-1")}_#{Keyword.get(opts, :arch, "amd64")}.deb",
body: body,
metadata: %{
"dist" => Keyword.get(opts, :dist, "stable"),
"component" => "main",
"architecture" => Keyword.get(opts, :arch, "amd64"),
"control" => Map.new(fields, fn {k, v} -> {to_string(k), v} end)
}
})
body
end
defp org_fixture(user) do
{:ok, org} =
GitGud.Organizations.create_organization(user, %{
"handle" => "org#{System.unique_integer([:positive])}",
"name" => "Test Org"
})
org
end
defp org_repo(org, user, visibility \\ "public") do
{:ok, repo} =
Repositories.create_repository_for_org(org, user, %{
"name" => unique_repo_name(),
"visibility" => visibility
})
GitGud.Repo.preload(repo, [:owner, :organization])
end
setup do
user = user_fixture()
org = org_fixture(user)
tools = org_repo(org, user)
agents = org_repo(org, user)
{:ok, user: user, org: org, tools: tools, agents: agents, root: "/pkg/deb/#{org.handle}/_all"}
end
describe "aggregation" do
test "one index carries packages from every repository in the org", %{
conn: conn,
tools: tools,
agents: agents,
root: root
} do
publish(tools, "widget")
publish(agents, "gadget")
body = get(conn, root <> "/dists/stable/main/binary-amd64/Packages").resp_body
assert body =~ "Package: widget"
assert body =~ "Package: gadget"
end
test "each entry points into its own repository's pool", %{
conn: conn,
tools: tools,
agents: agents,
root: root
} do
publish(tools, "widget")
publish(agents, "gadget")
body = get(conn, root <> "/dists/stable/main/binary-amd64/Packages").resp_body
assert body =~ "Filename: pool/main/#{tools.name}/w/widget/widget_1.0.0-1_amd64.deb"
assert body =~ "Filename: pool/main/#{agents.name}/g/gadget/gadget_1.0.0-1_amd64.deb"
end
test "two repositories publishing the same filename don't collide", %{
conn: conn,
tools: tools,
agents: agents,
root: root
} do
# Same package name and version in both, different bytes.
from_tools = publish(tools, "widget", version: "1.0.0-1")
from_agents = publish(agents, "widget", version: "1.0.0-1", arch: "arm64")
refute from_tools == from_agents
index = get(conn, root <> "/dists/stable/main/binary-amd64/Packages").resp_body
[_, tools_path] = Regex.run(~r/^Filename: (\S+)$/m, index)
assert get(build_conn(), root <> "/" <> tools_path).resp_body == from_tools
arm = get(build_conn(), root <> "/dists/stable/main/binary-arm64/Packages").resp_body
[_, agents_path] = Regex.run(~r/^Filename: (\S+)$/m, arm)
refute tools_path == agents_path
assert get(build_conn(), root <> "/" <> agents_path).resp_body == from_agents
end
test "Release spans the architectures and suites of the whole namespace", %{
conn: conn,
tools: tools,
agents: agents,
org: org,
root: root
} do
publish(tools, "widget", arch: "amd64")
publish(agents, "gadget", arch: "arm64")
release = get(conn, root <> "/dists/stable/Release").resp_body
assert release =~ ~r/^Origin: #{org.handle}$/m
assert release =~ ~r/^Architectures: amd64 arm64$/m
assert release =~ ~r/^Suite: stable$/m
end
test "a suite only one repository uses still resolves", %{
conn: conn,
tools: tools,
root: root
} do
publish(tools, "widget", dist: "trixie")
body = get(conn, root <> "/dists/trixie/main/binary-amd64/Packages").resp_body
assert body =~ "Package: widget"
assert get(build_conn(), root <> "/dists/stable/main/binary-amd64/Packages").resp_body == ""
end
test "every path Release lists is served, at the right size and digest", %{
conn: conn,
tools: tools,
agents: agents,
root: root
} do
publish(tools, "widget")
publish(agents, "gadget", arch: "all")
release = get(conn, root <> "/dists/stable/Release").resp_body
listed =
Regex.scan(~r/^ ([0-9a-f]{64}) +(\d+) +(\S+)$/m, release)
|> Map.new(fn [_, digest, size, path] -> {path, {digest, String.to_integer(size)}} end)
assert map_size(listed) > 0
for {path, {digest, size}} <- listed do
served = get(build_conn(), root <> "/dists/stable/" <> path)
assert served.status == 200, "#{path} is listed in Release but not served"
assert byte_size(served.resp_body) == size, "#{path} size disagrees with Release"
assert Base.encode16(:crypto.hash(:sha256, served.resp_body), case: :lower) == digest,
"#{path} checksum disagrees with Release"
end
end
end
describe "signing" do
test "the aggregate is signed with the same key as each repository", %{
conn: conn,
org: org,
tools: tools,
root: root
} do
publish(tools, "widget")
key = get(conn, "/pkg/_keys/deb/#{org.handle}").resp_body
release = get(build_conn(), root <> "/dists/stable/Release").resp_body
signature = get(build_conn(), root <> "/dists/stable/Release.gpg").resp_body
assert {:ok, true} = Native.verify_detached(key, release, signature)
# …and the per-repository view verifies against the very same key,
# which is the whole point: one keyring entry either way.
repo_root = "/pkg/deb/#{Storage.repo_handle(tools)}/#{tools.name}"
repo_release = get(build_conn(), repo_root <> "/dists/stable/Release").resp_body
repo_sig = get(build_conn(), repo_root <> "/dists/stable/Release.gpg").resp_body
assert {:ok, true} = Native.verify_detached(key, repo_release, repo_sig)
end
test "InRelease is clearsigned", %{conn: conn, tools: tools, root: root} do
publish(tools, "widget")
body = get(conn, root <> "/dists/stable/InRelease").resp_body
assert body =~ "-----BEGIN PGP SIGNED MESSAGE-----"
assert body =~ "Package" or body =~ "Suite: stable"
end
test "key.asc under _all is the namespace key", %{conn: conn, org: org, root: root} do
from_all = get(conn, root <> "/key.asc").resp_body
assert from_all =~ "-----BEGIN PGP PUBLIC KEY BLOCK-----"
assert get(build_conn(), "/pkg/_keys/deb/#{org.handle}").resp_body == from_all
end
end
describe "visibility" do
test "an anonymous caller sees only the public repositories", %{
conn: conn,
org: org,
user: user,
tools: tools,
root: root
} do
private = org_repo(org, user, "private")
publish(tools, "public-thing")
publish(private, "secret-thing")
body = get(conn, root <> "/dists/stable/main/binary-amd64/Packages").resp_body
assert body =~ "Package: public-thing"
refute body =~ "Package: secret-thing"
end
test "a repository's own deploy token still doesn't widen the view", %{
org: org,
user: user,
tools: tools,
root: root
} do
other_private = org_repo(org, user, "private")
publish(tools, "public-thing")
publish(other_private, "secret-thing")
# A token for a *different* private repo in the same namespace.
{:ok, _t, token} = Packages.create_registry_token(tools, "ci", ["pull"])
body =
build_conn()
|> put_req_header("authorization", token)
|> get(root <> "/dists/stable/main/binary-amd64/Packages")
|> Map.get(:resp_body)
assert body =~ "Package: public-thing"
refute body =~ "Package: secret-thing"
end
test "a private package's pool path 404s for a caller who can't read it", %{
conn: conn,
org: org,
user: user,
root: root
} do
private = org_repo(org, user, "private")
publish(private, "secret-thing")
path = "pool/main/#{private.name}/s/secret-thing/secret-thing_1.0.0-1_amd64.deb"
assert get(conn, root <> "/" <> path).status == 404
end
test "an empty namespace still serves a valid, signed Release", %{conn: conn, root: root} do
release = get(conn, root <> "/dists/stable/Release").resp_body
assert release =~ ~r/^Components: main$/m
assert release =~ ~r/^SHA256:$/m
assert get(build_conn(), root <> "/dists/stable/InRelease").status == 200
end
end
describe "user namespaces work the same way" do
setup do
user = user_fixture()
{_u, one} = repository_fixture(%{owner: user, visibility: "public"})
{_u, two} = repository_fixture(%{owner: user, visibility: "public"})
preload = &GitGud.Repo.preload(&1, [:owner, :organization])
{:ok,
solo: user,
one: preload.(one),
two: preload.(two),
solo_root: "/pkg/deb/#{Storage.owner_handle(user)}/_all"}
end
test "one index spans a user's own repositories", %{
conn: conn,
one: one,
two: two,
solo_root: root
} do
publish(one, "alpha")
publish(two, "beta")
body = get(conn, root <> "/dists/stable/main/binary-amd64/Packages").resp_body
assert body =~ "Package: alpha"
assert body =~ "Package: beta"
end
test "and it is signed by that user's key", %{
conn: conn,
solo: solo,
one: one,
solo_root: root
} do
publish(one, "alpha")
key = get(conn, "/pkg/_keys/deb/#{Storage.owner_handle(solo)}").resp_body
release = get(build_conn(), root <> "/dists/stable/Release").resp_body
signature = get(build_conn(), root <> "/dists/stable/Release.gpg").resp_body
assert {:ok, true} = Native.verify_detached(key, release, signature)
end
test "an org-owned repository is not in its creator's personal namespace", %{
conn: conn,
solo: solo,
solo_root: root
} do
org = org_fixture(solo)
org_owned = org_repo(org, solo)
publish(org_owned, "org-thing")
body = get(conn, root <> "/dists/stable/main/binary-amd64/Packages").resp_body
refute body =~ "Package: org-thing"
end
end
describe "the marker" do
test "publishing to _all is refused — a package needs a repository", %{
conn: conn,
root: root
} do
conn =
conn
|> put_req_header("content-type", "application/octet-stream")
|> post(root <> "/upload", deb(Package: "widget", Version: "1.0"))
assert conn.status == 405
assert conn.resp_body =~ "publish to a repository"
end
test "a format with no namespace index 404s there", %{conn: conn, org: org} do
assert get(conn, "/pkg/pypi/#{org.handle}/_all/simple/").status == 404
end
test "an unknown namespace 404s", %{conn: conn} do
assert get(conn, "/pkg/deb/nobody-at-all/_all/dists/stable/Release").status == 404
end
test "a repository can never be called _all", %{user: user} do
assert {:error, changeset} =
Repositories.create_repository(user, %{"name" => "_all", "visibility" => "public"})
assert Keyword.has_key?(changeset.errors, :name)
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
13.2 KiB · text
History
202c083