963ccc6553
Feature/demo support * index logs now debug * demo data loader
313 lines
9.6 KiB
Go
313 lines
9.6 KiB
Go
// errors.go provides error classification for AWS Cognito errors.
|
|
// It maps AWS error types to appropriate HTTP status codes and sanitized error messages
|
|
// suitable for returning in API responses without leaking sensitive information.
|
|
package usermanagement
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types"
|
|
"github.com/aws/smithy-go"
|
|
)
|
|
|
|
// CognitoErrorType represents a machine-readable error classification.
|
|
type CognitoErrorType string
|
|
|
|
const (
|
|
// ErrTypeUserNotFound indicates the user does not exist in Cognito.
|
|
ErrTypeUserNotFound CognitoErrorType = "UserNotFound"
|
|
|
|
// ErrTypeAccessDenied indicates the caller does not have permission to perform the operation.
|
|
ErrTypeAccessDenied CognitoErrorType = "AccessDenied"
|
|
|
|
// ErrTypeNotAuthorized indicates the caller is not authorized (e.g., invalid credentials).
|
|
ErrTypeNotAuthorized CognitoErrorType = "NotAuthorized"
|
|
|
|
// ErrTypeInvalidParameter indicates an invalid parameter was provided.
|
|
ErrTypeInvalidParameter CognitoErrorType = "InvalidParameter"
|
|
|
|
// ErrTypeRateLimited indicates too many requests were made.
|
|
ErrTypeRateLimited CognitoErrorType = "RateLimited"
|
|
|
|
// ErrTypeServiceUnavailable indicates the Cognito service is temporarily unavailable.
|
|
ErrTypeServiceUnavailable CognitoErrorType = "ServiceUnavailable"
|
|
|
|
// ErrTypeServiceError indicates a general error communicating with Cognito.
|
|
ErrTypeServiceError CognitoErrorType = "ServiceError"
|
|
|
|
// ErrTypeUserExists indicates the user already exists in Cognito.
|
|
ErrTypeUserExists CognitoErrorType = "UserExists"
|
|
)
|
|
|
|
// CognitoErrorInfo contains classified error information suitable for HTTP responses.
|
|
// The Message and Details fields are sanitized to avoid leaking sensitive information
|
|
// like ARNs, internal paths, or request IDs.
|
|
type CognitoErrorInfo struct {
|
|
// HTTPStatus is the appropriate HTTP status code for this error type.
|
|
HTTPStatus int
|
|
|
|
// ErrorType is a machine-readable error classification.
|
|
ErrorType CognitoErrorType
|
|
|
|
// Message is a user-friendly error message safe for HTTP responses.
|
|
Message string
|
|
|
|
// Details provides additional error context (sanitized).
|
|
Details string
|
|
}
|
|
|
|
// ClassifyCognitoError analyzes a Cognito error and returns appropriate HTTP response info.
|
|
// The returned CognitoErrorInfo contains sanitized messages suitable for API responses.
|
|
//
|
|
// Parameters:
|
|
// - err: The error returned from a Cognito operation
|
|
// - email: The user's email address for context in error messages
|
|
//
|
|
// Returns:
|
|
// - CognitoErrorInfo: Classified error information with appropriate HTTP status and messages
|
|
func ClassifyCognitoError(err error, email string) CognitoErrorInfo {
|
|
if err == nil {
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusOK,
|
|
ErrorType: "",
|
|
Message: "",
|
|
Details: "",
|
|
}
|
|
}
|
|
|
|
// Check for specific Cognito SDK error types
|
|
var userNotFoundErr *types.UserNotFoundException
|
|
if errors.As(err, &userNotFoundErr) {
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusNotFound,
|
|
ErrorType: ErrTypeUserNotFound,
|
|
Message: fmt.Sprintf("User not found: %s", email),
|
|
Details: "The specified user does not exist in Cognito",
|
|
}
|
|
}
|
|
|
|
var notAuthorizedErr *types.NotAuthorizedException
|
|
if errors.As(err, ¬AuthorizedErr) {
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusUnauthorized,
|
|
ErrorType: ErrTypeNotAuthorized,
|
|
Message: "Not authorized to perform this operation",
|
|
Details: sanitizeErrorMessage(err.Error()),
|
|
}
|
|
}
|
|
|
|
var invalidParamErr *types.InvalidParameterException
|
|
if errors.As(err, &invalidParamErr) {
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusBadRequest,
|
|
ErrorType: ErrTypeInvalidParameter,
|
|
Message: "Invalid parameter in request",
|
|
Details: sanitizeErrorMessage(err.Error()),
|
|
}
|
|
}
|
|
|
|
var tooManyRequestsErr *types.TooManyRequestsException
|
|
if errors.As(err, &tooManyRequestsErr) {
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusTooManyRequests,
|
|
ErrorType: ErrTypeRateLimited,
|
|
Message: "Too many requests to Cognito",
|
|
Details: "Rate limit exceeded. Please retry after a short delay.",
|
|
}
|
|
}
|
|
|
|
var internalErr *types.InternalErrorException
|
|
if errors.As(err, &internalErr) {
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusServiceUnavailable,
|
|
ErrorType: ErrTypeServiceUnavailable,
|
|
Message: "Cognito service temporarily unavailable",
|
|
Details: "Internal service error. Please retry later.",
|
|
}
|
|
}
|
|
|
|
var usernameExistsErr *types.UsernameExistsException
|
|
if errors.As(err, &usernameExistsErr) {
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusConflict,
|
|
ErrorType: ErrTypeUserExists,
|
|
Message: fmt.Sprintf("User already exists: %s", email),
|
|
Details: "A user with this email already exists in Cognito",
|
|
}
|
|
}
|
|
|
|
// Check for generic AWS API errors (including AccessDeniedException)
|
|
var apiErr smithy.APIError
|
|
if errors.As(err, &apiErr) {
|
|
return classifyAPIError(apiErr, email)
|
|
}
|
|
|
|
// Default: treat as a generic service communication error
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusBadGateway,
|
|
ErrorType: ErrTypeServiceError,
|
|
Message: "Failed to communicate with Cognito",
|
|
Details: sanitizeErrorMessage(err.Error()),
|
|
}
|
|
}
|
|
|
|
// classifyAPIError handles generic AWS API errors that may not have specific SDK types.
|
|
//
|
|
// Parameters:
|
|
// - apiErr: The AWS API error
|
|
// - email: The user's email address for context in error messages
|
|
//
|
|
// Returns:
|
|
// - CognitoErrorInfo: Classified error information
|
|
func classifyAPIError(apiErr smithy.APIError, email string) CognitoErrorInfo {
|
|
code := apiErr.ErrorCode()
|
|
|
|
switch code {
|
|
case "AccessDeniedException":
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusForbidden,
|
|
ErrorType: ErrTypeAccessDenied,
|
|
Message: "Access denied to Cognito user pool",
|
|
Details: "The service does not have permission to perform this Cognito operation",
|
|
}
|
|
|
|
case "UserNotFoundException":
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusNotFound,
|
|
ErrorType: ErrTypeUserNotFound,
|
|
Message: fmt.Sprintf("User not found: %s", email),
|
|
Details: "The specified user does not exist in Cognito",
|
|
}
|
|
|
|
case "NotAuthorizedException":
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusUnauthorized,
|
|
ErrorType: ErrTypeNotAuthorized,
|
|
Message: "Not authorized to perform this operation",
|
|
Details: sanitizeErrorMessage(apiErr.ErrorMessage()),
|
|
}
|
|
|
|
case "InvalidParameterException":
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusBadRequest,
|
|
ErrorType: ErrTypeInvalidParameter,
|
|
Message: "Invalid parameter in request",
|
|
Details: sanitizeErrorMessage(apiErr.ErrorMessage()),
|
|
}
|
|
|
|
case "TooManyRequestsException":
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusTooManyRequests,
|
|
ErrorType: ErrTypeRateLimited,
|
|
Message: "Too many requests to Cognito",
|
|
Details: "Rate limit exceeded. Please retry after a short delay.",
|
|
}
|
|
|
|
case "ServiceUnavailable", "InternalError", "InternalErrorException":
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusServiceUnavailable,
|
|
ErrorType: ErrTypeServiceUnavailable,
|
|
Message: "Cognito service temporarily unavailable",
|
|
Details: "Internal service error. Please retry later.",
|
|
}
|
|
|
|
case "UsernameExistsException":
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusConflict,
|
|
ErrorType: ErrTypeUserExists,
|
|
Message: fmt.Sprintf("User already exists: %s", email),
|
|
Details: "A user with this email already exists in Cognito",
|
|
}
|
|
|
|
default:
|
|
return CognitoErrorInfo{
|
|
HTTPStatus: http.StatusBadGateway,
|
|
ErrorType: ErrTypeServiceError,
|
|
Message: "Failed to communicate with Cognito",
|
|
Details: sanitizeErrorMessage(apiErr.ErrorMessage()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// sanitizeErrorMessage removes sensitive information from error messages.
|
|
// It strips out ARNs, request IDs, and internal resource names.
|
|
//
|
|
// Parameters:
|
|
// - msg: The raw error message
|
|
//
|
|
// Returns:
|
|
// - string: The sanitized error message
|
|
func sanitizeErrorMessage(msg string) string {
|
|
msg = removeARNs(msg)
|
|
msg = removeRequestIDs(msg)
|
|
msg = normalizeWhitespace(msg)
|
|
return msg
|
|
}
|
|
|
|
// removeARNs replaces all AWS ARNs with [REDACTED].
|
|
func removeARNs(msg string) string {
|
|
for {
|
|
start := strings.Index(msg, "arn:aws:")
|
|
if start == -1 {
|
|
return msg
|
|
}
|
|
end := findEndOfToken(msg, start)
|
|
msg = msg[:start] + "[REDACTED]" + msg[end:]
|
|
}
|
|
}
|
|
|
|
// removeRequestIDs removes request ID patterns and their values from the message.
|
|
func removeRequestIDs(msg string) string {
|
|
patterns := []string{"requestid:", "request-id:", "request id:"}
|
|
msgLower := strings.ToLower(msg)
|
|
|
|
for _, pattern := range patterns {
|
|
for {
|
|
idx := strings.Index(msgLower, pattern)
|
|
if idx == -1 {
|
|
break
|
|
}
|
|
end := idx + len(pattern)
|
|
end = skipWhitespace(msg, end)
|
|
end = findEndOfToken(msg, end)
|
|
msg = msg[:idx] + msg[end:]
|
|
msgLower = strings.ToLower(msg)
|
|
}
|
|
}
|
|
return msg
|
|
}
|
|
|
|
// findEndOfToken finds the end of a token (space, comma, or newline delimited).
|
|
func findEndOfToken(msg string, start int) int {
|
|
end := start
|
|
for end < len(msg) && !isTokenDelimiter(msg[end]) {
|
|
end++
|
|
}
|
|
return end
|
|
}
|
|
|
|
// isTokenDelimiter returns true if the character is a token delimiter.
|
|
func isTokenDelimiter(c byte) bool {
|
|
return c == ' ' || c == ',' || c == '\n' || c == '\r'
|
|
}
|
|
|
|
// skipWhitespace advances past whitespace characters.
|
|
func skipWhitespace(msg string, pos int) int {
|
|
for pos < len(msg) && (msg[pos] == ' ' || msg[pos] == '\t') {
|
|
pos++
|
|
}
|
|
return pos
|
|
}
|
|
|
|
// normalizeWhitespace trims and collapses multiple spaces.
|
|
func normalizeWhitespace(msg string) string {
|
|
msg = strings.TrimSpace(msg)
|
|
for strings.Contains(msg, " ") {
|
|
msg = strings.ReplaceAll(msg, " ", " ")
|
|
}
|
|
return msg
|
|
}
|