- Updated the OAuth guard to utilize a standardized response format for error handling, enhancing consistency across error responses. - Integrated detailed error information for insufficient scope and permission denied scenarios, providing clearer insights into access issues. - Modified the ACL enforcement logic to return structured error details, allowing for better handling of permission-related errors. - Enhanced the ErrorResponse struct to include optional fields for reason, required scopes, and missing scopes, following OAuth 2.0 extensibility guidelines.
83 lines
2.5 KiB
Go
83 lines
2.5 KiB
Go
package acl
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
|
)
|
|
|
|
// Enforce checks if a user has access to a resource based on the request context
|
|
func (acl *ACL) Enforce(c *gin.Context) (bool, error) {
|
|
// If ACL is not enabled, allow access
|
|
if !acl.Enabled() {
|
|
return true, nil
|
|
}
|
|
|
|
// If scope manager not loaded, deny access
|
|
if acl.Scope == nil {
|
|
return false, nil
|
|
}
|
|
|
|
// Get authorized info from context (set by OAuth guard middleware)
|
|
authInfo := authorized.GetInfo(c)
|
|
|
|
// Resolve all scopes (client + user + team) from authorized info
|
|
// Note: This should include scope expansion from roles, aliases, etc.
|
|
scopes := getScopes(authInfo)
|
|
|
|
// Build access request (focused on scope-based access control)
|
|
request := &AccessRequest{
|
|
Method: c.Request.Method,
|
|
Path: c.Request.URL.Path,
|
|
Scopes: scopes,
|
|
}
|
|
|
|
// Check scopes
|
|
decision := acl.Scope.Check(request)
|
|
|
|
if !decision.Allowed {
|
|
// Return error with details, let the caller handle the response
|
|
err := &Error{
|
|
Type: ErrorTypePermissionDenied,
|
|
Message: decision.Reason,
|
|
Details: map[string]interface{}{
|
|
"required_scopes": decision.RequiredScopes,
|
|
"missing_scopes": decision.MissingScopes,
|
|
},
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// getScopes resolves all scopes from authorized info
|
|
// This function is responsible for the complete scope resolution process:
|
|
// 1. Get base scopes from token (authInfo.Scope)
|
|
// 2. Get user role scopes from database (if authInfo.UserID exists)
|
|
// 3. Get team role scopes from database (if authInfo.TeamID exists)
|
|
// 4. Merge all scopes and return the complete list
|
|
//
|
|
// Scope resolution logic:
|
|
// - Pure API call (no user_id): Returns client scopes from token
|
|
// - User call: Returns merged scopes (client + user roles + team roles)
|
|
//
|
|
// This keeps the ACL layer focused on scope-based access control,
|
|
// while relying on the authorized package for context extraction.
|
|
func getScopes(authInfo *types.AuthorizedInfo) []string {
|
|
// TODO: Implement scope resolution
|
|
// 1. Parse base scopes from authInfo.Scope (space-separated)
|
|
// 2. Query user roles and convert to scopes (if authInfo.UserID exists)
|
|
// 3. Query team roles and convert to scopes (if authInfo.TeamID exists)
|
|
// 4. Merge and deduplicate all scopes
|
|
|
|
// For now, just return scopes from token
|
|
if authInfo.Scope == "" {
|
|
return []string{}
|
|
}
|
|
// Split space-separated scopes
|
|
// e.g., "read:users write:users" -> ["read:users", "write:users"]
|
|
return strings.Split(authInfo.Scope, " ")
|
|
}
|