From ad1f792b8b39980a1a46dba3832730dd01b15989 Mon Sep 17 00:00:00 2001 From: yuwaMSFT2 Date: Thu, 22 Jul 2021 06:49:58 -0700 Subject: [PATCH] Auth: Pass user role to Grafana using auth proxy (#36729) * Pass role to Grafana using auth proxy By default, the role will be applied to the default org of the user. If the request uses the standard header "X-Grafana-Org-Id", the role will be applied to the specified org Tested in both unit test and manually E2E * Address comment: only allow the user role to be applied to the default org Co-authored-by: Leonard Gram --- docs/sources/auth/auth-proxy.md | 2 +- pkg/middleware/middleware_test.go | 70 ++++++++++++++++++- .../contexthandler/authproxy/authproxy.go | 22 ++++-- .../authproxy/authproxy_test.go | 8 ++- 4 files changed, 93 insertions(+), 9 deletions(-) diff --git a/docs/sources/auth/auth-proxy.md b/docs/sources/auth/auth-proxy.md index 5cb6a5341d7..5e5b8449952 100755 --- a/docs/sources/auth/auth-proxy.md +++ b/docs/sources/auth/auth-proxy.md @@ -30,7 +30,7 @@ sync_ttl = 60 # Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120` whitelist = # Optionally define more headers to sync other user attributes -# Example `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPS` +# Example `headers = Name:X-WEBAUTH-NAME Role:X-WEBAUTH-ROLE Email:X-WEBAUTH-EMAIL Groups:X-WEBAUTH-GROUPS` headers = # Check out docs on this for more details on the below setting enable_login_token = false diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index db1d585f0c5..30ea0b1aff1 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "path/filepath" + "strconv" "testing" "time" @@ -349,6 +350,8 @@ func TestMiddlewareContext(t *testing.T) { t.Run("auth_proxy", func(t *testing.T) { const userID int64 = 33 const orgID int64 = 4 + const defaultOrgId int64 = 1 + const orgRole = "Admin" configure := func(cfg *setting.Cfg) { cfg.AuthProxyEnabled = true @@ -356,7 +359,7 @@ func TestMiddlewareContext(t *testing.T) { cfg.LDAPEnabled = true cfg.AuthProxyHeaderName = "X-WEBAUTH-USER" cfg.AuthProxyHeaderProperty = "username" - cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"} + cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS", "Role": "X-WEBAUTH-ROLE"} } const hdrName = "markelog" @@ -432,6 +435,71 @@ func TestMiddlewareContext(t *testing.T) { cfg.AuthProxyAutoSignUp = true }) + middlewareScenario(t, "Should assign role from header to default org", func(t *testing.T, sc *scenarioContext) { + var storedRoleInfo map[int64]models.RoleType = nil + bus.AddHandlerCtx("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error { + if query.UserId > 0 { + query.Result = &models.SignedInUser{OrgId: defaultOrgId, UserId: userID, OrgRole: storedRoleInfo[defaultOrgId]} + return nil + } + return models.ErrUserNotFound + }) + + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + cmd.Result = &models.User{Id: userID} + storedRoleInfo = cmd.ExternalUser.OrgRoles + return nil + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName) + sc.req.Header.Set("X-WEBAUTH-ROLE", orgRole) + sc.exec() + + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, userID, sc.context.UserId) + assert.Equal(t, defaultOrgId, sc.context.OrgId) + assert.Equal(t, orgRole, string(sc.context.OrgRole)) + }, func(cfg *setting.Cfg) { + configure(cfg) + cfg.LDAPEnabled = false + cfg.AuthProxyAutoSignUp = true + }) + + middlewareScenario(t, "Should NOT assign role from header to non-default org", func(t *testing.T, sc *scenarioContext) { + var storedRoleInfo map[int64]models.RoleType = nil + bus.AddHandlerCtx("test", func(ctx context.Context, query *models.GetSignedInUserQuery) error { + if query.UserId > 0 { + query.Result = &models.SignedInUser{OrgId: orgID, UserId: userID, OrgRole: storedRoleInfo[orgID]} + return nil + } + return models.ErrUserNotFound + }) + + bus.AddHandler("test", func(cmd *models.UpsertUserCommand) error { + cmd.Result = &models.User{Id: userID} + storedRoleInfo = cmd.ExternalUser.OrgRoles + return nil + }) + + sc.fakeReq("GET", "/") + sc.req.Header.Set(sc.cfg.AuthProxyHeaderName, hdrName) + sc.req.Header.Set("X-WEBAUTH-ROLE", "Admin") + sc.req.Header.Set("X-Grafana-Org-Id", strconv.FormatInt(orgID, 10)) + sc.exec() + + assert.True(t, sc.context.IsSignedIn) + assert.Equal(t, userID, sc.context.UserId) + assert.Equal(t, orgID, sc.context.OrgId) + + // For non-default org, the user role should be empty + assert.Equal(t, "", string(sc.context.OrgRole)) + }, func(cfg *setting.Cfg) { + configure(cfg) + cfg.LDAPEnabled = false + cfg.AuthProxyAutoSignUp = true + }) + middlewareScenario(t, "Should get an existing user from header", func(t *testing.T, sc *scenarioContext) { const userID int64 = 12 const orgID int64 = 2 diff --git a/pkg/services/contexthandler/authproxy/authproxy.go b/pkg/services/contexthandler/authproxy/authproxy.go index b7d95b1c99a..adc99ce7eb6 100644 --- a/pkg/services/contexthandler/authproxy/authproxy.go +++ b/pkg/services/contexthandler/authproxy/authproxy.go @@ -45,7 +45,7 @@ var isLDAPEnabled = func(cfg *setting.Cfg) bool { var newLDAP = multildap.New // supportedHeaders states the supported headers configuration fields -var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups"} +var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups", "Role"} // AuthProxy struct type AuthProxy struct { @@ -152,7 +152,7 @@ func HashCacheKey(key string) (string, error) { // getKey forms a key for the cache based on the headers received as part of the authentication flow. // Our configuration supports multiple headers. The main header contains the email or username. -// And the additional ones that allow us to specify extra attributes: Name, Email or Groups. +// And the additional ones that allow us to specify extra attributes: Name, Email, Role, or Groups. func (auth *AuthProxy) getKey() (string, error) { key := strings.TrimSpace(auth.header) // start the key with the main header @@ -278,9 +278,23 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) { } auth.headersIterator(func(field string, header string) { - if field == "Groups" { + switch field { + case "Groups": extUser.Groups = util.SplitString(header) - } else { + case "Role": + // If Role header is specified, we update the user role of the default org + if header != "" { + rt := models.RoleType(header) + if rt.IsValid() { + extUser.OrgRoles = map[int64]models.RoleType{} + orgID := int64(1) + if setting.AutoAssignOrg && setting.AutoAssignOrgId > 0 { + orgID = int64(setting.AutoAssignOrgId) + } + extUser.OrgRoles[orgID] = rt + } + } + default: reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header) } }) diff --git a/pkg/services/contexthandler/authproxy/authproxy_test.go b/pkg/services/contexthandler/authproxy/authproxy_test.go index cb396760057..24fbdd83752 100644 --- a/pkg/services/contexthandler/authproxy/authproxy_test.go +++ b/pkg/services/contexthandler/authproxy/authproxy_test.go @@ -107,8 +107,9 @@ func TestMiddlewareContext(t *testing.T) { t.Run("When the cache key contains additional headers", func(t *testing.T) { const id int64 = 33 const group = "grafana-core-team" + const role = "Admin" - h, err := HashCacheKey(hdrName + "-" + group) + h, err := HashCacheKey(hdrName + "-" + group + "-" + role) require.NoError(t, err) key := fmt.Sprintf(CachePrefix, h) err = cache.Set(key, id, 0) @@ -116,9 +117,10 @@ func TestMiddlewareContext(t *testing.T) { auth := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) { req.Header.Set("X-WEBAUTH-GROUPS", group) - cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS"} + req.Header.Set("X-WEBAUTH-ROLE", role) + cfg.AuthProxyHeaders = map[string]string{"Groups": "X-WEBAUTH-GROUPS", "Role": "X-WEBAUTH-ROLE"} }) - assert.Equal(t, "auth-proxy-sync-ttl:14f69b7023baa0ac98c96b31cec07bc0", key) + assert.Equal(t, "auth-proxy-sync-ttl:f5acfffd56daac98d502ef8c8b8c5d56", key) gotID, err := auth.Login(logger, false) require.NoError(t, err)