Add ACL support to OAuth guard and error handling
- Integrated ACL (Access Control List) functionality into the OAuth guard, enabling permission checks and rate limiting based on ACL configurations. - Updated error responses for token validation and refresh token handling to use standardized error types. - Enhanced error handling for ACL-related issues, providing detailed HTTP responses based on specific ACL error types. - Improved overall security and user experience by ensuring proper authorization checks are enforced during API requests.
This commit is contained in:
parent
140368a69b
commit
667d40b5a1
9 changed files with 586 additions and 5 deletions
263
openapi/oauth/ERRORS.md
Normal file
263
openapi/oauth/ERRORS.md
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
# OAuth Error Handling Documentation
|
||||
|
||||
This document describes the error handling specifications and ACL error definitions for OAuth services.
|
||||
|
||||
## Overview
|
||||
|
||||
OAuth services use a standardized error response format. All errors follow the `ErrorResponse` structure:
|
||||
|
||||
```go
|
||||
type ErrorResponse struct {
|
||||
Code string `json:"error"`
|
||||
ErrorDescription string `json:"error_description,omitempty"`
|
||||
ErrorURI string `json:"error_uri,omitempty"`
|
||||
State string `json:"state,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Errors
|
||||
|
||||
These errors are used during OAuth service initialization and configuration:
|
||||
|
||||
| Error Code | Variable Name | Description |
|
||||
| ---------------------------- | ----------------------------- | -------------------------------------------------------------------- |
|
||||
| `invalid_configuration` | `ErrInvalidConfiguration` | Invalid OAuth service configuration |
|
||||
| `store_missing` | `ErrStoreMissing` | Store is required for OAuth service |
|
||||
| `issuer_url_missing` | `ErrIssuerURLMissing` | Issuer URL is missing |
|
||||
| `certificate_missing` | `ErrCertificateMissing` | JWT signing certificate and key paths must both be provided or empty |
|
||||
| `invalid_token_lifetime` | `ErrInvalidTokenLifetime` | Token lifetime must be greater than 0 |
|
||||
| `pkce_configuration_invalid` | `ErrPKCEConfigurationInvalid` | PKCE configuration is invalid |
|
||||
|
||||
## Authentication & Authorization Errors
|
||||
|
||||
### Token Related Errors
|
||||
|
||||
| Error Code | Variable Name | HTTP Status | Description |
|
||||
| ----------------------- | ------------------------ | ----------- | -------------------------------------------------- |
|
||||
| `token_missing` | `ErrTokenMissing` | 401 | No access token provided in the request |
|
||||
| `invalid_token` | `ErrInvalidToken` | 401 | The access token is invalid, expired or malformed |
|
||||
| `token_expired` | `ErrTokenExpired` | 401 | The access token has expired |
|
||||
| `unauthorized` | `ErrUnauthorized` | 401 | Authentication is required to access this resource |
|
||||
| `refresh_token_missing` | `ErrRefreshTokenMissing` | 401 | No refresh token provided in the request |
|
||||
| `invalid_refresh_token` | `ErrInvalidRefreshToken` | 401 | The refresh token is invalid or expired |
|
||||
|
||||
### Permission Related Errors
|
||||
|
||||
| Error Code | Variable Name | HTTP Status | Description |
|
||||
| -------------------- | ---------------------- | ----------- | -------------------------------------------------- |
|
||||
| `forbidden` | `ErrForbidden` | 403 | You do not have permission to access this resource |
|
||||
| `access_denied` | `ErrAccessDenied` | 403 | Access to this resource has been denied |
|
||||
| `insufficient_scope` | `ErrInsufficientScope` | 403 | The access token does not have the required scope |
|
||||
|
||||
### ACL Errors
|
||||
|
||||
| Error Code | Variable Name | HTTP Status | Description |
|
||||
| -------------------- | --------------------- | ----------- | ---------------------------------------- |
|
||||
| `acl_check_failed` | `ErrACLCheckFailed` | 500 | ACL verification failed |
|
||||
| `acl_internal_error` | `ErrACLInternalError` | 500 | Internal error occurred during ACL check |
|
||||
|
||||
### Rate Limiting Errors
|
||||
|
||||
| Error Code | Variable Name | HTTP Status | Description |
|
||||
| --------------------- | ---------------------- | ----------- | ----------------------------------------- |
|
||||
| `rate_limit_exceeded` | `ErrRateLimitExceeded` | 429 | Too many requests. Please try again later |
|
||||
| `too_many_requests` | `ErrTooManyRequests` | 429 | Request rate limit exceeded |
|
||||
|
||||
### Resource Errors
|
||||
|
||||
| Error Code | Variable Name | HTTP Status | Description |
|
||||
| -------------------- | --------------------- | ----------- | ------------------------------------------------ |
|
||||
| `resource_not_found` | `ErrResourceNotFound` | 404 | The requested resource was not found |
|
||||
| `method_not_allowed` | `ErrMethodNotAllowed` | 405 | The HTTP method is not allowed for this resource |
|
||||
|
||||
### Server Errors
|
||||
|
||||
| Error Code | Variable Name | HTTP Status | Description |
|
||||
| ----------------------- | ------------------------ | ----------- | -------------------------------------- |
|
||||
| `internal_server_error` | `ErrInternalServerError` | 500 | An internal server error occurred |
|
||||
| `service_unavailable` | `ErrServiceUnavailable` | 503 | The service is temporarily unavailable |
|
||||
|
||||
## ACL Error Types
|
||||
|
||||
ACL implementations can return the following error types, which the Guard middleware automatically converts to appropriate HTTP responses:
|
||||
|
||||
### acl.Error Structure
|
||||
|
||||
```go
|
||||
type Error struct {
|
||||
Type ErrorType // Error type
|
||||
Message string // Error message
|
||||
Details map[string]interface{} // Additional error details
|
||||
RetryAfter int // Retry wait time (seconds)
|
||||
}
|
||||
```
|
||||
|
||||
### ACL Error Type Definitions
|
||||
|
||||
| Error Type | HTTP Status | Description | Retryable |
|
||||
| ---------------------- | ----------- | ------------------------------------------------- | --------- |
|
||||
| `permission_denied` | 403 | User does not have required permissions | No |
|
||||
| `rate_limit_exceeded` | 429 | Request rate limit exceeded | Yes |
|
||||
| `insufficient_scope` | 403 | Token scope is insufficient | No |
|
||||
| `resource_not_allowed` | 403 | Access to the resource is not allowed | No |
|
||||
| `method_not_allowed` | 405 | The HTTP method is not allowed | No |
|
||||
| `ip_blocked` | 403 | IP address is blocked | No |
|
||||
| `geo_restricted` | 403 | Access is restricted based on geographic location | No |
|
||||
| `time_restricted` | 403 | Access is restricted based on time | Yes |
|
||||
| `quota_exceeded` | 429 | Usage quota has been exceeded | Yes |
|
||||
| `invalid_request` | 400 | Request is invalid | No |
|
||||
| `internal_error` | 500 | Internal error occurred during ACL check | Yes |
|
||||
|
||||
### ACL Error Creation Functions
|
||||
|
||||
```go
|
||||
// Basic error creation
|
||||
acl.NewError(errorType, message)
|
||||
|
||||
// Permission denied
|
||||
acl.NewPermissionDeniedError("User does not have admin role")
|
||||
|
||||
// Rate limit error (with retry time)
|
||||
acl.NewRateLimitError("Too many requests", 60) // Retry after 60 seconds
|
||||
|
||||
// Insufficient scope
|
||||
acl.NewInsufficientScopeError("Missing required scope", []string{"read", "write"})
|
||||
|
||||
// Resource not allowed
|
||||
acl.NewResourceNotAllowedError("/admin/users")
|
||||
|
||||
// Method not allowed
|
||||
acl.NewMethodNotAllowedError("DELETE", []string{"GET", "POST"})
|
||||
|
||||
// IP blocked
|
||||
acl.NewIPBlockedError("192.168.1.1")
|
||||
|
||||
// Quota exceeded
|
||||
acl.NewQuotaExceededError("API quota exceeded", "api_calls", 1000, 1050)
|
||||
|
||||
// Internal error
|
||||
acl.NewInternalError("Failed to load ACL rules")
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Using in Guard Middleware
|
||||
|
||||
The Guard middleware automatically handles all error types:
|
||||
|
||||
```go
|
||||
func (s *Service) Guard(c *gin.Context) {
|
||||
// Token validation
|
||||
token := s.getAccessToken(c)
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, types.ErrTokenMissing)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// ACL check
|
||||
ok, err := acl.Global.Enforce(c)
|
||||
if err != nil {
|
||||
s.handleACLError(c, err) // Automatically handles different types of ACL errors
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Implementing ACL Enforce Method
|
||||
|
||||
```go
|
||||
func (a *MyACL) Enforce(c *gin.Context) (bool, error) {
|
||||
// Check rate limit
|
||||
if rateLimitExceeded {
|
||||
return false, acl.NewRateLimitError("Too many requests", 60)
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if !hasPermission {
|
||||
return false, acl.NewPermissionDeniedError("User does not have required permission")
|
||||
}
|
||||
|
||||
// Check IP
|
||||
if ipBlocked {
|
||||
return false, acl.NewIPBlockedError(clientIP)
|
||||
}
|
||||
|
||||
// Check quota
|
||||
if quotaExceeded {
|
||||
return false, acl.NewQuotaExceededError("API quota exceeded", "api_calls", limit, current)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Error Response Examples
|
||||
|
||||
#### Standard Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "token_missing",
|
||||
"error_description": "No access token provided in the request"
|
||||
}
|
||||
```
|
||||
|
||||
#### Rate Limit Error Response (with Retry-After header)
|
||||
|
||||
```
|
||||
HTTP/1.1 429 Too Many Requests
|
||||
Retry-After: 60
|
||||
|
||||
{
|
||||
"error": "rate_limit_exceeded",
|
||||
"error_description": "Too many requests. Please try again later"
|
||||
}
|
||||
```
|
||||
|
||||
#### Forbidden Response
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "forbidden",
|
||||
"error_description": "You do not have permission to access this resource"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling Best Practices
|
||||
|
||||
1. **Use Predefined Error Constants**: Always use `types.Err*` constants instead of manually creating error responses
|
||||
2. **Provide Detailed Error Information**: For ACL errors, use the Details field to provide additional context
|
||||
3. **Set Appropriate HTTP Status Codes**: The Guard middleware handles this automatically, but ensure correct status codes elsewhere
|
||||
4. **Set Retry-After for Retryable Errors**: For rate limiting and quota errors, provide retry time
|
||||
5. **Log Error Details**: Log detailed error information for debugging before returning errors
|
||||
6. **Avoid Leaking Sensitive Information**: Error messages should be user-friendly but not expose internal system details
|
||||
|
||||
## Standard HTTP Status Code Mapping
|
||||
|
||||
- **200 OK**: Request successful
|
||||
- **400 Bad Request**: Invalid request parameters
|
||||
- **401 Unauthorized**: Not authenticated or authentication failed
|
||||
- **403 Forbidden**: Authenticated but not authorized to access
|
||||
- **404 Not Found**: Resource does not exist
|
||||
- **405 Method Not Allowed**: HTTP method not allowed
|
||||
- **429 Too Many Requests**: Rate limit or quota exceeded
|
||||
- **500 Internal Server Error**: Internal server error
|
||||
- **503 Service Unavailable**: Service temporarily unavailable
|
||||
|
||||
## Testing Error Handling
|
||||
|
||||
It is recommended to test the following scenarios:
|
||||
|
||||
1. Missing Token
|
||||
2. Invalid Token
|
||||
3. Expired Token
|
||||
4. Insufficient Permissions
|
||||
5. Rate Limit Exceeded
|
||||
6. Quota Exceeded
|
||||
7. IP Blocked
|
||||
8. Resource Not Found
|
||||
9. Method Not Allowed
|
||||
10. ACL Internal Error
|
||||
|
||||
Each scenario should return the appropriate HTTP status code and error response.
|
||||
27
openapi/oauth/acl/acl.go
Normal file
27
openapi/oauth/acl/acl.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package acl
|
||||
|
||||
// Global is the global ACL enforcer
|
||||
var Global Enforcer = nil
|
||||
|
||||
// New creates a new ACL enforcer
|
||||
func New(config *Config) Enforcer {
|
||||
|
||||
if config == nil {
|
||||
config = &DefaultConfig
|
||||
}
|
||||
|
||||
return &ACL{
|
||||
Config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads the ACL enforcer
|
||||
func Load(config *Config) (Enforcer, error) {
|
||||
Global = New(config)
|
||||
return Global, nil
|
||||
}
|
||||
|
||||
// Enabled returns true if the ACL is enabled, otherwise false
|
||||
func (acl *ACL) Enabled() bool {
|
||||
return acl.Config.Enabled
|
||||
}
|
||||
8
openapi/oauth/acl/enforce.go
Normal file
8
openapi/oauth/acl/enforce.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
package acl
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Enforce checks if the user has access to the resource
|
||||
func (acl *ACL) Enforce(c *gin.Context) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
126
openapi/oauth/acl/errors.go
Normal file
126
openapi/oauth/acl/errors.go
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
package acl
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ErrorType represents different types of ACL errors
|
||||
type ErrorType string
|
||||
|
||||
const (
|
||||
// ErrorTypePermissionDenied indicates the user does not have required permissions
|
||||
ErrorTypePermissionDenied ErrorType = "permission_denied"
|
||||
|
||||
// ErrorTypeRateLimitExceeded indicates the request rate limit has been exceeded
|
||||
ErrorTypeRateLimitExceeded ErrorType = "rate_limit_exceeded"
|
||||
|
||||
// ErrorTypeInsufficientScope indicates the token scope is insufficient
|
||||
ErrorTypeInsufficientScope ErrorType = "insufficient_scope"
|
||||
|
||||
// ErrorTypeResourceNotAllowed indicates access to the resource is not allowed
|
||||
ErrorTypeResourceNotAllowed ErrorType = "resource_not_allowed"
|
||||
|
||||
// ErrorTypeMethodNotAllowed indicates the HTTP method is not allowed
|
||||
ErrorTypeMethodNotAllowed ErrorType = "method_not_allowed"
|
||||
|
||||
// ErrorTypeIPBlocked indicates the request IP is blocked
|
||||
ErrorTypeIPBlocked ErrorType = "ip_blocked"
|
||||
|
||||
// ErrorTypeGeoRestricted indicates access is restricted based on geographic location
|
||||
ErrorTypeGeoRestricted ErrorType = "geo_restricted"
|
||||
|
||||
// ErrorTypeTimeRestricted indicates access is restricted based on time
|
||||
ErrorTypeTimeRestricted ErrorType = "time_restricted"
|
||||
|
||||
// ErrorTypeQuotaExceeded indicates the usage quota has been exceeded
|
||||
ErrorTypeQuotaExceeded ErrorType = "quota_exceeded"
|
||||
|
||||
// ErrorTypeInvalidRequest indicates the request is invalid
|
||||
ErrorTypeInvalidRequest ErrorType = "invalid_request"
|
||||
|
||||
// ErrorTypeInternal indicates an internal error occurred during ACL check
|
||||
ErrorTypeInternal ErrorType = "internal_error"
|
||||
)
|
||||
|
||||
// Error represents an ACL-related error with additional context
|
||||
type Error struct {
|
||||
Type ErrorType
|
||||
Message string
|
||||
Details map[string]interface{}
|
||||
RetryAfter int // seconds to wait before retrying (for rate limit errors)
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e *Error) Error() string {
|
||||
if e.Message != "" {
|
||||
return fmt.Sprintf("ACL error [%s]: %s", e.Type, e.Message)
|
||||
}
|
||||
return fmt.Sprintf("ACL error: %s", e.Type)
|
||||
}
|
||||
|
||||
// IsRetryable returns true if the error is retryable (e.g., rate limit)
|
||||
func (e *Error) IsRetryable() bool {
|
||||
return e.Type == ErrorTypeRateLimitExceeded || e.Type == ErrorTypeQuotaExceeded
|
||||
}
|
||||
|
||||
// NewError creates a new ACL error
|
||||
func NewError(errorType ErrorType, message string) *Error {
|
||||
return &Error{
|
||||
Type: errorType,
|
||||
Message: message,
|
||||
Details: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// NewPermissionDeniedError creates a permission denied error
|
||||
func NewPermissionDeniedError(message string) *Error {
|
||||
return NewError(ErrorTypePermissionDenied, message)
|
||||
}
|
||||
|
||||
// NewRateLimitError creates a rate limit error with retry-after information
|
||||
func NewRateLimitError(message string, retryAfter int) *Error {
|
||||
err := NewError(ErrorTypeRateLimitExceeded, message)
|
||||
err.RetryAfter = retryAfter
|
||||
return err
|
||||
}
|
||||
|
||||
// NewInsufficientScopeError creates an insufficient scope error
|
||||
func NewInsufficientScopeError(message string, requiredScopes []string) *Error {
|
||||
err := NewError(ErrorTypeInsufficientScope, message)
|
||||
err.Details["required_scopes"] = requiredScopes
|
||||
return err
|
||||
}
|
||||
|
||||
// NewResourceNotAllowedError creates a resource not allowed error
|
||||
func NewResourceNotAllowedError(resource string) *Error {
|
||||
err := NewError(ErrorTypeResourceNotAllowed, "Access to this resource is not allowed")
|
||||
err.Details["resource"] = resource
|
||||
return err
|
||||
}
|
||||
|
||||
// NewMethodNotAllowedError creates a method not allowed error
|
||||
func NewMethodNotAllowedError(method string, allowedMethods []string) *Error {
|
||||
err := NewError(ErrorTypeMethodNotAllowed, fmt.Sprintf("HTTP method '%s' is not allowed", method))
|
||||
err.Details["method"] = method
|
||||
err.Details["allowed_methods"] = allowedMethods
|
||||
return err
|
||||
}
|
||||
|
||||
// NewIPBlockedError creates an IP blocked error
|
||||
func NewIPBlockedError(ip string) *Error {
|
||||
err := NewError(ErrorTypeIPBlocked, "Access from this IP address is blocked")
|
||||
err.Details["ip"] = ip
|
||||
return err
|
||||
}
|
||||
|
||||
// NewQuotaExceededError creates a quota exceeded error
|
||||
func NewQuotaExceededError(message string, quotaType string, limit int64, current int64) *Error {
|
||||
err := NewError(ErrorTypeQuotaExceeded, message)
|
||||
err.Details["quota_type"] = quotaType
|
||||
err.Details["limit"] = limit
|
||||
err.Details["current"] = current
|
||||
return err
|
||||
}
|
||||
|
||||
// NewInternalError creates an internal error
|
||||
func NewInternalError(message string) *Error {
|
||||
return NewError(ErrorTypeInternal, message)
|
||||
}
|
||||
12
openapi/oauth/acl/interfaces.go
Normal file
12
openapi/oauth/acl/interfaces.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package acl
|
||||
|
||||
import "github.com/gin-gonic/gin"
|
||||
|
||||
// Enforcer interface is used to enforce access control rules
|
||||
type Enforcer interface {
|
||||
// Enforce checks if a user has access to a resource
|
||||
Enforce(c *gin.Context) (bool, error)
|
||||
|
||||
// Enabled returns true if the ACL is enabled, otherwise false
|
||||
Enabled() bool
|
||||
}
|
||||
16
openapi/oauth/acl/types.go
Normal file
16
openapi/oauth/acl/types.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package acl
|
||||
|
||||
// DefaultConfig is the default configuration for the ACL
|
||||
var DefaultConfig = Config{
|
||||
Enabled: false,
|
||||
}
|
||||
|
||||
// Config is the configuration for the ACL
|
||||
type Config struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// ACL is the ACL checker
|
||||
type ACL struct {
|
||||
Config *Config
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
package oauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/openapi/oauth/acl"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
)
|
||||
|
||||
|
|
@ -16,7 +18,7 @@ func (s *Service) Guard(c *gin.Context) {
|
|||
|
||||
// Validate the token
|
||||
if token == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
c.JSON(http.StatusUnauthorized, types.ErrTokenMissing)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
|
@ -24,7 +26,7 @@ func (s *Service) Guard(c *gin.Context) {
|
|||
// Validate the token
|
||||
claims, err := s.VerifyToken(token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
||||
c.JSON(http.StatusUnauthorized, types.ErrInvalidToken)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
|
@ -36,6 +38,25 @@ func (s *Service) Guard(c *gin.Context) {
|
|||
|
||||
// Set Authorized Info
|
||||
s.setAuthorizedInfo(c, claims)
|
||||
|
||||
// Check if ACL is enabled
|
||||
if acl.Global == nil || !acl.Global.Enabled() {
|
||||
return
|
||||
}
|
||||
|
||||
// Check permissions and enforce rate limits when ACL is configured
|
||||
ok, err := acl.Global.Enforce(c)
|
||||
if err != nil {
|
||||
s.handleACLError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
// If permissions are not granted, return forbidden
|
||||
if !ok {
|
||||
c.JSON(http.StatusForbidden, types.ErrForbidden)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetAuthorizedInfo Get Authorized Info from context
|
||||
|
|
@ -114,7 +135,7 @@ func (s *Service) setAuthorizedInfo(c *gin.Context, claims *types.TokenClaims) {
|
|||
func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
|
||||
refreshToken := s.getRefreshToken(c)
|
||||
if refreshToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||
c.JSON(http.StatusUnauthorized, types.ErrRefreshTokenMissing)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
|
@ -122,7 +143,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, gin.H{"error": "Invalid token"})
|
||||
c.JSON(http.StatusUnauthorized, types.ErrInvalidRefreshToken)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
|
@ -195,3 +216,72 @@ func (s *Service) getSessionID(c *gin.Context) string {
|
|||
|
||||
return ""
|
||||
}
|
||||
|
||||
// handleACLError handles ACL errors and returns appropriate HTTP responses
|
||||
func (s *Service) handleACLError(c *gin.Context, err error) {
|
||||
// Check if it's an ACL error with detailed information
|
||||
if aclErr, ok := err.(*acl.Error); ok {
|
||||
var statusCode int
|
||||
var errResponse *types.ErrorResponse
|
||||
|
||||
switch aclErr.Type {
|
||||
case acl.ErrorTypeRateLimitExceeded:
|
||||
statusCode = http.StatusTooManyRequests
|
||||
errResponse = types.ErrRateLimitExceeded
|
||||
// Set Retry-After header if available
|
||||
if aclErr.RetryAfter > 0 {
|
||||
c.Header("Retry-After", fmt.Sprintf("%d", aclErr.RetryAfter))
|
||||
}
|
||||
|
||||
case acl.ErrorTypeQuotaExceeded:
|
||||
statusCode = http.StatusTooManyRequests
|
||||
errResponse = &types.ErrorResponse{
|
||||
Code: "quota_exceeded",
|
||||
ErrorDescription: aclErr.Message,
|
||||
}
|
||||
|
||||
case acl.ErrorTypeInsufficientScope:
|
||||
statusCode = http.StatusForbidden
|
||||
errResponse = types.ErrInsufficientScope
|
||||
|
||||
case acl.ErrorTypePermissionDenied:
|
||||
statusCode = http.StatusForbidden
|
||||
errResponse = types.ErrForbidden
|
||||
|
||||
case acl.ErrorTypeResourceNotAllowed:
|
||||
statusCode = http.StatusForbidden
|
||||
errResponse = types.ErrAccessDenied
|
||||
|
||||
case acl.ErrorTypeMethodNotAllowed:
|
||||
statusCode = http.StatusMethodNotAllowed
|
||||
errResponse = types.ErrMethodNotAllowed
|
||||
|
||||
case acl.ErrorTypeIPBlocked, acl.ErrorTypeGeoRestricted, acl.ErrorTypeTimeRestricted:
|
||||
statusCode = http.StatusForbidden
|
||||
errResponse = types.ErrAccessDenied
|
||||
|
||||
case acl.ErrorTypeInvalidRequest:
|
||||
statusCode = http.StatusBadRequest
|
||||
errResponse = &types.ErrorResponse{
|
||||
Code: "invalid_request",
|
||||
ErrorDescription: aclErr.Message,
|
||||
}
|
||||
|
||||
case acl.ErrorTypeInternal:
|
||||
statusCode = http.StatusInternalServerError
|
||||
errResponse = types.ErrACLInternalError
|
||||
|
||||
default:
|
||||
statusCode = http.StatusInternalServerError
|
||||
errResponse = types.ErrACLInternalError
|
||||
}
|
||||
|
||||
c.JSON(statusCode, errResponse)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// If it's not an ACL error, treat it as an internal error
|
||||
c.JSON(http.StatusInternalServerError, types.ErrACLInternalError)
|
||||
c.Abort()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package types
|
||||
|
||||
// Error definitions
|
||||
// Configuration Error definitions
|
||||
var (
|
||||
ErrInvalidConfiguration = &ErrorResponse{Code: "invalid_configuration", ErrorDescription: "Invalid OAuth service configuration"}
|
||||
ErrStoreMissing = &ErrorResponse{Code: "store_missing", ErrorDescription: "Store is required for OAuth service"}
|
||||
|
|
@ -9,3 +9,35 @@ var (
|
|||
ErrInvalidTokenLifetime = &ErrorResponse{Code: "invalid_token_lifetime", ErrorDescription: "Token lifetime must be greater than 0"}
|
||||
ErrPKCEConfigurationInvalid = &ErrorResponse{Code: "pkce_configuration_invalid", ErrorDescription: "PKCE configuration is invalid"}
|
||||
)
|
||||
|
||||
// Authentication & Authorization Error definitions
|
||||
var (
|
||||
// Token related errors
|
||||
ErrUnauthorized = &ErrorResponse{Code: "unauthorized", ErrorDescription: "Authentication is required to access this resource"}
|
||||
ErrInvalidToken = &ErrorResponse{Code: "invalid_token", ErrorDescription: "The access token provided is invalid, expired or malformed"}
|
||||
ErrTokenExpired = &ErrorResponse{Code: "token_expired", ErrorDescription: "The access token has expired"}
|
||||
ErrTokenMissing = &ErrorResponse{Code: "token_missing", ErrorDescription: "No access token provided in the request"}
|
||||
ErrInvalidRefreshToken = &ErrorResponse{Code: "invalid_refresh_token", ErrorDescription: "The refresh token provided is invalid or expired"}
|
||||
ErrRefreshTokenMissing = &ErrorResponse{Code: "refresh_token_missing", ErrorDescription: "No refresh token provided in the request"}
|
||||
|
||||
// Permission related errors
|
||||
ErrForbidden = &ErrorResponse{Code: "forbidden", ErrorDescription: "You do not have permission to access this resource"}
|
||||
ErrInsufficientScope = &ErrorResponse{Code: "insufficient_scope", ErrorDescription: "The access token does not have the required scope"}
|
||||
ErrAccessDenied = &ErrorResponse{Code: "access_denied", ErrorDescription: "Access to this resource has been denied"}
|
||||
|
||||
// ACL related errors
|
||||
ErrACLCheckFailed = &ErrorResponse{Code: "acl_check_failed", ErrorDescription: "ACL verification failed"}
|
||||
ErrACLInternalError = &ErrorResponse{Code: "acl_internal_error", ErrorDescription: "Internal error occurred during ACL verification"}
|
||||
|
||||
// Rate limiting errors
|
||||
ErrRateLimitExceeded = &ErrorResponse{Code: "rate_limit_exceeded", ErrorDescription: "Too many requests. Please try again later"}
|
||||
ErrTooManyRequests = &ErrorResponse{Code: "too_many_requests", ErrorDescription: "Request rate limit exceeded"}
|
||||
|
||||
// Resource related errors
|
||||
ErrResourceNotFound = &ErrorResponse{Code: "resource_not_found", ErrorDescription: "The requested resource was not found"}
|
||||
ErrMethodNotAllowed = &ErrorResponse{Code: "method_not_allowed", ErrorDescription: "The HTTP method is not allowed for this resource"}
|
||||
|
||||
// Server errors
|
||||
ErrInternalServerError = &ErrorResponse{Code: "internal_server_error", ErrorDescription: "An internal server error occurred"}
|
||||
ErrServiceUnavailable = &ErrorResponse{Code: "service_unavailable", ErrorDescription: "The service is temporarily unavailable"}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/yaoapp/yao/openapi/kb"
|
||||
"github.com/yaoapp/yao/openapi/messenger"
|
||||
"github.com/yaoapp/yao/openapi/oauth"
|
||||
"github.com/yaoapp/yao/openapi/oauth/acl"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/team"
|
||||
"github.com/yaoapp/yao/openapi/user"
|
||||
|
|
@ -63,6 +64,12 @@ func Load(appConfig config.Config) (*OpenAPI, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// Load the ACL enforcer
|
||||
_, err = acl.Load(&acl.Config{Enabled: true})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the OpenAPI server
|
||||
Server = &OpenAPI{Config: &config, OAuth: oauthService}
|
||||
return Server, nil
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue