Skip to main content

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:

  1. User Rights

    • CRUD (Create, Read, Update, Delete) operations
    • Inherited from ADFS groups via Keycloak
    • Role-based access control through Nextcloud
  2. Contract Rights

    • Application-level permissions
    • Process-specific authorizations
    • Compliance with FCS requirements
    • Integration with verwerkingen registers

Implementation

Access control is implemented through:

  1. User Authentication

    • Direct integration with Keycloak for identity management
    • ADFS synchronization for user and group information
    • Single Sign-On (SSO) capabilities
  2. Permission Management

    • CRUD-level permissions for all system entities
    • Hierarchical permission inheritance
    • Fine-grained access control at multiple levels
  3. 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

ColumnTypeDescription
idINTEGERPrimary key
uuidVARCHAR(36)Unique identifier
typeVARCHAR(20)Exception type: inclusion or exclusion
subject_typeVARCHAR(20)Subject type: user or group
subject_idVARCHAR(255)User ID or Group ID
schema_uuidVARCHAR(36)Schema UUID (nullable for global)
register_uuidVARCHAR(36)Register UUID (nullable)
organization_uuidVARCHAR(36)Organisation UUID (nullable)
actionVARCHAR(20)CRUD action: create, read, update, delete
priorityINTEGERPriority for resolution (higher = more important)
activeBOOLEANWhether exception is active
descriptionTEXTHuman-readable description
created_byVARCHAR(255)User who created the exception
created_atDATETIMECreation timestamp
updated_atDATETIMELast 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").
  • match conditions 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 system
  • adminOverride: Allow users in 'admin' group to bypass all RBAC checks

Performance Optimizations

  1. Lazy Group Loading

    • User groups are only fetched when RBAC is enabled
    • Results are cached within the request lifecycle
  2. Admin Fast Path

    • Admin users bypass permission checks entirely when override is enabled
    • Reduces database queries for administrative operations
  3. Query-Level Filtering

    • RBAC filters are applied at the database query level
    • Prevents loading unauthorized objects into memory
  4. Authorization Config Caching

    • Schema authorization configs are cached with schema entities
    • Reduces redundant JSON parsing
  5. Exception Priority Indexing

    • Database index on priority field for fast exception sorting
    • Composite index on (subject_type, subject_id, action, active) for fast matching

Integration Points

1. ObjectEntityMapper

  • Applies RBAC filters to all object queries via findAll(), find(), count()
  • Respects rbac parameter (default: true)

2. MagicMapper (Dynamic Tables)

  • Uses MagicRbacHandler for schema-specific table filtering
  • Consistent security across schema-generated tables
  • RBAC filtering applied via applyAdditionalFilters() in GuzzleSolrService
  • Currently logs RBAC application but full implementation pending

4. API Controllers

  • RBAC checks in ObjectsController, RegistersController, SchemasController
  • Validates permissions before CRUD operations

Best Practices

  1. Use Schema-Level Authorization

    • Define authorization at the schema level for consistency
    • Only override at object level when necessary
  2. Leverage Group-Based Permissions

    • Use Nextcloud groups for role management
    • Avoid user-specific permissions unless absolutely required
  3. Authorization Exceptions as Last Resort

    • Use exceptions sparingly for edge cases
    • Document the reason for each exception
    • Set appropriate priorities to avoid conflicts
  4. Test Permission Scenarios

    • Test unauthenticated access
    • Test group membership changes
    • Test admin override behavior
    • Test exception priority resolution
  5. 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

  1. Default Deny

    • When authorization is configured, default behavior is to deny access
    • Explicitly configure 'public' in read permissions for public access
  2. Admin Override

    • Admin override can be disabled for high-security environments
    • When disabled, even admins must have explicit permissions
  3. Authorization Inheritance

    • Objects inherit authorization from schemas
    • Object-level overrides take precedence
  4. Exception Priority

    • Exclusions should have higher priority than inclusions to ensure security
    • Use priority > 50 for security-critical exclusions
  5. 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:

OperationRoute visibilityBehaviour 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 & schemasnot publicBlocked 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

  1. Inclusions: Grant additional permissions to users or groups that they wouldn't normally have
  2. 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:

  1. Exclusions (highest priority) - Always deny access if applicable
  2. Inclusions (medium priority) - Grant access if no exclusions apply
  3. 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 ID
  • action: CRUD operation ('create', 'read', 'update', 'delete')
  • schema_uuid: Optional - limits exception to specific schema
  • register_uuid: Optional - limits exception to specific register
  • organization_uuid: Optional - limits exception to specific organization
  • priority: Integer priority for conflict resolution
  • active: Boolean to enable/disable exceptions
  • description: 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:

  1. Query Level: The ObjectEntityMapper::applyRbacFilters() method checks for exceptions before applying normal RBAC rules
  2. Object Level: The ObjectEntityMapper::checkObjectPermission() method evaluates exceptions first, then falls back to standard permission checks
  3. 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

  1. Check if exception is active:

    $exception = $mapper->findByUuid($uuid);
    var_dump($exception->getActive());
  2. Verify priority is high enough:

    $exceptions = $service->getUserExceptions($userId);
    usort($exceptions, fn($a, $b) => $b->getPriority() <=> $a->getPriority());
  3. Check scope matching:

    $result = $exception->matches($subjectType, $subjectId, $action, $schemaUuid);

Security Considerations

  1. Audit Trail: All exception creation/modification is logged with user information
  2. Admin Only: Only administrators should create/modify authorization exceptions
  3. Regular Review: Exceptions should be reviewed quarterly to ensure they're still appropriate
  4. Principle of Least Privilege: Use the most restrictive scope possible for each exception

Future Enhancements

  1. Audit Logging

    • Log all authorization decisions
    • Track permission changes over time
  2. Permission Testing Tool

    • UI for testing user permissions
    • Visualize effective permissions for users/groups
  3. RBAC Analytics

    • Permission usage statistics
    • Identify over-privileged users
    • Suggest permission optimizations