Fix authenticated API and operator forge gating.

6063fbdf0daa · AtlantisPleb · · parent 4bc8e78bd779

Fix authenticated API and operator forge gating.

- Add :authenticated_api pipeline and require_authenticated_api_user.

- Move /api/computers, /api/computer-agent-jobs, and /machines into correct scopes.

- Gate /admin/forge on Accounts.admin? and re-check on promote.

- Use git-backed commit_store in AdminForgeLive so only WAL-pushed SHAs promote.

- Unskip and green computers, computer agent jobs, and admin forge live tests.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

Not deployed through the forge lane

No push, promotion, build, or deploy receipt references this commit (receipts are scanned over a bounded recent window). Changes shipped by full node replacement carry their proof in the release gate receipt instead.

Changed files

  • modified lib/openagents_web/live/admin_forge_live.ex
  • modified lib/openagents_web/router.ex
  • modified lib/openagents_web/user_auth.ex
  • modified test/openagents_web/controllers/computer_agent_jobs_controller_test.exs
  • modified test/openagents_web/controllers/computers_controller_test.exs
  • modified test/openagents_web/live/admin_forge_live_test.exs

Diff

6 files changed, +66 -23

lib/openagents_web/live/admin_forge_live.ex modified +33 -14

@@ -11,30 +11,38 @@ defmodule OpenAgentsWeb.AdminForgeLive do

11 11
12 12
  use OpenAgentsWeb, :sarah_live_view
13 13
14
  alias OpenAgents.Accounts
14 15
  alias OpenAgents.Forge
15 16
  alias OpenAgents.Forge.Targets
16 17
17 18
  @impl true
18 19
  def mount(_params, _session, socket) do
19
    if connected?(socket) do
20
      Enum.each(["forge:pushes", "forge:target", "forge:builds", "forge:deploys"], fn topic ->
21
        Phoenix.PubSub.subscribe(OpenAgents.PubSub, topic)
22
      end)
23
    end
20
    if Accounts.admin?(socket.assigns.current_user) do
21
      if connected?(socket) do
22
        Enum.each(["forge:pushes", "forge:target", "forge:builds", "forge:deploys"], fn topic ->
23
          Phoenix.PubSub.subscribe(OpenAgents.PubSub, topic)
24
        end)
25
      end
24 26
25
    {:ok,
26
     socket
27
     |> assign(:page_title, "Operator · Forge")
28
     |> assign(:repo, primary_repo())
29
     |> load()}
27
      {:ok,
28
       socket
29
       |> assign(:page_title, "Operator · Forge")
30
       |> assign(:repo, primary_repo())
31
       |> load()}
32
    else
33
      {:ok, redirect(socket, to: ~p"/")}
34
    end
30 35
  end
31 36
32 37
  @impl true
33 38
  def handle_event("promote", %{"sha" => sha}, socket) do
34
    operator = "operator:" <> to_string(socket.assigns.current_user.github_id)
39
    unless Accounts.admin?(socket.assigns.current_user) do
40
      {:noreply, redirect(socket, to: ~p"/")}
41
    else
42
      operator = "operator:" <> to_string(socket.assigns.current_user.github_id)
35 43
36
    socket =
37
      case Targets.promote(socket.assigns.repo, sha, operator) do
44
      socket =
45
        case Targets.promote(socket.assigns.repo, sha, operator, commit_store: &git_store/2) do
38 46
        {:ok, _target} ->
39 47
          put_flash(socket, :info, "Promoted #{String.slice(sha, 0, 12)} as the fleet target.")
40 48

@@ -49,7 +57,8 @@ defmodule OpenAgentsWeb.AdminForgeLive do

49 57
          put_flash(socket, :error, "Promotion failed.")
50 58
      end
51 59
52
    {:noreply, load(socket)}
60
      {:noreply, load(socket)}
61
    end
53 62
  end
54 63
55 64
  @impl true

@@ -80,6 +89,16 @@ defmodule OpenAgentsWeb.AdminForgeLive do

80 89
    OpenAgents.Forge.Repos.allowed_repos() |> List.first() || "sarah"
81 90
  end
82 91
92
  defp git_store(repo, sha) do
93
    OpenAgents.Forge.Sync.ensure_fresh(repo)
94
    path = OpenAgents.Forge.Repos.bare_path(repo)
95
96
    case OpenAgents.Forge.Repos.git(path, ["cat-file", "-e", sha <> "^{commit}"]) do
97
      {_, 0} -> :ok
98
      _ -> :error
99
    end
100
  end
101
83 102
  defp short(sha) when is_binary(sha), do: String.slice(sha, 0, 12)
84 103
  defp short(_), do: "—"
85 104
lib/openagents_web/router.ex modified +19 -6

@@ -23,6 +23,14 @@ defmodule OpenAgentsWeb.Router do

23 23
    plug :accepts, ["json"]
24 24
  end
25 25
26
  pipeline :authenticated_api do
27
    plug :accepts, ["json"]
28
    plug :fetch_session
29
    plug :fetch_current_user
30
    plug :protect_from_forgery
31
    plug :require_authenticated_api_user
32
  end
33
26 34
  pipeline :status_probe_compat do
27 35
    plug OpenAgentsWeb.Plugs.StatusProbeCompat
28 36
  end

@@ -102,16 +110,21 @@ defmodule OpenAgentsWeb.Router do

102 110
    delete "/data/reset", DataController, :reset
103 111
104 112
    get "/machines", ComputersController, :index
105
    get "/api/computers", ComputersController, :index
106
    post "/api/computers/pairings/:id/approve", ComputersController, :approve_pairing
107
    delete "/api/computers/:id", ComputersController, :delete
108
    post "/api/computers/:machine_id/agent-jobs", ComputerAgentJobsController, :create
109
    get "/api/computer-agent-jobs/:id", ComputerAgentJobsController, :show
110
    delete "/api/computer-agent-jobs/:id", ComputerAgentJobsController, :delete
111 113
112 114
    get "/memory/export", MemoryExportController, :show
113 115
  end
114 116
117
  scope "/api", OpenAgentsWeb do
118
    pipe_through :authenticated_api
119
120
    get "/computers", ComputersController, :index
121
    post "/computers/pairings/:id/approve", ComputersController, :approve_pairing
122
    delete "/computers/:id", ComputersController, :delete
123
    post "/computers/:machine_id/agent-jobs", ComputerAgentJobsController, :create
124
    get "/computer-agent-jobs/:id", ComputerAgentJobsController, :show
125
    delete "/computer-agent-jobs/:id", ComputerAgentJobsController, :delete
126
  end
127
115 128
  forward "/git", OpenAgents.Forge.GitHTTP
116 129
117 130
  scope "/", OpenAgentsWeb do
lib/openagents_web/user_auth.ex modified +14

@@ -83,4 +83,18 @@ defmodule OpenAgentsWeb.UserAuth do

83 83
         |> Phoenix.LiveView.redirect(to: ~p"/")}
84 84
    end
85 85
  end
86
87
  def require_authenticated_api_user(
88
        %{assigns: %{current_user: %{status: "active"}}} = conn,
89
        _options
90
      ),
91
      do: conn
92
93
  def require_authenticated_api_user(conn, _options) do
94
    conn
95
    |> put_status(:unauthorized)
96
    |> put_resp_header("cache-control", "no-store")
97
    |> Phoenix.Controller.json(%{error: "authentication_required"})
98
    |> halt()
99
  end
86 100
end
test/openagents_web/controllers/computer_agent_jobs_controller_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgentsWeb.ComputerAgentJobsControllerTest do
2 2
  use OpenAgentsWeb.SarahConnCase, async: false
3
  @moduletag :skip
4 3
  import Phoenix.LiveViewTest
5 4
6 5
  alias OpenAgents.Machines
test/openagents_web/controllers/computers_controller_test.exs modified -1

@@ -1,6 +1,5 @@

1 1
defmodule OpenAgentsWeb.ComputersControllerTest do
2 2
  use OpenAgentsWeb.SarahConnCase, async: false
3
  @moduletag :skip
4 3
  alias OpenAgents.Computer
5 4
  alias OpenAgents.Machines
6 5
  alias OpenAgents.Machines.Pairing
test/openagents_web/live/admin_forge_live_test.exs modified -1

@@ -7,7 +7,6 @@ defmodule OpenAgentsWeb.AdminForgeLiveTest do

7 7
  """
8 8
9 9
  use OpenAgentsWeb.SarahConnCase, async: false
10
  @moduletag :skip
11 10
  import Phoenix.LiveViewTest
12 11
13 12
  alias OpenAgents.Forge.{Repos, Targets}

This page updates live while a promote is in flight · changelog