From c5d0acf7ece025c4af4b392ffc6180e3dd639363 Mon Sep 17 00:00:00 2001 From: Leonardo Luz Almeida Date: Tue, 16 Jul 2024 09:40:08 -0400 Subject: [PATCH] feat: forward the Argo CD logged in user to the proxy extension (#19075) * feat: forward the Argo CD logged in user to the proxy extension Signed-off-by: Leonardo Luz Almeida * Add docs Signed-off-by: Leonardo Luz Almeida --------- Signed-off-by: Leonardo Luz Almeida --- .../extensions/proxy-extensions.md | 4 +++ server/extension/extension.go | 27 +++++++++++++++---- server/extension/extension_test.go | 9 +++++-- server/server.go | 2 +- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/docs/developer-guide/extensions/proxy-extensions.md b/docs/developer-guide/extensions/proxy-extensions.md index 5d561657eb873..8cb242420ac53 100644 --- a/docs/developer-guide/extensions/proxy-extensions.md +++ b/docs/developer-guide/extensions/proxy-extensions.md @@ -264,6 +264,10 @@ Note that additional pre-configured headers can be added to outgoing request. See [backend service headers](#extensionsbackendservicesheaders-list) section for more details. +#### `Argocd-Username` + +Will be populated with the username logged in Argo CD. + ### Multi Backend Use-Case In some cases when Argo CD is configured to sync with multiple remote diff --git a/server/extension/extension.go b/server/extension/extension.go index 95dc539a70af1..6a430dedc3c9a 100644 --- a/server/extension/extension.go +++ b/server/extension/extension.go @@ -64,6 +64,10 @@ const ( // the client, its value will be overridden by the extension // handler. HeaderArgoCDTargetClusterName = "Argocd-Target-Cluster-Name" + + // HeaderArgoCDUsername is the header name that defines the logged + // in user authenticated by Argo CD. + HeaderArgoCDUsername = "Argocd-Username" ) // RequestResources defines the authorization scope for @@ -302,8 +306,13 @@ type Manager struct { rbac RbacEnforcer registry ExtensionRegistry metricsReg ExtensionMetricsRegistry + username UsernameGetterFunc } +// UsernameGetterFunc defines the function signature to retrieve the username +// from the context.Context. The real implementation is defined in session.Username() +type UsernameGetterFunc func(context.Context) string + // ExtensionMetricsRegistry exposes operations to update http metrics in the Argo CD // API server. type ExtensionMetricsRegistry interface { @@ -317,13 +326,14 @@ type ExtensionMetricsRegistry interface { } // NewManager will initialize a new manager. -func NewManager(log *log.Entry, sg SettingsGetter, ag ApplicationGetter, pg ProjectGetter, rbac RbacEnforcer) *Manager { +func NewManager(log *log.Entry, sg SettingsGetter, ag ApplicationGetter, pg ProjectGetter, rbac RbacEnforcer, userFn UsernameGetterFunc) *Manager { return &Manager{ log: log, settings: sg, application: ag, project: pg, rbac: rbac, + username: userFn, } } @@ -699,7 +709,7 @@ func (m *Manager) CallExtension() func(http.ResponseWriter, *http.Request) { return } - prepareRequest(r, extName, app) + prepareRequest(r, extName, app, m.username(r.Context())) m.log.Debugf("proxing request for extension %q", extName) // httpsnoop package is used to properly wrap the responseWriter // and avoid optional intefaces issue: @@ -719,9 +729,13 @@ func registerMetrics(extName string, metrics httpsnoop.Metrics, extensionMetrics } // prepareRequest is responsible for cleaning the incoming request URL removing -// the Argo CD extension API section from it. It will set the cluster destination name -// and cluster destination server in the headers as it is defined in the given app. -func prepareRequest(r *http.Request, extName string, app *v1alpha1.Application) { +// the Argo CD extension API section from it. It provides additional information to +// the backend service appending them in the outgoing request headers. The appended +// headers are: +// - Cluster destination name +// - Cluster destination server +// - Argo CD authenticated username +func prepareRequest(r *http.Request, extName string, app *v1alpha1.Application, username string) { r.URL.Path = strings.TrimPrefix(r.URL.Path, fmt.Sprintf("%s/%s", URLPrefix, extName)) if app.Spec.Destination.Name != "" { r.Header.Set(HeaderArgoCDTargetClusterName, app.Spec.Destination.Name) @@ -729,6 +743,9 @@ func prepareRequest(r *http.Request, extName string, app *v1alpha1.Application) if app.Spec.Destination.Server != "" { r.Header.Set(HeaderArgoCDTargetClusterURL, app.Spec.Destination.Server) } + if username != "" { + r.Header.Set(HeaderArgoCDUsername, username) + } } // AddMetricsRegistry will associate the given metricsReg in the Manager. diff --git a/server/extension/extension_test.go b/server/extension/extension_test.go index 7d6a8e5ffb02b..694682d48fed0 100644 --- a/server/extension/extension_test.go +++ b/server/extension/extension_test.go @@ -150,7 +150,7 @@ func TestRegisterExtensions(t *testing.T) { logger, _ := test.NewNullLogger() logEntry := logger.WithContext(context.Background()) - m := extension.NewManager(logEntry, settMock, nil, nil, nil) + m := extension.NewManager(logEntry, settMock, nil, nil, nil, nil) return &fixture{ settingsGetterMock: settMock, @@ -249,6 +249,10 @@ func TestCallExtension(t *testing.T) { } defaultProjectName := "project-name" + usernameFn := func(context.Context) string { + return "loggedinUser" + } + setup := func() *fixture { appMock := &mocks.ApplicationGetter{} settMock := &mocks.SettingsGetter{} @@ -258,7 +262,7 @@ func TestCallExtension(t *testing.T) { logger, _ := test.NewNullLogger() logEntry := logger.WithContext(context.Background()) - m := extension.NewManager(logEntry, settMock, appMock, projMock, rbacMock) + m := extension.NewManager(logEntry, settMock, appMock, projMock, rbacMock, usernameFn) m.AddMetricsRegistry(metricsMock) mux := http.NewServeMux() @@ -437,6 +441,7 @@ func TestCallExtension(t *testing.T) { assert.Equal(t, backendResponse, actual) assert.Equal(t, clusterURL, resp.Header.Get(extension.HeaderArgoCDTargetClusterURL)) assert.Equal(t, "Bearer some-bearer-token", resp.Header.Get("Authorization")) + assert.Equal(t, "loggedinUser", resp.Header.Get(extension.HeaderArgoCDUsername)) // waitgroup is necessary to make sure assertions aren't executed before // the goroutine initiated by extension.CallExtension concludes which would diff --git a/server/server.go b/server/server.go index 23520ee8c90fc..28591b2568664 100644 --- a/server/server.go +++ b/server/server.go @@ -326,7 +326,7 @@ func NewServer(ctx context.Context, opts ArgoCDServerOpts, appsetOpts Applicatio sg := extension.NewDefaultSettingsGetter(settingsMgr) ag := extension.NewDefaultApplicationGetter(appLister) pg := extension.NewDefaultProjectGetter(projLister, dbInstance) - em := extension.NewManager(logger, sg, ag, pg, enf) + em := extension.NewManager(logger, sg, ag, pg, enf, util_session.Username) a := &ArgoCDServer{ ArgoCDServerOpts: opts,