mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Add org to role mappings support to Google integration (#88891)
* Auth: Implement org role mapping for google oauth provider * Update docs * Remove unused function Co-authored-by: Misi <mgyongyosi@users.noreply.github.com>
This commit is contained in:
parent
5095ea84b2
commit
f3efd95bb4
@ -705,6 +705,7 @@ hosted_domain =
|
||||
allowed_groups =
|
||||
role_attribute_path =
|
||||
role_attribute_strict = false
|
||||
org_mapping =
|
||||
allow_assign_grafana_admin = false
|
||||
skip_org_role_sync = true
|
||||
tls_skip_verify_insecure = false
|
||||
|
@ -659,6 +659,7 @@
|
||||
;allowed_groups =
|
||||
;role_attribute_path =
|
||||
;role_attribute_strict = false
|
||||
;org_mapping =
|
||||
;allow_assign_grafana_admin = false
|
||||
;skip_org_role_sync = false
|
||||
;use_pkce = true
|
||||
|
@ -201,8 +201,7 @@ The user's role is retrieved using a [JMESPath](http://jmespath.org/examples.htm
|
||||
To map the server administrator role, use the `allow_assign_grafana_admin` configuration option.
|
||||
|
||||
If no valid role is found, the user is assigned the role specified by [the `auto_assign_org_role` option]({{< relref "../../../configure-grafana#auto_assign_org_role" >}}).
|
||||
You can disable this default role assignment by setting `role_attribute_strict = true`.
|
||||
This setting denies user access if no role or an invalid role is returned.
|
||||
You can disable this default role assignment by setting `role_attribute_strict = true`. This setting denies user access if no role or an invalid role is returned after evaluating the `role_attribute_path` and the `org_mapping` expressions.
|
||||
|
||||
To ease configuration of a proper JMESPath expression, go to [JMESPath](http://jmespath.org/) to test and evaluate expressions with custom payloads.
|
||||
|
||||
@ -212,6 +211,20 @@ To ease configuration of a proper JMESPath expression, go to [JMESPath](http://j
|
||||
|
||||
This section includes examples of JMESPath expressions used for role mapping.
|
||||
|
||||
##### Org roles mapping example
|
||||
|
||||
The Google integration uses the external users' groups in the `org_mapping` configuration to map organizations and roles based on their Google group membership.
|
||||
|
||||
In this example, the user has been granted the role of a `Viewer` in the `org_foo` organization, and the role of an `Editor` in the `org_bar` and `org_baz` orgs.
|
||||
|
||||
The external user is part of the following Google groups: `group-1` and `group-2`.
|
||||
|
||||
Config:
|
||||
|
||||
```ini
|
||||
org_mapping = group-1:org_foo:Viewer group-2:org_bar:Editor *:org_baz:Editor
|
||||
```
|
||||
|
||||
###### Map roles using user information from OAuth token
|
||||
|
||||
In this example, the user with email `admin@company.com` has been granted the `Admin` role.
|
||||
|
@ -140,26 +140,31 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
|
||||
}
|
||||
|
||||
userInfo := &social.BasicUserInfo{
|
||||
Id: data.ID,
|
||||
Name: data.Name,
|
||||
Email: data.Email,
|
||||
Login: data.Email,
|
||||
Role: "",
|
||||
IsGrafanaAdmin: nil,
|
||||
Groups: groups,
|
||||
Id: data.ID,
|
||||
Name: data.Name,
|
||||
Email: data.Email,
|
||||
Login: data.Email,
|
||||
Groups: groups,
|
||||
}
|
||||
|
||||
if s.info.AllowAssignGrafanaAdmin && s.info.SkipOrgRoleSync {
|
||||
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
|
||||
}
|
||||
|
||||
if !s.info.SkipOrgRoleSync {
|
||||
role, grafanaAdmin, errRole := s.extractRoleAndAdmin(data.rawJSON, groups)
|
||||
if errRole != nil {
|
||||
return nil, errRole
|
||||
directlyMappedRole, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, userInfo.Groups)
|
||||
if err != nil {
|
||||
s.log.Warn("Failed to extract role", "err", err)
|
||||
}
|
||||
|
||||
if s.info.AllowAssignGrafanaAdmin {
|
||||
userInfo.IsGrafanaAdmin = &grafanaAdmin
|
||||
}
|
||||
|
||||
userInfo.Role = role
|
||||
userInfo.OrgRoles = s.orgRoleMapper.MapOrgRoles(s.orgMappingCfg, userInfo.Groups, directlyMappedRole)
|
||||
if s.info.RoleAttributeStrict && len(userInfo.OrgRoles) == 0 {
|
||||
return nil, errRoleAttributeStrictViolation.Errorf("could not evaluate any valid roles using IdP provided data")
|
||||
}
|
||||
}
|
||||
|
||||
s.log.Debug("Resolved user info", "data", fmt.Sprintf("%+v", userInfo))
|
||||
|
@ -15,9 +15,10 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/login/social"
|
||||
"github.com/grafana/grafana/pkg/models/roletype"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/org"
|
||||
"github.com/grafana/grafana/pkg/services/org/orgtest"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings"
|
||||
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
|
||||
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
|
||||
@ -224,6 +225,21 @@ func (f *roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
return f.fn(req)
|
||||
}
|
||||
|
||||
const googleGroupsJSON = `
|
||||
{
|
||||
"memberships": [
|
||||
{
|
||||
"group": "test-group",
|
||||
"groupKey": {
|
||||
"id": "test-group@google.com"
|
||||
},
|
||||
"displayName": "Test Group"
|
||||
}
|
||||
],
|
||||
"nextPageToken": ""
|
||||
}
|
||||
`
|
||||
|
||||
func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
cl := jwt.Claims{
|
||||
Subject: "88888888888888",
|
||||
@ -250,6 +266,16 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
|
||||
tokenWithoutID := &oauth2.Token{}
|
||||
|
||||
groupClient := &http.Client{
|
||||
Transport: &roundTripperFunc{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
resp := httptest.NewRecorder()
|
||||
_, _ = resp.WriteString(googleGroupsJSON)
|
||||
return resp.Result(), nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type fields struct {
|
||||
Scopes []string
|
||||
apiURL string
|
||||
@ -257,6 +283,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
roleAttributePath string
|
||||
roleAttributeStrict bool
|
||||
allowAssignGrafanaAdmin bool
|
||||
orgMapping []string
|
||||
skipOrgRoleSync bool
|
||||
}
|
||||
type args struct {
|
||||
@ -295,27 +322,8 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
skipOrgRoleSync: true,
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithID,
|
||||
client: &http.Client{
|
||||
Transport: &roundTripperFunc{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
resp := httptest.NewRecorder()
|
||||
_, _ = resp.WriteString(`{
|
||||
"memberships": [
|
||||
{
|
||||
"group": "test-group",
|
||||
"groupKey": {
|
||||
"id": "test-group@google.com"
|
||||
},
|
||||
"displayName": "Test Group"
|
||||
}
|
||||
],
|
||||
"nextPageToken": ""
|
||||
}`)
|
||||
return resp.Result(), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
token: tokenWithID,
|
||||
client: groupClient,
|
||||
},
|
||||
wantData: &social.BasicUserInfo{
|
||||
Id: "88888888888888",
|
||||
@ -507,27 +515,8 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
allowedGroups: []string{"not-that-one"},
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithID,
|
||||
client: &http.Client{
|
||||
Transport: &roundTripperFunc{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
resp := httptest.NewRecorder()
|
||||
_, _ = resp.WriteString(`{
|
||||
"memberships": [
|
||||
{
|
||||
"group": "test-group",
|
||||
"groupKey": {
|
||||
"id": "test-group@google.com"
|
||||
},
|
||||
"displayName": "Test Group"
|
||||
}
|
||||
],
|
||||
"nextPageToken": ""
|
||||
}`)
|
||||
return resp.Result(), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
token: tokenWithID,
|
||||
client: groupClient,
|
||||
},
|
||||
wantData: &social.BasicUserInfo{
|
||||
Id: "88888888888888",
|
||||
@ -558,7 +547,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
Groups: []string{"test-group@google.com"},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrMsg: "idP did not return a role attribute, but role_attribute_strict is set",
|
||||
wantErrMsg: "[oauth.role_attribute_strict_violation] could not evaluate any valid roles using IdP provided data",
|
||||
},
|
||||
{
|
||||
name: "role mapping from id_token - no allowed assign Grafana Admin",
|
||||
@ -575,7 +564,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
Login: "test@example.com",
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
Role: roletype.RoleAdmin,
|
||||
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||
IsGrafanaAdmin: nil,
|
||||
},
|
||||
wantErr: false,
|
||||
@ -595,7 +584,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
Login: "test@example.com",
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
Role: roletype.RoleAdmin,
|
||||
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
|
||||
IsGrafanaAdmin: trueBoolPtr(),
|
||||
},
|
||||
wantErr: false,
|
||||
@ -607,55 +596,103 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
|
||||
roleAttributePath: "contains(groups[*], 'test-group@google.com') && 'Editor'",
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithID,
|
||||
client: &http.Client{
|
||||
Transport: &roundTripperFunc{
|
||||
fn: func(req *http.Request) (*http.Response, error) {
|
||||
resp := httptest.NewRecorder()
|
||||
_, _ = resp.WriteString(`{
|
||||
"memberships": [
|
||||
{
|
||||
"group": "test-group",
|
||||
"groupKey": {
|
||||
"id": "test-group@google.com"
|
||||
},
|
||||
"displayName": "Test Group"
|
||||
}
|
||||
],
|
||||
"nextPageToken": ""
|
||||
}`)
|
||||
return resp.Result(), nil
|
||||
},
|
||||
},
|
||||
},
|
||||
token: tokenWithID,
|
||||
client: groupClient,
|
||||
},
|
||||
wantData: &social.BasicUserInfo{
|
||||
Id: "88888888888888",
|
||||
Login: "test@example.com",
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
Role: "Editor",
|
||||
Groups: []string{"test-group@google.com"},
|
||||
Id: "88888888888888",
|
||||
Login: "test@example.com",
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
|
||||
Groups: []string{"test-group@google.com"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "mapping from groups",
|
||||
fields: fields{
|
||||
Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"},
|
||||
roleAttributePath: "contains(groups[*], 'test-group@google.com') && 'Editor'",
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithID,
|
||||
client: groupClient,
|
||||
},
|
||||
wantData: &social.BasicUserInfo{
|
||||
Id: "88888888888888",
|
||||
Login: "test@example.com",
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
|
||||
Groups: []string{"test-group@google.com"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Should map role when only org mapping is set",
|
||||
fields: fields{
|
||||
Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"},
|
||||
orgMapping: []string{"test-group@google.com:Org4:Editor", "*:Org5:Viewer"},
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithID,
|
||||
client: groupClient,
|
||||
},
|
||||
wantData: &social.BasicUserInfo{
|
||||
Id: "88888888888888",
|
||||
Login: "test@example.com",
|
||||
Email: "test@example.com",
|
||||
Name: "Test User",
|
||||
OrgRoles: map[int64]org.RoleType{4: org.RoleEditor, 5: org.RoleViewer},
|
||||
Groups: []string{"test-group@google.com"},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Should return error when neither role attribute path nor org mapping evaluates to a role and role attribute strict is enabled",
|
||||
fields: fields{
|
||||
Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"},
|
||||
orgMapping: []string{"other@google.com:Org4:Editor"},
|
||||
roleAttributeStrict: true,
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithID,
|
||||
client: groupClient,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Should return error when neither role attribute path nor org mapping is set and role attribute strict is enabled",
|
||||
fields: fields{
|
||||
Scopes: []string{"https://www.googleapis.com/auth/cloud-identity.groups.readonly"},
|
||||
roleAttributeStrict: true,
|
||||
},
|
||||
args: args{
|
||||
token: tokenWithID,
|
||||
client: groupClient,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
|
||||
s := NewGoogleProvider(
|
||||
&social.OAuthInfo{
|
||||
ApiUrl: tt.fields.apiURL,
|
||||
Scopes: tt.fields.Scopes,
|
||||
AllowedGroups: tt.fields.allowedGroups,
|
||||
AllowSignup: false,
|
||||
RoleAttributePath: tt.fields.roleAttributePath,
|
||||
RoleAttributeStrict: tt.fields.roleAttributeStrict,
|
||||
AllowAssignGrafanaAdmin: tt.fields.allowAssignGrafanaAdmin,
|
||||
SkipOrgRoleSync: tt.fields.skipOrgRoleSync,
|
||||
OrgMapping: tt.fields.orgMapping,
|
||||
},
|
||||
&setting.Cfg{},
|
||||
nil,
|
||||
cfg,
|
||||
ProvideOrgRoleMapper(cfg, &orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}),
|
||||
&ssosettingstests.MockService{},
|
||||
featuremgmt.WithFeatures())
|
||||
|
||||
|
@ -153,15 +153,6 @@ func (s *SocialBase) extractRoleAndAdminOptional(rawJSON []byte, groups []string
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string) (org.RoleType, bool, error) {
|
||||
role, gAdmin, err := s.extractRoleAndAdminOptional(rawJSON, groups)
|
||||
if role == "" {
|
||||
role = s.defaultRole()
|
||||
}
|
||||
|
||||
return role, gAdmin, err
|
||||
}
|
||||
|
||||
func (s *SocialBase) searchRole(rawJSON []byte, groups []string) (org.RoleType, bool) {
|
||||
role, err := util.SearchJSONForStringAttr(s.info.RoleAttributePath, rawJSON)
|
||||
if err == nil && role != "" {
|
||||
|
@ -168,7 +168,8 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
|
||||
|
||||
// This is required to implement OrgRole mapping for OAuth providers step by step
|
||||
switch c.providerName {
|
||||
case social.GenericOAuthProviderName, social.GitHubProviderName, social.GitlabProviderName, social.OktaProviderName:
|
||||
case social.GenericOAuthProviderName, social.GitHubProviderName,
|
||||
social.GitlabProviderName, social.OktaProviderName, social.GoogleProviderName:
|
||||
// Do nothing, these providers already supports OrgRole mapping
|
||||
default:
|
||||
userInfo.OrgRoles, userInfo.IsGrafanaAdmin, _ = getRoles(c.cfg, func() (org.RoleType, *bool, error) {
|
||||
|
Loading…
Reference in New Issue
Block a user