test/openagents_web/route_authority_test.exs

58e6347eeb72 · 12 KB

defmodule OpenAgentsWeb.RouteAuthorityTest do
  use ExUnit.Case, async: true

  alias OpenAgentsWeb.RouteAuthority

  test "every HTTP route and endpoint socket declares one authority, principal, and scope" do
    inventory = RouteAuthority.inventory()

    assert [_ | _] = inventory

    Enum.each(inventory, fn entry ->
      assert entry.class in RouteAuthority.classes(), inspect(entry)
      assert is_binary(entry.principal) and entry.principal != "", inspect(entry)
      assert is_binary(entry.scope) and entry.scope != "", inspect(entry)

      if entry.verb in ["post", "put", "patch", "delete", "connect", "*"] do
        assert entry.mutation, inspect(entry)
      end

      if entry.mutation do
        refute entry.class == :public_read, inspect(entry)
      end
    end)
  end

  test "GitHub sign-in is the bounded public identity action" do
    route = route!(:post, "/auth/github")

    assert route.class == :public_action
    assert route.principal == "OAuth applicant"
    assert route.scope == "identity:sign-in"
    assert route.mutation
  end

  test "public forge reads and bearer-authenticated forge writes are separate" do
    read = route!(:get, "/api/v1/repos/:owner/:repo/issues")
    write = route!(:post, "/api/v1/repos/:owner/:repo/issues")

    assert read.class == :public_read
    assert read.principal == "anonymous or first-party bearer token"
    assert read.scope == "forge:repository:read"
    assert read.mutation == false
    assert write.class == :authenticated_api
    assert write.principal == "first-party human or agent bearer token"
    assert write.scope == "forge:write or agent:participate"
    assert write.mutation

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "GET",
             "/api/v1/repos/OpenAgentsInc/openagents.com/issues",
             "stage.openagents.com"
           ).pipe_through == [:optional_forge_api]

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "POST",
             "/api/v1/repos/OpenAgentsInc/openagents.com/issues",
             "stage.openagents.com"
           ).pipe_through == [:agent_participation_api]
  end

  # The six ancillary issue families used to sit behind the credential-free
  # `:api` pipeline, which discarded a bearer token, so a private repository
  # refused its own owner. They are declared here rather than falling through
  # the `/api/v1` read catch-all, whose "anonymous" principal is now wrong for
  # them.
  test "issue metadata reads accept an optional bearer instead of discarding it" do
    for path <- [
          "/api/v1/repos/:owner/:repo/issues/:issue_number/comments",
          "/api/v1/repos/:owner/:repo/issues/comments/:id",
          "/api/v1/repos/:owner/:repo/issues/:issue_number/labels",
          "/api/v1/repos/:owner/:repo/issues/:issue_number/assignees",
          "/api/v1/repos/:owner/:repo/labels",
          "/api/v1/repos/:owner/:repo/labels/:name",
          "/api/v1/repos/:owner/:repo/milestones",
          "/api/v1/repos/:owner/:repo/milestones/:milestone_number",
          "/api/v1/repos/:owner/:repo/assignees",
          "/api/v1/repos/:owner/:repo/assignees/:assignee"
        ] do
      route = route!(:get, path)

      assert route.class == :public_read, inspect(route)
      assert route.principal == "anonymous or first-party bearer token", inspect(route)
      assert route.scope == "forge:repository:read", inspect(route)
      refute route.mutation, inspect(route)
    end

    for path <- [
          "/api/v1/repos/OpenAgentsInc/openagents.com/issues/1/comments",
          "/api/v1/repos/OpenAgentsInc/openagents.com/issues/comments/1",
          "/api/v1/repos/OpenAgentsInc/openagents.com/issues/1/labels",
          "/api/v1/repos/OpenAgentsInc/openagents.com/issues/1/assignees",
          "/api/v1/repos/OpenAgentsInc/openagents.com/labels",
          "/api/v1/repos/OpenAgentsInc/openagents.com/labels/bug",
          "/api/v1/repos/OpenAgentsInc/openagents.com/milestones",
          "/api/v1/repos/OpenAgentsInc/openagents.com/milestones/1",
          "/api/v1/repos/OpenAgentsInc/openagents.com/assignees",
          "/api/v1/repos/OpenAgentsInc/openagents.com/assignees/someone"
        ] do
      assert Phoenix.Router.route_info(
               OpenAgentsWeb.Router,
               "GET",
               path,
               "stage.openagents.com"
             ).pipe_through == [:optional_forge_api],
             "#{path} no longer runs behind the optional bearer"
    end
  end

  test "agent credential rotation is an agent-scoped bearer write" do
    route = route!(:post, "/api/v1/agent/credentials")

    assert route.class == :authenticated_api
    assert route.principal == "agent bearer token"
    assert route.scope == "agent:participate"
    assert route.mutation

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "POST",
             "/api/v1/agent/credentials",
             "stage.openagents.com"
           ).pipe_through == [:agent_token_api]
  end

  test "public browser forge surfaces remain separate from authenticated entries" do
    for {path, scope} <- [
          {"/issues", "forge:issues:web"},
          {"/projects", "forge:projects:web"}
        ] do
      route = route!(:get, path)

      assert route.class == :public_read
      assert route.principal == "anonymous visitor or signed-in person"
      assert route.scope == scope
      refute route.mutation
    end

    for path <- [
          "/:owner/:repo/labels",
          "/:owner/:repo/milestones",
          "/:owner/:repo/projects",
          "/:owner/:repo/projects/:number"
        ] do
      route = route!(:get, path)

      assert route.class == :public_read
      assert route.principal == "anonymous visitor or signed-in person"
      assert route.scope == "forge:repository:web"
      refute route.mutation
    end

    assert route!(:get, "/:owner/:repo/issues/new").class == :authenticated_browser
    assert route!(:get, "/:owner/:repo/members").class == :authenticated_browser
    assert route!(:get, "/:owner/:repo/assignees").class == :authenticated_browser
  end

  test "repository identity, list, and import status reads require bearer authentication" do
    forge_user = route!(:get, "/api/v1/user")
    repository_list = route!(:get, "/api/v1/user/repos")
    import_status = route!(:get, "/api/v1/repository-imports/:id")
    repository_view = route!(:get, "/api/v1/repos/:owner/:repo")

    assert forge_user.class == :authenticated_api
    assert forge_user.scope == "forge:read"
    assert repository_list.class == :authenticated_api
    assert repository_list.scope == "forge:read"
    assert import_status.class == :authenticated_api
    assert import_status.scope == "forge:read"
    assert repository_view.class == :public_read
    assert repository_view.principal == "anonymous or first-party bearer token"

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "GET",
             "/api/v1/user",
             "stage.openagents.com"
           ).pipe_through == [:forge_write_api]

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "GET",
             "/api/v1/user/repos",
             "stage.openagents.com"
           ).pipe_through == [:forge_write_api]

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "GET",
             "/api/v1/repos/octavia/project",
             "stage.openagents.com"
           ).pipe_through == [:optional_forge_api]
  end

  test "account chat uses its own scoped bearer pipeline" do
    events = route!(:get, "/api/v1/chat/events")
    turns = route!(:post, "/api/v1/chat/turns")

    assert events.scope == "chat:account"
    refute events.mutation
    assert turns.scope == "chat:account"
    assert turns.mutation

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "GET",
             "/api/v1/chat/events",
             "stage.openagents.com"
           ).pipe_through == [:chat_account_api]

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "POST",
             "/api/v1/chat/turns",
             "stage.openagents.com"
           ).pipe_through == [:chat_account_api]
  end

  test "Box control uses its human or delegated bearer pipeline" do
    route = route!(:get, "/api/v1/conversations/:conversation_id/boxes")
    create = route!(:post, "/api/v1/conversations/:conversation_id/boxes")
    command = route!(:post, "/api/v1/conversations/:conversation_id/boxes/:box_id/commands")

    assert route.class == :authenticated_api
    assert route.principal == "human or delegated box-control bearer token"
    assert route.scope == "box:control"
    refute route.mutation
    assert create.mutation
    assert command.mutation

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "GET",
             "/api/v1/conversations/00000000-0000-4000-8000-000000000001/boxes",
             "stage.openagents.com"
           ).pipe_through == [:box_control_api]
  end

  test "operator and computer surfaces cannot drift into browser or public classes" do
    assert route!(:get, "/admin").class == :operator
    assert route!(:get, "/admin/forge").scope == "forge:promote"
    assert route!(:get, "/admin/scv/accounts").scope == "scv:account:connect"
    assert route!(:post, "/controller/pairings").class == :computer
    assert route!(:get, "/controller/pairings/:id").scope == "computer:pairing:claim"
    assert route!(:get, "/controller/status").scope == "computer:status"
    assert route!(:post, "/api/inference/proxy").class == :internal_service
    assert Enum.find(RouteAuthority.socket_inventory(), &(&1.path == "/controller/socket"))

    assert Enum.any?(OpenAgentsWeb.Endpoint.__sockets__(), fn
             {"/controller/socket", OpenAgentsWeb.ControllerSocket, _options} -> true
             _socket -> false
           end)
  end

  test "the canonical Forge smart HTTP routes run through credential authentication" do
    route =
      Phoenix.Router.route_info(
        OpenAgentsWeb.Router,
        "GET",
        "/OpenAgentsInc/openagents.com.git/info/refs",
        "stage.openagents.com"
      )

    assert route.pipe_through == [:forge_git]
    assert route.route == "/:owner/:repo/info/refs"
  end

  test "the OAuth callback suppresses router parameter logging at the application boundary" do
    callback =
      Enum.find(OpenAgentsWeb.Router.__routes__(), &(&1.path == "/auth/github/callback"))

    assert callback.metadata.log == false

    assert Phoenix.Logger.filter_values(%{
             "code" => "oauth-code-sentinel",
             "state" => "oauth-state-sentinel",
             "safe" => "visible"
           }) == %{
             "code" => "[FILTERED]",
             "state" => "[FILTERED]",
             "safe" => "visible"
           }
  end

  test "fleet promotion is an operator mutation, not a generic api v1 write" do
    create = route!(:post, "/api/v1/admin/forge/targets")
    show = route!(:get, "/api/v1/admin/forge/targets/:id")
    index = route!(:get, "/api/v1/admin/forge/targets")

    for route <- [create, show, index] do
      assert route.class == :operator, inspect(route)
      assert route.principal == "current operator holding a privileged bearer token"
      assert route.scope == "deployments:promote"
    end

    assert create.mutation
    refute show.mutation
    refute index.mutation

    # The tenant deployment plane is a different scope on a different path, and
    # neither the generic /api/v1 write catch-all nor `deployments:write`
    # reaches fleet promotion.
    tenant = route!(:post, "/api/v1/repos/:owner/:repo/deployments")
    assert tenant.scope == "deployments:write"
    assert tenant.class == :authenticated_api

    for path <- ["/api/v1/admin/forge/targets", "/api/v1/admin/forge/targets/:id"] do
      assert Phoenix.Router.route_info(
               OpenAgentsWeb.Router,
               "GET",
               String.replace(path, ":id", "00000000-0000-4000-8000-000000000001"),
               "stage.openagents.com"
             ).pipe_through == [:fleet_promotion_api]
    end

    assert Phoenix.Router.route_info(
             OpenAgentsWeb.Router,
             "POST",
             "/api/v1/admin/forge/targets",
             "stage.openagents.com"
           ).pipe_through == [:fleet_promotion_api]
  end

  test "plugin discovery routes are public and not substring matched" do
    for path <- ["/api/v1/plugins", "/api/v1/plugins/:name"] do
      route = route!(:get, path)

      assert route.class == :public_read, inspect(route)
      assert route.principal == "anonymous", inspect(route)
      assert route.scope == "plugins:discover", inspect(route)
      refute route.mutation, inspect(route)
    end

    refute Enum.any?(
             OpenAgentsWeb.Router.__routes__(),
             &(&1.path == "/api/v1/pluginsXYZ")
           )
  end

  defp route!(verb, path) do
    Enum.find(RouteAuthority.inventory(), &(&1.verb == to_string(verb) and &1.path == path)) ||
      flunk("missing route #{verb} #{path}")
  end
end