Skip to content

Commit 0043938

Browse files
committed
embedded: add options for authn on the client
1 parent f892291 commit 0043938

File tree

4 files changed

+341
-44
lines changed

4 files changed

+341
-44
lines changed

README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,14 +159,19 @@ opts := proxy.NewOptions()
159159
opts.EmbeddedMode = true
160160
opts.SpiceDBOptions.SpiceDBEndpoint = "embedded://"
161161

162-
// Customize authentication headers (optional)
163-
opts.Authentication.Embedded.UsernameHeaders = []string{"Custom-User"}
164-
opts.Authentication.Embedded.GroupHeaders = []string{"Custom-Groups"}
165-
opts.Authentication.Embedded.ExtraHeaderPrefixes = []string{"Custom-Extra-"}
166-
167-
// Create and use the embedded proxy
168-
proxySrv, _ := proxy.NewServer(ctx, *opts)
169-
client := proxySrv.GetEmbeddedClient()
162+
// Complete configuration
163+
completedConfig, _ := opts.Complete(ctx)
164+
proxySrv, _ := proxy.NewServer(ctx, completedConfig)
165+
166+
// Get client with automatic authentication headers
167+
client := proxySrv.GetEmbeddedClient(
168+
proxy.WithUser("alice"),
169+
proxy.WithGroups("developers", "admin"),
170+
proxy.WithExtra("department", "engineering"),
171+
)
172+
173+
// Or get a basic client without authentication
174+
basicClient := proxySrv.GetEmbeddedClient()
170175
```
171176

172177
See [docs/embedding.md](docs/embedding.md) for detailed usage examples.

docs/embedding.md

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@ func main() {
5151
}
5252

5353
// Get an HTTP client that connects directly to the embedded proxy
54-
embeddedClient := proxySrv.GetEmbeddedClient()
54+
// Use functional options to automatically add authentication headers
55+
embeddedClient := proxySrv.GetEmbeddedClient(
56+
proxy.WithUser("my-user"),
57+
proxy.WithGroups("my-group", "admin"),
58+
proxy.WithExtra("department", "engineering"),
59+
)
5560

5661
// Create a Kubernetes client that uses the embedded proxy
57-
k8sClient := createKubernetesClient(embeddedClient, "my-user", []string{"my-group"})
62+
k8sClient := createKubernetesClient(embeddedClient)
5863

5964
// Use the client normally - all requests go through SpiceDB authorization
6065
pods, err := k8sClient.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
@@ -65,44 +70,19 @@ func main() {
6570
fmt.Printf("Found %d pods\n", len(pods.Items))
6671
}
6772

68-
func createKubernetesClient(embeddedClient *http.Client, username string, groups []string) *kubernetes.Clientset {
73+
func createKubernetesClient(embeddedClient *http.Client) *kubernetes.Clientset {
6974
restConfig := &rest.Config{
7075
Host: "http://embedded", // Special URL for embedded mode
7176
Transport: embeddedClient.Transport,
7277
}
7378

74-
// Add authentication headers
75-
restConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
76-
return &authTransport{
77-
username: username,
78-
groups: groups,
79-
rt: rt,
80-
}
81-
}
82-
8379
k8sClient, err := kubernetes.NewForConfig(restConfig)
8480
if err != nil {
8581
panic(err)
8682
}
8783

8884
return k8sClient
8985
}
90-
91-
type authTransport struct {
92-
username string
93-
groups []string
94-
rt http.RoundTripper
95-
}
96-
97-
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
98-
// Add authentication headers (using default header names)
99-
// These can be customized using --embedded-* flags
100-
req.Header.Set("X-Remote-User", t.username)
101-
for _, group := range t.groups {
102-
req.Header.Add("X-Remote-Group", group)
103-
}
104-
return t.base.RoundTrip(req)
105-
}
10686
```
10787

10888
### Configuration Options
@@ -163,14 +143,73 @@ opts.EmbeddedMode = true
163143
opts.Authentication.Embedded.UsernameHeaders = []string{"Custom-User"}
164144
opts.Authentication.Embedded.GroupHeaders = []string{"Custom-Groups"}
165145
opts.Authentication.Embedded.ExtraHeaderPrefixes = []string{"Custom-Extra-"}
146+
147+
// Complete and create the proxy server
148+
completedConfig, _ := opts.Complete(ctx)
149+
proxySrv, _ := proxy.NewServer(ctx, completedConfig)
150+
151+
// The client will automatically use the custom header names
152+
embeddedClient := proxySrv.GetEmbeddedClient(
153+
proxy.WithUser("alice"),
154+
proxy.WithGroups("developers", "admin"),
155+
proxy.WithExtra("department", "engineering"),
156+
)
157+
// Headers will be: Custom-User: alice, Custom-Groups: developers, etc.
166158
```
167159

168-
Then use custom headers in requests:
160+
This is similar to Kubernetes' request header authentication, but uses a separate dedicated `EmbeddedAuthentication` type for embedded mode and doesn't require client certificate configuration (the requests are trusted because the server is embedded).
161+
162+
### Functional Options for GetEmbeddedClient
163+
164+
The `GetEmbeddedClient()` method supports functional options that automatically add authentication headers based on your configured header names. This eliminates the need to manually add headers to each request:
165+
166+
```go
167+
// Basic client without authentication
168+
client := proxySrv.GetEmbeddedClient()
169+
170+
// Client with user authentication
171+
client := proxySrv.GetEmbeddedClient(
172+
proxy.WithUser("alice"),
173+
)
174+
175+
// Client with user and groups
176+
client := proxySrv.GetEmbeddedClient(
177+
proxy.WithUser("alice"),
178+
proxy.WithGroups("developers", "admin", "reviewers"),
179+
)
180+
181+
// Client with user, groups, and extra attributes
182+
client := proxySrv.GetEmbeddedClient(
183+
proxy.WithUser("alice"),
184+
proxy.WithGroups("developers", "admin"),
185+
proxy.WithExtra("department", "engineering"),
186+
proxy.WithExtra("team", "platform"),
187+
proxy.WithExtra("location", "remote"),
188+
)
169189
```
170-
Custom-User: alice
171-
Custom-Groups: developers
172-
Custom-Groups: admin
173-
Custom-Extra-Department: engineering
190+
191+
The functional options automatically use the header names you've configured in `opts.Authentication.Embedded`. For example, if you've configured custom header names:
192+
193+
```go
194+
opts.Authentication.Embedded.UsernameHeaders = []string{"My-User"}
195+
opts.Authentication.Embedded.GroupHeaders = []string{"My-Groups"}
196+
opts.Authentication.Embedded.ExtraHeaderPrefixes = []string{"My-Extra-"}
197+
198+
// This client will automatically add:
199+
// My-User: alice
200+
// My-Groups: developers
201+
// My-Groups: admin
202+
// My-Extra-department: engineering
203+
client := proxySrv.GetEmbeddedClient(
204+
proxy.WithUser("alice"),
205+
proxy.WithGroups("developers", "admin"),
206+
proxy.WithExtra("department", "engineering"),
207+
)
174208
```
175209

176-
This is similar to Kubernetes' request header authentication, but uses a separate dedicated `EmbeddedAuthentication` type for embedded mode and doesn't require client certificate configuration (the requests are trusted because the server is embedded).
210+
Available functional options:
211+
- `WithUser(username string)`: Sets the username
212+
- `WithGroups(groups ...string)`: Sets group memberships
213+
- `WithExtra(key, value string)`: Sets extra user attributes (can be called multiple times)
214+
215+
This approach provides a clean, type-safe way to configure authentication without manually managing headers.

pkg/proxy/embedded_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
)
2020

2121
func TestEmbeddedMode(t *testing.T) {
22+
defer require.NoError(t, logsv1.ResetForTest(utilfeature.DefaultFeatureGate))
2223
ctx := t.Context()
2324

2425
opts := createEmbeddedTestOptions(t)
@@ -260,6 +261,144 @@ func TestEmbeddedModeDefaults(t *testing.T) {
260261
require.NoError(t, err)
261262
}
262263

264+
func TestEmbeddedClientFunctionalOptions(t *testing.T) {
265+
defer require.NoError(t, logsv1.ResetForTest(utilfeature.DefaultFeatureGate))
266+
267+
ctx, cancel := context.WithCancel(context.Background())
268+
t.Cleanup(cancel)
269+
270+
// Create one proxy server for all subtests to avoid logging config issues
271+
opts := createEmbeddedTestOptions(t)
272+
completedConfig, err := opts.Complete(ctx)
273+
require.NoError(t, err)
274+
275+
proxySrv, err := NewServer(ctx, completedConfig)
276+
require.NoError(t, err)
277+
278+
t.Run("basic client without options", func(t *testing.T) {
279+
client := proxySrv.GetEmbeddedClient()
280+
require.NotNil(t, client)
281+
282+
// Basic client should not add any authentication headers automatically
283+
req, err := http.NewRequestWithContext(ctx, "GET", "http://embedded/healthz", nil)
284+
require.NoError(t, err)
285+
286+
resp, err := client.Do(req)
287+
require.NoError(t, err)
288+
defer resp.Body.Close()
289+
290+
require.NotEqual(t, 0, resp.StatusCode)
291+
_, err = io.ReadAll(resp.Body)
292+
require.NoError(t, err)
293+
})
294+
295+
t.Run("transport adds headers correctly", func(t *testing.T) {
296+
// Test the authHeaderTransport directly
297+
baseTransport := &testTransport{}
298+
transport := &authHeaderTransport{
299+
base: baseTransport,
300+
username: "test-user",
301+
groups: []string{"developers", "admin"},
302+
extra: map[string]string{"department": "engineering", "location": "remote"},
303+
usernameHeaders: []string{"X-Remote-User"},
304+
groupHeaders: []string{"X-Remote-Group"},
305+
extraHeaderPrefixes: []string{"X-Remote-Extra-"},
306+
}
307+
308+
req, err := http.NewRequest("GET", "http://example.com/test", nil)
309+
require.NoError(t, err)
310+
311+
_, err = transport.RoundTrip(req)
312+
require.NoError(t, err)
313+
314+
// Check that baseTransport received the request with added headers
315+
capturedReq := baseTransport.lastRequest
316+
require.NotNil(t, capturedReq)
317+
318+
// Check username header
319+
require.Equal(t, "test-user", capturedReq.Header.Get("X-Remote-User"))
320+
321+
// Check group headers
322+
groups := capturedReq.Header.Values("X-Remote-Group")
323+
require.Contains(t, groups, "developers")
324+
require.Contains(t, groups, "admin")
325+
require.Len(t, groups, 2)
326+
327+
// Check extra headers
328+
require.Equal(t, "engineering", capturedReq.Header.Get("X-Remote-Extra-department"))
329+
require.Equal(t, "remote", capturedReq.Header.Get("X-Remote-Extra-location"))
330+
})
331+
332+
t.Run("functional options create correct transport", func(t *testing.T) {
333+
client := proxySrv.GetEmbeddedClient(
334+
WithUser("alice"),
335+
WithGroups("security", "reviewers"),
336+
WithExtra("team", "platform"),
337+
)
338+
require.NotNil(t, client)
339+
340+
// Check that the transport is wrapped
341+
transport, ok := client.Transport.(*authHeaderTransport)
342+
require.True(t, ok, "transport should be wrapped with authHeaderTransport")
343+
344+
// Check configuration
345+
require.Equal(t, "alice", transport.username)
346+
require.Equal(t, []string{"security", "reviewers"}, transport.groups)
347+
require.Equal(t, "platform", transport.extra["team"])
348+
require.Equal(t, []string{"X-Remote-User"}, transport.usernameHeaders)
349+
require.Equal(t, []string{"X-Remote-Group"}, transport.groupHeaders)
350+
require.Equal(t, []string{"X-Remote-Extra-"}, transport.extraHeaderPrefixes)
351+
})
352+
353+
}
354+
355+
func TestEmbeddedClientCustomHeaderConfig(t *testing.T) {
356+
defer require.NoError(t, logsv1.ResetForTest(utilfeature.DefaultFeatureGate))
357+
358+
ctx, cancel := context.WithCancel(context.Background())
359+
t.Cleanup(cancel)
360+
361+
// Create proxy with custom header names
362+
customOpts := createEmbeddedTestOptions(t)
363+
customOpts.Authentication.Embedded.UsernameHeaders = []string{"Custom-User"}
364+
customOpts.Authentication.Embedded.GroupHeaders = []string{"Custom-Groups"}
365+
customOpts.Authentication.Embedded.ExtraHeaderPrefixes = []string{"Custom-Extra-"}
366+
367+
customCompletedConfig, err := customOpts.Complete(ctx)
368+
require.NoError(t, err)
369+
370+
customProxySrv, err := NewServer(ctx, customCompletedConfig)
371+
require.NoError(t, err)
372+
373+
client := customProxySrv.GetEmbeddedClient(
374+
WithUser("charlie"),
375+
WithGroups("security"),
376+
WithExtra("team", "infrastructure"),
377+
)
378+
require.NotNil(t, client)
379+
380+
// Check that custom header names are used
381+
transport, ok := client.Transport.(*authHeaderTransport)
382+
require.True(t, ok)
383+
require.Equal(t, []string{"Custom-User"}, transport.usernameHeaders)
384+
require.Equal(t, []string{"Custom-Groups"}, transport.groupHeaders)
385+
require.Equal(t, []string{"Custom-Extra-"}, transport.extraHeaderPrefixes)
386+
}
387+
388+
// testTransport is a simple transport that captures the last request
389+
type testTransport struct {
390+
lastRequest *http.Request
391+
}
392+
393+
func (t *testTransport) RoundTrip(req *http.Request) (*http.Response, error) {
394+
t.lastRequest = req
395+
return &http.Response{
396+
StatusCode: 200,
397+
Body: http.NoBody,
398+
Header: make(http.Header),
399+
}, nil
400+
}
401+
263402
// createEmbeddedTestOptions creates minimal options for embedded testing
264403
func createEmbeddedTestOptions(t *testing.T) *Options {
265404
t.Helper()

0 commit comments

Comments
 (0)