Auth: Use SSO settings service to load social connectors + refactor (#79005)

* Refactor to prevent cyclic dependencies

* Move list authorization to the API layer

* Init connectors using the SSO settings service in case the ssoSettingsApi feature toggle is enabled

* wip, need to handle the cyclic dep

* Remove cyclic dependency

* Align tests + refactor

* Move back OAuthInfo to social

* Delete pkg/login/social/constants

* Move reloadable registration to the social providers

* Rename connectors.Error to connectors.SocialError
This commit is contained in:
Misi 2023-12-08 11:20:42 +01:00 committed by GitHub
parent 58c5e5c9dc
commit 50f4e78a39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1173 additions and 1028 deletions

View File

@ -15,7 +15,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/socialtest"
"github.com/grafana/grafana/pkg/login/social/socialtest"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authtest"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"

View File

@ -14,7 +14,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/social/socialimpl"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
@ -25,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/updatechecker"
"github.com/grafana/grafana/pkg/setting"
@ -77,7 +78,7 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features *featuremgmt.
PluginSettings: cfg.PluginSettings,
}),
namespacer: request.GetNamespaceMapper(cfg),
SocialService: social.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage()),
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), &ssosettingstests.MockService{}),
}
m := web.New()

View File

@ -19,7 +19,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/socialtest"
"github.com/grafana/grafana/pkg/login/social/socialtest"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"bytes"

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"bytes"
@ -15,24 +15,25 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/login/social"
"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/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
AzureADProviderName = "azuread"
forceUseGraphAPIKey = "force_use_graph_api" // #nosec G101 not a hardcoded credential
)
const forceUseGraphAPIKey = "force_use_graph_api" // #nosec G101 not a hardcoded credential
var (
ExtraAzureADSettingKeys = []string{forceUseGraphAPIKey, allowedOrganizationsKey}
errAzureADMissingGroups = &Error{"either the user does not have any group membership or the groups claim is missing from the token."}
errAzureADMissingGroups = &SocialError{"either the user does not have any group membership or the groups claim is missing from the token."}
)
var _ SocialConnector = (*SocialAzureAD)(nil)
var _ social.SocialConnector = (*SocialAzureAD)(nil)
var _ ssosettings.Reloadable = (*SocialAzureAD)(nil)
type SocialAzureAD struct {
*SocialBase
@ -72,15 +73,10 @@ type keySetJWKS struct {
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)
func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager, cache remotecache.CacheStorage) *SocialAzureAD {
config := createOAuthConfig(info, cfg, social.AzureADProviderName)
provider := &SocialAzureAD{
SocialBase: newSocialBase(AzureADProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(social.AzureADProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
cache: cache,
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
forceUseGraphAPI: MustBool(info.Extra[forceUseGraphAPIKey], false),
@ -90,13 +86,17 @@ func NewAzureADProvider(settings map[string]any, cfg *setting.Cfg, features *fea
}
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(config, OfflineAccessScope)
appendUniqueScope(config, social.OfflineAccessScope)
}
return provider, nil
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettings.RegisterReloadable(social.AzureADProviderName, provider)
}
return provider
}
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) (*social.BasicUserInfo, error) {
idToken := token.Extra("id_token")
if idToken == nil {
return nil, ErrIDTokenNotFound
@ -155,7 +155,7 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
}
return &BasicUserInfo{
return &social.BasicUserInfo{
Id: claims.ID,
Name: claims.Name,
Email: email,
@ -166,7 +166,15 @@ func (s *SocialAzureAD) UserInfo(ctx context.Context, client *http.Client, token
}, nil
}
func (s *SocialAzureAD) GetOAuthInfo() *OAuthInfo {
func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialAzureAD) GetOAuthInfo() *social.OAuthInfo {
return s.info
}
@ -177,17 +185,17 @@ func (s *SocialAzureAD) validateClaims(ctx context.Context, client *http.Client,
}
if claims.OAuthVersion == "1.0" {
return nil, &Error{"AzureAD OAuth: version 1.0 is not supported. Please ensure the auth_url and token_url are set to the v2.0 endpoints."}
return nil, &SocialError{"AzureAD OAuth: version 1.0 is not supported. Please ensure the auth_url and token_url are set to the v2.0 endpoints."}
}
s.log.Debug("Validating audience", "audience", claims.Audience, "client_id", s.ClientID)
if claims.Audience != s.ClientID {
return nil, &Error{"AzureAD OAuth: audience mismatch"}
return nil, &SocialError{"AzureAD OAuth: audience mismatch"}
}
s.log.Debug("Validating tenant", "tenant", claims.TenantID, "allowed_tenants", s.allowedOrganizations)
if !s.isAllowedTenant(claims.TenantID) {
return nil, &Error{"AzureAD OAuth: tenant mismatch"}
return nil, &SocialError{"AzureAD OAuth: tenant mismatch"}
}
return claims, nil
}
@ -226,7 +234,7 @@ func (s *SocialAzureAD) validateIDTokenSignature(ctx context.Context, client *ht
s.log.Warn("AzureAD OAuth: signing key not found", "kid", keyID)
return nil, &Error{"AzureAD OAuth: signing key not found"}
return nil, &SocialError{"AzureAD OAuth: signing key not found"}
}
func (claims *azureClaims) extractEmail() string {
@ -248,11 +256,11 @@ func (s *SocialAzureAD) extractRoleAndAdmin(claims *azureClaims) (org.RoleType,
return s.defaultRole(), false, nil
}
roleOrder := []org.RoleType{RoleGrafanaAdmin, org.RoleAdmin, org.RoleEditor,
roleOrder := []org.RoleType{social.RoleGrafanaAdmin, org.RoleAdmin, org.RoleEditor,
org.RoleViewer, org.RoleNone}
for _, role := range roleOrder {
if found := hasRole(claims.Roles, role); found {
if role == RoleGrafanaAdmin {
if role == social.RoleGrafanaAdmin {
return org.RoleAdmin, true, nil
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -17,7 +17,9 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/setting"
)
@ -33,7 +35,7 @@ func falseBoolPtr() *bool {
func TestSocialAzureAD_UserInfo(t *testing.T) {
type fields struct {
providerCfg map[string]any
providerCfg *social.OAuthInfo
cfg *setting.Cfg
usGovURL bool
}
@ -47,7 +49,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
claims *azureClaims
args args
settingAutoAssignOrgRole string
want *BasicUserInfo
want *social.BasicUserInfo
wantErr bool
}{
{
@ -60,15 +62,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -80,9 +82,9 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "No email",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
@ -102,9 +104,9 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
name: "No id token",
claims: nil,
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
@ -123,16 +125,16 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
usGovURL: true,
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -151,15 +153,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
},
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -171,9 +173,9 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Admin role",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
@ -186,7 +188,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -198,9 +200,9 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Lowercase Admin role",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
@ -213,7 +215,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -225,9 +227,9 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Only other roles",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
@ -240,7 +242,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -260,15 +262,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
},
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -287,15 +289,15 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ID: "1234",
},
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
},
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -307,9 +309,9 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Admin and Editor roles in claim",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
@ -322,7 +324,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -334,10 +336,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Grafana Admin but setting is disabled",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": false,
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: false,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
@ -351,7 +353,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -364,10 +366,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Editor roles in claim and GrafanaAdminAssignment enabled",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": true,
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: true,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
@ -380,7 +382,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -393,10 +395,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Grafana Admin and Editor roles in claim",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": true,
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: true,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
@ -409,7 +411,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -422,11 +424,11 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Error if user is not a member of allowed_groups",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": false,
"allowed_groups": "dead-beef",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: false,
AllowedGroups: []string{"dead-beef"},
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
@ -446,11 +448,13 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Error if user is not a member of allowed_organizations",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": false,
"allowed_organizations": "uuid-1234",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: false,
Extra: map[string]string{
"allowed_organizations": "uuid-1234",
},
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Editor",
@ -471,10 +475,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "No error if user is a member of allowed_organizations",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allowed_organizations": "uuid-1234,uuid-5678",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
Extra: map[string]string{
"allowed_organizations": "uuid-1234,uuid-5678",
},
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
@ -489,7 +495,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -502,11 +508,11 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "No Error if user is a member of allowed_groups",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": "false",
"allowed_groups": "foo, bar",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: false,
AllowedGroups: []string{"foo", "bar"},
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
@ -520,7 +526,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -532,11 +538,11 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Error if user does not have groups but allowed groups",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": "false",
"allowed_groups": "foo, bar",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: false,
AllowedGroups: []string{"foo", "bar"},
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "Viewer",
@ -556,9 +562,9 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch groups when ClaimsNames and ClaimsSources is set",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
@ -574,7 +580,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
ClaimSources: nil, // set by the test
},
settingAutoAssignOrgRole: "",
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "test",
Email: "test@test.com",
@ -587,10 +593,12 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch groups when forceUseGraphAPI is set",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"force_use_graph_api": "true",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
Extra: map[string]string{
"force_use_graph_api": "true",
},
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
@ -607,7 +615,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
Groups: []string{"foo", "bar"}, // must be ignored
},
settingAutoAssignOrgRole: "",
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "test",
Email: "test@test.com",
@ -620,10 +628,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch empty role when strict attribute role is true and no match",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"role_attribute_strict": "true",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
RoleAttributeStrict: true,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
@ -643,10 +651,10 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
{
name: "Fetch empty role when strict attribute role is true and no role claims returned",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"role_attribute_strict": "true",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
RoleAttributeStrict: true,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
@ -699,8 +707,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, featuremgmt.WithFeatures(), cache)
require.NoError(t, err)
s := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), cache)
if tt.fields.usGovURL {
s.SocialBase.Endpoint.AuthURL = usGovAuthURL
@ -767,7 +774,7 @@ func TestSocialAzureAD_UserInfo(t *testing.T) {
func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
type fields struct {
SocialBase *SocialBase
providerCfg map[string]any
providerCfg *social.OAuthInfo
cfg *setting.Cfg
}
@ -776,21 +783,23 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
fields fields
claims *azureClaims
settingAutoAssignOrgRole string
want *BasicUserInfo
want *social.BasicUserInfo
wantErr bool
}{
{
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should get roles, skipOrgRoleSyncBase disabled",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "false",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: true,
// TODO: use this setting when SkipOrgRoleSync has moved to OAuthInfo
//SkipOrgRoleSync: false,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
OAuthSkipOrgRoleUpdateSync: false,
AzureADSkipOrgRoleSync: false,
},
},
claims: &azureClaims{
@ -800,7 +809,7 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -813,15 +822,17 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
{
name: "Grafana Admin and Editor roles in claim, skipOrgRoleSync disabled should not get roles",
fields: fields{
providerCfg: map[string]any{
"name": "azuread",
"client_id": "client-id-example",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "false",
providerCfg: &social.OAuthInfo{
Name: "azuread",
ClientId: "client-id-example",
AllowAssignGrafanaAdmin: true,
// TODO: use this setting when SkipOrgRoleSync has moved to OAuthInfo
// SkipOrgRoleSync: false,
},
cfg: &setting.Cfg{
AutoAssignOrgRole: "",
OAuthSkipOrgRoleUpdateSync: false,
AzureADSkipOrgRoleSync: false,
},
},
claims: &azureClaims{
@ -831,7 +842,7 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
Name: "My Name",
ID: "1234",
},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1234",
Name: "My Name",
Email: "me@example.com",
@ -875,8 +886,7 @@ func TestSocialAzureAD_SkipOrgRole(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, featuremgmt.WithFeatures(), cache)
require.NoError(t, err)
s := NewAzureADProvider(tt.fields.providerCfg, tt.fields.cfg, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), cache)
s.SocialBase.Endpoint.AuthURL = authURL
@ -943,13 +953,15 @@ func TestSocialAzureAD_InitializeExtraFields(t *testing.T) {
}
testCases := []struct {
name string
settings map[string]any
settings *social.OAuthInfo
want settingFields
}{
{
name: "forceUseGraphAPI is set to true",
settings: map[string]any{
"force_use_graph_api": "true",
settings: &social.OAuthInfo{
Extra: map[string]string{
"force_use_graph_api": "true",
},
},
want: settingFields{
forceUseGraphAPI: true,
@ -958,8 +970,10 @@ func TestSocialAzureAD_InitializeExtraFields(t *testing.T) {
},
{
name: "allowedOrganizations is set",
settings: map[string]any{
"allowed_organizations": "uuid-1234,uuid-5678",
settings: &social.OAuthInfo{
Extra: map[string]string{
"allowed_organizations": "uuid-1234,uuid-5678",
},
},
want: settingFields{
forceUseGraphAPI: false,
@ -970,8 +984,7 @@ func TestSocialAzureAD_InitializeExtraFields(t *testing.T) {
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)
s := NewAzureADProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
require.Equal(t, tc.want.forceUseGraphAPI, s.forceUseGraphAPI)
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -8,15 +8,17 @@ import (
"io"
"net/http"
"reflect"
"slices"
"strconv"
"strings"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/jmespath/go-jmespath"
"github.com/mitchellh/mapstructure"
"golang.org/x/oauth2"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
@ -27,7 +29,7 @@ const (
)
var (
errMissingGroupMembership = &Error{"user not a member of one of the required groups"}
errMissingGroupMembership = &SocialError{"user not a member of one of the required groups"}
)
type httpGetResponse struct {
@ -147,7 +149,7 @@ func (s *SocialBase) searchJSONForStringArrayAttr(attributePath string, data []b
return result, nil
}
func createOAuthConfig(info *OAuthInfo, cfg *setting.Cfg, defaultName string) *oauth2.Config {
func createOAuthConfig(info *social.OAuthInfo, cfg *setting.Cfg, defaultName string) *oauth2.Config {
var authStyle oauth2.AuthStyle
switch strings.ToLower(info.AuthStyle) {
case "inparams":
@ -166,7 +168,7 @@ func createOAuthConfig(info *OAuthInfo, cfg *setting.Cfg, defaultName string) *o
TokenURL: info.TokenUrl,
AuthStyle: authStyle,
},
RedirectURL: strings.TrimSuffix(cfg.AppURL, "/") + SocialBaseUrl + defaultName,
RedirectURL: strings.TrimSuffix(cfg.AppURL, "/") + social.SocialBaseUrl + defaultName,
Scopes: info.Scopes,
}
@ -195,18 +197,9 @@ func MustBool(value any, defaultValue bool) bool {
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) {
func CreateOAuthInfoFromKeyValues(settingsKV map[string]any) (*social.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)
@ -222,7 +215,7 @@ func CreateOAuthInfoFromKeyValues(settingsKV map[string]any) (*OAuthInfo, error)
return data, nil
}
var oauthInfo OAuthInfo
var oauthInfo social.OAuthInfo
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
DecodeHook: emptyStrToSliceDecodeHook,
Result: &oauthInfo,
@ -244,3 +237,9 @@ func CreateOAuthInfoFromKeyValues(settingsKV map[string]any) (*OAuthInfo, error)
return &oauthInfo, err
}
func appendUniqueScope(config *oauth2.Config, scope string) {
if !slices.Contains(config.Scopes, social.OfflineAccessScope) {
config.Scopes = append(config.Scopes, social.OfflineAccessScope)
}
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"errors"
@ -19,3 +19,12 @@ var (
errInvalidRole = errutil.BadRequest("oauth.invalid_role",
errutil.WithPublicMessage("IdP did not return a valid role attribute, please contact your administrator"))
)
// SocialError is a custom error type for social connectors to provide a public message when the connector expectaions are not met.
type SocialError struct {
s string
}
func (e SocialError) Error() string {
return e.s
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"bytes"
@ -12,14 +12,15 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
GenericOAuthProviderName = "generic_oauth"
nameAttributePathKey = "name_attribute_path"
loginAttributePathKey = "login_attribute_path"
idTokenAttributeNameKey = "id_token_attribute_name" // #nosec G101 not a hardcoded credential
@ -27,6 +28,9 @@ const (
var ExtraGenericOAuthSettingKeys = []string{nameAttributePathKey, loginAttributePathKey, idTokenAttributeNameKey, teamIdsKey, allowedOrganizationsKey}
var _ social.SocialConnector = (*SocialGenericOAuth)(nil)
var _ ssosettings.Reloadable = (*SocialGenericOAuth)(nil)
type SocialGenericOAuth struct {
*SocialBase
allowedOrganizations []string
@ -44,15 +48,10 @@ type SocialGenericOAuth struct {
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)
func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGenericOAuth {
config := createOAuthConfig(info, cfg, social.GenericOAuthProviderName)
provider := &SocialGenericOAuth{
SocialBase: newSocialBase(GenericOAuthProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(social.GenericOAuthProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamsUrl: info.TeamsUrl,
emailAttributeName: info.EmailAttributeName,
@ -70,7 +69,19 @@ func NewGenericOAuthProvider(settings map[string]any, cfg *setting.Cfg, features
// skipOrgRoleSync: info.SkipOrgRoleSync
}
return provider, nil
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettings.RegisterReloadable(social.GenericOAuthProviderName, provider)
}
return provider
}
func (s *SocialGenericOAuth) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
// TODOD: remove this in the next PR and use the isGroupMember from social.go
@ -151,7 +162,7 @@ func (info *UserInfoJson) String() string {
info.Name, info.DisplayName, info.Login, info.Username, info.Email, info.Upn, info.Attributes)
}
func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
s.log.Debug("Getting user info")
toCheck := make([]*UserInfoJson, 0, 2)
@ -162,7 +173,7 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
toCheck = append(toCheck, apiData)
}
userInfo := &BasicUserInfo{}
userInfo := &social.BasicUserInfo{}
for _, data := range toCheck {
s.log.Debug("Processing external user info", "source", data.source, "data", data)
@ -249,7 +260,7 @@ func (s *SocialGenericOAuth) UserInfo(ctx context.Context, client *http.Client,
return userInfo, nil
}
func (s *SocialGenericOAuth) GetOAuthInfo() *OAuthInfo {
func (s *SocialGenericOAuth) GetOAuthInfo() *social.OAuthInfo {
return s.info
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -12,16 +12,16 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/setting"
)
func TestSearchJSONForEmail(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
@ -105,8 +105,7 @@ func TestSearchJSONForEmail(t *testing.T) {
func TestSearchJSONForGroups(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
@ -165,8 +164,7 @@ func TestSearchJSONForGroups(t *testing.T) {
func TestSearchJSONForRole(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider, err := NewGenericOAuthProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
@ -224,10 +222,11 @@ func TestSearchJSONForRole(t *testing.T) {
}
func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
provider, err := NewGenericOAuthProvider(map[string]any{
"email_attribute_path": "email",
}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGenericOAuthProvider(&social.OAuthInfo{
EmailAttributePath: "email",
}, &setting.Cfg{},
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
tests := []struct {
Name string
@ -492,10 +491,11 @@ func TestUserInfoSearchesForEmailAndRole(t *testing.T) {
func TestUserInfoSearchesForLogin(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider, err := NewGenericOAuthProvider(map[string]any{
"login_attribute_path": "login",
}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGenericOAuthProvider(&social.OAuthInfo{
Extra: map[string]string{
"login_attribute_path": "login",
},
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
@ -585,10 +585,11 @@ func TestUserInfoSearchesForLogin(t *testing.T) {
func TestUserInfoSearchesForName(t *testing.T) {
t.Run("Given a generic OAuth provider", func(t *testing.T) {
provider, err := NewGenericOAuthProvider(map[string]any{
"name_attribute_path": "name",
}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGenericOAuthProvider(&social.OAuthInfo{
Extra: map[string]string{
"name_attribute_path": "name",
},
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
@ -726,11 +727,10 @@ func TestUserInfoSearchesForGroup(t *testing.T) {
require.NoError(t, err)
}))
provider, err := NewGenericOAuthProvider(map[string]any{
"groups_attribute_path": test.groupsAttributePath,
"api_url": ts.URL,
}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGenericOAuthProvider(&social.OAuthInfo{
GroupsAttributePath: test.groupsAttributePath,
ApiUrl: ts.URL,
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
token := &oauth2.Token{
AccessToken: "",
@ -748,10 +748,9 @@ func TestUserInfoSearchesForGroup(t *testing.T) {
}
func TestPayloadCompression(t *testing.T) {
provider, err := NewGenericOAuthProvider(map[string]any{
"email_attribute_path": "email",
}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGenericOAuthProvider(&social.OAuthInfo{
EmailAttributePath: "email",
}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
tests := []struct {
Name string
@ -824,13 +823,15 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
}
testCases := []struct {
name string
settings map[string]any
settings *social.OAuthInfo
want settingFields
}{
{
name: "nameAttributePath is set",
settings: map[string]any{
"name_attribute_path": "name",
settings: &social.OAuthInfo{
Extra: map[string]string{
"name_attribute_path": "name",
},
},
want: settingFields{
nameAttributePath: "name",
@ -842,8 +843,10 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
},
{
name: "loginAttributePath is set",
settings: map[string]any{
"login_attribute_path": "login",
settings: &social.OAuthInfo{
Extra: map[string]string{
"login_attribute_path": "login",
},
},
want: settingFields{
nameAttributePath: "",
@ -855,8 +858,10 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
},
{
name: "idTokenAttributeName is set",
settings: map[string]any{
"id_token_attribute_name": "id_token",
settings: &social.OAuthInfo{
Extra: map[string]string{
"id_token_attribute_name": "id_token",
},
},
want: settingFields{
nameAttributePath: "",
@ -868,8 +873,10 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
},
{
name: "teamIds is set",
settings: map[string]any{
"team_ids": "[\"team1\", \"team2\"]",
settings: &social.OAuthInfo{
Extra: map[string]string{
"team_ids": "[\"team1\", \"team2\"]",
},
},
want: settingFields{
nameAttributePath: "",
@ -881,8 +888,10 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
},
{
name: "allowedOrganizations is set",
settings: map[string]any{
"allowed_organizations": "org1, org2",
settings: &social.OAuthInfo{
Extra: map[string]string{
"allowed_organizations": "org1, org2",
},
},
want: settingFields{
nameAttributePath: "",
@ -896,8 +905,7 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
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)
s := NewGenericOAuthProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
require.Equal(t, tc.want.nameAttributePath, s.nameAttributePath)
require.Equal(t, tc.want.loginAttributePath, s.loginAttributePath)

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -12,17 +12,21 @@ 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/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/util/errutil"
)
const GitHubProviderName = "github"
var ExtraGithubSettingKeys = []string{allowedOrganizationsKey, teamIdsKey}
var _ social.SocialConnector = (*SocialGithub)(nil)
var _ ssosettings.Reloadable = (*SocialGithub)(nil)
type SocialGithub struct {
*SocialBase
allowedOrganizations []string
@ -51,17 +55,12 @@ var (
"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
}
func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGithub {
teamIds := mustInts(util.SplitString(info.Extra[teamIdsKey]))
config := createOAuthConfig(info, cfg, GitHubProviderName)
config := createOAuthConfig(info, cfg, social.GitHubProviderName)
provider := &SocialGithub{
SocialBase: newSocialBase(GitHubProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(social.GitHubProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamIds: teamIds,
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
@ -70,7 +69,19 @@ func NewGitHubProvider(settings map[string]any, cfg *setting.Cfg, features *feat
// skipOrgRoleSync: info.SkipOrgRoleSync
}
return provider, nil
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettings.RegisterReloadable(social.GitHubProviderName, provider)
}
return provider
}
func (s *SocialGithub) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGithub) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGithub) IsTeamMember(ctx context.Context, client *http.Client) bool {
@ -220,7 +231,7 @@ func (s *SocialGithub) FetchOrganizations(ctx context.Context, client *http.Clie
return logins, nil
}
func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
var data struct {
Id int `json:"id"`
Login string `json:"login"`
@ -264,7 +275,7 @@ func (s *SocialGithub) UserInfo(ctx context.Context, client *http.Client, token
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
}
userInfo := &BasicUserInfo{
userInfo := &social.BasicUserInfo{
Name: data.Login,
Login: data.Login,
Id: fmt.Sprintf("%d", data.Id),
@ -306,7 +317,7 @@ func (t *GithubTeam) GetShorthand() (string, error) {
return fmt.Sprintf("@%s/%s", t.Organization.Login, t.Slug), nil
}
func (s *SocialGithub) GetOAuthInfo() *OAuthInfo {
func (s *SocialGithub) GetOAuthInfo() *social.OAuthInfo {
return s.info
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -11,7 +11,9 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/setting"
)
@ -124,7 +126,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
settingSkipOrgRoleSync bool
roleAttributePath string
autoAssignOrgRole string
want *BasicUserInfo
want *social.BasicUserInfo
wantErr bool
}{
{
@ -133,7 +135,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
userTeamsRawJSON: testGHUserTeamsJSON,
autoAssignOrgRole: "",
roleAttributePath: "",
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
@ -148,7 +150,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
userRawJSON: testGHUserJSON,
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
@ -163,7 +165,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
userRawJSON: testGHUserJSON,
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
@ -178,7 +180,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
settingSkipOrgRoleSync: true,
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
@ -194,7 +196,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
settingAllowGrafanaAdmin: true,
userRawJSON: testGHUserJSON,
userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
@ -210,7 +212,7 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
userRawJSON: testGHUserJSON,
autoAssignOrgRole: "Editor",
userTeamsRawJSON: testGHUserTeamsJSON,
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "monalisa octocat",
Email: "octocat@github.com",
@ -239,16 +241,19 @@ func TestSocialGitHub_UserInfo(t *testing.T) {
}))
defer server.Close()
s, err := NewGitHubProvider(map[string]any{
"allowed_organizations": "",
"api_url": server.URL + "/user",
"team_ids": "",
"role_attribute_path": tt.roleAttributePath,
}, &setting.Cfg{
AutoAssignOrgRole: tt.autoAssignOrgRole,
GitHubSkipOrgRoleSync: tt.settingSkipOrgRoleSync,
}, featuremgmt.WithFeatures())
require.NoError(t, err)
s := NewGitHubProvider(
&social.OAuthInfo{
ApiUrl: server.URL + "/user",
RoleAttributePath: tt.roleAttributePath,
Extra: map[string]string{
"allowed_organizations": "",
"team_ids": "",
},
}, &setting.Cfg{
AutoAssignOrgRole: tt.autoAssignOrgRole,
GitHubSkipOrgRoleSync: tt.settingSkipOrgRoleSync,
}, &ssosettingstests.MockService{},
featuremgmt.WithFeatures())
token := &oauth2.Token{
AccessToken: "fake_token",
@ -273,13 +278,15 @@ func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
}
testCases := []struct {
name string
settings map[string]any
settings *social.OAuthInfo
want settingFields
}{
{
name: "teamIds is set",
settings: map[string]any{
"team_ids": "1234,5678",
settings: &social.OAuthInfo{
Extra: map[string]string{
"team_ids": "1234,5678",
},
},
want: settingFields{
teamIds: []int{1234, 5678},
@ -288,8 +295,10 @@ func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
},
{
name: "allowedOrganizations is set",
settings: map[string]any{
"allowed_organizations": "uuid-1234,uuid-5678",
settings: &social.OAuthInfo{
Extra: map[string]string{
"allowed_organizations": "uuid-1234,uuid-5678",
},
},
want: settingFields{
teamIds: []int{},
@ -298,9 +307,11 @@ func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
},
{
name: "teamIds and allowedOrganizations are empty",
settings: map[string]any{
"team_ids": "",
"allowed_organizations": "",
settings: &social.OAuthInfo{
Extra: map[string]string{
"team_ids": "",
"allowed_organizations": "",
},
},
want: settingFields{
teamIds: []int{},
@ -309,8 +320,10 @@ func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
},
{
name: "should not error when teamIds are not integers",
settings: map[string]any{
"team_ids": "abc1234,5678",
settings: &social.OAuthInfo{
Extra: map[string]string{
"team_ids": "abc1234,5678",
},
},
want: settingFields{
teamIds: []int{},
@ -321,8 +334,7 @@ func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
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)
s := NewGitHubProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
require.Equal(t, tc.want.teamIds, s.teamIds)
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -11,17 +11,22 @@ 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/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/setting"
)
const (
groupPerPage = 50
accessLevelGuest = "10"
GitlabProviderName = "gitlab"
groupPerPage = 50
accessLevelGuest = "10"
)
var _ social.SocialConnector = (*SocialGitlab)(nil)
var _ ssosettings.Reloadable = (*SocialGitlab)(nil)
type SocialGitlab struct {
*SocialBase
apiUrl string
@ -49,22 +54,29 @@ type userData struct {
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)
func NewGitLabProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGitlab {
config := createOAuthConfig(info, cfg, social.GitlabProviderName)
provider := &SocialGitlab{
SocialBase: newSocialBase(GitlabProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(social.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
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettings.RegisterReloadable(social.GitlabProviderName, provider)
}
return provider
}
func (s *SocialGitlab) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGitlab) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGitlab) getGroups(ctx context.Context, client *http.Client) []string {
@ -146,7 +158,7 @@ func (s *SocialGitlab) getGroupsPage(ctx context.Context, client *http.Client, n
return fullPaths, next
}
func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
data, err := s.extractFromToken(ctx, client, token)
if err != nil {
return nil, err
@ -161,7 +173,7 @@ func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token
}
}
userInfo := &BasicUserInfo{
userInfo := &social.BasicUserInfo{
Id: data.ID,
Name: data.Name,
Login: data.Login,
@ -182,7 +194,7 @@ func (s *SocialGitlab) UserInfo(ctx context.Context, client *http.Client, token
return userInfo, nil
}
func (s *SocialGitlab) GetOAuthInfo() *OAuthInfo {
func (s *SocialGitlab) GetOAuthInfo() *social.OAuthInfo {
return s.info
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -15,8 +15,10 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/setting"
)
@ -39,8 +41,7 @@ const (
func TestSocialGitlab_UserInfo(t *testing.T) {
var nilPointer *bool
provider, err := NewGitLabProvider(map[string]any{"skip_org_role_sync": false}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGitLabProvider(&social.OAuthInfo{SkipOrgRoleSync: false}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
type conf struct {
AllowAssignGrafanaAdmin bool
@ -346,21 +347,23 @@ func TestSocialGitlab_extractFromToken(t *testing.T) {
// Create a test client with a dummy token
client := oauth2.NewClient(context.Background(), &tokenSource{accessToken: "dummy_access_token"})
s, err := NewGitLabProvider(map[string]any{
"allowed_domains": []string{},
"allow_sign_up": false,
"role_attribute_path": "",
"role_attribute_strict": false,
"skip_org_role_sync": false,
"auth_url": tc.config.Endpoint.AuthURL,
"token_url": tc.config.Endpoint.TokenURL,
},
s := NewGitLabProvider(
&social.OAuthInfo{
AllowedDomains: []string{},
AllowSignup: false,
RoleAttributePath: "",
RoleAttributeStrict: false,
// TODO: use this setting when SkipOrgRoleSync has moved to OAuthInfo
//SkipOrgRoleSync: false,
AuthUrl: tc.config.Endpoint.AuthURL,
TokenUrl: tc.config.Endpoint.TokenURL,
},
&setting.Cfg{
AutoAssignOrgRole: "",
OAuthSkipOrgRoleUpdateSync: false,
}, featuremgmt.WithFeatures())
require.NoError(t, err)
GitLabSkipOrgRoleSync: false,
}, &ssosettingstests.MockService{},
featuremgmt.WithFeatures())
// Test case: successful extraction
token := &oauth2.Token{}
@ -450,8 +453,7 @@ func TestSocialGitlab_GetGroupsNextPage(t *testing.T) {
defer mockServer.Close()
// Create a SocialGitlab instance with the mock server URL
s, err := NewGitLabProvider(map[string]any{"api_url": mockServer.URL}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
s := NewGitLabProvider(&social.OAuthInfo{ApiUrl: mockServer.URL}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
// Call getGroups and verify that it returns all groups
expectedGroups := []string{"admins", "editors", "viewers", "serveradmins"}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -10,7 +10,10 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/setting"
)
@ -18,9 +21,11 @@ const (
legacyAPIURL = "https://www.googleapis.com/oauth2/v1/userinfo"
googleIAMGroupsEndpoint = "https://content-cloudidentity.googleapis.com/v1/groups/-/memberships:searchDirectGroups"
googleIAMScope = "https://www.googleapis.com/auth/cloud-identity.groups.readonly"
GoogleProviderName = "google"
)
var _ social.SocialConnector = (*SocialGoogle)(nil)
var _ ssosettings.Reloadable = (*SocialGoogle)(nil)
type SocialGoogle struct {
*SocialBase
hostedDomain string
@ -36,15 +41,10 @@ type googleUserData struct {
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)
func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGoogle {
config := createOAuthConfig(info, cfg, social.GoogleProviderName)
provider := &SocialGoogle{
SocialBase: newSocialBase(GoogleProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(social.GoogleProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
hostedDomain: info.HostedDomain,
apiUrl: info.ApiUrl,
skipOrgRoleSync: cfg.GoogleSkipOrgRoleSync,
@ -56,10 +56,22 @@ func NewGoogleProvider(settings map[string]any, cfg *setting.Cfg, features *feat
provider.log.Warn("Using legacy Google API URL, please update your configuration")
}
return provider, nil
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettings.RegisterReloadable(social.GoogleProviderName, provider)
}
return provider
}
func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
func (s *SocialGoogle) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
data, errToken := s.extractFromToken(ctx, client, token)
if errToken != nil {
return nil, errToken
@ -90,7 +102,7 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
return nil, errMissingGroupMembership
}
userInfo := &BasicUserInfo{
userInfo := &social.BasicUserInfo{
Id: data.ID,
Name: data.Name,
Email: data.Email,
@ -118,7 +130,7 @@ func (s *SocialGoogle) UserInfo(ctx context.Context, client *http.Client, token
return userInfo, nil
}
func (s *SocialGoogle) GetOAuthInfo() *OAuthInfo {
func (s *SocialGoogle) GetOAuthInfo() *social.OAuthInfo {
return s.info
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -14,8 +14,10 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/setting"
)
@ -181,22 +183,23 @@ func TestSocialGoogle_retrieveGroups(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := NewGoogleProvider(map[string]any{
"api_url": "",
"scopes": tt.fields.Scopes,
"hosted_domain": "",
"allowed_domains": []string{},
"allow_sign_up": false,
"role_attribute_path": "",
"role_attribute_strict": false,
"allow_assign_grafana_admin": false,
},
s := NewGoogleProvider(
&social.OAuthInfo{
ApiUrl: "",
Scopes: tt.fields.Scopes,
HostedDomain: "",
AllowedDomains: []string{},
AllowSignup: false,
RoleAttributePath: "",
RoleAttributeStrict: false,
AllowAssignGrafanaAdmin: false,
},
&setting.Cfg{
AutoAssignOrgRole: "",
GoogleSkipOrgRoleSync: false,
},
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
require.NoError(t, err)
got, err := s.retrieveGroups(context.Background(), tt.args.client, tt.args.userData)
if (err != nil) != tt.wantErr {
@ -259,7 +262,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
name string
fields fields
args args
wantData *BasicUserInfo
wantData *social.BasicUserInfo
wantErr bool
wantErrMsg string
}{
@ -272,7 +275,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
args: args{
token: tokenWithID,
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "88888888888888",
Login: "test@example.com",
Email: "test@example.com",
@ -309,7 +312,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
},
},
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "88888888888888",
Login: "test@example.com",
Email: "test@example.com",
@ -341,7 +344,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
},
},
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "99999999999999",
Login: "test@example.com",
Email: "test@example.com",
@ -459,7 +462,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
},
},
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "92222222222222222",
Name: "Test User",
Email: "test@example.com",
@ -521,7 +524,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
},
},
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "88888888888888",
Login: "test@example.com",
Email: "test@example.com",
@ -542,7 +545,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
args: args{
token: tokenWithID,
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "88888888888888",
Login: "test@example.com",
Email: "test@example.com",
@ -562,7 +565,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
args: args{
token: tokenWithID,
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "88888888888888",
Login: "test@example.com",
Email: "test@example.com",
@ -582,7 +585,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
args: args{
token: tokenWithID,
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "88888888888888",
Login: "test@example.com",
Email: "test@example.com",
@ -621,7 +624,7 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
},
},
},
wantData: &BasicUserInfo{
wantData: &social.BasicUserInfo{
Id: "88888888888888",
Login: "test@example.com",
Email: "test@example.com",
@ -635,20 +638,23 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := NewGoogleProvider(map[string]any{
"api_url": tt.fields.apiURL,
"scopes": tt.fields.Scopes,
"allowed_groups": tt.fields.allowedGroups,
"allow_sign_up": false,
"role_attribute_path": tt.fields.roleAttributePath,
"role_attribute_strict": tt.fields.roleAttributeStrict,
"allow_assign_grafana_admin": tt.fields.allowAssignGrafanaAdmin,
},
s := NewGoogleProvider(
&social.OAuthInfo{
ApiUrl: tt.fields.apiURL,
Scopes: tt.fields.Scopes,
AllowedGroups: tt.fields.allowedGroups,
AllowSignup: false,
RoleAttributePath: tt.fields.roleAttributePath,
RoleAttributeStrict: tt.fields.roleAttributeStrict,
AllowAssignGrafanaAdmin: tt.fields.allowAssignGrafanaAdmin,
// TODO: use this setting when SkipOrgRoleSync has moved to OAuthInfo
// SkipOrgRoleSync: tt.fields.skipOrgRoleSync,
},
&setting.Cfg{
GoogleSkipOrgRoleSync: tt.fields.skipOrgRoleSync,
},
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
require.NoError(t, err)
gotData, err := s.UserInfo(context.Background(), tt.args.client, tt.args.token)
if tt.wantErr {

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -8,21 +8,21 @@ 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/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
const (
GrafanaComProviderName = "grafana_com"
// legacy/old settings for the provider
GrafanaNetProviderName = "grafananet"
)
var ExtraGrafanaComSettingKeys = []string{allowedOrganizationsKey}
var _ social.SocialConnector = (*SocialGrafanaCom)(nil)
var _ ssosettings.Reloadable = (*SocialGrafanaCom)(nil)
type SocialGrafanaCom struct {
*SocialBase
url string
@ -34,20 +34,15 @@ type OrgRecord struct {
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
}
func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialGrafanaCom {
// Override necessary settings
info.AuthUrl = cfg.GrafanaComURL + "/oauth2/authorize"
info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token"
info.AuthStyle = "inheader"
config := createOAuthConfig(info, cfg, GrafanaComProviderName)
config := createOAuthConfig(info, cfg, social.GrafanaComProviderName)
provider := &SocialGrafanaCom{
SocialBase: newSocialBase(GrafanaComProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(social.GrafanaComProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
url: cfg.GrafanaComURL,
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
skipOrgRoleSync: cfg.GrafanaComSkipOrgRoleSync,
@ -55,7 +50,19 @@ func NewGrafanaComProvider(settings map[string]any, cfg *setting.Cfg, features *
// skipOrgRoleSync: info.SkipOrgRoleSync
}
return provider, nil
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettings.RegisterReloadable(social.GrafanaComProviderName, provider)
}
return provider
}
func (s *SocialGrafanaCom) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGrafanaCom) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialGrafanaCom) IsEmailAllowed(email string) bool {
@ -79,7 +86,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
}
// UserInfo is used for login credentials for the user
func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _ *oauth2.Token) (*BasicUserInfo, error) {
func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _ *oauth2.Token) (*social.BasicUserInfo, error) {
var data struct {
Id int `json:"id"`
Name string `json:"name"`
@ -105,7 +112,7 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _
if !s.skipOrgRoleSync {
role = org.RoleType(data.Role)
}
userInfo := &BasicUserInfo{
userInfo := &social.BasicUserInfo{
Id: fmt.Sprintf("%d", data.Id),
Name: data.Name,
Login: data.Login,
@ -122,6 +129,6 @@ func (s *SocialGrafanaCom) UserInfo(ctx context.Context, client *http.Client, _
return userInfo, nil
}
func (s *SocialGrafanaCom) GetOAuthInfo() *OAuthInfo {
func (s *SocialGrafanaCom) GetOAuthInfo() *social.OAuthInfo {
return s.info
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -6,9 +6,12 @@ import (
"net/http/httptest"
"testing"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/setting"
)
const (
@ -25,8 +28,7 @@ const (
)
func TestSocialGrafanaCom_UserInfo(t *testing.T) {
provider, err := NewGrafanaComProvider(map[string]any{}, &setting.Cfg{}, featuremgmt.WithFeatures())
require.NoError(t, err)
provider := NewGrafanaComProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
type conf struct {
skipOrgRoleSync bool
@ -36,14 +38,14 @@ func TestSocialGrafanaCom_UserInfo(t *testing.T) {
Name string
Cfg conf
userInfoResp string
want *BasicUserInfo
want *social.BasicUserInfo
ExpectedError error
}{
{
Name: "should return empty role as userInfo when Skip Org Role Sync Enabled",
userInfoResp: userResponse,
Cfg: conf{skipOrgRoleSync: true},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "Eric Leijonmarck",
Email: "octocat@github.com",
@ -55,7 +57,7 @@ func TestSocialGrafanaCom_UserInfo(t *testing.T) {
Name: "should return role as userInfo when Skip Org Role Sync Enabled",
userInfoResp: userResponse,
Cfg: conf{skipOrgRoleSync: false},
want: &BasicUserInfo{
want: &social.BasicUserInfo{
Id: "1",
Name: "Eric Leijonmarck",
Email: "octocat@github.com",
@ -99,20 +101,22 @@ func TestSocialGrafanaCom_InitializeExtraFields(t *testing.T) {
}
testCases := []struct {
name string
settings map[string]any
settings *social.OAuthInfo
want settingFields
}{
{
name: "allowedOrganizations is not set",
settings: map[string]any{},
settings: social.NewOAuthInfo(),
want: settingFields{
allowedOrganizations: []string{},
},
},
{
name: "allowedOrganizations is set",
settings: map[string]any{
"allowed_organizations": "uuid-1234,uuid-5678",
settings: &social.OAuthInfo{
Extra: map[string]string{
"allowed_organizations": "uuid-1234,uuid-5678",
},
},
want: settingFields{
allowedOrganizations: []string{"uuid-1234", "uuid-5678"},
@ -122,8 +126,7 @@ func TestSocialGrafanaCom_InitializeExtraFields(t *testing.T) {
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)
s := NewGrafanaComProvider(tc.settings, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
require.Equal(t, tc.want.allowedOrganizations, s.allowedOrganizations)
})

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -10,12 +10,16 @@ import (
"github.com/go-jose/go-jose/v3/jwt"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/setting"
)
const OktaProviderName = "okta"
var _ social.SocialConnector = (*SocialOkta)(nil)
var _ ssosettings.Reloadable = (*SocialOkta)(nil)
type SocialOkta struct {
*SocialBase
@ -43,15 +47,10 @@ type OktaClaims struct {
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)
func NewOktaProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager) *SocialOkta {
config := createOAuthConfig(info, cfg, social.OktaProviderName)
provider := &SocialOkta{
SocialBase: newSocialBase(OktaProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(social.OktaProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
allowedGroups: info.AllowedGroups,
// FIXME: Move skipOrgRoleSync to OAuthInfo
@ -60,10 +59,22 @@ func NewOktaProvider(settings map[string]any, cfg *setting.Cfg, features *featur
}
if info.UseRefreshToken && features.IsEnabledGlobally(featuremgmt.FlagAccessTokenExpirationCheck) {
appendUniqueScope(config, OfflineAccessScope)
appendUniqueScope(config, social.OfflineAccessScope)
}
return provider, nil
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
ssoSettings.RegisterReloadable(social.OktaProviderName, provider)
}
return provider
}
func (s *SocialOkta) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (s *SocialOkta) Reload(ctx context.Context, settings ssoModels.SSOSettings) error {
return nil
}
func (claims *OktaClaims) extractEmail() string {
@ -74,7 +85,7 @@ func (claims *OktaClaims) extractEmail() string {
return claims.Email
}
func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*social.BasicUserInfo, error) {
idToken := token.Extra("id_token")
if idToken == nil {
return nil, fmt.Errorf("no id_token found")
@ -123,7 +134,7 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o
s.log.Debug("AllowAssignGrafanaAdmin and skipOrgRoleSync are both set, Grafana Admin role will not be synced, consider setting one or the other")
}
return &BasicUserInfo{
return &social.BasicUserInfo{
Id: claims.ID,
Name: claims.Name,
Email: email,
@ -134,7 +145,7 @@ func (s *SocialOkta) UserInfo(ctx context.Context, client *http.Client, token *o
}, nil
}
func (s *SocialOkta) GetOAuthInfo() *OAuthInfo {
func (s *SocialOkta) GetOAuthInfo() *social.OAuthInfo {
return s.info
}

View File

@ -1,4 +1,4 @@
package social
package connectors
import (
"context"
@ -12,8 +12,10 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/setting"
)
@ -67,7 +69,7 @@ func TestSocialOkta_UserInfo(t *testing.T) {
},
{
name: "Should give grafanaAdmin role for specific GrafanaAdmin in the role assignement",
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "role": "%s" }`, RoleGrafanaAdmin),
userRawJSON: fmt.Sprintf(`{ "email": "okta-octopus@grafana.com", "role": "%s" }`, social.RoleGrafanaAdmin),
RoleAttributePath: "role",
allowAssignGrafanaAdmin: true,
OAuth2Extra: map[string]any{
@ -97,20 +99,21 @@ func TestSocialOkta_UserInfo(t *testing.T) {
}))
defer server.Close()
provider, err := NewOktaProvider(
map[string]any{
"api_url": server.URL + "/user",
"role_attribute_path": tt.RoleAttributePath,
"allow_assign_grafana_admin": tt.allowAssignGrafanaAdmin,
"skip_org_role_sync": tt.settingSkipOrgRoleSync,
provider := NewOktaProvider(
&social.OAuthInfo{
ApiUrl: server.URL + "/user",
RoleAttributePath: tt.RoleAttributePath,
AllowAssignGrafanaAdmin: tt.allowAssignGrafanaAdmin,
// TODO: use this setting when SkipOrgRoleSync has moved to OAuthInfo
// SkipOrgRoleSync: tt.settingSkipOrgRoleSync,
},
&setting.Cfg{
OktaSkipOrgRoleSync: tt.settingSkipOrgRoleSync,
AutoAssignOrgRole: tt.autoAssignOrgRole,
OAuthSkipOrgRoleUpdateSync: false,
},
&ssosettingstests.MockService{},
featuremgmt.WithFeatures())
require.NoError(t, err)
// create a oauth2 token with a id_token
staticToken := oauth2.Token{

View File

@ -0,0 +1,229 @@
package connectors
import (
"bytes"
"compress/zlib"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"regexp"
"strings"
"golang.org/x/oauth2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
)
type SocialBase struct {
*oauth2.Config
info *social.OAuthInfo
log log.Logger
allowSignup bool
allowAssignGrafanaAdmin bool
allowedDomains []string
allowedGroups []string
roleAttributePath string
roleAttributeStrict bool
autoAssignOrgRole string
skipOrgRoleSync bool
features featuremgmt.FeatureManager
useRefreshToken bool
}
func newSocialBase(name string,
config *oauth2.Config,
info *social.OAuthInfo,
autoAssignOrgRole string,
skipOrgRoleSync bool,
features featuremgmt.FeatureManager,
) *SocialBase {
logger := log.New("oauth." + name)
return &SocialBase{
Config: config,
info: info,
log: logger,
allowSignup: info.AllowSignup,
allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
allowedDomains: info.AllowedDomains,
allowedGroups: info.AllowedGroups,
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
autoAssignOrgRole: autoAssignOrgRole,
skipOrgRoleSync: skipOrgRoleSync,
features: features,
useRefreshToken: info.UseRefreshToken,
}
}
type groupStruct struct {
Groups []string `json:"groups"`
}
func (s *SocialBase) SupportBundleContent(bf *bytes.Buffer) error {
bf.WriteString("## Client configuration\n\n")
bf.WriteString("```ini\n")
bf.WriteString(fmt.Sprintf("allow_assign_grafana_admin = %v\n", s.allowAssignGrafanaAdmin))
bf.WriteString(fmt.Sprintf("allow_sign_up = %v\n", s.allowSignup))
bf.WriteString(fmt.Sprintf("allowed_domains = %v\n", s.allowedDomains))
bf.WriteString(fmt.Sprintf("auto_assign_org_role = %v\n", s.autoAssignOrgRole))
bf.WriteString(fmt.Sprintf("role_attribute_path = %v\n", s.roleAttributePath))
bf.WriteString(fmt.Sprintf("role_attribute_strict = %v\n", s.roleAttributeStrict))
bf.WriteString(fmt.Sprintf("skip_org_role_sync = %v\n", s.skipOrgRoleSync))
bf.WriteString(fmt.Sprintf("client_id = %v\n", s.Config.ClientID))
bf.WriteString(fmt.Sprintf("client_secret = %v ; issue if empty\n", strings.Repeat("*", len(s.Config.ClientSecret))))
bf.WriteString(fmt.Sprintf("auth_url = %v\n", s.Config.Endpoint.AuthURL))
bf.WriteString(fmt.Sprintf("token_url = %v\n", s.Config.Endpoint.TokenURL))
bf.WriteString(fmt.Sprintf("auth_style = %v\n", s.Config.Endpoint.AuthStyle))
bf.WriteString(fmt.Sprintf("redirect_url = %v\n", s.Config.RedirectURL))
bf.WriteString(fmt.Sprintf("scopes = %v\n", s.Config.Scopes))
bf.WriteString("```\n\n")
return nil
}
func (s *SocialBase) extractRoleAndAdminOptional(rawJSON []byte, groups []string) (org.RoleType, bool, error) {
if s.roleAttributePath == "" {
if s.roleAttributeStrict {
return "", false, errRoleAttributePathNotSet.Errorf("role_attribute_path not set and role_attribute_strict is set")
}
return "", false, nil
}
if role, gAdmin := s.searchRole(rawJSON, groups); role.IsValid() {
return role, gAdmin, nil
} else if role != "" {
return "", false, errInvalidRole.Errorf("invalid role: %s", role)
}
if s.roleAttributeStrict {
return "", false, errRoleAttributeStrictViolation.Errorf("idP did not return a role attribute, but role_attribute_strict is set")
}
return "", false, nil
}
func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string) (org.RoleType, bool, error) {
role, gAdmin, err := s.extractRoleAndAdminOptional(rawJSON, groups)
if role == "" {
role = s.defaultRole()
}
return role, gAdmin, err
}
func (s *SocialBase) searchRole(rawJSON []byte, groups []string) (org.RoleType, bool) {
role, err := s.searchJSONForStringAttr(s.roleAttributePath, rawJSON)
if err == nil && role != "" {
return getRoleFromSearch(role)
}
if groupBytes, err := json.Marshal(groupStruct{groups}); err == nil {
role, err := s.searchJSONForStringAttr(s.roleAttributePath, groupBytes)
if err == nil && role != "" {
return getRoleFromSearch(role)
}
}
return "", false
}
// defaultRole returns the default role for the user based on the autoAssignOrgRole setting
// if legacy is enabled "" is returned indicating the previous role assignment is used.
func (s *SocialBase) defaultRole() org.RoleType {
if s.autoAssignOrgRole != "" {
s.log.Debug("No role found, returning default.")
return org.RoleType(s.autoAssignOrgRole)
}
// should never happen
return org.RoleViewer
}
func (s *SocialBase) isGroupMember(groups []string) bool {
if len(s.allowedGroups) == 0 {
return true
}
for _, allowedGroup := range s.allowedGroups {
for _, group := range groups {
if group == allowedGroup {
return true
}
}
}
return false
}
func (s *SocialBase) retrieveRawIDToken(idToken any) ([]byte, error) {
tokenString, ok := idToken.(string)
if !ok {
return nil, fmt.Errorf("id_token is not a string: %v", idToken)
}
jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)$")
matched := jwtRegexp.FindStringSubmatch(tokenString)
if matched == nil {
return nil, fmt.Errorf("id_token is not in JWT format: %s", tokenString)
}
rawJSON, err := base64.RawURLEncoding.DecodeString(matched[2])
if err != nil {
return nil, fmt.Errorf("error base64 decoding id_token: %w", err)
}
headerBytes, err := base64.RawURLEncoding.DecodeString(matched[1])
if err != nil {
return nil, fmt.Errorf("error base64 decoding header: %w", err)
}
var header map[string]any
if err := json.Unmarshal(headerBytes, &header); err != nil {
return nil, fmt.Errorf("error deserializing header: %w", err)
}
if compressionVal, exists := header["zip"]; exists {
compression, ok := compressionVal.(string)
if !ok {
return nil, fmt.Errorf("unrecognized compression header: %v", compressionVal)
}
if compression != "DEF" {
return nil, fmt.Errorf("unknown compression algorithm: %s", compression)
}
fr, err := zlib.NewReader(bytes.NewReader(rawJSON))
if err != nil {
return nil, fmt.Errorf("error creating zlib reader: %w", err)
}
defer func() {
if err := fr.Close(); err != nil {
s.log.Warn("Failed closing zlib reader", "error", err)
}
}()
rawJSON, err = io.ReadAll(fr)
if err != nil {
return nil, fmt.Errorf("error decompressing payload: %w", err)
}
}
return rawJSON, nil
}
// match grafana admin role and translate to org role and bool.
// treat the JSON search result to ensure correct casing.
func getRoleFromSearch(role string) (org.RoleType, bool) {
if strings.EqualFold(role, social.RoleGrafanaAdmin) {
return org.RoleAdmin, true
}
return org.RoleType(cases.Title(language.Und).String(role)), false
}

View File

@ -2,45 +2,54 @@ package social
import (
"bytes"
"compress/zlib"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
"regexp"
"slices"
"strings"
"time"
"golang.org/x/oauth2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/setting"
"golang.org/x/oauth2"
)
const (
OfflineAccessScope = "offline_access"
RoleGrafanaAdmin = "GrafanaAdmin" // For AzureAD for example this value cannot contain spaces
AzureADProviderName = "azuread"
GenericOAuthProviderName = "generic_oauth"
GitHubProviderName = "github"
GitlabProviderName = "gitlab"
GoogleProviderName = "google"
GrafanaComProviderName = "grafana_com"
// legacy/old settings for the provider
GrafanaNetProviderName = "grafananet"
OktaProviderName = "okta"
)
type SocialService struct {
cfg *setting.Cfg
var (
SocialBaseUrl = "/login/"
)
socialMap map[string]SocialConnector
log log.Logger
type Service interface {
GetOAuthProviders() map[string]bool
GetOAuthHttpClient(string) (*http.Client, error)
GetConnector(string) (SocialConnector, error)
GetOAuthInfoProvider(string) *OAuthInfo
GetOAuthInfoProviders() map[string]*OAuthInfo
}
//go:generate mockery --name SocialConnector --structname MockSocialConnector --outpkg socialtest --filename social_connector_mock.go --output ./socialtest/
type SocialConnector interface {
UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error)
IsEmailAllowed(email string) bool
IsSignupAllowed() bool
GetOAuthInfo() *OAuthInfo
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error)
Client(ctx context.Context, t *oauth2.Token) *http.Client
TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
SupportBundleContent(*bytes.Buffer) error
}
type OAuthInfo struct {
@ -88,51 +97,6 @@ func NewOAuthInfo() *OAuthInfo {
}
}
func ProvideService(cfg *setting.Cfg,
features *featuremgmt.FeatureManager,
usageStats usagestats.Service,
bundleRegistry supportbundles.Service,
cache remotecache.CacheStorage,
) *SocialService {
ss := &SocialService{
cfg: cfg,
socialMap: make(map[string]SocialConnector),
log: log.New("login.social"),
}
usageStats.RegisterMetricsFunc(ss.getUsageStats)
for _, name := range allOauthes {
sec := cfg.Raw.Section("auth." + name)
settingsKVs := convertIniSectionToMap(sec)
info, err := CreateOAuthInfoFromKeyValues(settingsKVs)
if err != nil {
ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", name)
continue
}
if !info.Enabled {
continue
}
if name == GrafanaNetProviderName {
name = GrafanaComProviderName
}
conn, err := ss.createOAuthConnector(name, settingsKVs, cfg, features, cache)
if err != nil {
ss.log.Error("Failed to create OAuth provider", "error", err, "provider", name)
}
ss.socialMap[name] = conn
}
ss.registerSupportBundleCollectors(bundleRegistry)
return ss
}
type BasicUserInfo struct {
Id string
Name string
@ -147,394 +111,3 @@ func (b *BasicUserInfo) String() string {
return fmt.Sprintf("Id: %s, Name: %s, Email: %s, Login: %s, Role: %s, Groups: %v",
b.Id, b.Name, b.Email, b.Login, b.Role, b.Groups)
}
//go:generate mockery --name SocialConnector --structname MockSocialConnector --outpkg socialtest --filename social_connector_mock.go --output ../socialtest/
type SocialConnector interface {
UserInfo(ctx context.Context, client *http.Client, token *oauth2.Token) (*BasicUserInfo, error)
IsEmailAllowed(email string) bool
IsSignupAllowed() bool
GetOAuthInfo() *OAuthInfo
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error)
Client(ctx context.Context, t *oauth2.Token) *http.Client
TokenSource(ctx context.Context, t *oauth2.Token) oauth2.TokenSource
SupportBundleContent(*bytes.Buffer) error
}
type SocialBase struct {
*oauth2.Config
info *OAuthInfo
log log.Logger
allowSignup bool
allowAssignGrafanaAdmin bool
allowedDomains []string
allowedGroups []string
roleAttributePath string
roleAttributeStrict bool
autoAssignOrgRole string
skipOrgRoleSync bool
features featuremgmt.FeatureManager
useRefreshToken bool
}
type Error struct {
s string
}
func (e Error) Error() string {
return e.s
}
var (
SocialBaseUrl = "/login/"
SocialMap = make(map[string]SocialConnector)
allOauthes = []string{GitHubProviderName, GitlabProviderName, GoogleProviderName, GenericOAuthProviderName, GrafanaNetProviderName,
GrafanaComProviderName, AzureADProviderName, OktaProviderName}
)
type Service interface {
GetOAuthProviders() map[string]bool
GetOAuthHttpClient(string) (*http.Client, error)
GetConnector(string) (SocialConnector, error)
GetOAuthInfoProvider(string) *OAuthInfo
GetOAuthInfoProviders() map[string]*OAuthInfo
}
func newSocialBase(name string,
config *oauth2.Config,
info *OAuthInfo,
autoAssignOrgRole string,
skipOrgRoleSync bool,
features featuremgmt.FeatureManager,
) *SocialBase {
logger := log.New("oauth." + name)
return &SocialBase{
Config: config,
info: info,
log: logger,
allowSignup: info.AllowSignup,
allowAssignGrafanaAdmin: info.AllowAssignGrafanaAdmin,
allowedDomains: info.AllowedDomains,
allowedGroups: info.AllowedGroups,
roleAttributePath: info.RoleAttributePath,
roleAttributeStrict: info.RoleAttributeStrict,
autoAssignOrgRole: autoAssignOrgRole,
skipOrgRoleSync: skipOrgRoleSync,
features: features,
useRefreshToken: info.UseRefreshToken,
}
}
type groupStruct struct {
Groups []string `json:"groups"`
}
func (s *SocialBase) SupportBundleContent(bf *bytes.Buffer) error {
bf.WriteString("## Client configuration\n\n")
bf.WriteString("```ini\n")
bf.WriteString(fmt.Sprintf("allow_assign_grafana_admin = %v\n", s.allowAssignGrafanaAdmin))
bf.WriteString(fmt.Sprintf("allow_sign_up = %v\n", s.allowSignup))
bf.WriteString(fmt.Sprintf("allowed_domains = %v\n", s.allowedDomains))
bf.WriteString(fmt.Sprintf("auto_assign_org_role = %v\n", s.autoAssignOrgRole))
bf.WriteString(fmt.Sprintf("role_attribute_path = %v\n", s.roleAttributePath))
bf.WriteString(fmt.Sprintf("role_attribute_strict = %v\n", s.roleAttributeStrict))
bf.WriteString(fmt.Sprintf("skip_org_role_sync = %v\n", s.skipOrgRoleSync))
bf.WriteString(fmt.Sprintf("client_id = %v\n", s.Config.ClientID))
bf.WriteString(fmt.Sprintf("client_secret = %v ; issue if empty\n", strings.Repeat("*", len(s.Config.ClientSecret))))
bf.WriteString(fmt.Sprintf("auth_url = %v\n", s.Config.Endpoint.AuthURL))
bf.WriteString(fmt.Sprintf("token_url = %v\n", s.Config.Endpoint.TokenURL))
bf.WriteString(fmt.Sprintf("auth_style = %v\n", s.Config.Endpoint.AuthStyle))
bf.WriteString(fmt.Sprintf("redirect_url = %v\n", s.Config.RedirectURL))
bf.WriteString(fmt.Sprintf("scopes = %v\n", s.Config.Scopes))
bf.WriteString("```\n\n")
return nil
}
func (s *SocialBase) extractRoleAndAdminOptional(rawJSON []byte, groups []string) (org.RoleType, bool, error) {
if s.roleAttributePath == "" {
if s.roleAttributeStrict {
return "", false, errRoleAttributePathNotSet.Errorf("role_attribute_path not set and role_attribute_strict is set")
}
return "", false, nil
}
if role, gAdmin := s.searchRole(rawJSON, groups); role.IsValid() {
return role, gAdmin, nil
} else if role != "" {
return "", false, errInvalidRole.Errorf("invalid role: %s", role)
}
if s.roleAttributeStrict {
return "", false, errRoleAttributeStrictViolation.Errorf("idP did not return a role attribute, but role_attribute_strict is set")
}
return "", false, nil
}
func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string) (org.RoleType, bool, error) {
role, gAdmin, err := s.extractRoleAndAdminOptional(rawJSON, groups)
if role == "" {
role = s.defaultRole()
}
return role, gAdmin, err
}
func (s *SocialBase) searchRole(rawJSON []byte, groups []string) (org.RoleType, bool) {
role, err := s.searchJSONForStringAttr(s.roleAttributePath, rawJSON)
if err == nil && role != "" {
return getRoleFromSearch(role)
}
if groupBytes, err := json.Marshal(groupStruct{groups}); err == nil {
role, err := s.searchJSONForStringAttr(s.roleAttributePath, groupBytes)
if err == nil && role != "" {
return getRoleFromSearch(role)
}
}
return "", false
}
// defaultRole returns the default role for the user based on the autoAssignOrgRole setting
// if legacy is enabled "" is returned indicating the previous role assignment is used.
func (s *SocialBase) defaultRole() org.RoleType {
if s.autoAssignOrgRole != "" {
s.log.Debug("No role found, returning default.")
return org.RoleType(s.autoAssignOrgRole)
}
// should never happen
return org.RoleViewer
}
// match grafana admin role and translate to org role and bool.
// treat the JSON search result to ensure correct casing.
func getRoleFromSearch(role string) (org.RoleType, bool) {
if strings.EqualFold(role, RoleGrafanaAdmin) {
return org.RoleAdmin, true
}
return org.RoleType(cases.Title(language.Und).String(role)), false
}
// GetOAuthProviders returns available oauth providers and if they're enabled or not
func (ss *SocialService) GetOAuthProviders() map[string]bool {
result := map[string]bool{}
for name, conn := range ss.socialMap {
result[name] = conn.GetOAuthInfo().Enabled
}
return result
}
func (ss *SocialService) GetOAuthHttpClient(name string) (*http.Client, error) {
// The socialMap keys don't have "oauth_" prefix, but everywhere else in the system does
name = strings.TrimPrefix(name, "oauth_")
provider, ok := ss.socialMap[name]
if !ok {
return nil, fmt.Errorf("could not find %q in OAuth Settings", name)
}
info := provider.GetOAuthInfo()
if !info.Enabled {
return nil, fmt.Errorf("oauth provider %q is not enabled", name)
}
// handle call back
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: info.TlsSkipVerify,
},
DialContext: (&net.Dialer{
Timeout: time.Second * 10,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 15 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
}
oauthClient := &http.Client{
Transport: tr,
Timeout: time.Second * 15,
}
if info.TlsClientCert != "" || info.TlsClientKey != "" {
cert, err := tls.LoadX509KeyPair(info.TlsClientCert, info.TlsClientKey)
if err != nil {
ss.log.Error("Failed to setup TlsClientCert", "oauth", name, "error", err)
return nil, fmt.Errorf("failed to setup TlsClientCert: %w", err)
}
tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
}
if info.TlsClientCa != "" {
caCert, err := os.ReadFile(info.TlsClientCa)
if err != nil {
ss.log.Error("Failed to setup TlsClientCa", "oauth", name, "error", err)
return nil, fmt.Errorf("failed to setup TlsClientCa: %w", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tr.TLSClientConfig.RootCAs = caCertPool
}
return oauthClient, nil
}
func (ss *SocialService) GetConnector(name string) (SocialConnector, error) {
// The socialMap keys don't have "oauth_" prefix, but everywhere else in the system does
provider := strings.TrimPrefix(name, "oauth_")
connector, ok := ss.socialMap[provider]
if !ok {
return nil, fmt.Errorf("failed to find oauth provider for %q", name)
}
return connector, nil
}
func (ss *SocialService) GetOAuthInfoProvider(name string) *OAuthInfo {
connector, ok := ss.socialMap[name]
if !ok {
return nil
}
return connector.GetOAuthInfo()
}
// GetOAuthInfoProviders returns enabled OAuth providers
func (ss *SocialService) GetOAuthInfoProviders() map[string]*OAuthInfo {
result := map[string]*OAuthInfo{}
for name, connector := range ss.socialMap {
info := connector.GetOAuthInfo()
if info.Enabled {
result[name] = info
}
}
return result
}
func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]any, error) {
m := map[string]any{}
authTypes := map[string]bool{}
for provider, enabled := range ss.GetOAuthProviders() {
authTypes["oauth_"+provider] = enabled
}
for authType, enabled := range authTypes {
enabledValue := 0
if enabled {
enabledValue = 1
}
m["stats.auth_enabled."+authType+".count"] = enabledValue
}
return m, nil
}
func (s *SocialBase) isGroupMember(groups []string) bool {
if len(s.allowedGroups) == 0 {
return true
}
for _, allowedGroup := range s.allowedGroups {
for _, group := range groups {
if group == allowedGroup {
return true
}
}
}
return false
}
func (s *SocialBase) retrieveRawIDToken(idToken any) ([]byte, error) {
tokenString, ok := idToken.(string)
if !ok {
return nil, fmt.Errorf("id_token is not a string: %v", idToken)
}
jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)[.]([-_a-zA-Z0-9=]+)$")
matched := jwtRegexp.FindStringSubmatch(tokenString)
if matched == nil {
return nil, fmt.Errorf("id_token is not in JWT format: %s", tokenString)
}
rawJSON, err := base64.RawURLEncoding.DecodeString(matched[2])
if err != nil {
return nil, fmt.Errorf("error base64 decoding id_token: %w", err)
}
headerBytes, err := base64.RawURLEncoding.DecodeString(matched[1])
if err != nil {
return nil, fmt.Errorf("error base64 decoding header: %w", err)
}
var header map[string]any
if err := json.Unmarshal(headerBytes, &header); err != nil {
return nil, fmt.Errorf("error deserializing header: %w", err)
}
if compressionVal, exists := header["zip"]; exists {
compression, ok := compressionVal.(string)
if !ok {
return nil, fmt.Errorf("unrecognized compression header: %v", compressionVal)
}
if compression != "DEF" {
return nil, fmt.Errorf("unknown compression algorithm: %s", compression)
}
fr, err := zlib.NewReader(bytes.NewReader(rawJSON))
if err != nil {
return nil, fmt.Errorf("error creating zlib reader: %w", err)
}
defer func() {
if err := fr.Close(); err != nil {
s.log.Warn("Failed closing zlib reader", "error", err)
}
}()
rawJSON, err = io.ReadAll(fr)
if err != nil {
return nil, fmt.Errorf("error decompressing payload: %w", err)
}
}
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) {
if !slices.Contains(config.Scopes, OfflineAccessScope) {
config.Scopes = append(config.Scopes, OfflineAccessScope)
}
}

View File

@ -0,0 +1,245 @@
package socialimpl
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/http"
"os"
"strings"
"time"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/social/connectors"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/services/supportbundles"
"github.com/grafana/grafana/pkg/setting"
)
var (
allOauthes = []string{social.GitHubProviderName, social.GitlabProviderName, social.GoogleProviderName, social.GenericOAuthProviderName, social.GrafanaNetProviderName,
social.GrafanaComProviderName, social.AzureADProviderName, social.OktaProviderName}
)
type SocialService struct {
cfg *setting.Cfg
socialMap map[string]social.SocialConnector
log log.Logger
}
func ProvideService(cfg *setting.Cfg,
features *featuremgmt.FeatureManager,
usageStats usagestats.Service,
bundleRegistry supportbundles.Service,
cache remotecache.CacheStorage,
ssoSettings ssosettings.Service,
) *SocialService {
ss := &SocialService{
cfg: cfg,
socialMap: make(map[string]social.SocialConnector),
log: log.New("login.social"),
}
usageStats.RegisterMetricsFunc(ss.getUsageStats)
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
allSettings, err := ssoSettings.List(context.Background())
if err != nil {
ss.log.Error("Failed to get SSO settings", "error", err)
}
for _, ssoSetting := range allSettings {
conn, err := createOAuthConnector(ssoSetting.Provider, ssoSetting.OAuthSettings, cfg, ssoSettings, features, cache)
if err != nil {
ss.log.Error("Failed to create OAuth provider", "error", err, "provider", ssoSetting.Provider)
continue
}
ss.socialMap[ssoSetting.Provider] = conn
}
} else {
for _, name := range allOauthes {
sec := cfg.Raw.Section("auth." + name)
settingsKVs := convertIniSectionToMap(sec)
info, err := connectors.CreateOAuthInfoFromKeyValues(settingsKVs)
if err != nil {
ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", name)
continue
}
if !info.Enabled {
continue
}
if name == social.GrafanaNetProviderName {
name = social.GrafanaComProviderName
}
conn, _ := createOAuthConnector(name, info, cfg, ssoSettings, features, cache)
ss.socialMap[name] = conn
}
}
ss.registerSupportBundleCollectors(bundleRegistry)
return ss
}
// GetOAuthProviders returns available oauth providers and if they're enabled or not
func (ss *SocialService) GetOAuthProviders() map[string]bool {
result := map[string]bool{}
for name, conn := range ss.socialMap {
result[name] = conn.GetOAuthInfo().Enabled
}
return result
}
func (ss *SocialService) GetOAuthHttpClient(name string) (*http.Client, error) {
// The socialMap keys don't have "oauth_" prefix, but everywhere else in the system does
name = strings.TrimPrefix(name, "oauth_")
provider, ok := ss.socialMap[name]
if !ok {
return nil, fmt.Errorf("could not find %q in OAuth Settings", name)
}
info := provider.GetOAuthInfo()
if !info.Enabled {
return nil, fmt.Errorf("oauth provider %q is not enabled", name)
}
// handle call back
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: info.TlsSkipVerify,
},
DialContext: (&net.Dialer{
Timeout: time.Second * 10,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 15 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
}
oauthClient := &http.Client{
Transport: tr,
Timeout: time.Second * 15,
}
if info.TlsClientCert != "" || info.TlsClientKey != "" {
cert, err := tls.LoadX509KeyPair(info.TlsClientCert, info.TlsClientKey)
if err != nil {
ss.log.Error("Failed to setup TlsClientCert", "oauth", name, "error", err)
return nil, fmt.Errorf("failed to setup TlsClientCert: %w", err)
}
tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
}
if info.TlsClientCa != "" {
caCert, err := os.ReadFile(info.TlsClientCa)
if err != nil {
ss.log.Error("Failed to setup TlsClientCa", "oauth", name, "error", err)
return nil, fmt.Errorf("failed to setup TlsClientCa: %w", err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
tr.TLSClientConfig.RootCAs = caCertPool
}
return oauthClient, nil
}
func (ss *SocialService) GetConnector(name string) (social.SocialConnector, error) {
// The socialMap keys don't have "oauth_" prefix, but everywhere else in the system does
provider := strings.TrimPrefix(name, "oauth_")
connector, ok := ss.socialMap[provider]
if !ok {
return nil, fmt.Errorf("failed to find oauth provider for %q", name)
}
return connector, nil
}
func (ss *SocialService) GetOAuthInfoProvider(name string) *social.OAuthInfo {
connector, ok := ss.socialMap[name]
if !ok {
return nil
}
return connector.GetOAuthInfo()
}
// GetOAuthInfoProviders returns enabled OAuth providers
func (ss *SocialService) GetOAuthInfoProviders() map[string]*social.OAuthInfo {
result := map[string]*social.OAuthInfo{}
for name, connector := range ss.socialMap {
info := connector.GetOAuthInfo()
if info.Enabled {
result[name] = info
}
}
return result
}
func (ss *SocialService) getUsageStats(ctx context.Context) (map[string]any, error) {
m := map[string]any{}
authTypes := map[string]bool{}
for provider, enabled := range ss.GetOAuthProviders() {
authTypes["oauth_"+provider] = enabled
}
for authType, enabled := range authTypes {
enabledValue := 0
if enabled {
enabledValue = 1
}
m["stats.auth_enabled."+authType+".count"] = enabledValue
}
return m, nil
}
func createOAuthConnector(name string, info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssosettings.Service, features *featuremgmt.FeatureManager, cache remotecache.CacheStorage) (social.SocialConnector, error) {
switch name {
case social.AzureADProviderName:
return connectors.NewAzureADProvider(info, cfg, ssoSettings, features, cache), nil
case social.GenericOAuthProviderName:
return connectors.NewGenericOAuthProvider(info, cfg, ssoSettings, features), nil
case social.GitHubProviderName:
return connectors.NewGitHubProvider(info, cfg, ssoSettings, features), nil
case social.GitlabProviderName:
return connectors.NewGitLabProvider(info, cfg, ssoSettings, features), nil
case social.GoogleProviderName:
return connectors.NewGoogleProvider(info, cfg, ssoSettings, features), nil
case social.GrafanaComProviderName:
return connectors.NewGrafanaComProvider(info, cfg, ssoSettings, features), nil
case social.OktaProviderName:
return connectors.NewOktaProvider(info, cfg, ssoSettings, features), nil
default:
return nil, fmt.Errorf("unknown oauth provider: %s", name)
}
}
// 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
}

View File

@ -1,10 +1,13 @@
package social
package socialimpl
import (
"testing"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/social/connectors"
"github.com/stretchr/testify/require"
)
func TestMapping_IniSectionOAuthInfo(t *testing.T) {
@ -53,7 +56,7 @@ signout_redirect_url = https://oauth.com/signout?post_logout_redirect_uri=https:
iniFile, err := ini.Load([]byte(iniContent))
require.NoError(t, err)
expectedOAuthInfo := &OAuthInfo{
expectedOAuthInfo := &social.OAuthInfo{
Name: "OAuth",
Icon: "signin",
Enabled: true,
@ -96,7 +99,7 @@ signout_redirect_url = https://oauth.com/signout?post_logout_redirect_uri=https:
}
settingsKVs := convertIniSectionToMap(iniFile.Section("test"))
oauthInfo, err := CreateOAuthInfoFromKeyValues(settingsKVs)
oauthInfo, err := connectors.CreateOAuthInfoFromKeyValues(settingsKVs)
require.NoError(t, err)
require.Equal(t, expectedOAuthInfo, oauthInfo)

View File

@ -1,4 +1,4 @@
package social
package socialimpl
import (
"bytes"
@ -9,6 +9,7 @@ import (
"github.com/BurntSushi/toml"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/supportbundles"
)
@ -26,7 +27,7 @@ func (ss *SocialService) registerSupportBundleCollectors(bundleRegistry supportb
}
}
func (ss *SocialService) supportBundleCollectorFn(name string, sc SocialConnector) func(context.Context) (*supportbundles.SupportItem, error) {
func (ss *SocialService) supportBundleCollectorFn(name string, sc social.SocialConnector) func(context.Context) (*supportbundles.SupportItem, error) {
return func(ctx context.Context) (*supportbundles.SupportItem, error) {
bWriter := bytes.NewBuffer(nil)
@ -61,7 +62,7 @@ func (ss *SocialService) supportBundleCollectorFn(name string, sc SocialConnecto
}
}
func (ss *SocialService) healthCheckSocialConnector(ctx context.Context, name string, oinfo *OAuthInfo, bWriter *bytes.Buffer) {
func (ss *SocialService) healthCheckSocialConnector(ctx context.Context, name string, oinfo *social.OAuthInfo, bWriter *bytes.Buffer) {
bWriter.WriteString("## Health checks\n\n")
client, err := ss.GetOAuthHttpClient(name)
if err != nil {

View File

@ -22,11 +22,11 @@ type MockSocialConnector struct {
// AuthCodeURL provides a mock function with given fields: state, opts
func (_m *MockSocialConnector) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
_va := make([]any, len(opts))
_va := make([]interface{}, len(opts))
for _i := range opts {
_va[_i] = opts[_i]
}
var _ca []any
var _ca []interface{}
_ca = append(_ca, state)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)
@ -59,11 +59,11 @@ func (_m *MockSocialConnector) Client(ctx context.Context, t *oauth2.Token) *htt
// Exchange provides a mock function with given fields: ctx, code, authOptions
func (_m *MockSocialConnector) Exchange(ctx context.Context, code string, authOptions ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
_va := make([]any, len(authOptions))
_va := make([]interface{}, len(authOptions))
for _i := range authOptions {
_va[_i] = authOptions[_i]
}
var _ca []any
var _ca []interface{}
_ca = append(_ca, ctx, code)
_ca = append(_ca, _va...)
ret := _m.Called(_ca...)

View File

@ -31,6 +31,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
"github.com/grafana/grafana/pkg/infra/usagestats/validator"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/social/socialimpl"
"github.com/grafana/grafana/pkg/middleware/csrf"
"github.com/grafana/grafana/pkg/middleware/loggermw"
apiregistry "github.com/grafana/grafana/pkg/registry/apis"
@ -256,9 +257,9 @@ var wireBasicSet = wire.NewSet(
testdatasource.ProvideService,
ldapapi.ProvideService,
opentsdb.ProvideService,
social.ProvideService,
socialimpl.ProvideService,
influxdb.ProvideService,
wire.Bind(new(social.Service), new(*social.SocialService)),
wire.Bind(new(social.Service), new(*socialimpl.SocialService)),
tempo.ProvideService,
loki.ProvideService,
graphite.ProvideService,

View File

@ -16,7 +16,7 @@ import (
"github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/socialtest"
"github.com/grafana/grafana/pkg/login/social/socialtest"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/auth/identity"

View File

@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/social/connectors"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
@ -49,7 +50,7 @@ var (
errOAuthEmailNotAllowed = errutil.Unauthorized("auth.oauth.email.not-allowed", errutil.WithPublicMessage("Required email domain not fulfilled"))
)
func fromSocialErr(err *social.Error) error {
func fromSocialErr(err *connectors.SocialError) error {
return errutil.Unauthorized("auth.oauth.userinfo.failed", errutil.WithPublicMessage(err.Error())).Errorf("%w", err)
}
@ -118,7 +119,7 @@ func (c *OAuth) Authenticate(ctx context.Context, r *authn.Request) (*authn.Iden
userInfo, err := c.connector.UserInfo(ctx, c.connector.Client(clientCtx, token), token)
if err != nil {
var sErr *social.Error
var sErr *connectors.SocialError
if errors.As(err, &sErr) {
return nil, fromSocialErr(sErr)
}

View File

@ -13,7 +13,7 @@ import (
"golang.org/x/oauth2"
"golang.org/x/sync/singleflight"
"github.com/grafana/grafana/pkg/login/socialtest"
"github.com/grafana/grafana/pkg/login/social/socialtest"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
"github.com/grafana/grafana/pkg/services/user"

View File

@ -1,6 +1,7 @@
package api
import (
"context"
"errors"
"net/http"
@ -8,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/infra/log"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
@ -56,7 +58,7 @@ func (api *Api) RegisterAPIEndpoints() {
}
func (api *Api) listAllProvidersSettings(c *contextmodel.ReqContext) response.Response {
providers, err := api.SSOSettingsService.List(c.Req.Context(), c.SignedInUser)
providers, err := api.getAuthorizedList(c.Req.Context(), c.SignedInUser)
if err != nil {
return response.Error(500, "Failed to get providers", err)
}
@ -75,6 +77,31 @@ func (api *Api) listAllProvidersSettings(c *contextmodel.ReqContext) response.Re
return response.JSON(http.StatusOK, dtos)
}
func (api *Api) getAuthorizedList(ctx context.Context, identity identity.Requester) ([]*models.SSOSettings, error) {
allProviders, err := api.SSOSettingsService.List(ctx)
if err != nil {
return nil, err
}
var authorizedProviders []*models.SSOSettings
for _, provider := range allProviders {
ev := ac.EvalPermission(ac.ActionSettingsRead, ac.Scope("settings", "auth."+provider.Provider, "*"))
hasAccess, err := api.AccessControl.Evaluate(ctx, identity, ev)
if err != nil {
api.Log.FromContext(ctx).Error("Failed to evaluate permissions", "error", err)
return nil, err
}
if !hasAccess {
continue
}
authorizedProviders = append(authorizedProviders, provider)
}
return authorizedProviders, nil
}
func (api *Api) getProviderSettings(c *contextmodel.ReqContext) response.Response {
key, ok := web.Params(c.Req)[":key"]
if !ok {

View File

@ -4,7 +4,6 @@ import (
"context"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/ssosettings/models"
)
@ -21,7 +20,7 @@ var (
//go:generate mockery --name Service --structname MockService --outpkg ssosettingstests --filename service_mock.go --output ./ssosettingstests/
type Service interface {
// List returns all SSO settings from DB and config files
List(ctx context.Context, requester identity.Requester) ([]*models.SSOSettings, error)
List(ctx context.Context) ([]*models.SSOSettings, error)
// GetForProvider returns the SSO settings for a given provider (DB or config file)
GetForProvider(ctx context.Context, provider string) (*models.SSOSettings, error)
// Upsert creates or updates the SSO settings for a given provider
@ -30,15 +29,16 @@ type Service interface {
Delete(ctx context.Context, provider string) error
// Patch updates the specified SSO settings (key-value pairs) for a given provider
Patch(ctx context.Context, provider string, data map[string]any) error
// RegisterReloadable registers a reloadable provider
RegisterReloadable(ctx context.Context, provider string, reloadable Reloadable)
// Reload implements ssosettings.Reloadable interface
// RegisterReloadable registers a reloadable for a given provider
RegisterReloadable(provider string, reloadable Reloadable)
// Reload reloads the settings for a given provider
Reload(ctx context.Context, provider string)
}
// Reloadable is an interface that can be implemented by a provider to allow it to be reloaded
// Reloadable is an interface that can be implemented by a provider to allow it to be validated and reloaded
type Reloadable interface {
Reload(ctx context.Context) error
Reload(ctx context.Context, settings models.SSOSettings) error
Validate(ctx context.Context, settings models.SSOSettings) error
}
// FallbackStrategy is an interface that can be implemented to allow a provider to load settings from a different source

View File

@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/ssosettings"
@ -23,12 +22,14 @@ import (
var _ ssosettings.Service = (*SSOSettingsService)(nil)
type SSOSettingsService struct {
log log.Logger
cfg *setting.Cfg
store ssosettings.Store
ac ac.AccessControl
log log.Logger
cfg *setting.Cfg
store ssosettings.Store
ac ac.AccessControl
secrets secrets.Service
fbStrategies []ssosettings.FallbackStrategy
secrets secrets.Service
reloadables map[string]ssosettings.Reloadable
}
func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl,
@ -48,6 +49,7 @@ func ProvideService(cfg *setting.Cfg, sqlStore db.DB, ac ac.AccessControl,
ac: ac,
fbStrategies: strategies,
secrets: secrets,
reloadables: make(map[string]ssosettings.Reloadable),
}
if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) {
@ -81,7 +83,7 @@ func (s *SSOSettingsService) GetForProvider(ctx context.Context, provider string
return storeSettings, nil
}
func (s *SSOSettingsService) List(ctx context.Context, requester identity.Requester) ([]*models.SSOSettings, error) {
func (s *SSOSettingsService) List(ctx context.Context) ([]*models.SSOSettings, error) {
result := make([]*models.SSOSettings, 0, len(ssosettings.AllOAuthProviders))
storedSettings, err := s.store.List(ctx)
@ -90,25 +92,15 @@ func (s *SSOSettingsService) List(ctx context.Context, requester identity.Reques
}
for _, provider := range ssosettings.AllOAuthProviders {
ev := ac.EvalPermission(ac.ActionSettingsRead, ac.Scope("settings", "auth."+provider, "*"))
hasAccess, err := s.ac.Evaluate(ctx, requester, ev)
if err != nil {
return nil, err
}
if !hasAccess {
continue
}
settings := getSettingsByProvider(provider, storedSettings)
if len(settings) == 0 {
// If there is no data in the DB then we need to load the settings using the fallback strategy
fallbackSettings, err := s.loadSettingsUsingFallbackStrategy(ctx, provider)
setting, err := s.loadSettingsUsingFallbackStrategy(ctx, provider)
if err != nil {
return nil, err
}
settings = append(settings, fallbackSettings)
settings = append(settings, setting)
}
result = append(result, settings...)
}
@ -117,7 +109,8 @@ func (s *SSOSettingsService) List(ctx context.Context, requester identity.Reques
}
func (s *SSOSettingsService) Upsert(ctx context.Context, settings models.SSOSettings) error {
// TODO: validation (configurable provider? Contains the required fields? etc)
// TODO: also check whether the provider is configurable
// Get the connector for the provider (from the reloadables) and call Validate
if isOAuthProvider(settings.Provider) {
encryptedClientSecret, err := s.secrets.Encrypt(ctx, []byte(settings.OAuthSettings.ClientSecret), secrets.WithoutScope())
@ -131,6 +124,7 @@ func (s *SSOSettingsService) Upsert(ctx context.Context, settings models.SSOSett
if err != nil {
return err
}
return nil
}
@ -146,8 +140,11 @@ func (s *SSOSettingsService) Reload(ctx context.Context, provider string) {
panic("not implemented") // TODO: Implement
}
func (s *SSOSettingsService) RegisterReloadable(ctx context.Context, provider string, reloadable ssosettings.Reloadable) {
panic("not implemented") // TODO: Implement
func (s *SSOSettingsService) RegisterReloadable(provider string, reloadable ssosettings.Reloadable) {
if s.reloadables == nil {
s.reloadables = make(map[string]ssosettings.Reloadable)
}
s.reloadables[provider] = reloadable
}
func (s *SSOSettingsService) RegisterFallbackStrategy(providerRegex string, strategy ssosettings.FallbackStrategy) {

View File

@ -13,12 +13,10 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/auth/identity"
secretsFakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/services/ssosettings/models"
"github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
@ -106,34 +104,11 @@ func TestSSOSettingsService_GetForProvider(t *testing.T) {
}
func TestSSOSettingsService_List(t *testing.T) {
defaultIdentity := &user.SignedInUser{
UserID: 1,
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionSettingsRead: {accesscontrol.ScopeSettingsAll},
},
},
}
scopedIdentity := &user.SignedInUser{
UserID: 1,
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {
accesscontrol.ActionSettingsRead: []string{
accesscontrol.Scope("settings", "auth.azuread", "*"),
accesscontrol.Scope("settings", "auth.github", "*"),
},
},
},
}
testCases := []struct {
name string
setup func(env testEnv)
identity identity.Requester
want []*models.SSOSettings
wantErr bool
name string
setup func(env testEnv)
want []*models.SSOSettings
wantErr bool
}{
{
name: "should return successfully",
@ -153,7 +128,6 @@ func TestSSOSettingsService_List(t *testing.T) {
env.fallbackStrategy.ExpectedIsMatch = true
env.fallbackStrategy.ExpectedConfig = &social.OAuthInfo{Enabled: false}
},
identity: defaultIdentity,
want: []*models.SSOSettings{
{
Provider: "github",
@ -194,44 +168,10 @@ func TestSSOSettingsService_List(t *testing.T) {
wantErr: false,
},
{
name: "should return the settings that the user has access to",
setup: func(env testEnv) {
env.store.ExpectedSSOSettings = []*models.SSOSettings{
{
Provider: "github",
OAuthSettings: &social.OAuthInfo{Enabled: true},
Source: models.DB,
},
{
Provider: "okta",
OAuthSettings: &social.OAuthInfo{Enabled: true},
Source: models.DB,
},
}
env.fallbackStrategy.ExpectedIsMatch = true
env.fallbackStrategy.ExpectedConfig = &social.OAuthInfo{Enabled: false}
},
identity: scopedIdentity,
want: []*models.SSOSettings{
{
Provider: "github",
OAuthSettings: &social.OAuthInfo{Enabled: true},
Source: models.DB,
},
{
Provider: "azuread",
OAuthSettings: &social.OAuthInfo{Enabled: false},
Source: models.System,
},
},
wantErr: false,
},
{
name: "should return error if store returns an error",
setup: func(env testEnv) { env.store.ExpectedError = fmt.Errorf("error") },
identity: defaultIdentity,
want: nil,
wantErr: true,
name: "should return error if store returns an error",
setup: func(env testEnv) { env.store.ExpectedError = fmt.Errorf("error") },
want: nil,
wantErr: true,
},
{
name: "should use the fallback strategy if store returns empty list",
@ -240,7 +180,6 @@ func TestSSOSettingsService_List(t *testing.T) {
env.fallbackStrategy.ExpectedIsMatch = true
env.fallbackStrategy.ExpectedConfig = &social.OAuthInfo{Enabled: false}
},
identity: defaultIdentity,
want: []*models.SSOSettings{
{
Provider: "github",
@ -286,9 +225,8 @@ func TestSSOSettingsService_List(t *testing.T) {
env.store.ExpectedSSOSettings = []*models.SSOSettings{}
env.fallbackStrategy.ExpectedIsMatch = false
},
identity: defaultIdentity,
want: nil,
wantErr: true,
want: nil,
wantErr: true,
},
}
for _, tc := range testCases {
@ -298,7 +236,7 @@ func TestSSOSettingsService_List(t *testing.T) {
tc.setup(env)
}
actual, err := env.service.List(context.Background(), tc.identity)
actual, err := env.service.List(context.Background())
if tc.wantErr {
require.Error(t, err)
@ -416,6 +354,7 @@ func setupTestEnv(t *testing.T) testEnv {
store: store,
ac: accessControl,
fbStrategies: []ssosettings.FallbackStrategy{fallbackStrategy},
reloadables: make(map[string]ssosettings.Reloadable),
secrets: secrets,
}

View File

@ -1,14 +1,12 @@
// Code generated by mockery v2.37.1. DO NOT EDIT.
// Code generated by mockery v2.27.1. DO NOT EDIT.
package ssosettingstests
import (
context "context"
identity "github.com/grafana/grafana/pkg/services/auth/identity"
mock "github.com/stretchr/testify/mock"
models "github.com/grafana/grafana/pkg/services/ssosettings/models"
mock "github.com/stretchr/testify/mock"
ssosettings "github.com/grafana/grafana/pkg/services/ssosettings"
)
@ -58,25 +56,25 @@ func (_m *MockService) GetForProvider(ctx context.Context, provider string) (*mo
return r0, r1
}
// List provides a mock function with given fields: ctx, requester
func (_m *MockService) List(ctx context.Context, requester identity.Requester) ([]*models.SSOSettings, error) {
ret := _m.Called(ctx, requester)
// List provides a mock function with given fields: ctx
func (_m *MockService) List(ctx context.Context) ([]*models.SSOSettings, error) {
ret := _m.Called(ctx)
var r0 []*models.SSOSettings
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) ([]*models.SSOSettings, error)); ok {
return rf(ctx, requester)
if rf, ok := ret.Get(0).(func(context.Context) ([]*models.SSOSettings, error)); ok {
return rf(ctx)
}
if rf, ok := ret.Get(0).(func(context.Context, identity.Requester) []*models.SSOSettings); ok {
r0 = rf(ctx, requester)
if rf, ok := ret.Get(0).(func(context.Context) []*models.SSOSettings); ok {
r0 = rf(ctx)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*models.SSOSettings)
}
}
if rf, ok := ret.Get(1).(func(context.Context, identity.Requester) error); ok {
r1 = rf(ctx, requester)
if rf, ok := ret.Get(1).(func(context.Context) error); ok {
r1 = rf(ctx)
} else {
r1 = ret.Error(1)
}
@ -98,9 +96,9 @@ func (_m *MockService) Patch(ctx context.Context, provider string, data map[stri
return r0
}
// RegisterReloadable provides a mock function with given fields: ctx, provider, reloadable
func (_m *MockService) RegisterReloadable(ctx context.Context, provider string, reloadable ssosettings.Reloadable) {
_m.Called(ctx, provider, reloadable)
// RegisterReloadable provides a mock function with given fields: provider, reloadable
func (_m *MockService) RegisterReloadable(provider string, reloadable ssosettings.Reloadable) {
_m.Called(provider, reloadable)
}
// Reload provides a mock function with given fields: ctx, provider
@ -122,12 +120,13 @@ func (_m *MockService) Upsert(ctx context.Context, settings models.SSOSettings)
return r0
}
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockService(t interface {
type mockConstructorTestingTNewMockService interface {
mock.TestingT
Cleanup(func())
}) *MockService {
}
// NewMockService creates a new instance of MockService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockService(t mockConstructorTestingTNewMockService) *MockService {
mock := &MockService{}
mock.Mock.Test(t)

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.37.1. DO NOT EDIT.
// Code generated by mockery v2.27.1. DO NOT EDIT.
package ssosettingstests
@ -108,12 +108,13 @@ func (_m *MockStore) Upsert(ctx context.Context, settings models.SSOSettings) er
return r0
}
// NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockStore(t interface {
type mockConstructorTestingTNewMockStore interface {
mock.TestingT
Cleanup(func())
}) *MockStore {
}
// NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockStore(t mockConstructorTestingTNewMockStore) *MockStore {
mock := &MockStore{}
mock.Mock.Test(t)

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/login/social/connectors"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@ -15,11 +16,11 @@ type OAuthStrategy struct {
}
var extraKeysByProvider = map[string][]string{
social.AzureADProviderName: social.ExtraAzureADSettingKeys,
social.GenericOAuthProviderName: social.ExtraGenericOAuthSettingKeys,
social.GitHubProviderName: social.ExtraGithubSettingKeys,
social.GrafanaComProviderName: social.ExtraGrafanaComSettingKeys,
social.GrafanaNetProviderName: social.ExtraGrafanaComSettingKeys,
social.AzureADProviderName: connectors.ExtraAzureADSettingKeys,
social.GenericOAuthProviderName: connectors.ExtraGenericOAuthSettingKeys,
social.GitHubProviderName: connectors.ExtraGithubSettingKeys,
social.GrafanaComProviderName: connectors.ExtraGrafanaComSettingKeys,
social.GrafanaNetProviderName: connectors.ExtraGrafanaComSettingKeys,
}
var _ ssosettings.FallbackStrategy = (*OAuthStrategy)(nil)