feat(selection-list-service): route authorization through the Security API instead of embedding Permit - #699
Merged
Conversation
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>
Contributor
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
izzywdev
previously approved these changes
Aug 26, 2026
izzywdev
left a comment
Owner
There was a problem hiding this comment.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Step 2 of 3 in an owner-requested migration off the embedded Permit SDK, onto backend/security's provider-agnostic
AuthorizationProviderseam. config-service was step 1 (#679); billing-service follows separately (step 3).src/middleware/authz.ts(replacesmiddleware/permit.ts) talks to FuzeFront's own Security API via@fuzefront/auth'screateAuthzClient— no vendor SDK, no vendor API key in this service any more. Fail-closed throughout:DECISION_UNAVAILABLEand any transport error deny, never allow. Same flag-gated dark-deploy semantics as before (fuzefront.selection-list.authz-enabled, default OFF).requireAuthzCheck()maps toAuthzClient.check(); the PUT/DELETE handlers' directroleAssignments.assign/unassigncalls andgrantListOwner()map toAuthzClient.grant()/revoke(). Every call site passesresource: { 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 localselection_list_accessmirror table and was never used for authorization.grant()/revoke()are writes:AuthzClientthrows (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.PermitAuthorizationProvider.revoke()acceptedGrantRevokeRequest.resourceper theAuthorizationProvidercontract but silently discarded it (unassignRoleInPermit()'s parameter type explicitlyOmittedresource_instance) — an instance-scoped revoke would 204 successfully while Permit's state was unchanged. Now forwardsresource_instancethrough; 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.tsrenamed toauthz.flags.ts(same env-var flag mechanism, unrelated to the Permit SDK).permitiodependency; added@fuzefront/auth. Dockerfile:packages/authis now actually built (tsup) and itsdistcopied into the production image — mirrors config-service's Dockerfile treatment of the same package.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 treeAuthzError('DECISION_UNAVAILABLE')thrown bycheck()→ 403; a throwngrant()/revoke()→ 500 with the mirror-table write asserted not to have happenedcheck/grant/revoke(the privilege-escalation guard)backend/security/tests/role-revoke.resource-scope.test.ts— 4 tests for theresource_instanceforwarding fixgrep -rn permitio services/selection-list-service/— emptynpm ci;scripts/check-dockerfile-lockfile.mjs;scripts/check-workspace-deps.mjs— all greenpackages/authdependencyGenerated by Claude Code