mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# 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]>
187 lines
5.8 KiB
Go
187 lines
5.8 KiB
Go
package gerrors
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"connectrpc.com/connect"
|
|
"github.com/jackc/pgx/v5/pgconn"
|
|
"github.com/zitadel/sloggcp"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/protoadapt"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
"github.com/zitadel/zitadel/backend/v3/instrumentation/logging"
|
|
commandErrors "github.com/zitadel/zitadel/internal/command/errors"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
"github.com/zitadel/zitadel/pkg/grpc/error/v2"
|
|
"github.com/zitadel/zitadel/pkg/grpc/message"
|
|
)
|
|
|
|
func ZITADELToGRPCError(ctx context.Context, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if grpcStatus, ok := status.FromError(err); ok {
|
|
// Native gRPC status errors are already transport-safe and may carry details.
|
|
logging.Log(ctx, grpcStatusLevel(grpcStatus.Code()), grpcStatus.Message(), "err", err, "code", grpcStatus.Code())
|
|
return err
|
|
}
|
|
|
|
code, key, id, lvl := extractError(err)
|
|
msg := key
|
|
msg += " (" + id + ")"
|
|
logging.Log(ctx, lvl, msg, "err", err, "code", code)
|
|
|
|
errorInfo := getErrorInfo(ctx, id, key, err)
|
|
|
|
s, err := status.New(code, msg).WithDetails(errorInfo)
|
|
if err != nil {
|
|
logging.WithError(ctx, err).Debug("unable to add error detail")
|
|
return status.New(code, msg).Err()
|
|
}
|
|
|
|
return s.Err()
|
|
}
|
|
|
|
func ZITADELToConnectError(ctx context.Context, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
connectError := new(connect.Error)
|
|
if errors.As(err, &connectError) {
|
|
// Connect error may be returned by other middlewares,
|
|
// so we assume it's a client error and log as warning.
|
|
logging.Warn(ctx, connectError.Message(), "err", connectError.Unwrap(), "code", connectError.Code())
|
|
return err
|
|
}
|
|
code, key, id, lvl := extractError(err)
|
|
msg := key
|
|
msg += " (" + id + ")"
|
|
logging.Log(ctx, lvl, msg, "err", err)
|
|
|
|
errorInfo := getErrorInfo(ctx, id, key, err)
|
|
|
|
cErr := connect.NewError(connect.Code(code), errors.New(msg))
|
|
if detail, detailErr := connect.NewErrorDetail(errorInfo.(proto.Message)); detailErr == nil {
|
|
cErr.AddDetail(detail)
|
|
}
|
|
return cErr
|
|
}
|
|
|
|
func ExtractZITADELError(err error) (code codes.Code, msg, id string) {
|
|
if err == nil {
|
|
return codes.OK, "", ""
|
|
}
|
|
code, msg, id, _ = extractError(err)
|
|
return code, msg, id
|
|
}
|
|
|
|
func extractError(err error) (c codes.Code, msg, id string, lvl slog.Level) {
|
|
connErr := new(pgconn.ConnectError)
|
|
if ok := errors.As(err, &connErr); ok {
|
|
return codes.Internal, "db connection error", "", slog.LevelError
|
|
}
|
|
if grpcStatus, ok := status.FromError(err); ok {
|
|
return grpcStatus.Code(), grpcStatus.Message(), "", grpcStatusLevel(grpcStatus.Code())
|
|
}
|
|
zitadelErr, ok := zerrors.AsZitadelError(err)
|
|
if !ok {
|
|
return codes.Unknown, err.Error(), "", slog.LevelError
|
|
}
|
|
msg, id = zitadelErr.GetMessage(), zitadelErr.GetID()
|
|
|
|
switch zitadelErr.Kind {
|
|
case zerrors.KindAlreadyExists:
|
|
c, lvl = codes.AlreadyExists, slog.LevelWarn
|
|
case zerrors.KindDeadlineExceeded:
|
|
c, lvl = codes.DeadlineExceeded, slog.LevelError
|
|
case zerrors.KindInternal:
|
|
c, lvl = codes.Internal, slog.LevelError
|
|
case zerrors.KindInvalidArgument:
|
|
c, lvl = codes.InvalidArgument, slog.LevelWarn
|
|
case zerrors.KindNotFound:
|
|
c, lvl = codes.NotFound, slog.LevelWarn
|
|
case zerrors.KindPermissionDenied:
|
|
c, lvl = codes.PermissionDenied, slog.LevelWarn
|
|
case zerrors.KindPreconditionFailed:
|
|
c, lvl = codes.FailedPrecondition, slog.LevelWarn
|
|
case zerrors.KindUnauthenticated:
|
|
c, lvl = codes.Unauthenticated, slog.LevelWarn
|
|
case zerrors.KindUnavailable:
|
|
c, lvl = codes.Unavailable, slog.LevelError
|
|
case zerrors.KindUnimplemented:
|
|
c, lvl = codes.Unimplemented, slog.LevelInfo
|
|
case zerrors.KindResourceExhausted:
|
|
c, lvl = codes.ResourceExhausted, slog.LevelError
|
|
case zerrors.KindDataLoss:
|
|
c, lvl = codes.DataLoss, slog.LevelError
|
|
case zerrors.KindCanceled:
|
|
c, lvl = codes.Canceled, slog.LevelWarn
|
|
case zerrors.KindAborted:
|
|
c, lvl = codes.Aborted, slog.LevelError
|
|
case zerrors.KindOutOfRange:
|
|
c, lvl = codes.OutOfRange, slog.LevelWarn
|
|
case zerrors.KindUnknown:
|
|
c, lvl = codes.Unknown, slog.LevelError
|
|
default:
|
|
c, lvl = codes.Unknown, slog.LevelError
|
|
}
|
|
if id == zerrors.IDRecover {
|
|
lvl = sloggcp.LevelAlert
|
|
}
|
|
return c, msg, id, lvl
|
|
}
|
|
|
|
func grpcStatusLevel(code codes.Code) slog.Level {
|
|
switch code { //nolint:exhaustive
|
|
case codes.AlreadyExists,
|
|
codes.Canceled,
|
|
codes.FailedPrecondition,
|
|
codes.InvalidArgument,
|
|
codes.NotFound,
|
|
codes.OutOfRange,
|
|
codes.PermissionDenied,
|
|
codes.Unauthenticated:
|
|
return slog.LevelWarn
|
|
case codes.Unimplemented:
|
|
return slog.LevelInfo
|
|
default:
|
|
return slog.LevelError
|
|
}
|
|
}
|
|
|
|
|
|
func getErrorInfo(ctx context.Context, id, key string, err error) protoadapt.MessageV1 {
|
|
var wpe *commandErrors.WrongPasswordError
|
|
var zerr *zerrors.ZitadelError
|
|
if err != nil && errors.As(err, &wpe) {
|
|
return &message.CredentialsCheckError{Id: id, Message: key, FailedAttempts: wpe.FailedAttempts}
|
|
}
|
|
// Let's check the error is a zitadel error and does contain a slug, which should be the case for almost any error
|
|
// in the future, but not yet. In case it's not, we'll fall back to the old error details to prevent a breaking change.
|
|
if err != nil && errors.As(err, &zerr) && strings.Contains(zerr.GetID(), ".") {
|
|
if zerr.Details == nil {
|
|
return &errorpb.ErrorDetail{Slug: id, Message: key}
|
|
}
|
|
errDetails, err := zerr.GetDetails().MarshalJSON()
|
|
if err != nil {
|
|
logging.WithError(ctx, err).Error("unable to marshal error details")
|
|
return &errorpb.ErrorDetail{Slug: id, Message: key}
|
|
}
|
|
|
|
details := new(structpb.Struct)
|
|
err = details.UnmarshalJSON(errDetails)
|
|
if err != nil {
|
|
logging.WithError(ctx, err).Error("unable to unmarshal error details")
|
|
return &errorpb.ErrorDetail{Slug: id, Message: key}
|
|
}
|
|
return &errorpb.ErrorDetail{Slug: id, Message: key, Details: details}
|
|
}
|
|
return &message.ErrorDetail{Id: id, Message: key}
|
|
}
|