Refactor OAuth guard and ACL error handling for improved response structure

- 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.
This commit is contained in:
Max 2025-10-20 16:23:12 +08:00
parent 0f22d2d8c8
commit 0bd6bdcda3
4 changed files with 60 additions and 28 deletions

View file

@ -38,16 +38,16 @@ func (acl *ACL) Enforce(c *gin.Context) (bool, error) {
decision := acl.Scope.Check(request)
if !decision.Allowed {
// Return 403 Forbidden with details
c.JSON(403, map[string]interface{}{
"code": 403,
"message": "Access denied",
"reason": decision.Reason,
"required_scopes": decision.RequiredScopes,
"missing_scopes": decision.MissingScopes,
})
c.Abort()
return false, nil
// 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

View file

@ -10,6 +10,7 @@ import (
"github.com/yaoapp/yao/openapi/oauth/acl"
"github.com/yaoapp/yao/openapi/oauth/authorized"
"github.com/yaoapp/yao/openapi/oauth/types"
"github.com/yaoapp/yao/openapi/response"
)
// Guard is the OAuth guard middleware
@ -19,7 +20,7 @@ func (s *Service) Guard(c *gin.Context) {
// Validate the token
if token == "" {
c.JSON(http.StatusUnauthorized, types.ErrTokenMissing)
response.RespondWithError(c, http.StatusUnauthorized, types.ErrTokenMissing)
c.Abort()
return
}
@ -27,7 +28,7 @@ func (s *Service) Guard(c *gin.Context) {
// Validate the token
claims, err := s.VerifyToken(token)
if err != nil {
c.JSON(http.StatusUnauthorized, types.ErrInvalidToken)
response.RespondWithError(c, http.StatusUnauthorized, types.ErrInvalidToken)
c.Abort()
return
}
@ -53,9 +54,10 @@ func (s *Service) Guard(c *gin.Context) {
return
}
// If permissions are not granted, return forbidden
// If permissions are not granted but no error returned, it's an unexpected state
// This should not happen with the current implementation
if !ok {
c.JSON(http.StatusForbidden, types.ErrForbidden)
response.RespondWithError(c, http.StatusForbidden, types.ErrForbidden)
c.Abort()
return
}
@ -70,7 +72,7 @@ func GetAuthorizedInfo(c *gin.Context) *types.AuthorizedInfo {
func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
refreshToken := s.getRefreshToken(c)
if refreshToken == "" {
c.JSON(http.StatusUnauthorized, types.ErrRefreshTokenMissing)
response.RespondWithError(c, http.StatusUnauthorized, types.ErrRefreshTokenMissing)
c.Abort()
return
}
@ -78,7 +80,7 @@ func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
// Verify the refresh token
_, err := s.VerifyToken(refreshToken)
if err != nil {
c.JSON(http.StatusUnauthorized, types.ErrInvalidRefreshToken)
response.RespondWithError(c, http.StatusUnauthorized, types.ErrInvalidRefreshToken)
c.Abort()
return
}
@ -177,11 +179,32 @@ func (s *Service) handleACLError(c *gin.Context, err error) {
case acl.ErrorTypeInsufficientScope:
statusCode = http.StatusForbidden
errResponse = types.ErrInsufficientScope
// Include detailed scope information for insufficient scope errors
requiredScopes, _ := aclErr.Details["required_scopes"].([]string)
missingScopes, _ := aclErr.Details["missing_scopes"].([]string)
errResponse = &types.ErrorResponse{
Code: "insufficient_scope",
ErrorDescription: "The access token does not have the required scope",
Reason: aclErr.Message,
RequiredScopes: requiredScopes,
MissingScopes: missingScopes,
}
case acl.ErrorTypePermissionDenied:
statusCode = http.StatusForbidden
errResponse = types.ErrForbidden
// Include detailed information for permission denied errors
requiredScopes, _ := aclErr.Details["required_scopes"].([]string)
missingScopes, _ := aclErr.Details["missing_scopes"].([]string)
// Use standard ErrorResponse format with extended ACL fields
errResponse = &types.ErrorResponse{
Code: "forbidden",
ErrorDescription: "You do not have permission to access this resource",
Reason: aclErr.Message,
RequiredScopes: requiredScopes,
MissingScopes: missingScopes,
}
case acl.ErrorTypeResourceNotAllowed:
statusCode = http.StatusForbidden
@ -211,12 +234,12 @@ func (s *Service) handleACLError(c *gin.Context, err error) {
errResponse = types.ErrACLInternalError
}
c.JSON(statusCode, errResponse)
response.RespondWithError(c, statusCode, errResponse)
c.Abort()
return
}
// If it's not an ACL error, treat it as an internal error
c.JSON(http.StatusInternalServerError, types.ErrACLInternalError)
response.RespondWithError(c, http.StatusInternalServerError, types.ErrACLInternalError)
c.Abort()
}

View file

@ -34,6 +34,11 @@ type ErrorResponse struct {
ErrorDescription string `json:"error_description,omitempty"`
ErrorURI string `json:"error_uri,omitempty"`
State string `json:"state,omitempty"`
// Extended fields for ACL and permission errors (optional, following OAuth 2.0 extensibility)
Reason string `json:"reason,omitempty"` // Detailed reason for denial
RequiredScopes []string `json:"required_scopes,omitempty"` // Required scopes for access
MissingScopes []string `json:"missing_scopes,omitempty"` // Scopes that are missing
}
// Error implements the error interface

View file

@ -95,9 +95,10 @@ func importDataFromCSV(filename string, mod *model.Model, options ImportOption,
}
// Convert to interface slice and parse JSON fields
row := make([]interface{}, len(record))
for i, v := range record {
row[i] = parseJSONField(v, columnTypes[i])
// Ensure row length matches header length to prevent index out of range
row := make([]interface{}, len(header))
for i := 0; i < len(header) && i < len(record); i++ {
row[i] = parseJSONField(record[i], columnTypes[i])
}
chunk = append(chunk, row)
@ -195,9 +196,10 @@ func importDataFromXLSX(filename string, mod *model.Model, options ImportOption,
}
// Convert to interface slice and parse JSON fields
row := make([]interface{}, len(record))
for i, v := range record {
row[i] = parseJSONField(v, columnTypes[i])
// Ensure row length matches header length to prevent index out of range
row := make([]interface{}, len(header))
for i := 0; i < len(header) && i < len(record); i++ {
row[i] = parseJSONField(record[i], columnTypes[i])
}
chunk = append(chunk, row)
@ -425,7 +427,8 @@ func importBatch(mod *model.Model, columns []string, data [][]interface{}, start
for i, row := range data {
rowMap := maps.MakeMapStrAny()
for j, col := range columns {
if j < len(row) {
// Ensure we don't access beyond row length
if j < len(row) && j < len(columns) {
rowMap[col] = row[j]
}
}
@ -446,7 +449,8 @@ func importEach(mod *model.Model, columns []string, data [][]interface{}, startL
// Convert row to map
rowMap := maps.MakeMapStrAny()
for j, col := range columns {
if j < len(row) {
// Ensure we don't access beyond row length
if j < len(row) && j < len(columns) {
rowMap[col] = row[j]
}
}