feat(npm): wire authz enforcement for the npm dispatcher (part 4/4) (S09 Enforcement plan: 13/20)
Split into 4 stacked MRs to keep each part within the review size target (~800 reviewable LoC ideal). Each part targets the previous one (part 1 targets main); the stack is reviewed and merged bottom-up.
Part 4 of 4 of the S09 (authorization) enforcement plan — Step 13: npm wiring and route-table parity. Spec: S09 (authorization). Depends on parts 1–3 (merge order enforced by the merge-request dependencies).
📦 What this part does
Wraps the npm dispatcher with the authorization middleware at its catch-all registration, behind the same configured-clients gate as OCI and Maven:
withNpmAuthz— installs the resolution holder and composesauthz.NewNpmMiddlewarearound the dispatcher; panics on a nil resolver at wiring time.npmAuthzResolverAdapter— resolves the repository for the decision layer, translates the resolver's sentinel errors, and seeds the holder only on success, so the format middleware resolves exactly once per authorized request.logNpmAuthzWiring— boot-time log making the gate greppable, mirroring the OCI and Maven variants.
The composition suite proves the chain end to end: wrap/unwrap gate, anonymous-write masked 404 (byte-identical to npm's genuine not-found), role-based denial matrix, error-envelope rendering, replay across suspension and kind gates, per-request holder isolation under concurrency, scoped/escaped package forms, and the authz-driven parity direction (every registered route drives exactly one GLAZ check with the spec's action).
⚖️ Known trade-off
On deployments where both the iam: and GLAZ clients are configured, npm's granular route patterns are stamped below the wrap, so exit-path readers (RoutePatternMiddleware, the HTTP metrics route label) see / for npm requests. TestNpmAuthzComposition_RoutePatternExitReadIsRootMount pins the collapse so it is explicit rather than accidental; recorded on #152 (comment 3762478657).
📏 Size
1,179 reviewable LoC, past the 500 guideline: ~1,040 of the 1,146 added lines are test files (959 of them in cmd/artifact-registry/wire_npm_authz_test.go); the production Go is ~92 lines across the four wiring files. The composition tests exercise withNpmAuthz and the adapter directly, so splitting them out would separate the tests from the code they cover.
🔬 e2e scenarios
docs/testing/e2e/npm.md is updated in this MR: the Access rows record that enforcement is real while end-to-end automation stays blocked on role assignment, an unowned GitLab surface (same status as Maven's catalog).
📚 Stacked MRs (review/merge bottom-up)
- refactor(npm): move the npm mount predicate int... (!2131 - merged) • David Fernandez • 19.4
- feat(npm): add the resolution holder and middle... (!2132 - merged) • David Fernandez • 19.4
- refactor(npm): drive the dispatcher mux from a ... (!2133 - merged) • David Fernandez • 19.4
- feat(npm): wire authz enforcement for the npm d... (!2134 - merged) • David Fernandez • 19.4
👈
Related to #855 (closed)