This proposal expands on the discussions in #3298 and #3299 by moving from coarse 4-tier permissions to a compact, bitmask-based Fine-Grained Access Control (FGAC) system. This is a proposal, and I'm hoping to get a discussion around the backend model first before implementing. After that we can tackle any frontend details.
1. Database Storage & Project Access Model
In Vikunja, project sharing is mapped through users_projects (user shares), team_projects (team shares), link_sharings (public shares), and direct ownership (projects.owner_id):
projects Table: Add use_fine_grained_acls BOOLEAN DEFAULT false.
users_projects, team_projects, link_sharings Tables: Add fine_grained_acls BIGINT UNSIGNED DEFAULT 0.
Read Access Rule (Project Access)
- Row Existence = Project Read Access: The presence of a record in
users_projects or team_projects grants Project Read Access (canRead = true). A user with a mapping row can fetch and view the project and all its task properties.
fine_grained_acls = 0: Represents Read-Only Project Access (0 field write bits set, writable_fields: []).
fine_grained_acls > 0: Grants Project Read Access + specific attribute write permissions (writable_fields: [...]).
2. Permission Hierarchy & Inheritance
Permissions are resolved hierarchically using Bitwise OR (|), creating an additive permission model:
effective_acls = user_acls | team_acls_1 | team_acls_2 | parent_project_acls
- Additive Model: A user receives a field write permission if any assigned role, team, or parent project grants that bit.
- Coarse-Grained Fallback: When
use_fine_grained_acls is false for a project, standard 4-tier permission levels map to default bitmasks:
PermissionRead (0): 0x0
PermissionUpdate (1): TaskFieldState | TaskFieldComments
PermissionWrite (2): All defined task field bits enabled (TaskWriteAllBits), granting full write access to all task attributes without project admin rights.
PermissionAdmin (3): 0xFFFFFFFFFFFFFFFF (All 64 bits enabled as a universal wildcard).
- Forward Compatibility (
WriteAll Dynamic Expansion):
- To prevent administrative burden when updating Vikunja, permissions configured with full task write access (
TaskWriteAllBits) automatically inherit write access to newly introduced task field bits in future software versions.
TaskWriteAllBits is dynamically evaluated as the Bitwise OR of all registered task field bits in the running version.
3. PATCH-Based API Semantics (/api/v2)
Currently, task updates send full task payloads. Under FGAC, task mutations transition to PATCH /api/v2/tasks/{id} HTTP semantics:
-
Partial Body Validation:
- The backend unmarshals only the fields explicitly present in the
PATCH JSON body.
- Permission authorization checks
Task.CanUpdateField ONLY for the fields included in the request payload.
-
Targeted Forbidden Responses (All Failed Fields):
-
API Capability Export:
4. DB Query Impact & Performance Analysis
FGAC permission validation takes place in Go memory using project-level bitmask caching (map[int64]uint64), avoiding un-indexable bitwise SQL queries.
| Use Case |
Execution Model |
DB Query Impact |
| Bulk Project ACL Resolution |
checkPermissionsForProjects (Recursive CTE in pkg/models/project_permissions.go) |
Evaluates BIT_OR(...) per project. Zero row-level overhead. |
| Task Search & Filtering |
dbTaskSearcher.Search (pkg/models/task_search.go) |
Scoped by project ID (WHERE tasks.project_id IN (...)). Zero bitwise SQL WHERE clauses. |
| Task Collection Loading |
TaskCollection.ReadAll (pkg/models/task_collection.go) |
Loads full task rows. Computes writable_fields array per task payload in Go. |
| Kanban Bucket Moves |
TaskBucket.CanUpdate (pkg/models/kanban_task_bucket.go) |
Validates bitmask.CanUpdateField(TaskFieldState) in Go memory. |
| Task Mutation Validation |
Task.CanUpdate (pkg/models/tasks_permissions.go) |
Compares modified struct fields against effective bitmask in Go memory. |
5. Suggested Mapping of Permission Bits
Bits 0 through 63 are reserved strictly for feature and field write capabilities. 0xFFFFFFFFFFFFFFFF represents Admin / Full Access:
| Bit Position |
Enum Constant |
Covered Attributes / Capabilities |
1 << 0 (1) |
TaskFieldState |
Completion status (done, done_at) and bucket transitions (bucket_id). |
1 << 1 (2) |
TaskFieldTitle |
Task title / summary. |
1 << 2 (4) |
TaskFieldDescription |
Rich-text description. |
1 << 3 (8) |
TaskFieldDates |
due_date, start_date, end_date. |
1 << 4 (16) |
TaskFieldPriority |
Priority level and task color. |
1 << 5 (32) |
TaskFieldAssignees |
Adding or removing assignees. |
1 << 6 (64) |
TaskFieldLabels |
Adding or removing labels. |
1 << 7 (128) |
TaskFieldAttachments |
Uploading or deleting file attachments. |
1 << 8 (256) |
TaskFieldComments |
Adding comments. |
1 << 9 (512) |
TaskFieldReminders |
Setting reminder triggers. |
1 << 10 (1024) |
TaskFieldRelations |
Subtasks and task relationships. |
6. API Data Visibility & UI Enforcement
- Full API Data Return (Read vs. Write):
- Read Visibility: Governed at the project level (a user with project read permission can fetch and view all task properties). Fine-grained ACLs do not redact or hide fields from the API payload.
- Write Mutability: Governed by
writable_fields: []string. The API returns all task properties alongside the writable_fields list.
- UI Responsibility:
- The frontend uses
writable_fields to render non-writable task attributes in read-only mode (e.g. static labels, disabled datepickers, read-only text fields) so users can view details without being given edit controls. An empty field may be omitted if not writeable and empty.
- Action buttons (e.g., Delete Task, Move Project, Share Project) remain hidden if the corresponding capability is omitted.
This proposal expands on the discussions in #3298 and #3299 by moving from coarse 4-tier permissions to a compact, bitmask-based Fine-Grained Access Control (FGAC) system. This is a proposal, and I'm hoping to get a discussion around the backend model first before implementing. After that we can tackle any frontend details.
1. Database Storage & Project Access Model
In Vikunja, project sharing is mapped through
users_projects(user shares),team_projects(team shares),link_sharings(public shares), and direct ownership (projects.owner_id):projectsTable: Adduse_fine_grained_acls BOOLEAN DEFAULT false.users_projects,team_projects,link_sharingsTables: Addfine_grained_acls BIGINT UNSIGNED DEFAULT 0.Read Access Rule (Project Access)
users_projectsorteam_projectsgrants Project Read Access (canRead = true). A user with a mapping row can fetch and view the project and all its task properties.fine_grained_acls = 0: Represents Read-Only Project Access (0 field write bits set,writable_fields: []).fine_grained_acls > 0: Grants Project Read Access + specific attribute write permissions (writable_fields: [...]).2. Permission Hierarchy & Inheritance
Permissions are resolved hierarchically using Bitwise OR (
|), creating an additive permission model:use_fine_grained_aclsisfalsefor a project, standard 4-tier permission levels map to default bitmasks:PermissionRead (0):0x0PermissionUpdate (1):TaskFieldState | TaskFieldCommentsPermissionWrite (2): All defined task field bits enabled (TaskWriteAllBits), granting full write access to all task attributes without project admin rights.PermissionAdmin (3):0xFFFFFFFFFFFFFFFF(All 64 bits enabled as a universal wildcard).WriteAllDynamic Expansion):TaskWriteAllBits) automatically inherit write access to newly introduced task field bits in future software versions.TaskWriteAllBitsis dynamically evaluated as the Bitwise OR of all registered task field bits in the running version.3. PATCH-Based API Semantics (
/api/v2)Currently, task updates send full task payloads. Under FGAC, task mutations transition to
PATCH /api/v2/tasks/{id}HTTP semantics:Partial Body Validation:
PATCHJSON body.Task.CanUpdateFieldONLY for the fields included in the request payload.Targeted Forbidden Responses (All Failed Fields):
If a user sends a payload containing multiple fields they lack permission to modify, the API evaluates all patched fields and returns a single
403 Forbiddenresponse listing all unauthorized fields:{ "code": 403, "message": "You do not have permission to update fields: due_date, priority", "unauthorized_fields": [ "due_date", "priority" ] }API Capability Export:
/api/v2endpoints export permission capabilities as a list of strings so frontend Vue components and external clients know which fields can be patched:{ "id": 42, "title": "Migrate Database Schema", "due_date": "2026-08-15T00:00:00Z", "writable_fields": [ "state", "title", "description", "due_date", "comments" ], "is_admin": false }4. DB Query Impact & Performance Analysis
FGAC permission validation takes place in Go memory using project-level bitmask caching (
map[int64]uint64), avoiding un-indexable bitwise SQL queries.checkPermissionsForProjects(Recursive CTE inpkg/models/project_permissions.go)BIT_OR(...)per project. Zero row-level overhead.dbTaskSearcher.Search(pkg/models/task_search.go)WHERE tasks.project_id IN (...)). Zero bitwise SQLWHEREclauses.TaskCollection.ReadAll(pkg/models/task_collection.go)writable_fieldsarray per task payload in Go.TaskBucket.CanUpdate(pkg/models/kanban_task_bucket.go)bitmask.CanUpdateField(TaskFieldState)in Go memory.Task.CanUpdate(pkg/models/tasks_permissions.go)5. Suggested Mapping of Permission Bits
Bits
0through63are reserved strictly for feature and field write capabilities.0xFFFFFFFFFFFFFFFFrepresentsAdmin/Full Access:1 << 0(1)TaskFieldStatedone,done_at) and bucket transitions (bucket_id).1 << 1(2)TaskFieldTitle1 << 2(4)TaskFieldDescription1 << 3(8)TaskFieldDatesdue_date,start_date,end_date.1 << 4(16)TaskFieldPriority1 << 5(32)TaskFieldAssignees1 << 6(64)TaskFieldLabels1 << 7(128)TaskFieldAttachments1 << 8(256)TaskFieldComments1 << 9(512)TaskFieldReminders1 << 10(1024)TaskFieldRelations6. API Data Visibility & UI Enforcement
writable_fields: []string. The API returns all task properties alongside thewritable_fieldslist.writable_fieldsto render non-writable task attributes in read-only mode (e.g. static labels, disabled datepickers, read-only text fields) so users can view details without being given edit controls. An empty field may be omitted if not writeable and empty.