2.7 KiB · text History b868907
defmodule GitGudWeb.Plugs.HookAuthTest do
  @moduledoc """
  The hook and SSH-wrapper endpoints answer only this node's own hooks:
  the listener is public, so a caller has to show the secret the node
  handed its git processes.
  """

  use GitGudWeb.ConnCase, async: false

  import GitGud.AccountsFixtures
  import GitGud.ForgeFixtures

  alias GitGudWeb.Plugs.HookAuth

  @zero String.duplicate("0", 40)

  defp hook_post(conn, path, body, token) do
    conn =
      if token, do: put_req_header(conn, HookAuth.header(), token), else: conn

    conn
    |> put_req_header("content-type", "text/plain")
    |> post(path, body)
  end

  setup do
    {_owner, repo} = repository_fixture()
    %{repo: repo}
  end

  for path <- ["/_hooks/post-receive", "/_hooks/pre-receive"] do
    test "#{path} refuses a caller without the secret", %{conn: conn, repo: repo} do
      conn = put_req_header(conn, "x-repo-path", repo.disk_path)
      line = "#{@zero} #{String.duplicate("a", 40)} refs/heads/main\n"

      assert hook_post(conn, unquote(path), line, nil).status == 403
      assert hook_post(conn, unquote(path), line, "guess").status == 403
    end
  end

  test "a forged post-receive starts nothing", %{conn: conn, repo: repo} do
    victim = user_fixture()

    resp =
      conn
      |> put_req_header("x-repo-path", repo.disk_path)
      |> put_req_header("x-pusher-id", Integer.to_string(victim.id))
      |> hook_post(
        "/_hooks/post-receive",
        "#{@zero} #{String.duplicate("b", 40)} refs/heads/x\n",
        nil
      )

    assert resp.status == 403
    assert resp.halted
  end

  test "/_ssh/resolve refuses a caller without the secret", %{conn: conn, repo: repo} do
    body = Jason.encode!(%{"key_id" => "1", "repo" => repo.name, "cmd" => "git-receive-pack"})

    resp =
      conn
      |> put_req_header("content-type", "application/json")
      |> post("/_ssh/resolve", body)

    assert resp.status == 403
    refute resp.resp_body =~ "OK"
  end

  test "the node's own hook gets through", %{conn: conn, repo: repo} do
    resp =
      conn
      |> put_req_header("x-repo-path", repo.disk_path)
      |> hook_post("/_hooks/pre-receive", "", HookAuth.secret())

    assert resp.status == 200
    assert resp.resp_body =~ "OK"

    body = Jason.encode!(%{"key_id" => "999999999", "repo" => "a/b", "cmd" => "git-upload-pack"})

    resp =
      build_conn()
      |> put_req_header(HookAuth.header(), HookAuth.secret())
      |> put_req_header("content-type", "application/json")
      |> post("/_ssh/resolve", body)

    assert resp.status == 200
    assert resp.resp_body =~ "DENY"
  end

  test "the secret is in the environment git inherits" do
    assert System.get_env("GITGUD_HOOK_SECRET") == HookAuth.secret()
    assert byte_size(HookAuth.secret()) >= 32
  end
end