506 lines
16 KiB
Markdown
506 lines
16 KiB
Markdown
# Roles and Permissions - Architecture Overview
|
|
|
|
**Project:** Mila - Membership Management System
|
|
**Feature:** Role-Based Access Control (RBAC) with Hardcoded Permission Sets
|
|
**Version:** 2.0
|
|
**Last Updated:** 2025-11-13
|
|
**Status:** Architecture Design - MVP Approach
|
|
|
|
---
|
|
|
|
## Purpose of This Document
|
|
|
|
This document provides a high-level, conceptual overview of the Roles and Permissions architecture without code examples. It is designed for quick understanding of architectural decisions and concepts.
|
|
|
|
**For detailed technical implementation:** See [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md)
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Overview](#overview)
|
|
2. [Requirements Summary](#requirements-summary)
|
|
3. [Evaluated Approaches](#evaluated-approaches)
|
|
4. [Selected Architecture](#selected-architecture)
|
|
5. [Permission System Design](#permission-system-design)
|
|
6. [User-Member Linking Strategy](#user-member-linking-strategy)
|
|
7. [Field-Level Permissions Strategy](#field-level-permissions-strategy)
|
|
8. [Migration Strategy](#migration-strategy)
|
|
9. [Related Documents](#related-documents)
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The Mila membership management system requires a flexible authorization system that controls:
|
|
- **Who** can access **what** resources
|
|
- **Which** pages users can view
|
|
- **How** users interact with their own vs. others' data
|
|
|
|
### Key Design Principles
|
|
|
|
1. **Simplicity First:** Start with hardcoded permissions for fast MVP delivery
|
|
2. **Performance:** No database queries for permission checks in MVP
|
|
3. **Clear Migration Path:** Easy upgrade to database-backed permissions when needed
|
|
4. **Security:** Explicit action-based authorization with no ambiguity
|
|
5. **Maintainability:** Permission logic reviewable in Git, testable as pure functions
|
|
|
|
### Core Concepts
|
|
|
|
**Permission Set:** Defines a collection of permissions (e.g., "read_only", "admin")
|
|
|
|
**Role:** A named job function that references one Permission Set (e.g., "Vorstand" uses "read_only")
|
|
|
|
**User:** Each user has exactly one Role, inheriting that Role's Permission Set
|
|
|
|
**Scope:** Defines the breadth of access - "own" (only own data), "linked" (data connected to user), "all" (everything)
|
|
|
|
---
|
|
|
|
## Evaluated Approaches
|
|
|
|
During the design phase, we evaluated multiple implementation approaches to find the optimal balance between simplicity, performance, and future extensibility.
|
|
|
|
### Approach 1: JSONB in Roles Table
|
|
|
|
Store all permissions as a single JSONB column directly in the roles table.
|
|
|
|
**Advantages:**
|
|
- Simplest database schema (single table)
|
|
- Very flexible structure
|
|
- No additional tables needed
|
|
- Fast to implement
|
|
|
|
**Disadvantages:**
|
|
- Poor queryability (can't efficiently filter by specific permissions)
|
|
- No referential integrity
|
|
- Difficult to validate structure
|
|
- Hard to audit permission changes
|
|
- Can't leverage database indexes effectively
|
|
|
|
**Verdict:** Rejected - Poor queryability makes it unsuitable for complex permission logic.
|
|
|
|
---
|
|
|
|
### Approach 2: Normalized Database Tables
|
|
|
|
Separate tables for `permission_sets`, `permission_set_resources`, `permission_set_pages` with full normalization.
|
|
|
|
**Advantages:**
|
|
- Fully queryable with SQL
|
|
- Runtime configurable permissions
|
|
- Strong referential integrity
|
|
- Easy to audit changes
|
|
- Can index for performance
|
|
|
|
**Disadvantages:**
|
|
- Complex database schema (4+ tables)
|
|
- DB queries required for every permission check
|
|
- Requires ETS cache for performance
|
|
- Needs admin UI for permission management
|
|
- Longer implementation time (4-5 weeks)
|
|
- Overkill for fixed set of 4 permission sets
|
|
|
|
**Verdict:** Deferred to Phase 3 - Excellent for runtime configuration but too complex for MVP.
|
|
|
|
---
|
|
|
|
### Approach 3: Custom Authorizer
|
|
|
|
Implement a custom Ash Authorizer from scratch instead of using Ash Policies.
|
|
|
|
**Advantages:**
|
|
- Complete control over authorization logic
|
|
- Can implement any custom behavior
|
|
- Not constrained by Ash Policy DSL
|
|
|
|
**Disadvantages:**
|
|
- Significantly more code to write and maintain
|
|
- Loses benefits of Ash's declarative policies
|
|
- Harder to test than built-in policy system
|
|
- Mixes declarative and imperative approaches
|
|
- Must reimplement filter generation for queries
|
|
- Higher bug risk
|
|
|
|
**Verdict:** Rejected - Too much custom code, reduces maintainability and loses Ash ecosystem benefits.
|
|
|
|
---
|
|
|
|
### Approach 4: Simple Role Enum
|
|
|
|
Add a simple `:role` enum field directly on User resource with hardcoded checks in each policy.
|
|
|
|
**Advantages:**
|
|
- Very simple to implement (< 1 week)
|
|
- No extra tables needed
|
|
- Fast performance
|
|
- Easy to understand
|
|
|
|
**Disadvantages:**
|
|
- No separation between roles and permissions
|
|
- Can't add new roles without code changes
|
|
- No dynamic permission configuration
|
|
- Not extensible to field-level permissions
|
|
- Violates separation of concerns (role = job function, not permission set)
|
|
- Difficult to maintain as requirements grow
|
|
|
|
**Verdict:** Rejected - Too inflexible, doesn't meet requirement for configurable permissions and role separation.
|
|
|
|
---
|
|
|
|
### Approach 5: Hardcoded Permissions with Migration Path (SELECTED for MVP)
|
|
|
|
Permission Sets hardcoded in Elixir module, only Roles table in database.
|
|
|
|
**Advantages:**
|
|
- Fast implementation (2-3 weeks vs 4-5 weeks)
|
|
- Maximum performance (zero DB queries, < 1 microsecond)
|
|
- Simple to test (pure functions)
|
|
- Code-reviewable permissions (visible in Git)
|
|
- No migration needed for existing data
|
|
- Clearly defined 4 permission sets as required
|
|
- Clear migration path to database-backed solution (Phase 3)
|
|
- Maintains separation of roles and permission sets
|
|
|
|
**Disadvantages:**
|
|
- Permissions not editable at runtime (only role assignment possible)
|
|
- New permissions require code deployment
|
|
- Not suitable if permissions change frequently (> 1x/week)
|
|
- Limited to the 4 predefined permission sets
|
|
|
|
**Why Selected:**
|
|
- MVP requirement is for 4 fixed permission sets (not custom ones)
|
|
- No stated requirement for runtime permission editing
|
|
- Performance is critical for authorization checks
|
|
- Fast time-to-market (2-3 weeks)
|
|
- Clear upgrade path when runtime configuration becomes necessary
|
|
|
|
**Migration Path:**
|
|
When runtime permission editing becomes a business requirement, migrate to Approach 2 (normalized DB tables) without changing the public API of the PermissionSets module.
|
|
|
|
---
|
|
|
|
## Requirements Summary
|
|
|
|
### Four Predefined Permission Sets
|
|
|
|
1. **own_data** - Access only to own user account and linked member profile
|
|
2. **read_only** - Read access to all members and custom fields
|
|
3. **normal_user** - Create/Read/Update members and full CRUD on custom fields (no member deletion for safety)
|
|
4. **admin** - Unrestricted access to all resources including user management
|
|
|
|
### Example Roles
|
|
|
|
- **Mitglied (Member)** - Uses "own_data" permission set, default role
|
|
- **Vorstand (Board)** - Uses "read_only" permission set
|
|
- **Kassenwart (Treasurer)** - Uses "normal_user" permission set
|
|
- **Buchhaltung (Accounting)** - Uses "read_only" permission set
|
|
- **Admin** - Uses "admin" permission set
|
|
|
|
### Authorization Levels
|
|
|
|
**Resource Level (MVP):**
|
|
- Controls create, read, update, destroy actions on resources
|
|
- Resources: Member, User, Property, PropertyType, Role
|
|
|
|
**Page Level (MVP):**
|
|
- Controls access to LiveView pages
|
|
- Example: "/members/new" requires Member.create permission
|
|
|
|
**Field Level (Phase 2 - Future):**
|
|
- Controls read/write access to specific fields
|
|
- Example: Only Treasurer can see payment_history field
|
|
|
|
### Special Cases
|
|
|
|
1. **Own Credentials:** Users can always edit their own email and password
|
|
2. **Linked Member Email:** Only admins can edit email of members linked to users
|
|
3. **User-Member Linking:** Only admins can link/unlink users to members (except self-service creation)
|
|
|
|
---
|
|
|
|
## Selected Architecture
|
|
|
|
### Conceptual Model
|
|
|
|
```
|
|
Elixir Module: PermissionSets
|
|
↓ (defines)
|
|
Permission Set (:own_data, :read_only, :normal_user, :admin)
|
|
↓ (referenced by)
|
|
Role (stored in DB: "Vorstand" → "read_only")
|
|
↓ (assigned to)
|
|
User (each user has one role_id)
|
|
```
|
|
|
|
### Database Schema (MVP)
|
|
|
|
**Single Table: roles**
|
|
|
|
Contains:
|
|
- id (UUID)
|
|
- name (e.g., "Vorstand")
|
|
- description
|
|
- permission_set_name (String: "own_data", "read_only", "normal_user", "admin")
|
|
- is_system_role (boolean, protects critical roles)
|
|
|
|
**No Permission Tables:** Permission Sets are hardcoded in Elixir module.
|
|
|
|
### Why This Approach?
|
|
|
|
**Fast Implementation:** 2-3 weeks instead of 4-5 weeks
|
|
|
|
**Maximum Performance:**
|
|
- Zero database queries for permission checks
|
|
- Pure function calls (< 1 microsecond)
|
|
- No caching needed
|
|
|
|
**Code Review:**
|
|
- Permissions visible in Git diffs
|
|
- Easy to review changes
|
|
- No accidental runtime modifications
|
|
|
|
**Clear Upgrade Path:**
|
|
- Phase 1 (MVP): Hardcoded
|
|
- Phase 2: Add field-level permissions
|
|
- Phase 3: Migrate to database-backed with admin UI
|
|
|
|
**Meets Requirements:**
|
|
- Four predefined permission sets ✓
|
|
- Dynamic role creation ✓ (Roles in DB)
|
|
- Role-to-user assignment ✓
|
|
- No requirement for runtime permission changes stated
|
|
|
|
---
|
|
|
|
## Permission System Design
|
|
|
|
### Permission Structure
|
|
|
|
Each Permission Set contains:
|
|
|
|
**Resources:** List of resource permissions
|
|
- resource: "Member", "User", "Property", etc.
|
|
- action: :read, :create, :update, :destroy
|
|
- scope: :own, :linked, :all
|
|
- granted: true/false
|
|
|
|
**Pages:** List of accessible page paths
|
|
- Examples: "/", "/members", "/members/:id/edit"
|
|
- "*" for admin (all pages)
|
|
|
|
### Scope Definitions
|
|
|
|
**:own** - Only records where id == actor.id
|
|
- Example: User can read their own User record
|
|
|
|
**:linked** - Only records where user_id == actor.id
|
|
- Example: User can read Member linked to their account
|
|
|
|
**:all** - All records without restriction
|
|
- Example: Admin can read all Members
|
|
|
|
### How Authorization Works
|
|
|
|
1. User attempts action on resource (e.g., read Member)
|
|
2. System loads user's role from database
|
|
3. Role contains permission_set_name string
|
|
4. PermissionSets module returns permissions for that set
|
|
5. Custom Policy Check evaluates permissions against action
|
|
6. Access granted or denied based on scope
|
|
|
|
### Custom Policy Check
|
|
|
|
A reusable Ash Policy Check that:
|
|
- Reads user's permission_set_name from their role
|
|
- Calls PermissionSets.get_permissions/1
|
|
- Matches resource + action against permissions list
|
|
- Applies scope filters (own/linked/all)
|
|
- Returns authorized, forbidden, or filtered query
|
|
|
|
---
|
|
|
|
## User-Member Linking Strategy
|
|
|
|
### Problem Statement
|
|
|
|
Users need to create member profiles for themselves (self-service), but only admins should be able to:
|
|
- Link existing members to users
|
|
- Unlink members from users
|
|
- Create members pre-linked to arbitrary users
|
|
|
|
### Selected Approach: Separate Ash Actions
|
|
|
|
Instead of complex field-level validation, we use action-based authorization.
|
|
|
|
### Actions on Member Resource
|
|
|
|
**1. create_member_for_self** (All authenticated users)
|
|
- Automatically sets user_id = actor.id
|
|
- User cannot specify different user_id
|
|
- UI: "Create My Profile" button
|
|
|
|
**2. create_member** (Admin only)
|
|
- Can set user_id to any user or leave unlinked
|
|
- Full flexibility for admin
|
|
- UI: Admin member management form
|
|
|
|
**3. link_member_to_user** (Admin only)
|
|
- Updates existing member to set user_id
|
|
- Connects unlinked member to user account
|
|
|
|
**4. unlink_member_from_user** (Admin only)
|
|
- Sets user_id to nil
|
|
- Disconnects member from user account
|
|
|
|
**5. update** (Permission-based)
|
|
- Normal updates (name, address, etc.)
|
|
- user_id NOT in accept list (prevents manipulation)
|
|
- Available to users with Member.update permission
|
|
|
|
### Why Separate Actions?
|
|
|
|
**Explicit Semantics:** Each action has clear, single purpose
|
|
|
|
**Server-Side Security:** user_id set by server, not client input
|
|
|
|
**Better UX:** Different UI flows for different use cases
|
|
|
|
**Simple Policies:** Authorization at action level, not field level
|
|
|
|
**Easy Testing:** Each action independently testable
|
|
|
|
---
|
|
|
|
## Field-Level Permissions Strategy
|
|
|
|
### Status: Phase 2 (Future Implementation)
|
|
|
|
Field-level permissions are NOT implemented in MVP but have a clear strategy defined.
|
|
|
|
### Problem Statement
|
|
|
|
Some scenarios require field-level control:
|
|
- **Read restrictions:** Hide payment_history from certain roles
|
|
- **Write restrictions:** Only treasurer can edit payment fields
|
|
- **Complexity:** Ash Policies work at resource level, not field level
|
|
|
|
### Selected Strategy
|
|
|
|
**For Read Restrictions:**
|
|
Use Ash Calculations or Custom Preparations
|
|
- Calculations: Dynamically compute field based on permissions
|
|
- Preparations: Filter select to only allowed fields
|
|
- Field returns nil or "[Hidden]" if unauthorized
|
|
|
|
**For Write Restrictions:**
|
|
Use Custom Validations
|
|
- Validate changeset against field permissions
|
|
- Similar to existing linked-member email validation
|
|
- Return error if field modification not allowed
|
|
|
|
### Why This Strategy?
|
|
|
|
**Leverages Ash Features:** Uses built-in mechanisms, not custom authorizer
|
|
|
|
**Performance:** Calculations are lazy, Preparations run once per query
|
|
|
|
**Maintainable:** Clear validation logic, standard Ash patterns
|
|
|
|
**Extensible:** Easy to add new field restrictions
|
|
|
|
### Implementation Timeline
|
|
|
|
**Phase 1 (MVP):** No field-level permissions
|
|
|
|
**Phase 2:** Extend PermissionSets to include field permissions, implement Calculations/Validations
|
|
|
|
**Phase 3:** If migrating to database, add permission_set_fields table
|
|
|
|
---
|
|
|
|
## Migration Strategy
|
|
|
|
### Phase 1: MVP with Hardcoded Permissions (2-3 weeks)
|
|
|
|
**What's Included:**
|
|
- Roles table in database
|
|
- PermissionSets Elixir module with 4 predefined sets
|
|
- Custom Policy Check reading from module
|
|
- UI Authorization Helpers for LiveView
|
|
- Admin UI for role management (create, assign, delete roles)
|
|
|
|
**Limitations:**
|
|
- Permissions not editable at runtime
|
|
- New permissions require code deployment
|
|
- Only 4 permission sets available
|
|
|
|
**Benefits:**
|
|
- Fast implementation
|
|
- Maximum performance
|
|
- Simple testing and review
|
|
|
|
### Phase 2: Field-Level Permissions (Future, 2-3 weeks)
|
|
|
|
**When Needed:** Business requires field-level restrictions
|
|
|
|
**Implementation:**
|
|
- Extend PermissionSets module with :fields key
|
|
- Add Ash Calculations for read restrictions
|
|
- Add custom validations for write restrictions
|
|
- Update UI Helpers
|
|
|
|
**Migration:** No database changes, pure code additions
|
|
|
|
### Phase 3: Database-Backed Permissions (Future, 3-4 weeks)
|
|
|
|
**When Needed:** Runtime permission configuration required
|
|
|
|
**Implementation:**
|
|
- Create permission tables in database
|
|
- Seed script to migrate hardcoded permissions
|
|
- Update PermissionSets module to query database
|
|
- Add ETS cache for performance
|
|
- Build admin UI for permission management
|
|
|
|
**Migration:** Seamless, no changes to existing Policies or UI code
|
|
|
|
### Decision Matrix: When to Migrate?
|
|
|
|
| Scenario | Recommended Phase |
|
|
|----------|-------------------|
|
|
| MVP with 4 fixed permission sets | Phase 1 |
|
|
| Need field-level restrictions | Phase 2 |
|
|
| Permission changes < 1x/month | Stay Phase 1 |
|
|
| Need runtime permission config | Phase 3 |
|
|
| Custom permission sets needed | Phase 3 |
|
|
| Permission changes > 1x/week | Phase 3 |
|
|
|
|
---
|
|
|
|
## Related Documents
|
|
|
|
**This Document (Overview):** High-level concepts, no code examples
|
|
|
|
**[roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md):** Complete technical specification with code examples
|
|
|
|
**[roles-and-permissions-implementation-plan.md](./roles-and-permissions-implementation-plan.md):** Detailed implementation plan with TDD approach
|
|
|
|
**[CODE_GUIDELINES.md](../CODE_GUIDELINES.md):** Project coding standards
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
The selected architecture uses **hardcoded Permission Sets in Elixir** for the MVP, providing:
|
|
- **Speed:** 2-3 weeks implementation vs 4-5 weeks
|
|
- **Performance:** Zero database queries for authorization
|
|
- **Clarity:** Permissions in Git, reviewable and testable
|
|
- **Flexibility:** Clear migration path to database-backed system
|
|
|
|
**User-Member linking** uses **separate Ash Actions** for clarity and security.
|
|
|
|
**Field-level permissions** have a **defined strategy** (Calculations + Validations) for Phase 2 implementation.
|
|
|
|
The approach balances pragmatism for MVP delivery with extensibility for future requirements.
|
|
|