Merge pull request #1213 from trheyi/main
Add ACL support to OAuth guard and error handling
This commit is contained in:
commit
a33b13d6b7
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
|
package oauth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/yaoapp/yao/openapi/oauth/acl"
|
||||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -16,7 +18,7 @@ func (s *Service) Guard(c *gin.Context) {
|
||||||
|
|
||||||
// Validate the token
|
// Validate the token
|
||||||
if token == "" {
|
if token == "" {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
c.JSON(http.StatusUnauthorized, types.ErrTokenMissing)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +26,7 @@ func (s *Service) Guard(c *gin.Context) {
|
||||||
// Validate the token
|
// Validate the token
|
||||||
claims, err := s.VerifyToken(token)
|
claims, err := s.VerifyToken(token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
c.JSON(http.StatusUnauthorized, types.ErrInvalidToken)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +38,25 @@ func (s *Service) Guard(c *gin.Context) {
|
||||||
|
|
||||||
// Set Authorized Info
|
// Set Authorized Info
|
||||||
s.setAuthorizedInfo(c, claims)
|
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
|
// 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) {
|
func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
|
||||||
refreshToken := s.getRefreshToken(c)
|
refreshToken := s.getRefreshToken(c)
|
||||||
if refreshToken == "" {
|
if refreshToken == "" {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
c.JSON(http.StatusUnauthorized, types.ErrRefreshTokenMissing)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +143,7 @@ func (s *Service) tryAutoRefreshToken(c *gin.Context, _ *types.TokenClaims) {
|
||||||
// Verify the refresh token
|
// Verify the refresh token
|
||||||
_, err := s.VerifyToken(refreshToken)
|
_, err := s.VerifyToken(refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
c.JSON(http.StatusUnauthorized, types.ErrInvalidRefreshToken)
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -195,3 +216,72 @@ func (s *Service) getSessionID(c *gin.Context) string {
|
||||||
|
|
||||||
return ""
|
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
|
package types
|
||||||
|
|
||||||
// Error definitions
|
// Configuration Error definitions
|
||||||
var (
|
var (
|
||||||
ErrInvalidConfiguration = &ErrorResponse{Code: "invalid_configuration", ErrorDescription: "Invalid OAuth service configuration"}
|
ErrInvalidConfiguration = &ErrorResponse{Code: "invalid_configuration", ErrorDescription: "Invalid OAuth service configuration"}
|
||||||
ErrStoreMissing = &ErrorResponse{Code: "store_missing", ErrorDescription: "Store is required for OAuth service"}
|
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"}
|
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"}
|
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/kb"
|
||||||
"github.com/yaoapp/yao/openapi/messenger"
|
"github.com/yaoapp/yao/openapi/messenger"
|
||||||
"github.com/yaoapp/yao/openapi/oauth"
|
"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/oauth/types"
|
||||||
"github.com/yaoapp/yao/openapi/team"
|
"github.com/yaoapp/yao/openapi/team"
|
||||||
"github.com/yaoapp/yao/openapi/user"
|
"github.com/yaoapp/yao/openapi/user"
|
||||||
|
|
@ -63,6 +64,12 @@ func Load(appConfig config.Config) (*OpenAPI, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the ACL enforcer
|
||||||
|
_, err = acl.Load(&acl.Config{Enabled: true})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Create the OpenAPI server
|
// Create the OpenAPI server
|
||||||
Server = &OpenAPI{Config: &config, OAuth: oauthService}
|
Server = &OpenAPI{Config: &config, OAuth: oauthService}
|
||||||
return Server, nil
|
return Server, nil
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue