Auth: Refactor OAuth connectors' initialization (#77919)

* Refactor AzureAD to init itself

* Use mapstructure to convert data to OAuthInfo

* Update

* Align tests

* Remove unused functions

* Add owner to mapstructure

* Clean up, lint

* Refactor Okta init, Align tests

* Address review comments, fix name in newSocialBase

* Update newSocialBase first param

* Refactor GitLab init, align tests

* Update pkg/login/social/common.go

Co-authored-by: Karl Persson <kalle.persson@grafana.com>

* Use ini conversion to map

* Leftovers

* Refactor GitHub connector initialization, align tests

* Refactor Google connector init, align tests

* Refactor grafana_com connector, align tests

* Refactor generic_oauth connector init, align tests

* cleanup

* Remove util.go

* Add tests for custom field init

* Change OAuthInfo's Extra type

* Fix

* Replace interface{} with any

* clean up

---------

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
This commit is contained in:
Misi
2023-11-20 09:45:40 +01:00
committed by GitHub
parent 13d67be0a9
commit 437ae8e8c5
20 changed files with 1126 additions and 438 deletions

2
go.mod
View File

@@ -361,7 +361,7 @@ require (
github.com/mattn/go-ieproxy v0.0.3 // indirect github.com/mattn/go-ieproxy v0.0.3 // indirect
github.com/mattn/goveralls v0.0.6 // indirect github.com/mattn/goveralls v0.0.6 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 //@grafana/grafana-authnz-team
github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect

View File

@@ -16,9 +16,16 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
const azureADProviderName = "azuread"
var _ SocialConnector = (*SocialAzureAD)(nil)
type SocialAzureAD struct { type SocialAzureAD struct {
*SocialBase *SocialBase
cache remotecache.CacheStorage cache remotecache.CacheStorage
@@ -57,6 +64,30 @@ type keySetJWKS struct {
jose.JSONWebKeySet jose.JSONWebKeySet
} }
func NewAzureADProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager, cache remotecache.CacheStorage) (*SocialAzureAD, error) {
info, err := createOAuthInfoFromKeyValues(settings)
if err != nil {
return nil, err
}
config := createOAuthConfig(info, cfg, azureADProviderName)
provider := &SocialAzureAD{
SocialBase: newSocialBase(azureADProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
cache: cache,
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
forceUseGraphAPI: mustBool(info.Extra["force_use_graph_api"], false),
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync
}
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(config, OfflineAccessScope)
}
return provider, nil
}
func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
idToken := token.Extra("id_token") idToken := token.Extra("id_token")
if idToken == nil { if idToken == nil {
@@ -122,6 +153,10 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
}, nil }, nil
} }
func (s *SocialAzureAD) GetOAuthInfo() *OAuthInfo {
return s.info
}
func (s *SocialAzureAD) validateClaims(ctx context.Context, client *http.Client, parsedToken *jwt.JSONWebToken) (*azureClaims, error) { func (s *SocialAzureAD) validateClaims(ctx context.Context, client *http.Client, parsedToken *jwt.JSONWebToken) (*azureClaims, error) {
claims, err := s.validateIDTokenSignature(ctx, client, parsedToken) claims, err := s.validateIDTokenSignature(ctx, client, parsedToken)
if err != nil { if err != nil {

View File

@@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
) )
func trueBoolPtr() *bool { func trueBoolPtr() *bool {
@@ -32,11 +33,9 @@ func falseBoolPtr() *bool {
func TestSocialAzureAD_UserInfo(t *testing.T) { func TestSocialAzureAD_UserInfo(t *testing.T) {
type fields struct { type fields struct {
SocialBase *SocialBase providerCfg map[string]any
allowedGroups []string cfg *setting.Cfg
allowedOrganizations []string usGovURL bool
forceUseGraphAPI bool
usGovURL bool
} }
type args struct { type args struct {
client *http.Client client *http.Client
@@ -61,7 +60,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
@@ -75,7 +80,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "No email", name: "No email",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "", Email: "",
@@ -88,8 +99,17 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
wantErr: true, wantErr: true,
}, },
{ {
name: "No id token", name: "No id token",
claims: nil, claims: nil,
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
},
want: nil, want: nil,
wantErr: true, wantErr: true,
}, },
@@ -103,8 +123,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
usGovURL: true, "name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
usGovURL: true,
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
@@ -125,7 +151,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
@@ -139,7 +171,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Admin role", name: "Admin role",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -160,7 +198,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Lowercase Admin role", name: "Lowercase Admin role",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -181,7 +225,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Only other roles", name: "Only other roles",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Viewer", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -199,6 +249,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Groups: []string{}, Groups: []string{},
}, },
}, },
// TODO: @mgyongyosi check this test
{ {
name: "role from env variable", name: "role from env variable",
claims: &azureClaims{ claims: &azureClaims{
@@ -209,7 +260,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
},
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
@@ -230,7 +287,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234", ID: "1234",
}, },
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
},
}, },
want: &BasicUserInfo{ want: &BasicUserInfo{
Id: "1234", Id: "1234",
@@ -244,7 +307,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Admin and Editor roles in claim", name: "Admin and Editor roles in claim",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "Editor", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -263,8 +332,18 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
}, },
}, },
{ {
name: "Grafana Admin but setting is disabled", name: "Grafana Admin but setting is disabled",
fields: fields{SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures())}, fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": false,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
},
},
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
PreferredUsername: "", PreferredUsername: "",
@@ -285,8 +364,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Editor roles in claim and GrafanaAdminAssignment enabled", name: "Editor roles in claim and GrafanaAdminAssignment enabled",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", providerCfg: map[string]any{
&oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()), "name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": true,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -307,8 +392,16 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
}, },
{ {
name: "Grafana Admin and Editor roles in claim", name: "Grafana Admin and Editor roles in claim",
fields: fields{SocialBase: newSocialBase("azuread", fields: fields{
&oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures())}, providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": true,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
},
},
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
PreferredUsername: "", PreferredUsername: "",
@@ -329,8 +422,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Error if user is not a member of allowed_groups", name: "Error if user is not a member of allowed_groups",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
allowedGroups: []string{"dead-beef"}, "name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": false,
"allowed_groups": "dead-beef",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -346,8 +446,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Error if user is not a member of allowed_organizations", name: "Error if user is not a member of allowed_organizations",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Editor", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
allowedOrganizations: []string{"uuid-1234"}, "name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": false,
"allowed_organizations": "uuid-1234",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -360,10 +467,18 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
}, },
want: nil, want: nil,
wantErr: true, wantErr: true,
}, { },
{
name: "No error if user is a member of allowed_organizations", name: "No error if user is a member of allowed_organizations",
fields: fields{ fields: fields{
allowedOrganizations: []string{"uuid-1234", "uuid-5678"}, providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allowed_organizations": "uuid-1234,uuid-5678",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -387,9 +502,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "No Error if user is a member of allowed_groups", name: "No Error if user is a member of allowed_groups",
fields: fields{ fields: fields{
allowedGroups: []string{"foo", "bar"}, providerCfg: map[string]any{
SocialBase: newSocialBase("azuread", "name": "azuread",
&oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{AllowAssignGrafanaAdmin: false}, "Viewer", false, *featuremgmt.WithFeatures()), "client_id": "client-id-example",
"allow_assign_grafana_admin": "false",
"allowed_groups": "foo, bar",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -411,7 +532,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Fetch groups when ClaimsNames and ClaimsSources is set", name: "Fetch groups when ClaimsNames and ClaimsSources is set",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
ID: "1", ID: "1",
@@ -436,8 +563,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Fetch groups when forceUseGraphAPI is set", name: "Fetch groups when forceUseGraphAPI is set",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
forceUseGraphAPI: true, "name": "azuread",
"client_id": "client-id-example",
"force_use_graph_api": "true",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
ID: "1", ID: "1",
@@ -463,7 +596,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Fetch empty role when strict attribute role is true and no match", name: "Fetch empty role when strict attribute role is true and no match",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"role_attribute_strict": "true",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -479,7 +619,14 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{ {
name: "Fetch empty role when strict attribute role is true and no role claims returned", name: "Fetch empty role when strict attribute role is true and no role claims returned",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{RoleAttributeStrict: true}, "", false, *featuremgmt.WithFeatures()), providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"role_attribute_strict": "true",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -528,20 +675,8 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &SocialAzureAD{ s, err := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, featuremgmt.WithFeatures(), cache)
SocialBase: tt.fields.SocialBase, require.NoError(t, err)
allowedOrganizations: tt.fields.allowedOrganizations,
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
cache: cache,
}
if tt.fields.SocialBase == nil {
s.SocialBase = newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{}, "", false, *featuremgmt.WithFeatures())
}
if tt.fields.allowedGroups != nil {
s.allowedGroups = tt.fields.allowedGroups
}
if tt.fields.usGovURL { if tt.fields.usGovURL {
s.SocialBase.Endpoint.AuthURL = usGovAuthURL s.SocialBase.Endpoint.AuthURL = usGovAuthURL
@@ -592,9 +727,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
token = token.WithExtra(map[string]any{"id_token": raw}) token = token.WithExtra(map[string]any{"id_token": raw})
} }
if tt.fields.SocialBase != nil { tt.args.client = s.Client(context.Background(), token)
tt.args.client = s.Client(context.Background(), token)
}
got, err := s.UserInfo(context.Background(), tt.args.client, token) got, err := s.UserInfo(context.Background(), tt.args.client, token)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
@@ -609,20 +742,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
func TestSocialAzureAD_SkipOrgRole(t *testing.T) { func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
type fields struct { type fields struct {
SocialBase *SocialBase SocialBase *SocialBase
allowedGroups []string providerCfg map[string]any
forceUseGraphAPI bool cfg *setting.Cfg
skipOrgRoleSync bool
}
type args struct {
client *http.Client
} }
tests := []struct { tests := []struct {
name string name string
fields fields fields fields
claims *azureClaims claims *azureClaims
args args
settingAutoAssignOrgRole string settingAutoAssignOrgRole string
want *BasicUserInfo want *BasicUserInfo
wantErr bool wantErr bool
@@ -630,10 +758,16 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
{ {
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should get roles, skipOrgRoleSyncBase disabled", name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should get roles, skipOrgRoleSyncBase disabled",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", providerCfg: map[string]any{
&oauth2.Config{ClientID: "client-id-example"}, "name": "azuread",
&OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()), "client_id": "client-id-example",
skipOrgRoleSync: false, "allow_assign_grafana_admin": "true",
"skip_org_role_sync": "false",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
OAuthSkipOrgRoleUpdateSync: false,
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -655,10 +789,16 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
{ {
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should not get roles", name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should not get roles",
fields: fields{ fields: fields{
SocialBase: newSocialBase("azuread", providerCfg: map[string]any{
&oauth2.Config{ClientID: "client-id-example"}, "name": "azuread",
&OAuthInfo{AllowAssignGrafanaAdmin: true}, "", false, *featuremgmt.WithFeatures()), "client_id": "client-id-example",
skipOrgRoleSync: false, "allow_assign_grafana_admin": "true",
"skip_org_role_sync": "false",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
OAuthSkipOrgRoleUpdateSync: false,
},
}, },
claims: &azureClaims{ claims: &azureClaims{
Email: "me@example.com", Email: "me@example.com",
@@ -711,18 +851,8 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &SocialAzureAD{ s, err := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, featuremgmt.WithFeatures(), cache)
SocialBase: tt.fields.SocialBase, require.NoError(t, err)
forceUseGraphAPI: tt.fields.forceUseGraphAPI,
skipOrgRoleSync: tt.fields.skipOrgRoleSync,
cache: cache,
}
if tt.fields.SocialBase == nil {
s.SocialBase = newSocialBase("azuread", &oauth2.Config{ClientID: "client-id-example"}, &OAuthInfo{
AllowedGroups: tt.fields.allowedGroups,
}, "", false, *featuremgmt.WithFeatures())
}
s.SocialBase.Endpoint.AuthURL = authURL s.SocialBase.Endpoint.AuthURL = authURL
@@ -769,11 +899,9 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
token = token.WithExtra(map[string]any{"id_token": raw}) token = token.WithExtra(map[string]any{"id_token": raw})
} }
if tt.fields.SocialBase != nil { provClient := s.Client(context.Background(), token)
tt.args.client = s.Client(context.Background(), token)
}
got, err := s.UserInfo(context.Background(), tt.args.client, token) got, err := s.UserInfo(context.Background(), provClient, token)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("UserInfo() error = %v, wantErr %v", err, tt.wantErr)
return return
@@ -783,3 +911,46 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
}) })
} }
} }
func TestSocialAzureAD_InitializeExtraFields(t *testing.T) {
type settingFields struct {
forceUseGraphAPI bool
allowedOrganizations []string
}
testCases := []struct {
name string
settings map[string]any
want settingFields
}{
{
name: "forceUseGraphAPI is set to true",
settings: map[string]any{
"force_use_graph_api": "true",
},
want: settingFields{
forceUseGraphAPI: true,
allowedOrganizations: []string{},
},
},
{
name: "allowedOrganizations is set",
settings: map[string]any{
"allowed_organizations": "uuid-1234,uuid-5678",
},
want: settingFields{
forceUseGraphAPI: false,
allowedOrganizations: []string{"uuid-1234", "uuid-5678"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s, err := NewAzureADProvider(tc.settings, &setting.Cfg{}, featuremgmt.WithFeatures(), nil)
require.NoError(t, err)
require.Equal(t, tc.want.forceUseGraphAPI, s.forceUseGraphAPI)
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
})
}
}

View File

@@ -7,9 +7,16 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"reflect"
"strconv"
"strings" "strings"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/jmespath/go-jmespath" "github.com/jmespath/go-jmespath"
"github.com/mitchellh/mapstructure"
"golang.org/x/oauth2"
"gopkg.in/ini.v1"
) )
var ( var (
@@ -132,3 +139,101 @@ func (s *SocialBase) searchJSONForStringArrayAttr(attributePath string, data []b
return result, nil return result, nil
} }
func createOAuthConfig(info *OAuthInfo, cfg *setting.Cfg, defaultName string) *oauth2.Config {
var authStyle oauth2.AuthStyle
switch strings.ToLower(info.AuthStyle) {
case "inparams":
authStyle = oauth2.AuthStyleInParams
case "inheader":
authStyle = oauth2.AuthStyleInHeader
default:
authStyle = oauth2.AuthStyleAutoDetect
}
config := oauth2.Config{
ClientID: info.ClientId,
ClientSecret: info.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: info.AuthUrl,
TokenURL: info.TokenUrl,
AuthStyle: authStyle,
},
RedirectURL: strings.TrimSuffix(cfg.AppURL, "/") + SocialBaseUrl + defaultName,
Scopes: info.Scopes,
}
return &config
}
func mustBool(value any, defaultValue bool) bool {
if value == nil {
return defaultValue
}
str, ok := value.(string)
if ok {
result, err := strconv.ParseBool(str)
if err != nil {
return defaultValue
}
return result
}
result, ok := value.(bool)
if !ok {
return defaultValue
}
return result
}
// convertIniSectionToMap converts key value pairs from an ini section to a map[string]any
func convertIniSectionToMap(sec *ini.Section) map[string]any {
mappedSettings := make(map[string]any)
for k, v := range sec.KeysHash() {
mappedSettings[k] = v
}
return mappedSettings
}
// createOAuthInfoFromKeyValues creates an OAuthInfo struct from a map[string]any using mapstructure
// it puts all extra key values into OAuthInfo's Extra map
func createOAuthInfoFromKeyValues(settingsKV map[string]any) (*OAuthInfo, error) {
emptyStrToSliceDecodeHook := func(from reflect.Type, to reflect.Type, data any) (any, error) {
if from.Kind() == reflect.String && to.Kind() == reflect.Slice {
strData, ok := data.(string)
if !ok {
return nil, fmt.Errorf("failed to convert %v to string", data)
}
if strData == "" {
return []string{}, nil
}
return util.SplitString(strData), nil
}
return data, nil
}
var oauthInfo OAuthInfo
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: emptyStrToSliceDecodeHook,
Result: &oauthInfo,
WeaklyTypedInput: true,
})
if err != nil {
return nil, err
}
err = decoder.Decode(settingsKV)
if err != nil {
return nil, err
}
if oauthInfo.EmptyScopes {
oauthInfo.Scopes = []string{}
}
return &oauthInfo, err
}

View File

@@ -0,0 +1,101 @@
package social
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
)
func TestMapping_IniSectionOAuthInfo(t *testing.T) {
iniContent := `
[test]
name = OAuth
icon = signin
enabled = true
allow_sign_up = false
auto_login = true
client_id = test_client_id
client_secret = test_client_secret
scopes = ["openid", "profile", "email"]
empty_scopes = false
email_attribute_name = email:primary
email_attribute_path = email
login_attribute_path = login
name_attribute_path = name
role_attribute_path = role
role_attribute_strict = true
groups_attribute_path = groups
id_token_attribute_name = id_token
team_ids_attribute_path = team_ids
auth_url = test_auth_url
token_url = test_token_url
api_url = test_api_url
teams_url = test_teams_url
allowed_domains = domain1.com
allowed_groups =
team_ids = first, second
allowed_organizations = org1, org2
tls_skip_verify_insecure = true
tls_client_cert =
tls_client_key =
tls_client_ca =
use_pkce = false
auth_style =
allow_assign_grafana_admin = true
skip_org_role_sync = true
use_refresh_token = true
empty_scopes =
hosted_domain = test_hosted_domain
`
iniFile, err := ini.Load([]byte(iniContent))
require.NoError(t, err)
expectedOAuthInfo := &OAuthInfo{
Name: "OAuth",
Icon: "signin",
Enabled: true,
AllowSignup: false,
AutoLogin: true,
ClientId: "test_client_id",
ClientSecret: "test_client_secret",
Scopes: []string{"openid", "profile", "email"},
EmptyScopes: false,
EmailAttributeName: "email:primary",
EmailAttributePath: "email",
RoleAttributePath: "role",
RoleAttributeStrict: true,
GroupsAttributePath: "groups",
TeamIdsAttributePath: "team_ids",
AuthUrl: "test_auth_url",
TokenUrl: "test_token_url",
ApiUrl: "test_api_url",
TeamsUrl: "test_teams_url",
AllowedDomains: []string{"domain1.com"},
AllowedGroups: []string{},
TlsSkipVerify: true,
TlsClientCert: "",
TlsClientKey: "",
TlsClientCa: "",
UsePKCE: false,
AuthStyle: "",
AllowAssignGrafanaAdmin: true,
UseRefreshToken: true,
HostedDomain: "test_hosted_domain",
Extra: map[string]string{
"allowed_organizations": "org1, org2",
"id_token_attribute_name": "id_token",
"login_attribute_path": "login",
"name_attribute_path": "name",
"skip_org_role_sync": "true",
"team_ids": "first, second",
},
}
settingsKVs := convertIniSectionToMap(iniFile.Section("test"))
oauthInfo, err := createOAuthInfoFromKeyValues(settingsKVs)
require.NoError(t, err)
require.Equal(t, expectedOAuthInfo, oauthInfo)
}

View File

@@ -11,8 +11,14 @@ import (
"strconv" "strconv"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
const genericOAuthProviderName = "generic_oauth"
type SocialGenericOAuth struct { type SocialGenericOAuth struct {
*SocialBase *SocialBase
allowedOrganizations []string allowedOrganizations []string
@@ -30,6 +36,36 @@ type SocialGenericOAuth struct {
skipOrgRoleSync bool skipOrgRoleSync bool
} }
func NewGenericOAuthProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGenericOAuth, error) {
info, err := createOAuthInfoFromKeyValues(settings)
if err != nil {
return nil, err
}
config := createOAuthConfig(info, cfg, genericOAuthProviderName)
provider := &SocialGenericOAuth{
SocialBase: newSocialBase(genericOAuthProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamsUrl: info.TeamsUrl,
emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath,
nameAttributePath: info.Extra["name_attribute_path"],
groupsAttributePath: info.GroupsAttributePath,
loginAttributePath: info.Extra["login_attribute_path"],
idTokenAttributeName: info.Extra["id_token_attribute_name"],
teamIdsAttributePath: info.TeamIdsAttributePath,
teamIds: util.SplitString(info.Extra["team_ids"]),
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
allowedGroups: info.AllowedGroups,
skipOrgRoleSync: cfg.GenericOAuthSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync
}
return provider, nil
}
// TODOD: remove this in the next PR and use the isGroupMember from social.go
func (s *SocialGenericOAuth) IsGroupMember(groups []string) bool { func (s *SocialGenericOAuth) IsGroupMember(groups []string) bool {
if len(s.allowedGroups) == 0 { if len(s.allowedGroups) == 0 {
return true return true
@@ -205,6 +241,10 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
return userInfo, nil return userInfo, nil
} }
func (s *SocialGenericOAuth) GetOAuthInfo() *OAuthInfo {
return s.info
}
func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson { func (s *SocialGenericOAuth) extractFromToken(token *oauth2.Token) *UserInfoJson {
s.log.Debug("Extracting user info from OAuth token") s.log.Debug("Extracting user info from OAuth token")

View File

@@ -8,28 +8,20 @@ import (
"testing" "testing"
"time" "time"
"github.com/go-kit/log/level"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
)
func newLogger(name string, lev string) log.Logger { "github.com/grafana/grafana/pkg/setting"
logger := log.New(name) )
logger.Swap(level.NewFilter(logger.GetLogger(), level.AllowInfo()))
return logger
}
func TestSearchJSONForEmail(t *testing.T) { func TestSearchJSONForEmail(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := SocialGenericOAuth{ provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
SocialBase: &SocialBase{ require.NoError(t, err)
log: newLogger("generic_oauth_test", "debug"),
},
}
tests := []struct { tests := []struct {
Name string Name string
@@ -113,11 +105,8 @@ func TestSearchJSONForEmail(t *testing.T) {
func TestSearchJSONForGroups(t *testing.T) { func TestSearchJSONForGroups(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := SocialGenericOAuth{ provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
SocialBase: &SocialBase{ require.NoError(t, err)
log: newLogger("generic_oauth_test", "debug"),
},
}
tests := []struct { tests := []struct {
Name string Name string
@@ -176,11 +165,8 @@ func TestSearchJSONForGroups(t *testing.T) {
func TestSearchJSONForRole(t *testing.T) { func TestSearchJSONForRole(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := SocialGenericOAuth{ provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
SocialBase: &SocialBase{ require.NoError(t, err)
log: newLogger("generic_oauth_test", "debug"),
},
}
tests := []struct { tests := []struct {
Name string Name string
@@ -238,12 +224,10 @@ func TestSearchJSONForRole(t *testing.T) {
} }
func TestUserInfoSearchesForEmailAndRole(t *testing.T) { func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
provider := SocialGenericOAuth{ provider, err := NewGenericOAuthProvider(map[string]any{
SocialBase: &SocialBase{ "email_attribute_path": "email",
log: newLogger("generic_oauth_test", "debug"), }, &setting.Cfg{}, featuremgmt.WithFeatures())
}, require.NoError(t, err)
emailAttributePath: "email",
}
tests := []struct { tests := []struct {
Name string Name string
@@ -508,12 +492,10 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
func TestUserInfoSearchesForLogin(t *testing.T) { func TestUserInfoSearchesForLogin(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := SocialGenericOAuth{ provider, err := NewGenericOAuthProvider(map[string]any{
SocialBase: &SocialBase{ "login_attribute_path": "login",
log: newLogger("generic_oauth_test", "debug"), }, &setting.Cfg{}, featuremgmt.WithFeatures())
}, require.NoError(t, err)
loginAttributePath: "login",
}
tests := []struct { tests := []struct {
Name string Name string
@@ -603,12 +585,10 @@ func TestUserInfoSearchesForLogin(t *testing.T) {
func TestUserInfoSearchesForName(t *testing.T) { func TestUserInfoSearchesForName(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := SocialGenericOAuth{ provider, err := NewGenericOAuthProvider(map[string]any{
SocialBase: &SocialBase{ "name_attribute_path": "name",
log: newLogger("generic_oauth_test", "debug"), }, &setting.Cfg{}, featuremgmt.WithFeatures())
}, require.NoError(t, err)
nameAttributePath: "name",
}
tests := []struct { tests := []struct {
Name string Name string
@@ -701,12 +681,6 @@ func TestUserInfoSearchesForName(t *testing.T) {
func TestUserInfoSearchesForGroup(t *testing.T) { func TestUserInfoSearchesForGroup(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) { t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider := SocialGenericOAuth{
SocialBase: &SocialBase{
log: newLogger("generic_oauth_test", "debug"),
},
}
tests := []struct { tests := []struct {
name string name string
groupsAttributePath string groupsAttributePath string
@@ -742,7 +716,6 @@ func TestUserInfoSearchesForGroup(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
provider.groupsAttributePath = test.groupsAttributePath
body, err := json.Marshal(test.responseBody) body, err := json.Marshal(test.responseBody)
require.NoError(t, err) require.NoError(t, err)
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -752,7 +725,13 @@ func TestUserInfoSearchesForGroup(t *testing.T) {
_, err := w.Write(body) _, err := w.Write(body)
require.NoError(t, err) require.NoError(t, err)
})) }))
provider.apiUrl = ts.URL
provider, err := NewGenericOAuthProvider(map[string]any{
"groups_attribute_path": test.groupsAttributePath,
"api_url": ts.URL,
}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
token := &oauth2.Token{ token := &oauth2.Token{
AccessToken: "", AccessToken: "",
TokenType: "", TokenType: "",
@@ -769,12 +748,10 @@ func TestUserInfoSearchesForGroup(t *testing.T) {
} }
func TestPayloadCompression(t *testing.T) { func TestPayloadCompression(t *testing.T) {
provider := SocialGenericOAuth{ provider, err := NewGenericOAuthProvider(map[string]any{
SocialBase: &SocialBase{ "email_attribute_path": "email",
log: newLogger("generic_oauth_test", "debug"), }, &setting.Cfg{}, featuremgmt.WithFeatures())
}, require.NoError(t, err)
emailAttributePath: "email",
}
tests := []struct { tests := []struct {
Name string Name string
@@ -836,3 +813,97 @@ func TestPayloadCompression(t *testing.T) {
}) })
} }
} }
func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
type settingFields struct {
nameAttributePath string
loginAttributePath string
idTokenAttributeName string
teamIds []string
allowedOrganizations []string
}
testCases := []struct {
name string
settings map[string]any
want settingFields
}{
{
name: "nameAttributePath is set",
settings: map[string]any{
"name_attribute_path": "name",
},
want: settingFields{
nameAttributePath: "name",
loginAttributePath: "",
idTokenAttributeName: "",
teamIds: []string{},
allowedOrganizations: []string{},
},
},
{
name: "loginAttributePath is set",
settings: map[string]any{
"login_attribute_path": "login",
},
want: settingFields{
nameAttributePath: "",
loginAttributePath: "login",
idTokenAttributeName: "",
teamIds: []string{},
allowedOrganizations: []string{},
},
},
{
name: "idTokenAttributeName is set",
settings: map[string]any{
"id_token_attribute_name": "id_token",
},
want: settingFields{
nameAttributePath: "",
loginAttributePath: "",
idTokenAttributeName: "id_token",
teamIds: []string{},
allowedOrganizations: []string{},
},
},
{
name: "teamIds is set",
settings: map[string]any{
"team_ids": "[\"team1\", \"team2\"]",
},
want: settingFields{
nameAttributePath: "",
loginAttributePath: "",
idTokenAttributeName: "",
teamIds: []string{"team1", "team2"},
allowedOrganizations: []string{},
},
},
{
name: "allowedOrganizations is set",
settings: map[string]any{
"allowed_organizations": "org1, org2",
},
want: settingFields{
nameAttributePath: "",
loginAttributePath: "",
idTokenAttributeName: "",
teamIds: []string{},
allowedOrganizations: []string{"org1", "org2"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s, err := NewGenericOAuthProvider(tc.settings, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
require.Equal(t, tc.want.nameAttributePath, s.nameAttributePath)
require.Equal(t, tc.want.loginAttributePath, s.loginAttributePath)
require.Equal(t, tc.want.idTokenAttributeName, s.idTokenAttributeName)
require.Equal(t, tc.want.teamIds, s.teamIds)
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
})
}
}

View File

@@ -7,14 +7,20 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"strings" "strings"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil" "github.com/grafana/grafana/pkg/util/errutil"
) )
const gitHubProviderName = "github"
type SocialGithub struct { type SocialGithub struct {
*SocialBase *SocialBase
allowedOrganizations []string allowedOrganizations []string
@@ -43,6 +49,31 @@ var (
"User is not a member of one of the required organizations. Please contact identity provider administrator.")) "User is not a member of one of the required organizations. Please contact identity provider administrator."))
) )
func NewGitHubProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGithub, error) {
info, err := createOAuthInfoFromKeyValues(settings)
if err != nil {
return nil, err
}
teamIds, err := mustInts(util.SplitString(info.Extra["team_ids"]))
if err != nil {
return nil, err
}
config := createOAuthConfig(info, cfg, gitHubProviderName)
provider := &SocialGithub{
SocialBase: newSocialBase(gitHubProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamIds: teamIds,
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
skipOrgRoleSync: cfg.GitHubSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync
}
return provider, nil
}
func (s *SocialGithub) IsTeamMember(ctx context.Context, client *http.Client) bool { func (s *SocialGithub) IsTeamMember(ctx context.Context, client *http.Client) bool {
if len(s.teamIds) == 0 { if len(s.teamIds) == 0 {
return true return true
@@ -276,6 +307,10 @@ func (t *GithubTeam) GetShorthand() (string, error) {
return fmt.Sprintf("@%s/%s", t.Organization.Login, t.Slug), nil return fmt.Sprintf("@%s/%s", t.Organization.Login, t.Slug), nil
} }
func (s *SocialGithub) GetOAuthInfo() *OAuthInfo {
return s.info
}
func convertToGroupList(t []GithubTeam) []string { func convertToGroupList(t []GithubTeam) []string {
groups := make([]string, 0) groups := make([]string, 0)
for _, team := range t { for _, team := range t {
@@ -291,3 +326,15 @@ func convertToGroupList(t []GithubTeam) []string {
return groups return groups
} }
func mustInts(s []string) ([]int, error) {
result := make([]int, 0, len(s))
for _, v := range s {
num, err := strconv.Atoi(v)
if err != nil {
return nil, err
}
result = append(result, num)
}
return result, nil
}

View File

@@ -12,6 +12,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
) )
const testGHUserTeamsJSON = `[ const testGHUserTeamsJSON = `[
@@ -238,14 +239,16 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
})) }))
defer server.Close() defer server.Close()
s := &SocialGithub{ s, err := NewGitHubProvider(map[string]any{
SocialBase: newSocialBase("github", &oauth2.Config{}, "allowed_organizations": "",
&OAuthInfo{RoleAttributePath: tt.roleAttributePath}, tt.autoAssignOrgRole, false, *featuremgmt.WithFeatures()), "api_url": server.URL + "/user",
allowedOrganizations: []string{}, "team_ids": "",
apiUrl: server.URL + "/user", "role_attribute_path": tt.roleAttributePath,
teamIds: []int{}, }, &setting.Cfg{
skipOrgRoleSync: tt.settingSkipOrgRoleSync, AutoAssignOrgRole: tt.autoAssignOrgRole,
} GitHubSkipOrgRoleSync: tt.settingSkipOrgRoleSync,
}, featuremgmt.WithFeatures())
require.NoError(t, err)
token := &oauth2.Token{ token := &oauth2.Token{
AccessToken: "fake_token", AccessToken: "fake_token",
@@ -262,3 +265,57 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
}) })
} }
} }
func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
type settingFields struct {
teamIds []int
allowedOrganizations []string
}
testCases := []struct {
name string
settings map[string]any
want settingFields
}{
{
name: "teamIds is set",
settings: map[string]any{
"team_ids": "1234,5678",
},
want: settingFields{
teamIds: []int{1234, 5678},
allowedOrganizations: []string{},
},
},
{
name: "allowedOrganizations is set",
settings: map[string]any{
"allowed_organizations": "uuid-1234,uuid-5678",
},
want: settingFields{
teamIds: []int{},
allowedOrganizations: []string{"uuid-1234", "uuid-5678"},
},
},
{
name: "teamIds and allowedOrganizations are empty",
settings: map[string]any{
"team_ids": "",
"allowed_organizations": "",
},
want: settingFields{
teamIds: []int{},
allowedOrganizations: []string{},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s, err := NewGitHubProvider(tc.settings, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
require.Equal(t, tc.want.teamIds, s.teamIds)
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
})
}
}

View File

@@ -12,12 +12,14 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
const ( const (
groupPerPage = 50 groupPerPage = 50
accessLevelGuest = "10" accessLevelGuest = "10"
gitlabProviderName = "gitlab"
) )
type SocialGitlab struct { type SocialGitlab struct {
@@ -47,6 +49,24 @@ type userData struct {
IsGrafanaAdmin *bool `json:"-"` IsGrafanaAdmin *bool `json:"-"`
} }
func NewGitLabProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGitlab, error) {
info, err := createOAuthInfoFromKeyValues(settings)
if err != nil {
return nil, err
}
config := createOAuthConfig(info, cfg, gitlabProviderName)
provider := &SocialGitlab{
SocialBase: newSocialBase(gitlabProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync
}
return provider, nil
}
func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string { func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string {
groups := make([]string, 0) groups := make([]string, 0)
nextPage := new(int) nextPage := new(int)
@@ -162,6 +182,10 @@ func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token
return userInfo, nil return userInfo, nil
} }
func (s *SocialGitlab) GetOAuthInfo() *OAuthInfo {
return s.info
}
func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) { func (s *SocialGitlab) extractFromAPI(ctx context.Context, client *http.Client, token *oauth2.Token) (*userData, error) {
apiResp := &apiData{} apiResp := &apiData{}
response, err := s.httpGet(ctx, client, s.apiUrl+"/user") response, err := s.httpGet(ctx, client, s.apiUrl+"/user")

View File

@@ -15,7 +15,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
) )
const ( const (
@@ -36,12 +38,9 @@ const (
func TestSocialGitlab_UserInfo(t *testing.T) { func TestSocialGitlab_UserInfo(t *testing.T) {
var nilPointer *bool var nilPointer *bool
provider := SocialGitlab{
SocialBase: &SocialBase{ provider, err := NewGitLabProvider(map[string]any{"skip_org_role_sync": false}, &setting.Cfg{}, featuremgmt.WithFeatures())
log: newLogger("gitlab_oauth_test", "debug"), require.NoError(t, err)
},
skipOrgRoleSync: false,
}
type conf struct { type conf struct {
AllowAssignGrafanaAdmin bool AllowAssignGrafanaAdmin bool
@@ -217,7 +216,7 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
switch r.URL.Path { switch r.URL.Path {
case "/oauth/token": case "/oauth/token":
// Return a dummy access token // Return a dummy access token
_ = json.NewEncoder(w).Encode(map[string]interface{}{ _ = json.NewEncoder(w).Encode(map[string]any{
"access_token": "dummy_access_token", "access_token": "dummy_access_token",
"token_type": "Bearer", "token_type": "Bearer",
}) })
@@ -347,26 +346,27 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
// Create a test client with a dummy token // Create a test client with a dummy token
client := oauth2.NewClient(context.Background(), &tokenSource{accessToken: "dummy_access_token"}) client := oauth2.NewClient(context.Background(), &tokenSource{accessToken: "dummy_access_token"})
// Create a test SocialGitlab instance s, err := NewGitLabProvider(map[string]any{
s := &SocialGitlab{ "allowed_domains": []string{},
SocialBase: &SocialBase{ "allow_sign_up": false,
Config: tc.config, "role_attribute_path": "",
log: newLogger("test", "debug"), "role_attribute_strict": false,
allowSignup: false, "skip_org_role_sync": false,
allowedDomains: []string{}, "auth_url": tc.config.Endpoint.AuthURL,
roleAttributePath: "", "token_url": tc.config.Endpoint.TokenURL,
roleAttributeStrict: false, },
autoAssignOrgRole: "", &setting.Cfg{
skipOrgRoleSync: false, AutoAssignOrgRole: "",
}, OAuthSkipOrgRoleUpdateSync: false,
skipOrgRoleSync: false, }, featuremgmt.WithFeatures())
}
require.NoError(t, err)
// Test case: successful extraction // Test case: successful extraction
token := &oauth2.Token{} token := &oauth2.Token{}
// build jwt // build jwt
// header // header
header := map[string]interface{}{ header := map[string]any{
"alg": "RS256", "alg": "RS256",
"typ": "JWT", "typ": "JWT",
"kid": "dummy", "kid": "dummy",
@@ -381,7 +381,7 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
// build token // build token
idToken := fmt.Sprintf("%s.%s.%s", headerEncoded, payloadEncoded, signatureEncoded) idToken := fmt.Sprintf("%s.%s.%s", headerEncoded, payloadEncoded, signatureEncoded)
token = token.WithExtra(map[string]interface{}{"id_token": idToken}) token = token.WithExtra(map[string]any{"id_token": idToken})
data, err := s.extractFromToken(context.Background(), client, token) data, err := s.extractFromToken(context.Background(), client, token)
if tc.wantErrMessage != "" { if tc.wantErrMessage != "" {
require.Error(t, err) require.Error(t, err)
@@ -450,12 +450,8 @@ func TestSocialGitlab_GetGroupsNextPage(t *testing.T) {
defer mockServer.Close() defer mockServer.Close()
// Create a SocialGitlab instance with the mock server URL // Create a SocialGitlab instance with the mock server URL
s := &SocialGitlab{ s, err := NewGitLabProvider(map[string]any{"api_url": mockServer.URL}, &setting.Cfg{}, featuremgmt.WithFeatures())
apiUrl: mockServer.URL, require.NoError(t, err)
SocialBase: &SocialBase{
log: newLogger("test", "debug"),
},
}
// Call getGroups and verify that it returns all groups // Call getGroups and verify that it returns all groups
expectedGroups := []string{"admins", "editors", "viewers", "serveradmins"} expectedGroups := []string{"admins", "editors", "viewers", "serveradmins"}

View File

@@ -14,9 +14,12 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
const legacyAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo" const (
const googleIAMGroupsEndpoint = "https://content-cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups" legacyAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo"
const googleIAMScope = "https://www.googleapis.com/auth/cloud-identity.groups.readonly" googleIAMGroupsEndpoint = "https://content-cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups"
googleIAMScope = "https://www.googleapis.com/auth/cloud-identity.groups.readonly"
googleProviderName = "google"
)
type SocialGoogle struct { type SocialGoogle struct {
*SocialBase *SocialBase
@@ -33,6 +36,29 @@ type googleUserData struct {
rawJSON []byte `json:"-"` rawJSON []byte `json:"-"`
} }
func NewGoogleProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGoogle, error) {
info, err := createOAuthInfoFromKeyValues(settings)
if err != nil {
return nil, err
}
config := createOAuthConfig(info, cfg, googleProviderName)
provider := &SocialGoogle{
SocialBase: newSocialBase(googleProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
hostedDomain: info.HostedDomain,
apiUrl: info.ApiUrl,
skipOrgRoleSync: cfg.GoogleSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync
}
if strings.HasPrefix(info.ApiUrl, legacyAPIURL) {
provider.log.Warn("Using legacy Google API URL, please update your configuration")
}
return provider, nil
}
func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
data, errToken := s.extractFromToken(ctx, client, token) data, errToken := s.extractFromToken(ctx, client, token)
if errToken != nil { if errToken != nil {
@@ -92,6 +118,10 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
return userInfo, nil return userInfo, nil
} }
func (s *SocialGoogle) GetOAuthInfo() *OAuthInfo {
return s.info
}
type googleAPIData struct { type googleAPIData struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`

View File

@@ -14,8 +14,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
) )
func TestSocialGoogle_retrieveGroups(t *testing.T) { func TestSocialGoogle_retrieveGroups(t *testing.T) {
@@ -180,21 +181,23 @@ func TestSocialGoogle_retrieveGroups(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &SocialGoogle{ s, err := NewGoogleProvider(map[string]any{
SocialBase: &SocialBase{ "api_url": "",
Config: &oauth2.Config{Scopes: tt.fields.Scopes}, "scopes": tt.fields.Scopes,
log: log.NewNopLogger(), "hosted_domain": "",
allowSignup: false, "allowed_domains": []string{},
allowAssignGrafanaAdmin: false, "allow_sign_up": false,
allowedDomains: []string{}, "role_attribute_path": "",
roleAttributePath: "", "role_attribute_strict": false,
roleAttributeStrict: false, "allow_assign_grafana_admin": false,
autoAssignOrgRole: "", },
skipOrgRoleSync: false, &setting.Cfg{
AutoAssignOrgRole: "",
GoogleSkipOrgRoleSync: false,
}, },
hostedDomain: "", featuremgmt.WithFeatures())
apiUrl: "", require.NoError(t, err)
}
got, err := s.retrieveGroups(context.Background(), tt.args.client, tt.args.userData) got, err := s.retrieveGroups(context.Background(), tt.args.client, tt.args.userData)
if (err != nil) != tt.wantErr { if (err != nil) != tt.wantErr {
t.Errorf("SocialGoogle.retrieveGroups() error = %v, wantErr %v", err, tt.wantErr) t.Errorf("SocialGoogle.retrieveGroups() error = %v, wantErr %v", err, tt.wantErr)
@@ -632,19 +635,20 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
s := &SocialGoogle{ s, err := NewGoogleProvider(map[string]any{
apiUrl: tt.fields.apiURL, "api_url": tt.fields.apiURL,
SocialBase: &SocialBase{ "scopes": tt.fields.Scopes,
Config: &oauth2.Config{Scopes: tt.fields.Scopes}, "allowed_groups": tt.fields.allowedGroups,
log: log.NewNopLogger(), "allow_sign_up": false,
allowSignup: false, "role_attribute_path": tt.fields.roleAttributePath,
allowedGroups: tt.fields.allowedGroups, "role_attribute_strict": tt.fields.roleAttributeStrict,
roleAttributePath: tt.fields.roleAttributePath, "allow_assign_grafana_admin": tt.fields.allowAssignGrafanaAdmin,
roleAttributeStrict: tt.fields.roleAttributeStrict, },
allowAssignGrafanaAdmin: tt.fields.allowAssignGrafanaAdmin, &setting.Cfg{
GoogleSkipOrgRoleSync: tt.fields.skipOrgRoleSync,
}, },
skipOrgRoleSync: tt.fields.skipOrgRoleSync, featuremgmt.WithFeatures())
} require.NoError(t, err)
gotData, err := s.UserInfo(context.Background(), tt.args.client, tt.args.token) gotData, err := s.UserInfo(context.Background(), tt.args.client, tt.args.token)
if tt.wantErr { if tt.wantErr {

View File

@@ -9,9 +9,14 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
const grafanaComProviderName = "grafana_com"
type SocialGrafanaCom struct { type SocialGrafanaCom struct {
*SocialBase *SocialBase
url string url string
@@ -23,6 +28,30 @@ type OrgRecord struct {
Login string `json:"login"` Login string `json:"login"`
} }
func NewGrafanaComProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialGrafanaCom, error) {
info, err := createOAuthInfoFromKeyValues(settings)
if err != nil {
return nil, err
}
// Override necessary settings
info.AuthUrl = cfg.GrafanaComURL + "/oauth2/authorize"
info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token"
info.AuthStyle = "inheader"
config := createOAuthConfig(info, cfg, grafanaComProviderName)
provider := &SocialGrafanaCom{
SocialBase: newSocialBase(grafanaComProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
url: cfg.GrafanaComURL,
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
skipOrgRoleSync: cfg.GrafanaComSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync
}
return provider, nil
}
func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool { func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool {
return true return true
} }
@@ -86,3 +115,7 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _
return userInfo, nil return userInfo, nil
} }
func (s *SocialGrafanaCom) GetOAuthInfo() *OAuthInfo {
return s.info
}

View File

@@ -6,6 +6,8 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -23,11 +25,8 @@ const (
) )
func TestSocialGrafanaCom_UserInfo(t *testing.T) { func TestSocialGrafanaCom_UserInfo(t *testing.T) {
provider := SocialGrafanaCom{ provider, err := NewGrafanaComProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
SocialBase: &SocialBase{ require.NoError(t, err)
log: newLogger("grafana_com_oauth_test", "debug"),
},
}
type conf struct { type conf struct {
skipOrgRoleSync bool skipOrgRoleSync bool
@@ -93,3 +92,40 @@ func TestSocialGrafanaCom_UserInfo(t *testing.T) {
}) })
} }
} }
func TestSocialGrafanaCom_InitializeExtraFields(t *testing.T) {
type settingFields struct {
allowedOrganizations []string
}
testCases := []struct {
name string
settings map[string]any
want settingFields
}{
{
name: "allowedOrganizations is not set",
settings: map[string]any{},
want: settingFields{
allowedOrganizations: []string{},
},
},
{
name: "allowedOrganizations is set",
settings: map[string]any{
"allowed_organizations": "uuid-1234,uuid-5678",
},
want: settingFields{
allowedOrganizations: []string{"uuid-1234", "uuid-5678"},
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s, err := NewGrafanaComProvider(tc.settings, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
})
}
}

View File

@@ -11,8 +11,12 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
) )
const oktaProviderName = "okta"
type SocialOkta struct { type SocialOkta struct {
*SocialBase *SocialBase
apiUrl string apiUrl string
@@ -39,6 +43,29 @@ type OktaClaims struct {
Name string `json:"name"` Name string `json:"name"`
} }
func NewOktaProvider(settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager) (*SocialOkta, error) {
info, err := createOAuthInfoFromKeyValues(settings)
if err != nil {
return nil, err
}
config := createOAuthConfig(info, cfg, oktaProviderName)
provider := &SocialOkta{
SocialBase: newSocialBase(oktaProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
allowedGroups: info.AllowedGroups,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync
skipOrgRoleSync: cfg.OktaSkipOrgRoleSync,
}
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(config, OfflineAccessScope)
}
return provider, nil
}
func (claims *OktaClaims) extractEmail() string { func (claims *OktaClaims) extractEmail() string {
if claims.Email == "" && claims.PreferredUsername != "" { if claims.Email == "" && claims.PreferredUsername != "" {
return claims.PreferredUsername return claims.PreferredUsername
@@ -107,6 +134,10 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o
}, nil }, nil
} }
func (s *SocialOkta) GetOAuthInfo() *OAuthInfo {
return s.info
}
func (s *SocialOkta) extractAPI(ctx context.Context, data *OktaUserInfoJson, client *http.Client) error { func (s *SocialOkta) extractAPI(ctx context.Context, data *OktaUserInfoJson, client *http.Client) error {
rawUserInfoResponse, err := s.httpGet(ctx, client, s.apiUrl) rawUserInfoResponse, err := s.httpGet(ctx, client, s.apiUrl)
if err != nil { if err != nil {
@@ -134,6 +165,7 @@ func (s *SocialOkta) GetGroups(data *OktaUserInfoJson) []string {
return groups return groups
} }
// TODO: remove this in a separate PR and use the isGroupMember from the social.go
func (s *SocialOkta) IsGroupMember(groups []string) bool { func (s *SocialOkta) IsGroupMember(groups []string) bool {
if len(s.allowedGroups) == 0 { if len(s.allowedGroups) == 0 {
return true return true

View File

@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/models/roletype" "github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
) )
func TestSocialOkta_UserInfo(t *testing.T) { func TestSocialOkta_UserInfo(t *testing.T) {
@@ -95,14 +96,22 @@ func TestSocialOkta_UserInfo(t *testing.T) {
} }
})) }))
defer server.Close() defer server.Close()
provider := &SocialOkta{
SocialBase: newSocialBase("okta", &oauth2.Config{}, provider, err := NewOktaProvider(
&OAuthInfo{RoleAttributePath: tt.RoleAttributePath}, tt.autoAssignOrgRole, false, *featuremgmt.WithFeatures()), map[string]any{
apiUrl: server.URL + "/user", "api_url": server.URL + "/user",
skipOrgRoleSync: tt.settingSkipOrgRoleSync, "role_attribute_path": tt.RoleAttributePath,
} "allow_assign_grafana_admin": tt.allowAssignGrafanaAdmin,
provider.allowAssignGrafanaAdmin = tt.allowAssignGrafanaAdmin "skip_org_role_sync": tt.settingSkipOrgRoleSync,
provider.roleAttributePath = tt.RoleAttributePath },
&setting.Cfg{
OktaSkipOrgRoleSync: tt.settingSkipOrgRoleSync,
AutoAssignOrgRole: tt.autoAssignOrgRole,
OAuthSkipOrgRoleUpdateSync: false,
},
featuremgmt.WithFeatures())
require.NoError(t, err)
// create a oauth2 token with a id_token // create a oauth2 token with a id_token
staticToken := oauth2.Token{ staticToken := oauth2.Token{
AccessToken: "", AccessToken: "",

View File

@@ -29,7 +29,6 @@ import (
"github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/supportbundles" "github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
) )
const ( const (
@@ -45,34 +44,37 @@ type SocialService struct {
} }
type OAuthInfo struct { type OAuthInfo struct {
ApiUrl string `toml:"api_url"` ApiUrl string `mapstructure:"api_url"`
AuthUrl string `toml:"auth_url"` AuthUrl string `mapstructure:"auth_url"`
ClientId string `toml:"client_id"` AuthStyle string `mapstructure:"auth_style"`
ClientSecret string `toml:"-"` ClientId string `mapstructure:"client_id"`
EmailAttributeName string `toml:"email_attribute_name"` ClientSecret string `mapstructure:"client_secret"`
EmailAttributePath string `toml:"email_attribute_path"` EmailAttributeName string `mapstructure:"email_attribute_name"`
GroupsAttributePath string `toml:"groups_attribute_path"` EmailAttributePath string `mapstructure:"email_attribute_path"`
HostedDomain string `toml:"hosted_domain"` EmptyScopes bool `mapstructure:"empty_scopes"`
Icon string `toml:"icon"` GroupsAttributePath string `mapstructure:"groups_attribute_path"`
Name string `toml:"name"` HostedDomain string `mapstructure:"hosted_domain"`
RoleAttributePath string `toml:"role_attribute_path"` Icon string `mapstructure:"icon"`
TeamIdsAttributePath string `toml:"team_ids_attribute_path"` Name string `mapstructure:"name"`
TeamsUrl string `toml:"teams_url"` RoleAttributePath string `mapstructure:"role_attribute_path"`
TlsClientCa string `toml:"tls_client_ca"` TeamIdsAttributePath string `mapstructure:"team_ids_attribute_path"`
TlsClientCert string `toml:"tls_client_cert"` TeamsUrl string `mapstructure:"teams_url"`
TlsClientKey string `toml:"tls_client_key"` TlsClientCa string `mapstructure:"tls_client_ca"`
TokenUrl string `toml:"token_url"` TlsClientCert string `mapstructure:"tls_client_cert"`
AllowedDomains []string `toml:"allowed_domains"` TlsClientKey string `mapstructure:"tls_client_key"`
AllowedGroups []string `toml:"allowed_groups"` TokenUrl string `mapstructure:"token_url"`
Scopes []string `toml:"scopes"` AllowedDomains []string `mapstructure:"allowed_domains"`
AllowAssignGrafanaAdmin bool `toml:"allow_assign_grafana_admin"` AllowedGroups []string `mapstructure:"allowed_groups"`
AllowSignup bool `toml:"allow_signup"` Scopes []string `mapstructure:"scopes"`
AutoLogin bool `toml:"auto_login"` AllowAssignGrafanaAdmin bool `mapstructure:"allow_assign_grafana_admin"`
Enabled bool `toml:"enabled"` AllowSignup bool `mapstructure:"allow_sign_up"`
RoleAttributeStrict bool `toml:"role_attribute_strict"` AutoLogin bool `mapstructure:"auto_login"`
TlsSkipVerify bool `toml:"tls_skip_verify"` Enabled bool `mapstructure:"enabled"`
UsePKCE bool `toml:"use_pkce"` RoleAttributeStrict bool `mapstructure:"role_attribute_strict"`
UseRefreshToken bool `toml:"use_refresh_token"` TlsSkipVerify bool `mapstructure:"tls_skip_verify_insecure"`
UsePKCE bool `mapstructure:"use_pkce"`
UseRefreshToken bool `mapstructure:"use_refresh_token"`
Extra map[string]string `mapstructure:",remain"`
} }
func ProvideService(cfg *setting.Cfg, func ProvideService(cfg *setting.Cfg,
@@ -93,40 +95,11 @@ func ProvideService(cfg *setting.Cfg,
for _, name := range allOauthes { for _, name := range allOauthes {
sec := cfg.Raw.Section("auth." + name) sec := cfg.Raw.Section("auth." + name)
info := &OAuthInfo{ settingsKVs := convertIniSectionToMap(sec)
ClientId: sec.Key("client_id").String(), info, err := createOAuthInfoFromKeyValues(settingsKVs)
ClientSecret: sec.Key("client_secret").String(), if err != nil {
Scopes: util.SplitString(sec.Key("scopes").String()), ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", name)
AuthUrl: sec.Key("auth_url").String(), continue
TokenUrl: sec.Key("token_url").String(),
ApiUrl: sec.Key("api_url").String(),
TeamsUrl: sec.Key("teams_url").String(),
Enabled: sec.Key("enabled").MustBool(),
EmailAttributeName: sec.Key("email_attribute_name").String(),
EmailAttributePath: sec.Key("email_attribute_path").String(),
RoleAttributePath: sec.Key("role_attribute_path").String(),
RoleAttributeStrict: sec.Key("role_attribute_strict").MustBool(),
GroupsAttributePath: sec.Key("groups_attribute_path").String(),
TeamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()),
HostedDomain: sec.Key("hosted_domain").String(),
AllowSignup: sec.Key("allow_sign_up").MustBool(),
Name: sec.Key("name").MustString(name),
Icon: sec.Key("icon").String(),
TlsClientCert: sec.Key("tls_client_cert").String(),
TlsClientKey: sec.Key("tls_client_key").String(),
TlsClientCa: sec.Key("tls_client_ca").String(),
TlsSkipVerify: sec.Key("tls_skip_verify_insecure").MustBool(),
UsePKCE: sec.Key("use_pkce").MustBool(),
UseRefreshToken: sec.Key("use_refresh_token").MustBool(false),
AllowAssignGrafanaAdmin: sec.Key("allow_assign_grafana_admin").MustBool(false),
AutoLogin: sec.Key("auto_login").MustBool(false),
AllowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
}
// when empty_scopes parameter exists and is true, overwrite scope with empty value
if sec.Key("empty_scopes").MustBool() {
info.Scopes = []string{}
} }
if !info.Enabled { if !info.Enabled {
@@ -137,133 +110,13 @@ func ProvideService(cfg *setting.Cfg,
name = grafanaCom name = grafanaCom
} }
ss.oAuthProvider[name] = info conn, err := ss.createOAuthConnector(name, settingsKVs, cfg, features, cache)
if err != nil {
var authStyle oauth2.AuthStyle ss.log.Error("Failed to create OAuth provider", "error", err, "provider", name)
switch strings.ToLower(sec.Key("auth_style").String()) {
case "inparams":
authStyle = oauth2.AuthStyleInParams
case "inheader":
authStyle = oauth2.AuthStyleInHeader
case "autodetect", "":
authStyle = oauth2.AuthStyleAutoDetect
default:
ss.log.Warn("Invalid auth style specified, defaulting to auth style AutoDetect", "auth_style", sec.Key("auth_style").String())
authStyle = oauth2.AuthStyleAutoDetect
} }
config := oauth2.Config{ ss.socialMap[name] = conn
ClientID: info.ClientId, ss.oAuthProvider[name] = ss.socialMap[name].GetOAuthInfo()
ClientSecret: info.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: info.AuthUrl,
TokenURL: info.TokenUrl,
AuthStyle: authStyle,
},
RedirectURL: strings.TrimSuffix(cfg.AppURL, "/") + SocialBaseUrl + name,
Scopes: info.Scopes,
}
// GitHub.
if name == "github" {
ss.socialMap["github"] = &SocialGithub{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamIds: sec.Key("team_ids").Ints(","),
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
skipOrgRoleSync: cfg.GitHubSkipOrgRoleSync,
}
}
// GitLab.
if name == "gitlab" {
ss.socialMap["gitlab"] = &SocialGitlab{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync,
}
}
// Google.
if name == "google" {
if strings.HasPrefix(info.ApiUrl, legacyAPIURL) {
ss.log.Warn("Using legacy Google API URL, please update your configuration")
}
ss.socialMap["google"] = &SocialGoogle{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
hostedDomain: info.HostedDomain,
apiUrl: info.ApiUrl,
skipOrgRoleSync: cfg.GoogleSkipOrgRoleSync,
}
}
// AzureAD.
if name == "azuread" {
ss.socialMap["azuread"] = &SocialAzureAD{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
cache: cache,
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
forceUseGraphAPI: sec.Key("force_use_graph_api").MustBool(false),
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
}
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(&config, OfflineAccessScope)
}
}
// Okta
if name == "okta" {
ss.socialMap["okta"] = &SocialOkta{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
skipOrgRoleSync: cfg.OktaSkipOrgRoleSync,
}
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(&config, OfflineAccessScope)
}
}
// Generic - Uses the same scheme as GitHub.
if name == "generic_oauth" {
ss.socialMap["generic_oauth"] = &SocialGenericOAuth{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamsUrl: info.TeamsUrl,
emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath,
nameAttributePath: sec.Key("name_attribute_path").String(),
groupsAttributePath: info.GroupsAttributePath,
loginAttributePath: sec.Key("login_attribute_path").String(),
idTokenAttributeName: sec.Key("id_token_attribute_name").String(),
teamIdsAttributePath: sec.Key("team_ids_attribute_path").String(),
teamIds: sec.Key("team_ids").Strings(","),
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
allowedGroups: util.SplitString(sec.Key("allowed_groups").String()),
skipOrgRoleSync: cfg.GenericOAuthSkipOrgRoleSync,
}
}
if name == grafanaCom {
config = oauth2.Config{
ClientID: info.ClientId,
ClientSecret: info.ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: cfg.GrafanaComURL + "/oauth2/authorize",
TokenURL: cfg.GrafanaComURL + "/api/oauth2/token",
AuthStyle: oauth2.AuthStyleInHeader,
},
RedirectURL: strings.TrimSuffix(cfg.AppURL, "/") + SocialBaseUrl + name,
Scopes: info.Scopes,
}
ss.socialMap[grafanaCom] = &SocialGrafanaCom{
SocialBase: newSocialBase(name, &config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
url: cfg.GrafanaComURL,
allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
skipOrgRoleSync: cfg.GrafanaComSkipOrgRoleSync,
}
}
} }
ss.registerSupportBundleCollectors(bundleRegistry) ss.registerSupportBundleCollectors(bundleRegistry)
@@ -292,6 +145,8 @@ type SocialConnector interface {
IsEmailAllowed(email string) bool IsEmailAllowed(email string) bool
IsSignupAllowed() bool IsSignupAllowed() bool
GetOAuthInfo() *OAuthInfo
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error)
Client(ctx context.Context, t *oauth2.Token) *http.Client Client(ctx context.Context, t *oauth2.Token) *http.Client
@@ -301,6 +156,7 @@ type SocialConnector interface {
type SocialBase struct { type SocialBase struct {
*oauth2.Config *oauth2.Config
info *OAuthInfo
log log.Logger log log.Logger
allowSignup bool allowSignup bool
allowAssignGrafanaAdmin bool allowAssignGrafanaAdmin bool
@@ -353,6 +209,7 @@ func newSocialBase(name string,
return &SocialBase{ return &SocialBase{
Config: config, Config: config,
info: info,
log: logger, log: logger,
allowSignup: info.AllowSignup, allowSignup: info.AllowSignup,
allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin, allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
@@ -553,8 +410,8 @@ func (ss *SocialService) GetOAuthInfoProviders() map[string]*OAuthInfo {
return ss.oAuthProvider return ss.oAuthProvider
} }
func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]interface{}, error) { func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]any, error) {
m := map[string]interface{}{} m := map[string]any{}
authTypes := map[string]bool{} authTypes := map[string]bool{}
for provider, enabled := range ss.GetOAuthProviders() { for provider, enabled := range ss.GetOAuthProviders() {
@@ -589,7 +446,7 @@ func (s *SocialBase) isGroupMember(groups []string) bool {
return false return false
} }
func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) { func (s *SocialBase) retrieveRawIDToken(idToken any) ([]byte, error) {
tokenString, ok := idToken.(string) tokenString, ok := idToken.(string)
if !ok { if !ok {
return nil, fmt.Errorf("id_token is not a string: %v", idToken) return nil, fmt.Errorf("id_token is not a string: %v", idToken)
@@ -611,7 +468,7 @@ func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
return nil, fmt.Errorf("error base64 decoding header: %w", err) return nil, fmt.Errorf("error base64 decoding header: %w", err)
} }
var header map[string]interface{} var header map[string]any
if err := json.Unmarshal(headerBytes, &header); err != nil { if err := json.Unmarshal(headerBytes, &header); err != nil {
return nil, fmt.Errorf("error deserializing header: %w", err) return nil, fmt.Errorf("error deserializing header: %w", err)
} }
@@ -645,6 +502,27 @@ func (s *SocialBase) retrieveRawIDToken(idToken interface{}) ([]byte, error) {
return rawJSON, nil return rawJSON, nil
} }
func (ss *SocialService) createOAuthConnector(name string, settings map[string]any, cfg *setting.Cfg, features *featuremgmt.FeatureManager, cache remotecache.CacheStorage) (SocialConnector, error) {
switch name {
case azureADProviderName:
return NewAzureADProvider(settings, cfg, features, cache)
case genericOAuthProviderName:
return NewGenericOAuthProvider(settings, cfg, features)
case gitHubProviderName:
return NewGitHubProvider(settings, cfg, features)
case gitlabProviderName:
return NewGitLabProvider(settings, cfg, features)
case googleProviderName:
return NewGoogleProvider(settings, cfg, features)
case grafanaComProviderName:
return NewGrafanaComProvider(settings, cfg, features)
case oktaProviderName:
return NewOktaProvider(settings, cfg, features)
default:
return nil, fmt.Errorf("unknown oauth provider: %s", name)
}
}
func appendUniqueScope(config *oauth2.Config, scope string) { func appendUniqueScope(config *oauth2.Config, scope string) {
if !slices.Contains(config.Scopes, OfflineAccessScope) { if !slices.Contains(config.Scopes, OfflineAccessScope) {
config.Scopes = append(config.Scopes, OfflineAccessScope) config.Scopes = append(config.Scopes, OfflineAccessScope)

View File

@@ -90,6 +90,22 @@ func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOp
return r0, r1 return r0, r1
} }
// GetOAuthInfo provides a mock function with given fields:
func (_m *MockSocialConnector) GetOAuthInfo() *social.OAuthInfo {
ret := _m.Called()
var r0 *social.OAuthInfo
if rf, ok := ret.Get(0).(func() *social.OAuthInfo); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*social.OAuthInfo)
}
}
return r0
}
// IsEmailAllowed provides a mock function with given fields: email // IsEmailAllowed provides a mock function with given fields: email
func (_m *MockSocialConnector) IsEmailAllowed(email string) bool { func (_m *MockSocialConnector) IsEmailAllowed(email string) bool {
ret := _m.Called(email) ret := _m.Called(email)

View File

@@ -29,29 +29,32 @@ func (s *OAuthStrategy) IsMatch(provider string) bool {
return s.supportedProvidersRegex.MatchString(provider) return s.supportedProvidersRegex.MatchString(provider)
} }
func (s *OAuthStrategy) ParseConfigFromSystem(_ context.Context) (map[string]interface{}, error) { func (s *OAuthStrategy) ParseConfigFromSystem(_ context.Context) (map[string]any, error) {
section := s.cfg.SectionWithEnvOverrides("auth." + s.provider) section := s.cfg.SectionWithEnvOverrides("auth." + s.provider)
result := map[string]interface{}{ // TODO: load the provider specific keys separately
"client_id": section.Key("client_id").Value(), result := map[string]any{
"client_secret": section.Key("client_secret").Value(), "client_id": section.Key("client_id").Value(),
"scopes": section.Key("scopes").Value(), "client_secret": section.Key("client_secret").Value(),
"auth_url": section.Key("auth_url").Value(), "scopes": section.Key("scopes").Value(),
"token_url": section.Key("token_url").Value(), "auth_url": section.Key("auth_url").Value(),
"api_url": section.Key("api_url").Value(), "token_url": section.Key("token_url").Value(),
"teams_url": section.Key("teams_url").Value(), "api_url": section.Key("api_url").Value(),
"enabled": section.Key("enabled").MustBool(false), "teams_url": section.Key("teams_url").Value(),
"email_attribute_name": section.Key("email_attribute_name").Value(), "enabled": section.Key("enabled").MustBool(false),
"email_attribute_path": section.Key("email_attribute_path").Value(), "email_attribute_name": section.Key("email_attribute_name").Value(),
"role_attribute_path": section.Key("role_attribute_path").Value(), "email_attribute_path": section.Key("email_attribute_path").Value(),
"role_attribute_strict": section.Key("role_attribute_strict").MustBool(false), "role_attribute_path": section.Key("role_attribute_path").Value(),
"groups_attribute_path": section.Key("groups_attribute_path").Value(), "role_attribute_strict": section.Key("role_attribute_strict").MustBool(false),
"team_ids_attribute_path": section.Key("team_ids_attribute_path").Value(), "groups_attribute_path": section.Key("groups_attribute_path").Value(),
"allowed_domains": section.Key("allowed_domains").Value(), "team_ids_attribute_path": section.Key("team_ids_attribute_path").Value(),
"hosted_domain": section.Key("hosted_domain").Value(), "allowed_domains": section.Key("allowed_domains").Value(),
"allow_sign_up": section.Key("allow_sign_up").MustBool(true), "hosted_domain": section.Key("hosted_domain").Value(),
"name": section.Key("name").MustString("default name"), // TODO: change this default value "allow_sign_up": section.Key("allow_sign_up").MustBool(true),
"icon": section.Key("icon").Value(), "name": section.Key("name").MustString("default name"), // TODO: change this default value
"icon": section.Key("icon").Value(),
// TODO: @mgyongyosi move skipOrgRoleSync here in a separate PR
// "skip_org_role_sync": section.Key("skip_org_role_sync").MustBool(false),
"tls_client_cert": section.Key("tls_client_cert").Value(), "tls_client_cert": section.Key("tls_client_cert").Value(),
"tls_client_key": section.Key("tls_client_key").Value(), "tls_client_key": section.Key("tls_client_key").Value(),
"tls_client_ca": section.Key("tls_client_ca").Value(), "tls_client_ca": section.Key("tls_client_ca").Value(),