Skip to content

permissions: backend-driven permission catalog endpoint#1799

Merged
gugu merged 8 commits into
mainfrom
feature/permissions-catalog-endpoint
May 28, 2026
Merged

permissions: backend-driven permission catalog endpoint#1799
gugu merged 8 commits into
mainfrom
feature/permissions-catalog-endpoint

Conversation

@gugu

@gugu gugu commented May 22, 2026

Copy link
Copy Markdown
Contributor

Adds GET /permissions/available, derived from CEDAR_SCHEMA.RocketAdmin.actions so the catalog stays in sync with the Cedar action enum. Replaces the hardcoded POLICY_ACTION_GROUPS list in the frontend with a signal-backed httpResource on UsersService, and switches PolicyAction's needsTable/needsDashboard booleans to a single resource discriminator. New ActionEvent and Panel categories now appear in the Cedar policy editor.

Summary by CodeRabbit

  • New Features

    • Added GET /permissions/available and client-side consumption to list available permission actions with optional resource scopes.
    • New permission-display helpers for user-facing labels, short labels, group names, and icons.
  • Refactor

    • Permissions UI now builds display groups/actions dynamically from fetched data; scope handling updated with a dashboard-create special case and avoidance of duplicate simple actions.
  • Tests

    • New unit and end-to-end tests covering the permission catalog, display helpers, and auth behavior.

Review Change Stack

Adds GET /permissions/available, derived from CEDAR_SCHEMA.RocketAdmin.actions
so the catalog stays in sync with the Cedar action enum. Replaces the hardcoded
POLICY_ACTION_GROUPS list in the frontend with a signal-backed httpResource
on UsersService, and switches PolicyAction's needsTable/needsDashboard booleans
to a single resource discriminator. New ActionEvent and Panel categories now
appear in the Cedar policy editor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds GET /permissions/available that returns AvailablePermissionDs built from CEDAR_SCHEMA; protects the route with AuthMiddleware. Frontend fetches this catalog via UsersService and uses new permission-display helpers and updated types to render and group actions.

Changes

Permission Catalog System

Layer / File(s) Summary
Backend Response DTOs
backend/src/entities/permission/application/data-structures/available-permissions.ds.ts
Defines AvailablePermissionDs (value, optional resource) and AvailablePermissionsResponseDs (actions: AvailablePermissionDs[]).
Backend Permission Catalog Builder
backend/src/entities/permission/permission-catalog.builder.ts
Implements buildPermissionCatalog(), buildAction, and deriveResource to convert CEDAR_SCHEMA.RocketAdmin.actions into AvailablePermissionDs entries.
Backend API Endpoint and Authentication
backend/src/entities/permission/permission.controller.ts, backend/src/entities/permission/permission.module.ts
Adds @Get('permissions/available') handler returning { actions: buildPermissionCatalog() }, imports Get, and extends AuthMiddleware registration to include this route.
Backend E2E Tests
backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts
Boots Nest app and tests authenticated 200 response contains actions covering CedarAction enum (excluding synthesized wildcards) and unauthenticated 401.
Frontend Policy Item Types
frontend/src/app/lib/cedar-policy-items.ts
Adds PolicyActionResource union and updates PolicyAction to use optional resource instead of needsTable/needsDashboard; removes POLICY_ACTION_GROUPS/POLICY_ACTIONS.
Frontend Permission Display
frontend/src/app/lib/permission-display.ts, frontend/src/app/lib/permission-display.spec.ts
Adds groupNameForAction, actionLabel, actionShortLabel, actionIcon, PERMISSION_GROUP_ORDER, and tests for formatting, grouping, and icon selection.
Frontend Service Integration
frontend/src/app/services/users.service.ts
Adds HttpResourceRef for /permissions/available and computed availablePermissions and availablePermissionGroups grouped and ordered for UI consumption.
Frontend Component Migration
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts, ...component.html, ...component.spec.ts
Injects UsersService, builds displayActions/displayGroups from service data, uses permission-display helpers for labels/icons, derives scope from action resource, updates template selects, and adapts tests to signal-backed UsersService mocks.
Test harness & misc test updates
frontend/src/app/app.component.spec.ts, frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts
Refines setTimeout capture in app.component.spec and updates _restoreBetween to use UTC ISO slicing for date/time parts.

Sequence Diagram

sequenceDiagram
  participant Client
  participant PermissionController
  participant buildPermissionCatalog
  participant CEDAR_SCHEMA

  Client->>PermissionController: GET /permissions/available
  PermissionController->>buildPermissionCatalog: call
  buildPermissionCatalog->>CEDAR_SCHEMA: read RocketAdmin.actions
  CEDAR_SCHEMA-->>buildPermissionCatalog: actions list
  buildPermissionCatalog-->>PermissionController: AvailablePermissionDs[]
  PermissionController-->>Client: AvailablePermissionsResponseDs
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • lyubov-voloshko
  • Artuomka

Poem

🐰 In Cedar's grove the actions bloom,
Values listed, resources loom.
Backend counts them, frontend sings,
Grouped and labeled with tidy strings.
The rabbit hops and cheers, "Deploy with zoom!"

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main change: a backend endpoint for permission catalog that is derived from Cedar schema, replacing hardcoded frontend lists.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Security Check ✅ Passed Endpoint properly authenticated with JWT validation. Response limited to safe fields only. No injection attack vectors, no XSS vulnerabilities, DoS mitigated with timeout decorator.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/permissions-catalog-endpoint

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot requested a review from lyubov-voloshko May 22, 2026 19:53
@gugu gugu marked this pull request as draft May 22, 2026 19:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
backend/src/entities/permission/permission-catalog.builder.ts (1)

7-12: ⚡ Quick win

Use interfaces for object-shape declarations.

ActionMetadataOverride and SchemaShape are object shapes declared as type; guideline requires interface for object shapes.

Suggested fix
-type ActionMetadataOverride = {
+interface ActionMetadataOverride {
 	label?: string;
 	shortLabel?: string;
 	icon?: string;
 	resourceOverride?: 'none' | string;
-};
+}

-type SchemaShape = {
+interface SchemaShape {
 	RocketAdmin: {
 		actions: Record<string, { appliesTo: { principalTypes: Array<string>; resourceTypes: Array<string> } }>;
 	};
-};
+}

As per coding guidelines, "Use interfaces for object shapes and type for unions and primitives".

Also applies to: 177-181

🤖 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 `@backend/src/entities/permission/permission-catalog.builder.ts` around lines 7
- 12, Change the object-shape declarations from type aliases to interfaces:
replace the `type ActionMetadataOverride = { ... }` declaration with `interface
ActionMetadataOverride { ... }` and likewise convert the `SchemaShape` type
alias to an `interface SchemaShape { ... }`; keep the same property names and
optional markers (label, shortLabel, icon, resourceOverride, etc.) and preserve
exported/visibility modifiers and any JSDoc so all usages of
ActionMetadataOverride and SchemaShape continue to work.
backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts (1)

76-79: ⚡ Quick win

Assert panel:* wildcard to lock the full wildcard contract.

The catalog builder includes a Panel wildcard path; adding this assertion prevents silent regressions for panel permissions.

Suggested fix
 t.true(values.has('*'), 'catalog must include the General * wildcard');
 t.true(values.has('table:*'), 'catalog must include the table:* wildcard');
 t.true(values.has('dashboard:*'), 'catalog must include the dashboard:* wildcard');
+t.true(values.has('panel:*'), 'catalog must include the panel:* wildcard');
🤖 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
`@backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts`
around lines 76 - 79, The test currently checks that the catalog includes '*' ,
'table:*' and 'dashboard:*' but misses the panel wildcard; update the test in
non-saas-permission-catalog-e2e.test.ts to also assert that
values.has('panel:*') is true by adding an assertion using the existing
t.true(values.has('panel:*'), 'catalog must include the panel:* wildcard') so
the catalog builder's panel wildcard is covered and prevents regressions (look
for the variable values and the nearby t.true assertions to place this check).
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts (1)

67-68: ⚡ Quick win

Align private signal fields with frontend naming/modifier rules.

Use underscore-prefixed private members and mark computed signal fields as readonly.

✏️ Suggested cleanup
-	private availableActions = computed(() => this._users.availablePermissions());
-	private availableGroups = computed(() => this._users.availablePermissionGroups());
+	private readonly _availableActions = computed(() => this._users.availablePermissions());
+	private readonly _availableGroups = computed(() => this._users.availablePermissionGroups());

As per coding guidelines, frontend/**/*.ts: "Prefix private members with underscore (e.g., _privateMethod, _dataService)" and frontend/**/*.component.ts: "Signals for component state must be declared as protected or private readonly and initialized with signal() or computed()".

🤖 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
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`
around lines 67 - 68, The signals availableActions and availableGroups must
follow component naming/modifier rules: rename them to private readonly
_availableActions and private readonly _availableGroups and update their usages
accordingly; keep the computed initialization but change the declarations from
"private" to "private readonly" and prefix the identifiers with an underscore so
computed(() => this._users.availablePermissions()) and computed(() =>
this._users.availablePermissionGroups()) are assigned to _availableActions and
_availableGroups respectively.
🤖 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 `@backend/src/entities/permission/permission-catalog.builder.ts`:
- Around line 52-53: The code currently filters emitted groups to only those in
GROUP_ORDER, causing any new/unknown Cedar action groups to be dropped; update
the grouping logic (where GROUP_ORDER is used and where groups are emitted —
e.g., the code that builds groupedActions/resultGroups around lines referencing
GROUP_ORDER) so that after adding groups in GROUP_ORDER order you append any
remaining group keys from the actual grouping map (preserve their original names
and either original detection order or sorted lexicographically) rather than
omitting them; ensure the same change is applied to the other occurrence noted
(lines 83–86) so unknown groups are included in API output.
- Around line 62-175: Convert the standalone function declarations into const
arrow function expressions while preserving their names, signatures and return
types: replace appendAction, buildAction, buildWildcardAllAction,
buildPrefixWildcard, deriveResource, resolveGroupName, autoLabel,
autoShortLabel, and capitalize with const <name> = (<params>): <returnType> => {
... } forms; ensure existing type annotations (e.g., AvailablePermissionDs,
Array<string>, Map types) remain intact and behavior is unchanged (including use
of throw/return and side effects like map mutation in appendAction).

In
`@backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts`:
- Line 41: Remove the explicit call to app.getHttpServer().listen(0) in the test
setup; supertest directly uses app.getHttpServer(), so delete the listen
invocation (reference: the call to app.getHttpServer().listen(0)) to avoid
redundant server startup and teardown flakiness and let tests use the provided
server instance without awaiting or manually listening.

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`:
- Around line 281-283: _actionResource currently returns undefined when
_findAction(value) is missing, causing downstream flags
(needsTable/needsDashboard) to be false; modify _actionResource to return a
deterministic fallback resource when _findAction(value) is undefined by deriving
a resource object from the action string (for example parse the value to infer
resource type and id or return a stable default resource shape such as { type:
inferredType, id: inferredId } ), so code that relies on
PolicyAction['resource'] (and the needsTable/needsDashboard logic) always
receives a predictable resource; reference the _actionResource and _findAction
symbols and ensure the fallback logic covers missing catalog/availability states
instead of returning undefined.

---

Nitpick comments:
In `@backend/src/entities/permission/permission-catalog.builder.ts`:
- Around line 7-12: Change the object-shape declarations from type aliases to
interfaces: replace the `type ActionMetadataOverride = { ... }` declaration with
`interface ActionMetadataOverride { ... }` and likewise convert the
`SchemaShape` type alias to an `interface SchemaShape { ... }`; keep the same
property names and optional markers (label, shortLabel, icon, resourceOverride,
etc.) and preserve exported/visibility modifiers and any JSDoc so all usages of
ActionMetadataOverride and SchemaShape continue to work.

In
`@backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts`:
- Around line 76-79: The test currently checks that the catalog includes '*' ,
'table:*' and 'dashboard:*' but misses the panel wildcard; update the test in
non-saas-permission-catalog-e2e.test.ts to also assert that
values.has('panel:*') is true by adding an assertion using the existing
t.true(values.has('panel:*'), 'catalog must include the panel:* wildcard') so
the catalog builder's panel wildcard is covered and prevents regressions (look
for the variable values and the nearby t.true assertions to place this check).

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`:
- Around line 67-68: The signals availableActions and availableGroups must
follow component naming/modifier rules: rename them to private readonly
_availableActions and private readonly _availableGroups and update their usages
accordingly; keep the computed initialization but change the declarations from
"private" to "private readonly" and prefix the identifiers with an underscore so
computed(() => this._users.availablePermissions()) and computed(() =>
this._users.availablePermissionGroups()) are assigned to _availableActions and
_availableGroups respectively.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 605fe83c-45ad-45e0-810e-39fdda36adca

📥 Commits

Reviewing files that changed from the base of the PR and between f80c6a0 and 5ed31bc.

📒 Files selected for processing (9)
  • backend/src/entities/permission/application/data-structures/available-permissions.ds.ts
  • backend/src/entities/permission/permission-catalog.builder.ts
  • backend/src/entities/permission/permission.controller.ts
  • backend/src/entities/permission/permission.module.ts
  • backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts
  • frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts
  • frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts
  • frontend/src/app/lib/cedar-policy-items.ts
  • frontend/src/app/services/users.service.ts

Comment on lines +52 to +53
const GROUP_ORDER = ['General', 'Connection', 'Group', 'Table', 'ActionEvent', 'Dashboard', 'Panel'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't drop unknown Cedar action groups from the response.

The current return path only emits groups listed in GROUP_ORDER. If Cedar adds a new action prefix, actions get grouped but then omitted from API output, which breaks the “schema-driven sync” goal.

Suggested fix
- return GROUP_ORDER.filter((name) => grouped.has(name)).map((name) => ({
- 	group: name,
- 	actions: grouped.get(name)!,
- }));
+ const orderedKnownGroups = GROUP_ORDER.filter((name) => grouped.has(name));
+ const unknownGroups = Array.from(grouped.keys()).filter((name) => !GROUP_ORDER.includes(name)).sort();
+ const orderedGroups = [...orderedKnownGroups, ...unknownGroups];
+
+ return orderedGroups.map((name) => ({
+ 	group: name,
+ 	actions: grouped.get(name)!,
+ }));

Also applies to: 83-86

🤖 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 `@backend/src/entities/permission/permission-catalog.builder.ts` around lines
52 - 53, The code currently filters emitted groups to only those in GROUP_ORDER,
causing any new/unknown Cedar action groups to be dropped; update the grouping
logic (where GROUP_ORDER is used and where groups are emitted — e.g., the code
that builds groupedActions/resultGroups around lines referencing GROUP_ORDER) so
that after adding groups in GROUP_ORDER order you append any remaining group
keys from the actual grouping map (preserve their original names and either
original detection order or sorted lexicographically) rather than omitting them;
ensure the same change is applied to the other occurrence noted (lines 83–86) so
unknown groups are included in API output.

Comment on lines +62 to +175
export function buildPermissionCatalog(): Array<AvailablePermissionGroupDs> {
const schemaActions = (CEDAR_SCHEMA as SchemaShape).RocketAdmin.actions;
const grouped = new Map<string, Array<AvailablePermissionDs>>();

for (const [value, definition] of Object.entries(schemaActions)) {
const action = buildAction(value, definition.appliesTo.resourceTypes);
const groupName = resolveGroupName(value);
appendAction(grouped, groupName, action);
}

appendAction(grouped, 'General', buildWildcardAllAction());

for (const groupName of WILDCARD_GROUPS) {
const actions = grouped.get(groupName);
if (actions && actions.length > 1) {
const prefix = groupName.toLowerCase();
const resource = actions.find((a) => a.resource)?.resource;
actions.unshift(buildPrefixWildcard(prefix, groupName, resource));
}
}

return GROUP_ORDER.filter((name) => grouped.has(name)).map((name) => ({
group: name,
actions: grouped.get(name)!,
}));
}

function appendAction(
target: Map<string, Array<AvailablePermissionDs>>,
groupName: string,
action: AvailablePermissionDs,
): void {
const list = target.get(groupName);
if (list) {
list.push(action);
} else {
target.set(groupName, [action]);
}
}

function buildAction(value: string, resourceTypes: Array<string>): AvailablePermissionDs {
const meta = ACTION_DISPLAY_METADATA[value] ?? {};
const derivedResource = deriveResource(resourceTypes);
const resource =
meta.resourceOverride === NONE_RESOURCE_OVERRIDE ? undefined : (meta.resourceOverride ?? derivedResource);

const action: AvailablePermissionDs = {
value,
label: meta.label ?? autoLabel(value),
shortLabel: meta.shortLabel ?? autoShortLabel(value),
icon: meta.icon ?? 'help_outline',
};
if (resource) {
action.resource = resource;
}
return action;
}

function buildWildcardAllAction(): AvailablePermissionDs {
return {
value: '*',
label: 'Full access (all permissions)',
shortLabel: 'Full access',
icon: 'shield',
};
}

function buildPrefixWildcard(prefix: string, groupName: string, resource: string | undefined): AvailablePermissionDs {
const labels = WILDCARD_LABELS[groupName] ?? { label: `Full ${prefix} access`, shortLabel: 'Full access' };
const action: AvailablePermissionDs = {
value: `${prefix}:*`,
label: labels.label,
shortLabel: labels.shortLabel,
icon: 'shield',
};
if (resource) {
action.resource = resource;
}
return action;
}

function deriveResource(resourceTypes: Array<string>): string | undefined {
for (const type of resourceTypes) {
const candidate = type.charAt(0).toLowerCase() + type.slice(1);
if (UI_RENDERABLE_RESOURCES.has(candidate)) {
return candidate;
}
}
return undefined;
}

function resolveGroupName(actionValue: string): string {
const [prefix] = actionValue.split(':');
return PREFIX_TO_GROUP[prefix] ?? capitalize(prefix);
}

function autoLabel(value: string): string {
const [prefix, verb = ''] = value.split(':');
const groupName = PREFIX_TO_GROUP[prefix] ?? capitalize(prefix);
const verbWords = verb.split('-').map(capitalize).join(' ');
return verbWords ? `${groupName} ${verbWords.toLowerCase()}` : groupName;
}

function autoShortLabel(value: string): string {
const verb = value.split(':')[1] ?? value;
return verb
.split('-')
.map((part, index) => (index === 0 ? capitalize(part) : part))
.join(' ');
}

function capitalize(text: string): string {
return text ? text.charAt(0).toUpperCase() + text.slice(1) : text;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify function declarations in this file
rg -nP '^\s*function\s+\w+\s*\(' backend/src/entities/permission/permission-catalog.builder.ts

Repository: rocket-admin/rocketadmin

Length of output: 665


Use arrow functions for helper declarations in backend/src/entities/permission/permission-catalog.builder.ts.
This file still defines multiple helpers with function declarations (appendAction, buildAction, buildWildcardAllAction, buildPrefixWildcard, deriveResource, resolveGroupName, autoLabel, autoShortLabel, capitalize); convert them to const ... = (...) => {} to match the TS style guideline.

🤖 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 `@backend/src/entities/permission/permission-catalog.builder.ts` around lines
62 - 175, Convert the standalone function declarations into const arrow function
expressions while preserving their names, signatures and return types: replace
appendAction, buildAction, buildWildcardAllAction, buildPrefixWildcard,
deriveResource, resolveGroupName, autoLabel, autoShortLabel, and capitalize with
const <name> = (<params>): <returnType> => { ... } forms; ensure existing type
annotations (e.g., AvailablePermissionDs, Array<string>, Map types) remain
intact and behavior is unchanged (including use of throw/return and side effects
like map mutation in appendAction).

}),
);
await app.init();
app.getHttpServer().listen(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the explicit server listen in this E2E setup.

supertest uses app.getHttpServer() directly; listen(0) here is redundant and may create teardown flakiness because it isn’t awaited.

Suggested fix
- app.getHttpServer().listen(0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.getHttpServer().listen(0);
🤖 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
`@backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts`
at line 41, Remove the explicit call to app.getHttpServer().listen(0) in the
test setup; supertest directly uses app.getHttpServer(), so delete the listen
invocation (reference: the call to app.getHttpServer().listen(0)) to avoid
redundant server startup and teardown flakiness and let tests use the provided
server instance without awaiting or manually listening.

Comment on lines +281 to +283
private _actionResource(value: string): PolicyAction['resource'] | undefined {
return this._findAction(value)?.resource;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a deterministic fallback when action metadata is missing.

At Line 282, _actionResource relies only on catalog lookup. If an action is not present in availablePermissions (loading/error/stale catalog), needsTable / needsDashboard become false and save paths can emit policies without required tableName/dashboardId.

💡 Proposed fix
 private _actionResource(value: string): PolicyAction['resource'] | undefined {
-	return this._findAction(value)?.resource;
+	const resource = this._findAction(value)?.resource;
+	if (resource != null) return resource;
+	if (value.startsWith('table:')) return 'table';
+	if (value.startsWith('dashboard:')) return 'dashboard';
+	return undefined;
 }
🤖 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
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`
around lines 281 - 283, _actionResource currently returns undefined when
_findAction(value) is missing, causing downstream flags
(needsTable/needsDashboard) to be false; modify _actionResource to return a
deterministic fallback resource when _findAction(value) is undefined by deriving
a resource object from the action string (for example parse the value to infer
resource type and id or return a stable default resource shape such as { type:
inferredType, id: inferredId } ), so code that relies on
PolicyAction['resource'] (and the needsTable/needsDashboard logic) always
receives a predictable resource; reference the _actionResource and _findAction
symbols and ensure the fallback logic covers missing catalog/availability states
instead of returning undefined.

gugu and others added 2 commits May 22, 2026 20:29
Drop labels, short labels, icons, and synthesized wildcards from the backend
response. Each action is now { value, resource } only, where resource is the
first appliesTo.resourceTypes entry lowercased. Wildcards (*, table:*, etc.)
and display copy move to the frontend: a new lib/permission-display.ts derives
labels/icons algorithmically, and CedarPolicyListComponent synthesizes the
General and per-prefix wildcard entries at render time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend now returns { actions: [...] } instead of pre-grouped categories.
Group names ('Connection', 'ActionEvent', etc.) and group ordering live in
permission-display.ts on the frontend, where groupNameForAction(value)
derives them from the action prefix. UsersService computes
availablePermissionGroups from the flat list at read time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gugu gugu requested a review from Artuomka May 26, 2026 08:57
@gugu gugu marked this pull request as ready for review May 26, 2026 08:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts (1)

288-291: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add deterministic fallback when catalog lookup misses an action.

_scopeResource still returns undefined when displayActions() has no matching entry (Line 290), which can incorrectly disable required scope checks for table/dashboard actions.

Suggested fix
 	private _scopeResource(value: string): PolicyActionResource | undefined {
 		if (value === 'dashboard:create') return undefined;
-		return this._findAction(value)?.resource;
+		const resource = this._findAction(value)?.resource;
+		if (resource != null) return resource;
+		if (value.startsWith('table:')) return 'table';
+		if (value.startsWith('dashboard:')) return 'dashboard';
+		if (value.startsWith('panel:')) return 'panel';
+		if (value.startsWith('connection:')) return 'connection';
+		if (value.startsWith('group:')) return 'group';
+		if (value.startsWith('actionEvent:')) return 'actionEvent';
+		return undefined;
 	}
🤖 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
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`
around lines 288 - 291, _scopeResource currently returns undefined when
_findAction(value) misses, which can disable scope checks; update _scopeResource
to return a deterministic fallback by parsing the resource token from the action
string (split value on ':'), map that token to the PolicyActionResource enum or
a small lookup (e.g., "table" => PolicyActionResource.Table, "dashboard" =>
PolicyActionResource.Dashboard), and return that mapped value when
_findAction(value) is undefined (keeping the existing special-case for
'dashboard:create' if required); implement the mapping logic inside
_scopeResource so callers no longer receive undefined for unknown/missed
actions.
🧹 Nitpick comments (4)
backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts (1)

59-61: ⚡ Quick win

Use interfaces for the response/action object shapes in this test.

The inline object literal assertion here conflicts with the TS object-shape guideline.

♻️ Suggested minimal change
+interface AvailablePermissionAction {
+	value: string;
+	resource?: string;
+}
+
+interface AvailablePermissionsResponseBody {
+	actions: Array<AvailablePermissionAction>;
+}
+
-	const body = response.body as {
-		actions: Array<{ value: string; resource?: string }>;
-	};
+	const body = response.body as AvailablePermissionsResponseBody;

As per coding guidelines, "Use interfaces for object shapes and type for unions and primitives."

🤖 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
`@backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts`
around lines 59 - 61, Replace the inline type literal used for response.body
with named interfaces: declare an interface (e.g., PermissionAction with
properties value: string and optional resource?: string) and an interface (e.g.,
PermissionResponse with actions: PermissionAction[]), then cast response.body to
that interface (e.g., const body = response.body as PermissionResponse) so the
test uses named interfaces instead of an inline object literal; update any
references to actions accordingly in non-saas-permission-catalog-e2e.test.ts.
frontend/src/app/lib/permission-display.ts (1)

21-80: ⚡ Quick win

Convert utility function declarations to arrow functions for guideline compliance.

Lines 21–80 introduce several function declarations; this repo’s JS/TS guideline prefers arrow functions. Please convert these helpers to const fn = (...) => ... style for consistency.

As per coding guidelines, "Prefer arrow functions over function declarations".

🤖 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 `@frontend/src/app/lib/permission-display.ts` around lines 21 - 80, Convert the
top-level helper function declarations to arrow-function expressions to follow
repo guidelines: replace the function declarations for groupNameForAction,
actionLabel, actionShortLabel, actionIcon, verbInLabel, and capitalize with
const <name> = (params): returnType => { ... } (preserving exports like export
on groupNameForAction and actionLabel), keep existing parameter and return
types, preserve internal logic (including early returns and constants like
WILDCARD_ICON, VERB_TO_ICON, etc.), and ensure no behavioral changes or scope
differences occur when switching to arrow functions.
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts (1)

70-71: ⚡ Quick win

Align private computed signal fields with frontend naming/read-only conventions.

displayGroups and displayActions are private state signals; they should follow the private-member underscore convention and be readonly.

As per coding guidelines, "Prefix private members with underscore (e.g., _privateMethod, _dataService)" and "Signals for component state must be declared as protected or private readonly and initialized with signal() or computed()".

🤖 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
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`
around lines 70 - 71, The private computed signals displayGroups and
displayActions violate naming and immutability conventions; rename them to
_displayGroups and _displayActions and declare them as private readonly computed
signals (private readonly _displayGroups = computed(...), private readonly
_displayActions = computed(...)) and update all usages in this component to
reference _displayGroups() and _displayActions() (and where displayGroups() or
displayActions() are used, switch to the new names) so they follow the
underscore private-member convention and read-only signal rule; keep the
computation logic in _buildDisplayGroups() unchanged.
frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts (1)

270-272: ⚡ Quick win

Use a named testable type alias for protected signal access.

Instead of the inline intersection cast, define a dedicated type alias for test access and reuse it in the spec.

As per coding guidelines, "Use Partial<ServiceType> instead of as any for mock services in tests, and create testable type aliases for accessing protected signals".

🤖 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
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts`
around lines 270 - 272, Replace the inline intersection cast used for test
access with a named testable type alias: define a type alias (e.g.,
TestableCedarPolicyListComponent) that extends CedarPolicyListComponent and
declares the protected accessor (addActionGroups: () => PolicyActionGroup[]),
then cast component to that alias (const testable = component as
TestableCedarPolicyListComponent) instead of the inline intersection to access
the protected signal; this follows the guideline to create testable type aliases
for protected access and avoids ad-hoc casts.
🤖 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 `@frontend/src/app/services/users.service.ts`:
- Around line 65-68: The current return drops groups not listed in
PERMISSION_GROUP_ORDER; change it to include known groups in the defined order
first and then append any unknown groups from byGroup. Concretely, build the
orderedKnown array by filtering PERMISSION_GROUP_ORDER for names present in
byGroup and mapping to {group, actions} (same as current), build unknown array
by iterating Array.from(byGroup.keys()) and selecting keys not in
PERMISSION_GROUP_ORDER and mapping them similarly, and finally return
[...orderedKnown, ...unknown] so backend-driven groups are preserved after the
known ordered groups.

---

Duplicate comments:
In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`:
- Around line 288-291: _scopeResource currently returns undefined when
_findAction(value) misses, which can disable scope checks; update _scopeResource
to return a deterministic fallback by parsing the resource token from the action
string (split value on ':'), map that token to the PolicyActionResource enum or
a small lookup (e.g., "table" => PolicyActionResource.Table, "dashboard" =>
PolicyActionResource.Dashboard), and return that mapped value when
_findAction(value) is undefined (keeping the existing special-case for
'dashboard:create' if required); implement the mapping logic inside
_scopeResource so callers no longer receive undefined for unknown/missed
actions.

---

Nitpick comments:
In
`@backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts`:
- Around line 59-61: Replace the inline type literal used for response.body with
named interfaces: declare an interface (e.g., PermissionAction with properties
value: string and optional resource?: string) and an interface (e.g.,
PermissionResponse with actions: PermissionAction[]), then cast response.body to
that interface (e.g., const body = response.body as PermissionResponse) so the
test uses named interfaces instead of an inline object literal; update any
references to actions accordingly in non-saas-permission-catalog-e2e.test.ts.

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts`:
- Around line 270-272: Replace the inline intersection cast used for test access
with a named testable type alias: define a type alias (e.g.,
TestableCedarPolicyListComponent) that extends CedarPolicyListComponent and
declares the protected accessor (addActionGroups: () => PolicyActionGroup[]),
then cast component to that alias (const testable = component as
TestableCedarPolicyListComponent) instead of the inline intersection to access
the protected signal; this follows the guideline to create testable type aliases
for protected access and avoids ad-hoc casts.

In
`@frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts`:
- Around line 70-71: The private computed signals displayGroups and
displayActions violate naming and immutability conventions; rename them to
_displayGroups and _displayActions and declare them as private readonly computed
signals (private readonly _displayGroups = computed(...), private readonly
_displayActions = computed(...)) and update all usages in this component to
reference _displayGroups() and _displayActions() (and where displayGroups() or
displayActions() are used, switch to the new names) so they follow the
underscore private-member convention and read-only signal rule; keep the
computation logic in _buildDisplayGroups() unchanged.

In `@frontend/src/app/lib/permission-display.ts`:
- Around line 21-80: Convert the top-level helper function declarations to
arrow-function expressions to follow repo guidelines: replace the function
declarations for groupNameForAction, actionLabel, actionShortLabel, actionIcon,
verbInLabel, and capitalize with const <name> = (params): returnType => { ... }
(preserving exports like export on groupNameForAction and actionLabel), keep
existing parameter and return types, preserve internal logic (including early
returns and constants like WILDCARD_ICON, VERB_TO_ICON, etc.), and ensure no
behavioral changes or scope differences occur when switching to arrow functions.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 364edf3d-6d70-4467-bfbc-6dd19b6c1159

📥 Commits

Reviewing files that changed from the base of the PR and between 5ed31bc and e4d2057.

📒 Files selected for processing (11)
  • backend/src/entities/permission/application/data-structures/available-permissions.ds.ts
  • backend/src/entities/permission/permission-catalog.builder.ts
  • backend/src/entities/permission/permission.controller.ts
  • backend/test/ava-tests/non-saas-tests/non-saas-permission-catalog-e2e.test.ts
  • frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.html
  • frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts
  • frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts
  • frontend/src/app/lib/cedar-policy-items.ts
  • frontend/src/app/lib/permission-display.spec.ts
  • frontend/src/app/lib/permission-display.ts
  • frontend/src/app/services/users.service.ts

Comment thread frontend/src/app/services/users.service.ts
gugu and others added 3 commits May 26, 2026 22:15
…-test pollution

AppComponent never unsubscribes from authCast, so prior tests' component
instances re-fire on cast.next and queue their own setTimeouts, overwriting
the captured callback. Gate the capture behind a flag set by THIS app's
mocked initializeUserSession so we only grab the timeout from this instance's
restoration branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tTimeout

date-time filter restored bounds via local-tz format() but emitted them with a
Z suffix, breaking round-trip outside UTC; switch to slicing toISOString() so
the test passes regardless of host timezone.

app.component login-flow test schedules a 10s logout setTimeout that fires
during whenStable() now that a transitively-injected httpResource keeps the
zone busy — mock setTimeout for the duration of the test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gugu gugu enabled auto-merge (squash) May 28, 2026 07:42
@coderabbitai coderabbitai Bot requested a review from Artuomka May 28, 2026 07:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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
`@frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts`:
- Around line 151-158: The BETWEEN restore uses new Date(...).toISOString()
(UTC) which mismatches the single-value restore that uses date-fns/format()
(local time); update DateTimeFilterComponent._restoreBetween to format value[0]
and value[1] using the same local-time formatter as ngOnInit (e.g. date-fns
format with 'yyyy-MM-dd' for date and 'HH:mm:ss' for time) and assign those
results to lowerDate/lowerTime and upperDate/upperTime so both restore paths
render consistent wall-clock values.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a6b08ade-2d43-47d6-96f8-438fb8e48069

📥 Commits

Reviewing files that changed from the base of the PR and between f86d75d and e5808d1.

📒 Files selected for processing (2)
  • frontend/src/app/app.component.spec.ts
  • frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/app/app.component.spec.ts

Comment on lines +151 to +158
const iso = new Date(value[0]).toISOString();
this.lowerDate = iso.slice(0, 10);
this.lowerTime = iso.slice(11, 19);
}
if (value[1]) {
const upper = new Date(value[1]);
this.upperDate = format(upper, 'yyyy-MM-dd');
this.upperTime = format(upper, 'HH:mm:ss');
const iso = new Date(value[1]).toISOString();
this.upperDate = iso.slice(0, 10);
this.upperTime = iso.slice(11, 19);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In JavaScript, does Date.prototype.toISOString()always serialize in UTC, and doesdate-fns@4 format(date, 'yyyy-MM-dd')/format(date, 'HH:mm:ss') use the local timezone by default?

💡 Result:

Yes—Date.prototype.toISOString always serializes using UTC (it appends the “Z” UTC designator, per the documented return format). [1] For date-fns v4: date-fns/format formats using the system time zone by default (i.e., it relies on the underlying JavaScript Date, which uses the environment’s local time zone). date-fns only lets you control time zone explicitly via its v4 time zone support (e.g., the “in” context option with @date-fns/tz). [2][3] So, specifically: - date-fns format(date, 'yyyy-MM-dd') and format(date, 'HH:mm:ss') default to local/system time zone behavior for the provided Date instance (not UTC). - If you need UTC or another time zone, you must use date-fns’s time zone mechanisms (e.g., @date-fns/utc / @date-fns/tz and the relevant options/context). [2][3]

Citations:


Fix timezone inconsistency between BETWEEN restore and single-value restore
DateTimeFilterComponent._restoreBetween derives lowerDate/lowerTime and upperDate/upperTime via new Date(...).toISOString() (UTC), while the single-value restore path in ngOnInit uses date-fns/format() (local time). The same persisted timestamp can therefore render with different wall-clock values depending on whether the filter is between vs gte/lte.

Suggested local fix
 	if (value[0]) {
-		const iso = new Date(value[0]).toISOString();
-		this.lowerDate = iso.slice(0, 10);
-		this.lowerTime = iso.slice(11, 19);
+		const lower = new Date(value[0]);
+		this.lowerDate = format(lower, 'yyyy-MM-dd');
+		this.lowerTime = format(lower, 'HH:mm:ss');
 	}
 	if (value[1]) {
-		const iso = new Date(value[1]).toISOString();
-		this.upperDate = iso.slice(0, 10);
-		this.upperTime = iso.slice(11, 19);
+		const upper = new Date(value[1]);
+		this.upperDate = format(upper, 'yyyy-MM-dd');
+		this.upperTime = format(upper, 'HH:mm:ss');
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const iso = new Date(value[0]).toISOString();
this.lowerDate = iso.slice(0, 10);
this.lowerTime = iso.slice(11, 19);
}
if (value[1]) {
const upper = new Date(value[1]);
this.upperDate = format(upper, 'yyyy-MM-dd');
this.upperTime = format(upper, 'HH:mm:ss');
const iso = new Date(value[1]).toISOString();
this.upperDate = iso.slice(0, 10);
this.upperTime = iso.slice(11, 19);
if (value[0]) {
const lower = new Date(value[0]);
this.lowerDate = format(lower, 'yyyy-MM-dd');
this.lowerTime = format(lower, 'HH:mm:ss');
}
if (value[1]) {
const upper = new Date(value[1]);
this.upperDate = format(upper, 'yyyy-MM-dd');
this.upperTime = format(upper, 'HH:mm:ss');
}
🤖 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
`@frontend/src/app/components/ui-components/filter-fields/date-time/date-time.component.ts`
around lines 151 - 158, The BETWEEN restore uses new Date(...).toISOString()
(UTC) which mismatches the single-value restore that uses date-fns/format()
(local time); update DateTimeFilterComponent._restoreBetween to format value[0]
and value[1] using the same local-time formatter as ngOnInit (e.g. date-fns
format with 'yyyy-MM-dd' for date and 'HH:mm:ss' for time) and assign those
results to lowerDate/lowerTime and upperDate/upperTime so both restore paths
render consistent wall-clock values.

@gugu gugu merged commit a7b6915 into main May 28, 2026
18 of 19 checks passed
@gugu gugu deleted the feature/permissions-catalog-endpoint branch May 28, 2026 07:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants