Repository navigation
feat(core): managementApi.proxy.scopes and a tenant-isolation suite on real Postgres (F6) - #685
Conversation
…proxy (ADR-056 §3.5) - `managementApi.proxy.scopes`, default ['individual'], lists the entity scopes the proxy accepts. Validated in resolveManagementApiConfig, which loadAppDefinition now calls: an unknown scope, 'global', a non-list, an empty list or a duplicate throws ManagementApiConfigError (MANAGEMENT_API_CONFIG_INVALID) at load, even with the proxy off. Added to the app-definition JSON schema. - ExecuteEntityProxyRequest checks, after the existing visibility 404, the description the v2 lists return (GetEntityModuleForUser.describe): access must grant use (`use` or `manage`; `view` is refused), and the scope must be in proxy.scopes. Both refusals are 403 PROXY_NOT_ALLOWED with details.reason 'access' or 'scope'; an entity not visible in the active organization stays 404. - grantsUse(access) in scope-visibility is the one rule for "use access". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
…ot a 500 Every module repository adapter throws `Entity <id> not found` for a missing row, but GetEntityModuleForUser.loadOwnedEntity mapped only malformed ids to 404 and rethrew the rest. So on v2 (GET/DELETE /entities/:id, its options and test-auth routes, and the proxy) an id that does not exist answered 500 while another tenant's answered 404: the status code said which ids exist. Found by the real-Postgres isolation suite; the unit tests mocked findEntityById as resolving null, which no adapter does. entity-not-found.js holds the error and its one predicate, as integration-not-found.js does for integrations; the adapters build it, the use case matches it, and the message is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
….6, F6) Real adopter-JWT auth (HS256), the real v2 router, use cases and Prisma repositories against PostgreSQL; the migrations are applied first and every table is truncated before each test. Only the upstream API behind the proxy is a stand-in. Users A and B, organizations X and Y: - A cannot list, read, proxy or bind B's entities (an org admin neither); a missing id is the same 404 as a foreign one. - A active in X cannot reach A's own Y entities, and can in Y. - Organization entities: 403 reason 'scope' under the default proxy.scopes, even for an admin; a plain member with use access proxies once the app opts in; never visible from another organization; scopes ['organization'] refuses individual entities. - individualAcross 'all': stored with no organization, usable by its owner in every organization, by nobody else; an integration using it belongs to the organization it was created in. Runs when FRIGG_TEST_POSTGRES_URL names a database it may wipe; otherwise skipped, and it says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
- docs/guides/entity-scopes.md: the proxy's three checks (404 visibility, 403 access, 403 scope), managementApi.proxy.scopes and its load-time validation, the missing-id 404, how to run the Postgres suite; F6 removed from Deferred. - packages/core/CLAUDE.md: Management API v2 notes; root CLAUDE.md: one line on proxy access and scopes. - types: ManagementApiConfig / ManagementApiProxyConfig on AppDefinition (scopes excludes 'global'), ManagementApiConfigError, which index.js now exports beside OwnershipConfigError. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
Every failure is in The base PR, #684 (job 116782953973), fails the same 33 suites by name with the same counts ( I haven't ported a fix, because none exists yet. The devtools failures were already there before this stack; I'll port a fix into this PR once one lands. Generated by Claude Code |
…/internal-mcp-06-proxy-scopes Brings in the stack fix round (deprecated module-definitions wrapper, DocumentDB ownership drains, v1 POST /api/entity owner and admin gate, membership-role fallback). Conflict in types/modules/index.d.ts: kept both ManagementApiConfigError (F6) and the deprecated getModulesDefinitionFromIntegrationClasses (F3). Core suite: 14 suites / 61 tests failing, the same names as 05c; 3534 passing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
|
Frigg 2.0.1 review (Stack A) — COMMENT onlyVerdict (would): changes needed Proxy scope gate ( Blocking
Should-fix
No formal approve / request-changes on this review. |
| const path = require('node:path'); | ||
| const { execFileSync } = require('node:child_process'); | ||
|
|
||
| const describePostgres = POSTGRES_URL ? describe : describe.skip; |
There was a problem hiding this comment.
blocking — tenant-isolation suite is skipped in default CI
describePostgres = POSTGRES_URL ? describe : describe.skip, and .github/workflows/frigg-ci.js.yml runs only npm run test:all with no Postgres service and no FRIGG_TEST_POSTGRES_URL. Core Jest uses --passWithNoTests, so a fully skipped suite is green.
Scenario: A regression in ownership / proxy scopes / cross-org 404s merges while CI stays green. The warning on lines 41–42 admits skip ≠ pass — please wire a CI job that sets FRIGG_TEST_POSTGRES_URL (and fails the job if this file would be skipped).



Summary
This is work package F6 of the internal-MCP build plan (ADR-056 §3.5). It is stacked on #684 (05c); retarget it to the next branch down once that merges.
managementApi.proxy.scopes['individual']. It is validated inresolveManagementApiConfig, whichloadAppDefinitionnow calls too.'global', a non-list, an empty list or a repeated value throwsManagementApiConfigError(MANAGEMENT_API_CONFIG_INVALID), even when the proxy is off. It never falls back to the default.scopesis added to the app-definition JSON schema inpackages/schemas.Proxy guard
ExecuteEntityProxyRequest.assertProxyable, not the handler.{ scope, access }fromGetEntityModuleForUser.describe, the resolver the v2 lists use, so there is one ownership rule, not two.PROXY_NOT_ALLOWED,details.reason: 'access'.proxy.scopes: 403PROXY_NOT_ALLOWED,details.reason: 'scope'.useormanage, in one helper,grantsUse. An entity's owner holdsmanage, so a strict=== 'use'would stop owners proxying their own entities.Fix found by the new suite
modules/repositories/entity-not-found.js, modelled onintegration-not-found.js, is thrown by all four module repositories and mapped to 404 byGetEntityModuleForUser. This affects every v2 entity route.findEntityById(get-module,module-factory,test-module-auth, …) behave as before.Tenant-isolation suite, real PostgreSQL
File:
packages/core/management-api/v2/tenant-isolation.postgres.test.js.Setup. It applies the real migrations and asserts it is talking to PostgreSQL. Requests go through real HS256 adopter-JWT auth, the real router, use cases and Prisma repositories. Only the API module's
_proxyRequestis stubbed, so no upstream is called. The users are A and B; the orgs are X and Y.What it proves:
scopeby default, even for an admin. Withorganizationinscopes, a plain member with use access can proxy through them. They are never visible from another org.individualAcross: 'all':organizationId;When it runs. Only when
FRIGG_TEST_POSTGRES_URLis set; it truncates all tables before each test. Without the variable it is skipped, with a printed warning, so the default unit run doesn't gain a suite that always fails.Docs and types
docs/guides/entity-scopes.md: F6 removed from the Deferred list.packages/core/CLAUDE.mdand the rootCLAUDE.md.ManagementApiConfig,ManagementApiProxyConfig(whose scopes exclude'global') andManagementApiConfigError, whichindex.jsnow exports.Test plan
packages/schemas: 207 of 207 pass.grantsUseaccepting onlymanage; a wider default scope list; ignoring the setting; no 404 for a missing row; no visibility check on load; ignoring'all'; dropping the org match.Notes for the reviewer
viewaccess yet: the org-admin view of members' entities is still deferred. The access refusal is therefore reachable only in unit tests.🤖 Generated with Claude Code
https://claude.ai/code/session_01HyrF4oqcTdK2AtvcrocxE6
Generated by Claude Code
Note
High Risk
Changes authentication-adjacent tenant boundaries for the entity proxy and normalizes missing-entity responses across v2 entity routes; misconfigured
proxy.scopesnow prevents app startup.Overview
Implements F6 (ADR-056 §3.5): configurable entity-proxy scopes, stricter proxy authorization, and a real-PostgreSQL tenant-isolation suite.
managementApi.proxy.scopesdefaults to['individual']and is resolved inresolveManagementApiConfig, whichloadAppDefinitionnow invokes so invalid values fail startup withManagementApiConfigError(MANAGEMENT_API_CONFIG_INVALID) even when the proxy is disabled. The app-definition JSON schema and TypeScript types document the setting;globalis never allowed.Entity proxy (
ExecuteEntityProxyRequest.assertProxyable) reuses the same{ scope, access }as v2 lists viaGetEntityModuleForUser.describe: invisible entities stay 404; visible callers without use or manage get 403PROXY_NOT_ALLOWED(reason: 'access'); scopes outside the allow-list get 403 (reason: 'scope'). Organization entities require an explicit opt-in toorganizationinscopes.Missing entity IDs no longer leak existence via 500: all module repositories throw through
entity-not-found.js, andGetEntityModuleForUsermaps that to the same 404 as cross-tenant access (affects v2 entity routes broadly).Adds
tenant-isolation.postgres.test.js(runs only whenFRIGG_TEST_POSTGRES_URLis set) for list/read/proxy/bind isolation, scope gating, andindividualAcross: 'all'. Docs and OpenAPI descriptions are updated accordingly.Reviewed by Cursor Bugbot for commit a13fdab. Bugbot is set up for automated code reviews on this repo. Configure here.
📦 Published PR as canary version:
2.0.0--canary.685.a13fdab.0✨ Test out this PR locally via:
npm install @friggframework/admin-scripts@2.0.0--canary.685.a13fdab.0 npm install @friggframework/core@2.0.0--canary.685.a13fdab.0 npm install @friggframework/devtools@2.0.0--canary.685.a13fdab.0 npm install @friggframework/eslint-config@2.0.0--canary.685.a13fdab.0 npm install @friggframework/prettier-config@2.0.0--canary.685.a13fdab.0 npm install @friggframework/schemas@2.0.0--canary.685.a13fdab.0 npm install @friggframework/serverless-plugin@2.0.0--canary.685.a13fdab.0 npm install @friggframework/test@2.0.0--canary.685.a13fdab.0 npm install @friggframework/ui@2.0.0--canary.685.a13fdab.0 # or yarn add @friggframework/admin-scripts@2.0.0--canary.685.a13fdab.0 yarn add @friggframework/core@2.0.0--canary.685.a13fdab.0 yarn add @friggframework/devtools@2.0.0--canary.685.a13fdab.0 yarn add @friggframework/eslint-config@2.0.0--canary.685.a13fdab.0 yarn add @friggframework/prettier-config@2.0.0--canary.685.a13fdab.0 yarn add @friggframework/schemas@2.0.0--canary.685.a13fdab.0 yarn add @friggframework/serverless-plugin@2.0.0--canary.685.a13fdab.0 yarn add @friggframework/test@2.0.0--canary.685.a13fdab.0 yarn add @friggframework/ui@2.0.0--canary.685.a13fdab.0