Skip to content

Allow declaring content scope dimensions at runtime - #6115

Open
fraxachun wants to merge 5 commits into
claude/user-permissions-content-scope-summaryfrom
claude/user-permissions-content-scope-dimensions
Open

Allow declaring content scope dimensions at runtime#6115
fraxachun wants to merge 5 commits into
claude/user-permissions-content-scope-summaryfrom
claude/user-permissions-content-scope-dimensions

Conversation

@fraxachun

@fraxachun fraxachun commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Problem

Content scope dimensions were only known implicitly from the keys of the availableContentScopes values. An optional dimension that is not part of availableContentScopes (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 availableContentScopeDimensions option to the UserPermissionsModule to declare the content scope dimensions (with optional labels) at runtime. When omitted, the dimensions are derived from the keys of availableContentScopes as 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 of availableContentScopes.

A content scope for a dimension that is not part of availableContentScopes may now hold any value (including the "*" wildcard).

Removing checkContentScopes

Content scopes are no longer validated against availableContentScopes (the checkContentScopes method is removed), because:

  • It was not the authorization boundary. Access is enforced per request by isAllowed / isScopeWithin, which compares the requested resource's scope against the user's granted scopes and never consults availableContentScopes. A stale content scope (e.g. one pointing at a removed domain) matches no real resource, so it cannot cause an over-grant.
  • It gave no durable guarantee. availableContentScopes can 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.
  • It became incoherent. With dimensions that can live outside availableContentScopes and hold any value (including "*"), availableContentScopes no longer describes the full space of valid content scopes, so matching against it would reject legitimate scopes.

Example

UserPermissionsModule.forRootAsync({
    useFactory: () => ({
        availableContentScopes: [ ... ],
        availableContentScopeDimensions: [{ name: "domain", label: "Domain (Website)" }, { name: "language" }, { name: "product" }],
        // ...
    }),
    // ...
});

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):

  1. Support wildcard values for content scope dimensions → main (Support wildcard values for content scope dimensions #6114)
  2. Summarize a user's content scopes per dimension in the users list → Support wildcard values for content scope dimensions #6114 (Remove the Permissions and Scopes columns from the user permissions users list #6135)
  3. Allow declaring content scope dimensions at runtime (this PR) → Remove the Permissions and Scopes columns from the user permissions users list #6135 (Allow declaring content scope dimensions at runtime #6115)
  4. Rework content scopes management in the user permissions panel → Allow declaring content scope dimensions at runtime #6115 (Rework content scopes management in the user permissions panel #6116)

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 availableContentScopeDimensions option (with optional labels) and a ContentScopeDimension type to declare dimensions at runtime.
  • Expose dimensions via userPermissionsAvailableContentScopeDimensions query and implement getAvailableContentScopeDimensions() in UserPermissionsService.
  • 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.

Comment thread packages/api/cms-api/src/user-permissions/user-permissions.service.ts Outdated
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from f6ed12b to ed0458c Compare August 6, 2026 07:12

Copy link
Copy Markdown
Contributor Author

Re-linking this PR to the working session (stacked: #6114#6115#6116).


Generated by Claude Code

@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from ed0458c to b056db0 Compare August 6, 2026 08:56
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from b056db0 to de1a9cf Compare August 6, 2026 11:08
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from de1a9cf to e9308f0 Compare August 6, 2026 13:02
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from e9308f0 to fce743f Compare August 6, 2026 13:16
@fraxachun
fraxachun changed the base branch from claude/user-permissions-content-scope-wildcard to claude/user-permissions-content-scope-summary August 6, 2026 13:28
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from fce743f to 4704cdb Compare August 6, 2026 13:46
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from 4704cdb to fd7379d Compare August 7, 2026 11:31
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from 47750b6 to 38ac720 Compare August 11, 2026 06:45
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from 38ac720 to c21e9fa Compare August 11, 2026 06:58
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from c21e9fa to b4f3cc6 Compare August 11, 2026 08:21
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from b4f3cc6 to c969534 Compare August 11, 2026 12:26
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from 6d4934f to c539b06 Compare August 13, 2026 08:15
@fraxachun
fraxachun marked this pull request as ready for review August 13, 2026 08:59
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from c539b06 to 86d4b5a Compare August 13, 2026 09:25
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from 86d4b5a to 7fed471 Compare August 13, 2026 10:23
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch from 7fed471 to 47897d6 Compare August 13, 2026 14:22
claude added 5 commits August 14, 2026 11:29
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.
@fraxachun
fraxachun force-pushed the claude/user-permissions-content-scope-dimensions branch 2 times, most recently from fbd39d4 to 42976e4 Compare August 14, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants