mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-24 00:02:15 +00:00
fix(grpc): return 401 for unauthenticated v1 gateway errors (#11786)
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]>
This commit is contained in:
co-authored by
Gayathri Vijayan
parent
035ac4b9a2
commit
fbd43ced6e
@@ -23,6 +23,11 @@ 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
|
||||
@@ -78,6 +83,9 @@ func extractError(err error) (c codes.Code, msg, id string, lvl slog.Level) {
|
||||
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
|
||||
@@ -126,6 +134,24 @@ func extractError(err error) (c codes.Code, msg, id string, lvl slog.Level) {
|
||||
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
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/protoadapt"
|
||||
|
||||
commandErrors "github.com/zitadel/zitadel/internal/command/errors"
|
||||
@@ -49,6 +51,14 @@ func TestCaosToGRPCError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestZITADELToGRPCError_PreservesGRPCStatus(t *testing.T) {
|
||||
err := ZITADELToGRPCError(t.Context(), status.Error(codes.Unauthenticated, "auth header missing"))
|
||||
grpcStatus, ok := status.FromError(err)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, codes.Unauthenticated, grpcStatus.Code())
|
||||
assert.Equal(t, "auth header missing", grpcStatus.Message())
|
||||
}
|
||||
|
||||
func Test_getErrorInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -205,6 +215,14 @@ func Test_extractError(t *testing.T) {
|
||||
"",
|
||||
slog.LevelError,
|
||||
},
|
||||
{
|
||||
"grpc unauthenticated",
|
||||
args{status.Error(codes.Unauthenticated, "auth header missing")},
|
||||
codes.Unauthenticated,
|
||||
"auth header missing",
|
||||
"",
|
||||
slog.LevelWarn,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestErrorHandler_PreservesUnauthenticatedStatus(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/auth/v1/users/me", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
errorHandler(context.Background(), runtime.NewServeMux(), jsonMarshaler, resp, req, status.Error(codes.Unauthenticated, "auth header missing"))
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.Code)
|
||||
assert.Contains(t, resp.Body.String(), `"code":16`)
|
||||
assert.Contains(t, resp.Body.String(), `"message":"auth header missing"`)
|
||||
}
|
||||
@@ -43,6 +43,18 @@ func Test_toGRPCError(t *testing.T) {
|
||||
want: nil,
|
||||
wantCode: codes.FailedPrecondition,
|
||||
},
|
||||
{
|
||||
name: "grpc status error",
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
req: &mockReq{},
|
||||
handler: func(context.Context, any) (any, error) {
|
||||
return nil, status.Error(codes.Unauthenticated, "auth header missing")
|
||||
},
|
||||
},
|
||||
want: nil,
|
||||
wantCode: codes.Unauthenticated,
|
||||
},
|
||||
{
|
||||
name: "panic with string",
|
||||
args: args{
|
||||
|
||||
Reference in New Issue
Block a user