Skip to content

feat(selection-list-service): route authorization through the Security API instead of embedding Permit - #699

Merged
izzywdev merged 2 commits into
masterfrom
claude/authz-security-selection-list
Aug 26, 2026
Merged

feat(selection-list-service): route authorization through the Security API instead of embedding Permit#699
izzywdev merged 2 commits into
masterfrom
claude/authz-security-selection-list

Conversation

@claude

@claude claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Step 2 of 3 in an owner-requested migration off the embedded Permit SDK, onto backend/security's provider-agnostic AuthorizationProvider seam. config-service was step 1 (#679); billing-service follows separately (step 3).

  • src/middleware/authz.ts (replaces middleware/permit.ts) talks to FuzeFront's own Security API via @fuzefront/auth's createAuthzClient — no vendor SDK, no vendor API key in this service any more. Fail-closed throughout: DECISION_UNAVAILABLE and any transport error deny, never allow. Same flag-gated dark-deploy semantics as before (fuzefront.selection-list.authz-enabled, default OFF).
  • This service does more than decisions: requireAuthzCheck() maps to AuthzClient.check(); the PUT/DELETE handlers' direct roleAssignments.assign/unassign calls and grantListOwner() map to AuthzClient.grant()/revoke(). Every call site passes resource: { type: 'SelectionList', key: listId } — dropping it would silently widen a list-scoped grant/revoke/check to tenant-wide (a real privilege-escalation surface). countActiveOwners() is untouched — it reads only the local selection_list_access mirror table and was never used for authorization.
  • grant()/revoke() are writes: AuthzClient throws (never resolves) on a Security API failure, and every call site performs that write before touching the mirror row, so a thrown grant/revoke can never leave the mirror claiming a role change that did not happen. Covered by dedicated tests asserting the mirror write is never reached.
  • Found and fixed a resource-scoping gap in backend/security this migration depends on for correctness: PermitAuthorizationProvider.revoke() accepted GrantRevokeRequest.resource per the AuthorizationProvider contract but silently discarded it (unassignRoleInPermit()'s parameter type explicitly Omitted resource_instance) — an instance-scoped revoke would 204 successfully while Permit's state was unchanged. Now forwards resource_instance through; existing tenant-wide callers (organization-role helpers) are unaffected since it stays optional. New regression test: backend/security/tests/role-revoke.resource-scope.test.ts.
  • middleware/permit.flags.ts renamed to authz.flags.ts (same env-var flag mechanism, unrelated to the Permit SDK).
  • Removed the permitio dependency; added @fuzefront/auth. Dockerfile: packages/auth is now actually built (tsup) and its dist copied into the production image — mirrors config-service's Dockerfile treatment of the same package.
  • Helm: PERMIT_API_KEY (SealedSecret) drops; SECURITY_SERVICE_URL (plain in-cluster Service DNS) takes its place in the deployment template and the sealed-secret template/GO-LIVE instructions.
  • openapi.yaml: prose-only fix to authorization descriptions that named Permit specifically — no schema/status-code change.

Test plan

  • services/selection-list-service: tsc --noEmit, npm run build, npm test — 234 tests / 9 suites, all green on the committed tree
  • Explicit fail-closed coverage: AuthzError('DECISION_UNAVAILABLE') thrown by check() → 403; a thrown grant()/revoke() → 500 with the mirror-table write asserted not to have happened
  • Resource-instance scoping asserted on the wire for check/grant/revoke (the privilege-escalation guard)
  • backend/security/tests/role-revoke.resource-scope.test.ts — 4 tests for the resource_instance forwarding fix
  • grep -rn permitio services/selection-list-service/ — empty
  • Root npm ci; scripts/check-dockerfile-lockfile.mjs; scripts/check-workspace-deps.mjs — all green
  • Docker image build — not verified in this sandbox (no Docker daemon available); Dockerfile changes closely mirror config-service's already-merged, working pattern for the same packages/auth dependency

Generated by Claude Code

claude added 2 commits August 17, 2026 14:22
Implement grant(), revoke(), and listGrants() methods on the AuthzClient
interface, mirroring the server's grant/revoke endpoints at
/api/v1/security/authz/grants.

- grant(req, token): POST with 201 success, 400 MALFORMED, 502 PROVIDER_ERROR
- revoke(req, token): DELETE with 204 success, 400 MALFORMED, 502 PROVIDER_ERROR
- listGrants(query, token): GET with cursor-paginated result

Error codes MALFORMED and PROVIDER_ERROR distinguish caller bugs (400) from
upstream transients (502). A grant/revoke is a write; any failure throws and
never silently succeeds.

Tests cover successful grants with and without resource scope, both revoke
forms (by grantId and by subject+tenant+role), error distinction, timeout
handling, and listGrants pagination.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session
…y API instead of embedding Permit

Step 2 of 3 (owner-requested migration; config-service was step 1, #679;
billing-service follows separately). selection-list-service consumed the
permitio SDK directly, giving it its own PERMIT_API_KEY and defeating
backend/security's provider-agnostic AuthorizationProvider seam
(authzFactory.ts) — swapping providers meant touching every service instead
of just backend/security.

- src/middleware/authz.ts (replaces middleware/permit.ts) talks to
  FuzeFront's own Security API via @fuzefront/auth's createAuthzClient — no
  vendor SDK, no vendor API key in this service any more. Fail-closed
  throughout: DECISION_UNAVAILABLE and any transport error deny, never
  allow. Same flag-gated dark-deploy semantics as the Permit-backed
  predecessor (fuzefront.selection-list.authz-enabled, default OFF).
- This service does MORE than decisions: requirePermit()/requireAuthzCheck()
  maps to AuthzClient.check(); the PUT/DELETE handlers' direct
  roleAssignments.assign/unassign calls and grantListOwner() map to
  AuthzClient.grant()/revoke(). All three call sites pass
  resource: { type: 'SelectionList', key: listId } — the resource is what
  scopes a grant/revoke/check to one list instance instead of silently
  widening to tenant-wide (a real privilege-escalation surface if dropped).
  countActiveOwners() is untouched: it reads only the local
  selection_list_access mirror table and was never used for authorization.
- grant()/revoke() are writes: AuthzClient throws (never resolves) on a
  Security API failure, and every call site performs that write BEFORE
  touching the selection_list_access mirror row, so a thrown grant/revoke
  can never leave the mirror claiming a role change that did not happen.
  Covered by dedicated tests asserting the mirror write is never reached.
- Found and fixed a resource-scoping gap in backend/security this migration
  depends on for correctness: PermitAuthorizationProvider.revoke() accepted
  GrantRevokeRequest.resource per the AuthorizationProvider contract but
  discarded it (unassignRoleInPermit()'s parameter type explicitly Omit'd
  resource_instance), so an instance-scoped revoke would silently succeed
  while Permit's state was unchanged. Now forwards resource_instance
  through; existing tenant-wide callers (organization-role helpers) are
  unaffected since resource_instance stays optional.
- middleware/permit.flags.ts renamed to authz.flags.ts (same env-var-based
  flag mechanism, unrelated to the Permit SDK — the old filename no longer
  fit).
- Removed the permitio dependency; added @fuzefront/auth (pinned to the
  sibling workspace version, matching backend/applications' convention for
  in-repo packages). Dockerfile: packages/auth is now actually BUILT (tsup)
  and its dist copied into the production image (config-service's Dockerfile
  is the reference pattern for the same package) — the pre-existing
  packages/auth/package.json COPY in both npm ci stages (from #679's prep)
  only satisfied hoisting, not the runtime import this service now has.
- Helm: PERMIT_API_KEY (SealedSecret) drops; SECURITY_SERVICE_URL (plain
  in-cluster Service DNS, matching config-service's convention) takes its
  place, in the deployment template and the sealed-secret template/GO-LIVE
  instructions.
- openapi.yaml: prose-only fix to authorization descriptions that named
  Permit specifically — no schema/status-code change.

Verified on the committed tree: services/selection-list-service type-check,
build, and `npm test` (234 tests / 9 suites, up from prior baseline — new
DECISION_UNAVAILABLE fail-closed tests, resource-scoping assertions on
grant/revoke, and write-ordering tests proving a thrown grant/revoke never
reaches the mirror-table write); backend/security's new regression test for
the resource-scoping fix (4 tests); root `npm ci`;
scripts/check-dockerfile-lockfile.mjs and scripts/check-workspace-deps.mjs;
`grep -rn permitio services/selection-list-service/` empty.

Claude-Session: https://claude.ai/code/session_0183JMAkioT5pGt8aVmddPtc

Co-authored-by: Claude <noreply@anthropic.com>
@claude
claude Bot requested a review from izzywdev as a code owner August 17, 2026 14:47
@claude claude Bot added the auto-merge Enable squash auto-merge once CI passes label Aug 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Automated code review (gate-code-review)

Credit balance is too low

Report-only — this check never blocks merge.

izzywdev
izzywdev previously approved these changes Aug 26, 2026

@izzywdev izzywdev left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All CI gates pass (gate-authz, gate-ds-conformance, gate-identifier, gate-frames-first, gate-test, gate-lint, gate-build, gate-sast, gate-toolchain, gate-version, gate-localup, etc.). Approving per governance policy.

Base automatically changed from claude/authz-client-grants to master August 26, 2026 05:04
@izzywdev
izzywdev dismissed their stale review August 26, 2026 05:04

The base branch was changed.

@izzywdev
izzywdev merged commit 09d498a into master Aug 26, 2026
33 of 37 checks passed
@izzywdev
izzywdev deleted the claude/authz-security-selection-list branch August 26, 2026 05:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-merge Enable squash auto-merge once CI passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants