Skip to content

Commit 5b8e4b5

Browse files
feat(server): pass authenticated userId as header to extensions (#24356)
Signed-off-by: Alexandre Gaudreault <[email protected]> Co-authored-by: Michael Crenshaw <[email protected]>
1 parent 88a32d6 commit 5b8e4b5

File tree

4 files changed

+137
-52
lines changed

4 files changed

+137
-52
lines changed

docs/developer-guide/extensions/proxy-extensions.md

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Proxy Extensions
22

33
!!! warning "Beta Feature (Since 2.7.0)"
4-
This feature is in the [Beta](https://github.com/argoproj/argoproj/blob/main/community/feature-status.md#beta) stage.
4+
5+
This feature is in the [Beta](https://github.com/argoproj/argoproj/blob/main/community/feature-status.md#beta) stage.
56
It is generally considered stable, but there may be unhandled edge cases.
67

78
## Overview
@@ -29,7 +30,7 @@ metadata:
2930
name: argocd-cmd-params-cm
3031
namespace: argocd
3132
data:
32-
server.enable.proxy.extension: "true"
33+
server.enable.proxy.extension: 'true'
3334
```
3435
3536
Once the proxy extension is enabled, it can be configured in the main
@@ -102,11 +103,12 @@ respect the new configuration.
102103

103104
Every configuration entry is explained below:
104105

105-
#### `extensions` (*list*)
106+
#### `extensions` (_list_)
106107

107108
Defines configurations for all extensions enabled.
108109

109-
#### `extensions.name` (*string*)
110+
#### `extensions.name` (_string_)
111+
110112
(mandatory)
111113

112114
Defines the endpoint that will be used to register the extension
@@ -116,54 +118,61 @@ following url:
116118

117119
<argocd-host>/extensions/my-extension
118120

119-
#### `extensions.backend.connectionTimeout` (*duration string*)
121+
#### `extensions.backend.connectionTimeout` (_duration string_)
122+
120123
(optional. Default: 2s)
121124

122125
Is the maximum amount of time a dial to the extension server will wait
123-
for a connect to complete.
126+
for a connect to complete.
127+
128+
#### `extensions.backend.keepAlive` (_duration string_)
124129

125-
#### `extensions.backend.keepAlive` (*duration string*)
126130
(optional. Default: 15s)
127131

128132
Specifies the interval between keep-alive probes for an active network
129133
connection between the API server and the extension server.
130134

131-
#### `extensions.backend.idleConnectionTimeout` (*duration string*)
135+
#### `extensions.backend.idleConnectionTimeout` (_duration string_)
136+
132137
(optional. Default: 60s)
133138

134139
Is the maximum amount of time an idle (keep-alive) connection between
135140
the API server and the extension server will remain idle before
136141
closing itself.
137142

138-
#### `extensions.backend.maxIdleConnections` (*int*)
143+
#### `extensions.backend.maxIdleConnections` (_int_)
144+
139145
(optional. Default: 30)
140146

141147
Controls the maximum number of idle (keep-alive) connections between
142148
the API server and the extension server.
143149

144-
#### `extensions.backend.services` (*list*)
150+
#### `extensions.backend.services` (_list_)
145151

146152
Defines a list with backend url by cluster.
147153

148-
#### `extensions.backend.services.url` (*string*)
154+
#### `extensions.backend.services.url` (_string_)
155+
149156
(mandatory)
150157

151158
Is the address where the extension backend must be available.
152159

153-
#### `extensions.backend.services.headers` (*list*)
160+
#### `extensions.backend.services.headers` (_list_)
154161

155162
If provided, the headers list will be added on all outgoing requests
156163
for this service config. Existing headers in the incoming request with
157164
the same name will be overridden by the one in this list. Reserved header
158165
names will be ignored (see the [headers](#incoming-request-headers) below).
159166

160-
#### `extensions.backend.services.headers.name` (*string*)
167+
#### `extensions.backend.services.headers.name` (_string_)
168+
161169
(mandatory)
162170

163171
Defines the name of the header. It is a mandatory field if a header is
164172
provided.
165173

166-
#### `extensions.backend.services.headers.value` (*string*)
174+
#### `extensions.backend.services.headers.value` (_string_)
175+
167176
(mandatory)
168177

169178
Defines the value of the header. It is a mandatory field if a header is
@@ -178,7 +187,8 @@ Example:
178187
In the example above, the value will be replaced with the one from
179188
the argocd-secret with key 'some.argocd.secret.key'.
180189

181-
#### `extensions.backend.services.cluster` (*object*)
190+
#### `extensions.backend.services.cluster` (_object_)
191+
182192
(optional)
183193

184194
If provided, and multiple services are configured, will have to match
@@ -190,17 +200,19 @@ send requests to the proper backend service. If only one backend
190200
service is configured, this field is ignored, and all requests are
191201
forwarded to the configured one.
192202

193-
#### `extensions.backend.services.cluster.name` (*string*)
203+
#### `extensions.backend.services.cluster.name` (_string_)
204+
194205
(optional)
195206

196207
It will be matched with the value from
197208
`Application.Spec.Destination.Name`
198209

199-
#### `extensions.backend.services.cluster.server` (*string*)
210+
#### `extensions.backend.services.cluster.server` (_string_)
211+
200212
(optional)
201213

202214
It will be matched with the value from
203-
`Application.Spec.Destination.Server`.
215+
`Application.Spec.Destination.Server`.
204216

205217
## Usage
206218

@@ -245,7 +257,7 @@ Argo CD UI keeps the authentication token stored in a cookie
245257
(`argocd.token`). This value needs to be sent in the `Cookie` header
246258
so the API server can validate its authenticity.
247259

248-
Example:
260+
Example:
249261

250262
Cookie: argocd.token=eyJhbGciOiJIUzI1Ni...
251263

@@ -299,11 +311,16 @@ section for more details.
299311

300312
#### `Argocd-Username`
301313

302-
Will be populated with the username logged in Argo CD.
314+
Will be populated with the username logged in Argo CD. This is primarily useful for display purposes.
315+
To identify a user for programmatic needs, `Argocd-User-Id` is probably a better choice.
316+
317+
#### `Argocd-User-Id`
318+
319+
Will be populated with the internal user id, most often defined by the `sub` claim, logged in Argo CD.
303320

304321
#### `Argocd-User-Groups`
305322

306-
Will be populated with the 'groups' claim from the user logged in Argo CD.
323+
Will be populated with the configured RBAC scopes, most often the `groups` claim, from the user logged in Argo CD.
307324

308325
### Multi Backend Use-Case
309326

server/extension/extension.go

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,14 @@ const (
7474
// handler.
7575
HeaderArgoCDTargetClusterName = "Argocd-Target-Cluster-Name"
7676

77-
// HeaderArgoCDUsername is the header name that defines the logged
77+
// HeaderArgoCDUsername is the header name that defines the username of the logged
7878
// in user authenticated by Argo CD.
7979
HeaderArgoCDUsername = "Argocd-Username"
8080

81+
// HeaderArgoCDUserId is the header name that defines the internal user id of the logged
82+
// in user authenticated by Argo CD.
83+
HeaderArgoCDUserId = "Argocd-User-Id"
84+
8185
// HeaderArgoCDGroups is the header name that provides the 'groups'
8286
// claim from the users authenticated in Argo CD.
8387
HeaderArgoCDGroups = "Argocd-User-Groups"
@@ -284,7 +288,8 @@ func (p *DefaultProjectGetter) GetClusters(project string) ([]*v1alpha1.Cluster,
284288

285289
// UserGetter defines the contract to retrieve info from the logged in user.
286290
type UserGetter interface {
287-
GetUser(ctx context.Context) string
291+
GetUserId(ctx context.Context) string
292+
GetUsername(ctx context.Context) string
288293
GetGroups(ctx context.Context) []string
289294
}
290295

@@ -300,11 +305,16 @@ func NewDefaultUserGetter(policyEnf *rbacpolicy.RBACPolicyEnforcer) *DefaultUser
300305
}
301306
}
302307

303-
// GetUser will return the current logged in user
304-
func (u *DefaultUserGetter) GetUser(ctx context.Context) string {
308+
// GetUsername will return the username of the current logged in user
309+
func (u *DefaultUserGetter) GetUsername(ctx context.Context) string {
305310
return session.Username(ctx)
306311
}
307312

313+
// GetUserId will return the user id of the current logged in user
314+
func (u *DefaultUserGetter) GetUserId(ctx context.Context) string {
315+
return session.GetUserIdentifier(ctx)
316+
}
317+
308318
// GetGroups will return the groups associated with the logged in user.
309319
func (u *DefaultUserGetter) GetGroups(ctx context.Context) []string {
310320
return session.Groups(ctx, u.policyEnf.GetScopes())
@@ -783,11 +793,13 @@ func (m *Manager) CallExtension() func(http.ResponseWriter, *http.Request) {
783793
return
784794
}
785795

786-
user := m.userGetter.GetUser(r.Context())
796+
userId := m.userGetter.GetUserId(r.Context())
797+
username := m.userGetter.GetUsername(r.Context())
787798
groups := m.userGetter.GetGroups(r.Context())
788-
prepareRequest(r, m.namespace, extName, app, user, groups)
799+
prepareRequest(r, m.namespace, extName, app, userId, username, groups)
789800
m.log.WithFields(log.Fields{
790-
HeaderArgoCDUsername: user,
801+
HeaderArgoCDUserId: userId,
802+
HeaderArgoCDUsername: username,
791803
HeaderArgoCDGroups: strings.Join(groups, ","),
792804
HeaderArgoCDNamespace: m.namespace,
793805
HeaderArgoCDApplicationName: fmt.Sprintf("%s:%s", app.GetNamespace(), app.GetName()),
@@ -819,7 +831,7 @@ func registerMetrics(extName string, metrics httpsnoop.Metrics, extensionMetrics
819831
// - Cluster destination name
820832
// - Cluster destination server
821833
// - Argo CD authenticated username
822-
func prepareRequest(r *http.Request, namespace string, extName string, app *v1alpha1.Application, username string, groups []string) {
834+
func prepareRequest(r *http.Request, namespace string, extName string, app *v1alpha1.Application, userId string, username string, groups []string) {
823835
r.URL.Path = strings.TrimPrefix(r.URL.Path, fmt.Sprintf("%s/%s", URLPrefix, extName))
824836
r.Header.Set(HeaderArgoCDNamespace, namespace)
825837
if app.Spec.Destination.Name != "" {
@@ -828,6 +840,9 @@ func prepareRequest(r *http.Request, namespace string, extName string, app *v1al
828840
if app.Spec.Destination.Server != "" {
829841
r.Header.Set(HeaderArgoCDTargetClusterURL, app.Spec.Destination.Server)
830842
}
843+
if userId != "" {
844+
r.Header.Set(HeaderArgoCDUserId, userId)
845+
}
831846
if username != "" {
832847
r.Header.Set(HeaderArgoCDUsername, username)
833848
}

server/extension/extension_test.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -352,8 +352,9 @@ func TestCallExtension(t *testing.T) {
352352
f.rbacMock.On("EnforceErr", mock.Anything, rbac.ResourceExtensions, rbac.ActionInvoke, mock.Anything).Return(extAccessError)
353353
}
354354

355-
withUser := func(f *fixture, username string, groups []string) {
356-
f.userMock.On("GetUser", mock.Anything).Return(username)
355+
withUser := func(f *fixture, userId string, username string, groups []string) {
356+
f.userMock.On("GetUserId", mock.Anything).Return(userId)
357+
f.userMock.On("GetUsername", mock.Anything).Return(username)
357358
f.userMock.On("GetGroups", mock.Anything).Return(groups)
358359
}
359360

@@ -411,7 +412,7 @@ func TestCallExtension(t *testing.T) {
411412
}))
412413
defer backendSrv.Close()
413414
withRbac(f, true, true)
414-
withUser(f, "some-user", []string{"group1", "group2"})
415+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
415416
withExtensionConfig(getExtensionConfig(backendEndpoint, backendSrv.URL), f)
416417
ts := startTestServer(t, f)
417418
defer ts.Close()
@@ -448,6 +449,7 @@ func TestCallExtension(t *testing.T) {
448449
assert.Equal(t, clusterURL, resp.Header.Get(extension.HeaderArgoCDTargetClusterURL))
449450
assert.Equal(t, "Bearer some-bearer-token", resp.Header.Get("Authorization"))
450451
assert.Equal(t, "some-user", resp.Header.Get(extension.HeaderArgoCDUsername))
452+
assert.Equal(t, "some-user-id", resp.Header.Get(extension.HeaderArgoCDUserId))
451453
assert.Equal(t, "group1,group2", resp.Header.Get(extension.HeaderArgoCDGroups))
452454

453455
// waitgroup is necessary to make sure assertions aren't executed before
@@ -464,7 +466,7 @@ func TestCallExtension(t *testing.T) {
464466
withExtensionConfig(getExtensionConfigString(), f)
465467
withRbac(f, true, true)
466468
withMetrics(f)
467-
withUser(f, "some-user", []string{"group1", "group2"})
469+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
468470
cluster1Name := "cluster1"
469471
f.appGetterMock.On("Get", "namespace", "app-name").Return(getApp(cluster1Name, "", defaultProjectName), nil)
470472
withProject(getProjectWithDestinations("project-name", []string{cluster1Name}, []string{"some-url"}), f)
@@ -507,7 +509,7 @@ func TestCallExtension(t *testing.T) {
507509
withExtensionConfig(getExtensionConfigWith2Backends(extName, beSrv1.URL, cluster1Name, cluster1URL, beSrv2.URL, cluster2Name, cluster2URL), f)
508510
withProject(getProjectWithDestinations("project-name", []string{cluster1Name}, []string{cluster2URL}), f)
509511
withMetrics(f)
510-
withUser(f, "some-user", []string{"group1", "group2"})
512+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
511513

512514
ts := startTestServer(t, f)
513515
defer ts.Close()
@@ -554,7 +556,7 @@ func TestCallExtension(t *testing.T) {
554556
withRbac(f, allowApp, allowExtension)
555557
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
556558
withMetrics(f)
557-
withUser(f, "some-user", []string{"group1", "group2"})
559+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
558560
ts := startTestServer(t, f)
559561
defer ts.Close()
560562
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
@@ -578,7 +580,7 @@ func TestCallExtension(t *testing.T) {
578580
withRbac(f, allowApp, allowExtension)
579581
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
580582
withMetrics(f)
581-
withUser(f, "some-user", []string{"group1", "group2"})
583+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
582584
ts := startTestServer(t, f)
583585
defer ts.Close()
584586
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
@@ -603,7 +605,7 @@ func TestCallExtension(t *testing.T) {
603605
withRbac(f, allowApp, allowExtension)
604606
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
605607
withMetrics(f)
606-
withUser(f, "some-user", []string{"group1", "group2"})
608+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
607609
ts := startTestServer(t, f)
608610
defer ts.Close()
609611
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
@@ -629,7 +631,7 @@ func TestCallExtension(t *testing.T) {
629631
withRbac(f, allowApp, allowExtension)
630632
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
631633
withMetrics(f)
632-
withUser(f, "some-user", []string{"group1", "group2"})
634+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
633635
ts := startTestServer(t, f)
634636
defer ts.Close()
635637
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
@@ -655,7 +657,7 @@ func TestCallExtension(t *testing.T) {
655657
withRbac(f, allowApp, allowExtension)
656658
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
657659
withMetrics(f)
658-
withUser(f, "some-user", []string{"group1", "group2"})
660+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
659661
ts := startTestServer(t, f)
660662
defer ts.Close()
661663
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
@@ -687,7 +689,7 @@ func TestCallExtension(t *testing.T) {
687689
withExtensionConfig(getExtensionConfigWith2Backends(extName, "url1", "cluster1Name", "cluster1URL", "url2", "cluster2Name", "cluster2URL"), f)
688690
withProject(getProjectWithDestinations("project-name", nil, []string{"srv1", destinationServer}), f)
689691
withMetrics(f)
690-
withUser(f, "some-user", []string{"group1", "group2"})
692+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
691693

692694
ts := startTestServer(t, f)
693695
defer ts.Close()
@@ -721,7 +723,7 @@ func TestCallExtension(t *testing.T) {
721723
withRbac(f, allowApp, allowExtension)
722724
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
723725
withMetrics(f)
724-
withUser(f, "some-user", []string{"group1", "group2"})
726+
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
725727
ts := startTestServer(t, f)
726728
defer ts.Close()
727729
r := newExtensionRequest(t, "Get", ts.URL+"/extensions/")

0 commit comments

Comments
 (0)