fix(rbac): harden policy validation and error handling across RBAC write paths#9121
fix(rbac): harden policy validation and error handling across RBAC write paths#9121PatAKnight wants to merge 17 commits into
Conversation
Changed Packages
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Hardens RBAC policy ingestion and write paths to prevent Casbin CSV poisoning and improve error surfacing/typing across REST, config-driven defaults, and file-based loaders.
Changes:
- Adds stricter validation for policy fields, plugin-id payloads, conditional policy shapes, and configurable validation limits.
- Improves error typing (InputError/NotFoundError/NotAllowedError) and rethrows enforcer loadPolicy failures after audit logging.
- Makes CSV/YAML file ingestion more resilient (skip malformed CSV lines; enforce YAML size/doc limits).
Reviewed changes
Copilot reviewed 30 out of 31 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.ts | Rejects unsafe permission values containing double quotes. |
| workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.test.ts | Adds regression test for quoted permission rejection. |
| workspaces/rbac/plugins/rbac-backend/src/validation/plugin-validation.ts | Tightens plugin ID registration validation (bounds/dup/length) and uses InputError. |
| workspaces/rbac/plugins/rbac-backend/src/validation/plugin-validation.test.ts | Adds tests for new plugin ID validation rules. |
| workspaces/rbac/plugins/rbac-backend/src/validation/condition-validation.ts | Adds distinct-action checks and configurable limits for conditional criteria complexity. |
| workspaces/rbac/plugins/rbac-backend/src/validation/condition-validation.test.ts | Adds tests for conditional validation limits and duplicates. |
| workspaces/rbac/plugins/rbac-backend/src/service/policies-rest-api.ts | Centralizes body parsing validation; improves missing-role handling (404/403). |
| workspaces/rbac/plugins/rbac-backend/src/service/policies-rest-api.test.ts | Updates REST API tests for new error messages and 404 missing-role behavior. |
| workspaces/rbac/plugins/rbac-backend/src/service/permission-definition-routes.test.ts | Adds REST tests for plugin-id validation edge cases. |
| workspaces/rbac/plugins/rbac-backend/src/service/enforcer-delegate.ts | Rethrows loadPolicy failures after audit logging. |
| workspaces/rbac/plugins/rbac-backend/src/service/enforcer-delegate.test.ts | Adds test ensuring loadPolicy failures are rethrown and audited. |
| workspaces/rbac/plugins/rbac-backend/src/policies/permission-policy.ts | Wires config-based conditional validation and YAML file ingestion limits; renames watcher import. |
| workspaces/rbac/plugins/rbac-backend/src/helper.ts | Aligns owner-filter semantics with rule behavior; handles default roles and unknown keys. |
| workspaces/rbac/plugins/rbac-backend/src/helper.test.ts | Updates owner-key tests and adds coverage for default roles/unknown keys. |
| workspaces/rbac/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.ts | Renames watcher; enforces YAML max bytes/docs and validates each document. |
| workspaces/rbac/plugins/rbac-backend/src/file-permissions/yaml-conditional-file-watcher.test.ts | Adds tests for YAML size/document limits. |
| workspaces/rbac/plugins/rbac-backend/src/file-permissions/lowercase-file-adapter.ts | Skips malformed Casbin policy lines with warnings instead of aborting. |
| workspaces/rbac/plugins/rbac-backend/src/file-permissions/csv-file-watcher.ts | Parses CSV line-by-line; skips malformed lines with warnings; passes logger to adapter. |
| workspaces/rbac/plugins/rbac-backend/src/file-permissions/csv-file-watcher.test.ts | Adds fixture-based test asserting malformed CSV lines are skipped and logged. |
| workspaces/rbac/plugins/rbac-backend/src/default-permissions/default-permissions.ts | Validates default permissions against new policy validation rules; uses InputError. |
| workspaces/rbac/plugins/rbac-backend/src/default-permissions/default-permissions.test.ts | Updates tests to assert InputError and new invalid-policy behavior. |
| workspaces/rbac/plugins/rbac-backend/src/database/role-metadata.ts | Prevents duplicate inclusion of cached default role metadata. |
| workspaces/rbac/plugins/rbac-backend/src/admin-permissions/admin-creation.ts | Validates admin entity refs and throws InputError on invalid config. |
| workspaces/rbac/plugins/rbac-backend/src/admin-permissions/admin-creation.test.ts | Adds test for invalid admin entity reference handling. |
| workspaces/rbac/plugins/rbac-backend/docs/conditions.md | Documents distinct action requirement for permissionMapping. |
| workspaces/rbac/plugins/rbac-backend/docs/apis.md | Documents permission must not contain double quotes. |
| workspaces/rbac/plugins/rbac-backend/config.d.ts | Adds config schema for conditional validation and YAML file limits. |
| workspaces/rbac/plugins/rbac-backend/fixtures/data/invalid-csv/malformed-unquoted-quote.csv | Adds malformed CSV fixture to validate skip behavior. |
| workspaces/rbac/plugins/rbac-backend/README.md | Documents new hardening behavior and validation limits configuration. |
| workspaces/rbac/.gitignore | Ignores local CSV policy files. |
| workspaces/rbac/.changeset/olive-shirts-nail.md | Adds changeset describing hardening and compatibility impact. |
Comments suppressed due to low confidence (1)
workspaces/rbac/plugins/rbac-backend/src/validation/policies-validation.ts:1
- The helper name/comment suggests it detects unescaped quotes, but the implementation rejects any double quote. Please rename to reflect the actual behavior (e.g.,
permissionContainsDoubleQuote) and/or adjust the comment to avoid implying support for escaped quotes.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let conditionValidationLimits: ConditionValidationLimits = { | ||
| ...DEFAULT_CONDITION_VALIDATION_LIMITS, | ||
| }; | ||
|
|
||
| export function configureConditionValidationLimits( | ||
| limits: Partial<ConditionValidationLimits>, | ||
| ): void { | ||
| const nextLimits = { | ||
| ...conditionValidationLimits, | ||
| ...limits, | ||
| }; | ||
|
|
||
| assertPositiveInteger(nextLimits.maxConditionDepth, 'maxConditionDepth'); | ||
| assertPositiveInteger( | ||
| nextLimits.maxConditionNodeCount, | ||
| 'maxConditionNodeCount', | ||
| ); | ||
| assertPositiveInteger(nextLimits.maxCriteriaItems, 'maxCriteriaItems'); | ||
|
|
||
| conditionValidationLimits = nextLimits; | ||
| } | ||
|
|
||
| function getConditionValidationLimits(): ConditionValidationLimits { | ||
| return conditionValidationLimits; | ||
| } |
| | Json field | Description | Type | | ||
| | ----------------- | ------------------------------------------------------------------------------------------------------- | ------------ | | ||
| | result | Always has the value "CONDITIONAL" | String | | ||
| | roleEntityRef | String entity reference to the RBAC role ('role:default/dev') | String | | ||
| | pluginId | Corresponding plugin ID (e.g., "catalog") | String | | ||
| | permissionMapping | Distinct Backstage permission actions only (`create`, `read`, `update`, `delete`, `use`); no duplicates | String array | | ||
| | resourceType | Resource type provided by the plugin (e.g., "catalog-entity") | String | | ||
| | conditions | Condition JSON with parameters or array parameters joined by criteria | JSON | |
There was a problem hiding this comment.
Skipping this as I believe this comment was made in error
| expectAuditorLog([ | ||
| { | ||
| event: { eventId: ConditionEvents.CONDITIONAL_POLICIES_FILE_CHANGE }, | ||
| fail: { | ||
| error: new Error( | ||
| 'conditional policies file exceeds maximum size of 1048576 bytes', | ||
| ), | ||
| }, | ||
| }, | ||
| ]); |
| private static readConditionalFileLimits( | ||
| configApi: ConfigApi, | ||
| ): Partial<ConditionalPoliciesFileLimits> { | ||
| const maxBytes = configApi.getOptionalNumber( | ||
| 'permission.rbac.validation.conditionalPoliciesFile.maxBytes', | ||
| ); | ||
| const maxDocuments = configApi.getOptionalNumber( | ||
| 'permission.rbac.validation.conditionalPoliciesFile.maxDocuments', | ||
| ); | ||
|
|
||
| return { | ||
| maxBytes: maxBytes ?? DEFAULT_CONDITIONAL_POLICIES_FILE_LIMITS.maxBytes, | ||
| maxDocuments: | ||
| maxDocuments ?? DEFAULT_CONDITIONAL_POLICIES_FILE_LIMITS.maxDocuments, | ||
| }; | ||
| } |
| const isPluginEnabled = env.config.getOptionalBoolean('permission.enabled'); | ||
| const conditionValidationLimits = resolveConditionValidationLimits( | ||
| readConditionValidationLimitsFromConfig(env.config), | ||
| ); |
A low priority item from GitHub copilot that I think was relevant, should have been fixed in: 9406e14 |
| handler(lowercasedLine, model); | ||
| } catch (error) { | ||
| const message = error instanceof Error ? error.message : String(error); | ||
| this.logger?.warn( |
There was a problem hiding this comment.
Maybe we don't need to warn here and CSVFileWatcher.parse() can be single source of warnings?
Because when testing, I am getting twice the same error logs:
2026-05-15T14:24:35.733Z permission warn Skipping invalid CSV policy line 5 in /Users/dzemanov/Documents/Configs/rbac/permissions.csv: Invalid Opening Quote: a quote is found on field 2 at line 1, value is "catalog-entity"; line=p, role:default/catalog-different, catalog-entity"fuzz, read, allow
2026-05-15T14:24:35.734Z permission warn Skipping invalid policy line 5 in /Users/dzemanov/Documents/Configs/rbac/permissions.csv: Invalid Opening Quote: a quote is found on field 2 at line 1, value is "catalog-entity"; line=p, role:default/catalog-different, catalog-entity"fuzz, read, allow
Or we can change it to debug?
There was a problem hiding this comment.
Lines can still be fed to Helper.loadPolicyLine when we rebuild the temp enforcer from the file. Casbin may throw on lines that didn’t fail csv-parse, so I ended up going with debug log to avoid the duplicating warn with while still leaving something for deep troubleshooting.
dzemanov
left a comment
There was a problem hiding this comment.
Thank you @PatAKnight, works great except one small comment.
Tested:
- Conditional policies file exceeds maximum of 1 YAML documents
- Conditional criteria exceeds maximum of 2 nodes
- Unable to convert config value for key 'permission.rbac.validation.conditionalPoliciesFile.maxDocuments' in 'app-config.local.yaml' to a number
- comments in files
- duplicates: Found condition with conflicted permission action '["delete"]'. Role could have multiple conditions for the same resource type
Invalid policy / role lines from csv file, like "INVALID LINE", are silently skipped. This can be handled in a separate PR if we want to take a look at it.
…silient policy CSV loading Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
… API errors Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
…th depth and payload limits Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
…rable Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
…ility notes Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
…ule globals Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
…ror identity checks Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
…s positive integers Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
…tion Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
… disabled Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
aff454c to
b4f979f
Compare
| if (!metadata) { | ||
| if (policy.entityReference?.startsWith('role:default')) { | ||
| throw new NotFoundError( | ||
| `Corresponding role ${policy.entityReference} was not found`, | ||
| ); | ||
| } | ||
| throw new NotAllowedError(); // 403 | ||
| } |
| export function validatePermissionDependentPlugin( | ||
| plugin: PermissionDependentPluginList, | ||
| ) { | ||
| if (!plugin.ids) { | ||
| throw new Error( | ||
| throw new InputError( | ||
| `'ids' must be specified in the permission dependent plugin`, | ||
| ); | ||
| } |
I think it is fine to leave for now as it’s pre-existing and doesn’t currently break RBAC. I do agree on a follow-up PR to warn on junk rows that parse as CSV but aren’t real policies though. |
…parseEntityRef Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
…r invalid Assisted-by: Cursor AI Signed-off-by: Patrick Knight <pknight@redhat.com>
dzemanov
left a comment
There was a problem hiding this comment.
Thank you for the changes!
Hey, I just made a Pull Request!
This PR hardens RBAC policy ingestion and write paths to prevent Casbin CSV poisoning and reduce confusing failure modes. It rejects unsafe permission values before persistence, rethrows loadPolicy() failures so root causes surface correctly, strengthens API validation/error typing (400/404 vs generic 500), and adds bounded limits for conditional payloads, plugin-id registration payloads, and YAML conditional file ingestion (with config-based tuning).
It also improves resilience for CSV file-based policy loading by skipping malformed lines with warnings instead of aborting the full load, and updates docs/config schema/changelog to describe new validation behavior and compatibility implications.
✔️ Checklist
Signed-off-byline in the message. (more info)