Skip to content

Commit 126bdf1

Browse files
cnvergencekkk777-7
andauthored
feat: use grpc status errors in jwtinterceptor (#9020)
* use grpc status errors in jwtinterceptor Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> * use grpc status errors in tokenreview Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> --------- Signed-off-by: Karol Szwaj <karol.szwaj@gmail.com> Signed-off-by: kkk777-7 <kota.kimura0725@gmail.com> Co-authored-by: kkk777-7 <kota.kimura0725@gmail.com>
1 parent 1917fcb commit 126bdf1

3 files changed

Lines changed: 66 additions & 46 deletions

File tree

internal/xds/server/kubejwt/jwtinterceptor.go

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ package kubejwt
77

88
import (
99
"context"
10-
"fmt"
1110
"strings"
1211

1312
discoveryv3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
1413
"google.golang.org/grpc"
14+
"google.golang.org/grpc/codes"
1515
"google.golang.org/grpc/metadata"
16+
"google.golang.org/grpc/status"
1617
"k8s.io/client-go/kubernetes"
1718

1819
"github.com/envoyproxy/gateway/internal/logging"
@@ -51,18 +52,20 @@ func (i *JWTAuthInterceptor) authenticate(ctx context.Context, msg any) error {
5152

5253
md, ok := metadata.FromIncomingContext(ctx)
5354
if !ok {
54-
return fmt.Errorf("missing metadata")
55+
return status.Errorf(codes.Unauthenticated, "missing metadata for node %s", nodeID)
5556
}
5657

5758
authHeader := md.Get("authorization")
5859
if len(authHeader) == 0 {
59-
return fmt.Errorf("missing authorization token in metadata: %s", md)
60+
return status.Errorf(codes.Unauthenticated, "missing authorization header for node %s", nodeID)
6061
}
6162
token := strings.TrimPrefix(authHeader[0], "Bearer ")
6263

6364
if err := i.validateKubeJWT(ctx, token, nodeID); err != nil {
64-
i.logger.Error(err, "failed to validate token")
65-
return fmt.Errorf("failed to validate token: %w", err)
65+
if s, ok := status.FromError(err); ok {
66+
return status.Errorf(s.Code(), "failed to validate the token for node %s: %s", nodeID, s.Message())
67+
}
68+
return status.Errorf(codes.Unauthenticated, "failed to validate the token for node %s: %v", nodeID, err)
6669
}
6770

6871
return nil
@@ -109,15 +112,15 @@ func extractNodeID(m any) (string, error) {
109112
switch req := m.(type) {
110113
case *discoveryv3.DeltaDiscoveryRequest:
111114
if req.Node == nil || req.Node.Id == "" {
112-
return "", fmt.Errorf("missing node ID")
115+
return "", status.Error(codes.InvalidArgument, "missing node ID")
113116
}
114117
return req.Node.Id, nil
115118
case *discoveryv3.DiscoveryRequest:
116119
if req.Node == nil || req.Node.Id == "" {
117-
return "", fmt.Errorf("missing node ID")
120+
return "", status.Error(codes.InvalidArgument, "missing node ID")
118121
}
119122
return req.Node.Id, nil
120123
default:
121-
return "", fmt.Errorf("unexpected message type: %T", m)
124+
return "", status.Errorf(codes.InvalidArgument, "unexpected message type: %T", m)
122125
}
123126
}

internal/xds/server/kubejwt/jwtinterceptor_test.go

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import (
1616
"github.com/stretchr/testify/assert"
1717
"github.com/stretchr/testify/require"
1818
"google.golang.org/grpc"
19+
"google.golang.org/grpc/codes"
1920
"google.golang.org/grpc/metadata"
21+
"google.golang.org/grpc/status"
2022

2123
egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
2224
"github.com/envoyproxy/gateway/internal/logging"
@@ -42,85 +44,96 @@ func (m *mockServerStream) RecvMsg(_ any) error { return nil }
4244
// authTestCase defines a single test case for the authenticate method.
4345
// The same cases are run against both the stream (RecvMsg) and unary interceptor paths.
4446
type authTestCase struct {
45-
name string
46-
msg any
47-
ctx context.Context
48-
wantErr string
47+
name string
48+
msg any
49+
ctx context.Context
50+
wantErr string
51+
wantCode codes.Code
4952
}
5053

5154
func authTestCases() []authTestCase {
5255
return []authTestCase{
5356
{
54-
name: "unknown message type is rejected (fail-closed)",
55-
msg: &discoveryv3.DiscoveryResponse{},
56-
ctx: context.Background(),
57-
wantErr: "unexpected message type",
57+
name: "unknown message type is rejected (fail-closed)",
58+
msg: &discoveryv3.DiscoveryResponse{},
59+
ctx: context.Background(),
60+
wantErr: "unexpected message type",
61+
wantCode: codes.InvalidArgument,
5862
},
5963
{
60-
name: "nil message is rejected",
61-
msg: nil,
62-
ctx: context.Background(),
63-
wantErr: "unexpected message type",
64+
name: "nil message is rejected",
65+
msg: nil,
66+
ctx: context.Background(),
67+
wantErr: "unexpected message type",
68+
wantCode: codes.InvalidArgument,
6469
},
6570
{
66-
name: "DeltaDiscoveryRequest with nil node is rejected",
67-
msg: &discoveryv3.DeltaDiscoveryRequest{},
68-
ctx: context.Background(),
69-
wantErr: "missing node ID",
71+
name: "DeltaDiscoveryRequest with nil node is rejected",
72+
msg: &discoveryv3.DeltaDiscoveryRequest{},
73+
ctx: context.Background(),
74+
wantErr: "missing node ID",
75+
wantCode: codes.InvalidArgument,
7076
},
7177
{
72-
name: "DiscoveryRequest with nil node is rejected",
73-
msg: &discoveryv3.DiscoveryRequest{},
74-
ctx: context.Background(),
75-
wantErr: "missing node ID",
78+
name: "DiscoveryRequest with nil node is rejected",
79+
msg: &discoveryv3.DiscoveryRequest{},
80+
ctx: context.Background(),
81+
wantErr: "missing node ID",
82+
wantCode: codes.InvalidArgument,
7683
},
7784
{
7885
name: "DeltaDiscoveryRequest with empty node ID is rejected",
7986
msg: &discoveryv3.DeltaDiscoveryRequest{
8087
Node: &corev3.Node{Id: ""},
8188
},
82-
ctx: context.Background(),
83-
wantErr: "missing node ID",
89+
ctx: context.Background(),
90+
wantErr: "missing node ID",
91+
wantCode: codes.InvalidArgument,
8492
},
8593
{
8694
name: "DiscoveryRequest with empty node ID is rejected",
8795
msg: &discoveryv3.DiscoveryRequest{
8896
Node: &corev3.Node{Id: ""},
8997
},
90-
ctx: context.Background(),
91-
wantErr: "missing node ID",
98+
ctx: context.Background(),
99+
wantErr: "missing node ID",
100+
wantCode: codes.InvalidArgument,
92101
},
93102
{
94103
name: "DeltaDiscoveryRequest without metadata is rejected",
95104
msg: &discoveryv3.DeltaDiscoveryRequest{
96105
Node: &corev3.Node{Id: "pod-1"},
97106
},
98-
ctx: context.Background(),
99-
wantErr: "missing metadata",
107+
ctx: context.Background(),
108+
wantErr: "missing metadata",
109+
wantCode: codes.Unauthenticated,
100110
},
101111
{
102112
name: "DiscoveryRequest without metadata is rejected",
103113
msg: &discoveryv3.DiscoveryRequest{
104114
Node: &corev3.Node{Id: "pod-1"},
105115
},
106-
ctx: context.Background(),
107-
wantErr: "missing metadata",
116+
ctx: context.Background(),
117+
wantErr: "missing metadata",
118+
wantCode: codes.Unauthenticated,
108119
},
109120
{
110121
name: "DeltaDiscoveryRequest without auth header is rejected",
111122
msg: &discoveryv3.DeltaDiscoveryRequest{
112123
Node: &corev3.Node{Id: "pod-1"},
113124
},
114-
ctx: metadata.NewIncomingContext(context.Background(), metadata.MD{}),
115-
wantErr: "missing authorization token",
125+
ctx: metadata.NewIncomingContext(context.Background(), metadata.MD{}),
126+
wantErr: "missing authorization header",
127+
wantCode: codes.Unauthenticated,
116128
},
117129
{
118130
name: "DiscoveryRequest without auth header is rejected",
119131
msg: &discoveryv3.DiscoveryRequest{
120132
Node: &corev3.Node{Id: "pod-1"},
121133
},
122-
ctx: metadata.NewIncomingContext(context.Background(), metadata.MD{}),
123-
wantErr: "missing authorization token",
134+
ctx: metadata.NewIncomingContext(context.Background(), metadata.MD{}),
135+
wantErr: "missing authorization header",
136+
wantCode: codes.Unauthenticated,
124137
},
125138
}
126139
}
@@ -141,6 +154,7 @@ func TestAuthenticate_Stream(t *testing.T) {
141154
err := ws.RecvMsg(tt.msg)
142155
require.Error(t, err)
143156
assert.Contains(t, err.Error(), tt.wantErr)
157+
assert.Equal(t, tt.wantCode, status.Code(err), "unexpected gRPC status code")
144158
})
145159
}
146160
}
@@ -157,6 +171,7 @@ func TestAuthenticate_Unary(t *testing.T) {
157171
_, err := unary(tt.ctx, tt.msg, &grpc.UnaryServerInfo{}, handler)
158172
require.Error(t, err)
159173
assert.Contains(t, err.Error(), tt.wantErr)
174+
assert.Equal(t, tt.wantCode, status.Code(err), "unexpected gRPC status code")
160175
})
161176
}
162177
}

internal/xds/server/kubejwt/tokenreview.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"fmt"
1111
"slices"
1212

13+
"google.golang.org/grpc/codes"
14+
"google.golang.org/grpc/status"
1315
authenticationv1 "k8s.io/api/authentication/v1"
1416
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1517
"k8s.io/apiserver/pkg/authentication/serviceaccount"
@@ -42,31 +44,31 @@ func (i *JWTAuthInterceptor) validateKubeJWT(ctx context.Context, token, nodeID
4244

4345
tokenReview, err := i.clientset.AuthenticationV1().TokenReviews().Create(ctx, tokenReview, metav1.CreateOptions{})
4446
if err != nil {
45-
return fmt.Errorf("failed to call TokenReview API to verify service account JWT: %w", err)
47+
return status.Errorf(codes.Internal, "failed to call TokenReview API to verify service account JWT: %v", err)
4648
}
4749

4850
if tokenReview.Status.Error != "" {
49-
return fmt.Errorf("token review found error: %s", tokenReview.Status.Error)
51+
return status.Errorf(codes.Unauthenticated, "token review found error: %s", tokenReview.Status.Error)
5052
}
5153

5254
if !slices.Contains(tokenReview.Status.User.Groups, "system:serviceaccounts") {
53-
return fmt.Errorf("the token is not a service account")
55+
return status.Error(codes.Unauthenticated, "the token is not a service account")
5456
}
5557

5658
if !tokenReview.Status.Authenticated {
57-
return fmt.Errorf("token is not authenticated")
59+
return status.Error(codes.Unauthenticated, "token is not authenticated")
5860
}
5961

6062
// Check if the node ID in the request matches the pod name in the token review response.
6163
// This is used to prevent a client from accessing the xDS resource of another one.
6264
if tokenReview.Status.User.Extra != nil {
6365
podName := tokenReview.Status.User.Extra[serviceaccount.PodNameKey]
6466
if podName[0] == "" {
65-
return fmt.Errorf("pod name not found in token review response")
67+
return status.Error(codes.Unauthenticated, "pod name not found in token review response")
6668
}
6769

6870
if podName[0] != nodeID {
69-
return fmt.Errorf("pod name mismatch: expected %s, got %s", nodeID, podName[0])
71+
return status.Errorf(codes.Unauthenticated, "pod name mismatch: expected %s, got %s", nodeID, podName[0])
7072
}
7173
}
7274

0 commit comments

Comments
 (0)