Spaces:
Build error
Access Control System
The PDF-TEI-Editor implements a multi-layered access control system that combines role-based access control (RBAC) with collection-based filtering.
Overview
The access control system operates at three levels:
- Role-Based Access Control (RBAC) - Controls what operations users can perform
- Collection-Based Access Control - Controls which documents users can see and edit
- Document-Level Access Control - Controls visibility and editability of individual documents
Architecture
User → Groups → Collections
Users are assigned to groups, and groups have access to specific collections. Documents belong to collections, and users can only access documents in collections their groups have access to.
User
↓ belongs to
Groups (one or more)
↓ have access to
Collections (one or more)
↓ contain
Documents
Configuration Files
Access control is configured through JSON files in the data/db/ directory (or config/ for defaults):
users.json
[
{
"username": "editor1",
"fullname": "Jane Editor",
"email": "editor1@example.com",
"passwd_hash": "...",
"roles": ["user", "annotator"],
"groups": ["editors"],
"session_id": null
}
]
groups.json
[
{
"id": "editors",
"name": "Editors Group",
"description": "Editors with access to manuscripts",
"collections": ["manuscripts", "letters"]
}
]
collections.json
[
{
"id": "manuscripts",
"name": "Medieval Manuscripts",
"description": "Collection of medieval manuscript transcriptions"
}
]
roles.json
[
{
"id": "user",
"roleName": "User",
"description": "Basic user with read access"
},
{
"id": "annotator",
"roleName": "Annotator",
"description": "Can create and edit version files"
},
{
"id": "reviewer",
"roleName": "Reviewer",
"description": "Can edit gold standard files and promote versions"
}
]
Wildcard Access (*)
The wildcard * character grants unrestricted access at any level:
User-Level Wildcards
Wildcard Roles: Users with "roles": ["*"] have all permissions
{
"username": "superadmin",
"roles": ["*"],
"groups": ["admin"]
}
Admin Role: Users with "roles": ["admin"] have access to all collections
{
"username": "admin",
"roles": ["admin", "user"],
"groups": ["staff"]
}
Wildcard Groups: Users with "groups": ["*"] have access to all collections
{
"username": "manager",
"roles": ["user"],
"groups": ["*"]
}
Group-Level Wildcards
Wildcard Collections: Groups with "collections": ["*"] grant access to all collections
{
"id": "admin-group",
"name": "Administrators",
"collections": ["*"]
}
Collection Access Resolution
The system resolves collection access using this priority order:
- Check for wildcard roles - If user has
*oradminin roles → access to all collections - Check for wildcard groups - If user has
*in groups → access to all collections - Check each group - For each group the user belongs to:
- If group has
*in collections → access to all collections - Otherwise, collect all specific collection IDs from the group
- If group has
- Return result:
null(if any wildcard was found) = access to all collections[](empty list) = no collection access["col1", "col2"]= access to specific collections
Implementation
Helper Functions
The user_utils.py module provides collection access helpers:
get_user_collections(user, db_dir)
Returns the list of collections accessible to a user.
from fastapi_app.lib.permissions.user_utils import get_user_collections
# Get accessible collections
accessible_collections = get_user_collections(current_user, settings.db_dir)
if accessible_collections is None:
# User has access to all collections
pass
elif not accessible_collections:
# User has no collection access
pass
else:
# User has access to specific collections
print(f"User can access: {accessible_collections}")
Returns:
None- User has access to all collections (wildcard)[]- User has no collection access (anonymous or no groups)["col1", "col2", ...]- User has access to specific collections
user_has_collection_access(user, collection_id, db_dir)
Checks if a user has access to a specific collection.
from fastapi_app.lib.permissions.user_utils import user_has_collection_access
# Check access to specific collection
if user_has_collection_access(current_user, 'manuscripts', settings.db_dir):
# User has access
pass
Returns: True if user has access, False otherwise
API Endpoints
Files List Endpoint
The /api/v1/files/list endpoint (files_list.py:155-178) filters files based on collection access:
# Get user's accessible collections
accessible_collections = get_user_collections(current_user, settings.db_dir)
if accessible_collections is not None:
# Filter documents by collections
files_data = []
for doc_group in documents_map.values():
doc_collections = doc_group.collections or []
# Include if document has any accessible collection
if any(col in accessible_collections for col in doc_collections):
files_data.append(doc_group)
else:
# User has access to all collections
files_data = list(documents_map.values())
# Apply document-level access control
files_data = DocumentAccessFilter.filter_files_by_access(files_data, current_user)
File Save Endpoint
The /api/v1/files/save endpoint (files_save.py:56-89) validates collection access before saving:
def _validate_collection_access(user, doc_collections, db_dir, logger_inst):
"""Validate user has access to document's collections."""
# Documents must have at least one collection
if not doc_collections:
raise HTTPException(403, "Cannot save file: document has no collections")
# Check if user has access to any of the document's collections
has_access = any(
user_has_collection_access(user, col_id, db_dir)
for col_id in doc_collections
)
if not has_access:
raise HTTPException(
403,
f"You do not have access to any of this document's collections: {', '.join(doc_collections)}"
)
This validation is performed before:
- Updating existing files
- Creating new versions
- Creating new gold standard files
Document Collections
Documents inherit their collections from their source PDF file. All TEI artifacts (gold standards and versions) for a document share the same collections.
# Get PDF file to inherit collections
pdf_file = file_repo.get_pdf_for_document(doc_id)
doc_collections = pdf_file.doc_collections if pdf_file else []
# Create file with inherited collections
created_file = file_repo.insert_file(FileCreate(
id=saved_hash,
doc_id=doc_id,
doc_collections=doc_collections, # Inherited from PDF
# ... other fields
))
RBAC Manager Plugin
The frontend RBAC Manager plugin (app/src/plugins/rbac-manager.js) provides a user interface for managing access control. It allows administrators to:
- View and manage users, groups, and collections
- Assign users to groups
- Configure group collection access
- Manage user roles
The RBAC Manager integrates with the backend API endpoints to provide real-time access control management.
Management CLI
The bin/manage.py script provides commands to manage users, groups, and collections from the command line.
User Management
# Add user to group
./bin/manage.py user add-group editor1 editors
# Remove user from group
./bin/manage.py user remove-group editor1 editors
# List users with their groups
./bin/manage.py user list
Group Management
# Create group
./bin/manage.py group add editors "Editors Group" --description "Manuscript editors"
# Add collection to group
./bin/manage.py group add-collection editors manuscripts
# Add wildcard collection access
./bin/manage.py group add-collection admin-group "*"
# Remove collection from group
./bin/manage.py group remove-collection editors manuscripts
# List groups with their collections
./bin/manage.py group list
Collection Management
# Create collection
./bin/manage.py collection add manuscripts "Manuscripts" --description "Medieval manuscripts"
# List collections
./bin/manage.py collection list
Access Control Flow
Reading Files (GET /api/v1/files/list)
1. User authenticates
2. System loads user's groups from users.json
3. System resolves accessible collections:
- Check for wildcards (*, admin role)
- Collect collections from all user's groups
4. System queries all files from database
5. System filters files by collections:
- Keep only documents in accessible collections
6. System applies document-level ACL:
- Check visibility (public/private)
- Check editability (editable/protected)
7. Return filtered file list
Writing Files (POST /api/v1/files/save)
1. User authenticates
2. System determines document collections:
- For existing files: use file's doc_collections
- For new files: inherit from PDF's doc_collections
3. System validates collection access:
- Resolve user's accessible collections
- Check if user has access to any doc collection
- Reject if no access
4. System validates role permissions:
- Check if user can edit gold/versions
5. System saves file
Security Considerations
Collection Requirement
Documents must belong to at least one collection. Attempting to save a file with no collections will result in an HTTP 403 error.
Anonymous Access
Anonymous users (not authenticated) have:
- No collection access
- Only read access to public documents
- No write access
Wildcard Security
Use wildcards carefully:
"roles": ["*"]grants superuser permissions"groups": ["*"]grants access to all collections"collections": ["*"]in a group grants all members access to all collections
Collection Inheritance
Collections are inherited from the source PDF file. Users cannot:
- Change a document's collections via the save endpoint
- Create files in collections they don't have access to
- Edit files in collections they don't have access to
Testing
The test_collection_access_control.py test suite verifies:
- Anonymous user has no collection access
- Wildcard roles grant access to all collections
- Admin role grants access to all collections
- Wildcard groups grant access to all collections
- Specific group collections work correctly
- Multiple group memberships combine collections
- Group wildcard collections grant access to all
- Users with no groups have no collection access
- Nonexistent groups don't grant access
- Mixed wildcard and specific groups resolve correctly
Test Isolation
The tests use isolated temporary directories and do not modify the main application's data files in data/db/. Each test:
- Creates a
tempfile.TemporaryDirectory()for the test database - Passes the temporary
db_dirto all utility functions - Cleans up the temporary directory after the test completes
This ensures tests are:
- Safe - Never modify production/development data
- Fast - No I/O to main filesystem
- Isolated - Each test has a clean environment
- Repeatable - No side effects between test runs
Run tests:
uv run python -m pytest tests/unit/fastapi/test_collection_access_control.py -v
Examples
Example 1: Basic Editor Setup
// users.json
{
"username": "editor1",
"roles": ["user", "annotator"],
"groups": ["manuscript-editors"]
}
// groups.json
{
"id": "manuscript-editors",
"name": "Manuscript Editors",
"collections": ["manuscripts"]
}
Result: User can see and edit documents in the manuscripts collection.
Example 2: Multi-Collection Access
// users.json
{
"username": "researcher1",
"roles": ["user"],
"groups": ["manuscripts-group", "letters-group"]
}
// groups.json
[
{
"id": "manuscripts-group",
"collections": ["manuscripts"]
},
{
"id": "letters-group",
"collections": ["letters", "correspondence"]
}
]
Result: User can access documents in manuscripts, letters, and correspondence collections.
Example 3: Administrator Setup
// users.json
{
"username": "admin",
"roles": ["admin", "reviewer"],
"groups": ["admin-group"]
}
// groups.json
{
"id": "admin-group",
"name": "Administrators",
"collections": ["*"]
}
Result: User has access to all collections and all permissions.
Example 4: Project Manager
// users.json
{
"username": "pm1",
"roles": ["user"],
"groups": ["*"]
}
Result: User has access to all collections but limited to user-level permissions (read-only).
Migration Notes
If migrating from a system without collection-based access control:
Create default collection:
./bin/manage.py collection add default "Default Collection"Create default group:
./bin/manage.py group add default "Default Group" ./bin/manage.py group add-collection default defaultAdd all users to default group:
./bin/manage.py user add-group user1 default ./bin/manage.py user add-group user2 default # ... for all usersAssign collections to existing documents (requires database update):
UPDATE files SET doc_collections = '["default"]' WHERE doc_collections IS NULL;
Document-Level Access Control Modes
In addition to collection-based access control, the system supports three modes for document-level permissions. This is configured application-wide via access-control.mode in config/config.json.
Configuration
{
"access-control.mode": "role-based",
"access-control.default-visibility": "collection",
"access-control.default-editability": "owner"
}
Mode values:
role-based(default) - No document-level permissions, only role restrictionsowner-based- Documents editable only by their creatorgranular- Database-backed per-document visibility and editability settings
Mode 1: Role-Based (Default)
In role-based mode, document access is determined solely by user roles and file types:
- Gold files: Only reviewers can edit
- Version files: Annotators and reviewers can edit
- Deletion: Only reviewers and document owners can delete
No document-level permission UI is shown. This mode is suitable for teams where role-based restrictions are sufficient.
Mode 2: Owner-Based
In owner-based mode, documents are editable only by their creator:
- Documents are read-only for everyone except the owner
- Reviewers can delete any document
- To edit a non-owned document, users must create their own version
When a non-owner opens a document, they see a notification: "This document is owned by [username]. Create your own version to edit."
Mode 3: Granular
Granular mode provides per-document visibility and editability settings stored in a SQLite database (data/db/permissions.db).
Permission attributes:
visibility:'collection'(visible to all with collection access) or'owner'(visible only to owner and reviewers)editability:'collection'(editable by all) or'owner'(editable only by owner)owner: Username of the document creator
UI: Owners and reviewers see two toggle switches in the status bar:
- Visibility switch: Toggle between "Visible to all" and "Visible to owner"
- Editability switch: Toggle between "Editable by all" and "Editable by owner"
Default permissions for new documents:
visibility: value ofaccess-control.default-visibility(default:collection)editability: value ofaccess-control.default-editability(default:owner)
Backend Implementation
Core modules:
- access_control.py - Mode-aware permission checking functions
- acl_utils.py - Role checking and high-level permission API
- permissions_db.py - SQLite database for granular permissions
API endpoints (granular mode only):
GET /api/v1/files/access_control_mode- Returns current mode and defaultsGET /api/v1/files/permissions/{stable_id}- Get permissions for a documentPOST /api/v1/files/set_permissions- Set permissions for a document
Permission checking functions:
from fastapi_app.lib.permissions.access_control import (
can_view_document,
can_edit_document,
can_delete_document,
can_modify_permissions,
can_promote_demote,
check_file_access, # Backwards-compatible wrapper
DocumentAccessFilter # For filtering file lists
)
High-level API (handles mode internally):
from fastapi_app.lib.permissions.acl_utils import (
get_access_control_mode,
get_file_permissions,
set_default_permissions_for_new_file,
delete_permissions_for_file
)
Frontend Implementation
The access-control.js plugin:
- Fetches the access control mode on startup via
filesAccessControlMode() - Shows/hides UI elements based on mode
- In granular mode, displays switches for owners/reviewers to modify permissions
- Enforces read-only state when user lacks edit permissions
ACL utilities in frontend:
import {
canEditDocumentWithPermissions,
canViewDocumentWithPermissions,
canEditFile,
userHasReviewerRole,
userHasAnnotatorRole
} from '../modules/acl-utils.js'
Database Schema (Granular Mode)
CREATE TABLE document_permissions (
stable_id TEXT PRIMARY KEY,
visibility TEXT NOT NULL DEFAULT 'collection',
editability TEXT NOT NULL DEFAULT 'owner',
owner TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CHECK (visibility IN ('collection', 'owner')),
CHECK (editability IN ('collection', 'owner'))
)
Reviewer Override Rules
Reviewers have elevated privileges but with intentional limitations:
- Can always: View all documents, delete any document, modify permissions
- Cannot: Edit documents with
editability: 'owner'when not the owner (prevents accidental overwriting) - To edit such documents, reviewers must create their own version or change the editability first
Troubleshooting
User cannot see any documents
Check:
- User has at least one group:
./bin/manage.py user list - Groups have collections:
./bin/manage.py group list - Documents have collections assigned (check
doc_collectionsin database)
User can see documents but cannot edit
Check:
- User has required role (annotator/reviewer) for the operation
- User has collection access to the document
- Document's visibility/editability settings (document-level ACL)
Wildcard not working
Check:
- Wildcard is exactly
"*"(string with single asterisk) - Wildcard is in the correct field (roles/groups/collections)
- No typos in JSON configuration files