Access Control
Access Control provides enterprise-grade permissions management through integration with Nextcloud RBAC (Role-Based Access Control) and Keycloak.
Overview
The access control system integrates with:
- ADFS (Active Directory Federation Services) for user and group management via Keycloak
- Nextcloud RBAC for role-based permissions
- FCS (Federal Cloud Services) compliance requirements
- Verwerkingen registers for process tracking
Permission Levels
Access can be controlled at multiple levels:
- Register level - Control access to entire registers
- Schema level - Manage permissions for specific register/schema combinations
- Object level - Set permissions on individual objects, make one object
private, and invite a principal to it (see Object Sharing) - Property level - Fine-grained, conditional control over specific object properties (see Property Authorization)
Permission Types
Permissions are granted through:
-
User Rights
- CRUD (Create, Read, Update, Delete) operations
- Inherited from ADFS groups via Keycloak
- Role-based access control through Nextcloud
-
Contract Rights
- Application-level permissions
- Process-specific authorizations
- Compliance with FCS requirements
- Integration with verwerkingen registers
Implementation
Access control is implemented through:
-
User Authentication
- Direct integration with Keycloak for identity management
- ADFS synchronization for user and group information
- Single Sign-On (SSO) capabilities
-
Permission Management
- CRUD-level permissions for all system entities
- Hierarchical permission inheritance
- Fine-grained access control at multiple levels
-
Process Integration
- Compliance with FCS guidelines
- Integration with verwerkingen registers for process tracking
- Application-specific permission contracts
Technical Implementation
Architecture Overview
Authorization Flow
Schema Authorization Configuration
The action vocabulary: closed by default, extensible by declaration
An authorization block may name create, read, update or delete, plus
any action the schema declares for itself:
"x-openregister-action": {
"sendMail": {
"name": "Send mail",
"description": "Send a message as the acting user."
}
}
With that declaration in the schema's configuration, "sendMail": ["staff"]
is a legal authorization rule. Without it, the import fails, naming the
offending action and listing what is allowed.
That refusal is the point, not a safety rail bolted onto it. An open vocabulary
would let a typo — raed for read — save cleanly as a permission that is
never granted and never errors: a rule that appears to protect something and
protects nothing. A schema that declares a permission nobody can satisfy is the
bug being prevented.
Declaring an action grants and enforces nothing. It defines a name that an
authorization block, an event listener and the grantable-rights index can all
refer to; the app still enforces its own operation. A declaration with no
description is ignored, because an action that reaches a permission matrix
without telling the person granting it what they are agreeing to is worse than
no entry at all.
The mcp scope: what may be OFFERED, never what is HELD
mcp sits beside public, authenticated and admin, and means: this action,
on this schema, may be offered to an agent.
It does not mean an agent has it. RBAC resolves through Nextcloud groups,
which are per user — two agents owned by one person are indistinguishable to
it. Whether a specific agent holds a right stays resolved in Hermiq against that
agent's own grants, where the request-and-approve flow already lives. The
relationship is: mcp bounds the menu, Hermiq picks from it.
It never grants. An administrator can create a real Nextcloud group called
mcp; without a guard, its members would inherit every action any schema ever
documented as an agent surface. The evaluator refuses the match outright, in both
the bare-string and {"group": "mcp"} rule forms, in both rule interpreters.
⚠️ Writing mcp into authorization DOES opt the schema into authorization.
A block is fail-closed once non-empty, so {"read": ["mcp"]} denies every action
it does not list — including read itself, since the scope satisfies nobody. If
you want to record an agent surface without changing enforcement, use the
x-openregister-mcp dialect instead. That is where the surface belongs, and
where every live declaration on this instance already is.
That rule was chosen after the alternative was measured and rejected. Treating an offers-only block as absent — so the annotation changed nothing — sounds like the right reading of a descriptive token, but "absent" means default-open: the schema became readable by every authenticated user and by anonymous callers. An author documenting an agent surface would have published the schema instead of restricting it. Between a fail-safe and a fail-open reading of an ambiguous block, the block gets the fail-safe one: a denial is loud and quickly fixed, a silent grant to anonymous is a breach.
An explicitly empty rule list ("read": []") means grant this action to
nobody and is preserved for the same reason — it is the strictest rule the
grammar can express, and reading it as "no rule" would flip it to default-open.
Authorization Exception System
RBAC Query Filtering Process
Database Schema
Authorization Exception Table: oc_openregister_authorization_exceptions
| Column | Type | Description |
|---|---|---|
id | INTEGER | Primary key |
uuid | VARCHAR(36) | Unique identifier |
type | VARCHAR(20) | Exception type: inclusion or exclusion |
subject_type | VARCHAR(20) | Subject type: user or group |
subject_id | VARCHAR(255) | User ID or Group ID |
schema_uuid | VARCHAR(36) | Schema UUID (nullable for global) |
register_uuid | VARCHAR(36) | Register UUID (nullable) |
organization_uuid | VARCHAR(36) | Organisation UUID (nullable) |
action | VARCHAR(20) | CRUD action: create, read, update, delete |
priority | INTEGER | Priority for resolution (higher = more important) |
active | BOOLEAN | Whether exception is active |
description | TEXT | Human-readable description |
created_by | VARCHAR(255) | User who created the exception |
created_at | DATETIME | Creation timestamp |
updated_at | DATETIME | Last update timestamp |
Schema Authorization Field:
- Stored in
oc_openregister_schemas.authorization(JSON) - Format:
{
"create": ["admin", "editors"],
"read": ["admin", "editors", "viewers", "public"],
"update": ["admin", "editors"],
"delete": ["admin"],
"inheritFromPublic": true
}
The optional inheritFromPublic boolean controls whether authenticated users
qualify for public rules on this schema. It defaults to true (the
pre-change behaviour). See Disabling public-group inheritance for
authenticated users
below.
Object Authorization Field:
- Stored in
oc_openregister_objects.authorization(JSON) - Inherits from schema but can be overridden per-object
- Same format as schema authorization
Permission Resolution Algorithm
Code Examples
Schema Authorization Configuration
// Setting authorization on a schema
$schema->setAuthorization([
'create' => ['admin', 'editors'],
'read' => ['admin', 'editors', 'viewers', 'public'],
'update' => ['admin', 'editors'],
'delete' => ['admin']
]);
// Checking if a user has permission
$hasPermission = $schema->hasPermission('read', $userGroups);
Creating Authorization Exceptions
use OCA\OpenRegister\Db\AuthorizationException;
// Create an inclusion exception (grant extra permission)
$inclusion = new AuthorizationException();
$inclusion->setType(AuthorizationException::TYPE_INCLUSION);
$inclusion->setSubjectType(AuthorizationException::SUBJECT_TYPE_USER);
$inclusion->setSubjectId('user123');
$inclusion->setSchemaUuid($schemaUuid);
$inclusion->setAction(AuthorizationException::ACTION_UPDATE);
$inclusion->setPriority(10);
$inclusion->setActive(true);
$inclusion->setDescription('Allow user123 to update objects in this schema');
// Create an exclusion exception (deny permission)
$exclusion = new AuthorizationException();
$exclusion->setType(AuthorizationException::TYPE_EXCLUSION);
$exclusion->setSubjectType(AuthorizationException::SUBJECT_TYPE_GROUP);
$exclusion->setSubjectId('restricted_group');
$exclusion->setRegisterUuid($registerUuid);
$exclusion->setAction(AuthorizationException::ACTION_DELETE);
$exclusion->setPriority(20);
$exclusion->setActive(true);
$exclusion->setDescription('Prevent restricted_group from deleting objects in this register');
// Check if an exception matches criteria
$matches = $exception->matches(
subjectType: 'user',
subjectId: 'user123',
action: 'update',
schemaUuid: $schemaUuid
);
RBAC Query Filtering (MagicMapper)
use OCA\OpenRegister\Service\MagicMapperHandlers\MagicRbacHandler;
// Apply RBAC filters to a dynamic table query
$rbacHandler->applyRbacFilters(
qb: $queryBuilder,
register: $register,
schema: $schema,
tableAlias: 't',
userId: $currentUserId,
rbac: true
);
// Check if current user is admin
$isAdmin = $rbacHandler->isCurrentUserAdmin();
// Get current user's groups
$userGroups = $rbacHandler->getCurrentUserGroups();
Object-Level Authorization
// Get object authorization (inherits from schema if not set)
$objectAuth = $object->getAuthorization();
// Override schema authorization for specific object
$object->setAuthorization([
'read' => ['admin', 'special_viewers'],
'update' => ['admin']
]);
Conditional rules work identically at every level
Schema-level, object-level, and property-level authorization blocks all accept the same conditional rule grammar. A rule of the form { "group": "...", "match": { ... } } evaluates the same way whether it sits on a schema, an object, or a single property, and whether it is enforced at list time (SQL WHERE via MagicRbacHandler), at single-object fetch time (PermissionHandler::hasPermission), or during property filtering (PropertyRbacHandler).
All three enforcement points route conditional match evaluation through the shared ConditionMatcher service. The operator set and dynamic-variable set listed below therefore apply uniformly:
- Operators:
$eq,$ne,$gt,$gte,$lt,$lte,$in,$nin,$exists. - Dynamic variables resolved at evaluation time:
$organisation/$activeOrganisation,$userId/$user,$now.
A schema authored with { "read": [{ "group": "public", "match": { "publishDate": { "$lte": "$now" } } }] } returns the same object set from GET /api/objects/{register}/{schema} (list) and GET /api/objects/{register}/{schema}/{id} (find). List-vs-find drift caused by differing grammar is no longer possible.
Disabling public-group inheritance for authenticated users (inheritFromPublic)
By default, authenticated users qualify for any rule that targets the public
group — they inherit at least the rights of an anonymous visitor. This is
convenient for most schemas, but it gets in the way of two patterns:
- Privacy-strict schemas where authentication is meant to be a strict gate, not a superset of public access. For example: a schema where the public group can only see redacted/anonymised rows via a conditional rule, but logged-in users should be channelled through a different curated view rather than seeing the same redacted set.
- Tiered visibility flows where a public catalogue uses a date-windowed
match(e.g.publishedAt $lte $now) and a separate authenticated curated view uses its own group rule. With public inheritance on, the authenticated view leaks the public catalogue rows.
The optional inheritFromPublic boolean on the authorization block of a
schema or register lets a tenant opt out of the inherit-from-public
behaviour. When false, authenticated users no longer qualify for public
rules on that schema/register; they must qualify via their own group
memberships. Anonymous users are unaffected — the flag does not change
what an unauthenticated visitor sees.
Cascade
The effective value is resolved per schema, walking the cascade until the first explicitly-set value is found:
- The schema's own
authorization.inheritFromPublic. - The parent register's
authorization.inheritFromPublic. - The tenant-wide IAppConfig key
openregister.rbac.inherit_from_public_default(read viaIAppConfig::getValueBool, which acceptstrue/false/"true"/"false"/"1"/"0"/1/0at the storage layer). - Hard-coded
true(preserves pre-change behaviour).
Strict-boolean check at schema and register levels. Steps 1 and 2 require
the stored value to be a literal true or false — anything else (string
forms, integers, mistyped JSON) is rejected as "unset" and the cascade
falls through, with a warning logged. This closes a foot-gun where a seed
write or migration that bypasses the schema validator could store the
string "false" and silently invert the gate ((bool) "false" is true).
Always store real JSON booleans on schema/register authorization blocks.
The IAppConfig layer keeps its broader tolerance because the occ /
operator surface intentionally accepts those forms.
null at any level is treated as "unset" — the cascade falls through to
the next level. The first explicit boolean wins; a schema that sets the
flag overrides its register and the tenant default.
The tenant default can be flipped from the OpenRegister settings UI under
RBAC Configuration → Authenticated users inherit public group rights
(default), or via occ config:app:set openregister rbac.inherit_from_public_default --value=false --type=boolean.
Four-state matrix
For a schema with read: [{ "group": "public", "match": <m> }]:
| User | inheritFromPublic | Result |
|---|---|---|
| anonymous | true (default) | granted when <m> matches (pre-change behaviour) |
| anonymous | false | granted when <m> matches — anonymous users are unaffected |
| authenticated | true (default) | granted when <m> matches (authenticated user inherits public) |
| authenticated | false | denied unless the user qualifies via another rule (own group / owner / admin) |
Owner-shortcut and admin-bypass paths are unaffected by the flag — an
object's owner and any user in the admin group always have access
regardless of inheritFromPublic.
The PHP-side per-object check (PermissionHandler::hasPermission) and the
SQL-side listing filter (MagicRbacHandler::applyRbacFilters /
buildRbacConditionsSql) both honour the flag identically; per-object
checks and listing membership cannot drift.
Worked example: a publication-style schema with a curated authenticated view
A publication schema needs to be visible to anonymous visitors only after
its publishedAt timestamp, but logged-in editors should see the curated
in-progress queue (which is a different rule) instead of the public-time
window. Authenticated users without editors membership should see
nothing — they should not inherit the public window.
{
"authorization": {
"inheritFromPublic": false,
"read": [
{ "group": "public", "match": { "publishedAt": { "$lte": "$now" } } },
{ "group": "editors", "match": { "status": { "$in": ["draft", "review"] } } }
]
}
}
Behaviour:
- An anonymous visitor sees rows where
publishedAt <= now(public match applies; the flag does not affect anonymous users). - An
editorsmember sees rows wherestatusisdraftorreview(their group rule applies). They do NOT inherit the public time-window becauseinheritFromPublicisfalse. - A logged-in user who is not in
editorssees nothing on this schema. They have no qualifying rule, and the flag prevents them from falling back to the public match. - An owner of a row sees it via the owner shortcut, regardless of the flag.
If the same schema were authored with inheritFromPublic: true (or the
field omitted), the third case would change: the non-editors logged-in
user would inherit the public window and see the same rows the anonymous
visitor sees.
When to reach for 'authenticated' instead of 'public'
inheritFromPublic governs the public group only. The simple-string
rule 'authenticated' is a separate construct — it grants access to any
logged-in user, independent of the flag. If you want "any logged-in user,
no group filtering, regardless of inheritFromPublic", use
{ "read": ["authenticated"] } rather than relying on public-inheritance.
Property-Level Authorization
In addition to the schema- and object-level rules above, individual properties can carry their own authorization block with conditional rules. This is covered in depth in Property Authorization; this section is a short map into that feature.
Property-level rules support the same grammar as schema-level (listed above):
- Group checks — the same groups used at schema level (including
"public"). matchconditions on the object, with the operators listed above.- Dynamic variables — the same set listed above.
Example — a publishedAt field that is only visible after its own timestamp:
{
"publishedAt": {
"type": "string",
"format": "date-time",
"authorization": {
"read": [
{ "group": "public", "match": { "publishedAt": { "$lte": "$now" } } }
]
}
}
}
Example — internal notes scoped to the owning organisation:
{
"interneAantekening": {
"type": "string",
"authorization": {
"read": [{ "group": "public", "match": { "_organisation": "$organisation" } }],
"update": [{ "group": "editors", "match": { "_organisation": "$organisation" } }]
}
}
}
Reads that fail property authorization strip the field from the response; writes that fail produce a validation error rather than a silent drop. Schemas with no authorization blocks on any property skip property-RBAC evaluation entirely.
See Property Authorization for the full operator catalogue, variable reference, edge cases, and more examples.
RBAC Configuration
RBAC can be configured in Nextcloud app settings:
{
"enabled": true,
"adminOverride": true,
"inheritFromPublicDefault": true
}
enabled: Master switch for RBAC systemadminOverride: Allow users in 'admin' group to bypass all RBAC checksinheritFromPublicDefault: Tenant-wide default for the schema-levelinheritFromPublicflag. Whentrue(default), authenticated users qualify forpublicrules on any schema that does not explicitly set the flag. Whenfalse, authenticated users must qualify via their own group memberships unless a schema or register opts back in. Persisted to the IAppConfig keyopenregister.rbac.inherit_from_public_default. See Disabling public-group inheritance for authenticated users.
Performance Optimizations
-
Lazy Group Loading
- User groups are only fetched when RBAC is enabled
- Results are cached within the request lifecycle
-
Admin Fast Path
- Admin users bypass permission checks entirely when override is enabled
- Reduces database queries for administrative operations
-
Query-Level Filtering
- RBAC filters are applied at the database query level
- Prevents loading unauthorized objects into memory
-
Authorization Config Caching
- Schema authorization configs are cached with schema entities
- Reduces redundant JSON parsing
-
Exception Priority Indexing
- Database index on
priorityfield for fast exception sorting - Composite index on
(subject_type, subject_id, action, active)for fast matching
- Database index on
Integration Points
1. ObjectEntityMapper
- Applies RBAC filters to all object queries via
findAll(),find(),count() - Respects
rbacparameter (default: true)
2. MagicMapper (Dynamic Tables)
- Uses
MagicRbacHandlerfor schema-specific table filtering - Consistent security across schema-generated tables
3. Solr Search
- RBAC filtering applied via
applyAdditionalFilters()inGuzzleSolrService - Currently logs RBAC application but full implementation pending
4. API Controllers
- RBAC checks in
ObjectsController,RegistersController,SchemasController - Validates permissions before CRUD operations
Best Practices
-
Use Schema-Level Authorization
- Define authorization at the schema level for consistency
- Only override at object level when necessary
-
Leverage Group-Based Permissions
- Use Nextcloud groups for role management
- Avoid user-specific permissions unless absolutely required
-
Authorization Exceptions as Last Resort
- Use exceptions sparingly for edge cases
- Document the reason for each exception
- Set appropriate priorities to avoid conflicts
-
Test Permission Scenarios
- Test unauthenticated access
- Test group membership changes
- Test admin override behavior
- Test exception priority resolution
-
Monitor Authorization Exceptions
- Regularly audit active exceptions
- Deactivate or delete obsolete exceptions
- Review exception conflicts (overlapping priorities)
Debugging & Monitoring
Enable Debug Logging
// In GuzzleSolrService
$this->logger->debug('[SOLR] RBAC filtering applied');
// In MagicRbacHandler
$this->logger->debug('Applying RBAC filters', [
'user_id' => $userId,
'user_groups' => $userGroups,
'schema_uuid' => $schema->getUuid()
]);
Check Authorization Config
# Query schema authorization
docker exec -u 33 master-nextcloud-1 php -r "
\$schema = \OC::$server->get(\OCA\OpenRegister\Db\SchemaMapper::class)->find(1);
var_dump(\$schema->getAuthorization());
"
Query Authorization Exceptions
# List active exceptions
docker exec -it master-database-mysql-1 mysql -u nextcloud -pnextcloud nextcloud -e "
SELECT type, subject_type, subject_id, action, priority, description
FROM oc_openregister_authorization_exceptions
WHERE active = 1
ORDER BY priority DESC;
"
Security Considerations
-
Default Deny
- When authorization is configured, default behavior is to deny access
- Explicitly configure 'public' in read permissions for public access
-
Admin Override
- Admin override can be disabled for high-security environments
- When disabled, even admins must have explicit permissions
-
Authorization Inheritance
- Objects inherit authorization from schemas
- Object-level overrides take precedence
-
Exception Priority
- Exclusions should have higher priority than inclusions to ensure security
- Use priority > 50 for security-critical exclusions
-
Unauthenticated Access
- Unauthenticated users only see objects with 'public' in read permissions
- Fallback to published object filtering can be enabled (currently disabled)
Restricting OpenRegister to a user group
Nextcloud administrators can limit OpenRegister to specific groups via Apps → OpenRegister → Limit to groups (or occ app:enable openregister --groups <group>). Because Nextcloud gates app routes by IAppManager::isEnabledForUser(), this blocks every non-public route for any user outside those groups — including admins who are not members, and other apps calling OpenRegister on a user's behalf. Only routes marked #[PublicPage] bypass the restriction.
OpenRegister is a data platform whose register, schema, and object read endpoints are consumed by other apps for all users. To keep those reachable while limiting management to the restriction group, the read surface is public-by-route and write/management endpoints are not:
| Operation | Route visibility | Behaviour under app-group restriction |
|---|---|---|
Read registers / schemas / objects (index, show) | #[PublicPage] | Reachable by all users (and consuming apps), in or out of the group |
| Create / update / delete registers & schemas | not public | Blocked for users outside the group (and additionally gated to admin / manage-permission) |
Anonymous access to the public read endpoints is limited to published resources. A register or schema is only returned to an unauthenticated caller when its published field is set and it has not been depublished; an anonymous request for an unpublished register/schema returns 401. Authenticated users are unaffected and continue to receive results scoped by the RBAC rules described above. Object reads remain governed by ObjectService / PermissionHandler regardless of the resolved identity.
Net effect: group-restricting OpenRegister hides the management UI and write operations from non-group users while leaving the consumed read APIs (and published catalogue data) available. See also the register/schema write-authorization rules below and in RegistersController / SchemasController.
Authorization Exceptions
The Authorization Exception System provides a flexible way to override the standard Role-Based Access Control (RBAC) system. It allows for fine-grained control over permissions by defining specific inclusions and exclusions that take precedence over normal authorization rules.
Exception Types
- Inclusions: Grant additional permissions to users or groups that they wouldn't normally have
- Exclusions: Deny permissions to users or groups even if they would normally have access through RBAC
Subject Types
- User: Exceptions that apply to specific individual users
- Group: Exceptions that apply to all members of a specific group
Priority System
Authorization exceptions use a priority system to resolve conflicts:
- Exclusions (highest priority) - Always deny access if applicable
- Inclusions (medium priority) - Grant access if no exclusions apply
- Normal RBAC (lowest priority) - Default system behavior
Within each type, exceptions with higher numerical priority values take precedence.
Database Schema
The oc_openregister_authorization_exceptions table stores authorization exceptions with the following key fields:
type: 'inclusion' or 'exclusion'subject_type: 'user' or 'group'subject_id: The actual user ID or group IDaction: CRUD operation ('create', 'read', 'update', 'delete')schema_uuid: Optional - limits exception to specific schemaregister_uuid: Optional - limits exception to specific registerorganization_uuid: Optional - limits exception to specific organizationpriority: Integer priority for conflict resolutionactive: Boolean to enable/disable exceptionsdescription: Human-readable description
Usage Examples
Example 1: Group Cross-Organization Access
Allow users in the 'ambtenaar' group to read 'gebruik' objects from all organizations:
$exception = new AuthorizationException();
$exception->setType(AuthorizationException::TYPE_INCLUSION);
$exception->setSubjectType(AuthorizationException::SUBJECT_TYPE_GROUP);
$exception->setSubjectId('ambtenaar');
$exception->setAction(AuthorizationException::ACTION_READ);
$exception->setSchemaUuid('gebruik-schema-uuid');
$exception->setPriority(20); // High priority to override multi-tenancy
$exception->setDescription('Allow ambtenaar group to read gebruik objects from all organizations');
$authService->createException(/* parameters */);
Example 2: User Exclusion
Deny a specific user update access despite group membership:
$exception = new AuthorizationException();
$exception->setType(AuthorizationException::TYPE_EXCLUSION);
$exception->setSubjectType(AuthorizationException::SUBJECT_TYPE_USER);
$exception->setSubjectId('problematic-user');
$exception->setAction(AuthorizationException::ACTION_UPDATE);
$exception->setSchemaUuid('sensitive-schema-uuid');
$exception->setPriority(15);
$exception->setDescription('Deny user update access due to security concerns');
API Usage
Creating Exceptions
# Create user inclusion
curl -X POST http://localhost/index.php/apps/openregister/api/authorization-exceptions \
-u 'admin:admin' \
-H 'Content-Type: application/json' \
-H 'OCS-APIREQUEST: true' \
-d '{
"type": "inclusion",
"subject_type": "user",
"subject_id": "special-user",
"action": "read",
"schema_uuid": "confidential-schema-uuid",
"priority": 10,
"description": "Allow special user to read confidential data"
}'
Listing Exceptions
# List all exceptions
curl http://localhost/index.php/apps/openregister/api/authorization-exceptions \
-u 'admin:admin' \
-H 'OCS-APIREQUEST: true'
# Filter by criteria
curl 'http://localhost/index.php/apps/openregister/api/authorization-exceptions?type=inclusion&active=true' \
-u 'admin:admin' \
-H 'OCS-APIREQUEST: true'
Integration with RBAC
The authorization exception system integrates seamlessly with the existing RBAC system:
- Query Level: The
ObjectEntityMapper::applyRbacFilters()method checks for exceptions before applying normal RBAC rules - Object Level: The
ObjectEntityMapper::checkObjectPermission()method evaluates exceptions first, then falls back to standard permission checks - Evaluation Order: Exclusions → Inclusions → Normal RBAC → Object ownership → Publication status
Best Practices
1. Use Specific Scope
Always limit exceptions to the most specific scope possible:
// Good - specific to schema and organization
$exception->setSchemaUuid('specific-schema');
$exception->setOrganizationUuid('specific-org');
// Avoid - too broad, affects everything
// (leaving schema_uuid and organization_uuid as null)
2. Set Appropriate Priorities
Use priority levels consistently:
- 1-10: Low priority inclusions
- 11-20: Medium priority inclusions
- 21-30: High priority inclusions
- 31-40: Low priority exclusions
- 41-50: Medium priority exclusions
- 51+: High priority exclusions
3. Document Exceptions
Always provide clear descriptions explaining why the exception exists:
$exception->setDescription('Allow support team to read customer data for troubleshooting purposes - ticket #12345');
4. Regular Audits
Regularly review authorization exceptions to ensure they're still needed:
// Find old exceptions
$oldExceptions = $mapper->findByCriteria([
'created_at' => '<' . (new DateTime('-6 months'))->format('Y-m-d'),
'active' => true
]);
Troubleshooting
Exception Not Working
-
Check if exception is active:
$exception = $mapper->findByUuid($uuid);
var_dump($exception->getActive()); -
Verify priority is high enough:
$exceptions = $service->getUserExceptions($userId);
usort($exceptions, fn($a, $b) => $b->getPriority() <=> $a->getPriority()); -
Check scope matching:
$result = $exception->matches($subjectType, $subjectId, $action, $schemaUuid);
Security Considerations
- Audit Trail: All exception creation/modification is logged with user information
- Admin Only: Only administrators should create/modify authorization exceptions
- Regular Review: Exceptions should be reviewed quarterly to ensure they're still appropriate
- Principle of Least Privilege: Use the most restrictive scope possible for each exception
Future Enhancements
-
Audit Logging
- Log all authorization decisions
- Track permission changes over time
-
Permission Testing Tool
- UI for testing user permissions
- Visualize effective permissions for users/groups
-
RBAC Analytics
- Permission usage statistics
- Identify over-privileged users
- Suggest permission optimizations