Make developer portal instance support a single organization - #2977
Conversation
📝 WalkthroughWalkthroughThe Developer Portal is changed from multi-organization routing to a single configured organization. Configuration, startup seeding, authentication, routes, APIs, assets, webhook workers, tests, and documentation now use the instance’s organization handle. ChangesSingle-organization Developer Portal
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant DeveloperPortal
participant orgContext
participant Database
Client->>DeveloperPortal: Request organization-scoped route
DeveloperPortal->>orgContext: Validate requested handle or claim
orgContext->>Database: Resolve configured organization
Database-->>orgContext: Return organization UUID
orgContext-->>DeveloperPortal: Allow pinned organization or reject
DeveloperPortal-->>Client: Serve response or return 403/404
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
portals/developer-portal/src/services/seederService.js (1)
43-76: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftFail startup when the pinned organization cannot be seeded.
seedDefaultOrg()logs non-recoverable failures and returns, whileonListening()catches any rejection and starts workers anyway. Also,onListening()runs only after the HTTP listener is bound, so requests can arrive before asynchronous seeding completes.
portals/developer-portal/src/services/seederService.js#L43-L76: rethrow non-recoverable lookup/create failures after logging.portals/developer-portal/src/server.js#L113-L120: seed successfully before binding the listener; do not catch-and-continue into worker startup.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/developer-portal/src/services/seederService.js` around lines 43 - 76, Update portals/developer-portal/src/services/seederService.js lines 43-76 in seedDefaultOrg() to rethrow non-NotFoundError lookup failures and organization creation failures after logging, so startup receives the rejection. Update portals/developer-portal/src/server.js lines 113-120 in onListening() to await successful seedDefaultOrg() completion before binding the HTTP listener and starting workers; remove the catch-and-continue behavior so seeding failure prevents startup.portals/developer-portal/docs/devportal-openapi-spec-v0.9.yaml (3)
161-166: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winAlign the GET organization error contract.
The description says missing or foreign organizations return 403, but a 404 response remains documented. Also, the shared
Forbiddenexample says “Write operations are disabled,” which is incorrect for this GET. Use one status contract and a generic or endpoint-specific 403 example.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/developer-portal/docs/devportal-openapi-spec-v0.9.yaml` around lines 161 - 166, Update the GET organization response definitions to use a single 403 contract: remove the documented 404 response and ensure the referenced Forbidden response example describes missing or foreign organizations rather than disabled write operations. Keep the existing 500 response unchanged.
84-91: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRemove impossible success responses from lifecycle operations.
POST /organizations,GET /organizations, andDELETE /organizationsnow state that they always return 405, but their response maps still advertise 201, 200, and 204 success paths. Remove the stale success examples, schemas, and headers, then regeneratedocs/rest-apis/devportal/organizations.md.Also applies to: 108-111, 187-190
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/developer-portal/docs/devportal-openapi-spec-v0.9.yaml` around lines 84 - 91, The lifecycle operations for POST, GET, and DELETE /organizations must no longer advertise success responses because these operations always return 405. Remove the 201, 200, and 204 response entries along with their associated examples, schemas, and headers from the OpenAPI response maps, then regenerate docs/rest-apis/devportal/organizations.md.
210-221: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winMark ignored
orgIdparameters as deprecated in OpenAPI.The prose says these parameters are deprecated, but neither parameter sets
deprecated: true. Add that property so generated clients and API tooling receive the deprecation signal.Also applies to: 639-649
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/developer-portal/docs/devportal-openapi-spec-v0.9.yaml` around lines 210 - 221, Add deprecated: true to both orgId query-parameter definitions in the OpenAPI specification, including the corresponding definition noted in the comment. Keep their existing optional string schemas and descriptions unchanged.docs/rest-apis/devportal/organizations.md (1)
3-22: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftKeep generated Markdown changes in the generator/source.
Both files are generated outputs. Apply the durable documentation changes to the generator/template or source specification, then regenerate each affected file.
docs/rest-apis/devportal/organizations.md#L3-L22: move the single-organization lifecycle changes to the generator/source.docs/rest-apis/devportal/organization-content.md#L28-L28: move the deprecatedorgIdcontract change to the generator/source.Based on learnings, Markdown files under
docs/rest-apis/devportal/are auto-generated and should be changed through their generator or source.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/rest-apis/devportal/organizations.md` around lines 3 - 22, The generated Markdown files must not be edited directly. Locate the generator or source specification producing the organization documentation and apply the single-organization lifecycle changes for docs/rest-apis/devportal/organizations.md lines 3-22, plus the deprecated orgId contract change for docs/rest-apis/devportal/organization-content.md line 28, then regenerate both affected files.Source: Learnings
portals/developer-portal/src/middlewares/authMiddleware.js (2)
280-291: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winLocal-auth session path fails open on a missing/empty organization claim.
resolveScopedOrgshort-circuits withreturn null(success) whenidentifieris falsy (Line 201). The IDP session path (Lines 316-320) explicitly checksif (!sessionOrgClaim)and rejects with 403 before callingresolveScopedOrg, but this platform-jwt session path callsresolveScopedOrg(req, orgHandle, ...)directly without that guard. IforgHandle(req.user[constants.ROLES.ORGANIZATION_CLAIM]) is ever empty on a session — e.g. a legacy/tampered session predating this claim, or a future refactor ofhandleLocalLogin— the request proceeds as authenticated withreq.orgIdleft unset, silently bypassing the pin check rather than failing closed like every other path in this function.🔒 Proposed fix to mirror the IDP session path's explicit guard
const orgHandle = req.user[constants.ROLES.ORGANIZATION_CLAIM]; + if (!orgHandle) { + const err = new Error('Missing organization claim in session'); + err.status = 403; + return next(err); + } const orgErr = await resolveScopedOrg(req, orgHandle, 'platform-jwt session');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/developer-portal/src/middlewares/authMiddleware.js` around lines 280 - 291, Guard the local-auth session branch in authResolver before calling resolveScopedOrg: reject with the same 403 missing-organization response used by the IDP session path when orgHandle is falsy. Only invoke resolveScopedOrg for a present organization claim, preserving the existing scoped-organization error handling.
336-404: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winReturn org-scoping failures directly as JSON on API routes.
authResolverruns on the devportal API router, but the new org mismatches propagate through the global Express error middleware rather than the API’s JSON error path. Use the standard JSON error helper/response here, or define a dedicated JSON error formatter before the generic handler, so bearer/API-key/mTLS org mismatches return JSON instead of an HTML error body.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/developer-portal/src/middlewares/authMiddleware.js` around lines 336 - 404, The authentication branches in authMiddleware must return organization-scoping failures through the API’s standard JSON error response path instead of passing them to the global Express handler. Update the resolveScopedOrg and resolvePortalOrg error handling in the bearer token, API key, and mTLS branches to use the existing JSON error helper or API response formatter, preserving each error’s status and message.Source: Learnings
🧹 Nitpick comments (2)
portals/developer-portal/src/dao/organizationDao.js (1)
108-123: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAdd regression coverage for exact-handle isolation.
Test mixed-case handle resolution and confirm that a handle matching another organization’s
display_nameoridp_ref_iddoes not fall through to that organization. This protects the single-organization pin consumed byorgContext.getOrgUuid().🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/developer-portal/src/dao/organizationDao.js` around lines 108 - 123, Add regression tests for getByHandle covering case-insensitive exact-handle resolution and isolation from organizations whose display_name or idp_ref_id matches the requested handle. Verify the pinned organization returned through orgContext.getOrgUuid() is the exact handle match and does not fall back to another organization.portals/developer-portal/src/services/adminService.js (1)
267-303: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueLGTM overall; minor duplicate lookup for idpRefId check.
The pin/handle/idpRefId guards are correct. Line 297's
orgDao.getByHandle(currentHandle)is a separate DB round trip that duplicates data available fromexistingOrgfetched shortly after (Line 307) in the transaction. Not urgent given the low-traffic single-organization update path, but could be folded into the transaction's existing fetch to save a query.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@portals/developer-portal/src/services/adminService.js` around lines 267 - 303, The idpRefId validation in the organization update flow performs a redundant lookup before the transaction fetches existingOrg. Move or consolidate this validation around the existingOrg fetch so it reuses that record’s idp_ref_id, while preserving the current rejection behavior for attempted changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/rest-apis/devportal/api-content.md`:
- Around line 281-291: Update the API content documentation around the orgId
parameter to state that anonymous image requests still require orgId for
compatibility, while it is ignored as an organization selector. Ensure the
documented behavior distinguishes anonymous image requests from authenticated
requests and other anonymous content types; alternatively, synchronize the
implementation and integration tests if changing this requirement.
In `@docs/rest-apis/devportal/README.md`:
- Around line 21-25: Update the REST-documentation generator or template that
produces the organization operation entries instead of editing
docs/rest-apis/devportal/README.md lines 21-25, then regenerate the
documentation. Also apply the orgId documentation change in the generator or
template responsible for docs/rest-apis/devportal/api-content.md lines 281-291
and regenerate that output; both generated files require no direct edits.
In `@portals/developer-portal/distribution/README.md`:
- Around line 115-116: Standardize the spelling of “organization” throughout the
README, updating the existing “organisation” usage near the earlier organization
configuration documentation to match the new entries. Preserve the meaning and
formatting of all configuration descriptions.
In `@portals/developer-portal/docs/administer/manage-organizations.md`:
- Around line 45-53: Update the later scripted-access example to stop calling
the unsupported GET /organizations endpoint. Replace that request with GET
/organizations/{handle}, preserving the example’s purpose and surrounding
instructions.
In `@portals/developer-portal/docs/devportal-openapi-spec-v0.9.yaml`:
- Around line 630-633: Update the API asset read operation’s OpenAPI security
definition to represent conditional authentication: allow anonymous access only
for IMAGE assets and require a session for all other content categories. Remove
the blanket security declaration and use separate operation/schema modeling
consistent with the existing asset-read contract.
In `@portals/developer-portal/docs/introduction/what-is-developer-portal.md`:
- Around line 31-36: Update the organization-claim sentence in the
“Organizations and views” section to use authentication-neutral wording, stating
that signing in requires an authenticated token whose organization claim matches
the organization; preserve the existing behavior description for other
organizations.
In `@portals/developer-portal/it/README.md`:
- Around line 146-149: Update the GET request in the “GET
/devportal/organizations/{handle} returns the configured organization” Cypress
example to use the documented organizations REST path, such as
/organizations/${Cypress.env('ORG_HANDLE')} or
/api/v0.9/organizations/${Cypress.env('ORG_HANDLE')}. Keep the existing response
assertions unchanged.
In `@portals/developer-portal/src/middlewares/orgGuard.js`:
- Around line 62-82: Update orgGuard to accept an optional sterile error
responder and invoke it for guard rejections while preserving existing next(err)
HTML behavior when no responder is supplied. In
portals/developer-portal/src/middlewares/orgGuard.js lines 62-82, retain the
existing status handling and pass failures to the responder; in
portals/developer-portal/src/routes/pages/mcpRegistryRoute.js line 39 and
portals/developer-portal/src/routes/pages/tryoutProxyRoute.js line 140, provide
responders that serialize guard 404/500 failures using each route’s existing
standardized JSON error shape.
---
Outside diff comments:
In `@docs/rest-apis/devportal/organizations.md`:
- Around line 3-22: The generated Markdown files must not be edited directly.
Locate the generator or source specification producing the organization
documentation and apply the single-organization lifecycle changes for
docs/rest-apis/devportal/organizations.md lines 3-22, plus the deprecated orgId
contract change for docs/rest-apis/devportal/organization-content.md line 28,
then regenerate both affected files.
In `@portals/developer-portal/docs/devportal-openapi-spec-v0.9.yaml`:
- Around line 161-166: Update the GET organization response definitions to use a
single 403 contract: remove the documented 404 response and ensure the
referenced Forbidden response example describes missing or foreign organizations
rather than disabled write operations. Keep the existing 500 response unchanged.
- Around line 84-91: The lifecycle operations for POST, GET, and DELETE
/organizations must no longer advertise success responses because these
operations always return 405. Remove the 201, 200, and 204 response entries
along with their associated examples, schemas, and headers from the OpenAPI
response maps, then regenerate docs/rest-apis/devportal/organizations.md.
- Around line 210-221: Add deprecated: true to both orgId query-parameter
definitions in the OpenAPI specification, including the corresponding definition
noted in the comment. Keep their existing optional string schemas and
descriptions unchanged.
In `@portals/developer-portal/src/middlewares/authMiddleware.js`:
- Around line 280-291: Guard the local-auth session branch in authResolver
before calling resolveScopedOrg: reject with the same 403 missing-organization
response used by the IDP session path when orgHandle is falsy. Only invoke
resolveScopedOrg for a present organization claim, preserving the existing
scoped-organization error handling.
- Around line 336-404: The authentication branches in authMiddleware must return
organization-scoping failures through the API’s standard JSON error response
path instead of passing them to the global Express handler. Update the
resolveScopedOrg and resolvePortalOrg error handling in the bearer token, API
key, and mTLS branches to use the existing JSON error helper or API response
formatter, preserving each error’s status and message.
In `@portals/developer-portal/src/services/seederService.js`:
- Around line 43-76: Update
portals/developer-portal/src/services/seederService.js lines 43-76 in
seedDefaultOrg() to rethrow non-NotFoundError lookup failures and organization
creation failures after logging, so startup receives the rejection. Update
portals/developer-portal/src/server.js lines 113-120 in onListening() to await
successful seedDefaultOrg() completion before binding the HTTP listener and
starting workers; remove the catch-and-continue behavior so seeding failure
prevents startup.
---
Nitpick comments:
In `@portals/developer-portal/src/dao/organizationDao.js`:
- Around line 108-123: Add regression tests for getByHandle covering
case-insensitive exact-handle resolution and isolation from organizations whose
display_name or idp_ref_id matches the requested handle. Verify the pinned
organization returned through orgContext.getOrgUuid() is the exact handle match
and does not fall back to another organization.
In `@portals/developer-portal/src/services/adminService.js`:
- Around line 267-303: The idpRefId validation in the organization update flow
performs a redundant lookup before the transaction fetches existingOrg. Move or
consolidate this validation around the existingOrg fetch so it reuses that
record’s idp_ref_id, while preserving the current rejection behavior for
attempted changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 846437ae-3ae2-4533-b5cb-d5ee0de149ca
📒 Files selected for processing (67)
cli/src/cmd/devportal/apply.gocli/src/internal/devportal/helpers.godistribution/all-in-one/devportal-config.tomldocs/rest-apis/devportal/README.mddocs/rest-apis/devportal/api-content.mddocs/rest-apis/devportal/organization-content.mddocs/rest-apis/devportal/organizations.mdkubernetes/helm/developer-portal-ui-helm-chart/templates/configmap.yamlkubernetes/helm/developer-portal-ui-helm-chart/values.yamlportals/developer-portal/README.mdportals/developer-portal/configs/config-template.tomlportals/developer-portal/configs/config.tomlportals/developer-portal/distribution/README.mdportals/developer-portal/docs/README.mdportals/developer-portal/docs/administer/manage-organizations.mdportals/developer-portal/docs/devportal-openapi-spec-v0.9.yamlportals/developer-portal/docs/introduction/concepts.mdportals/developer-portal/docs/introduction/quick-start.mdportals/developer-portal/docs/introduction/what-is-developer-portal.mdportals/developer-portal/it/README.mdportals/developer-portal/it/docker-compose.test.postgres.yamlportals/developer-portal/it/docker-compose.test.yamlportals/developer-portal/it/rest-api/auth/foreign-org-login.spec.jsportals/developer-portal/it/rest-api/organizations/organizations.spec.jsportals/developer-portal/it/rest-api/organizations/single-org-isolation.spec.jsportals/developer-portal/it/rest-api/support/fixtures.jsportals/developer-portal/it/rest-api/webhook-subscribers/webhook-subscribers.spec.jsportals/developer-portal/it/test-config.tomlportals/developer-portal/it/ui/cypress.config.jsportals/developer-portal/it/ui/cypress/e2e/000-smoke/001-smoke.cy.jsportals/developer-portal/it/ui/cypress/support/e2e.jsportals/developer-portal/src/app.jsportals/developer-portal/src/config/configDefaults.jsportals/developer-portal/src/config/configLoader.jsportals/developer-portal/src/controllers/authController.jsportals/developer-portal/src/controllers/orgContentController.jsportals/developer-portal/src/dao/eventDao.jsportals/developer-portal/src/dao/organizationDao.jsportals/developer-portal/src/middlewares/authMiddleware.jsportals/developer-portal/src/middlewares/orgGuard.jsportals/developer-portal/src/middlewares/passportConfig.jsportals/developer-portal/src/middlewares/registerPartials.jsportals/developer-portal/src/pages/default-home/page.hbsportals/developer-portal/src/routes/api/handlers/organizationsHandler.jsportals/developer-portal/src/routes/pages/apiContentRoute.jsportals/developer-portal/src/routes/pages/apiKeysOverviewRoute.jsportals/developer-portal/src/routes/pages/apiWorkflowsRoute.jsportals/developer-portal/src/routes/pages/applicationsContentRoute.jsportals/developer-portal/src/routes/pages/authRoute.jsportals/developer-portal/src/routes/pages/customPageRoute.jsportals/developer-portal/src/routes/pages/mcpRegistryRoute.jsportals/developer-portal/src/routes/pages/orgContentRoute.jsportals/developer-portal/src/routes/pages/settingsRoute.jsportals/developer-portal/src/routes/pages/subscriptionsContentRoute.jsportals/developer-portal/src/routes/pages/tryoutProxyRoute.jsportals/developer-portal/src/server.jsportals/developer-portal/src/services/adminService.jsportals/developer-portal/src/services/apiMetadataService.jsportals/developer-portal/src/services/devportalService.jsportals/developer-portal/src/services/seederService.jsportals/developer-portal/src/services/webhooks/deliveryWorker.jsportals/developer-portal/src/services/webhooks/dispatcher.jsportals/developer-portal/src/utils/orgContext.jsportals/developer-portal/src/utils/util.jstests/integration-e2e/devportal-config.tomltests/integration-e2e/docker-compose.yamltests/integration-e2e/steps_devportal_test.go
💤 Files with no reviewable changes (2)
- portals/developer-portal/src/pages/default-home/page.hbs
- portals/developer-portal/src/controllers/orgContentController.js
Purpose
$subject
Summary
organization.handle(required — the portal refuses to start without it, and seeds the organization if it doesn't exist).Approach
handleanddisplay_nameconfigs to config.toml