Skip to content

Tracking: converting the route table to pure handlers #7032

Description

@otavio

The roadmap for the handler conversion, recovered from a local planning file that was never
committed. It is here so the stack has a link.

Decision

Deepen the route declaration seam before converting the remaining ~160 handlers.

#6941 gave routes three shapes and a gateway.Declaration recording what a
route claims. #7030 made mounting and declaring the same act, so a declaration
now knows its own address, permission and API-key policy, and the route-table tests are invariants
over every route the router mounts rather than a hand-maintained ledger.

Stack

master
└─ #6941 / cloud#2509 ......... three shapes                                 (open)
   └─ #7030 .................... registration declares the whole claim       (open)
      └─ A-cloud ............... cloud adopts the same registration  (spec: team#239)
         └─ B .................. list-query contract moves into the List shape
            └─ A′ ............... every route states a permission or why it needs none
               └─ A′-cloud ...... the same for the /admin/api surface
                  └─ C… ......... per-resource conversion (separate planning pass)

The original plan ordered this A → A′ → B, with cloud following one step behind at each rung.
Reordered for two reasons: cloud/ does not compile against #7030 until A-cloud lands, which
taxes every change made in the workspace meanwhile; and A′ is a security review needing a human
answer per route, so it should not block a mechanical cleanup. B depends on A only — Accepts
is a RouteOption on the mount call, which #7030 delivered.

A-cloud — cloud adopts the same registration

Spec: shellhub-io/team#239.

The four shape constructors changed return type in #7030, so internal/billing/routes,
internal/admin/routes and internal/cloud/routes no longer build. Mechanical: the same diff shape
as #7030 over 81 call sites, with Requires/NoAPIKey/Guard already built and tested. Cloud
registers no declarations today, so the audits do not yet see it.

Note the billing webhook registers on the router root, and the two admin groups are groups — all
three are covered by gateway.MountOn(router, target).

B — the list-query contract belongs to the List shape

Goal. Delete the 14 shellhub (and later 7 cloud) copies of normalize → unmarshal → validate →
log → 400, and the 171-line go/ast test that polices them.

Shape. gateway.Accepts(services.DeviceQuery) on the registration; prepare[T] runs the
contract before the handler is entered, where it already normalizes the paginator and sorter. The
per-resource FieldSet/FieldConstraints pairs move behind one named value per resource.

Carries these fixes. The List shape owns X-Total-Count, so the three endpoints answering it
with len(list) — the page size, not the collection size — are corrected as a side effect:
access-policy.go:32, service-account.go:34, ssh-identity.go:48. invitation.go's
FormatInt/Itoa split goes with them. The default sort columns picked in api-key.go:52 and
install-key.go:57 move to the resource's contract, where they are policy rather than handler code.

Deletes. server/api/routes/list_validation_test.go, replaced by a check over Declarations():
a List route names a query contract.

A′ — every route states a permission claim, or why it needs none

A security review, not a mechanical diff. 21 mutating shellhub routes carry no permission today.
Most are legitimately self-service or pre-credential (auth, register, accept invite, leave
namespace, web reauth, setup) and want gateway.NoPermission("...") with a written reason.

One is not what the route table suggests: DeleteSSHIdentity registers with no permission while its
two siblings require SSHIdentityAdd. It is guarded, in the handler body, so the behaviour is
correct — the route table under-reports it, which is exactly the class of thing declaring the
permission at the registration is meant to end.

Ends with the invariant: a route names a permission or states why it needs none. Same argument
as Unbounded/Anonymous — the claim cannot arrive by omission.

A′-cloud covers the 30 /admin/api routes, which need a decision rather than a default:
shellhub-io/team#238.

Deferred, and why

  • Per-resource conversion of the remaining ~160 handlers — the bulk of the work, but it wants
    its own planning pass once A and B fix the shape of a converted route. Each resource carries
    scope-into-the-service and actor-instead-of-req.UserID for that resource.
  • gateway.Context deletion (15 accessors, duplicate pairs, GetTennat) — falls out of the
    conversion, not before it.
  • MCP calling handlers directly — needs A, because the httptest round-trip is currently what
    re-runs the permission check.
  • Authorization decided in handlers (req.Manage, the silent req.All downgrade,
    GetNamespace's membership check) — behaviour changes; keep them out of mechanical PRs so they
    are reviewed as such.
  • The tenant guard and the legacy authorize middleware — the latter looks close to vestigial,
    and deciding its fate is a behaviour question.
  • Collapsing the anonymous allowlist into the route table — the allowlist must survive for
    routes that never touch the gateway, and registering from the mount call needs an authenticator
    the mount helper does not have. The all-routes anonymity invariant added in refactor(server): make route registration and declaration the same act #7030 is what makes
    this safe to do later.

CI

A PR based on a feature branch runs zero checks in these repos: the workflows are gated on
branches: [master]. Every PR in this stack needs lint and tests run locally, in-container, before
it is called green.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions