feat(portal): master-admin provisioning pipeline + CRUD API (FF-EPIC-09 S2/S3) - #464
feat(portal): master-admin provisioning pipeline + CRUD API (FF-EPIC-09 S2/S3)#464claude[bot] wants to merge 2 commits into
Conversation
…09 S2/S3) Implements the resumable portal provisioning pipeline (org -> Permit tenant -> ReBAC instance/parent link -> portal row -> default subdomain -> owner invite) and the master-admin CRUD surface against the frozen services/portal-service/openapi.yaml contract, building on the portals/ portal_domains schema and root-portal seed landed in #424. - backend/src/services/portalProvisioning.ts: idempotent, advisory-lock serialized pipeline keyed by slug (mirrors organizationProvisioning.ts's reconcile pattern). Resumes from a failed step on retrigger without re-creating prior resources, serializes concurrent same-slug requests, and leaves the portal provisioned-pending-invite (never silently active) even when the owner-invite step itself fails -- always emitting portal.created. - backend/src/migrations/017_portal_provisioning.ts: the resumable step ledger, keyed by slug (not portal_id, which doesn't exist until step 5). - backend/src/routes/adminPortals.ts: the 6 master-admin routes, flag-gated (404 off) and Permit platform-admin gated (403 FORBIDDEN, fail-closed) via checkOrganizationPermission against the ROOT organization -- the same ReBAC org-admin derivation permit/schema.ts already declares. Implements cursor pagination on the fleet list per governance/pagination-standard.md. - shared/src/kafka: adds the portal.created event (TOPICS + zod schema). - portalRepository.ts: adds the contract's PortalDomain.active field. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127R9Zcw92tmBkxUUuLEB79
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
…rge master Replace the polynomial-backtracking email regex /^[^\s@]+@[^\s@]+\.[^\s@]+$/ — flagged high-severity by CodeQL as ReDoS on the attacker-controlled ownerEmail — with a structural O(n) validator (indexOf/slice + single-char \s test), preserving the exact accept/reject semantics. Also updates the branch from master (no code conflicts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0127R9Zcw92tmBkxUUuLEB79
Automated code review (gate-code-review)Credit balance is too low Report-only — this check never blocks merge. |
izzywdev
left a comment
There was a problem hiding this comment.
Approving per repo-owner authorization to merge EPIC-09 S2/S3.
Implementation matches the frozen services/portal-service/openapi.yaml contract (all six /api/v1/admin/portals routes, Permit platform-admin fail-closed gate, root-portal-protected suspend, cursor pagination), plus the resumable provisioning pipeline and portal.created event. 335 unit + 183 integration tests pass. The high-severity CodeQL ReDoS in the ownerEmail validator (adminPortals.ts) has been fixed on b5d1c53e with a linear structural validator preserving the same accept/reject semantics.
Note: mcp-maintain / gate-code-review failures on this PR are the CI runner's Anthropic-credit exhaustion ("Credit balance is too low"), not code defects.
Generated by Claude Code
|
Closing this PR as superseded. While it was in flight, The one part No action needed on this PR. Generated by Claude Code |
Pull request was closed
…(FF-EPIC-09-S2) (#504) Salvages the S2 value from the now-closed parallel PR #464 that master's merged admin-portals CRUD lacked: the resumable, advisory-locked portal provisioning pipeline and the portal.created event, layered onto master's existing POST /api/v1/admin/portals route (routes/rate-limiting/authz/ pagination/validation/SLUG_TAKEN/ROOT_PORTAL_PROTECTED all untouched). - services/portalProvisioning.ts: org -> Permit tenant -> Organization ReBAC instance/parent-link -> portals row -> default subdomain -> owner invite, keyed by slug (Postgres advisory lock serializes concurrent same-slug requests -> SlugTakenError), resumable mid-step, never throws out of the transaction on an infra-step failure, and checkpoints into provisioned-pending-invite + emits portal.created exactly once regardless of whether the owner-invite step itself succeeds. - migrations/018_portal_provisioning.ts: the resumable step ledger (renumbered from 017, already taken by app_scope_levels_and_installations on master), reusing provisioning_status_enum from migration 009. - shared/src/kafka: portal.created TOPICS entry + zod schema (portalCreatedSchemaV1), plus the compiled shared/dist/kafka output. - eventPublisher.ts: publishPortalCreated. - organizationProvisioning.ts: exports rowToOrganization for reuse. - routes/adminPortals.ts: createAdminPortalStore().create() now drives provisionPortal() instead of a single bare insert; the POST route also maps SlugTakenError to 409 SLUG_TAKEN alongside the existing pg 23505 check. Master's existing validEmail() was already a linear indexOf/slice-based validator, so no ReDoS fix was needed. - tests/portal-provisioning.test.ts: happy path, AC2 resume-after-mid-step- failure, AC3 concurrent-same-slug serialization, AC4 invite-failure still checkpoints + emits portal.created, plus two tests wiring createAdminPortalStore().create() itself through the real pipeline. Claude-Session: https://claude.ai/code/session_0127R9Zcw92tmBkxUUuLEB79 Co-authored-by: Claude (product-designer) <izzy.weinberg@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Israel Weinberg <99821070+izzywdev@users.noreply.github.com>
📋 Description
Implements FF-EPIC-09 stories S2 (resumable portal provisioning pipeline) and S3 (master-admin portal CRUD API) against the already-frozen
services/portal-service/openapi.yamlcontract, building on theportals/portal_domainsschema, root-portal seed, and multi-tenant-portals flag landed in #424. Does not re-create or modify any of that S1/S4 work.🔄 Type of Change
🧪 Testing
Test Configuration:
Test Instructions
🔧 Implementation Details
Changes Made
backend/src/services/portalProvisioning.ts— the S2 resumable pipeline: org → Permit tenant → Organization ReBAC instance/parent-link → portals row → default subdomain → owner invite. MirrorsorganizationProvisioning.ts's idempotent reconcile pattern rather than reinventing it. Keyed byslug(notportal_id, which doesn't exist until step 5). Never throws out of the transaction on an infra-step failure (would roll back prior progress in the same call, defeating resumability) — recordsfailedon the step row and returns a result object instead;SlugTakenErroris the sole intentional throw, raised before any row is touched.backend/src/migrations/017_portal_provisioning.ts— the resumable step ledger (portal_provisioning), reusing the existingprovisioning_status_enumfrom migration 009.backend/src/routes/adminPortals.ts— the S3 CRUD surface:GET/POST /api/v1/admin/portals,GET/PATCH /api/v1/admin/portals/{portalId},POST .../suspend,POST .../resume. Every route:authenticateToken(401) → master flagfuzefront.platform.multi-tenant-portals(404 when OFF, unchanged pre-epic behavior) → Permit platform-admin gate (403FORBIDDEN, fail-closed on any check error) viacheckOrganizationPermission(userId, action, ROOT_ORG_ID)— the same ReBACorg-adminparent→child derivation already declared inpermit/schema.tsand wired byservices/rootOrgAdmin.ts. Cursor pagination on the fleet list pergovernance/pagination-standard.md(opaquebase64url({lastCreatedAt, lastId})cursor, server-clampedlimit,{items, page}envelope). suspend/resume callinvalidatePortalCache(portalId)so the FF-EPIC-10 resolver picks up the change immediately; the root portal (is_root) refuses suspend with 409ROOT_PORTAL_PROTECTED.backend/src/repositories/portalRepository.ts— adds the contract'sPortalDomain.activefield (added to the frozen contract in feat(custom-domains): FuzeInfra custom-hostname integration + wildcard Ingress (FFRNT-91) #431 after feat(portal): schema + context resolution + boot endpoint (FF-EPIC-10, FF-EPIC-09-S1) #424 landed; derived from existing columns, no migration needed).backend/src/services/eventPublisher.ts+shared/src/kafka— adds theportal.createdevent (TOPICS + zod schemaportalCreatedSchemaV1), emitted exactly once when a portal's infra steps all complete (checkpoint intoprovisioned-pending-invite), regardless of whether the owner-invite step itself succeeds (AC4).backend/src/index.ts— mounts the new router at/api/v1/admin/portals.Code Quality
📋 Checklist
Pre-submission
Code Quality
@fuzefront/custom-hostname-clientmodule-resolution errors remain, identical on clean master)Security Checklist
validation_error)📝 Additional Notes
Deployment Notes
017_portal_provisioning.ts— additive, idempotentCREATE TYPE ... EXCEPTION WHEN duplicate_object+hasTableguard)Future Work
organization_invitationsrow into a real membership + promotingorganizations.owner_idfrom the provisioning admin to the accepted owner) is a later epic, out of scope here.verificationStatus/tlsStatusbeyondverified/noneforkind: customdomains.🎯 Test plan / verification (backend slice only)
npx tsc --noEmit— clean except the 2 pre-existing@fuzefront/custom-hostname-clienterrors (confirmed identical viagit stash)npm run test:coverage -- --runInBand --testPathIgnorePatterns="permit-integration|billing-"— 19 suites / 335 tests passednpm run test:integration— 8 suites / 183 tests passednpm run lint— 0 errors, pre-existing unrelated warnings onlybackend/tests/portal-provisioning.test.ts(6 tests: happy path, branding/identityPolicy override, AC2 resume-after-failure, AC3 concurrent-serialization, AC4 invite-failure-still-emits-event, genuine-duplicate SLUG_TAKEN) andbackend/tests/admin-portals-routes.test.ts(34 tests: flag OFF→404 on all 6 routes, non-admin→403 FORBIDDEN on all 6 routes, contract-shape assertions, validation, lifecycle suspend/resume + immediate cache invalidation + ROOT_PORTAL_PROTECTED, pagination envelope/clamp/cursor-walk/status-filter)🤖 Generated with Claude Code
https://claude.ai/code/session_0127R9Zcw92tmBkxUUuLEB79
Generated by Claude Code