diff --git a/openapi/oauth/acl/enforce.go b/openapi/oauth/acl/enforce.go index 04314609..062d9a75 100644 --- a/openapi/oauth/acl/enforce.go +++ b/openapi/oauth/acl/enforce.go @@ -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 diff --git a/openapi/oauth/guard.go b/openapi/oauth/guard.go index dbfe5372..da377833 100644 --- a/openapi/oauth/guard.go +++ b/openapi/oauth/guard.go @@ -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() } diff --git a/openapi/oauth/types/types.go b/openapi/oauth/types/types.go index d10069f7..9ce16960 100644 --- a/openapi/oauth/types/types.go +++ b/openapi/oauth/types/types.go @@ -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 diff --git a/seed/seed.go b/seed/seed.go index 897880f8..12a37d42 100644 --- a/seed/seed.go +++ b/seed/seed.go @@ -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] } }