Auth: Add org to role mappings support to Okta integration (#88770)

* Add org mapping support to Okta

* Update docs and configs

* Prettier docs

* Apply suggestions from code review

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>

* Improve tests

---------

Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
This commit is contained in:
Misi
2024-06-06 10:35:06 +02:00
committed by GitHub
co-authored by Christopher Moyer
parent 50b3269ef0
commit 4f2a9a47f3
7 changed files with 184 additions and 67 deletions
+24 -16
View File
@@ -11,7 +11,6 @@ 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/ssosettings"
@@ -139,32 +138,41 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o
return nil, errMissingGroupMembership
}
var role roletype.RoleType
var isGrafanaAdmin *bool
userInfo := &social.BasicUserInfo{
Id: claims.ID,
Name: claims.Name,
Email: email,
Login: email,
Groups: groups,
}
if !s.info.SkipOrgRoleSync {
var grafanaAdmin bool
role, grafanaAdmin, err = s.extractRoleAndAdmin(data.rawJSON, groups)
directlyMappedRole, grafanaAdmin, err := s.extractRoleAndAdminOptional(data.rawJSON, groups)
if err != nil {
return nil, err
s.log.Warn("Failed to extract role", "err", err)
}
if s.info.AllowAssignGrafanaAdmin {
isGrafanaAdmin = &grafanaAdmin
userInfo.IsGrafanaAdmin = &grafanaAdmin
}
externalOrgs, err := s.extractOrgs(data.rawJSON)
if err != nil {
s.log.Warn("Failed to extract orgs", "err", err)
return nil, err
}
userInfo.OrgRoles = s.orgRoleMapper.MapOrgRoles(s.orgMappingCfg, externalOrgs, directlyMappedRole)
if s.info.RoleAttributeStrict && len(userInfo.OrgRoles) == 0 {
return nil, errRoleAttributeStrictViolation.Errorf("could not evaluate any valid roles using IdP provided data")
}
}
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")
}
return &social.BasicUserInfo{
Id: claims.ID,
Name: claims.Name,
Email: email,
Login: email,
Role: role,
IsGrafanaAdmin: isGrafanaAdmin,
Groups: groups,
}, nil
return userInfo, nil
}
func (s *SocialOkta) extractAPI(ctx context.Context, data *OktaUserInfoJson, client *http.Client) error {
+136 -46
View File
@@ -13,9 +13,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"
@@ -29,63 +30,142 @@ func TestSocialOkta_UserInfo(t *testing.T) {
tests := []struct {
name string
userRawJSON string
OAuth2Extra any
autoAssignOrgRole string
settingSkipOrgRoleSync bool
oAuth2Extra any
skipOrgRoleSync bool
allowAssignGrafanaAdmin bool
RoleAttributePath string
ExpectedEmail string
ExpectedRole roletype.RoleType
ExpectedGrafanaAdmin *bool
ExpectedErr error
wantErr bool
roleAttributePath string
roleAttributeStrict bool
orgMapping []string
orgAttributePath string
expectedEmail string
expectedOrgRoles map[int64]org.RoleType
expectedGrafanaAdmin *bool
expectedErr error
}{
{
name: "Should give role from JSON and email from id token",
name: "should give role from JSON and email from id token",
userRawJSON: `{ "email": "okta-octopus@grafana.com", "role": "Admin" }`,
RoleAttributePath: "role",
OAuth2Extra: map[string]any{
roleAttributePath: "role",
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
ExpectedEmail: "okto.octopus@test.com",
ExpectedRole: "Admin",
ExpectedGrafanaAdmin: boolPointer,
wantErr: false,
expectedEmail: "okto.octopus@test.com",
expectedOrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
expectedGrafanaAdmin: boolPointer,
},
{
name: "Should give empty role and nil pointer for GrafanaAdmin when skip org role sync enable",
userRawJSON: `{ "email": "okta-octopus@grafana.com", "role": "Admin" }`,
RoleAttributePath: "role",
settingSkipOrgRoleSync: true,
OAuth2Extra: map[string]any{
name: "should give empty role and nil pointer for GrafanaAdmin when skip org role sync enable",
userRawJSON: `{ "email": "okta-octopus@grafana.com", "role": "Admin" }`,
roleAttributePath: "role",
skipOrgRoleSync: true,
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
ExpectedEmail: "okto.octopus@test.com",
ExpectedRole: "",
ExpectedGrafanaAdmin: boolPointer,
wantErr: false,
expectedEmail: "okto.octopus@test.com",
expectedOrgRoles: nil,
expectedGrafanaAdmin: boolPointer,
},
{
name: "Should give grafanaAdmin role for specific GrafanaAdmin in the role assignement",
name: "should give grafanaAdmin role for specific GrafanaAdmin in the role assignement",
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "role": "%s" }`, social.RoleGrafanaAdmin),
RoleAttributePath: "role",
roleAttributePath: "role",
allowAssignGrafanaAdmin: true,
OAuth2Extra: map[string]any{
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
ExpectedEmail: "okto.octopus@test.com",
ExpectedRole: "Admin",
ExpectedGrafanaAdmin: trueBoolPtr(),
wantErr: false,
expectedEmail: "okto.octopus@test.com",
expectedOrgRoles: map[int64]org.RoleType{1: org.RoleAdmin},
expectedGrafanaAdmin: trueBoolPtr(),
},
{
name: "should fallback to default org role when role attribute path is empty",
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "groups": ["Group 1"], "role": "%s" }`, org.RoleEditor),
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
expectedEmail: "okto.octopus@test.com",
expectedOrgRoles: map[int64]org.RoleType{1: org.RoleViewer},
},
{
name: "should map role when only org mapping is set",
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "groups": ["Group 1"], "role": "%s" }`, org.RoleEditor),
orgAttributePath: "groups",
orgMapping: []string{"Group 1:Org4:Editor", "*:Org5:Viewer"},
roleAttributeStrict: false,
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
expectedEmail: "okto.octopus@test.com",
expectedOrgRoles: map[int64]org.RoleType{4: org.RoleEditor, 5: org.RoleViewer},
},
{
name: "should map role when only org mapping is set and role attribute strict is enabled",
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "groups": ["Group 1"], "role": "%s" }`, org.RoleEditor),
orgAttributePath: "groups",
orgMapping: []string{"Group 1:Org4:Editor", "*:Org5:Viewer"},
roleAttributeStrict: true,
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
expectedEmail: "okto.octopus@test.com",
expectedOrgRoles: map[int64]org.RoleType{4: org.RoleEditor, 5: org.RoleViewer},
},
{
name: "should return nil OrgRoles when SkipOrgRoleSync is enabled",
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "role": "%s" }`, org.RoleEditor),
roleAttributePath: "role",
skipOrgRoleSync: true,
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
expectedOrgRoles: nil,
expectedEmail: "okto.octopus@test.com",
},
{
name: "should return error when neither role attribute path nor org mapping evaluates to a role and role attribute strict is enabled",
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "role": "%s" }`, org.RoleEditor),
roleAttributePath: "invalid_role_path",
roleAttributeStrict: true,
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
expectedErr: errRoleAttributeStrictViolation,
},
{
name: "should return error when neither role attribute path nor org mapping is set and role attribute strict is enabled",
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "role": "%s" }`, org.RoleEditor),
roleAttributeStrict: true,
oAuth2Extra: map[string]any{
// {
// "email": "okto.octopus@test.com"
// },
"id_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQWRtaW4iLCJlbWFpbCI6Im9rdG8ub2N0b3B1c0B0ZXN0LmNvbSJ9.yhg0nvYCpMVCVrRvwtmHzhF0RJqid_YFbjJ_xuBCyHs",
},
expectedErr: errRoleAttributeStrictViolation,
},
}
for _, tt := range tests {
@@ -103,17 +183,23 @@ func TestSocialOkta_UserInfo(t *testing.T) {
}))
defer server.Close()
cfg := &setting.Cfg{
AutoAssignOrgRole: "Viewer", // default role
}
provider := NewOktaProvider(
&social.OAuthInfo{
ApiUrl: server.URL + "/user",
RoleAttributePath: tt.RoleAttributePath,
RoleAttributePath: tt.roleAttributePath,
RoleAttributeStrict: tt.roleAttributeStrict,
OrgMapping: tt.orgMapping,
OrgAttributePath: tt.orgAttributePath,
AllowAssignGrafanaAdmin: tt.allowAssignGrafanaAdmin,
SkipOrgRoleSync: tt.settingSkipOrgRoleSync,
SkipOrgRoleSync: tt.skipOrgRoleSync,
},
&setting.Cfg{
AutoAssignOrgRole: tt.autoAssignOrgRole,
},
nil,
cfg,
ProvideOrgRoleMapper(cfg,
&orgtest.FakeOrgService{ExpectedOrgs: []*org.OrgDTO{{ID: 4, Name: "Org4"}, {ID: 5, Name: "Org5"}}}),
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
@@ -124,15 +210,19 @@ func TestSocialOkta_UserInfo(t *testing.T) {
RefreshToken: "",
Expiry: time.Now(),
}
token := staticToken.WithExtra(tt.OAuth2Extra)
got, err := provider.UserInfo(context.Background(), server.Client(), token)
if (err != nil) != tt.wantErr {
t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr)
token := staticToken.WithExtra(tt.oAuth2Extra)
actual, err := provider.UserInfo(context.Background(), server.Client(), token)
if tt.expectedErr != nil {
require.Error(t, err)
require.ErrorIs(t, err, tt.expectedErr)
return
}
require.Equal(t, tt.ExpectedEmail, got.Email)
require.Equal(t, tt.ExpectedRole, got.Role)
require.Equal(t, tt.ExpectedGrafanaAdmin, got.IsGrafanaAdmin)
require.Equal(t, tt.expectedEmail, actual.Email)
require.Equal(t, tt.expectedOrgRoles, actual.OrgRoles)
require.Equal(t, tt.expectedGrafanaAdmin, actual.IsGrafanaAdmin)
})
}
}
+1 -1
View File
@@ -168,7 +168,7 @@ 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:
case social.GenericOAuthProviderName, social.GitHubProviderName, social.GitlabProviderName, social.OktaProviderName:
// Do nothing, these providers already supports OrgRole mapping
default:
userInfo.OrgRoles, userInfo.IsGrafanaAdmin, _ = getRoles(c.cfg, func() (org.RoleType, *bool, error) {