Allow declaring content scope dimensions at runtime - #6115
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a runtime representation for content scope dimensions (e.g. domain, language, product) by introducing a new availableContentScopeDimensions option on UserPermissionsModule and exposing it via a new userPermissionsAvailableContentScopeDimensions GraphQL query. This enables optional/non-enumerable dimensions (too many values to list in availableContentScopes) to be declared and surfaced in the admin UI while keeping backward compatibility by deriving dimensions from availableContentScopes when not configured.
Changes:
- Add
availableContentScopeDimensionsoption (with optional labels) and aContentScopeDimensiontype to declare dimensions at runtime. - Expose dimensions via
userPermissionsAvailableContentScopeDimensionsquery and implementgetAvailableContentScopeDimensions()inUserPermissionsService. - Update content scope validation/filtering to only validate the enumerable part (dimensions present in
availableContentScopes), allowing free values (including*) for declared non-enumerable dimensions; add/adjust tests and demo wiring.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/api/cms-api/src/user-permissions/user-permissions.types.ts | Adds ContentScopeDimension config type and availableContentScopeDimensions option. |
| packages/api/cms-api/src/user-permissions/user-permissions.service.ts | Implements dimension retrieval and updates scope validation/matching for non-enumerable dimensions. |
| packages/api/cms-api/src/user-permissions/user-permissions.service.spec.ts | Adds unit tests for dimension derivation and updated validation semantics. |
| packages/api/cms-api/src/user-permissions/user-content-scopes.resolver.ts | Adds GraphQL query for available scope dimensions. |
| packages/api/cms-api/src/user-permissions/user-content-scopes.resolver.spec.ts | Tests the new resolver query and updates content scope test data. |
| packages/api/cms-api/src/user-permissions/dto/content-scope.ts | Adds GraphQL ContentScopeDimension object type. |
| packages/api/cms-api/src/index.ts | Exports the new ContentScopeDimension type from the public API. |
| packages/api/cms-api/schema.gql | Updates schema with the new type and query. |
| demo/api/src/content-scope/content-scope.interface.ts | Extends demo content scope with optional product dimension. |
| demo/api/src/auth/access-control.service.ts | Uses wildcard for product in demo content scopes. |
| demo/api/src/auth/access-control.service.spec.ts | Updates expectations for added product wildcard dimension. |
| demo/api/src/app.module.ts | Declares product via availableContentScopeDimensions in demo module config. |
| demo/api/schema.gql | Updates demo schema with the new type and query. |
| .changeset/content-scope-dimensions.md | Adds changeset describing the new runtime dimension capability. |
f6ed12b to
ed0458c
Compare
|
Re-linking this PR to the working session (stacked: #6114 → #6115 → #6116). Generated by Claude Code |
ed0458c to
b056db0
Compare
b056db0 to
de1a9cf
Compare
de1a9cf to
e9308f0
Compare
e9308f0 to
fce743f
Compare
fce743f to
4704cdb
Compare
4704cdb to
fd7379d
Compare
47750b6 to
38ac720
Compare
38ac720 to
c21e9fa
Compare
c21e9fa to
b4f3cc6
Compare
b4f3cc6 to
c969534
Compare
6d4934f to
c539b06
Compare
c539b06 to
86d4b5a
Compare
86d4b5a to
7fed471
Compare
7fed471 to
47897d6
Compare
Add an optional availableContentScopeDimensions option to the UserPermissionsModule to declare content scope dimensions (with optional labels) at runtime, exposed via the userPermissionsAvailableContentScopeDimensions query. When omitted, the dimensions are derived from the keys of availableContentScopes. A content scope for a dimension that is not part of availableContentScopes may hold any value; the content scope check only validates the enumerable dimensions and rejects unknown dimensions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVwZJtKmfrQq2VZmPAB76j
…heckContentScopes checkContentScopes fetched the available content scopes itself and then called getAvailableContentScopeDimensions, which - when availableContentScopeDimensions is not configured - resolved the available content scopes again to derive the dimension names. With an async availableContentScopes factory this ran the factory twice per check. Pass the already-resolved scopes into getAvailableContentScopeDimensions so they are resolved only once. Also demonstrate dimension labels in the demo.
…tentScopes With content scope dimensions that can be declared at runtime and hold values outside availableContentScopes, the available content scopes no longer describe the full space of valid content scopes. Validating or filtering assigned content scopes against them was therefore incoherent, and it only ever ran at write time (checkContentScopes) or as cosmetic read-time filtering - neither is the authorization boundary. Access is enforced per request by isAllowed, which compares the requested scope against the user's granted scopes and does not depend on availableContentScopes. Remove checkContentScopes and its two callers, the read-time filtering in filterContentScopesForUser, and the redundant intersection in the userPermissionsContentScopes resolver (which also dropped scopes carrying a non-enumerable dimension).
…-dimension wildcard filterContentScopesForUser had two ways to represent a user with access to all content scopes: a per-dimension wildcard or the expanded concrete scopes, selected via representAllContentScopesAsWildcard, and it took the available content scopes as a parameter to build either. Drop both parameters and always produce the wildcard, derived from the declared content scope dimensions (getAvailableContentScopeDimensions). Building the wildcard from the declared dimensions also fixes coverage of non-enumerable dimensions: previously the wildcard only spanned the dimensions present in availableContentScopes, so an admin's "all scopes" representation omitted e.g. a product dimension and could not be recognized as covering (or impersonating) a user who had a scope on that dimension. The userPermissionsContentScopes query now returns the wildcard for such users instead of the expanded concrete scopes; expanding it for display is handled in the admin.
…dmin panel layer The userPermissionsAvailableContentScopeDimensions query and the humanized dimension labels are only needed by the content scopes admin panel, so move them to that layer. This layer keeps the availableContentScopeDimensions option and uses the declared dimension names to represent all-content-scopes access as a per-dimension wildcard; it no longer exposes them via GraphQL or humanizes their labels, so camelCaseToHumanReadable stays local to getAvailableContentScopes.
fbd39d4 to
42976e4
Compare
Problem
Content scope dimensions were only known implicitly from the keys of the
availableContentScopesvalues. An optional dimension that is not part ofavailableContentScopes(e.g. one with too many values to enumerate) therefore had no runtime representation at all — it could neither be declared nor used with arbitrary values.Solution
Add an optional
availableContentScopeDimensionsoption to theUserPermissionsModuleto declare the content scope dimensions (with optional labels) at runtime. When omitted, the dimensions are derived from the keys ofavailableContentScopesas before, so existing apps are unaffected. The declared dimensions are used to represent access to all content scopes as a per-dimension wildcard ({ domain: "*", ... }), which therefore also covers dimensions that are not part ofavailableContentScopes.A content scope for a dimension that is not part of
availableContentScopesmay now hold any value (including the"*"wildcard).Removing
checkContentScopesContent scopes are no longer validated against
availableContentScopes(thecheckContentScopesmethod is removed), because:isAllowed/isScopeWithin, which compares the requested resource's scope against the user's granted scopes and never consultsavailableContentScopes. A stale content scope (e.g. one pointing at a removed domain) matches no real resource, so it cannot cause an over-grant.availableContentScopescan be resolved dynamically (a factory), so validating at write time says nothing about validity later: a scope valid when saved can become invalid after a config change, and vice versa.availableContentScopesand hold any value (including"*"),availableContentScopesno longer describes the full space of valid content scopes, so matching against it would reject legitimate scopes.Example
Stacked pull request
This is layer 3 of 4 of a stacked pull request. Review and merge bottom-up (each layer targets the one above it):
main(Support wildcard values for content scope dimensions #6114)🤖 Generated with Claude Code