Files
sendico/api/pkg/auth/USAGE.md
Stephan D 62a6631b9a
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
service backend
2025-11-07 18:35:26 +01:00

6.6 KiB

Auth.Indexable Usage Guide

Secure Reordering with Permission Checking

The auth.Indexable implementation adds permission checking to the generic reordering functionality using EnforceBatch.

  • Core Implementation: api/pkg/auth/indexable.go - generic implementation with permission checking
  • Project Factory: api/pkg/auth/project_indexable.go - convenient factory for projects
  • Key Feature: Uses EnforceBatch to check permissions for all affected objects

How It Works

Permission Checking Flow

  1. Get current object to find its index
  2. Determine affected objects that will be shifted during reordering
  3. Check permissions using EnforceBatch for all affected objects + target object
  4. Verify all permissions - if any object lacks update permission, return error
  5. Proceed with reordering only if all permissions are granted

Key Differences from Basic Indexable

  • Additional parameter: accountRef for permission checking
  • Permission validation: All affected objects must have ActionUpdate permission
  • Security: Prevents unauthorized reordering that could affect other users' data

Usage

1. Using the Generic Auth.Indexable Implementation

import "github.com/tech/sendico/pkg/auth"

// For any type that embeds model.Indexable, define helper functions:
createEmpty := func() *YourType {
    return &YourType{}
}

getIndexable := func(obj *YourType) *model.Indexable {
    return &obj.Indexable
}

// Create auth.IndexableDB with enforcer
indexableDB := auth.NewIndexableDB(repo, logger, enforcer, createEmpty, getIndexable)

// Use with account reference for permission checking
err := indexableDB.Reorder(ctx, accountRef, objectID, newIndex, filter)
import "github.com/tech/sendico/pkg/auth"

// Create auth.ProjectIndexableDB (automatically applies org filter)
projectDB := auth.NewProjectIndexableDB(repo, logger, enforcer, organizationRef)

// Reorder project with permission checking
err := projectDB.Reorder(ctx, accountRef, projectID, newIndex, repository.Query())

// Reorder with additional filters (combined with org filter)
additionalFilter := repository.Query().Comparison(repository.Field("state"), builder.Eq, "active")
err := projectDB.Reorder(ctx, accountRef, projectID, newIndex, additionalFilter)

Examples for Different Types

Project Auth.IndexableDB

createEmpty := func() *model.Project {
    return &model.Project{}
}

getIndexable := func(p *model.Project) *model.Indexable {
    return &p.Indexable
}

projectDB := auth.NewIndexableDB(repo, logger, enforcer, createEmpty, getIndexable)
orgFilter := repository.OrgFilter(organizationRef)
projectDB.Reorder(ctx, accountRef, projectID, 2, orgFilter)

Status Auth.IndexableDB

createEmpty := func() *model.Status {
    return &model.Status{}
}

getIndexable := func(s *model.Status) *model.Indexable {
    return &s.Indexable
}

statusDB := auth.NewIndexableDB(repo, logger, enforcer, createEmpty, getIndexable)
projectFilter := repository.Query().Comparison(repository.Field("projectRef"), builder.Eq, projectRef)
statusDB.Reorder(ctx, accountRef, statusID, 1, projectFilter)

Task Auth.IndexableDB

createEmpty := func() *model.Task {
    return &model.Task{}
}

getIndexable := func(t *model.Task) *model.Indexable {
    return &t.Indexable
}

taskDB := auth.NewIndexableDB(repo, logger, enforcer, createEmpty, getIndexable)
statusFilter := repository.Query().Comparison(repository.Field("statusRef"), builder.Eq, statusRef)
taskDB.Reorder(ctx, accountRef, taskID, 3, statusFilter)

Permission Checking Details

What Gets Checked

When reordering an object from index A to index B:

  1. Target object - the object being moved
  2. Affected objects - all objects whose indices will be shifted:
    • Moving down: objects between A+1 and B (shifted up by -1)
    • Moving up: objects between B and A-1 (shifted down by +1)

Permission Requirements

  • Action: model.ActionUpdate
  • Scope: All affected objects must be PermissionBoundStorable
  • Result: If any object lacks permission, the entire operation fails

Error Handling

// Permission denied error
if err != nil {
    if strings.Contains(err.Error(), "accessDenied") {
        // Handle permission denied
    }
}

Security Benefits

Comprehensive Permission Checking

  • Checks permissions for all affected objects, not just the target
  • Prevents unauthorized reordering that could affect other users' data
  • Uses efficient EnforceBatch for bulk permission checking

Type Safety

  • Generic implementation works with any Indexable struct
  • Compile-time type checking
  • No runtime type assertions

Flexible Filtering

  • Single builder.Query parameter for scoping
  • Can combine organization filters with additional criteria
  • Project factory automatically applies organization filtering

Clean Architecture

  • Separates permission logic from reordering logic
  • Easy to test with mock enforcers
  • Follows existing auth patterns

Testing

Mock Enforcer Setup

mockEnforcer := &MockEnforcer{}

// Grant all permissions
permissions := map[primitive.ObjectID]bool{
    objectID1: true,
    objectID2: true,
}
mockEnforcer.On("EnforceBatch", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
    Return(permissions, nil)

// Deny specific permission
permissions[objectID2] = false
mockEnforcer.On("EnforceBatch", mock.Anything, mock.Anything, mock.Anything, mock.Anything).
    Return(permissions, nil)

Test Scenarios

  • Permission granted - reordering succeeds
  • Permission denied - reordering fails with access denied error
  • 🔄 No change needed - early return, minimal permission checking
  • 🏢 Organization filtering - automatic org scope for projects

Comparison: Basic vs Auth.Indexable

Feature Basic Indexable Auth.Indexable
Permission checking No Yes
Account parameter No Required
Security None Comprehensive
Performance Fast ⚠️ Slower (permission checks)
Use case Internal operations User-facing operations

Best Practices

  1. Use Auth.Indexable for user-facing reordering operations
  2. Use Basic Indexable for internal/system operations
  3. Always provide account reference for proper permission checking
  4. Test permission scenarios thoroughly with mock enforcers
  5. Handle permission errors gracefully in user interfaces

That's it! Secure, type-safe reordering with comprehensive permission checking using EnforceBatch.