Skip to content

Fine-Grained Access Control (FGAC) Design Proposal #3442

Description

@mcassaniti

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:

  1. 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.
  2. 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 Forbidden response listing all unauthorized fields:

      {
        "code": 403,
        "message": "You do not have permission to update fields: due_date, priority",
        "unauthorized_fields": [
          "due_date",
          "priority"
        ]
      }
  3. API Capability Export:

    • /api/v2 endpoints 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.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/api-v2Huma-backed /api/v2 API surfacearea/databaseDatabase engine behavior, schema issues, cross-engine DB bugsarea/permissionsSharing, link sharing, roles, access control, assignee roles

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions