Auth: Add org to role mappings support to GitHub integration (#88537)

* wip: add extra tests for verifying current logic

* Add OrgRole mapping and tests

* Update docs

* Clean up

* Update docs/sources/setup-grafana/configure-security/configure-authentication/github/index.md

Co-authored-by: Mihai Doarna <mihai.doarna@grafana.com>

* Update docs with None role

* Apply suggestions from code review

Co-authored-by: Jack Baldry <jack.baldry@grafana.com>

* Fix

* Prettier docs

* Cleanup tests

---------

Co-authored-by: Mihai Doarna <mihai.doarna@grafana.com>
Co-authored-by: Jack Baldry <jack.baldry@grafana.com>
This commit is contained in:
Misi
2024-06-03 14:24:58 +02:00
committed by GitHub
co-authored by Mihai Doarna Jack Baldry
parent e147c58a7b
commit eacf6e0a4d
11 changed files with 258 additions and 185 deletions
+25 -25
View File
@@ -13,9 +13,9 @@ 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/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/validation"
@@ -307,37 +307,37 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token
return nil, fmt.Errorf("error getting user teams: %s", err)
}
teams := convertToGroupList(teamMemberships)
var role roletype.RoleType
var isGrafanaAdmin *bool = nil
if !s.info.SkipOrgRoleSync {
var grafanaAdmin bool
role, grafanaAdmin, err = s.extractRoleAndAdmin(response.Body, teams)
if err != nil {
return nil, err
}
if s.info.AllowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin
}
userInfo := &social.BasicUserInfo{
Name: data.Login,
Login: data.Login,
Id: fmt.Sprintf("%d", data.Id),
Email: data.Email,
Groups: convertToGroupList(teamMemberships),
}
// we skip allowing assignment of GrafanaAdmin if skipOrgRoleSync is present
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")
}
userInfo := &social.BasicUserInfo{
Name: data.Login,
Login: data.Login,
Id: fmt.Sprintf("%d", data.Id),
Email: data.Email,
Role: role,
Groups: teams,
IsGrafanaAdmin: isGrafanaAdmin,
var directlyMappedRole org.RoleType
if !s.info.SkipOrgRoleSync {
var grafanaAdmin bool
directlyMappedRole, grafanaAdmin, err = s.extractRoleAndAdminOptional(response.Body, userInfo.Groups)
if err != nil {
s.log.Warn("Failed to extract role", "err", err)
}
if s.info.AllowAssignGrafanaAdmin {
userInfo.IsGrafanaAdmin = &grafanaAdmin
}
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")
}
}
if data.Name != "" {
userInfo.Name = data.Name
}
+133 -71
View File
@@ -4,7 +4,6 @@ import (
"context"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
@@ -14,6 +13,8 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"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"
@@ -142,69 +143,76 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
settingAllowGrafanaAdmin bool
settingSkipOrgRoleSync bool
roleAttributePath string
autoAssignOrgRole string
roleAttributeStrict bool
orgMapping []string
want *social.BasicUserInfo
wantErr bool
oAuthExtraInfo map[string]string
}{
{
name: "Basic User info",
name: "should return default role if no role attribute path is set",
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
autoAssignOrgRole: "",
roleAttributePath: "",
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "Viewer",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
OrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
name: "Admin mapping takes precedence over auto assign org role",
roleAttributePath: "[login==octocat] && 'Admin' || 'Viewer'",
userRawJSON: testGHUserJSON,
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
name: "should fail when role attribute path is empty and role attribute strict is enabled",
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
roleAttributePath: "",
roleAttributeStrict: true,
wantErr: true,
},
{
name: "admin mapping takes precedence over auto assign org role",
roleAttributePath: "[login==octocat] && 'Admin' || 'Viewer'",
userRawJSON: testGHUserJSON,
settingAutoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "Admin",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
name: "Editor mapping via groups",
name: "should map role when role attribute path is set",
roleAttributePath: "contains(groups[*], '@github/justice-league') && 'Editor' || 'Viewer'",
userRawJSON: testGHUserJSON,
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "Editor",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
name: "Should be empty role if setting skipOrgRoleSync is set to true",
name: "should return empty role when skip org role sync is true",
roleAttributePath: "contains(groups[*], '@github/justice-league') && 'Editor' || 'Viewer'",
settingSkipOrgRoleSync: true,
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
OrgRoles: nil,
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
@@ -219,45 +227,89 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "",
OrgRoles: nil,
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
IsGrafanaAdmin: boolPointer,
},
},
{
name: "fallback to default org role",
roleAttributePath: "",
userRawJSON: testGHUserJSON,
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
name: "should fallback to default org role when role attribute path is empty and auto assign org role is set",
roleAttributePath: "",
userRawJSON: testGHUserJSON,
settingAutoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "Editor",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
// see: https://github.com/grafana/grafana/issues/85916
name: "should check parent team id for team membership",
roleAttributePath: "",
userRawJSON: testGHUserJSON,
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
name: "should check parent team id for team membership",
roleAttributePath: "",
userRawJSON: testGHUserJSON,
settingAutoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
oAuthExtraInfo: map[string]string{
"team_ids": "99",
},
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
Role: "Editor",
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
OrgRoles: map[int64]org.RoleType{1: org.RoleEditor},
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
name: "should map role when only org mapping is set",
orgMapping: []string{"@github/justice-league:Org4:Editor", "*:Org5:Viewer"},
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
OrgRoles: map[int64]org.RoleType{4: "Editor", 5: "Viewer"},
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
name: "should map role when only org mapping is set and role attribute strict is enabled",
orgMapping: []string{"@github/justice-league:Org4:Editor", "*:Org5:Viewer"},
roleAttributeStrict: true,
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
Login: "octocat",
OrgRoles: map[int64]org.RoleType{4: "Editor", 5: "Viewer"},
Groups: []string{"https://github.com/orgs/github/teams/justice-league", "@github/justice-league"},
},
},
{
name: "should return error when neither role attribute path nor org mapping evaluates to a role and role attribute strict is enabled",
orgMapping: []string{"@github/avengers:Org4:Editor"},
roleAttributeStrict: true,
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
wantErr: true,
},
{
name: "should return error when neither role attribute path nor org mapping is set and role attribute strict is enabled",
roleAttributeStrict: true,
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -278,15 +330,26 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
}))
defer server.Close()
cfg := &setting.Cfg{
AutoAssignOrgRole: "Viewer", // default role
}
if tt.settingAutoAssignOrgRole != "" {
cfg.AutoAssignOrgRole = tt.settingAutoAssignOrgRole
}
s := NewGitHubProvider(
&social.OAuthInfo{
ApiUrl: server.URL + "/user",
RoleAttributePath: tt.roleAttributePath,
SkipOrgRoleSync: tt.settingSkipOrgRoleSync,
Extra: tt.oAuthExtraInfo,
}, &setting.Cfg{
AutoAssignOrgRole: tt.autoAssignOrgRole,
}, nil, &ssosettingstests.MockService{},
ApiUrl: server.URL + "/user",
RoleAttributePath: tt.roleAttributePath,
RoleAttributeStrict: tt.roleAttributeStrict,
OrgMapping: tt.orgMapping,
SkipOrgRoleSync: tt.settingSkipOrgRoleSync,
Extra: tt.oAuthExtraInfo,
}, cfg,
ProvideOrgRoleMapper(cfg,
&orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}),
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
token := &oauth2.Token{
@@ -294,13 +357,12 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
}
got, err := s.UserInfo(context.Background(), server.Client(), token)
if (err != nil) != tt.wantErr {
t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr)
if tt.wantErr {
require.Error(t, err)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("UserInfo() got = %v, want %v", got, tt.want)
}
require.EqualValues(t, tt.want, got)
})
}
}
+2 -2
View File
@@ -168,8 +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:
// Do nothing, GenericOAuthProvider already supports OrgRole mapping
case social.GenericOAuthProviderName, social.GitHubProviderName:
// Do nothing, GenericOAuthProvider and GitHub already supports OrgRole mapping
default:
userInfo.OrgRoles, userInfo.IsGrafanaAdmin, _ = getRoles(c.cfg, func() (org.RoleType, *bool, error) {
return userInfo.Role, userInfo.IsGrafanaAdmin, nil