202 lines
6.6 KiB
Markdown
202 lines
6.6 KiB
Markdown
# 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
|
|
|
|
```go
|
|
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)
|
|
```
|
|
|
|
### 2. Using the Project Factory (Recommended for Projects)
|
|
|
|
```go
|
|
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
|
|
```go
|
|
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
|
|
```go
|
|
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
|
|
```go
|
|
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
|
|
```go
|
|
// 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
|
|
```go
|
|
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`. |