org
This commit is contained in:
267
kms/internal/errors/secure_responses.go
Normal file
267
kms/internal/errors/secure_responses.go
Normal file
@ -0,0 +1,267 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// SecureErrorResponse represents a sanitized error response for clients
|
||||
type SecureErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"request_id,omitempty"`
|
||||
Code int `json:"code"`
|
||||
}
|
||||
|
||||
// ErrorHandler provides secure error handling for HTTP responses
|
||||
type ErrorHandler struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewErrorHandler creates a new secure error handler
|
||||
func NewErrorHandler(logger *zap.Logger) *ErrorHandler {
|
||||
return &ErrorHandler{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleError handles errors securely by logging detailed information and returning sanitized responses
|
||||
func (eh *ErrorHandler) HandleError(c *gin.Context, err error, userMessage string) {
|
||||
requestID := eh.getOrGenerateRequestID(c)
|
||||
|
||||
// Log detailed error information for internal debugging
|
||||
eh.logger.Error("HTTP request error",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("user_agent", c.Request.UserAgent()),
|
||||
zap.String("remote_addr", c.ClientIP()),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
// Determine appropriate HTTP status code and error type
|
||||
statusCode, errorType := eh.determineErrorResponse(err)
|
||||
|
||||
// Create sanitized response
|
||||
response := SecureErrorResponse{
|
||||
Error: errorType,
|
||||
Message: eh.sanitizeErrorMessage(userMessage, err),
|
||||
RequestID: requestID,
|
||||
Code: statusCode,
|
||||
}
|
||||
|
||||
c.JSON(statusCode, response)
|
||||
}
|
||||
|
||||
// HandleValidationError handles input validation errors
|
||||
func (eh *ErrorHandler) HandleValidationError(c *gin.Context, field string, message string) {
|
||||
requestID := eh.getOrGenerateRequestID(c)
|
||||
|
||||
eh.logger.Warn("Validation error",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("field", field),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
)
|
||||
|
||||
response := SecureErrorResponse{
|
||||
Error: "validation_error",
|
||||
Message: "Invalid input provided",
|
||||
RequestID: requestID,
|
||||
Code: http.StatusBadRequest,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusBadRequest, response)
|
||||
}
|
||||
|
||||
// HandleAuthenticationError handles authentication failures
|
||||
func (eh *ErrorHandler) HandleAuthenticationError(c *gin.Context, err error) {
|
||||
requestID := eh.getOrGenerateRequestID(c)
|
||||
|
||||
eh.logger.Warn("Authentication error",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("remote_addr", c.ClientIP()),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
response := SecureErrorResponse{
|
||||
Error: "authentication_failed",
|
||||
Message: "Authentication required",
|
||||
RequestID: requestID,
|
||||
Code: http.StatusUnauthorized,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusUnauthorized, response)
|
||||
}
|
||||
|
||||
// HandleAuthorizationError handles authorization failures
|
||||
func (eh *ErrorHandler) HandleAuthorizationError(c *gin.Context, resource string) {
|
||||
requestID := eh.getOrGenerateRequestID(c)
|
||||
|
||||
eh.logger.Warn("Authorization error",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("resource", resource),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("remote_addr", c.ClientIP()),
|
||||
)
|
||||
|
||||
response := SecureErrorResponse{
|
||||
Error: "access_denied",
|
||||
Message: "Insufficient permissions",
|
||||
RequestID: requestID,
|
||||
Code: http.StatusForbidden,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusForbidden, response)
|
||||
}
|
||||
|
||||
// HandleInternalError handles internal server errors
|
||||
func (eh *ErrorHandler) HandleInternalError(c *gin.Context, err error) {
|
||||
requestID := eh.getOrGenerateRequestID(c)
|
||||
|
||||
eh.logger.Error("Internal server error",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("remote_addr", c.ClientIP()),
|
||||
zap.Error(err),
|
||||
)
|
||||
|
||||
response := SecureErrorResponse{
|
||||
Error: "internal_error",
|
||||
Message: "An internal error occurred",
|
||||
RequestID: requestID,
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusInternalServerError, response)
|
||||
}
|
||||
|
||||
// HandleNotFoundError handles resource not found errors
|
||||
func (eh *ErrorHandler) HandleNotFoundError(c *gin.Context, resource string, message string) {
|
||||
requestID := eh.getOrGenerateRequestID(c)
|
||||
|
||||
eh.logger.Warn("Resource not found",
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("resource", resource),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("remote_addr", c.ClientIP()),
|
||||
)
|
||||
|
||||
response := SecureErrorResponse{
|
||||
Error: "resource_not_found",
|
||||
Message: message,
|
||||
RequestID: requestID,
|
||||
Code: http.StatusNotFound,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusNotFound, response)
|
||||
}
|
||||
|
||||
// determineErrorResponse determines the appropriate HTTP status and error type
|
||||
func (eh *ErrorHandler) determineErrorResponse(err error) (int, string) {
|
||||
if appErr, ok := err.(*AppError); ok {
|
||||
return appErr.StatusCode, eh.getErrorTypeFromCode(appErr.Code)
|
||||
}
|
||||
|
||||
// For unknown errors, log as internal error but don't expose details
|
||||
return http.StatusInternalServerError, "internal_error"
|
||||
}
|
||||
|
||||
// sanitizeErrorMessage removes sensitive information from error messages
|
||||
func (eh *ErrorHandler) sanitizeErrorMessage(userMessage string, err error) string {
|
||||
if userMessage != "" {
|
||||
return userMessage
|
||||
}
|
||||
|
||||
// Provide generic messages for different error types
|
||||
if appErr, ok := err.(*AppError); ok {
|
||||
return eh.getGenericMessageFromCode(appErr.Code)
|
||||
}
|
||||
|
||||
return "An error occurred"
|
||||
}
|
||||
|
||||
// getErrorTypeFromCode converts an error code to a sanitized error type string
|
||||
func (eh *ErrorHandler) getErrorTypeFromCode(code ErrorCode) string {
|
||||
switch code {
|
||||
case ErrValidationFailed, ErrInvalidInput, ErrMissingField, ErrInvalidFormat:
|
||||
return "validation_error"
|
||||
case ErrUnauthorized, ErrInvalidToken, ErrTokenExpired, ErrInvalidCredentials:
|
||||
return "authentication_failed"
|
||||
case ErrForbidden, ErrInsufficientPermissions:
|
||||
return "access_denied"
|
||||
case ErrNotFound, ErrApplicationNotFound, ErrTokenNotFound, ErrPermissionNotFound:
|
||||
return "resource_not_found"
|
||||
case ErrAlreadyExists, ErrConflict:
|
||||
return "resource_conflict"
|
||||
case ErrRateLimit:
|
||||
return "rate_limit_exceeded"
|
||||
case ErrTimeout:
|
||||
return "timeout"
|
||||
default:
|
||||
return "internal_error"
|
||||
}
|
||||
}
|
||||
|
||||
// getGenericMessageFromCode provides generic user-safe messages for error codes
|
||||
func (eh *ErrorHandler) getGenericMessageFromCode(code ErrorCode) string {
|
||||
switch code {
|
||||
case ErrValidationFailed, ErrInvalidInput, ErrMissingField, ErrInvalidFormat:
|
||||
return "Invalid input provided"
|
||||
case ErrUnauthorized, ErrInvalidToken, ErrTokenExpired, ErrInvalidCredentials:
|
||||
return "Authentication required"
|
||||
case ErrForbidden, ErrInsufficientPermissions:
|
||||
return "Access denied"
|
||||
case ErrNotFound, ErrApplicationNotFound, ErrTokenNotFound, ErrPermissionNotFound:
|
||||
return "Resource not found"
|
||||
case ErrAlreadyExists, ErrConflict:
|
||||
return "Resource conflict"
|
||||
case ErrRateLimit:
|
||||
return "Rate limit exceeded"
|
||||
case ErrTimeout:
|
||||
return "Request timeout"
|
||||
default:
|
||||
return "An error occurred"
|
||||
}
|
||||
}
|
||||
|
||||
// getOrGenerateRequestID gets or generates a request ID for tracking
|
||||
func (eh *ErrorHandler) getOrGenerateRequestID(c *gin.Context) string {
|
||||
// Try to get existing request ID from context
|
||||
if requestID, exists := c.Get("request_id"); exists {
|
||||
if id, ok := requestID.(string); ok {
|
||||
return id
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get from header
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID != "" {
|
||||
return requestID
|
||||
}
|
||||
|
||||
// Generate a simple request ID (in production, use a proper UUID library)
|
||||
return generateSimpleID()
|
||||
}
|
||||
|
||||
// generateSimpleID generates a simple request ID
|
||||
func generateSimpleID() string {
|
||||
// Simple implementation - in production use proper UUID generation
|
||||
bytes := make([]byte, 8)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
// Fallback to timestamp-based ID
|
||||
return fmt.Sprintf("req_%d", time.Now().UnixNano())
|
||||
}
|
||||
return "req_" + hex.EncodeToString(bytes)
|
||||
}
|
||||
Reference in New Issue
Block a user