AuthN: add flag for org roles sync (#63507)

* AuthN: Add flag to control org role syncs

* JWT: Only sync org roles if the skip flag for jwt is false

* LDAP: Only sync org role if skip flag for ldap is false

* OAuth: Skip org roles sync if no roles were provided by upstream service

* Grafana: Set SyncOrgRoles to true for authentication through proxy with grafana as backend
This commit is contained in:
Karl Persson
2023-02-22 10:27:48 +01:00
committed by GitHub
parent 1e84d5d93c
commit 207a55be66
12 changed files with 92 additions and 74 deletions

View File

@@ -47,6 +47,8 @@ type ClientParams struct {
FetchSyncedUser bool FetchSyncedUser bool
// SyncTeams will sync the groups from identity to teams in grafana, enterprise only feature // SyncTeams will sync the groups from identity to teams in grafana, enterprise only feature
SyncTeams bool SyncTeams bool
// SyncOrgRoles will sync the roles from the identity to orgs in grafana
SyncOrgRoles bool
// CacheAuthProxyKey if this key is set we will try to cache the user id for proxy client // CacheAuthProxyKey if this key is set we will try to cache the user id for proxy client
CacheAuthProxyKey string CacheAuthProxyKey string
// LookUpParams are the arguments used to look up the entity in the DB. // LookUpParams are the arguments used to look up the entity in the DB.

View File

@@ -25,7 +25,7 @@ type OrgSync struct {
} }
func (s *OrgSync) SyncOrgRolesHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error { func (s *OrgSync) SyncOrgRolesHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error {
if !id.ClientParams.SyncUser { if !id.ClientParams.SyncOrgRoles {
return nil return nil
} }

View File

@@ -79,7 +79,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) {
OrgRoles: map[int64]roletype.RoleType{1: org.RoleAdmin, 2: org.RoleEditor}, OrgRoles: map[int64]roletype.RoleType{1: org.RoleAdmin, 2: org.RoleEditor},
IsGrafanaAdmin: ptrBool(false), IsGrafanaAdmin: ptrBool(false),
ClientParams: authn.ClientParams{ ClientParams: authn.ClientParams{
SyncUser: true, SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{ LookUpParams: login.UserLookupParams{
UserID: nil, UserID: nil,
Email: ptrString("test"), Email: ptrString("test"),
@@ -97,7 +97,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) {
OrgID: 1, //set using org OrgID: 1, //set using org
IsGrafanaAdmin: ptrBool(false), IsGrafanaAdmin: ptrBool(false),
ClientParams: authn.ClientParams{ ClientParams: authn.ClientParams{
SyncUser: true, SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{ LookUpParams: login.UserLookupParams{
UserID: nil, UserID: nil,
Email: ptrString("test"), Email: ptrString("test"),

View File

@@ -38,6 +38,7 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern
SyncUser: true, SyncUser: true,
SyncTeams: true, SyncTeams: true,
FetchSyncedUser: true, FetchSyncedUser: true,
SyncOrgRoles: true,
AllowSignUp: c.cfg.AuthProxyAutoSignUp, AllowSignUp: c.cfg.AuthProxyAutoSignUp,
}, },
} }
@@ -69,15 +70,11 @@ func (c *Grafana) AuthenticateProxy(ctx context.Context, r *authn.Request, usern
} }
if v, ok := additional[proxyFieldRole]; ok { if v, ok := additional[proxyFieldRole]; ok {
role := org.RoleType(v) orgRoles, isGrafanaAdmin, _ := getRoles(c.cfg, func() (org.RoleType, *bool, error) {
if role.IsValid() { return org.RoleType(v), nil, nil
orgID := int64(1) })
if c.cfg.AutoAssignOrg && c.cfg.AutoAssignOrgId > 0 { identity.OrgRoles = orgRoles
orgID = int64(c.cfg.AutoAssignOrgId) identity.IsGrafanaAdmin = isGrafanaAdmin
}
identity.OrgID = orgID
identity.OrgRoles = map[int64]org.RoleType{orgID: role}
}
} }
if v, ok := additional[proxyFieldGroups]; ok { if v, ok := additional[proxyFieldGroups]; ok {

View File

@@ -40,7 +40,6 @@ func TestGrafana_AuthenticateProxy(t *testing.T) {
proxyFieldEmail: "email@email.com", proxyFieldEmail: "email@email.com",
}, },
expectedIdentity: &authn.Identity{ expectedIdentity: &authn.Identity{
OrgID: 1,
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Login: "test", Login: "test",
Name: "name", Name: "name",
@@ -53,6 +52,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) {
SyncTeams: true, SyncTeams: true,
AllowSignUp: true, AllowSignUp: true,
FetchSyncedUser: true, FetchSyncedUser: true,
SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{ LookUpParams: login.UserLookupParams{
Email: strPtr("email@email.com"), Email: strPtr("email@email.com"),
Login: strPtr("test"), Login: strPtr("test"),
@@ -74,6 +74,7 @@ func TestGrafana_AuthenticateProxy(t *testing.T) {
SyncUser: true, SyncUser: true,
SyncTeams: true, SyncTeams: true,
AllowSignUp: true, AllowSignUp: true,
SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{ LookUpParams: login.UserLookupParams{
Email: strPtr("test@test.com"), Email: strPtr("test@test.com"),
Login: strPtr("test@test.com"), Login: strPtr("test@test.com"),

View File

@@ -10,7 +10,6 @@ import (
"github.com/jmespath/go-jmespath" "github.com/jmespath/go-jmespath"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" authJWT "github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn"
@@ -23,11 +22,11 @@ import (
var _ authn.ContextAwareClient = new(JWT) var _ authn.ContextAwareClient = new(JWT)
var ( var (
ErrJWTInvalid = errutil.NewBase(errutil.StatusUnauthorized, errJWTInvalid = errutil.NewBase(errutil.StatusUnauthorized,
"jwt.invalid", errutil.WithPublicMessage("Failed to verify JWT")) "jwt.invalid", errutil.WithPublicMessage("Failed to verify JWT"))
ErrJWTMissingClaim = errutil.NewBase(errutil.StatusUnauthorized, errJWTMissingClaim = errutil.NewBase(errutil.StatusUnauthorized,
"jwt.missing_claim", errutil.WithPublicMessage("Missing mandatory claim in JWT")) "jwt.missing_claim", errutil.WithPublicMessage("Missing mandatory claim in JWT"))
ErrJWTInvalidRole = errutil.NewBase(errutil.StatusForbidden, errJWTInvalidRole = errutil.NewBase(errutil.StatusForbidden,
"jwt.invalid_role", errutil.WithPublicMessage("Invalid Role in claim")) "jwt.invalid_role", errutil.WithPublicMessage("Invalid Role in claim"))
) )
@@ -55,13 +54,13 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
claims, err := s.jwtService.Verify(ctx, jwtToken) claims, err := s.jwtService.Verify(ctx, jwtToken)
if err != nil { if err != nil {
s.log.Debug("Failed to verify JWT", "error", err) s.log.Debug("Failed to verify JWT", "error", err)
return nil, ErrJWTInvalid.Errorf("failed to verify JWT: %w", err) return nil, errJWTInvalid.Errorf("failed to verify JWT: %w", err)
} }
sub, _ := claims["sub"].(string) sub, _ := claims["sub"].(string)
if sub == "" { if sub == "" {
s.log.Warn("Got a JWT without the mandatory 'sub' claim", "error", err) s.log.Warn("Got a JWT without the mandatory 'sub' claim", "error", err)
return nil, ErrJWTMissingClaim.Errorf("missing mandatory 'sub' claim in JWT") return nil, errJWTMissingClaim.Errorf("missing mandatory 'sub' claim in JWT")
} }
id := &authn.Identity{ id := &authn.Identity{
@@ -71,6 +70,7 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
ClientParams: authn.ClientParams{ ClientParams: authn.ClientParams{
SyncUser: true, SyncUser: true,
FetchSyncedUser: true, FetchSyncedUser: true,
SyncOrgRoles: !s.cfg.JWTAuthSkipOrgRoleSync,
AllowSignUp: s.cfg.JWTAuthAutoSignUp, AllowSignUp: s.cfg.JWTAuthAutoSignUp,
}} }}
@@ -87,41 +87,34 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi
id.Name = name id.Name = name
} }
var role roletype.RoleType orgRoles, isGrafanaAdmin, err := getRoles(s.cfg, func() (org.RoleType, *bool, error) {
var grafanaAdmin bool if s.cfg.JWTAuthSkipOrgRoleSync {
if !s.cfg.JWTAuthSkipOrgRoleSync { return "", nil, nil
role, grafanaAdmin = s.extractRoleAndAdmin(claims) }
role, grafanaAdmin := s.extractRoleAndAdmin(claims)
if s.cfg.JWTAuthRoleAttributeStrict && !role.IsValid() { if s.cfg.JWTAuthRoleAttributeStrict && !role.IsValid() {
s.log.Warn("extracted Role is invalid", "role", role, "auth_id", id.AuthID) return "", nil, errJWTInvalidRole.Errorf("invalid role claim in JWT: %s", role)
return nil, ErrJWTInvalidRole.Errorf("invalid role claim in JWT: %s", role)
} }
if role.IsValid() { if !s.cfg.JWTAuthAllowAssignGrafanaAdmin {
var orgID int64 return role, nil, nil
// FIXME (jguer): GetIDForNewUser already has the auto assign information
// just needs the org role. Find a meaningful way to pass this default
// role to it (that doesn't involve id.OrgRoles[0] = role)
if s.cfg.AutoAssignOrg && s.cfg.AutoAssignOrgId > 0 {
orgID = int64(s.cfg.AutoAssignOrgId)
s.log.Debug("The user has a role assignment and organization membership is auto-assigned",
"role", role, "orgId", orgID)
} else {
orgID = int64(1)
s.log.Debug("The user has a role assignment and organization membership is not auto-assigned",
"role", role, "orgId", orgID)
} }
id.OrgRoles[orgID] = role return role, &grafanaAdmin, nil
if s.cfg.JWTAuthAllowAssignGrafanaAdmin { })
id.IsGrafanaAdmin = &grafanaAdmin
} if err != nil {
} return nil, err
} }
id.OrgRoles = orgRoles
id.IsGrafanaAdmin = isGrafanaAdmin
if id.Login == "" && id.Email == "" { if id.Login == "" && id.Email == "" {
s.log.Debug("Failed to get an authentication claim from JWT", s.log.Debug("Failed to get an authentication claim from JWT",
"login", id.Login, "email", id.Email) "login", id.Login, "email", id.Email)
return nil, ErrJWTMissingClaim.Errorf("missing login and email claim in JWT") return nil, errJWTMissingClaim.Errorf("missing login and email claim in JWT")
} }
return id, nil return id, nil

View File

@@ -52,6 +52,7 @@ func TestAuthenticateJWT(t *testing.T) {
SyncUser: true, SyncUser: true,
AllowSignUp: true, AllowSignUp: true,
FetchSyncedUser: true, FetchSyncedUser: true,
SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{ LookUpParams: login.UserLookupParams{
UserID: nil, UserID: nil,
Email: stringPtr("eai.doe@cor.po"), Email: stringPtr("eai.doe@cor.po"),

View File

@@ -41,7 +41,7 @@ func (c *LDAP) AuthenticateProxy(ctx context.Context, r *authn.Request, username
return nil, err return nil, err
} }
return identityFromLDAPInfo(r.OrgID, info, c.cfg.LDAPAllowSignup), nil return c.identityFromLDAPInfo(r.OrgID, info), nil
} }
func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) { func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, username, password string) (*authn.Identity, error) {
@@ -66,10 +66,10 @@ func (c *LDAP) AuthenticatePassword(ctx context.Context, r *authn.Request, usern
return nil, err return nil, err
} }
return identityFromLDAPInfo(r.OrgID, info, c.cfg.LDAPAllowSignup), nil return c.identityFromLDAPInfo(r.OrgID, info), nil
} }
func identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo, allowSignup bool) *authn.Identity { func (c *LDAP) identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo) *authn.Identity {
return &authn.Identity{ return &authn.Identity{
OrgID: orgID, OrgID: orgID,
OrgRoles: info.OrgRoles, OrgRoles: info.OrgRoles,
@@ -85,7 +85,8 @@ func identityFromLDAPInfo(orgID int64, info *login.ExternalUserInfo, allowSignup
SyncTeams: true, SyncTeams: true,
EnableDisabledUsers: true, EnableDisabledUsers: true,
FetchSyncedUser: true, FetchSyncedUser: true,
AllowSignUp: allowSignup, SyncOrgRoles: !c.cfg.LDAPSkipOrgRoleSync,
AllowSignUp: c.cfg.LDAPAllowSignup,
LookUpParams: login.UserLookupParams{ LookUpParams: login.UserLookupParams{
Login: &info.Login, Login: &info.Login,
Email: &info.Email, Email: &info.Email,

View File

@@ -52,6 +52,7 @@ func TestLDAP_AuthenticateProxy(t *testing.T) {
SyncTeams: true, SyncTeams: true,
EnableDisabledUsers: true, EnableDisabledUsers: true,
FetchSyncedUser: true, FetchSyncedUser: true,
SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{ LookUpParams: login.UserLookupParams{
Email: strPtr("test@test.com"), Email: strPtr("test@test.com"),
Login: strPtr("test"), Login: strPtr("test"),
@@ -116,6 +117,7 @@ func TestLDAP_AuthenticatePassword(t *testing.T) {
SyncTeams: true, SyncTeams: true,
EnableDisabledUsers: true, EnableDisabledUsers: true,
FetchSyncedUser: true, FetchSyncedUser: true,
SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{ LookUpParams: login.UserLookupParams{
Email: strPtr("test@test.com"), Email: strPtr("test@test.com"),
Login: strPtr("test"), Login: strPtr("test"),

View File

@@ -123,21 +123,30 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
return nil, errOAuthEmailNotAllowed.Errorf("provided email is not allowed") return nil, errOAuthEmailNotAllowed.Errorf("provided email is not allowed")
} }
orgRoles, isGrafanaAdmin, _ := getRoles(c.cfg, func() (org.RoleType, *bool, error) {
if c.cfg.OAuthSkipOrgRoleUpdateSync {
return "", nil, nil
}
return userInfo.Role, userInfo.IsGrafanaAdmin, nil
})
return &authn.Identity{ return &authn.Identity{
Login: userInfo.Login, Login: userInfo.Login,
Name: userInfo.Name, Name: userInfo.Name,
Email: userInfo.Email, Email: userInfo.Email,
IsGrafanaAdmin: userInfo.IsGrafanaAdmin, IsGrafanaAdmin: isGrafanaAdmin,
AuthModule: c.moduleName, AuthModule: c.moduleName,
AuthID: userInfo.Id, AuthID: userInfo.Id,
Groups: userInfo.Groups, Groups: userInfo.Groups,
OAuthToken: token, OAuthToken: token,
OrgRoles: getOAuthOrgRole(userInfo, c.cfg), OrgRoles: orgRoles,
ClientParams: authn.ClientParams{ ClientParams: authn.ClientParams{
SyncUser: true, SyncUser: true,
SyncTeams: true, SyncTeams: true,
FetchSyncedUser: true, FetchSyncedUser: true,
AllowSignUp: c.connector.IsSignupAllowed(), AllowSignUp: c.connector.IsSignupAllowed(),
// skip org role flag is checked and handled in the connector. For now we can skip the hook if no roles are passed
SyncOrgRoles: len(orgRoles) > 0,
LookUpParams: login.UserLookupParams{Email: &userInfo.Email}, LookUpParams: login.UserLookupParams{Email: &userInfo.Email},
}, },
}, nil }, nil
@@ -217,22 +226,3 @@ func hashOAuthState(state, secret, seed string) string {
hashBytes := sha256.Sum256([]byte(state + secret + seed)) hashBytes := sha256.Sum256([]byte(state + secret + seed))
return hex.EncodeToString(hashBytes[:]) return hex.EncodeToString(hashBytes[:])
} }
func getOAuthOrgRole(userInfo *social.BasicUserInfo, cfg *setting.Cfg) map[int64]org.RoleType {
orgRoles := make(map[int64]org.RoleType, 0)
if cfg.OAuthSkipOrgRoleUpdateSync {
return orgRoles
}
if userInfo.Role == "" || !userInfo.Role.IsValid() {
return orgRoles
}
orgID := int64(1)
if cfg.AutoAssignOrg && cfg.AutoAssignOrgId > 0 {
orgID = int64(cfg.AutoAssignOrgId)
}
orgRoles[orgID] = userInfo.Role
return orgRoles
}

View File

@@ -139,6 +139,7 @@ func TestOAuth_Authenticate(t *testing.T) {
SyncTeams: true, SyncTeams: true,
AllowSignUp: true, AllowSignUp: true,
FetchSyncedUser: true, FetchSyncedUser: true,
SyncOrgRoles: true,
LookUpParams: login.UserLookupParams{Email: strPtr("some@email.com")}, LookUpParams: login.UserLookupParams{Email: strPtr("some@email.com")},
}, },
}, },

View File

@@ -0,0 +1,30 @@
package clients
import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
)
// roleExtractor should return the org role, optional isGrafanaAdmin or an error
type roleExtractor func() (org.RoleType, *bool, error)
// getRoles only handles one org role for now, could be subject to change
func getRoles(cfg *setting.Cfg, extract roleExtractor) (map[int64]org.RoleType, *bool, error) {
role, isGrafanaAdmin, err := extract()
orgRoles := make(map[int64]org.RoleType, 0)
if err != nil {
return orgRoles, nil, err
}
if role == "" || !role.IsValid() {
return orgRoles, nil, nil
}
orgID := int64(1)
if cfg.AutoAssignOrg && cfg.AutoAssignOrgId > 0 {
orgID = int64(cfg.AutoAssignOrgId)
}
orgRoles[orgID] = role
return orgRoles, isGrafanaAdmin, nil
}