package serrors import ( "context" "encoding/json" "errors" "fmt" "net/http" "strconv" "github.com/zitadel/logging" "golang.org/x/text/language" http_util "github.com/zitadel/zitadel/internal/api/http" zhttp_middleware "github.com/zitadel/zitadel/internal/api/http/middleware" "github.com/zitadel/zitadel/internal/api/scim/schemas" "github.com/zitadel/zitadel/internal/i18n" "github.com/zitadel/zitadel/internal/zerrors" ) type scimErrorType string type wrappedScimError struct { Parent error ScimType scimErrorType Status int } type ScimError struct { Schemas []schemas.ScimSchemaType `json:"schemas"` ScimType scimErrorType `json:"scimType,omitempty"` Detail string `json:"detail,omitempty"` StatusCode int `json:"-"` Status string `json:"status"` ZitadelDetail *ErrorDetail `json:"urn:ietf:params:scim:api:zitadel:messages:2.0:ErrorDetail,omitempty"` } type ErrorDetail struct { ID string `json:"id"` Message string `json:"message"` } const ( // ScimTypeInvalidValue A required value was missing, // or the value specified was not compatible with the operation, // or attribute type (see Section 2.2 of RFC7643), // or resource schema (see Section 4 of RFC7643). ScimTypeInvalidValue scimErrorType = "invalidValue" // ScimTypeInvalidSyntax The request body message structure was invalid or did // not conform to the request schema. ScimTypeInvalidSyntax scimErrorType = "invalidSyntax" // ScimTypeInvalidFilter The specified filter syntax as invalid, or the // specified attribute and filter comparison combination is not supported. ScimTypeInvalidFilter scimErrorType = "invalidFilter" // ScimTypeInvalidPath The "path" attribute was invalid or malformed. ScimTypeInvalidPath scimErrorType = "invalidPath" // ScimTypeNoTarget The specified "path" did not // yield an attribute or attribute value that could be operated on. // This occurs when the specified "path" value contains a filter that yields no match. ScimTypeNoTarget scimErrorType = "noTarget" // ScimTypeUniqueness One or more of the attribute values are already in use or are reserved. ScimTypeUniqueness scimErrorType = "uniqueness" ) var translator *i18n.Translator func ErrorHandler(next zhttp_middleware.HandlerFuncWithError) http.Handler { var err error translator, err = i18n.NewZitadelTranslator(language.English) logging.OnError(err).Panic("unable to get translator") return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err = next(w, r); err == nil { return } scimErr := MapToScimError(r.Context(), err) w.WriteHeader(scimErr.StatusCode) jsonErr := json.NewEncoder(w).Encode(scimErr) logging.OnError(jsonErr).Warn("Failed to marshal scim error response") }) } func ThrowInvalidValue(parent error) error { return &wrappedScimError{ Parent: parent, ScimType: ScimTypeInvalidValue, } } func ThrowInvalidSyntax(parent error) error { return &wrappedScimError{ Parent: parent, ScimType: ScimTypeInvalidSyntax, } } func ThrowInvalidFilter(parent error) error { return &wrappedScimError{ Parent: parent, ScimType: ScimTypeInvalidFilter, } } func ThrowInvalidPath(parent error) error { return &wrappedScimError{ Parent: parent, ScimType: ScimTypeInvalidPath, } } func ThrowNoTarget(parent error) error { return &wrappedScimError{ Parent: parent, ScimType: ScimTypeNoTarget, } } func ThrowPayloadTooLarge(parent error) error { return &wrappedScimError{ Parent: parent, Status: http.StatusRequestEntityTooLarge, } } func IsScimOrZitadelError(err error) bool { return IsScimError(err) || zerrors.IsZitadelError(err) } func IsScimError(err error) bool { var scimErr *wrappedScimError return errors.As(err, &scimErr) } func (err *ScimError) Error() string { return fmt.Sprintf("SCIM Error: %s: %s", err.ScimType, err.Detail) } func (err *wrappedScimError) Error() string { return fmt.Sprintf("SCIM Error: %s: %s", err.ScimType, err.Parent.Error()) } func MapToScimError(ctx context.Context, err error) *ScimError { scimError := new(ScimError) if ok := errors.As(err, &scimError); ok { return scimError } scimWrappedError := new(wrappedScimError) if ok := errors.As(err, &scimWrappedError); ok { mappedErr := MapToScimError(ctx, scimWrappedError.Parent) if scimWrappedError.ScimType != "" { mappedErr.ScimType = scimWrappedError.ScimType } if scimWrappedError.Status != 0 { mappedErr.Status = strconv.Itoa(scimWrappedError.Status) mappedErr.StatusCode = scimWrappedError.Status } return mappedErr } zitadelErr := new(zerrors.ZitadelError) if ok := errors.As(err, &zitadelErr); !ok { return &ScimError{ Schemas: []schemas.ScimSchemaType{schemas.IdError}, Detail: "Unknown internal server error", Status: strconv.Itoa(http.StatusInternalServerError), StatusCode: http.StatusInternalServerError, } } statusCode, ok := http_util.ZitadelErrorToHTTPStatusCode(err) if !ok { statusCode = http.StatusInternalServerError } localizedMsg := translator.LocalizeFromCtx(ctx, zitadelErr.GetMessage(), nil) return &ScimError{ Schemas: []schemas.ScimSchemaType{schemas.IdError, schemas.IdZitadelErrorDetail}, ScimType: mapErrorToScimErrorType(err), Detail: localizedMsg, StatusCode: statusCode, Status: strconv.Itoa(statusCode), ZitadelDetail: &ErrorDetail{ ID: zitadelErr.GetID(), Message: zitadelErr.GetMessage(), }, } } func mapErrorToScimErrorType(err error) scimErrorType { switch { case zerrors.IsErrorInvalidArgument(err): return ScimTypeInvalidValue case zerrors.IsErrorAlreadyExists(err): return ScimTypeUniqueness default: return "" } }