Record the newly public forge surfaces in the route authority

2740bd179527 · Devin AI · · parent 52fbab615a74

Record the newly public forge surfaces in the route authority

The route authority classified /issues, /projects, and the repository
tracker reads as authenticated browser surfaces. Classify them as public
reads so the inventory matches the router, and keep issues/new, members,
and assignees behind sign-in.

Co-Authored-By: Christopher David <chris@openagents.com>
Co-Authored-By
Christopher David <chris@openagents.com>

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/route_authority.ex
  • modified test/openagents_web/route_authority_test.exs

Diff

2 files changed, +71 -11

lib/openagents_web/route_authority.ex modified +39 -11

@@ -42,11 +42,6 @@ defmodule OpenAgentsWeb.RouteAuthority do

42 42
    "/memory",
43 43
    "/device",
44 44
    "/repositories",
45
    # The workspace-wide issue and project lists. Repository-scoped reading is
46
    # public; reading across every repository is not, because the set being
47
    # read is one person's.
48
    "/issues",
49
    "/projects",
50 45
    # The forum. Reading and posting happen signed in, matching the other
51 46
    # workspace-wide surfaces.
52 47
    "/forum",

@@ -123,6 +118,24 @@ defmodule OpenAgentsWeb.RouteAuthority do

123 118
       when path in @public_browser_paths and verb in [:get, :head],
124 119
       do: declaration(:public_read, "anonymous", "published:web", false)
125 120
121
  defp policy(%{path: "/issues", verb: verb}) when verb in [:get, :head],
122
    do:
123
      declaration(
124
        :public_read,
125
        "anonymous visitor or signed-in person",
126
        "forge:issues:web",
127
        false
128
      )
129
130
  defp policy(%{path: "/projects", verb: verb}) when verb in [:get, :head],
131
    do:
132
      declaration(
133
        :public_read,
134
        "anonymous visitor or signed-in person",
135
        "forge:projects:web",
136
        false
137
      )
138
126 139
  defp policy(%{path: "/auth/github", verb: :post}),
127 140
    do: declaration(:authenticated_browser, "explicit OAuth applicant", "identity:connect", true)
128 141

@@ -270,6 +283,17 @@ defmodule OpenAgentsWeb.RouteAuthority do

270 283
      tracker_browser_path?(path) ->
271 284
        declaration(:authenticated_browser, "active encrypted browser session", "forge:web", true)
272 285
286
      tracker_read_browser_path?(path) and verb in [:get, :head] ->
287
        # Reading tracker surfaces is public on a public repository. The GET
288
        # mutates nothing: every write rides the LiveView channel and is
289
        # re-checked against writability in the view's event handlers.
290
        declaration(
291
          :public_read,
292
          "anonymous visitor or signed-in person",
293
          "forge:repository:web",
294
          false
295
        )
296
273 297
      issue_browser_path?(path) and verb in [:get, :head] ->
274 298
        # Reading issues is public on a public repository. The GET mutates
275 299
        # nothing: every write rides the LiveView channel and is re-checked

@@ -313,8 +337,6 @@ defmodule OpenAgentsWeb.RouteAuthority do

313 337
  defp browser_scope("/data" <> _path), do: "data:self"
314 338
  defp browser_scope("/memory/" <> _path), do: "memory:self"
315 339
  defp browser_scope("/github/connection"), do: "github-tools:self"
316
  defp browser_scope("/issues"), do: "forge:issues:self"
317
  defp browser_scope("/projects"), do: "forge:projects:self"
318 340
  defp browser_scope("/settings/api-tokens"), do: "api-token:self"
319 341
  defp browser_scope(_path), do: "product:self"
320 342

@@ -324,10 +346,16 @@ defmodule OpenAgentsWeb.RouteAuthority do

324 346
  defp browser_mutation?(_path, _verb), do: true
325 347
326 348
  defp tracker_browser_path?(path) do
327
    String.match?(
328
      path,
329
      ~r{\A/:owner/:repo/(issues/new|labels|milestones|assignees|projects|members)}
330
    )
349
    String.match?(path, ~r{\A/:owner/:repo/(issues/new|assignees|members)\z})
350
  end
351
352
  defp tracker_read_browser_path?(path) do
353
    path in [
354
      "/:owner/:repo/labels",
355
      "/:owner/:repo/milestones",
356
      "/:owner/:repo/projects",
357
      "/:owner/:repo/projects/:number"
358
    ]
331 359
  end
332 360
333 361
  # The issue index and detail pages live in their own public-read session;
test/openagents_web/route_authority_test.exs modified +32

@@ -51,6 +51,38 @@ defmodule OpenAgentsWeb.RouteAuthorityTest do

51 51
           ).pipe_through == [:forge_write_api]
52 52
  end
53 53
54
  test "public browser forge surfaces remain separate from authenticated entries" do
55
    for {path, scope} <- [
56
          {"/issues", "forge:issues:web"},
57
          {"/projects", "forge:projects:web"}
58
        ] do
59
      route = route!(:get, path)
60
61
      assert route.class == :public_read
62
      assert route.principal == "anonymous visitor or signed-in person"
63
      assert route.scope == scope
64
      refute route.mutation
65
    end
66
67
    for path <- [
68
          "/:owner/:repo/labels",
69
          "/:owner/:repo/milestones",
70
          "/:owner/:repo/projects",
71
          "/:owner/:repo/projects/:number"
72
        ] do
73
      route = route!(:get, path)
74
75
      assert route.class == :public_read
76
      assert route.principal == "anonymous visitor or signed-in person"
77
      assert route.scope == "forge:repository:web"
78
      refute route.mutation
79
    end
80
81
    assert route!(:get, "/:owner/:repo/issues/new").class == :authenticated_browser
82
    assert route!(:get, "/:owner/:repo/members").class == :authenticated_browser
83
    assert route!(:get, "/:owner/:repo/assignees").class == :authenticated_browser
84
  end
85
54 86
  test "repository identity, list, and import status reads require bearer authentication" do
55 87
    forge_user = route!(:get, "/api/v3/user")
56 88
    repository_list = route!(:get, "/api/v3/user/repos")

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