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.
This commit is contained in:
Max 2025-10-22 09:51:06 +08:00
parent e96896eb40
commit 7aff66e3b4
11 changed files with 282 additions and 134 deletions

View file

@ -346,11 +346,27 @@ func HandleRequest(c *gin.Context) {
// e.g., WHERE user_id = authInfo.UserID
}
if authInfo.Constraints.CreatorOnly {
// Only return data created by current user
// e.g., WHERE created_by = authInfo.UserID
}
if authInfo.Constraints.EditorOnly {
// Only return data last edited by current user
// e.g., WHERE updated_by = authInfo.UserID
}
if authInfo.Constraints.TeamOnly {
// Only return data owned by current team
// e.g., WHERE team_id = authInfo.TeamID
}
// Check extra constraints
if dept, ok := authInfo.Constraints.Extra["department_only"].(bool); ok && dept {
// Apply department filter
// e.g., WHERE department_id = authInfo.DepartmentID
}
// Process request...
}
```
@ -362,13 +378,14 @@ After successful ACL enforcement, the `AuthorizedInfo` is automatically updated
```go
// DataConstraints represents data access constraints
type DataConstraints struct {
OwnerOnly bool // Only access owner's data (filter by UserID)
TeamOnly bool // Only access team's data (filter by TeamID)
// Built-in constraints
OwnerOnly bool // Only access owner's data (current owner)
CreatorOnly bool // Only access creator's data (who created the resource)
EditorOnly bool // Only access editor's data (who last updated the resource)
TeamOnly bool // Only access team's data (filter by TeamID)
// Future constraints:
// DepartmentOnly bool
// ProjectOnly bool
// RegionOnly bool
// Extra constraints (user-defined, flexible extension)
Extra map[string]interface{} // Custom constraints like department_only, region_only, etc.
}
type AuthorizedInfo struct {
@ -394,78 +411,79 @@ type AuthorizedInfo struct {
The constraint system uses a map-based approach for easy extension:
```go
// Step 1: Add field to DataConstraints (types/types.go)
// The constraint system is already extensible through the Extra map!
// For custom constraints, use the Extra field directly - no code changes needed.
// Current structure (already supports custom constraints):
type DataConstraints struct {
OwnerOnly bool
TeamOnly bool
DepartmentOnly bool // New constraint
// Built-in constraints (pre-defined)
OwnerOnly bool
CreatorOnly bool
EditorOnly bool
TeamOnly bool
// Extra constraints (user-defined, flexible)
Extra map[string]interface{}
}
// Step 2: Add field to EndpointInfo (acl/types.go)
type EndpointInfo struct {
OwnerOnly bool
TeamOnly bool
DepartmentOnly bool // New constraint
// Define custom constraints in scope YAML:
// collections:read:department:
// description: "Read collections in user's department"
// extra:
// department_only: true
// region: "us-west"
// project_ids: ["proj1", "proj2"]
// endpoints:
// - GET /kb/collections/department
// Access in handler code:
func GetCollections(c *gin.Context) {
authInfo := authorized.GetInfo(c)
query := db.Query("SELECT * FROM collections")
// Check built-in constraints
if authInfo.Constraints.OwnerOnly {
query = query.Where("user_id = ?", authInfo.UserID)
}
// Check extra constraints (no code changes needed!)
if dept, ok := authInfo.Constraints.Extra["department_only"].(bool); ok && dept {
query = query.Where("department_id = ?", authInfo.DepartmentID)
}
if region, ok := authInfo.Constraints.Extra["region"].(string); ok {
query = query.Where("region = ?", region)
}
if projectIDs, ok := authInfo.Constraints.Extra["project_ids"].([]interface{}); ok {
query = query.Where("project_id IN (?)", projectIDs)
}
// Execute query...
}
// Step 3: Update GetConstraints to include new constraint (acl/types.go)
func (e *EndpointInfo) GetConstraints() map[string]interface{} {
constraints := make(map[string]interface{})
if e.OwnerOnly {
constraints["owner_only"] = true
}
if e.TeamOnly {
constraints["team_only"] = true
}
if e.DepartmentOnly {
constraints["department_only"] = true // New
}
return constraints
}
// Step 4: Update GetConstraints reader (authorized/utils.go)
func GetConstraints(c *gin.Context) types.DataConstraints {
constraints := types.DataConstraints{}
if ownerOnly, ok := c.Get("__owner_only"); ok {
if ownerOnlyBool, ok := ownerOnly.(bool); ok {
constraints.OwnerOnly = ownerOnlyBool
}
}
if teamOnly, ok := c.Get("__team_only"); ok {
if teamOnlyBool, ok := teamOnly.(bool); ok {
constraints.TeamOnly = teamOnlyBool
}
}
if departmentOnly, ok := c.Get("__department_only"); ok {
if deptBool, ok := departmentOnly.(bool); ok {
constraints.DepartmentOnly = deptBool // New
}
}
return constraints
}
// No changes needed to enforce.go or handler code!
// ONLY if you need a new BUILT-IN constraint (used frequently across the system):
// Follow these steps to add it alongside OwnerOnly, CreatorOnly, etc.
// But for most cases, using Extra is sufficient and more flexible!
```
**Example Endpoint Configuration**:
```yaml
# openapi/scopes/collections/read.yml
collections:read:
name: "collections:read"
description: "Read collections"
owner: true # This sets OwnerOnly = true
collections:read:own:
name: "collections:read:own"
description: "Read own collections"
owner: true # Sets OwnerOnly = true
creator: true # Sets CreatorOnly = true
editor: true # Sets EditorOnly = true
extra: # Sets Extra constraints
department_only: true
region: "us-west"
endpoints:
- "GET /api/collections"
- "GET /api/collections/:id"
- "GET /api/collections/own"
- "GET /api/collections/own/:id"
```
**Example API Handler**:
@ -476,17 +494,25 @@ func GetCollections(c *gin.Context) {
query := db.Query("SELECT * FROM collections")
// Apply data access constraints
// Apply built-in data access constraints
if authInfo.Constraints.OwnerOnly {
query = query.Where("user_id = ?", authInfo.UserID)
} else if authInfo.Constraints.CreatorOnly {
query = query.Where("created_by = ?", authInfo.UserID)
} else if authInfo.Constraints.EditorOnly {
query = query.Where("updated_by = ?", authInfo.UserID)
} else if authInfo.Constraints.TeamOnly {
query = query.Where("team_id = ?", authInfo.TeamID)
}
// Future: Handle additional constraints
// if authInfo.Constraints.DepartmentOnly {
// query = query.Where("department_id = ?", authInfo.DepartmentID)
// }
// Apply extra constraints
if dept, ok := authInfo.Constraints.Extra["department_only"].(bool); ok && dept {
query = query.Where("department_id = ?", authInfo.DepartmentID)
}
if region, ok := authInfo.Constraints.Extra["region"].(string); ok {
query = query.Where("region = ?", region)
}
// Execute query and return results
collections, _ := query.Get()
@ -601,16 +627,32 @@ A: Use restricted scopes when you want to:
- Temporarily revoke access to certain endpoints without changing the base role
- Implement exceptions to general permissions
**Q: How do OwnerOnly and TeamOnly constraints work?**
**Q: How do data constraints work?**
A: After successful ACL enforcement:
1. The system checks if the matched endpoint has `owner: true` or `team: true` in its scope definition
2. These flags are automatically set in `AuthorizedInfo.Constraints` (`authInfo.Constraints.OwnerOnly`, `authInfo.Constraints.TeamOnly`)
3. API handlers can read these flags from `authorized.GetInfo(c)` and apply data filters
1. The system checks if the matched endpoint has constraint flags in its scope definition (`owner`, `creator`, `editor`, `team`, `extra`)
2. These flags are automatically set in `AuthorizedInfo.Constraints`
3. API handlers read these flags from `authorized.GetInfo(c)` and apply data filters
4. Example: If `authInfo.Constraints.OwnerOnly = true`, the API should only return records where `user_id = authInfo.UserID`
**Q: Can both OwnerOnly and TeamOnly be true at the same time?**
A: Yes, if a scope definition has both `owner: true` and `team: true`. In this case, the API handler should typically use the more restrictive filter (`authInfo.Constraints.OwnerOnly`).
**Q: What's the difference between Owner, Creator, and Editor constraints?**
A:
**Q: What happens if OwnerOnly is true but UserID is empty?**
A: This would be an edge case for pure client credential grants. The API handler should handle this gracefully (e.g., return empty results or an appropriate error).
- **OwnerOnly**: Filters by current owner (who owns it now) - can be transferred
- **CreatorOnly**: Filters by original creator (who created it) - immutable
- **EditorOnly**: Filters by last editor (who last updated it) - changes on each edit
**Q: Can multiple constraints be true at the same time?**
A: Yes, a scope can have multiple constraints. The API handler should apply filters based on the most restrictive or appropriate constraint for the use case.
**Q: How do I use Extra constraints?**
A: Define them in the scope configuration YAML under `extra:`, then access them in your handler:
```go
if dept, ok := authInfo.Constraints.Extra["department_only"].(bool); ok && dept {
query = query.Where("department_id = ?", userDepartmentID)
}
```
**Q: What happens if constraints are set but the user context is missing?**
A: For client credential grants with no user context, the API handler should handle this gracefully (e.g., return empty results or an appropriate error).

View file

@ -118,7 +118,8 @@ collections:read:all:
- GET /kb/collections/:collectionID/exists
collections:read:own:
owner: true
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
@ -127,6 +128,7 @@ collections:read:own:
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
@ -134,21 +136,33 @@ collections:write:own:
- DELETE /kb/collections/own/:collectionID
collections:read:team:
team: true
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) |
| `team` | bool | No | false | If `true`, data access is restricted to team only (sets `TeamOnly` constraint) |
| `endpoints` | array | Yes | - | List of API endpoints this scope grants access to |
| 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

View file

@ -164,7 +164,7 @@ func (acl *ACL) enforceClient(ctx context.Context, authInfo *types.AuthorizedInf
if err != nil {
return false, nil, &Error{
Type: ErrorTypeInternal,
Message: fmt.Sprintf("failed to get client role: %v", err),
Message: fmt.Sprintf("failed to get client role [client_id=%s]: %v", authInfo.ClientID, err),
Stage: EnforcementStageClient,
}
}
@ -174,7 +174,7 @@ func (acl *ACL) enforceClient(ctx context.Context, authInfo *types.AuthorizedInf
if err != nil {
return false, nil, &Error{
Type: ErrorTypeInternal,
Message: fmt.Sprintf("failed to get client scopes: %v", err),
Message: fmt.Sprintf("failed to get client scopes [client_id=%s, role=%s]: %v", authInfo.ClientID, clientRole, err),
Stage: EnforcementStageClient,
}
}
@ -193,6 +193,9 @@ func (acl *ACL) enforceClient(ctx context.Context, authInfo *types.AuthorizedInf
Message: decision.Reason,
Stage: EnforcementStageClient,
Details: map[string]interface{}{
"client_id": authInfo.ClientID,
"method": request.Method,
"path": request.Path,
"required_scopes": decision.RequiredScopes,
"missing_scopes": decision.MissingScopes,
},
@ -214,6 +217,9 @@ func (acl *ACL) enforceClient(ctx context.Context, authInfo *types.AuthorizedInf
Message: "access denied by restriction: " + restrictDecision.Reason,
Stage: EnforcementStageClient,
Details: map[string]interface{}{
"client_id": authInfo.ClientID,
"method": request.Method,
"path": request.Path,
"restricted_scopes": restrictedScopes,
"matched_pattern": restrictDecision.MatchedPattern,
},
@ -259,6 +265,10 @@ func (acl *ACL) enforceScope(_ context.Context, authInfo *types.AuthorizedInfo,
Message: decision.Reason,
Stage: EnforcementStageScope,
Details: map[string]interface{}{
"client_id": authInfo.ClientID,
"user_id": authInfo.UserID,
"method": request.Method,
"path": request.Path,
"required_scopes": decision.RequiredScopes,
"missing_scopes": decision.MissingScopes,
},
@ -277,7 +287,7 @@ func (acl *ACL) enforceUser(ctx context.Context, authInfo *types.AuthorizedInfo,
if err != nil {
return false, nil, &Error{
Type: ErrorTypeInternal,
Message: fmt.Sprintf("failed to get user role: %v", err),
Message: fmt.Sprintf("failed to get user role [user_id=%s]: %v", authInfo.UserID, err),
Stage: EnforcementStageUser,
}
}
@ -287,7 +297,7 @@ func (acl *ACL) enforceUser(ctx context.Context, authInfo *types.AuthorizedInfo,
if err != nil {
return false, nil, &Error{
Type: ErrorTypeInternal,
Message: fmt.Sprintf("failed to get user scopes: %v", err),
Message: fmt.Sprintf("failed to get user scopes [user_id=%s, role=%s]: %v", authInfo.UserID, userRole, err),
Stage: EnforcementStageUser,
}
}
@ -306,6 +316,9 @@ func (acl *ACL) enforceUser(ctx context.Context, authInfo *types.AuthorizedInfo,
Message: decision.Reason,
Stage: EnforcementStageUser,
Details: map[string]interface{}{
"user_id": authInfo.UserID,
"method": request.Method,
"path": request.Path,
"required_scopes": decision.RequiredScopes,
"missing_scopes": decision.MissingScopes,
},
@ -327,6 +340,9 @@ func (acl *ACL) enforceUser(ctx context.Context, authInfo *types.AuthorizedInfo,
Message: "access denied by restriction: " + restrictDecision.Reason,
Stage: EnforcementStageUser,
Details: map[string]interface{}{
"user_id": authInfo.UserID,
"method": request.Method,
"path": request.Path,
"restricted_scopes": restrictedScopes,
"matched_pattern": restrictDecision.MatchedPattern,
},
@ -346,7 +362,7 @@ func (acl *ACL) enforceTeam(ctx context.Context, authInfo *types.AuthorizedInfo,
if err != nil {
return false, nil, &Error{
Type: ErrorTypeInternal,
Message: fmt.Sprintf("failed to get team role: %v", err),
Message: fmt.Sprintf("failed to get team role [team_id=%s]: %v", authInfo.TeamID, err),
Stage: EnforcementStageTeam,
}
}
@ -356,7 +372,7 @@ func (acl *ACL) enforceTeam(ctx context.Context, authInfo *types.AuthorizedInfo,
if err != nil {
return false, nil, &Error{
Type: ErrorTypeInternal,
Message: fmt.Sprintf("failed to get team scopes: %v", err),
Message: fmt.Sprintf("failed to get team scopes [team_id=%s, role=%s]: %v", authInfo.TeamID, teamRole, err),
Stage: EnforcementStageTeam,
}
}
@ -375,6 +391,10 @@ func (acl *ACL) enforceTeam(ctx context.Context, authInfo *types.AuthorizedInfo,
Message: decision.Reason,
Stage: EnforcementStageTeam,
Details: map[string]interface{}{
"team_id": authInfo.TeamID,
"user_id": authInfo.UserID,
"method": request.Method,
"path": request.Path,
"required_scopes": decision.RequiredScopes,
"missing_scopes": decision.MissingScopes,
},
@ -396,6 +416,10 @@ func (acl *ACL) enforceTeam(ctx context.Context, authInfo *types.AuthorizedInfo,
Message: "access denied by restriction: " + restrictDecision.Reason,
Stage: EnforcementStageTeam,
Details: map[string]interface{}{
"team_id": authInfo.TeamID,
"user_id": authInfo.UserID,
"method": request.Method,
"path": request.Path,
"restricted_scopes": restrictedScopes,
"matched_pattern": restrictDecision.MatchedPattern,
},
@ -415,7 +439,7 @@ func (acl *ACL) enforceMember(ctx context.Context, authInfo *types.AuthorizedInf
if err != nil {
return false, nil, &Error{
Type: ErrorTypeInternal,
Message: fmt.Sprintf("failed to get member role: %v", err),
Message: fmt.Sprintf("failed to get member role [team_id=%s, user_id=%s]: %v", authInfo.TeamID, authInfo.UserID, err),
Stage: EnforcementStageMember,
}
}
@ -425,7 +449,7 @@ func (acl *ACL) enforceMember(ctx context.Context, authInfo *types.AuthorizedInf
if err != nil {
return false, nil, &Error{
Type: ErrorTypeInternal,
Message: fmt.Sprintf("failed to get member scopes: %v", err),
Message: fmt.Sprintf("failed to get member scopes [team_id=%s, user_id=%s, role=%s]: %v", authInfo.TeamID, authInfo.UserID, memberRole, err),
Stage: EnforcementStageMember,
}
}
@ -444,6 +468,10 @@ func (acl *ACL) enforceMember(ctx context.Context, authInfo *types.AuthorizedInf
Message: decision.Reason,
Stage: EnforcementStageMember,
Details: map[string]interface{}{
"team_id": authInfo.TeamID,
"user_id": authInfo.UserID,
"method": request.Method,
"path": request.Path,
"required_scopes": decision.RequiredScopes,
"missing_scopes": decision.MissingScopes,
},
@ -465,6 +493,10 @@ func (acl *ACL) enforceMember(ctx context.Context, authInfo *types.AuthorizedInf
Message: "access denied by restriction: " + restrictDecision.Reason,
Stage: EnforcementStageMember,
Details: map[string]interface{}{
"team_id": authInfo.TeamID,
"user_id": authInfo.UserID,
"method": request.Method,
"path": request.Path,
"restricted_scopes": restrictedScopes,
"matched_pattern": restrictDecision.MatchedPattern,
},

View file

@ -188,8 +188,7 @@ func (m *Manager) getTeamRole(ctx context.Context, teamID string) (string, error
// Note: Teams might not have a role_id field, adjust based on your schema
roleID, ok := teamInfo["role_id"].(string)
if !ok || roleID == "" {
// If team doesn't have a role, return a default team role
return "team:default", nil
return "", fmt.Errorf("team %s has no role_id assigned", teamID)
}
return roleID, nil

View file

@ -240,7 +240,10 @@ func (m *ScopeManager) buildIndexes() error {
Name: name,
Description: def.Description,
Owner: def.Owner,
Creator: def.Creator,
Editor: def.Editor,
Team: def.Team,
Extra: def.Extra,
Endpoints: def.Endpoints,
}
}
@ -315,16 +318,33 @@ func (m *ScopeManager) addEndpointRule(method, path, action string, scopes []str
RequiredScopes: scopes,
}
// Set owner/team constraints from scope definitions
// Set constraints from scope definitions
if len(scopes) > 0 {
for _, scopeName := range scopes {
if def := m.scopes[scopeName]; def != nil {
// Built-in constraints
if def.Owner {
info.OwnerOnly = true
}
if def.Creator {
info.CreatorOnly = true
}
if def.Editor {
info.EditorOnly = true
}
if def.Team {
info.TeamOnly = true
}
// Merge extra constraints
if len(def.Extra) > 0 {
if info.Extra == nil {
info.Extra = make(map[string]interface{})
}
for key, value := range def.Extra {
info.Extra[key] = value
}
}
}
}
}

View file

@ -78,11 +78,14 @@ type AliasConfig map[string][]string
// ScopeDefinition represents a scope definition (from subdirectory yml files)
type ScopeDefinition struct {
Name string `json:"name" yaml:"name"` // Scope name (e.g. collections:read:all)
Description string `json:"description" yaml:"description"` // Description
Owner bool `json:"owner" yaml:"owner"` // Owner only
Team bool `json:"team" yaml:"team"` // Team only
Endpoints []string `json:"endpoints" yaml:"endpoints"` // Endpoint list (format: METHOD /path)
Name string `json:"name" yaml:"name"` // Scope name (e.g. collections:read:all)
Description string `json:"description" yaml:"description"` // Description
Owner bool `json:"owner" yaml:"owner"` // Owner only (current owner)
Creator bool `json:"creator" yaml:"creator"` // Creator only (who created)
Editor bool `json:"editor" yaml:"editor"` // Editor only (who last updated)
Team bool `json:"team" yaml:"team"` // Team only
Extra map[string]interface{} `json:"extra,omitempty" yaml:"extra,omitempty"` // Extra constraints
Endpoints []string `json:"endpoints" yaml:"endpoints"` // Endpoint list (format: METHOD /path)
}
// ============ Runtime Structures (optimized for querying) ============
@ -143,9 +146,16 @@ type EndpointInfo struct {
// If Policy is require-scopes, the scopes required to access
RequiredScopes []string // Scope list (OR relationship, any one satisfied)
// Resource constraints
OwnerOnly bool // Owner only
TeamOnly bool // Team only
// Built-in resource constraints (common cases)
OwnerOnly bool // Owner only (current owner of the resource)
CreatorOnly bool // Creator only (who created the resource)
EditorOnly bool // Editor only (who last updated the resource)
TeamOnly bool // Team only
// Extra constraints (user-defined, flexible extension)
// Examples: "department_only", "region_only", "project_only"
// Value can be bool, string, or other types for complex constraints
Extra map[string]interface{} `json:"extra,omitempty" yaml:"extra,omitempty"`
}
// GetConstraints returns all data access constraints as a map
@ -157,19 +167,29 @@ func (e *EndpointInfo) GetConstraints() map[string]interface{} {
constraints := make(map[string]interface{})
// Built-in constraints
if e.OwnerOnly {
constraints["owner_only"] = true
}
if e.CreatorOnly {
constraints["creator_only"] = true
}
if e.EditorOnly {
constraints["editor_only"] = true
}
if e.TeamOnly {
constraints["team_only"] = true
}
// Future constraints can be added here without breaking existing code
// Example:
// if e.DepartmentOnly {
// constraints["department_only"] = true
// }
// Merge extra constraints
if e.Extra != nil {
for key, value := range e.Extra {
constraints[key] = value
}
}
return constraints
}
@ -188,11 +208,14 @@ const (
// Scope represents a permission scope
type Scope struct {
Name string // Scope name
Description string // Description
Owner bool // Owner only
Team bool // Team only
Endpoints []string // Associated endpoint list
Name string // Scope name
Description string // Description
Owner bool // Owner only (current owner)
Creator bool // Creator only (who created)
Editor bool // Editor only (who last updated)
Team bool // Team only
Extra map[string]interface{} // Extra constraints
Endpoints []string // Associated endpoint list
}
// ============ Request Context (permission check context) ============

View file

@ -55,24 +55,37 @@ func GetInfo(c *gin.Context) *types.AuthorizedInfo {
func GetConstraints(c *gin.Context) types.DataConstraints {
constraints := types.DataConstraints{}
// Built-in constraints
if ownerOnly, ok := c.Get("__owner_only"); ok {
if ownerOnlyBool, ok := ownerOnly.(bool); ok {
constraints.OwnerOnly = ownerOnlyBool
}
}
if creatorOnly, ok := c.Get("__creator_only"); ok {
if creatorOnlyBool, ok := creatorOnly.(bool); ok {
constraints.CreatorOnly = creatorOnlyBool
}
}
if editorOnly, ok := c.Get("__editor_only"); ok {
if editorOnlyBool, ok := editorOnly.(bool); ok {
constraints.EditorOnly = editorOnlyBool
}
}
if teamOnly, ok := c.Get("__team_only"); ok {
if teamOnlyBool, ok := teamOnly.(bool); ok {
constraints.TeamOnly = teamOnlyBool
}
}
// Future constraints can be read here:
// if departmentOnly, ok := c.Get("__department_only"); ok {
// if deptBool, ok := departmentOnly.(bool); ok {
// constraints.DepartmentOnly = deptBool
// }
// }
// Extra constraints
if extraConstraints, ok := c.Get("__extra_constraints"); ok {
if extra, ok := extraConstraints.(map[string]interface{}); ok {
constraints.Extra = extra
}
}
return constraints
}

View file

@ -7,6 +7,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/openapi/oauth/acl"
"github.com/yaoapp/yao/openapi/oauth/authorized"
"github.com/yaoapp/yao/openapi/oauth/types"
@ -50,6 +51,7 @@ func (s *Service) Guard(c *gin.Context) {
// Check permissions and enforce rate limits when ACL is configured
ok, err := acl.Global.Enforce(c)
if err != nil {
log.Error("[OAuth] ACL enforcement failed: %v", err)
s.handleACLError(c, err)
return
}

View file

@ -142,7 +142,7 @@ var (
// DefaultTeamFields contains basic team fields
DefaultTeamFields = []interface{}{
"team_id", "name", "display_name", "description", "website", "logo",
"owner_id", "status", "type_id", "type", "is_verified", "verified_at",
"owner_id", "status", "role_id", "type_id", "type", "is_verified", "verified_at",
"created_at", "updated_at",
}
@ -150,7 +150,7 @@ var (
DefaultTeamDetailFields = []interface{}{
"team_id", "name", "display_name", "description", "website", "logo",
"owner_id", "contact_email", "contact_phone", "is_verified", "verified_at", "verified_by",
"team_code", "team_code_type", "status", "type_id", "type", "address", "street_address",
"team_code", "team_code_type", "status", "role_id", "type_id", "type", "address", "street_address",
"city", "state_province", "postal_code", "country", "country_name", "region", "zoneinfo",
"settings", "metadata", "created_at", "updated_at",
}

View file

@ -599,13 +599,15 @@ type TokenClaims struct {
// DataConstraints represents data access constraints
// These constraints are set by ACL enforcement and used by API handlers to filter data
type DataConstraints struct {
OwnerOnly bool `json:"owner_only,omitempty"` // Only access owner's data (filter by UserID)
TeamOnly bool `json:"team_only,omitempty"` // Only access team's data (filter by TeamID)
// Built-in constraints
OwnerOnly bool `json:"owner_only,omitempty"` // Only access owner's data (current owner)
CreatorOnly bool `json:"creator_only,omitempty"` // Only access creator's data (who created)
EditorOnly bool `json:"editor_only,omitempty"` // Only access editor's data (who last updated)
TeamOnly bool `json:"team_only,omitempty"` // Only access team's data (filter by TeamID)
// Future constraints can be added here:
// DepartmentOnly bool `json:"department_only,omitempty"` // Only access department's data
// ProjectOnly bool `json:"project_only,omitempty"` // Only access project's data
// RegionOnly bool `json:"region_only,omitempty"` // Only access region's data
// Extra constraints (user-defined, flexible extension)
// Examples: department_only, region_only, project_only
Extra map[string]interface{} `json:"extra,omitempty"` // Extra constraints
}
// AuthorizedInfo represents authorized information

View file

@ -434,12 +434,13 @@ func TestGetTeamRole(t *testing.T) {
require.NoError(t, err)
defer provider.DeleteTeam(ctx, teamID)
// Get team role (should return default)
// Get team role (should return error when no role_id assigned)
roleID, err := manager.GetTeamRole(ctx, teamID)
assert.NoError(t, err)
assert.Equal(t, "team:default", roleID)
assert.Error(t, err, "Should return error when team has no role_id")
assert.Contains(t, err.Error(), "has no role_id assigned", "Error message should indicate missing role_id")
assert.Empty(t, roleID, "Role ID should be empty when error occurs")
t.Logf("Successfully returned default role for team without role_id: %s", roleID)
t.Logf("Correctly returns error for team without role_id: %v", err)
})
t.Run("GetRoleForNonExistentTeam", func(t *testing.T) {