Skip to content

Commit d0a2fcb

Browse files
authored
Refactor Client Interceptors and add auth header for streams (#84)
1 parent f3e4dac commit d0a2fcb

File tree

15 files changed

+637
-163
lines changed

15 files changed

+637
-163
lines changed

doc/index.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1984,6 +1984,14 @@ <h2>Table of Contents</h2>
19841984
<a href="#metalstack.infra.v2.UpdateBMCInfoResponse"><span class="badge">M</span>UpdateBMCInfoResponse</a>
19851985
</li>
19861986

1987+
<li>
1988+
<a href="#metalstack.infra.v2.WaitForMachineEventRequest"><span class="badge">M</span>WaitForMachineEventRequest</a>
1989+
</li>
1990+
1991+
<li>
1992+
<a href="#metalstack.infra.v2.WaitForMachineEventResponse"><span class="badge">M</span>WaitForMachineEventResponse</a>
1993+
</li>
1994+
19871995

19881996

19891997

@@ -15673,6 +15681,20 @@ <h3 id="metalstack.infra.v2.UpdateBMCInfoResponse">UpdateBMCInfoResponse</h3>
1567315681

1567415682

1567515683

15684+
<h3 id="metalstack.infra.v2.WaitForMachineEventRequest">WaitForMachineEventRequest</h3>
15685+
<p>WaitForMachineEventRequest</p>
15686+
15687+
15688+
15689+
15690+
15691+
<h3 id="metalstack.infra.v2.WaitForMachineEventResponse">WaitForMachineEventResponse</h3>
15692+
<p>WaitForMachineEventResponse</p>
15693+
15694+
15695+
15696+
15697+
1567615698

1567715699

1567815700

@@ -15694,6 +15716,13 @@ <h3 id="metalstack.infra.v2.BMCService">BMCService</h3>
1569415716
<td><p>UpdateBMCInfo</p></td>
1569515717
</tr>
1569615718

15719+
<tr>
15720+
<td>WaitForMachineEvent</td>
15721+
<td><a href="#metalstack.infra.v2.WaitForMachineEventRequest">WaitForMachineEventRequest</a></td>
15722+
<td><a href="#metalstack.infra.v2.WaitForMachineEventResponse">WaitForMachineEventResponse</a> stream</td>
15723+
<td><p>WaitForMachineEvent is called by the metal-bmc and is returned with a bmc command to execute.</p></td>
15724+
</tr>
15725+
1569715726
</tbody>
1569815727
</table>
1569915728

generate/go_client.tpl

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package client
33

44
import (
5-
"context"
65
"sync"
76

87
"connectrpc.com/connect"
@@ -53,33 +52,17 @@ func New(config *DialConfig) (Client, error) {
5352
interceptors: []connect.Interceptor{},
5453
}
5554

56-
authInterceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
57-
return connect.UnaryFunc(func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) {
58-
request.Header().Add("Authorization", "Bearer "+config.Token)
59-
return next(ctx, request)
60-
})
61-
})
62-
63-
loggingInterceptor := connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
64-
return connect.UnaryFunc(func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) {
65-
config.Log.Debug("intercept", "request procedure", request.Spec().Procedure, "body", request.Any())
66-
response, err := next(ctx, request)
67-
if err != nil {
68-
return nil, err
69-
}
70-
config.Log.Debug("intercept", "request procedure", request.Spec().Procedure, "response", response.Any())
71-
return response, err
72-
})
73-
})
74-
7555
if config.Token != "" {
56+
authInterceptor := &authInterceptor{config: config}
7657
c.interceptors = append(c.interceptors, authInterceptor)
7758
}
7859
if config.Log != nil {
60+
loggingInterceptor := &loggingInterceptor{config: config}
7961
c.interceptors = append(c.interceptors, loggingInterceptor)
8062
}
8163
c.interceptors = append(c.interceptors, config.Interceptors...)
8264

65+
// TODO convert to interceptor
8366
go c.startTokenRenewal()
8467

8568
return c, nil

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
buf.build/go/protovalidate v1.1.0
88
connectrpc.com/connect v1.19.1
99
github.com/bufbuild/protocompile v0.14.1
10+
github.com/davecgh/go-spew v1.1.1
1011
github.com/go-task/slim-sprig/v3 v3.0.0
1112
github.com/golang-jwt/jwt/v5 v5.3.0
1213
github.com/google/go-cmp v0.7.0
@@ -18,7 +19,6 @@ require (
1819
require (
1920
cel.dev/expr v0.25.1 // indirect
2021
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
21-
github.com/davecgh/go-spew v1.1.1 // indirect
2222
github.com/google/cel-go v0.26.1 // indirect
2323
github.com/klauspost/compress v1.18.2 // indirect
2424
github.com/kr/pretty v0.3.1 // indirect

go/client/client-interceptors.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package client
2+
3+
import (
4+
"context"
5+
6+
"connectrpc.com/connect"
7+
)
8+
9+
// authinterceptor adds the required auth headers
10+
type authInterceptor struct {
11+
config *DialConfig
12+
}
13+
14+
func (i *authInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
15+
return connect.UnaryFunc(func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) {
16+
request.Header().Add("Authorization", "Bearer "+i.config.Token)
17+
return next(ctx, request)
18+
})
19+
}
20+
21+
func (i *authInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
22+
return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn {
23+
return &streamingInterceptorConn{
24+
StreamingClientConn: next(ctx, spec),
25+
token: i.config.Token,
26+
}
27+
}
28+
}
29+
30+
func (i *authInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
31+
return next
32+
}
33+
34+
type streamingInterceptorConn struct {
35+
connect.StreamingClientConn
36+
token string
37+
}
38+
39+
func (conn *streamingInterceptorConn) Send(m any) error {
40+
conn.RequestHeader().Add("Authorization", "Bearer "+conn.token)
41+
return conn.StreamingClientConn.Send(m)
42+
}
43+
44+
type loggingInterceptor struct {
45+
config *DialConfig
46+
}
47+
48+
func (i *loggingInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
49+
return connect.UnaryFunc(func(ctx context.Context, request connect.AnyRequest) (connect.AnyResponse, error) {
50+
i.config.Log.Debug("intercept", "request procedure", request.Spec().Procedure, "body", request.Any())
51+
response, err := next(ctx, request)
52+
if err != nil {
53+
return nil, err
54+
}
55+
i.config.Log.Debug("intercept", "request procedure", request.Spec().Procedure, "response", response.Any())
56+
return response, err
57+
})
58+
}
59+
60+
func (i *loggingInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
61+
// TODO also log here
62+
return next
63+
}
64+
65+
func (i *loggingInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
66+
return next
67+
}

go/client/client.go

Lines changed: 3 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go/client/client_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"github.com/metal-stack/api/go/client"
2121
apiv2 "github.com/metal-stack/api/go/metalstack/api/v2"
2222
"github.com/metal-stack/api/go/metalstack/api/v2/apiv2connect"
23+
infrav2 "github.com/metal-stack/api/go/metalstack/infra/v2"
24+
"github.com/metal-stack/api/go/metalstack/infra/v2/infrav2connect"
2325
"github.com/stretchr/testify/require"
2426
)
2527

@@ -155,3 +157,74 @@ func (m *mockTokenService) Revoke(context.Context, *apiv2.TokenServiceRevokeRequ
155157
func (m *mockTokenService) Update(context.Context, *apiv2.TokenServiceUpdateRequest) (*apiv2.TokenServiceUpdateResponse, error) {
156158
panic("unimplemented")
157159
}
160+
161+
func Test_ClientInterceptors(t *testing.T) {
162+
var (
163+
bs = &mockBMCService{}
164+
mux = http.NewServeMux()
165+
log = slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
166+
ctx = t.Context()
167+
)
168+
169+
mux.Handle(infrav2connect.NewBMCServiceHandler(bs))
170+
server := httptest.NewTLSServer(mux)
171+
server.EnableHTTP2 = true
172+
defer func() {
173+
server.Close()
174+
}()
175+
176+
tokenString, err := generateToken(1 * time.Second)
177+
require.NoError(t, err)
178+
179+
c, err := client.New(&client.DialConfig{
180+
BaseURL: server.URL,
181+
Token: tokenString,
182+
Transport: server.Client().Transport,
183+
Log: log,
184+
})
185+
require.NoError(t, err)
186+
187+
// Synchronous call has authheader set
188+
resp, err := c.Infrav2().BMC().UpdateBMCInfo(ctx, &infrav2.UpdateBMCInfoRequest{})
189+
require.NoError(t, err)
190+
require.NotNil(t, resp)
191+
require.Equal(t, tokenString, bs.token)
192+
bs.token = ""
193+
194+
// Asynchronous call has authheader set
195+
stream, err := c.Infrav2().BMC().WaitForMachineEvent(ctx, &infrav2.WaitForMachineEventRequest{})
196+
require.NoError(t, err)
197+
require.NotNil(t, stream)
198+
require.Equal(t, tokenString, bs.token)
199+
}
200+
201+
type mockBMCService struct {
202+
token string
203+
}
204+
205+
func (m *mockBMCService) UpdateBMCInfo(ctx context.Context, _ *infrav2.UpdateBMCInfoRequest) (*infrav2.UpdateBMCInfoResponse, error) {
206+
callinfo, _ := connect.CallInfoForHandlerContext(ctx)
207+
authHeader := callinfo.RequestHeader().Get("Authorization")
208+
209+
_, token, found := strings.Cut(authHeader, "Bearer ")
210+
211+
if !found {
212+
return nil, fmt.Errorf("unable to extract token from header:%s", authHeader)
213+
}
214+
m.token = token
215+
return &infrav2.UpdateBMCInfoResponse{}, nil
216+
}
217+
218+
func (m *mockBMCService) WaitForMachineEvent(ctx context.Context, _ *infrav2.WaitForMachineEventRequest, stream *connect.ServerStream[infrav2.WaitForMachineEventResponse]) error {
219+
callinfo, _ := connect.CallInfoForHandlerContext(ctx)
220+
authHeader := callinfo.RequestHeader().Get("Authorization")
221+
222+
_, token, found := strings.Cut(authHeader, "Bearer ")
223+
224+
if !found {
225+
return fmt.Errorf("unable to extract token from header:%s", authHeader)
226+
}
227+
228+
m.token = token
229+
return stream.Send(&infrav2.WaitForMachineEventResponse{})
230+
}

0 commit comments

Comments
 (0)