Data security¶
Mechanics for resource ACL, sharing UI, list filters, and resolver code. For principles (default-open, admin vs data access, trust boundaries, identity modes), see Security design.
Related: Console & authentication (login, catalog keys, tokens), Data models — Data security (schemas), API reference — resource ACL (HTTP).
For AI agents — quick facts¶
| Question | Answer |
|---|---|
| Resolver | resource_acl_service.py; guards: resource_guard.py (standalone), context_guard.py (hierarchical) |
| When is data restricted? | Any ACL row on the resource or an ancestor in its chain |
| Others | grantee_type=authenticated; permissions=0 = explicit deny for non-owner, non-group users |
| Bits | r=1, w=2, m=4; manage satisfies read/write in perm_satisfies |
| Owner storage | Local: users.id. OIDC: JWT sub. PUT: normalize_user_grantee_id; read: user_grant_matches |
| Sharing API | GET/PUT /api/resource-acl/{type}/{id}; GET …/owner-candidates |
| Legacy | Data-resource scope APIs → 410; use resource ACL |
New list/get handlers: use resource_guard.load_scoped_resource / require_resource_by_id for standalone types, or check_resource_access / instance_visible / channel predicates for hierarchical types. New securable types: extend RESOURCE_REGISTRY in resource_guard.py, SECURABLE_RESOURCE_TYPES, resource_context_chain, the Alembic revision, and this page.
Layer 1 — Operation permissions (RBAC)¶
Gates features and Console tools — not individual channels or documents.
| Storage | Role |
|---|---|
security_permissions |
Keys + optional route/API patterns |
security_roles + security_role_permissions |
Roles → keys |
user_security_roles |
Local users → roles |
OIDC JWT realm_access.roles |
Matched to security_roles.name |
Enforcement: require_permission on routes; SPA via GET /api/auth/permission-catalog. Optional strict Layer 1 middleware — see Configuration.
| Flag | Default | Effect |
|---|---|---|
OPENKMS_ENFORCE_PERMISSION_PATTERNS_STRICT |
false |
Layer 1: every /api call must match a catalog pattern and permission key |
OPENKMS_ENFORCE_RESOURCE_ACL |
false |
Layer 2 default-closed: deny resources with no ACL rows until sharing is set |
Canonical keys: Console & authentication — Permission catalog.
Layer 2 — Resource ACL (data plane)¶
Tables¶
| Table | Purpose |
|---|---|
resource_acl_entries |
(resource_type, resource_id, grantee_type, grantee_id) → bitmask |
access_groups |
Named groups (grantee_type=group) |
access_group_members |
subject ↔ group_id (local id, OIDC sub, or alias strings) |
Columns: Data models — Data security.
Securable types¶
backend/app/services/acl/resource_acl_constants.py — SECURABLE_RESOURCE_TYPES:
resource_type |
Container chain |
|---|---|
document_channel |
Parent channels to root |
article_channel |
Parent article channels |
wiki_space |
— |
knowledge_base, evaluation, glossary, dataset, object_type, link_type |
Standalone |
Wiki pages are not securable rows: visibility is wiki space ACL only (wiki_page_scope), same pattern as documents on channels.
Documents and articles use channel-only ACL (document_scope / article_scope).
Grantees and bits¶
| Bit | Value | Use |
|---|---|---|
| Read | 1 | List, view, download |
| Write | 2 | Upload, edit, move |
| Manage | 4 | Change sharing (m implies r/w) |
grantee_type |
grantee_id |
UI |
|---|---|---|
user |
Canonical sub / local user id |
Owner |
group |
access_groups.id |
Groups row |
authenticated |
NULL |
Others |
User grants: PUT runs normalize_user_grantee_id() (username, email, legacy local id → canonical). Read uses user_grant_matches() for alias / legacy-id bridging.
Per-resource sharing applies when ACL rows exist. Set OPENKMS_ENFORCE_RESOURCE_ACL=true for default-closed mode: resources with no ACL rows are denied until sharing is configured (Layer 1 keys alone are not enough).
Others defaults (wiki space, knowledge base, channels, evaluations, glossaries, datasets, object types, link types): Alembic seeds grantee_type=authenticated with r/w/m on resources that already existed when sharing shipped (y7z8a9b0c1d2 document channels, b2c3d4e5f6a9 article channels, h8i9j0k1l2m3 wiki spaces and knowledge bases, m2n3o4p5q6r7 evaluations, glossaries, object types, link types; n3o4p5q6r7s8 datasets). bootstrap_owner_acl on create adds owner rwm only — no Others row, so non-owners are denied until sharing is changed.
Inheritance and evaluation¶
resource_acl_service.py.
Context chain¶
resource_context_chain() — nearest first, then ancestors (document/article channel trees, wiki page → space).
effective_permissions¶
- Groups —
user_group_ids()matchesaccess_group_members.subjectto JWT aliases (sub,preferred_username,email,name; local mode also user id/username). - Others —
_authenticated_bits_from_chain(): nearest explicitauthenticatedgrant on chain; child overrides parent;0= deny for non-owner/non-group. - User + group rows — union matching grants on any chain node.
- Check —
check_resource_access(..., required)viaperm_satisfies.
Owner bootstrap¶
- Create:
bootstrap_owner_acl(..., sub)withrwm; channels setcreated_by+created_by_name. - GET: default owner row from
created_bywhen no owner ACL (display only). - PUT: preserves owner if omitted; normalizes user grantee ids.
- New document/article channels: owner-only (no Others row).
- Pre-migration channels: Alembic backfilled Others
rwm(y7z8a9b0c1d2,b2c3d4e5f6a9).
OIDC display: Owner label resolves from oidc_identities (upsert on login / sync-session / GET /me), then created_by_name, group-member aliases, users, or legacy grantee_label on ACL rows. Access checks use JWT sub via normalize_user_grantee_id / user_grant_matches. Migrations d4e5f6a7b8c1, e5f6a7b8c9d0 fix legacy grantee ids; j9k0l1m2n3o4 adds the identity directory.
Caller matrix¶
| Caller | Resource ACL read/write | Resource ACL manage |
|---|---|---|
| Normal user | From ACL | Needs m |
JWT admin |
From ACL | Bypass |
local-cli |
Skipped | Skipped |
| Personal API key | As owner_sub |
As owner_sub |
Enforcement (where ACL is applied)¶
| Surface | Mechanism |
|---|---|
| Standalone resources (KB, eval, glossary, dataset, ontology types) | resource_guard.py — load_scoped_resource, require_resource_by_id |
| Documents | Channel ACL — read: require_document_read; write/mutate: require_document_write (channel w) |
| Articles | Channel ACL — read: require_article_read; write/mutate: require_article_write (channel w) |
| Document / article channels | Read lists: batched readable_*_channel_ids. Mutations: require_*_channel_write |
| Wiki / KB linked documents | List: all links when caller can read space/KB. Add: space/KB write + document read (channel) |
| Wiki pages | Space ACL — wiki_page_scope / get_wiki_page_in_space |
| Job runs | Args resolved to document (channel write for create/retry) or KB (scoped write/read) |
| Global search | Scoped in global_search.py |
| Sharing API | GET needs read; PUT needs manage |
data_resource_policy.py delegates visibility checks to resource ACL (no data_resources table).
Operator and user UI¶
Console — access groups¶
/console/data-security/groups,…/groups/:id/members— permissionconsole:groupsPUT /api/admin/groups/{id}/memberswithsubjects(username/email accepted; stored as canonical OIDCsubor localusers.id, duplicates collapsed)GET …/groups/{id}/shared-resources— grants referencing the group
Console — issues audit¶
/console/data-security/issues—GET /api/admin/resource-acl/issues(summary or?issue=&limit=&offset=)- Issue codes: Others manage/write; missing/broken owner; unknown/empty group; implicit Others; review Others read (+ groups)
- Inline Fix sharing:
GET/PUT /api/admin/resource-acl/{type}/{id};owner-candidatesfor admin audit
Per-resource sharing (ResourceSharePanel)¶
Routes: document/article channel settings (Sharing tab), wiki space settings (#sharing), knowledge base settings tab (?tab=settings), evaluation dataset settings (/evaluations/{id}/settings?tab=sharing), glossary settings (/glossaries/{id}/settings?tab=sharing), ontology dataset sharing (/ontology-manager/datasets/{id}/sharing), object type / link type sharing (/ontology-manager/object-types|link-types/{id}/sharing).
| Row | grantee_type |
Notes |
|---|---|---|
| Owner | user |
Autocomplete via owner-candidates; free-text subject still accepted |
| Groups | group |
Per-row r/w/m |
| Others | authenticated |
Empty = deny; always included in PUT |
Shows Your access: effective_permissions. Read-only without manage. Save → PUT owner + groups + Others.
Owner candidates:
| Auth | List source |
|---|---|
| Local | users (id, username) |
| OIDC | user_api_keys, mapped users, access_group_members — merged to one row per person via normalize_user_grantee_id (username / local users.id → OIDC sub when known) |
Example: restricted channel¶
Owner bob rwm, group QA rwm, Others empty → user alice with documents:read gets 404 on channel and ?channel_id=. Parent channel without ACL stays visible; child ACL is evaluated independently.
Migrations (legacy → ACL)¶
| Revision | Change |
|---|---|
x6y7z8a9b0c1 |
resource_acl_entries; copy junction scopes; access_group_members |
k0l1m2n3o4p5 |
Drop legacy access_group_* junction tables (scopes only in resource_acl_entries) |
l1m2n3o4p5q6 |
Drop data_resources table |
y7z8a9b0c1d2 |
Others on existing document channels |
z8a9b0c1d2e4 |
document_channels.created_by |
a1b2c3d4e5f8 |
article_channels.created_by |
b2c3d4e5f6a9 |
Others on existing article channels |
c3d4e5f6a7b0 |
created_by_name on channels |
d4e5f6a7b8c1 |
Username → canonical grantee id |
e5f6a7b8c9d0 |
Local users.id owner → OIDC sub when API key maps user |
Extending data security¶
- Catalog key + route/SPA patterns (Layer 1).
SECURABLE_RESOURCE_TYPES+resource_context_chain+ model/migration.- List/get filters (
check_resource_access, predicates). - Create:
created_by/bootstrap_owner_acl. - UI:
ResourceSharePanel(consoleAuditonly for admin audit). - Owner:
owner-candidates+ normalized PUT. - Tests:
backend/tests/test_resource_acl.py(no Postgres required for most ACL unit tests).
Known gaps (implementation)¶
| Topic | Status |
|---|---|
| Share UI coverage | Channels, wiki, KB; API supports all SECURABLE_RESOURCE_TYPES |
| Wiki page ACL | Space-only (same as document/channel); no wiki_page sharing rows |
| KB search / ask side channel | Indexed chunk content returned to KB readers without re-checking document channel ACL |
| OIDC owner picker directory | API keys + group members, not full IdP sync |
| List filter performance | Standalone + channel trees batched; restricted channels still per-id check_resource_access |
Policy non-goals (admin read-all, Object Explorer Cypher): Security design — Deliberate non-goals.
Code map¶
| Path | Role |
|---|---|
resource_acl_constants.py |
Types, bits, grantees |
resource_guard.py |
Unified Layer 2 guard + RESOURCE_REGISTRY for standalone types |
context_guard.py |
Hierarchical guard + CONTEXT_*_REGISTRY for documents, articles, channels |
document_scope.py, article_scope.py, wiki_page_scope.py, channel_scope.py |
Container-only visibility aliases |
job_scope.py, channel_list_filter.py |
Job args ACL; channel subtree list filter |
resource_acl_service.py |
Resolve, filters, normalize/match owner, acl_check_required |
api/resource_acl.py |
Sharing HTTP API (parse + persist grants) |
resource_acl_presentation.py, schemas/resource_acl.py |
Grant labels, owner/creator enrichment, ACL response models (shared by sharing API and Console audit) |
api/admin/resource_acl_admin.py |
Issues + audit ACL |
api/admin/groups.py |
Groups, members, shared list |
models/resource_acl.py |
ResourceAclEntry |
data_scope.py |
Channel tree + re-exports |
ResourceSharePanel.tsx / resourceAclApi.ts |
SPA sharing |
tests/test_resource_acl.py |
Unit tests |
tests/test_document_write_acl.py |
Document write vs read channel ACL |
tests/test_wiki_page_space_visibility.py |
Wiki page read via space ACL |