Skip to content

feat(portal): master-admin provisioning pipeline + CRUD API (FF-EPIC-09 S2/S3) - #464

Closed
claude[bot] wants to merge 2 commits into
masterfrom
claude/portal-epic09-provisioning-crud
Closed

feat(portal): master-admin provisioning pipeline + CRUD API (FF-EPIC-09 S2/S3)#464
claude[bot] wants to merge 2 commits into
masterfrom
claude/portal-epic09-provisioning-crud

Conversation

@claude

@claude claude Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

📋 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.yaml contract, building on the portals/portal_domains schema, 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

  • ✨ New feature (non-breaking change which adds functionality)

🧪 Testing

  • Unit tests
  • Integration tests (real Postgres)

Test Configuration:

  • Node.js version: 22.22.2 (repo floor is Node 24; ran under the available runtime)
  • npm version: 10.9.7
  • OS: Linux

Test Instructions

cd backend && npm install
PERMIT_API_KEY=ci-no-real-permit-calls npm run test:coverage -- --runInBand --testPathIgnorePatterns="permit-integration|billing-"
PERMIT_API_KEY=ci-no-real-permit-calls npm run test:integration
npx tsc --noEmit
npm run lint

🔧 Implementation Details

Changes Made

  • Backend Changes:
    • backend/src/services/portalProvisioning.ts — the S2 resumable pipeline: org → Permit tenant → Organization ReBAC instance/parent-link → portals row → default subdomain → owner invite. Mirrors organizationProvisioning.ts's idempotent reconcile pattern rather than reinventing it. Keyed by slug (not portal_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) — records failed on the step row and returns a result object instead; SlugTakenError is 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 existing provisioning_status_enum from 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 flag fuzefront.platform.multi-tenant-portals (404 when OFF, unchanged pre-epic behavior) → Permit platform-admin gate (403 FORBIDDEN, fail-closed on any check error) via checkOrganizationPermission(userId, action, ROOT_ORG_ID) — the same ReBAC org-admin parent→child derivation already declared in permit/schema.ts and wired by services/rootOrgAdmin.ts. Cursor pagination on the fleet list per governance/pagination-standard.md (opaque base64url({lastCreatedAt, lastId}) cursor, server-clamped limit, {items, page} envelope). suspend/resume call invalidatePortalCache(portalId) so the FF-EPIC-10 resolver picks up the change immediately; the root portal (is_root) refuses suspend with 409 ROOT_PORTAL_PROTECTED.
    • backend/src/repositories/portalRepository.ts — adds the contract's PortalDomain.active field (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 the portal.created event (TOPICS + zod schema portalCreatedSchemaV1), emitted exactly once when a portal's infra steps all complete (checkpoint into provisioned-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

  • Code follows the project's coding standards
  • Self-review of code completed
  • Code is commented, particularly in hard-to-understand areas
  • No console.log or debugging statements left in code

📋 Checklist

Pre-submission

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Code Quality

  • Code follows conventional commit format
  • TypeScript strict mode passes (only the 2 pre-existing @fuzefront/custom-hostname-client module-resolution errors remain, identical on clean master)
  • ESLint passes without errors (0 errors; pre-existing unrelated warnings only)
  • No security vulnerabilities introduced

Security Checklist

  • No sensitive data exposed
  • Input validation implemented (create/update payload validation → 400 validation_error)
  • Authentication/authorization properly handled (401 → 404-flag-gate → 403 platform-admin, fail-closed on error)
  • No SQL injection vulnerabilities (all queries via Knex query builder, no raw string interpolation of request input)

📝 Additional Notes

Deployment Notes

  • Requires database migration (017_portal_provisioning.ts — additive, idempotent CREATE TYPE ... EXCEPTION WHEN duplicate_object + hasTable guard)

Future Work

  • Owner-invite acceptance flow (converting the organization_invitations row into a real membership + promoting organizations.owner_id from the provisioning admin to the accepted owner) is a later epic, out of scope here.
  • FF-EPIC-16 (custom domains) owns populating verificationStatus/tlsStatus beyond verified/none for kind: custom domains.

🎯 Test plan / verification (backend slice only)

  • npx tsc --noEmit — clean except the 2 pre-existing @fuzefront/custom-hostname-client errors (confirmed identical via git stash)
  • npm run test:coverage -- --runInBand --testPathIgnorePatterns="permit-integration|billing-"19 suites / 335 tests passed
  • npm run test:integration8 suites / 183 tests passed
  • npm run lint — 0 errors, pre-existing unrelated warnings only
  • New tests: backend/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) and backend/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

…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
@claude
claude Bot requested a review from izzywdev as a code owner July 29, 2026 22:38
@claude claude Bot added the auto-merge Enable squash auto-merge once CI passes label Jul 29, 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.

Comment thread backend/src/routes/adminPortals.ts Fixed
…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
@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.

@github-actions
github-actions Bot enabled auto-merge (squash) July 29, 2026 22:48

@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.

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

izzywdev commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Closing this PR as superseded. While it was in flight, master independently received a full EPIC-09 admin-portals implementation (6c3cdf0544aa221f: fleet listing, provision, detail, update, suspend, resume, plus rate-limiting), so this PR's S3 CRUD routes are a conflicting duplicate of already-shipped code.

The one part master does not yet have is this PR's S2 resumable provisioning pipeline (advisory-locked, idempotent-resume, provisioned-pending-invite checkpoint) and the portal.created event. Rather than clobber master's rate-limited routes by force-merging, that S2 work is being salvaged into a focused follow-up PR that layers it onto master's existing create route. Nothing is lost — the valuable, non-duplicate piece carries forward; the ReDoS fix on the email validator will be applied there too if master's validator needs it.

No action needed on this PR.


Generated by Claude Code

@izzywdev izzywdev closed this Aug 2, 2026
auto-merge was automatically disabled August 2, 2026 16:41

Pull request was closed

izzywdev added a commit that referenced this pull request Aug 2, 2026
…(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>
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