Files
zitadel/backend/v3/domain/errors.go
T
a2bf528ca0 feat(domain): introduce error slugs (#12030)
# Which Problems Are Solved

As part of https://github.com/zitadel/zitadel/issues/11917 we want to
introduce error slugs so (API) clients can rely on stable,
machine-readable errors and act accordingly.

# How the Problems Are Solved

- Added a `NewSlug` helper function in the domain package.
- Added `ErrorDetails` to the `ZitadelError`
- Added an `zitadel.error.v2.ErrorDetail` proto message
- Updated the connectRPC error interceptor to map new slug based errors
to the new `ErrorDetail`
- Defined some common slugs and error functions like internal errors
- Defined (session) specific slugs used in the `DeleteSession` and
`CheckUser` functions and replaced old implementations
- Updated integration tests to check specific errors if the relation
database feature is enabled
- Updated doc and guideline to reflect the latest changes and decisions
- Updated DeleteSession endpoint API to list possible slugs

# Additional Changes

None

# Additional Context

- closes #11957

---------

Co-authored-by: Wim Van Laer <[email protected]>
2026-04-13 09:47:04 +02:00

219 lines
7.6 KiB
Go

package domain
import (
"errors"
"fmt"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/internal/zerrors"
)
func NewSlug(resource, cause string) zerrors.Slug {
return zerrors.Slug(fmt.Sprintf("%s.%s", resource, cause))
}
var (
// SlugInternalError is a general slug for any internal error, where the client doesn't have any influence on the error.
// It's returned when an unexpected condition in the server occurs, such as a database error, an external service failure, or any other error that is not caused by the client's request.
// The error details might contain additional information.
SlugInternalError = NewSlug("zitadel", "internal_error")
// SlugRequestInvalid is a general slug for any request containing missing, malformed or otherwise invalid parameters.
// The details typically contain additional information on how to resolve the problem.
SlugRequestInvalid = NewSlug("request", "invalid")
// SlugAuthMissingPermission is a general slug for any authorization error where the user is missing a required permission to perform an action.
// The details typically contain the necessary required permission.
SlugAuthMissingPermission = NewSlug("auth", "missing_permission")
)
var (
// ErrIDMissing is an error that can be returned on any resource if the necessary (resource) ID is missing
ErrIDMissing = func() error {
return zerrors.CreateZitadelError(zerrors.KindInvalidArgument, nil, string(SlugRequestInvalid), "validation failed: id is required", 1).
WithDetails(zerrors.ErrorDetailsMap{"id": "required"})
}
// ErrInstanceIDMissing is an error that can be returned on any resource if the necessary instance ID is missing
ErrInstanceIDMissing = func() error {
return zerrors.CreateZitadelError(zerrors.KindInvalidArgument, nil, string(SlugRequestInvalid), "validation failed: instance_id is required", 1).
WithDetails(zerrors.ErrorDetailsMap{"instance_id": "required"})
}
// ErrInvalidRequest is a general error for any invalid request type, like missing or wrong parameters.
ErrInvalidRequest = func(message string) error {
return zerrors.CreateZitadelError(zerrors.KindInvalidArgument, nil, string(SlugRequestInvalid), message, 1)
}
// ErrInternal is a general error for any internal error, where the client doesn't have any influence on the error.
// This error should be used for any error that is not caused by the client's request, but rather by an unexpected condition in the server.
ErrInternal = func(err error, message string) error {
return zerrors.CreateZitadelError(zerrors.KindInternal, err, string(SlugInternalError), message, 1)
}
// ErrMoreThanOneRowAffected is an error that can be returned when a database operation affects more rows than expected,
// which could indicate a potential issue with the query or the data integrity.
ErrMoreThanOneRowAffected = func(message string, rows int64) error {
return zerrors.CreateZitadelError(zerrors.KindInternal, nil, string(SlugInternalError), message, 1).
WithDetails(zerrors.ErrorDetailsMap{"rows": rows})
}
// ErrAuthMissingPermission is an error that can be returned when a user tries to perform an action that requires a specific permission,
// but the user does not have that permission.
ErrAuthMissingPermission = func(err error, message, permission string) error {
return zerrors.CreateZitadelError(zerrors.KindPermissionDenied, err, string(SlugAuthMissingPermission), message, 1).
WithDetails(zerrors.ErrorDetailsMap{"permission": permission})
}
)
type wrongIDPTypeError struct {
expected IDPType
got string
}
func NewIDPWrongTypeError(expected IDPType, got fmt.Stringer) error {
return &wrongIDPTypeError{
expected: expected,
got: got.String(),
}
}
func (e *wrongIDPTypeError) Error() string {
return fmt.Sprintf("wrong idp type returned, expected: %v, got: %v", e.expected, e.got)
}
func (e *wrongIDPTypeError) Is(target error) bool {
_, ok := target.(*wrongIDPTypeError)
return ok
}
type MultipleObjectsUpdatedError struct {
Msg string
Expected int64
Actual int64
}
func NewMultipleObjectsUpdatedError(expected, actual int64) error {
return &MultipleObjectsUpdatedError{
Expected: expected,
Actual: actual,
}
}
func (err *MultipleObjectsUpdatedError) Error() string {
return fmt.Sprintf("Message=expecting %d row(s) updated, got %d", err.Expected, err.Actual)
}
func (err *MultipleObjectsUpdatedError) Is(target error) bool {
_, ok := target.(*MultipleObjectsUpdatedError)
return ok
}
type UnexpectedQueryTypeError[T any] struct {
assertedType T
}
func NewUnexpectedQueryTypeError[T any](assertedType T) error {
return &UnexpectedQueryTypeError[T]{
assertedType: assertedType,
}
}
func (u *UnexpectedQueryTypeError[T]) Error() string {
return fmt.Sprintf("Message=unexpected query type '%T'", u.assertedType)
}
type UnexpectedTextQueryOperationError[T any] struct {
assertedType T
}
func NewUnexpectedTextQueryOperationError[T any](assertedType T) error {
return &UnexpectedTextQueryOperationError[T]{
assertedType: assertedType,
}
}
func (u *UnexpectedTextQueryOperationError[T]) Error() string {
return fmt.Sprintf("Message=unexpected text query operation type '%T'", u.assertedType)
}
type PasswordVerificationError struct {
failedAttempts uint8
}
func NewPasswordVerificationError(failedPassAttempts uint8) error {
return &PasswordVerificationError{
failedAttempts: failedPassAttempts,
}
}
func (e *PasswordVerificationError) Error() string {
return fmt.Sprintf("Message=failed password attempts (%d)", e.failedAttempts)
}
// handleGetError wraps DB errors coming from Get calls into [zerrors] errors.
//
// - errorID should be in the format <package short name>-<random id>
// - objectType should be the string representation of a DB object (e.g. 'user', 'session', 'idp'...)
//
// The function wraps [database.NoRowFoundError] to [zerrors.NotFound] error
// and any other error to [zerrors.InternalError]
func handleGetError(inputErr error, errorID, objectType string) error {
if inputErr == nil {
return nil
}
if errors.Is(inputErr, &database.NoRowFoundError{}) {
return zerrors.CreateZitadelError(zerrors.KindNotFound, inputErr, errorID, fmt.Sprintf("%s not found", objectType), 1)
}
return zerrors.CreateZitadelError(zerrors.KindInternal, inputErr, errorID, fmt.Sprintf("failed fetching %s", objectType), 1)
}
func handleUpdateError(inputErr error, expectedRowCount, actualRowCount int64, errorID, objectType string) error {
if inputErr == nil && expectedRowCount == actualRowCount {
return nil
}
if inputErr != nil {
return zerrors.CreateZitadelError(zerrors.KindInternal, inputErr, errorID, fmt.Sprintf("failed updating %s", objectType), 1)
}
if actualRowCount == 0 {
return zerrors.CreateZitadelError(zerrors.KindNotFound, nil, errorID, fmt.Sprintf("%s not found", objectType), 1)
}
if actualRowCount != expectedRowCount {
return zerrors.CreateZitadelError(zerrors.KindInternal, NewMultipleObjectsUpdatedError(expectedRowCount, actualRowCount), errorID, "unexpected number of rows updated", 1)
}
return nil
}
func (err *PasswordVerificationError) Is(target error) bool {
_, ok := target.(*PasswordVerificationError)
return ok
}
type RowsReturnedMismatchError struct {
Msg string
Expected int64
Actual int64
}
func NewRowsReturnedMismatchError(expected, actual int64) error {
return &RowsReturnedMismatchError{
Expected: expected,
Actual: actual,
}
}
func (err *RowsReturnedMismatchError) Error() string {
return fmt.Sprintf("Message=expecting %d row(s) returned, got %d", err.Expected, err.Actual)
}
func (err *RowsReturnedMismatchError) Is(target error) bool {
_, ok := target.(*RowsReturnedMismatchError)
return ok
}