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
- 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
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"]
}
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.
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
}
enabled: Master switch for RBAC systemadminOverride: Allow users in 'admin' group to bypass all RBAC checks
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