From 36a19bfa838e7f57651297130e6dd10e765c6f22 Mon Sep 17 00:00:00 2001 From: Jo Date: Fri, 1 Mar 2024 11:31:06 +0100 Subject: [PATCH] AuthProxy: Allow disabling Auth Proxy cache (#83755) * extract auth proxy settings * simplify auth proxy methods * add doc mentions --- conf/defaults.ini | 4 +- .../auth-proxy/index.md | 7 +- pkg/api/datasources.go | 4 +- pkg/api/datasources_test.go | 14 ++-- pkg/api/frontendsettings.go | 4 +- pkg/api/login.go | 4 +- pkg/api/login_test.go | 8 +-- pkg/api/user.go | 6 +- .../usagestats/service/usage_stats_test.go | 2 +- .../usagestats/statscollector/service_test.go | 2 +- pkg/services/authn/authnimpl/service.go | 2 +- pkg/services/authn/authnimpl/usage_stats.go | 2 +- .../authn/authnimpl/usage_stats_test.go | 2 +- pkg/services/authn/clients/grafana.go | 6 +- pkg/services/authn/clients/grafana_test.go | 4 +- pkg/services/authn/clients/proxy.go | 70 ++++++++++++------- pkg/services/authn/clients/proxy_test.go | 12 ++-- pkg/services/contexthandler/contexthandler.go | 6 +- .../contexthandler/contexthandler_test.go | 6 +- .../datasources/service/datasource.go | 2 +- pkg/setting/setting.go | 36 +--------- pkg/setting/setting_auth_proxy.go | 45 ++++++++++++ pkg/setting/setting_test.go | 7 +- 23 files changed, 145 insertions(+), 110 deletions(-) create mode 100644 pkg/setting/setting_auth_proxy.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 93632560e13..a3772b884e1 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -832,7 +832,7 @@ enabled = false header_name = X-WEBAUTH-USER header_property = username auto_sign_up = true -sync_ttl = 60 +sync_ttl = 15 whitelist = headers = headers_encoded = false @@ -1305,7 +1305,7 @@ loki_basic_auth_password = # mylabelkey = mylabelvalue [unified_alerting.state_history.annotations] -# Controls retention of annotations automatically created while evaluating alert rules. +# Controls retention of annotations automatically created while evaluating alert rules. # Alert state history backend must be configured to be annotations (see setting [unified_alerting.state_history].backend). # Configures how long alert annotations are stored for. Default is 0, which keeps them forever. diff --git a/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md b/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md index 287d146bf69..5ed036bb6ed 100644 --- a/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md +++ b/docs/sources/setup-grafana/configure-security/configure-authentication/auth-proxy/index.md @@ -36,7 +36,8 @@ header_property = username auto_sign_up = true # Define cache time to live in minutes # If combined with Grafana LDAP integration it is also the sync interval -sync_ttl = 60 +# Set to 0 to always fetch and sync the latest user data +sync_ttl = 15 # Limit where auth proxy requests come from by configuring a list of IP addresses. # This can be used to prevent users spoofing the X-WEBAUTH-USER header. # Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120` @@ -231,6 +232,10 @@ ProxyPassReverse / http://grafana:3000/ With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file. +{{% admonition type="note" %}} +If the user is deleted from Grafana, the user will be not be able to login and resync until after the `sync_ttl` has expired. +{{% /admonition %}} + ### Team Sync (Enterprise only) > Only available in Grafana Enterprise v6.3+ diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 872e93dd555..f9182da0c72 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -344,11 +344,11 @@ func validateJSONData(ctx context.Context, jsonData *simplejson.Json, cfg *setti return nil } - if cfg.AuthProxyEnabled { + if cfg.AuthProxy.Enabled { for key, value := range jsonData.MustMap() { if strings.HasPrefix(key, datasources.CustomHeaderName) { header := fmt.Sprint(value) - if http.CanonicalHeaderKey(header) == http.CanonicalHeaderKey(cfg.AuthProxyHeaderName) { + if http.CanonicalHeaderKey(header) == http.CanonicalHeaderKey(cfg.AuthProxy.HeaderName) { datasourcesLogger.Error("Forbidden to add a data source header with a name equal to auth proxy header name", "headerName", key) return errors.New("validation error, invalid header name specified") } diff --git a/pkg/api/datasources_test.go b/pkg/api/datasources_test.go index 8ede32a8499..61481bec706 100644 --- a/pkg/api/datasources_test.go +++ b/pkg/api/datasources_test.go @@ -147,10 +147,10 @@ func TestAddDataSource_InvalidJSONData(t *testing.T) { sc := setupScenarioContext(t, "/api/datasources") hs.Cfg = setting.NewCfg() - hs.Cfg.AuthProxyEnabled = true - hs.Cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER" + hs.Cfg.AuthProxy.Enabled = true + hs.Cfg.AuthProxy.HeaderName = "X-AUTH-PROXY-HEADER" jsonData := simplejson.New() - jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName) + jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxy.HeaderName) sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ @@ -201,10 +201,10 @@ func TestUpdateDataSource_InvalidJSONData(t *testing.T) { } sc := setupScenarioContext(t, "/api/datasources/1234") - hs.Cfg.AuthProxyEnabled = true - hs.Cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER" + hs.Cfg.AuthProxy.Enabled = true + hs.Cfg.AuthProxy.HeaderName = "X-AUTH-PROXY-HEADER" jsonData := simplejson.New() - jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName) + jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxy.HeaderName) sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{ @@ -297,7 +297,7 @@ func TestUpdateDataSourceTeamHTTPHeaders_InvalidJSONData(t *testing.T) { }, } sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID)) - hs.Cfg.AuthProxyEnabled = true + hs.Cfg.AuthProxy.Enabled = true jsonData := simplejson.New() jsonData.Set("teamHttpHeaders", tc.data) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 29d678c100f..dc3d3296929 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -171,7 +171,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro AppUrl: hs.Cfg.AppURL, AppSubUrl: hs.Cfg.AppSubURL, AllowOrgCreate: (hs.Cfg.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, - AuthProxyEnabled: hs.Cfg.AuthProxyEnabled, + AuthProxyEnabled: hs.Cfg.AuthProxy.Enabled, LdapEnabled: hs.Cfg.LDAPAuthEnabled, JwtHeaderName: hs.Cfg.JWTAuth.HeaderName, JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin, @@ -322,7 +322,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro oauthProviders := hs.SocialService.GetOAuthInfoProviders() frontendSettings.Auth = dtos.FrontendSettingsAuthDTO{ - AuthProxyEnableLoginToken: hs.Cfg.AuthProxyEnableLoginToken, + AuthProxyEnableLoginToken: hs.Cfg.AuthProxy.EnableLoginToken, OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync, SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync, LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync, diff --git a/pkg/api/login.go b/pkg/api/login.go index 5443e95d564..355ce2bc602 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -125,8 +125,8 @@ func (hs *HTTPServer) LoginView(c *contextmodel.ReqContext) { if c.IsSignedIn { // Assign login token to auth proxy users if enable_login_token = true - if hs.Cfg.AuthProxyEnabled && - hs.Cfg.AuthProxyEnableLoginToken && + if hs.Cfg.AuthProxy.Enabled && + hs.Cfg.AuthProxy.EnableLoginToken && c.SignedInUser.AuthenticatedBy == loginservice.AuthProxyAuthModule { user := &user.User{ID: c.SignedInUser.UserID, Email: c.SignedInUser.Email, Login: c.SignedInUser.Login} err := hs.loginUserWithUser(user, c) diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index 35048e3cedb..2b0ef5df7c3 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -600,8 +600,8 @@ func TestAuthProxyLoginWithEnableLoginTokenAndEnabledOauthAutoLogin(t *testing.T return response.Empty(http.StatusOK) }) - sc.cfg.AuthProxyEnabled = true - sc.cfg.AuthProxyEnableLoginToken = true + sc.cfg.AuthProxy.Enabled = true + sc.cfg.AuthProxy.EnableLoginToken = true sc.m.Get(sc.url, sc.defaultHandler) sc.fakeReqNoAssertions("GET", sc.url).exec() @@ -640,8 +640,8 @@ func setupAuthProxyLoginTest(t *testing.T, enableLoginToken bool) *scenarioConte return response.Empty(http.StatusOK) }) - sc.cfg.AuthProxyEnabled = true - sc.cfg.AuthProxyEnableLoginToken = enableLoginToken + sc.cfg.AuthProxy.Enabled = true + sc.cfg.AuthProxy.EnableLoginToken = enableLoginToken sc.m.Get(sc.url, sc.defaultHandler) sc.fakeReqNoAssertions("GET", sc.url).exec() diff --git a/pkg/api/user.go b/pkg/api/user.go index a00b378e6f7..29f32175237 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -147,11 +147,11 @@ func (hs *HTTPServer) UpdateSignedInUser(c *contextmodel.ReqContext) response.Re return errResponse } - if hs.Cfg.AuthProxyEnabled { - if hs.Cfg.AuthProxyHeaderProperty == "email" && cmd.Email != c.SignedInUser.GetEmail() { + if hs.Cfg.AuthProxy.Enabled { + if hs.Cfg.AuthProxy.HeaderProperty == "email" && cmd.Email != c.SignedInUser.GetEmail() { return response.Error(http.StatusBadRequest, "Not allowed to change email when auth proxy is using email property", nil) } - if hs.Cfg.AuthProxyHeaderProperty == "username" && cmd.Login != c.SignedInUser.GetLogin() { + if hs.Cfg.AuthProxy.HeaderProperty == "username" && cmd.Login != c.SignedInUser.GetLogin() { return response.Error(http.StatusBadRequest, "Not allowed to change username when auth proxy is using username property", nil) } } diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go index 352345aca8c..f2e826b47bd 100644 --- a/pkg/infra/usagestats/service/usage_stats_test.go +++ b/pkg/infra/usagestats/service/usage_stats_test.go @@ -85,7 +85,7 @@ func TestMetrics(t *testing.T) { AnonymousEnabled: true, BasicAuthEnabled: true, LDAPAuthEnabled: true, - AuthProxyEnabled: true, + AuthProxy: setting.AuthProxySettings{Enabled: true}, Packaging: "deb", ReportingDistributor: "hosted-grafana", } diff --git a/pkg/infra/usagestats/statscollector/service_test.go b/pkg/infra/usagestats/statscollector/service_test.go index 7ffc509e0a1..9261b490cf9 100644 --- a/pkg/infra/usagestats/statscollector/service_test.go +++ b/pkg/infra/usagestats/statscollector/service_test.go @@ -145,7 +145,7 @@ func TestCollectingUsageStats(t *testing.T) { AnonymousEnabled: true, BasicAuthEnabled: true, LDAPAuthEnabled: true, - AuthProxyEnabled: true, + AuthProxy: setting.AuthProxySettings{Enabled: true}, Packaging: "deb", ReportingDistributor: "hosted-grafana", RemoteCacheOptions: &setting.RemoteCacheOptions{ diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 881a23d060a..08fd7e30052 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -122,7 +122,7 @@ func ProvideService( } } - if s.cfg.AuthProxyEnabled && len(proxyClients) > 0 { + if s.cfg.AuthProxy.Enabled && len(proxyClients) > 0 { proxy, err := clients.ProvideProxy(cfg, cache, proxyClients...) if err != nil { s.log.Error("Failed to configure auth proxy", "err", err) diff --git a/pkg/services/authn/authnimpl/usage_stats.go b/pkg/services/authn/authnimpl/usage_stats.go index b6ea1a95c6d..52f224c7326 100644 --- a/pkg/services/authn/authnimpl/usage_stats.go +++ b/pkg/services/authn/authnimpl/usage_stats.go @@ -13,7 +13,7 @@ func (s *Service) getUsageStats(ctx context.Context) (map[string]any, error) { authTypes := map[string]bool{} authTypes["basic_auth"] = s.cfg.BasicAuthEnabled authTypes["ldap"] = s.cfg.LDAPAuthEnabled - authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled + authTypes["auth_proxy"] = s.cfg.AuthProxy.Enabled authTypes["anonymous"] = s.cfg.AnonymousEnabled authTypes["jwt"] = s.cfg.JWTAuth.Enabled authTypes["grafana_password"] = !s.cfg.DisableLogin diff --git a/pkg/services/authn/authnimpl/usage_stats_test.go b/pkg/services/authn/authnimpl/usage_stats_test.go index cded65c117e..04d9776ce31 100644 --- a/pkg/services/authn/authnimpl/usage_stats_test.go +++ b/pkg/services/authn/authnimpl/usage_stats_test.go @@ -20,7 +20,7 @@ func TestService_getUsageStats(t *testing.T) { svc.cfg.DisableLoginForm = false svc.cfg.DisableLogin = false svc.cfg.BasicAuthEnabled = true - svc.cfg.AuthProxyEnabled = true + svc.cfg.AuthProxy.Enabled = true svc.cfg.JWTAuth.Enabled = true svc.cfg.LDAPAuthEnabled = true svc.cfg.EditorsCanAdmin = true diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index f84a88d2ec6..10e33008600 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -40,11 +40,11 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern FetchSyncedUser: true, SyncOrgRoles: true, SyncPermissions: true, - AllowSignUp: c.cfg.AuthProxyAutoSignUp, + AllowSignUp: c.cfg.AuthProxy.AutoSignUp, }, } - switch c.cfg.AuthProxyHeaderProperty { + switch c.cfg.AuthProxy.HeaderProperty { case "username": identity.Login = username addr, err := mail.ParseAddress(username) @@ -55,7 +55,7 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern identity.Login = username identity.Email = username default: - return nil, errInvalidProxyHeader.Errorf("invalid auth proxy header property, expected username or email but got: %s", c.cfg.AuthProxyHeaderProperty) + return nil, errInvalidProxyHeader.Errorf("invalid auth proxy header property, expected username or email but got: %s", c.cfg.AuthProxy.HeaderProperty) } if v, ok := additional[proxyFieldName]; ok { diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index 02d31b95572..f77d233744f 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -94,8 +94,8 @@ func TestGrafana_AuthenticateProxy(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { cfg := setting.NewCfg() - cfg.AuthProxyAutoSignUp = true - cfg.AuthProxyHeaderProperty = tt.proxyProperty + cfg.AuthProxy.AutoSignUp = true + cfg.AuthProxy.HeaderProperty = tt.proxyProperty c := ProvideGrafana(cfg, usertest.NewUserServiceFake()) identity, err := c.AuthenticateProxy(context.Background(), tt.req, tt.username, tt.additional) diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index fdfe1960e9d..cd6a8907615 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -3,6 +3,7 @@ package clients import ( "context" "encoding/hex" + "errors" "fmt" "hash/fnv" "net" @@ -12,6 +13,7 @@ import ( "time" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/remotecache" authidentity "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" @@ -43,7 +45,7 @@ var ( ) func ProvideProxy(cfg *setting.Cfg, cache proxyCache, clients ...authn.ProxyClient) (*Proxy, error) { - list, err := parseAcceptList(cfg.AuthProxyWhitelist) + list, err := parseAcceptList(cfg.AuthProxy.Whitelist) if err != nil { return nil, err } @@ -73,34 +75,22 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden return nil, errNotAcceptedIP.Errorf("request ip is not in the configured accept list") } - username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded) + username := getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded) if len(username) == 0 { return nil, errEmptyProxyHeader.Errorf("no username provided in auth proxy header") } additional := getAdditionalProxyHeaders(r, c.cfg) - cacheKey, ok := getProxyCacheKey(username, additional) - if ok { - // See if we have cached the user id, in that case we can fetch the signed-in user and skip sync. - // Error here means that we could not find anything in cache, so we can proceed as usual - if entry, err := c.cache.Get(ctx, cacheKey); err == nil { - uid, err := strconv.ParseInt(string(entry), 10, 64) - if err != nil { - c.log.FromContext(ctx).Warn("Failed to parse user id from cache", "error", err, "userId", string(entry)) - } else { - return &authn.Identity{ - ID: authn.NamespacedID(authn.NamespaceUser, uid), - OrgID: r.OrgID, - // FIXME: This does not match the actual auth module used, but should not have any impact - // Maybe caching the auth module used with the user ID would be a good idea - AuthenticatedBy: login.AuthProxyAuthModule, - ClientParams: authn.ClientParams{ - FetchSyncedUser: true, - SyncPermissions: true, - }, - }, nil - } + + if c.cfg.AuthProxy.SyncTTL != 0 && ok { + identity, errCache := c.retrieveIDFromCache(ctx, cacheKey, r) + if errCache == nil { + return identity, nil + } + + if !errors.Is(errCache, remotecache.ErrCacheItemNotFound) { + c.log.FromContext(ctx).Warn("Failed to fetch auth proxy info from cache", "error", errCache) } } @@ -117,8 +107,34 @@ func (c *Proxy) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden return nil, clientErr } +// See if we have cached the user id, in that case we can fetch the signed-in user and skip sync. +// Error here means that we could not find anything in cache, so we can proceed as usual +func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *authn.Request) (*authn.Identity, error) { + entry, err := c.cache.Get(ctx, cacheKey) + if err != nil { + return nil, err + } + + uid, err := strconv.ParseInt(string(entry), 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse user id from cache: %w - entry: %s", err, string(entry)) + } + + return &authn.Identity{ + ID: authn.NamespacedID(authn.NamespaceUser, uid), + OrgID: r.OrgID, + // FIXME: This does not match the actual auth module used, but should not have any impact + // Maybe caching the auth module used with the user ID would be a good idea + AuthenticatedBy: login.AuthProxyAuthModule, + ClientParams: authn.ClientParams{ + FetchSyncedUser: true, + SyncPermissions: true, + }, + }, nil +} + func (c *Proxy) Test(ctx context.Context, r *authn.Request) bool { - return len(getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded)) != 0 + return len(getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded)) != 0 } func (c *Proxy) Priority() uint { @@ -147,7 +163,7 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req // 3. Name = x; Role = Admin # cache hit with key Name=x;Role=Admin, no update, the user stays with Role=Editor // To avoid such a problem we also cache the key used using `prefix:[username]`. // Then whenever we get a cache miss due to changes in any header we use it to invalidate the previous item. - username := getProxyHeader(r, c.cfg.AuthProxyHeaderName, c.cfg.AuthProxyHeadersEncoded) + username := getProxyHeader(r, c.cfg.AuthProxy.HeaderName, c.cfg.AuthProxy.HeadersEncoded) userKey := fmt.Sprintf("%s:%s", proxyCachePrefix, username) // invalidate previously cached user id @@ -159,7 +175,7 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req c.log.FromContext(ctx).Debug("Cache proxy user", "userId", id) bytes := []byte(strconv.FormatInt(id, 10)) - duration := time.Duration(c.cfg.AuthProxySyncTTL) * time.Minute + duration := time.Duration(c.cfg.AuthProxy.SyncTTL) * time.Minute if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, duration); err != nil { c.log.Warn("Failed to cache proxy user", "error", err, "userId", id) } @@ -232,7 +248,7 @@ func getProxyHeader(r *authn.Request, headerName string, encoded bool) string { func getAdditionalProxyHeaders(r *authn.Request, cfg *setting.Cfg) map[string]string { additional := make(map[string]string, len(proxyFields)) for _, k := range proxyFields { - if v := getProxyHeader(r, cfg.AuthProxyHeaders[k], cfg.AuthProxyHeadersEncoded); v != "" { + if v := getProxyHeader(r, cfg.AuthProxy.Headers[k], cfg.AuthProxy.HeadersEncoded); v != "" { additional[k] = v } } diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index 04b0d6c492d..1f980122476 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -100,9 +100,9 @@ func TestProxy_Authenticate(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { cfg := setting.NewCfg() - cfg.AuthProxyHeaderName = "X-Username" - cfg.AuthProxyHeaders = tt.proxyHeaders - cfg.AuthProxyWhitelist = tt.ips + cfg.AuthProxy.HeaderName = "X-Username" + cfg.AuthProxy.Headers = tt.proxyHeaders + cfg.AuthProxy.Whitelist = tt.ips calledUsername := "" var calledAdditional map[string]string @@ -166,7 +166,7 @@ func TestProxy_Test(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { cfg := setting.NewCfg() - cfg.AuthProxyHeaderName = "Proxy-Header" + cfg.AuthProxy.HeaderName = "Proxy-Header" c, _ := ProvideProxy(cfg, nil, nil, nil) assert.Equal(t, tt.expectedOK, c.Test(context.Background(), tt.req)) @@ -197,8 +197,8 @@ func (f fakeCache) Delete(ctx context.Context, key string) error { func TestProxy_Hook(t *testing.T) { cfg := setting.NewCfg() - cfg.AuthProxyHeaderName = "X-Username" - cfg.AuthProxyHeaders = map[string]string{ + cfg.AuthProxy.HeaderName = "X-Username" + cfg.AuthProxy.Headers = map[string]string{ proxyFieldRole: "X-Role", } cache := &fakeCache{data: make(map[string][]byte)} diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index b405fed5ad4..02ff630fc66 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -195,9 +195,9 @@ func WithAuthHTTPHeaders(ctx context.Context, cfg *setting.Cfg) context.Context } // if auth proxy is enabled add the main proxy header and all configured headers - if cfg.AuthProxyEnabled { - list.Items = append(list.Items, cfg.AuthProxyHeaderName) - for _, header := range cfg.AuthProxyHeaders { + if cfg.AuthProxy.Enabled { + list.Items = append(list.Items, cfg.AuthProxy.HeaderName) + for _, header := range cfg.AuthProxy.Headers { if header != "" { list.Items = append(list.Items, header) } diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index 6662115355a..f7c46f7208c 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -114,9 +114,9 @@ func TestContextHandler(t *testing.T) { cfg := setting.NewCfg() cfg.JWTAuth.Enabled = true cfg.JWTAuth.HeaderName = "jwt-header" - cfg.AuthProxyEnabled = true - cfg.AuthProxyHeaderName = "proxy-header" - cfg.AuthProxyHeaders = map[string]string{ + cfg.AuthProxy.Enabled = true + cfg.AuthProxy.HeaderName = "proxy-header" + cfg.AuthProxy.Headers = map[string]string{ "name": "proxy-header-name", } diff --git a/pkg/services/datasources/service/datasource.go b/pkg/services/datasources/service/datasource.go index a73e71b8870..43bf6f0a6fe 100644 --- a/pkg/services/datasources/service/datasource.go +++ b/pkg/services/datasources/service/datasource.go @@ -675,7 +675,7 @@ func (s *Service) getCustomHeaders(jsonData *simplejson.Json, decryptedValues ma // skip a header with name that corresponds to auth proxy header's name // to make sure that data source proxy isn't used to circumvent auth proxy. // For more context take a look at CVE-2022-35957 - if s.cfg.AuthProxyEnabled && http.CanonicalHeaderKey(key) == http.CanonicalHeaderKey(s.cfg.AuthProxyHeaderName) { + if s.cfg.AuthProxy.Enabled && http.CanonicalHeaderKey(key) == http.CanonicalHeaderKey(s.cfg.AuthProxy.HeaderName) { continue } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index d9f8f487ced..efa62cfae5f 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -256,15 +256,7 @@ type Cfg struct { Azure *azsettings.AzureSettings // Auth proxy settings - AuthProxyEnabled bool - AuthProxyHeaderName string - AuthProxyHeaderProperty string - AuthProxyAutoSignUp bool - AuthProxyEnableLoginToken bool - AuthProxyWhitelist string - AuthProxyHeaders map[string]string - AuthProxyHeadersEncoded bool - AuthProxySyncTTL int + AuthProxy AuthProxySettings // OAuth OAuthAutoLogin bool @@ -1197,6 +1189,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.handleAWSConfig() cfg.readAzureSettings() cfg.readAuthJWTSettings() + cfg.readAuthProxySettings() cfg.readSessionConfig() if err := cfg.readSmtpSettings(); err != nil { return err @@ -1617,31 +1610,6 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { cfg.ExtendedJWTExpectAudience = authExtendedJWT.Key("expect_audience").MustString("") cfg.ExtendedJWTExpectIssuer = authExtendedJWT.Key("expect_issuer").MustString("") - // Auth Proxy - authProxy := iniFile.Section("auth.proxy") - cfg.AuthProxyEnabled = authProxy.Key("enabled").MustBool(false) - - cfg.AuthProxyHeaderName = valueAsString(authProxy, "header_name", "") - cfg.AuthProxyHeaderProperty = valueAsString(authProxy, "header_property", "") - cfg.AuthProxyAutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) - cfg.AuthProxyEnableLoginToken = authProxy.Key("enable_login_token").MustBool(false) - - cfg.AuthProxySyncTTL = authProxy.Key("sync_ttl").MustInt() - - cfg.AuthProxyWhitelist = valueAsString(authProxy, "whitelist", "") - - cfg.AuthProxyHeaders = make(map[string]string) - headers := valueAsString(authProxy, "headers", "") - - for _, propertyAndHeader := range util.SplitString(headers) { - split := strings.SplitN(propertyAndHeader, ":", 2) - if len(split) == 2 { - cfg.AuthProxyHeaders[split[0]] = split[1] - } - } - - cfg.AuthProxyHeadersEncoded = authProxy.Key("headers_encoded").MustBool(false) - // SSO Settings ssoSettings := iniFile.Section("sso_settings") cfg.SSOSettingsReloadInterval = ssoSettings.Key("reload_interval").MustDuration(1 * time.Minute) diff --git a/pkg/setting/setting_auth_proxy.go b/pkg/setting/setting_auth_proxy.go new file mode 100644 index 00000000000..36d300e78e9 --- /dev/null +++ b/pkg/setting/setting_auth_proxy.go @@ -0,0 +1,45 @@ +package setting + +import ( + "strings" + + "github.com/grafana/grafana/pkg/util" +) + +type AuthProxySettings struct { + // Auth Proxy + Enabled bool + HeaderName string + HeaderProperty string + AutoSignUp bool + EnableLoginToken bool + Whitelist string + Headers map[string]string + HeadersEncoded bool + SyncTTL int +} + +func (cfg *Cfg) readAuthProxySettings() { + authProxySettings := AuthProxySettings{} + authProxy := cfg.Raw.Section("auth.proxy") + authProxySettings.Enabled = authProxy.Key("enabled").MustBool(false) + authProxySettings.HeaderName = valueAsString(authProxy, "header_name", "") + authProxySettings.HeaderProperty = valueAsString(authProxy, "header_property", "") + authProxySettings.AutoSignUp = authProxy.Key("auto_sign_up").MustBool(true) + authProxySettings.EnableLoginToken = authProxy.Key("enable_login_token").MustBool(false) + authProxySettings.SyncTTL = authProxy.Key("sync_ttl").MustInt(15) + authProxySettings.Whitelist = valueAsString(authProxy, "whitelist", "") + authProxySettings.Headers = make(map[string]string) + headers := valueAsString(authProxy, "headers", "") + + for _, propertyAndHeader := range util.SplitString(headers) { + split := strings.SplitN(propertyAndHeader, ":", 2) + if len(split) == 2 { + authProxySettings.Headers[split[0]] = split[1] + } + } + + authProxySettings.HeadersEncoded = authProxy.Key("headers_encoded").MustBool(false) + + cfg.AuthProxy = authProxySettings +} diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index 184e5023b94..00b302981bb 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -13,11 +13,12 @@ import ( "testing" "time" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/util/osutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/ini.v1" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/util/osutil" ) const ( @@ -274,7 +275,7 @@ func TestLoadingSettings(t *testing.T) { }) require.Nil(t, err) - require.Equal(t, 2, cfg.AuthProxySyncTTL) + require.Equal(t, 2, cfg.AuthProxy.SyncTTL) }) t.Run("Test reading string values from .ini file", func(t *testing.T) {