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:
Karl Persson
2024-06-07 14:07:35 +02:00
committed by GitHub
parent 5095ea84b2
commit f3efd95bb4
7 changed files with 148 additions and 99 deletions

View File

@@ -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))

View File

@@ -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())

View File

@@ -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 != "" {

View File

@@ -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) {