Skip to content

Access Control

The Access Control module (id access_control, v1.0.0) gives you a structured data layer for physical security: doors, readers, access controllers, cardholders, credentials, and access schedules. All CRUD operations work, all data persists correctly, and an HMAC-chained event log records every access event. What is absent is the hardware bridge - the adapters that would translate an API call into a signal on an actual door relay.


The following objects are fully CRUD-operational via the API:

ObjectWhat it represents
AccessControllerA physical access-control panel or reader hub (vendor, model, host, site)
DoorA named door with default unlock time and associated readers
CardholderA person record holding one or more credentials
AccessCredentialA card number, PIN, or mobile credential assigned to a cardholder
AccessScheduleA named schedule (weekday windows, time zones) attachable to credentials
AccessEventImmutable log entries: granted / denied / forced / held-open / unlocked / locked / alarm
  • PIN values are hashed with Argon2id - they are not stored in plaintext or reversible.
  • Card number and facility code are Fernet-encrypted at rest (column-level, applied in migration 013).
  • API responses use dedicated response schemas (AccessCredentialResponse, AccessCredentialListResponse) that strip the PIN hash and card ciphertext - these fields never leave the server.

Access events are written with an HMAC hash chain. Each event’s hash covers its own fields plus the hash of the preceding event, so any tampering with historical records is detectable. You can validate the chain for a door over a time range:

GET /api/v1/access_control/access/events/chain/validate?door_id=<uuid>&start_time=<iso>&end_time=<iso>

The event_retention_days setting is defined in the module schema but automatic pruning of access events is not available in this release. Use pg_dump or the pg-backup container for long-term retention management.

Access events are emitted on the Fabric bus regardless of adapter state. You can wire them to other module operations in Connections today - no adapter is needed for events:

EventTrigger
access.door.grantedA credential passed at a reader
access.door.deniedA credential was rejected
access.door.forcedDoor opened without a valid credential
access.door.held_openDoor held beyond its unlock window
access.door.unlockedDoor unlocked (software command or credential)
access.door.lockedDoor locked
access.door.alarmAn access-control alarm was raised

Example connection: wire access.door.forcedcameras.snapshotstorage.store_blob to automatically capture and archive a still image whenever a door is forced. See Connections for how to author this in Fabric.

A access_control.relock_door_after Celery task is registered. When unlock is called, the task is scheduled to flip the door’s DB state back to locked after the unlock window expires. The task updates DB-side state only because no hardware adapter ships today.


POST /api/v1/access_control/access/doors/{door_id}/lock → 501 DoorControlUnavailable
POST /api/v1/access_control/access/doors/{door_id}/unlock → 501 DoorControlUnavailable

AccessControlService._get_door_adapter() inspects every registered adapter and returns None because none implement lock_door / unlock_door. The 501 response is intentional. Both endpoints return 501 without writing an AccessEvent row. DoorControlUnavailableError is raised immediately after the adapter check returns None, before the _store_event('remote_lock' / 'remote_unlock') call that follows the adapter invocation. An audit entry is written only after a successful hardware-adapter call, which today is never reached. Site-limited operators get 404 for doors outside their grant boundary. Rejected lock/unlock attempts do not create an AccessEvent row in this release.

Anti-passback enforcement appeared in an earlier version of the module manifest. It was never implemented - no code tracks a last-access-zone per credential or enforces re-entry rules. The settings fields have been removed from the schema. Anti-passback does not appear in the settings panel.

The current frontend page at /access is a placeholder. There is no working management UI. All management must go through the API directly.


The module router mounts at /api/v1/access_control/access/. All endpoints enforce require_permissions(...). The service builds per-request with your organization ID and respects per-user site grants (accessible_site_ids for site-limited users) - requests for resources outside your grant return 403 with a generic message (no ID disclosure).

MethodPathPurposePermission
GET/doorsList doors (site_id, controller_id, door_status; limit 1-100)access.view
GET/doors/statsAggregate door stats by siteaccess.view
GET/doors/{door_id}Get one dooraccess.view
POST/doorsCreate door (201)access.manage_doors
PATCH/doors/{door_id}Update dooraccess.manage_doors
DELETE/doors/{door_id}Delete door (204)access.manage_doors
POST/doors/{door_id}/lockLock door - returns 501access.door_control
POST/doors/{door_id}/unlockUnlock door - returns 501 (query: duration 1-300 s)access.door_control
MethodPathPurposePermission
GET/credentialsList (cardholder_id, credential_type, is_active); strips hash/ciphertextaccess.view
GET/credentials/{credential_id}Get one; strips hash/ciphertextaccess.view
POST/credentialsCreate (201); PIN Argon2id-hashed, card/facility Fernet-encryptedaccess.manage_credentials
PATCH/credentials/{credential_id}Updateaccess.manage_credentials
POST/credentials/{credential_id}/revokeRevoke (sets is_active=false)access.manage_credentials
MethodPathPurposePermission
GET/cardholdersList (site_id, is_active, search; limit/offset)access.view
GET/cardholders/{cardholder_id}Get oneaccess.view
POST/cardholdersCreate (201)access.manage_credentials
PATCH/cardholders/{cardholder_id}Updateaccess.manage_credentials
DELETE/cardholders/{cardholder_id}Delete (204)access.manage_credentials
MethodPathPurposePermission
GET/schedulesList (site_id; limit/offset)access.view
GET/schedules/{schedule_id}Get oneaccess.view
POST/schedulesCreate (201)access.manage_schedules
PATCH/schedules/{schedule_id}Updateaccess.manage_schedules
DELETE/schedules/{schedule_id}Delete (204)access.manage_schedules
MethodPathPurposePermission
GET/controllersList (site_id; limit/offset)access.view
GET/controllers/{controller_id}Get oneaccess.view
POST/controllersCreate (201)access.manage_doors
PATCH/controllers/{controller_id}Updateaccess.manage_doors
DELETE/controllers/{controller_id}Delete (204)access.manage_doors
MethodPathPurposePermission
GET/eventsSearch events (door_id, cardholder_id, event_type, start_time, end_time; limit 1-1000)access.view_events
GET/events/statsAggregate stats (site_id, time range)access.view_events
POST/events/{event_id}/ackAcknowledge event (records actor + timestamp)access.view_events
GET/events/chain/validateValidate HMAC hash chain for tamper detectionaccess.view_events

Permission codeWhat it gatesMinimum role
access.viewRead doors, readers, controllersviewer
access.view_eventsRead access event logviewer
access.manage_doorsCreate/update/delete doors, readers, controllerssite_admin
access.manage_credentialsIssue, update, and revoke credentials; manage cardholderssite_admin
access.manage_schedulesCreate and edit access schedulessite_admin
access.door_controlCall lock/unlock endpoints (currently always 501)operator

For the full role hierarchy see Roles and Permissions.


Two settings are configurable per organization under Settings → Modules → Access Control:

SettingDefaultRangeDescription
default_door_unlock_time5 s1-60 sHow long a door stays unlocked after a valid credential (hardware-side effect only when an adapter ships)
event_retention_days9030-365Retention period (days). Defined in schema; pruning is not available in this release

Column-level encryption for credentials (PIN Argon2id hash column, card number and facility code Fernet columns) is applied in migration 013. If you are running a fresh install this is applied automatically. If you are upgrading from a pre-013 instance, run alembic upgrade head before enabling the module.


ClaimAccurate status
Door CRUD, credential CRUD, cardholder CRUD, schedule CRUDWorks
Tamper-evident HMAC event logWorks
Fabric event emissionWorks
Per-site tenant isolation and per-user site grantsWorks
Credential encryption at restWorks (requires migration 013)
Frontend management UIAPI-only placeholder
Door lock / unlockHTTP 501 - no adapter for any vendor
Anti-passbackRemoved - was never implemented

  • Fabric Connections - wire access.door.forced or access.door.denied to downstream actions (camera snapshots, notifications, webhooks) today.
  • Roles and Permissions - understand the 7-tier hierarchy and how site grants interact with access.manage_doors and access.door_control.
  • Security Model - learn how FreeSDN enforces tenant isolation at the application layer and how credentials are protected at rest.
  • Configuration Backup - access-control configuration (doors, cardholders, credentials, schedules) is NOT included in .fsdn snapshots. No backup contributor has been implemented for this module (AccessControlModule has no get_backup_contributor() hook, and CoreBackupContributor covers only sites, controllers, devices, users, and automation rules). Use your database backup (pg_dump or the pg-backup container) to preserve access control data.