yao/openapi/oauth/acl/SCOPES_CONFIGURATION.md
Max 7aff66e3b4 Enhance ACL enforcement and logging for improved error handling
- Added detailed logging for ACL enforcement failures in the OAuth guard, providing clearer insights into permission issues.
- Updated error messages in the ACL enforcement logic to include contextual information such as client and user IDs, enhancing debugging capabilities.
- Introduced new constraints (CreatorOnly, EditorOnly) in the data access model, allowing for more granular control over data visibility based on user roles.
- Enhanced scope definitions to support additional constraints, improving flexibility in access control configurations.
- Updated documentation to reflect changes in data constraints and their usage in API handlers.
2025-10-22 09:51:06 +08:00

816 lines
21 KiB
Markdown

# ACL Scopes Configuration Guide
## Overview
This guide explains how to configure and manage ACL (Access Control List) scopes for your OAuth-protected APIs. Scopes define what resources and actions are accessible to different users, teams, and clients.
---
## Directory Structure
All scope configurations should be placed in the `openapi/scopes/` directory with the following structure:
```
openapi/scopes/
├── scopes.yml # Global configuration and default policies
├── alias.yml # Scope aliases for simplified permission management
└── <resource>/ # Resource-specific scope definitions
├── collections.yml # Collections resource scopes
├── documents.yml # Documents resource scopes
└── ...
```
**Organization Guidelines**:
- Group related scopes by resource (e.g., `kb/`, `user/`, `job/`, `file/`)
- Use descriptive filenames matching the resource name
- Keep each file focused on a single resource or logical grouping
---
## Configuration Files
### 1. Global Configuration (`scopes.yml`)
The `scopes.yml` file defines global ACL behavior, public endpoints, and default rules.
#### Structure
```yaml
# Default action for unmatched API endpoints
default: deny # Options: "deny" or "allow"
# Public endpoints (accessible without authentication)
public:
- GET /user/entry
- GET /user/entry/captcha
- POST /user/entry/verify
- GET /user/teams/invitations/:invitation_id
# Default endpoint rules (can be overridden by specific scopes)
endpoints:
# Read operations allowed for authenticated users
- GET /kb/* allow
- GET /kb/collections allow
# Write operations require specific scopes
- POST /kb/* deny
- PUT /kb/* deny
- DELETE /kb/* deny
```
#### Fields
| Field | Type | Required | Description |
| ----------- | ------ | -------- | ------------------------------------------------------------- |
| `default` | string | Yes | Default policy for unmatched endpoints: `"allow"` or `"deny"` |
| `public` | array | No | List of public endpoints (no authentication required) |
| `endpoints` | array | No | Default endpoint rules (see Endpoint Rules below) |
#### Endpoint Rules Format
Each endpoint rule can be specified as:
**Simple String Format** (recommended):
```yaml
- GET /api/users allow
- POST /api/users deny
- DELETE /api/users/* deny
```
**Struct Format**:
```yaml
- method: GET
path: /api/users
action: allow
```
**Path Patterns**:
- **Exact path**: `/kb/collections` - matches exactly
- **Parameter path**: `/kb/collections/:collectionID` - matches with parameters
- **Wildcard path**: `/kb/*` - matches all paths under `/kb/`
**Best Practices**:
- Set `default: deny` for security (deny by default, allow explicitly)
- List public endpoints explicitly (login, registration, health checks)
- Use wildcards for broad policies, then override with specific scopes
- Order matters: more specific rules should come after general ones
---
### 2. Scope Definitions (Resource Files)
Scope definition files define specific permissions for resources. Each file contains multiple scope definitions.
#### Structure
```yaml
# Scope naming convention: resource:action:level
collections:read:all:
description: "Read knowledge base for all users"
endpoints:
- GET /kb/collections
- GET /kb/collections/:collectionID
- GET /kb/collections/:collectionID/exists
collections:read:own:
owner: true # Only show collections owned by current user
creator: true # Only show collections created by current user
description: "Read knowledge base for own collections"
endpoints:
- GET /kb/collections/own
- GET /kb/collections/own/:collectionID
- GET /kb/collections/own/:collectionID/exists
collections:write:own:
owner: true
editor: true # Only allow editing by last editor
description: "Write knowledge base for own collections"
endpoints:
- POST /kb/collections/own
- PUT /kb/collections/own/:collectionID
- DELETE /kb/collections/own/:collectionID
collections:read:team:
team: true # Only show team collections
description: "Read knowledge base for team collections"
endpoints:
- GET /kb/collections/team
- GET /kb/collections/team/:collectionID
collections:read:department:
extra: # Custom constraints
department_only: true
region: "us-west"
description: "Read collections for department in specific region"
endpoints:
- GET /kb/collections/department
- GET /kb/collections/department/:collectionID
```
#### Scope Definition Fields
| Field | Type | Required | Default | Description |
| ------------- | ------ | -------- | ------- | ---------------------------------------------------------------------------------- |
| `description` | string | No | "" | Human-readable description of the scope |
| `owner` | bool | No | false | If `true`, data access is restricted to owner only (sets `OwnerOnly` constraint) |
| `creator` | bool | No | false | If `true`, data access is restricted to creator only (sets `CreatorOnly` constraint) |
| `editor` | bool | No | false | If `true`, data access is restricted to editor only (sets `EditorOnly` constraint) |
| `team` | bool | No | false | If `true`, data access is restricted to team only (sets `TeamOnly` constraint) |
| `extra` | map | No | {} | User-defined custom constraints (key-value pairs) |
| `endpoints` | array | Yes | - | List of API endpoints this scope grants access to |
#### Endpoint Format
Each endpoint in the `endpoints` array should be formatted as:
```
METHOD /path
```
**Examples**:
```yaml
endpoints:
- GET /kb/collections
- GET /kb/collections/:collectionID
- POST /kb/collections/own
- PUT /kb/collections/:collectionID
- DELETE /kb/collections/own/:collectionID
```
**Supported HTTP Methods**:
- `GET` - Read operations
- `POST` - Create operations
- `PUT` - Update operations
- `DELETE` - Delete operations
- `PATCH` - Partial update operations
**Path Parameters**:
- Use `:paramName` syntax for path parameters (e.g., `:collectionID`, `:userID`)
- Parameter names should be descriptive and consistent
---
### 3. Scope Aliases (`alias.yml`)
Aliases allow you to group multiple scopes under a single name for simplified permission management.
#### Structure
```yaml
# Alias naming: category:level
user:auth:
- entry:access:public
- entry:register:authenticated
- entry:logout:own
kb:read:
- collections:read:all
- documents:read:all
- search:read:all
- hits:read:all
kb:own:
- collections:read:own
- collections:write:own
- collections:delete:own
- documents:read:own
- documents:write:own
- documents:delete:own
kb:admin:
- collections:read:all
- collections:write:all
- collections:delete:all
- documents:read:all
- documents:write:all
- documents:delete:all
- search:read:all
- graphs:read:all
# System root permission - absolute highest privilege
system:root:
- "*:*:*"
```
#### Alias Usage
**In Role Configuration**:
```go
// Assign aliases to roles instead of individual scopes
role := &Role{
ID: "kb-viewer",
AllowedScopes: []string{
"kb:read", // Expands to all KB read scopes
"user:auth", // Expands to all auth scopes
},
}
```
**Benefits**:
- **Simplified Management**: Change multiple scopes by updating one alias
- **Consistency**: Ensure users get consistent permission sets
- **Readability**: Clear, semantic permission names
- **Maintenance**: Easier to add/remove scopes from permission groups
**Best Practices**:
- Use hierarchical naming: `resource:level` (e.g., `kb:read`, `kb:own`, `kb:admin`)
- Create aliases for common permission patterns
- Document what each alias includes
- Use wildcards (`*:*:*`) sparingly and only for system-level access
---
## Scope Naming Convention
Follow a consistent three-part naming convention for scopes:
```
resource:action:level
```
### Components
1. **Resource** (noun): The resource being accessed
- Examples: `collections`, `documents`, `profile`, `jobs`, `files`
- Should be plural for collections, singular for single resources
2. **Action** (verb): The operation being performed
- `read` - View/retrieve data (GET)
- `write` - Create/update data (POST, PUT, PATCH)
- `delete` - Remove data (DELETE)
- `control` - Special operations (start, stop, pause)
- `access` - Generic access without CRUD semantics
3. **Level** (scope): The access level or data visibility
- `all` - Full access to all resources
- `own` - Access only to user's own resources
- `team` - Access to team resources
- `public` - Public/unauthenticated access
- `authenticated` - Basic authenticated access
### Examples
| Scope | Description |
| ------------------------ | ----------------------------- |
| `collections:read:all` | Read all collections |
| `collections:read:own` | Read only own collections |
| `collections:read:team` | Read team collections |
| `collections:write:own` | Create/update own collections |
| `collections:delete:own` | Delete own collections |
| `documents:write:all` | Create/update any document |
| `documents:delete:team` | Delete team documents |
| `profile:read:own` | Read own profile |
| `jobs:control:own` | Control (start/stop) own jobs |
| `search:read:all` | Search across all resources |
---
## Data Access Constraints
Data access constraints control how API handlers should filter data based on ownership.
### Owner-Only Access (`owner: true`)
When `owner: true` is set, the scope grants access only to resources owned by the current user.
```yaml
collections:read:own:
owner: true
description: "Read knowledge base for own collections"
endpoints:
- GET /kb/collections/own
- GET /kb/collections/own/:collectionID
```
**API Implementation**:
```go
func GetCollections(c *gin.Context) {
authInfo := authorized.GetInfo(c)
query := db.Query("SELECT * FROM collections")
// Apply owner constraint
if authInfo.Constraints.OwnerOnly {
query = query.Where("user_id = ?", authInfo.UserID)
}
collections, _ := query.Get()
c.JSON(200, collections)
}
```
### Team-Only Access (`team: true`)
When `team: true` is set, the scope grants access only to resources owned by the current team.
```yaml
collections:read:team:
team: true
description: "Read knowledge base for team collections"
endpoints:
- GET /kb/collections/team
- GET /kb/collections/team/:collectionID
```
**API Implementation**:
```go
func GetCollections(c *gin.Context) {
authInfo := authorized.GetInfo(c)
query := db.Query("SELECT * FROM collections")
// Apply team constraint
if authInfo.Constraints.TeamOnly {
query = query.Where("team_id = ?", authInfo.TeamID)
}
collections, _ := query.Get()
c.JSON(200, collections)
}
```
### Combined Constraints
Both constraints can be applied:
```yaml
documents:read:own:
owner: true
team: true # Can be used together
description: "Read own documents within team context"
endpoints:
- GET /kb/documents/own
```
**API Implementation**:
```go
func GetDocuments(c *gin.Context) {
authInfo := authorized.GetInfo(c)
query := db.Query("SELECT * FROM documents")
// Apply constraints (OwnerOnly is more restrictive)
if authInfo.Constraints.OwnerOnly {
query = query.Where("user_id = ?", authInfo.UserID)
} else if authInfo.Constraints.TeamOnly {
query = query.Where("team_id = ?", authInfo.TeamID)
}
documents, _ := query.Get()
c.JSON(200, documents)
}
```
---
## Complete Example
Let's create a complete scope configuration for a blog system.
### Directory Structure
```
openapi/scopes/
├── scopes.yml
├── alias.yml
└── blog/
├── posts.yml
├── comments.yml
└── categories.yml
```
### `scopes.yml`
```yaml
default: deny
public:
- GET /blog/posts
- GET /blog/posts/:postID
- GET /blog/categories
endpoints:
# Read operations allowed for authenticated users
- GET /blog/* allow
# Write operations require specific scopes
- POST /blog/* deny
- PUT /blog/* deny
- DELETE /blog/* deny
```
### `blog/posts.yml`
```yaml
posts:read:all:
description: "Read all blog posts"
endpoints:
- GET /blog/posts
- GET /blog/posts/:postID
posts:read:own:
owner: true
description: "Read own blog posts"
endpoints:
- GET /blog/posts/own
- GET /blog/posts/own/:postID
posts:write:own:
owner: true
description: "Create and update own blog posts"
endpoints:
- POST /blog/posts
- PUT /blog/posts/:postID
- PATCH /blog/posts/:postID
posts:delete:own:
owner: true
description: "Delete own blog posts"
endpoints:
- DELETE /blog/posts/:postID
posts:write:all:
description: "Create and update any blog post (admin)"
endpoints:
- POST /blog/posts/admin
- PUT /blog/posts/admin/:postID
posts:delete:all:
description: "Delete any blog post (admin)"
endpoints:
- DELETE /blog/posts/admin/:postID
```
### `blog/comments.yml`
```yaml
comments:read:all:
description: "Read all comments"
endpoints:
- GET /blog/posts/:postID/comments
- GET /blog/comments/:commentID
comments:write:own:
owner: true
description: "Write own comments"
endpoints:
- POST /blog/posts/:postID/comments
- PUT /blog/comments/:commentID
comments:delete:own:
owner: true
description: "Delete own comments"
endpoints:
- DELETE /blog/comments/:commentID
comments:delete:all:
description: "Delete any comment (moderator)"
endpoints:
- DELETE /blog/comments/admin/:commentID
```
### `alias.yml`
```yaml
# Blog reader - can read all posts and comments
blog:reader:
- posts:read:all
- comments:read:all
# Blog author - can manage own posts and comments
blog:author:
- posts:read:all
- posts:write:own
- posts:delete:own
- comments:read:all
- comments:write:own
- comments:delete:own
# Blog moderator - can manage all comments
blog:moderator:
- posts:read:all
- comments:read:all
- comments:delete:all
# Blog admin - full access to all blog features
blog:admin:
- posts:read:all
- posts:write:all
- posts:delete:all
- comments:read:all
- comments:write:own
- comments:delete:all
```
---
## Wildcard Scopes
Wildcard scopes allow flexible permission matching using `*` as a placeholder.
### Syntax
```yaml
system:root:
- "*:*:*" # Matches everything
blog:admin:
- "posts:*:*" # Matches all post operations at all levels
- "comments:*:*" # Matches all comment operations at all levels
kb:read:
- "collections:read:*" # Matches collections:read:all, collections:read:own, etc.
- "documents:read:*" # Matches documents:read:all, documents:read:own, etc.
```
### Matching Rules
1. **Full wildcard** (`*:*:*`): Matches any scope
2. **Resource wildcard** (`posts:*:*`): Matches any action and level for the resource
3. **Action wildcard** (`posts:read:*`): Matches any level for the resource and action
4. **No partial wildcards**: `post*:read:all` is NOT supported
### Use Cases
- **System root access**: `*:*:*` for system administrators
- **Resource administrators**: `resource:*:*` for resource-level admins
- **Grouped permissions**: `resource:action:*` for action-level permissions
### Security Considerations
- Use wildcards sparingly
- Prefer explicit scope lists for most roles
- Reserve `*:*:*` for system-level accounts only
- Document wildcard usage clearly
- Consider restricted scopes to block specific actions even with wildcards
---
## Best Practices
### 1. Scope Design
**DO**:
- Use consistent naming conventions
- Group related scopes in the same file
- Provide clear descriptions for each scope
- Design scopes around resources and actions, not UI features
- Keep scopes granular but not too fine-grained
**DON'T**:
- Mix different resources in one scope file
- Create scopes for every single endpoint
- Use vague or inconsistent naming
- Duplicate endpoint definitions across scopes
### 2. Permission Levels
Create a clear hierarchy of permission levels:
1. **Public** (`public`): No authentication required
2. **Authenticated** (`authenticated`): Basic logged-in access
3. **Owner** (`own`): User's own resources
4. **Team** (`team`): Team's resources
5. **All** (`all`): All resources (admin level)
### 3. Aliases
**DO**:
- Create aliases for common user roles (viewer, editor, admin)
- Use aliases to group related scopes
- Document what each alias grants
- Keep alias names intuitive
**DON'T**:
- Create single-scope aliases (use the scope directly)
- Nest aliases (aliases should reference scopes, not other aliases)
- Use ambiguous alias names
### 4. Data Constraints
**DO**:
- Set `owner: true` for personal resource scopes
- Set `team: true` for team resource scopes
- Implement constraint checks in ALL relevant API handlers
- Return appropriate errors when constraints are violated
**DON'T**:
- Rely solely on URL paths (`/own`, `/team`) for access control
- Skip constraint validation in database queries
- Assume constraints are enforced automatically
### 5. Endpoint Definitions
**DO**:
- List all related endpoints for a scope
- Use consistent parameter naming (`:id`, `:userID`, `:collectionID`)
- Include all HTTP methods the scope covers
- Group similar endpoints together
**DON'T**:
- Define the same endpoint in multiple scopes (unless intentional)
- Use inconsistent path formats
- Forget to include related endpoints
### 6. Testing
- Test each scope definition with real requests
- Verify data constraints are enforced correctly
- Test wildcard matching behavior
- Ensure public endpoints are accessible without auth
- Validate that denied endpoints return proper errors
### 7. Documentation
- Comment complex scope definitions
- Document the purpose of each alias
- Maintain a scope reference for developers
- Update documentation when scopes change
- Provide examples of scope usage in roles
---
## Troubleshooting
### Common Issues
**Issue**: Endpoint not accessible even with correct scope
**Solution**:
- Check if endpoint is in `scopes.yml` default deny list
- Verify scope name matches exactly (case-sensitive)
- Ensure endpoint path matches (check for typos, extra slashes)
- Verify HTTP method matches
---
**Issue**: Data constraint not working
**Solution**:
- Confirm `owner: true` or `team: true` is set in scope definition
- Check if API handler reads `authInfo.Constraints`
- Verify database query applies constraint filters
- Ensure `authInfo.UserID` or `authInfo.TeamID` is populated
---
**Issue**: Wildcard scope not matching
**Solution**:
- Verify wildcard syntax (`*` in correct position)
- Check scope name format (must be `part1:part2:part3`)
- Ensure no typos in scope name parts
- Remember: wildcards only work with colon-separated scopes
---
**Issue**: Changes not taking effect
**Solution**:
- Restart the application to reload scope configurations
- Clear role cache: `role.RoleManager.ClearCache()`
- Verify YAML syntax is correct (use YAML validator)
- Check file is in correct directory
---
## Reference
### Related Files
- **[types.go](./types.go)**: Scope configuration structures
- **[scope.go](./scope.go)**: Scope matching and validation logic
- **[README.md](./README.md)**: ACL enforcement logic
- **[DESIGN.md](./DESIGN.md)**: Overall ACL system design
### Related Concepts
- **OAuth 2.1 Scopes**: Standard OAuth scope mechanism
- **RBAC**: Role-Based Access Control
- **Data Constraints**: Fine-grained data access control
- **Endpoint Matching**: Path pattern matching algorithm
---
## Migration Guide
### From Legacy Permissions
If migrating from a legacy permission system:
1. **Map old permissions to scopes**:
```
can_read_posts → posts:read:all
can_edit_own_posts → posts:write:own
can_delete_any_post → posts:delete:all
```
2. **Create scope definitions** for each permission
3. **Define aliases** for existing roles:
```yaml
role:editor:
- posts:read:all
- posts:write:own
- posts:delete:own
```
4. **Update API handlers** to check constraints
5. **Migrate role assignments** to use new scopes/aliases
6. **Test thoroughly** before deploying
### Version Compatibility
- **v1.0**: Basic scope checking
- **v1.1**: Data constraints (`owner`, `team`)
- **v1.2**: Wildcard scopes, restricted scopes
---
## Summary
Key points to remember:
1. **Three main files**: `scopes.yml` (global), `alias.yml` (aliases), resource files (scopes)
2. **Naming convention**: `resource:action:level`
3. **Data constraints**: Use `owner: true` and `team: true` for data filtering
4. **Aliases**: Group scopes for easier role management
5. **Wildcards**: Use `*` for flexible matching, but sparingly
6. **Testing**: Always test scope configurations thoroughly
For more details, refer to:
- [README.md](./README.md) - Enforcement logic
- [DESIGN.md](./DESIGN.md) - System architecture