mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
Fixes #11730 The v1 gRPC middleware can return native gRPC status errors such as `codes.Unauthenticated` when the auth header is missing. Those errors were being passed through the generic gRPC error converter, which only handled ZITADEL errors and downgraded raw gRPC statuses to `codes.Unknown`. Through grpc-gateway that caused `/auth/v1/users/me` to return HTTP 500 instead of HTTP 401. So this change preserves native gRPC status errors in `ZITADELToGRPCError`, keeps `ExtractZITADELError` aligned with those transport codes for activity reporting, and adds regression coverage for the converter, middleware, and gateway HTTP behavior. --------- Co-authored-by: Gayathri Vijayan <[email protected]>
167 lines
4.8 KiB
Go
167 lines
4.8 KiB
Go
package gerrors
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
|
|
"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"
|
|
|
|
"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/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(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(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(id, key string, err error) protoadapt.MessageV1 {
|
|
var errorInfo protoadapt.MessageV1
|
|
|
|
var wpe *commandErrors.WrongPasswordError
|
|
if err != nil && errors.As(err, &wpe) {
|
|
errorInfo = &message.CredentialsCheckError{Id: id, Message: key, FailedAttempts: wpe.FailedAttempts}
|
|
} else {
|
|
errorInfo = &message.ErrorDetail{Id: id, Message: key}
|
|
}
|
|
|
|
return errorInfo
|
|
}
|