Skip to content

Roles and Privileges Assessment and Recommendation

Date: 2025-10-17 Author: Claude Code Status: Recommendation - Pending Implementation

Executive Summary

This document assesses the current roles and privileges implementation in S5 Slidefactory for both API Keys and User Accounts, identifies issues with the current dual approach, and recommends a unified scope-based authorization system.

Recommendation: Adopt scope-based authorization (currently used for API keys) for ALL access control - both API keys and user sessions.

Current State Analysis

API Keys Implementation

Location: app/api/models.py, app/api/auth.py, cli/commands/api_key.py

Architecture: - Fully database-backed with api_keys table - Scope-based permissions using JSON array - Fine-grained, action-level authorization - Full lifecycle management via CLI

Features:

# API Key Model
class ApiKey(Base):
    scopes = Column(JSON)  # e.g., ["templates:read", "presentations:generate", "*"]
    is_active = Column(Boolean)
    expires_at = Column(DateTime)
    usage_count = Column(Integer)
    rate_limit_per_hour = Column(Integer)

Authorization Pattern:

# Scope checking
def has_scope(self, scope: str) -> bool:
    scopes_list = self.scopes if self.scopes else []
    return scope in scopes_list or "*" in scopes_list

# Usage
if not api_key.has_scope("templates:write"):
    raise HTTPException(403, "Missing scope")

Predefined Scopes (app/api/auth.py:209-232): - * - Full access - templates:read, templates:write, templates:delete - presentations:generate, presentations:read - workflows:read, workflows:execute - results:read, results:write, results:delete

Management Tools: - CLI: python -m cli api-key create/list/test/revoke - Security: SHA256 hashing, key prefix for identification - Tracking: Last used timestamp, usage count

User Accounts Implementation

Location: app/auth/users/models.py, app/auth/auth.py, app/config.py

Architecture: DUAL STORAGE (Problem!) 1. Database: app_users table (app/auth/users/models.py:8-18) 2. Config File: Hardcoded ALLOWED_USERS dict (app/config.py:38-92)

Features:

# User Model (Database - Currently UNUSED)
class User(Base):
    __tablename__ = "app_users"
    email = Column(String(255), unique=True)
    attributes = Column(JSON)  # JSON blob for permissions
    active = Column(Boolean)

# Config File (Currently ACTIVE)
ALLOWED_USERS = {
    "user@example.com": {
        "id": -100,
        "attributes": {
            "checks": ["view", "admin"],
            "prompts": ["view", "admin"],
            "contexts": ["view", "admin"],
            "users": ["view", "admin"],
            "files": ["view", "admin"],
            "workflows": ["esg2", "poc_ideation"]
        }
    }
}

Authorization Pattern:

# Workflow-level access check
user_workflows = user_config.get('attributes', {}).get('workflows', [])
if workflow_folder not in user_workflows:
    return None

# Resource-level permissions (app/filemanager/workflow_files/permissions.py)
def check_template_permission(user, workflow_folder, action):
    user_permissions = get_user_workflow_permissions(user, workflow_folder)
    template_perms = user_permissions.get('templates', [])
    return action in template_perms

Session-Based Auth: - FastAPI Sessions with in-memory backend - Cookie-based authentication - SessionData contains user attributes - verifier dependency for protected endpoints

Permission Checking Patterns

Current State - Inconsistent:

  1. API Endpoints with API key support (app/api/presentations.py):
    async def generate_presentation(
        user: Union[SessionData, ApiKeyAuth] = Depends(verify_api_access)
    )
    
  2. Uses verify_api_access (allows both API key and session)
  3. API keys check scopes
  4. Sessions check workflow attributes

  5. Web UI Endpoints (app/filemanager/workflow_files/router.py:46):

    async def list_workflow_templates(
        session: SessionData = Depends(verifier)
    )
    

  6. Uses verifier (session only)
  7. Custom permission checking logic
  8. Manual mapping in permissions.py

  9. Hybrid Permission Checking (app/filemanager/workflow_files/permissions.py:13-48):

    def check_template_permission(user, workflow_folder, action):
        # Check if API key
        if isinstance(user, ApiKeyAuth):
            scope_map = {'view': 'templates:read', 'upload': 'templates:write'}
            return user.has_scope(scope_map.get(action))
    
        # Check if session user
        user_permissions = get_user_workflow_permissions(user, workflow_folder)
        return action in user_permissions.get('templates', [])
    

Issues with Current Approach

1. Conflicting User Storage

  • Problem: Users defined in both database table AND config file
  • Impact: Database table is unused, all auth happens via config
  • Risk: Database schema exists but provides no value

2. Inconsistent Permission Models

  • API Keys: Scope-based (templates:read)
  • Users: Attribute-based (attributes.workflows, attributes.checks)
  • Impact: Duplicate permission checking logic, harder to maintain

3. No Unified RBAC

  • Problem: "admin" is per-resource, not a role
  • Example: User can be admin for "checks" but not for "prompts"
  • Impact: Cannot easily grant "admin everywhere" or "editor everywhere"

4. Limited Permission Granularity

  • Problem: Users get workflow-level access, actions are binary (view/admin)
  • Missing: Cannot grant "read templates but not delete" for workflows
  • Impact: Either full admin or just view, no middle ground

5. Hardcoded Permission Logic

  • Problem: permissions.py has manual action→scope mapping
  • Example: {'view': 'templates:read', 'upload': 'templates:write'}
  • Impact: Adding new resources requires code changes in multiple places

6. Config File Scalability

  • Problem: Users hardcoded in config.py
  • Impact: Requires code deployment to add/remove users
  • Risk: User credentials in version control

Requirements Analysis

Based on user input:

  1. Access Type: Equal mix of API and web UI access
  2. Authorization Model: Scope-based preferred over roles (simpler)
  3. Storage: Database only (no config file)
  4. Granularity:
  5. ✅ Workflow-level (control which workflows user can access)
  6. ✅ Action-level (read, write, execute, delete)
  7. ✅ Resource-level (templates, outputs, contexts)
  8. ❌ Object-level (not required)

Recommendation: Unified Scope-Based Authorization

Overview

Adopt the scope-based approach (currently used for API keys) for ALL access control - both API keys and user sessions.

Why Scope-Based?

  1. Already 50% Implemented: API key system works well, extend it
  2. Unified Model: Single permission system for API keys and users
  3. Simplicity: Easier to understand than role hierarchies
  4. Flexibility: Fine-grained permissions without complex roles
  5. Database-First: Aligns with requirement for database-only storage
  6. Scalable: Adding new resources/actions is straightforward
  7. Industry Standard: OAuth 2.0, Google APIs, AWS IAM all use scopes

Proposed Scope Hierarchy

Format: <resource>:<action> or <resource>:<workflow_folder>:<action>

# Global Scopes (all workflows)
*                              # Full access (superadmin)

# Resource-Level Scopes (all workflows)
templates:read                 # Read all templates
templates:write                # Create/update all templates
templates:delete               # Delete any template

presentations:read             # View all presentations
presentations:generate         # Generate presentations

workflows:read                 # View all workflows
workflows:execute              # Execute any workflow

results:read                   # View all results
results:write                  # Create/update results
results:delete                 # Delete results

contexts:read                  # View all contexts
contexts:write                 # Manage contexts

users:read                     # View users
users:write                    # Manage users (admin function)

# Workflow-Specific Scopes (granular control)
workflows:esg2:read            # View specific workflow
workflows:esg2:execute         # Execute specific workflow
workflows:presales_poc:execute # Execute specific workflow

templates:esg2:read            # Read templates for specific workflow
templates:esg2:write           # Manage templates for specific workflow
templates:presales_poc:write   # Manage templates for specific workflow

# Scope Resolution (most specific wins)
1. Check workflow-specific: templates:esg2:write
2. Fallback to global: templates:write
3. Fallback to wildcard: *

Example Scope Assignments

Superadmin User:

["*"]

ESG Team Member (workflow-specific):

[
  "workflows:esg2:read",
  "workflows:esg2:execute",
  "templates:esg2:read",
  "presentations:read",
  "results:read"
]

ESG Administrator (workflow admin):

[
  "workflows:esg2:read",
  "workflows:esg2:execute",
  "workflows:esg3:read",
  "workflows:esg3:execute",
  "templates:esg2:write",
  "templates:esg3:write",
  "contexts:write",
  "presentations:generate",
  "presentations:read",
  "results:write"
]

API Integration (automation):

[
  "presentations:generate",
  "presentations:read",
  "results:read"
]

Platform Administrator:

[
  "*"
]

Implementation Plan

Phase 1: Database Schema Changes

Goal: Add scopes to users, prepare for config migration

Tasks: 1. Add scopes JSONB column to app_users table 2. Create Alembic migration 3. Add indexes for performance 4. Create conversion utility: attributes → scopes

Migration Script:

-- Add scopes column
ALTER TABLE app_users ADD COLUMN scopes JSONB NOT NULL DEFAULT '[]'::jsonb;

-- Add index for scope queries
CREATE INDEX idx_app_users_scopes ON app_users USING gin(scopes);

-- Add index for active users
CREATE INDEX idx_app_users_active ON app_users(active) WHERE active = true;

Conversion Logic (attributes → scopes):

def convert_attributes_to_scopes(attributes: dict) -> List[str]:
    scopes = []

    # Convert workflow access
    for workflow in attributes.get('workflows', []):
        scopes.append(f"workflows:{workflow}:read")
        scopes.append(f"workflows:{workflow}:execute")

    # Convert resource permissions
    resource_map = {
        'checks': 'checks',
        'prompts': 'prompts',
        'contexts': 'contexts',
        'users': 'users',
        'files': 'templates'
    }

    for attr_key, scope_resource in resource_map.items():
        perms = attributes.get(attr_key, [])
        if 'view' in perms:
            scopes.append(f"{scope_resource}:read")
        if 'admin' in perms:
            scopes.append(f"{scope_resource}:write")
            scopes.append(f"{scope_resource}:delete")

    return scopes

Files to Modify: - Create: alembic/versions/YYYYMMDD_add_user_scopes.py - Create: scripts/migrate_users_to_scopes.py

Phase 2: Auth Layer Unification

Goal: Create unified authorization interface

Tasks: 1. Create BaseAuth mixin with scope checking 2. Extend both ApiKeyAuth and SessionData from BaseAuth 3. Update SessionData to load scopes from database 4. Ensure verify_api_access works seamlessly for both

Architecture:

# Base authorization interface
class BaseAuth:
    scopes: List[str] = []

    def has_scope(self, scope: str) -> bool:
        """Check if has specific scope or wildcard."""
        return scope in self.scopes or "*" in self.scopes

    def has_workflow_access(self, workflow: str, action: str = "read") -> bool:
        """Check workflow-specific access with fallback to global."""
        # 1. Try workflow-specific scope
        if self.has_scope(f"workflows:{workflow}:{action}"):
            return True
        # 2. Fall back to global workflow scope
        if self.has_scope(f"workflows:{action}"):
            return True
        # 3. Fall back to wildcard
        return self.has_scope("*")

    def has_resource_access(self, resource: str, action: str, workflow: str = None) -> bool:
        """Check resource access with optional workflow scoping."""
        # 1. Try workflow-specific resource scope
        if workflow and self.has_scope(f"{resource}:{workflow}:{action}"):
            return True
        # 2. Fall back to global resource scope
        if self.has_scope(f"{resource}:{action}"):
            return True
        # 3. Fall back to wildcard
        return self.has_scope("*")

# API Key Auth (already has scopes from database)
class ApiKeyAuth(BaseAuth):
    def __init__(self, api_key: ApiKey, user_id: str, user_email: str = None):
        self.api_key = api_key
        self.scopes = api_key.scopes or []
        # ... rest of init

# Session Data (add scopes from user)
class SessionData(BaseModel, BaseAuth):
    id: Optional[int] = None
    email: Optional[str] = None
    scopes: List[str] = []  # NEW: Load from database
    # ... rest of fields

User Login Update:

# In auth router, when creating session
async def login(username: str, password: str):
    # Verify credentials
    user = db.query(User).filter(User.email == username).first()
    if not user or not verify_password(password, user.password):
        raise HTTPException(401, "Invalid credentials")

    # Create session with scopes
    session_data = SessionData(
        id=user.id,
        email=user.email,
        name=user.name,
        scopes=user.scopes or [],  # Load from database
        attributes=user.attributes or {}
    )

    # Store session
    await backend.create(session_id, session_data)

Files to Modify: - Modify: app/api/auth.py (add BaseAuth) - Modify: app/auth/auth.py (update SessionData) - Modify: app/auth/router.py (load scopes on login)

Phase 3: Application Layer Updates

Goal: Replace all permission checks with scope validation

Tasks: 1. Create scope checking decorators/dependencies 2. Update all check_*_permission() functions 3. Replace Depends(verifier) with Depends(verify_api_access) everywhere 4. Remove custom permission mapping logic

Scope Checking Helpers:

# In app/api/auth.py

def require_scope(*required_scopes: str):
    """Dependency that requires one or more scopes."""
    async def _verify(
        user: Union[SessionData, ApiKeyAuth] = Depends(verify_api_access)
    ) -> Union[SessionData, ApiKeyAuth]:
        # Check if user has ANY of the required scopes
        for scope in required_scopes:
            if user.has_scope(scope):
                return user

        raise HTTPException(
            status_code=403,
            detail=f"Missing required scope. Need one of: {', '.join(required_scopes)}"
        )

    return _verify

def require_workflow_access(workflow_param: str = "workflow_folder", action: str = "read"):
    """Dependency that checks workflow-specific access."""
    async def _verify(
        user: Union[SessionData, ApiKeyAuth] = Depends(verify_api_access),
        workflow: str = Path(..., alias=workflow_param)
    ) -> Union[SessionData, ApiKeyAuth]:
        if not user.has_workflow_access(workflow, action):
            raise HTTPException(
                status_code=403,
                detail=f"Missing workflow access: {workflow}:{action}"
            )
        return user

    return _verify

Updated Endpoint Examples:

# API endpoint with scope check
@router.post("/api/presentations/generate")
async def generate_presentation(
    request: PresentationGenerateRequest,
    user: Union[SessionData, ApiKeyAuth] = Depends(require_scope("presentations:generate"))
):
    # User guaranteed to have presentations:generate scope
    pass

# Workflow-specific template endpoint
@router.get("/api/workflow-files/workflows/{workflow_folder}/templates")
async def list_workflow_templates(
    workflow_folder: str,
    user: Union[SessionData, ApiKeyAuth] = Depends(require_workflow_access(action="read"))
):
    # User guaranteed to have workflows:{workflow_folder}:read scope
    # Additional check for templates
    if not user.has_resource_access("templates", "read", workflow_folder):
        raise HTTPException(403, "Cannot read templates for this workflow")

    templates = await list_templates(workflow_folder, user)
    return templates

# Flexible scope checking
@router.post("/api/workflow-files/workflows/{workflow_folder}/templates")
async def upload_template(
    workflow_folder: str,
    file: UploadFile,
    user: Union[SessionData, ApiKeyAuth] = Depends(verify_api_access)
):
    # Check workflow-specific OR global template write scope
    if not user.has_resource_access("templates", "write", workflow_folder):
        raise HTTPException(
            403,
            f"Missing scope: templates:{workflow_folder}:write or templates:write"
        )

    template = await upload_template(workflow_folder, file, user)
    return template

Simplified Permissions Module:

# app/filemanager/workflow_files/permissions.py - SIMPLIFIED

def check_template_permission(user: Union[SessionData, ApiKeyAuth],
                              workflow_folder: str,
                              action: str) -> bool:
    """Check if user can perform template action."""
    action_map = {'view': 'read', 'upload': 'write', 'delete': 'delete'}
    scope_action = action_map.get(action, action)
    return user.has_resource_access("templates", scope_action, workflow_folder)

def check_output_permission(user: Union[SessionData, ApiKeyAuth],
                           workflow_folder: str,
                           action: str) -> bool:
    """Check if user can perform output action."""
    action_map = {'view': 'read', 'download': 'read', 'delete': 'delete'}
    scope_action = action_map.get(action, action)
    return user.has_resource_access("outputs", scope_action, workflow_folder)

# Remove get_user_workflow_permissions - no longer needed
# Remove get_user_accessible_workflows - replaced by scope checking

Files to Modify: - Modify: app/api/auth.py (add helpers) - Modify: app/filemanager/workflow_files/permissions.py (simplify) - Modify: app/filemanager/workflow_files/router.py (use new dependencies) - Modify: app/api/presentations.py (already uses verify_api_access) - Modify: app/api/templates.py (add scope checks) - Modify: app/api/results.py (add scope checks) - Modify: All other routers with Depends(verifier)

Phase 4: User Management System

Goal: Create tools for managing users and scopes

Tasks: 1. Create CLI commands for user management 2. Create admin web UI for user management 3. Create scope presets/templates for common roles 4. Migration utility to import users from config

CLI Commands:

# User management
python -m cli user create <email> --name "John Doe" --password "secret"
python -m cli user list
python -m cli user update <email> --scopes "templates:read,presentations:generate"
python -m cli user add-scope <email> "workflows:esg2:execute"
python -m cli user remove-scope <email> "workflows:esg2:execute"
python -m cli user deactivate <email>
python -m cli user activate <email>

# Scope presets
python -m cli user create <email> --preset admin
python -m cli user create <email> --preset workflow-user --workflow esg2
python -m cli user create <email> --preset api-only

# Migration
python -m cli migrate-users-from-config

CLI Implementation:

# cli/commands/user.py

SCOPE_PRESETS = {
    'admin': ['*'],
    'workflow-admin': lambda workflow: [
        f'workflows:{workflow}:read',
        f'workflows:{workflow}:execute',
        f'templates:{workflow}:write',
        f'presentations:generate',
        f'results:write'
    ],
    'workflow-user': lambda workflow: [
        f'workflows:{workflow}:read',
        f'workflows:{workflow}:execute',
        f'templates:{workflow}:read',
        f'presentations:read',
        f'results:read'
    ],
    'api-only': [
        'presentations:generate',
        'presentations:read',
        'results:read'
    ]
}

def create_user(email: str, name: str, password: str,
                scopes: List[str] = None, preset: str = None,
                workflow: str = None):
    """Create a new user."""
    from app.auth.users.models import User
    from app.util.database import SessionLocal
    import bcrypt

    db = SessionLocal()
    try:
        # Check if user exists
        existing = db.query(User).filter(User.email == email).first()
        if existing:
            print_error(f"User {email} already exists")
            return

        # Determine scopes
        if preset:
            if preset in ['workflow-admin', 'workflow-user']:
                if not workflow:
                    print_error("--workflow required for this preset")
                    return
                user_scopes = SCOPE_PRESETS[preset](workflow)
            else:
                user_scopes = SCOPE_PRESETS[preset]
        else:
            user_scopes = scopes or []

        # Hash password
        password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

        # Create user
        user = User(
            email=email,
            name=name,
            password=password_hash,
            scopes=user_scopes,
            active=True
        )

        db.add(user)
        db.commit()

        print_success(f"User {email} created successfully!")
        print_field("Scopes", ', '.join(user_scopes))

    finally:
        db.close()

Admin Web UI (future): - User list with search/filter - User detail page with scope management - Scope builder UI with checkboxes - Workflow assignment interface - Activity logs

Files to Create: - Create: cli/commands/user.py - Create: scripts/migrate_users_from_config.py - Create: app/auth/users/router.py (admin API) - Create: app/auth/users/templates/ (admin UI)

Phase 5: Configuration Cleanup

Goal: Remove hardcoded users from config

Tasks: 1. Run migration script to import users from config 2. Update config.py to remove ALLOWED_USERS 3. Add environment variable for emergency admin 4. Update documentation

Emergency Admin Pattern:

# In config.py - keep ONE emergency admin for bootstrapping
EMERGENCY_ADMIN_EMAIL = os.environ.get("EMERGENCY_ADMIN_EMAIL")
EMERGENCY_ADMIN_PASSWORD = os.environ.get("EMERGENCY_ADMIN_PASSWORD")

# In auth router
async def login(username: str, password: str):
    # Check emergency admin first (if configured)
    if (settings.EMERGENCY_ADMIN_EMAIL and
        username == settings.EMERGENCY_ADMIN_EMAIL and
        password == settings.EMERGENCY_ADMIN_PASSWORD):

        return SessionData(
            email=username,
            name="Emergency Admin",
            scopes=["*"]  # Full access
        )

    # Otherwise check database
    user = db.query(User).filter(User.email == username).first()
    # ... normal auth flow

Files to Modify: - Modify: app/config.py (remove ALLOWED_USERS, add emergency admin) - Modify: app/auth/router.py (add emergency admin check) - Update: README.md (document new user management) - Update: CLAUDE.md (update auth section)

Phase 6: Testing & Validation

Goal: Ensure all functionality still works

Tasks: 1. Unit tests for scope checking logic 2. Integration tests for auth flows 3. Manual testing checklist 4. Permission audit

Test Cases:

# tests/unit/auth/test_scopes.py

def test_scope_checking():
    """Test basic scope checking."""
    auth = BaseAuth()
    auth.scopes = ["templates:read", "presentations:generate"]

    assert auth.has_scope("templates:read") == True
    assert auth.has_scope("templates:write") == False
    assert auth.has_scope("presentations:generate") == True

def test_wildcard_scope():
    """Test wildcard scope grants everything."""
    auth = BaseAuth()
    auth.scopes = ["*"]

    assert auth.has_scope("anything") == True
    assert auth.has_scope("workflows:esg2:execute") == True

def test_workflow_specific_scope():
    """Test workflow-specific access."""
    auth = BaseAuth()
    auth.scopes = ["workflows:esg2:read", "workflows:esg2:execute"]

    assert auth.has_workflow_access("esg2", "read") == True
    assert auth.has_workflow_access("esg2", "execute") == True
    assert auth.has_workflow_access("esg3", "read") == False

def test_resource_access_with_workflow():
    """Test resource access with workflow scoping."""
    auth = BaseAuth()
    auth.scopes = ["templates:esg2:write", "templates:read"]

    # Has workflow-specific write
    assert auth.has_resource_access("templates", "write", "esg2") == True
    # Does not have workflow-specific write for other workflow
    assert auth.has_resource_access("templates", "write", "esg3") == False
    # Has global read
    assert auth.has_resource_access("templates", "read", "esg3") == True

def test_scope_migration():
    """Test converting old attributes to scopes."""
    attributes = {
        "workflows": ["esg2", "presales_poc"],
        "templates": ["view", "admin"],
        "contexts": ["view"]
    }

    scopes = convert_attributes_to_scopes(attributes)

    assert "workflows:esg2:read" in scopes
    assert "workflows:esg2:execute" in scopes
    assert "workflows:presales_poc:read" in scopes
    assert "templates:read" in scopes
    assert "templates:write" in scopes
    assert "contexts:read" in scopes
    assert "contexts:write" not in scopes

Manual Testing Checklist: - [ ] API key authentication still works - [ ] Session authentication still works - [ ] Template upload/download permissions enforced - [ ] Workflow execution permissions enforced - [ ] Presentation generation permissions enforced - [ ] Admin users can manage all resources - [ ] Limited users cannot access restricted resources - [ ] Workflow-specific permissions work correctly - [ ] CLI user management commands work - [ ] Migration script converts all existing users correctly

Files to Create: - Create: tests/unit/auth/test_scopes.py - Create: tests/integration/test_auth_flows.py - Create: tests/manual_test_checklist.md

Migration Guide

For Existing Users

Before Migration:

# User in config.py
"user@example.com": {
    "id": -100,
    "attributes": {
        "workflows": ["esg2"],
        "templates": ["view", "admin"]
    }
}

After Migration:

# User in database
{
  "email": "user@example.com",
  "scopes": [
    "workflows:esg2:read",
    "workflows:esg2:execute",
    "templates:esg2:read",
    "templates:esg2:write",
    "templates:esg2:delete"
  ]
}

Deployment Steps

  1. Deploy Code (with backward compatibility)
  2. Auth layer supports both old and new formats
  3. New endpoints use scopes
  4. Old endpoints still work with config

  5. Run Migration

    # Import users from config to database
    python -m cli migrate-users-from-config
    
    # Verify all users imported
    python -m cli user list
    

  6. Validate

  7. Test login with migrated users
  8. Verify permissions work correctly
  9. Check API key access still works

  10. Switch Over

  11. Set EMERGENCY_ADMIN_EMAIL in environment
  12. Remove ALLOWED_USERS from config.py
  13. Redeploy

  14. Monitor

  15. Watch for authentication errors
  16. Verify all users can access their workflows
  17. Check API key usage

Rollback Plan

If issues arise:

  1. Immediate: Restore ALLOWED_USERS in config.py
  2. Redeploy: Old code still works with config
  3. Fix Issues: Debug scope assignment problems
  4. Retry: Fix and re-run migration

Benefits Summary

For Users

  • Single, consistent permission model
  • Easy to understand: "I have scope X, I can do X"
  • Flexible workflow access control
  • Self-service via CLI (for admins)

For Developers

  • Unified auth checking: user.has_scope("templates:write")
  • Less code: Remove custom permission logic
  • Easier to extend: Add new scopes without changing code
  • Better security: Database-backed, not config file

For Administrators

  • Easy user management via CLI
  • Audit trail in database
  • Fine-grained control without complexity
  • Can grant/revoke individual permissions

For System

  • Database-backed: Scalable, no code deploys for user changes
  • Performance: Indexed JSONB queries are fast
  • Standard pattern: Industry-standard approach
  • API parity: Same auth for API and web UI

Open Questions / Future Enhancements

1. Role Templates

Should we add predefined roles in addition to scopes?

Pros: Easier user management ("make them an editor") Cons: Adds complexity, scopes are already flexible

Recommendation: Start with scopes only, add roles if needed later

2. Scope Hierarchy

Should templates:write automatically include templates:read?

Current: No hierarchy, must grant both Alternative: Write implies read

Recommendation: Keep flat for now, explicit is better than implicit

3. Audit Logging

Should we log all permission checks?

Pros: Security audit trail, debugging Cons: Database bloat, performance impact

Recommendation: Log failed permission checks only

4. Time-Based Scopes

Should scopes have expiration times?

Example: Grant temporary access for contractors

Recommendation: Not in MVP, could add to User model later

5. Scope Groups

Should we support scope groups/bundles?

Example: @esg-team = all ESG workflow scopes

Recommendation: Not needed, scopes are already flexible

Conclusion

The unified scope-based authorization system provides:

  1. Consistency: Same auth model for API keys and users
  2. Simplicity: Easy to understand and manage
  3. Flexibility: Fine-grained permissions without complexity
  4. Scalability: Database-first, no code deploys for user changes
  5. Security: Industry-standard pattern with full audit trail

Next Steps: 1. Review and approve this plan 2. Implement Phase 1 (database schema) 3. Implement Phase 2 (auth layer) 4. Implement Phase 3 (application layer) 5. Implement Phase 4 (user management) 6. Deploy and migrate users

Timeline Estimate: - Phase 1: 1 day (schema + migration) - Phase 2: 2 days (auth layer refactoring) - Phase 3: 3 days (update all endpoints) - Phase 4: 2 days (CLI + admin tools) - Phase 5: 1 day (cleanup) - Phase 6: 2 days (testing) - Total: ~2 weeks with thorough testing

Risk Level: Medium - Large refactoring touching auth system - Mitigated by: Backward compatibility, thorough testing, rollback plan