Enhance ACL configuration and enforcement with path prefix support
- Updated ACL configuration to include a PathPrefix field, allowing for dynamic path stripping from request URLs. - Enhanced ACL enforcement logic to log the configured path prefix and adjust request paths accordingly during access checks. - Improved logging throughout the enforcement process to provide clearer insights into access decisions and scope matching. - Registered built-in scopes for temporary access tokens, enhancing flexibility in access control for specific endpoints. - Updated scope management to support constraints for matched scopes, improving granularity in access control configurations.
This commit is contained in:
parent
7aff66e3b4
commit
cc181a52f6
9 changed files with 423 additions and 53 deletions
|
|
@ -33,6 +33,13 @@ func New(config *Config) (Enforcer, error) {
|
|||
// Init Role Manager
|
||||
role.RoleManager = role.NewManager(config.Cache, config.Provider)
|
||||
log.Info("[ACL] Role manager loaded successfully")
|
||||
|
||||
// Log PathPrefix configuration
|
||||
if config.PathPrefix != "" {
|
||||
log.Info("[ACL] Path prefix configured: %s (will be stripped from request paths)", config.PathPrefix)
|
||||
} else {
|
||||
log.Info("[ACL] No path prefix configured")
|
||||
}
|
||||
}
|
||||
|
||||
return acl, nil
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/yao/openapi/oauth/acl/role"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
|
|
@ -15,30 +16,44 @@ import (
|
|||
func (acl *ACL) Enforce(c *gin.Context) (bool, error) {
|
||||
// If ACL is not enabled, allow access
|
||||
if !acl.Enabled() {
|
||||
log.Trace("[ACL] ACL is disabled, allowing access")
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// If scope manager not loaded, deny access
|
||||
if acl.Scope == nil {
|
||||
log.Trace("[ACL] Scope manager not loaded, denying access")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Get authorized info from context (set by OAuth guard middleware)
|
||||
authInfo := authorized.GetInfo(c)
|
||||
|
||||
// Get request path and strip PathPrefix if configured
|
||||
requestPath := c.Request.URL.Path
|
||||
if acl.Config.PathPrefix != "" && strings.HasPrefix(requestPath, acl.Config.PathPrefix) {
|
||||
requestPath = strings.TrimPrefix(requestPath, acl.Config.PathPrefix)
|
||||
log.Trace("[ACL] Stripped path prefix %s from request path, new path: %s", acl.Config.PathPrefix, requestPath)
|
||||
}
|
||||
|
||||
// Build access request
|
||||
request := &AccessRequest{
|
||||
Method: c.Request.Method,
|
||||
Path: c.Request.URL.Path,
|
||||
Path: requestPath,
|
||||
}
|
||||
|
||||
log.Trace("[ACL] Starting enforcement chain: method=%s, path=%s (original=%s), client_id=%s, user_id=%s, team_id=%s, scope=%s",
|
||||
request.Method, request.Path, c.Request.URL.Path, authInfo.ClientID, authInfo.UserID, authInfo.TeamID, authInfo.Scope)
|
||||
|
||||
// Execute enforcement chain and collect endpoint info
|
||||
allowed, endpointInfo, err := acl.enforce(c.Request.Context(), authInfo, request)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Enforcement failed with error: %v", err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !allowed {
|
||||
log.Trace("[ACL] Access denied by enforcement chain")
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +61,9 @@ func (acl *ACL) Enforce(c *gin.Context) (bool, error) {
|
|||
if endpointInfo != nil {
|
||||
constraints := endpointInfo.GetConstraints()
|
||||
authorized.UpdateConstraints(c, constraints)
|
||||
log.Trace("[ACL] Access granted, constraints applied: %+v", constraints)
|
||||
} else {
|
||||
log.Trace("[ACL] Access granted, no constraints")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
|
|
@ -62,6 +80,7 @@ func (acl *ACL) enforce(ctx context.Context, authInfo *types.AuthorizedInfo, req
|
|||
return false, nil, err
|
||||
}
|
||||
if !allowed {
|
||||
log.Trace("[ACL] Enforcement chain terminated: client permission check failed")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied: client permission check failed",
|
||||
|
|
@ -77,107 +96,132 @@ func (acl *ACL) enforce(ctx context.Context, authInfo *types.AuthorizedInfo, req
|
|||
return false, nil, err
|
||||
}
|
||||
if !allowed {
|
||||
log.Trace("[ACL] Enforcement chain terminated: token scope check failed")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied: token scope check failed",
|
||||
Stage: EnforcementStageScope,
|
||||
}
|
||||
}
|
||||
// Collect endpoint info
|
||||
if matchedEndpoint == nil && endpoint != nil {
|
||||
// Update endpoint info (later stages override earlier ones)
|
||||
if endpoint != nil {
|
||||
matchedEndpoint = endpoint
|
||||
log.Trace("[ACL] Step 2: Updated matched endpoint from token scope check")
|
||||
}
|
||||
} else {
|
||||
log.Trace("[ACL] Step 2: Token scope is empty, skipping scope check")
|
||||
}
|
||||
|
||||
// Step 3: Check team or user permissions
|
||||
// 3.1: If TeamID is present, this is a team login
|
||||
if authInfo.TeamID != "" {
|
||||
log.Trace("[ACL] Detected team login (team_id=%s), checking team and member permissions", authInfo.TeamID)
|
||||
|
||||
// 3.1.1: Check team permissions - MUST pass
|
||||
allowed, endpoint, err := acl.enforceTeam(ctx, authInfo, request)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if !allowed {
|
||||
log.Trace("[ACL] Enforcement chain terminated: team permission check failed")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied: team permission check failed",
|
||||
Stage: EnforcementStageTeam,
|
||||
}
|
||||
}
|
||||
// Collect endpoint info
|
||||
if matchedEndpoint == nil && endpoint != nil {
|
||||
// Update endpoint info (later stages override earlier ones)
|
||||
if endpoint != nil {
|
||||
matchedEndpoint = endpoint
|
||||
log.Trace("[ACL] Step 3.1: Updated matched endpoint from team check")
|
||||
}
|
||||
|
||||
// 3.1.2: Check member permissions (user's role in the team) - MUST pass
|
||||
// This is the final stage for team login, its constraints take precedence
|
||||
allowed, endpoint, err = acl.enforceMember(ctx, authInfo, request)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if !allowed {
|
||||
log.Trace("[ACL] Enforcement chain terminated: member permission check failed")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied: member permission check failed",
|
||||
Stage: EnforcementStageMember,
|
||||
}
|
||||
}
|
||||
// Collect endpoint info
|
||||
if matchedEndpoint == nil && endpoint != nil {
|
||||
// Update endpoint info (FINAL stage for team login - takes precedence)
|
||||
if endpoint != nil {
|
||||
matchedEndpoint = endpoint
|
||||
log.Trace("[ACL] Step 3.1.2: Updated matched endpoint from member check (FINAL)")
|
||||
}
|
||||
|
||||
// All checks passed for team login
|
||||
log.Trace("[ACL] Enforcement chain completed successfully: all team login checks passed")
|
||||
return true, matchedEndpoint, nil
|
||||
}
|
||||
|
||||
// 3.2: This is a user login (no TeamID)
|
||||
if authInfo.UserID != "" {
|
||||
log.Trace("[ACL] Detected user login (user_id=%s), checking user permissions", authInfo.UserID)
|
||||
|
||||
// This is the final stage for user login, its constraints take precedence
|
||||
allowed, endpoint, err := acl.enforceUser(ctx, authInfo, request)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if !allowed {
|
||||
log.Trace("[ACL] Enforcement chain terminated: user permission check failed")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied: user permission check failed",
|
||||
Stage: EnforcementStageUser,
|
||||
}
|
||||
}
|
||||
// Collect endpoint info
|
||||
if matchedEndpoint == nil && endpoint != nil {
|
||||
// Update endpoint info (FINAL stage for user login - takes precedence)
|
||||
if endpoint != nil {
|
||||
matchedEndpoint = endpoint
|
||||
log.Trace("[ACL] Step 3.2: Updated matched endpoint from user check (FINAL)")
|
||||
}
|
||||
|
||||
// All checks passed for user login
|
||||
log.Trace("[ACL] Enforcement chain completed successfully: all user login checks passed")
|
||||
return true, matchedEndpoint, nil
|
||||
}
|
||||
|
||||
// All checks passed (pure API call - only client check required)
|
||||
log.Trace("[ACL] Enforcement chain completed successfully: pure API call (client only)")
|
||||
return true, matchedEndpoint, nil
|
||||
}
|
||||
|
||||
// enforceClient checks client permissions independently
|
||||
// Returns: (allowed bool, endpointInfo *EndpointInfo, error)
|
||||
func (acl *ACL) enforceClient(ctx context.Context, authInfo *types.AuthorizedInfo, request *AccessRequest) (bool, *EndpointInfo, error) {
|
||||
log.Trace("[ACL] Step 1: enforceClient - Starting client permission check for client_id=%s", authInfo.ClientID)
|
||||
|
||||
// Get client role
|
||||
clientRole, err := role.RoleManager.GetClientRole(ctx, authInfo.ClientID)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Step 1: enforceClient - Failed to get client role: %v", err)
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypeInternal,
|
||||
Message: fmt.Sprintf("failed to get client role [client_id=%s]: %v", authInfo.ClientID, err),
|
||||
Stage: EnforcementStageClient,
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 1: enforceClient - Retrieved client role: %s", clientRole)
|
||||
|
||||
// Get scopes for client role
|
||||
allowedScopes, restrictedScopes, err := role.RoleManager.GetScopes(ctx, clientRole)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Step 1: enforceClient - Failed to get scopes for role %s: %v", clientRole, err)
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypeInternal,
|
||||
Message: fmt.Sprintf("failed to get client scopes [client_id=%s, role=%s]: %v", authInfo.ClientID, clientRole, err),
|
||||
Stage: EnforcementStageClient,
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 1: enforceClient - Retrieved scopes: allowed=%v, restricted=%v", allowedScopes, restrictedScopes)
|
||||
|
||||
// Step 1: Check if allowed scopes grant access
|
||||
allowedRequest := &AccessRequest{
|
||||
|
|
@ -187,7 +231,11 @@ func (acl *ACL) enforceClient(ctx context.Context, authInfo *types.AuthorizedInf
|
|||
}
|
||||
|
||||
decision := acl.Scope.Check(allowedRequest)
|
||||
log.Trace("[ACL] Step 1: enforceClient - Allowed scopes check: allowed=%v, reason=%s, required_scopes=%v, missing_scopes=%v, matched_pattern=%s",
|
||||
decision.Allowed, decision.Reason, decision.RequiredScopes, decision.MissingScopes, decision.MatchedPattern)
|
||||
|
||||
if !decision.Allowed {
|
||||
log.Trace("[ACL] Step 1: enforceClient - Access denied by allowed scopes check")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: decision.Reason,
|
||||
|
|
@ -211,7 +259,11 @@ func (acl *ACL) enforceClient(ctx context.Context, authInfo *types.AuthorizedInf
|
|||
}
|
||||
|
||||
restrictDecision := acl.Scope.CheckRestricted(restrictedRequest)
|
||||
log.Trace("[ACL] Step 1: enforceClient - Restricted scopes check: allowed=%v, reason=%s, matched_pattern=%s",
|
||||
restrictDecision.Allowed, restrictDecision.Reason, restrictDecision.MatchedPattern)
|
||||
|
||||
if !restrictDecision.Allowed {
|
||||
log.Trace("[ACL] Step 1: enforceClient - Access denied by restricted scopes")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied by restriction: " + restrictDecision.Reason,
|
||||
|
|
@ -227,15 +279,32 @@ func (acl *ACL) enforceClient(ctx context.Context, authInfo *types.AuthorizedInf
|
|||
}
|
||||
}
|
||||
|
||||
// Return matched endpoint info (contains OwnerOnly, TeamOnly, and future constraints)
|
||||
return true, decision.MatchedEndpoint, nil
|
||||
// Return matched endpoint info with scope-specific constraints
|
||||
var endpointInfo *EndpointInfo
|
||||
if decision.MatchedEndpoint != nil {
|
||||
if decision.MatchedScope != "" {
|
||||
// Get constraints for the specific matched scope
|
||||
endpointInfo = acl.Scope.GetScopeConstraints(decision.MatchedScope, request.Method, decision.MatchedPattern)
|
||||
log.Trace("[ACL] Step 1: enforceClient - Success, matched scope '%s': %+v", decision.MatchedScope, endpointInfo)
|
||||
} else {
|
||||
// No specific scope matched (e.g., public endpoint)
|
||||
endpointInfo = decision.MatchedEndpoint
|
||||
log.Trace("[ACL] Step 1: enforceClient - Success, matched endpoint: %+v", endpointInfo)
|
||||
}
|
||||
} else {
|
||||
log.Trace("[ACL] Step 1: enforceClient - Success, no endpoint info")
|
||||
}
|
||||
return true, endpointInfo, nil
|
||||
}
|
||||
|
||||
// enforceScope checks the explicit scopes from token independently
|
||||
// Returns: (allowed bool, endpointInfo *EndpointInfo, error)
|
||||
func (acl *ACL) enforceScope(_ context.Context, authInfo *types.AuthorizedInfo, request *AccessRequest) (bool, *EndpointInfo, error) {
|
||||
log.Trace("[ACL] Step 2: enforceScope - Starting token scope check")
|
||||
|
||||
// Parse scopes from token (space-separated)
|
||||
if authInfo.Scope == "" {
|
||||
log.Trace("[ACL] Step 2: enforceScope - Token scope is empty, skipping")
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
|
|
@ -250,6 +319,7 @@ func (acl *ACL) enforceScope(_ context.Context, authInfo *types.AuthorizedInfo,
|
|||
scopes = append(scopes, scope)
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 2: enforceScope - Parsed token scopes: %v", scopes)
|
||||
|
||||
// Build request with token scopes and check
|
||||
checkRequest := &AccessRequest{
|
||||
|
|
@ -259,7 +329,11 @@ func (acl *ACL) enforceScope(_ context.Context, authInfo *types.AuthorizedInfo,
|
|||
}
|
||||
|
||||
decision := acl.Scope.Check(checkRequest)
|
||||
log.Trace("[ACL] Step 2: enforceScope - Token scope check: allowed=%v, reason=%s, required_scopes=%v, missing_scopes=%v, matched_pattern=%s",
|
||||
decision.Allowed, decision.Reason, decision.RequiredScopes, decision.MissingScopes, decision.MatchedPattern)
|
||||
|
||||
if !decision.Allowed {
|
||||
log.Trace("[ACL] Step 2: enforceScope - Access denied by token scope")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: decision.Reason,
|
||||
|
|
@ -275,32 +349,52 @@ func (acl *ACL) enforceScope(_ context.Context, authInfo *types.AuthorizedInfo,
|
|||
}
|
||||
}
|
||||
|
||||
// Return matched endpoint info
|
||||
return true, decision.MatchedEndpoint, nil
|
||||
// Return matched endpoint info with scope-specific constraints
|
||||
var endpointInfo *EndpointInfo
|
||||
if decision.MatchedEndpoint != nil {
|
||||
if decision.MatchedScope != "" {
|
||||
// Get constraints for the specific matched scope
|
||||
endpointInfo = acl.Scope.GetScopeConstraints(decision.MatchedScope, request.Method, decision.MatchedPattern)
|
||||
log.Trace("[ACL] Step 2: enforceScope - Success, matched scope '%s': %+v", decision.MatchedScope, endpointInfo)
|
||||
} else {
|
||||
// No specific scope matched (e.g., public endpoint)
|
||||
endpointInfo = decision.MatchedEndpoint
|
||||
log.Trace("[ACL] Step 2: enforceScope - Success, matched endpoint: %+v", endpointInfo)
|
||||
}
|
||||
} else {
|
||||
log.Trace("[ACL] Step 2: enforceScope - Success, no endpoint info")
|
||||
}
|
||||
return true, endpointInfo, nil
|
||||
}
|
||||
|
||||
// enforceUser checks user permissions independently
|
||||
// Returns: (allowed bool, endpointInfo *EndpointInfo, error)
|
||||
func (acl *ACL) enforceUser(ctx context.Context, authInfo *types.AuthorizedInfo, request *AccessRequest) (bool, *EndpointInfo, error) {
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Starting user permission check for user_id=%s", authInfo.UserID)
|
||||
|
||||
// Get user role
|
||||
userRole, err := role.RoleManager.GetUserRole(ctx, authInfo.UserID)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Failed to get user role: %v", err)
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypeInternal,
|
||||
Message: fmt.Sprintf("failed to get user role [user_id=%s]: %v", authInfo.UserID, err),
|
||||
Stage: EnforcementStageUser,
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Retrieved user role: %s", userRole)
|
||||
|
||||
// Get scopes for user role
|
||||
allowedScopes, restrictedScopes, err := role.RoleManager.GetScopes(ctx, userRole)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Failed to get scopes for role %s: %v", userRole, err)
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypeInternal,
|
||||
Message: fmt.Sprintf("failed to get user scopes [user_id=%s, role=%s]: %v", authInfo.UserID, userRole, err),
|
||||
Stage: EnforcementStageUser,
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Retrieved scopes: allowed=%v, restricted=%v", allowedScopes, restrictedScopes)
|
||||
|
||||
// Step 1: Check if allowed scopes grant access
|
||||
allowedRequest := &AccessRequest{
|
||||
|
|
@ -310,7 +404,11 @@ func (acl *ACL) enforceUser(ctx context.Context, authInfo *types.AuthorizedInfo,
|
|||
}
|
||||
|
||||
decision := acl.Scope.Check(allowedRequest)
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Allowed scopes check: allowed=%v, reason=%s, required_scopes=%v, missing_scopes=%v, matched_pattern=%s",
|
||||
decision.Allowed, decision.Reason, decision.RequiredScopes, decision.MissingScopes, decision.MatchedPattern)
|
||||
|
||||
if !decision.Allowed {
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Access denied by allowed scopes check")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: decision.Reason,
|
||||
|
|
@ -334,7 +432,11 @@ func (acl *ACL) enforceUser(ctx context.Context, authInfo *types.AuthorizedInfo,
|
|||
}
|
||||
|
||||
restrictDecision := acl.Scope.CheckRestricted(restrictedRequest)
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Restricted scopes check: allowed=%v, reason=%s, matched_pattern=%s",
|
||||
restrictDecision.Allowed, restrictDecision.Reason, restrictDecision.MatchedPattern)
|
||||
|
||||
if !restrictDecision.Allowed {
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Access denied by restricted scopes")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied by restriction: " + restrictDecision.Reason,
|
||||
|
|
@ -350,32 +452,52 @@ func (acl *ACL) enforceUser(ctx context.Context, authInfo *types.AuthorizedInfo,
|
|||
}
|
||||
}
|
||||
|
||||
// Return matched endpoint info
|
||||
return true, decision.MatchedEndpoint, nil
|
||||
// Return matched endpoint info with scope-specific constraints
|
||||
var endpointInfo *EndpointInfo
|
||||
if decision.MatchedEndpoint != nil {
|
||||
if decision.MatchedScope != "" {
|
||||
// Get constraints for the specific matched scope
|
||||
endpointInfo = acl.Scope.GetScopeConstraints(decision.MatchedScope, request.Method, decision.MatchedPattern)
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Success, matched scope '%s': %+v", decision.MatchedScope, endpointInfo)
|
||||
} else {
|
||||
// No specific scope matched (e.g., public endpoint)
|
||||
endpointInfo = decision.MatchedEndpoint
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Success, matched endpoint: %+v", endpointInfo)
|
||||
}
|
||||
} else {
|
||||
log.Trace("[ACL] Step 3.2: enforceUser - Success, no endpoint info")
|
||||
}
|
||||
return true, endpointInfo, nil
|
||||
}
|
||||
|
||||
// enforceTeam checks team permissions independently
|
||||
// Returns: (allowed bool, endpointInfo *EndpointInfo, error)
|
||||
func (acl *ACL) enforceTeam(ctx context.Context, authInfo *types.AuthorizedInfo, request *AccessRequest) (bool, *EndpointInfo, error) {
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Starting team permission check for team_id=%s", authInfo.TeamID)
|
||||
|
||||
// Get team role
|
||||
teamRole, err := role.RoleManager.GetTeamRole(ctx, authInfo.TeamID)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Failed to get team role: %v", err)
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypeInternal,
|
||||
Message: fmt.Sprintf("failed to get team role [team_id=%s]: %v", authInfo.TeamID, err),
|
||||
Stage: EnforcementStageTeam,
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Retrieved team role: %s", teamRole)
|
||||
|
||||
// Get scopes for team role
|
||||
allowedScopes, restrictedScopes, err := role.RoleManager.GetScopes(ctx, teamRole)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Failed to get scopes for role %s: %v", teamRole, err)
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypeInternal,
|
||||
Message: fmt.Sprintf("failed to get team scopes [team_id=%s, role=%s]: %v", authInfo.TeamID, teamRole, err),
|
||||
Stage: EnforcementStageTeam,
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Retrieved scopes: allowed=%v, restricted=%v", allowedScopes, restrictedScopes)
|
||||
|
||||
// Step 1: Check if allowed scopes grant access
|
||||
allowedRequest := &AccessRequest{
|
||||
|
|
@ -385,7 +507,11 @@ func (acl *ACL) enforceTeam(ctx context.Context, authInfo *types.AuthorizedInfo,
|
|||
}
|
||||
|
||||
decision := acl.Scope.Check(allowedRequest)
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Allowed scopes check: allowed=%v, reason=%s, required_scopes=%v, missing_scopes=%v, matched_pattern=%s",
|
||||
decision.Allowed, decision.Reason, decision.RequiredScopes, decision.MissingScopes, decision.MatchedPattern)
|
||||
|
||||
if !decision.Allowed {
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Access denied by allowed scopes check")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: decision.Reason,
|
||||
|
|
@ -410,7 +536,11 @@ func (acl *ACL) enforceTeam(ctx context.Context, authInfo *types.AuthorizedInfo,
|
|||
}
|
||||
|
||||
restrictDecision := acl.Scope.CheckRestricted(restrictedRequest)
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Restricted scopes check: allowed=%v, reason=%s, matched_pattern=%s",
|
||||
restrictDecision.Allowed, restrictDecision.Reason, restrictDecision.MatchedPattern)
|
||||
|
||||
if !restrictDecision.Allowed {
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Access denied by restricted scopes")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied by restriction: " + restrictDecision.Reason,
|
||||
|
|
@ -427,32 +557,52 @@ func (acl *ACL) enforceTeam(ctx context.Context, authInfo *types.AuthorizedInfo,
|
|||
}
|
||||
}
|
||||
|
||||
// Return matched endpoint info
|
||||
return true, decision.MatchedEndpoint, nil
|
||||
// Return matched endpoint info with scope-specific constraints
|
||||
var endpointInfo *EndpointInfo
|
||||
if decision.MatchedEndpoint != nil {
|
||||
if decision.MatchedScope != "" {
|
||||
// Get constraints for the specific matched scope
|
||||
endpointInfo = acl.Scope.GetScopeConstraints(decision.MatchedScope, request.Method, decision.MatchedPattern)
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Success, matched scope '%s': %+v", decision.MatchedScope, endpointInfo)
|
||||
} else {
|
||||
// No specific scope matched (e.g., public endpoint)
|
||||
endpointInfo = decision.MatchedEndpoint
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Success, matched endpoint: %+v", endpointInfo)
|
||||
}
|
||||
} else {
|
||||
log.Trace("[ACL] Step 3.1: enforceTeam - Success, no endpoint info")
|
||||
}
|
||||
return true, endpointInfo, nil
|
||||
}
|
||||
|
||||
// enforceMember checks member permissions independently
|
||||
// Returns: (allowed bool, endpointInfo *EndpointInfo, error)
|
||||
func (acl *ACL) enforceMember(ctx context.Context, authInfo *types.AuthorizedInfo, request *AccessRequest) (bool, *EndpointInfo, error) {
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Starting member permission check for team_id=%s, user_id=%s", authInfo.TeamID, authInfo.UserID)
|
||||
|
||||
// Get member role (user's role in the team)
|
||||
memberRole, err := role.RoleManager.GetMemberRole(ctx, authInfo.TeamID, authInfo.UserID)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Failed to get member role: %v", err)
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypeInternal,
|
||||
Message: fmt.Sprintf("failed to get member role [team_id=%s, user_id=%s]: %v", authInfo.TeamID, authInfo.UserID, err),
|
||||
Stage: EnforcementStageMember,
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Retrieved member role: %s", memberRole)
|
||||
|
||||
// Get scopes for member role
|
||||
allowedScopes, restrictedScopes, err := role.RoleManager.GetScopes(ctx, memberRole)
|
||||
if err != nil {
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Failed to get scopes for role %s: %v", memberRole, err)
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypeInternal,
|
||||
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,
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Retrieved scopes: allowed=%v, restricted=%v", allowedScopes, restrictedScopes)
|
||||
|
||||
// Step 1: Check if allowed scopes grant access
|
||||
allowedRequest := &AccessRequest{
|
||||
|
|
@ -462,7 +612,11 @@ func (acl *ACL) enforceMember(ctx context.Context, authInfo *types.AuthorizedInf
|
|||
}
|
||||
|
||||
decision := acl.Scope.Check(allowedRequest)
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Allowed scopes check: allowed=%v, reason=%s, required_scopes=%v, missing_scopes=%v, matched_pattern=%s",
|
||||
decision.Allowed, decision.Reason, decision.RequiredScopes, decision.MissingScopes, decision.MatchedPattern)
|
||||
|
||||
if !decision.Allowed {
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Access denied by allowed scopes check")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: decision.Reason,
|
||||
|
|
@ -487,7 +641,11 @@ func (acl *ACL) enforceMember(ctx context.Context, authInfo *types.AuthorizedInf
|
|||
}
|
||||
|
||||
restrictDecision := acl.Scope.CheckRestricted(restrictedRequest)
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Restricted scopes check: allowed=%v, reason=%s, matched_pattern=%s",
|
||||
restrictDecision.Allowed, restrictDecision.Reason, restrictDecision.MatchedPattern)
|
||||
|
||||
if !restrictDecision.Allowed {
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Access denied by restricted scopes")
|
||||
return false, nil, &Error{
|
||||
Type: ErrorTypePermissionDenied,
|
||||
Message: "access denied by restriction: " + restrictDecision.Reason,
|
||||
|
|
@ -504,6 +662,20 @@ func (acl *ACL) enforceMember(ctx context.Context, authInfo *types.AuthorizedInf
|
|||
}
|
||||
}
|
||||
|
||||
// Return matched endpoint info
|
||||
return true, decision.MatchedEndpoint, nil
|
||||
// Return matched endpoint info with scope-specific constraints
|
||||
var endpointInfo *EndpointInfo
|
||||
if decision.MatchedEndpoint != nil {
|
||||
if decision.MatchedScope != "" {
|
||||
// Get constraints for the specific matched scope
|
||||
endpointInfo = acl.Scope.GetScopeConstraints(decision.MatchedScope, request.Method, decision.MatchedPattern)
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Success, matched scope '%s': %+v", decision.MatchedScope, endpointInfo)
|
||||
} else {
|
||||
// No specific scope matched (e.g., public endpoint)
|
||||
endpointInfo = decision.MatchedEndpoint
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Success, matched endpoint: %+v", endpointInfo)
|
||||
}
|
||||
} else {
|
||||
log.Trace("[ACL] Step 3.1.2: enforceMember - Success, no endpoint info")
|
||||
}
|
||||
return true, endpointInfo, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,45 @@ import (
|
|||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/yaoapp/gou/application"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// builtinScopes stores scopes registered by code (before configuration loading)
|
||||
var builtinScopes = make(map[string]*ScopeDefinition)
|
||||
var builtinScopesMutex sync.RWMutex
|
||||
|
||||
// Register registers built-in scopes that will be automatically loaded
|
||||
// This should be called in init() functions before the ACL system is initialized
|
||||
// Supports registering multiple scopes at once
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// acl.Register(
|
||||
// &acl.ScopeDefinition{
|
||||
// Name: "builtin:mfa:verification",
|
||||
// Description: "MFA verification - temporary access for MFA setup",
|
||||
// Endpoints: []string{"POST /user/mfa/totp/enable", "POST /user/mfa/totp/verify"},
|
||||
// },
|
||||
// &acl.ScopeDefinition{
|
||||
// Name: "builtin:team:selection",
|
||||
// Description: "Team selection scope",
|
||||
// Endpoints: []string{"POST /user/teams/select"},
|
||||
// },
|
||||
// )
|
||||
func Register(scopes ...*ScopeDefinition) {
|
||||
builtinScopesMutex.Lock()
|
||||
defer builtinScopesMutex.Unlock()
|
||||
|
||||
for _, scope := range scopes {
|
||||
builtinScopes[scope.Name] = scope
|
||||
log.Trace("[ACL] Registered builtin scope: %s (%d endpoints)", scope.Name, len(scope.Endpoints))
|
||||
}
|
||||
}
|
||||
|
||||
// LoadScopes loads the scope configuration from the openapi/scopes directory
|
||||
func LoadScopes() (*ScopeManager, error) {
|
||||
manager := &ScopeManager{
|
||||
|
|
@ -22,6 +55,20 @@ func LoadScopes() (*ScopeManager, error) {
|
|||
scopes: make(map[string]*ScopeDefinition),
|
||||
}
|
||||
|
||||
// Step 1: Load builtin scopes first (registered by code)
|
||||
builtinScopesMutex.RLock()
|
||||
builtinCount := len(builtinScopes)
|
||||
for name, scopeDef := range builtinScopes {
|
||||
// Create a copy to avoid mutation
|
||||
defCopy := *scopeDef
|
||||
manager.scopes[name] = &defCopy
|
||||
}
|
||||
builtinScopesMutex.RUnlock()
|
||||
|
||||
if builtinCount > 0 {
|
||||
log.Info("[ACL] Loaded %d builtin scopes from code registration", builtinCount)
|
||||
}
|
||||
|
||||
// Check if scopes directory exists
|
||||
scopesDir := filepath.Join("openapi", "scopes")
|
||||
exists, err := application.App.Exists(scopesDir)
|
||||
|
|
@ -30,30 +77,38 @@ func LoadScopes() (*ScopeManager, error) {
|
|||
}
|
||||
if !exists {
|
||||
log.Warn("[ACL] Scopes directory not found, using default deny policy")
|
||||
// Still build indexes for builtin scopes
|
||||
if builtinCount > 0 {
|
||||
if err := manager.buildIndexes(); err != nil {
|
||||
return nil, fmt.Errorf("failed to build indexes for builtin scopes: %w", err)
|
||||
}
|
||||
}
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
// Load global configuration (scopes.yml)
|
||||
// Step 2: Load global configuration (scopes.yml)
|
||||
if err := manager.loadGlobalConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load global config: %w", err)
|
||||
}
|
||||
|
||||
// Load alias configuration (alias.yml)
|
||||
// Step 3: Load alias configuration (alias.yml)
|
||||
if err := manager.loadAliasConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load alias config: %w", err)
|
||||
}
|
||||
|
||||
// Load scope definitions from subdirectories
|
||||
// Step 4: Load scope definitions from subdirectories
|
||||
// This will merge with builtin scopes (file scopes override builtin if same name)
|
||||
if err := manager.loadScopeDefinitions(); err != nil {
|
||||
return nil, fmt.Errorf("failed to load scope definitions: %w", err)
|
||||
}
|
||||
|
||||
// Build runtime indexes
|
||||
// Step 5: Build runtime indexes
|
||||
if err := manager.buildIndexes(); err != nil {
|
||||
return nil, fmt.Errorf("failed to build indexes: %w", err)
|
||||
}
|
||||
|
||||
log.Info("[ACL] Loaded %d scopes, %d aliases", len(manager.scopeIndex), len(manager.aliasIndex))
|
||||
log.Info("[ACL] Loaded %d scopes (%d builtin, %d from files), %d aliases",
|
||||
len(manager.scopeIndex), builtinCount, len(manager.scopes)-builtinCount, len(manager.aliasIndex))
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
|
|
@ -359,11 +414,55 @@ func (m *ScopeManager) addEndpointRule(method, path, action string, scopes []str
|
|||
Endpoint: info,
|
||||
})
|
||||
} else if strings.Contains(path, ":") {
|
||||
// Parameter path
|
||||
matcher.paramPaths[path] = info
|
||||
// Parameter path - merge with existing if present (support multiple scopes per endpoint)
|
||||
if existing := matcher.paramPaths[path]; existing != nil {
|
||||
// Endpoint already exists, merge scopes and constraints
|
||||
existing.RequiredScopes = append(existing.RequiredScopes, info.RequiredScopes...)
|
||||
// Merge constraints (OR logic: if any scope requires it, set to true)
|
||||
existing.OwnerOnly = existing.OwnerOnly || info.OwnerOnly
|
||||
existing.CreatorOnly = existing.CreatorOnly || info.CreatorOnly
|
||||
existing.EditorOnly = existing.EditorOnly || info.EditorOnly
|
||||
existing.TeamOnly = existing.TeamOnly || info.TeamOnly
|
||||
// Merge extra constraints
|
||||
if info.Extra != nil {
|
||||
if existing.Extra == nil {
|
||||
existing.Extra = make(map[string]interface{})
|
||||
}
|
||||
for key, value := range info.Extra {
|
||||
existing.Extra[key] = value
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Merged endpoint %s %s: scopes=%v, owner=%v, team=%v",
|
||||
method, path, existing.RequiredScopes, existing.OwnerOnly, existing.TeamOnly)
|
||||
} else {
|
||||
matcher.paramPaths[path] = info
|
||||
log.Trace("[ACL] Added endpoint %s %s: scopes=%v, owner=%v, team=%v",
|
||||
method, path, info.RequiredScopes, info.OwnerOnly, info.TeamOnly)
|
||||
}
|
||||
} else {
|
||||
// Exact path
|
||||
matcher.exactPaths[path] = info
|
||||
// Exact path - merge with existing if present (support multiple scopes per endpoint)
|
||||
if existing := matcher.exactPaths[path]; existing != nil {
|
||||
// Endpoint already exists, merge scopes and constraints
|
||||
existing.RequiredScopes = append(existing.RequiredScopes, info.RequiredScopes...)
|
||||
existing.OwnerOnly = existing.OwnerOnly || info.OwnerOnly
|
||||
existing.CreatorOnly = existing.CreatorOnly || info.CreatorOnly
|
||||
existing.EditorOnly = existing.EditorOnly || info.EditorOnly
|
||||
existing.TeamOnly = existing.TeamOnly || info.TeamOnly
|
||||
if info.Extra != nil {
|
||||
if existing.Extra == nil {
|
||||
existing.Extra = make(map[string]interface{})
|
||||
}
|
||||
for key, value := range info.Extra {
|
||||
existing.Extra[key] = value
|
||||
}
|
||||
}
|
||||
log.Trace("[ACL] Merged endpoint %s %s: scopes=%v, owner=%v, team=%v",
|
||||
method, path, existing.RequiredScopes, existing.OwnerOnly, existing.TeamOnly)
|
||||
} else {
|
||||
matcher.exactPaths[path] = info
|
||||
log.Trace("[ACL] Added endpoint %s %s: scopes=%v, owner=%v, team=%v",
|
||||
method, path, info.RequiredScopes, info.OwnerOnly, info.TeamOnly)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -417,27 +516,29 @@ func (m *ScopeManager) Check(req *AccessRequest) *AccessDecision {
|
|||
|
||||
// Check if user has any required scope (OR relationship)
|
||||
decision.RequiredScopes = endpoint.RequiredScopes
|
||||
hasScope := false
|
||||
var matchedScope string
|
||||
for _, required := range endpoint.RequiredScopes {
|
||||
for _, userScope := range expandedScopes {
|
||||
// Check for exact match or wildcard match
|
||||
if userScope == required || m.matchesWildcardScope(userScope, required) {
|
||||
hasScope = true
|
||||
matchedScope = required
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasScope {
|
||||
if matchedScope != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasScope {
|
||||
if matchedScope == "" {
|
||||
decision.Allowed = false
|
||||
decision.Reason = "missing required scopes"
|
||||
decision.MissingScopes = m.findMissingScopes(expandedScopes, endpoint.RequiredScopes)
|
||||
return decision
|
||||
}
|
||||
|
||||
// Record which scope was matched
|
||||
decision.MatchedScope = matchedScope
|
||||
decision.Allowed = true
|
||||
decision.Reason = "scope matched"
|
||||
return decision
|
||||
|
|
@ -663,6 +764,42 @@ func (m *ScopeManager) findMissingScopes(userScopes, requiredScopes []string) []
|
|||
return missing
|
||||
}
|
||||
|
||||
// GetScopeConstraints returns the constraints for a specific scope
|
||||
// This allows getting the original constraints for a matched scope,
|
||||
// instead of using merged constraints from multiple scopes
|
||||
func (m *ScopeManager) GetScopeConstraints(scopeName string, method, path string) *EndpointInfo {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
// Get the scope definition
|
||||
scopeDef := m.scopes[scopeName]
|
||||
if scopeDef == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create an EndpointInfo with this scope's constraints
|
||||
info := &EndpointInfo{
|
||||
Method: method,
|
||||
Path: path,
|
||||
Policy: PolicyRequireScopes,
|
||||
RequiredScopes: []string{scopeName},
|
||||
OwnerOnly: scopeDef.Owner,
|
||||
CreatorOnly: scopeDef.Creator,
|
||||
EditorOnly: scopeDef.Editor,
|
||||
TeamOnly: scopeDef.Team,
|
||||
}
|
||||
|
||||
// Copy extra constraints
|
||||
if len(scopeDef.Extra) > 0 {
|
||||
info.Extra = make(map[string]interface{})
|
||||
for key, value := range scopeDef.Extra {
|
||||
info.Extra[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
// Reload reloads the scope configuration
|
||||
func (m *ScopeManager) Reload() error {
|
||||
m.mu.Lock()
|
||||
|
|
|
|||
|
|
@ -16,9 +16,10 @@ var DefaultConfig = Config{
|
|||
|
||||
// Config is the configuration for the ACL
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Cache store.Store `json:"-"`
|
||||
Provider types.UserProvider `json:"-"`
|
||||
Enabled bool `json:"enabled"`
|
||||
PathPrefix string `json:"path_prefix"` // BaseURL prefix to strip from request paths (e.g., "/v1")
|
||||
Cache store.Store `json:"-"`
|
||||
Provider types.UserProvider `json:"-"`
|
||||
}
|
||||
|
||||
// ACL is the ACL checker
|
||||
|
|
@ -236,6 +237,7 @@ type AccessDecision struct {
|
|||
// Matched endpoint info
|
||||
MatchedEndpoint *EndpointInfo
|
||||
MatchedPattern string // Matched path pattern
|
||||
MatchedScope string // Which scope was actually matched (for constraint lookup)
|
||||
|
||||
// Permission check details
|
||||
RequiredScopes []string // Required scopes
|
||||
|
|
|
|||
|
|
@ -65,7 +65,12 @@ func Load(appConfig config.Config) (*OpenAPI, error) {
|
|||
}
|
||||
|
||||
// Load the ACL enforcer
|
||||
_, err = acl.Load(&acl.Config{Enabled: true, Cache: oauthConfig.Cache, Provider: oauthConfig.UserProvider})
|
||||
_, err = acl.Load(&acl.Config{
|
||||
Enabled: true,
|
||||
PathPrefix: config.BaseURL,
|
||||
Cache: oauthConfig.Cache,
|
||||
Provider: oauthConfig.UserProvider,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/kun/maps"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
||||
|
|
@ -22,8 +23,8 @@ import (
|
|||
|
||||
// GinMemberList handles GET /teams/:team_id/members - Get team members
|
||||
func GinMemberList(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
|
||||
authInfo := authorized.GetInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/kun/maps"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/authorized"
|
||||
"github.com/yaoapp/yao/openapi/oauth/providers/user"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
)
|
||||
|
|
@ -76,7 +77,7 @@ func GinTeamList(c *gin.Context) {
|
|||
// GinTeamGet handles GET /teams/:id - Get user team details
|
||||
func GinTeamGet(c *gin.Context) {
|
||||
// Get authorized user info
|
||||
authInfo := oauth.GetAuthorizedInfo(c)
|
||||
authInfo := authorized.GetInfo(c)
|
||||
if authInfo == nil || authInfo.UserID == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidClient.Code,
|
||||
|
|
@ -129,16 +130,16 @@ func GinTeamGet(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
// Check if user owns this team
|
||||
ownerID := toString(teamData["owner_id"])
|
||||
if ownerID != authInfo.UserID {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrAccessDenied.Code,
|
||||
ErrorDescription: "Access denied: you don't own this team",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
return
|
||||
}
|
||||
// // Check if user owns this team
|
||||
// ownerID := toString(teamData["owner_id"])
|
||||
// if ownerID != authInfo.UserID {
|
||||
// errorResp := &response.ErrorResponse{
|
||||
// Code: response.ErrAccessDenied.Code,
|
||||
// ErrorDescription: "Access denied: you don't own this team",
|
||||
// }
|
||||
// response.RespondWithError(c, response.StatusForbidden, errorResp)
|
||||
// return
|
||||
// }
|
||||
|
||||
// Convert to response format
|
||||
team := mapToTeamDetailResponse(teamData)
|
||||
|
|
|
|||
|
|
@ -34,13 +34,13 @@ const (
|
|||
|
||||
const (
|
||||
// ScopeMFAVerification is the MFA verification scope for temporary access token
|
||||
ScopeMFAVerification = "mfa_verification"
|
||||
ScopeMFAVerification = "builtin:mfa:verification"
|
||||
// ScopeTeamSelection is the team selection scope for temporary access token
|
||||
ScopeTeamSelection = "team_selection"
|
||||
ScopeTeamSelection = "builtin:teams:selection"
|
||||
// ScopeInviteVerification is the invite verification scope for temporary access token
|
||||
ScopeInviteVerification = "invite_verification"
|
||||
ScopeInviteVerification = "builtin:invite:verification"
|
||||
// ScopeEntryVerification is the entry verification scope for temporary access token (login or register)
|
||||
ScopeEntryVerification = "entry_verification"
|
||||
ScopeEntryVerification = "builtin:entry:verification"
|
||||
)
|
||||
|
||||
// FormConfig represents the form configuration
|
||||
|
|
|
|||
|
|
@ -5,10 +5,55 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/process"
|
||||
"github.com/yaoapp/yao/openapi/oauth/acl"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register builtin scopes for temporary tokens (before ACL initialization)
|
||||
// These scopes grant limited access to specific endpoints for special purposes
|
||||
acl.Register(
|
||||
// MFA verification scope - allows users to complete MFA setup during login
|
||||
&acl.ScopeDefinition{
|
||||
Name: ScopeMFAVerification,
|
||||
Description: "MFA verification - temporary access for completing MFA challenge",
|
||||
Endpoints: []string{
|
||||
"POST /user/mfa/totp/verify",
|
||||
"POST /user/mfa/sms/verify",
|
||||
"GET /user/mfa/totp",
|
||||
},
|
||||
},
|
||||
// Team selection scope - allows users to select a team and issue new tokens
|
||||
&acl.ScopeDefinition{
|
||||
Name: ScopeTeamSelection,
|
||||
Description: "Team selection - temporary access for selecting a team after login",
|
||||
Endpoints: []string{
|
||||
"POST /user/teams/select",
|
||||
"GET /user/teams/config",
|
||||
},
|
||||
},
|
||||
// Invite verification scope - allows users to accept team invitations
|
||||
&acl.ScopeDefinition{
|
||||
Name: ScopeInviteVerification,
|
||||
Description: "Invite verification - temporary access for accepting team invitations",
|
||||
Endpoints: []string{
|
||||
"POST /user/teams/invitations/:invitation_id/accept",
|
||||
"GET /user/teams/invitations/:invitation_id",
|
||||
},
|
||||
},
|
||||
// Entry verification scope - allows users to complete registration or login verification
|
||||
&acl.ScopeDefinition{
|
||||
Name: ScopeEntryVerification,
|
||||
Description: "Entry verification - temporary access for completing registration or login verification",
|
||||
Endpoints: []string{
|
||||
"POST /user/entry/register",
|
||||
"POST /user/entry/login",
|
||||
"POST /user/entry/invite/verify",
|
||||
"POST /user/entry/otp",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Register user process handlers
|
||||
process.RegisterGroup("user", map[string]process.Handler{
|
||||
"team.list": ProcessTeamList,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue