mirror of
https://github.com/grafana/grafana.git
synced 2025-09-26 02:44:12 +08:00
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 <leo@xlson.com>
This commit is contained in:
@ -30,7 +30,7 @@ sync_ttl = 60
|
|||||||
# Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120`
|
# Example `whitelist = 192.168.1.1, 192.168.1.0/24, 2001::23, 2001::0/120`
|
||||||
whitelist =
|
whitelist =
|
||||||
# Optionally define more headers to sync other user attributes
|
# 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 =
|
headers =
|
||||||
# Check out docs on this for more details on the below setting
|
# Check out docs on this for more details on the below setting
|
||||||
enable_login_token = false
|
enable_login_token = false
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -349,6 +350,8 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
t.Run("auth_proxy", func(t *testing.T) {
|
t.Run("auth_proxy", func(t *testing.T) {
|
||||||
const userID int64 = 33
|
const userID int64 = 33
|
||||||
const orgID int64 = 4
|
const orgID int64 = 4
|
||||||
|
const defaultOrgId int64 = 1
|
||||||
|
const orgRole = "Admin"
|
||||||
|
|
||||||
configure := func(cfg *setting.Cfg) {
|
configure := func(cfg *setting.Cfg) {
|
||||||
cfg.AuthProxyEnabled = true
|
cfg.AuthProxyEnabled = true
|
||||||
@ -356,7 +359,7 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
cfg.LDAPEnabled = true
|
cfg.LDAPEnabled = true
|
||||||
cfg.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
cfg.AuthProxyHeaderName = "X-WEBAUTH-USER"
|
||||||
cfg.AuthProxyHeaderProperty = "username"
|
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"
|
const hdrName = "markelog"
|
||||||
@ -432,6 +435,71 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
cfg.AuthProxyAutoSignUp = true
|
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) {
|
middlewareScenario(t, "Should get an existing user from header", func(t *testing.T, sc *scenarioContext) {
|
||||||
const userID int64 = 12
|
const userID int64 = 12
|
||||||
const orgID int64 = 2
|
const orgID int64 = 2
|
||||||
|
@ -45,7 +45,7 @@ var isLDAPEnabled = func(cfg *setting.Cfg) bool {
|
|||||||
var newLDAP = multildap.New
|
var newLDAP = multildap.New
|
||||||
|
|
||||||
// supportedHeaders states the supported headers configuration fields
|
// supportedHeaders states the supported headers configuration fields
|
||||||
var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups"}
|
var supportedHeaderFields = []string{"Name", "Email", "Login", "Groups", "Role"}
|
||||||
|
|
||||||
// AuthProxy struct
|
// AuthProxy struct
|
||||||
type 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.
|
// 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.
|
// 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) {
|
func (auth *AuthProxy) getKey() (string, error) {
|
||||||
key := strings.TrimSpace(auth.header) // start the key with the main header
|
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) {
|
auth.headersIterator(func(field string, header string) {
|
||||||
if field == "Groups" {
|
switch field {
|
||||||
|
case "Groups":
|
||||||
extUser.Groups = util.SplitString(header)
|
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)
|
reflect.ValueOf(extUser).Elem().FieldByName(field).SetString(header)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -107,8 +107,9 @@ func TestMiddlewareContext(t *testing.T) {
|
|||||||
t.Run("When the cache key contains additional headers", func(t *testing.T) {
|
t.Run("When the cache key contains additional headers", func(t *testing.T) {
|
||||||
const id int64 = 33
|
const id int64 = 33
|
||||||
const group = "grafana-core-team"
|
const group = "grafana-core-team"
|
||||||
|
const role = "Admin"
|
||||||
|
|
||||||
h, err := HashCacheKey(hdrName + "-" + group)
|
h, err := HashCacheKey(hdrName + "-" + group + "-" + role)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
key := fmt.Sprintf(CachePrefix, h)
|
key := fmt.Sprintf(CachePrefix, h)
|
||||||
err = cache.Set(key, id, 0)
|
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) {
|
auth := prepareMiddleware(t, cache, func(req *http.Request, cfg *setting.Cfg) {
|
||||||
req.Header.Set("X-WEBAUTH-GROUPS", group)
|
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)
|
gotID, err := auth.Login(logger, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
Reference in New Issue
Block a user