Auth: Load ini/env vars settings in the fallback strategy (#78495)

* Return data in camelCase from the OAuth fb strategy

* changes

* wip

* Add defaults for oauth fb strategy

* revert other changes

* Add tests

* Add Defaults to cfg and use it in OAuthStrategy

* Return *OAuthInfo from OAuthStrategy

* lint

* Remove unnecessary Defaults

* Introduce const for fields, fix import order

* Align failing tests

* clean up

* Changes requested by @gamab

* Update pkg/services/ssosettings/strategies/oauth_strategy_test.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* Load data on startup

* Rename + simplify

---------

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Misi
2023-12-01 15:35:44 +01:00
committed by GitHub
parent 46044efdf8
commit d3a269ab3b
18 changed files with 402 additions and 151 deletions

View File

@@ -22,11 +22,15 @@ import (
"github.com/grafana/grafana/pkg/util"
)
var (
errAzureADMissingGroups = &Error{"either the user does not have any group membership or the groups claim is missing from the token."}
const (
AzureADProviderName = "azuread"
forceUseGraphAPIKey = "force_use_graph_api" // #nosec G101 not a hardcoded credential
)
const azureADProviderName = "azuread"
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."}
)
var _ SocialConnector = (*SocialAzureAD)(nil)
@@ -74,12 +78,12 @@ func NewAzureADProvider(settings map[string]any, cfg *setting.Cfg, features *fea
return nil, err
}
config := createOAuthConfig(info, cfg, azureADProviderName)
config := createOAuthConfig(info, cfg, AzureADProviderName)
provider := &SocialAzureAD{
SocialBase: newSocialBase(azureADProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(AzureADProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
cache: cache,
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
forceUseGraphAPI: mustBool(info.Extra["force_use_graph_api"], false),
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
forceUseGraphAPI: MustBool(info.Extra[forceUseGraphAPIKey], false),
skipOrgRoleSync: cfg.AzureADSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync

View File

@@ -19,6 +19,13 @@ import (
"gopkg.in/ini.v1"
)
const (
// consider moving this to OAuthInfo
teamIdsKey = "team_ids"
// consider moving this to OAuthInfo
allowedOrganizationsKey = "allowed_organizations"
)
var (
errMissingGroupMembership = &Error{"user not a member of one of the required groups"}
)
@@ -166,7 +173,7 @@ func createOAuthConfig(info *OAuthInfo, cfg *setting.Cfg, defaultName string) *o
return &config
}
func mustBool(value any, defaultValue bool) bool {
func MustBool(value any, defaultValue bool) bool {
if value == nil {
return defaultValue
}

View File

@@ -83,6 +83,7 @@ signout_redirect_url = https://oauth.com/signout?post_logout_redirect_uri=https:
AuthStyle: "",
AllowAssignGrafanaAdmin: true,
UseRefreshToken: true,
SkipOrgRoleSync: true,
HostedDomain: "test_hosted_domain",
SignoutRedirectUrl: "https://oauth.com/signout?post_logout_redirect_uri=https://grafana.com",
Extra: map[string]string{
@@ -90,7 +91,6 @@ signout_redirect_url = https://oauth.com/signout?post_logout_redirect_uri=https:
"id_token_attribute_name": "id_token",
"login_attribute_path": "login",
"name_attribute_path": "name",
"skip_org_role_sync": "true",
"team_ids": "first, second",
},
}

View File

@@ -17,7 +17,15 @@ import (
"github.com/grafana/grafana/pkg/util"
)
const genericOAuthProviderName = "generic_oauth"
const (
GenericOAuthProviderName = "generic_oauth"
nameAttributePathKey = "name_attribute_path"
loginAttributePathKey = "login_attribute_path"
idTokenAttributeNameKey = "id_token_attribute_name" // #nosec G101 not a hardcoded credential
)
var ExtraGenericOAuthSettingKeys = []string{nameAttributePathKey, loginAttributePathKey, idTokenAttributeNameKey, teamIdsKey, allowedOrganizationsKey}
type SocialGenericOAuth struct {
*SocialBase
@@ -42,20 +50,20 @@ func NewGenericOAuthProvider(settings map[string]any, cfg *setting.Cfg, features
return nil, err
}
config := createOAuthConfig(info, cfg, genericOAuthProviderName)
config := createOAuthConfig(info, cfg, GenericOAuthProviderName)
provider := &SocialGenericOAuth{
SocialBase: newSocialBase(genericOAuthProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(GenericOAuthProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamsUrl: info.TeamsUrl,
emailAttributeName: info.EmailAttributeName,
emailAttributePath: info.EmailAttributePath,
nameAttributePath: info.Extra["name_attribute_path"],
nameAttributePath: info.Extra[nameAttributePathKey],
groupsAttributePath: info.GroupsAttributePath,
loginAttributePath: info.Extra["login_attribute_path"],
idTokenAttributeName: info.Extra["id_token_attribute_name"],
loginAttributePath: info.Extra[loginAttributePathKey],
idTokenAttributeName: info.Extra[idTokenAttributeNameKey],
teamIdsAttributePath: info.TeamIdsAttributePath,
teamIds: util.SplitString(info.Extra["team_ids"]),
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
teamIds: util.SplitString(info.Extra[teamIdsKey]),
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
allowedGroups: info.AllowedGroups,
skipOrgRoleSync: cfg.GenericOAuthSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo

View File

@@ -19,7 +19,9 @@ import (
"github.com/grafana/grafana/pkg/util/errutil"
)
const gitHubProviderName = "github"
const GitHubProviderName = "github"
var ExtraGithubSettingKeys = []string{allowedOrganizationsKey, teamIdsKey}
type SocialGithub struct {
*SocialBase
@@ -55,17 +57,17 @@ func NewGitHubProvider(settings map[string]any, cfg *setting.Cfg, features *feat
return nil, err
}
teamIds, err := mustInts(util.SplitString(info.Extra["team_ids"]))
teamIds, err := mustInts(util.SplitString(info.Extra[teamIdsKey]))
if err != nil {
return nil, err
}
config := createOAuthConfig(info, cfg, gitHubProviderName)
config := createOAuthConfig(info, cfg, GitHubProviderName)
provider := &SocialGithub{
SocialBase: newSocialBase(gitHubProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(GitHubProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
teamIds: teamIds,
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
skipOrgRoleSync: cfg.GitHubSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync

View File

@@ -19,7 +19,7 @@ import (
const (
groupPerPage = 50
accessLevelGuest = "10"
gitlabProviderName = "gitlab"
GitlabProviderName = "gitlab"
)
type SocialGitlab struct {
@@ -55,9 +55,9 @@ func NewGitLabProvider(settings map[string]any, cfg *setting.Cfg, features *feat
return nil, err
}
config := createOAuthConfig(info, cfg, gitlabProviderName)
config := createOAuthConfig(info, cfg, GitlabProviderName)
provider := &SocialGitlab{
SocialBase: newSocialBase(gitlabProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(GitlabProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
skipOrgRoleSync: cfg.GitLabSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo

View File

@@ -18,7 +18,7 @@ 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"
GoogleProviderName = "google"
)
type SocialGoogle struct {
@@ -42,9 +42,9 @@ func NewGoogleProvider(settings map[string]any, cfg *setting.Cfg, features *feat
return nil, err
}
config := createOAuthConfig(info, cfg, googleProviderName)
config := createOAuthConfig(info, cfg, GoogleProviderName)
provider := &SocialGoogle{
SocialBase: newSocialBase(googleProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(GoogleProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
hostedDomain: info.HostedDomain,
apiUrl: info.ApiUrl,
skipOrgRoleSync: cfg.GoogleSkipOrgRoleSync,

View File

@@ -15,7 +15,13 @@ import (
"github.com/grafana/grafana/pkg/util"
)
const grafanaComProviderName = "grafana_com"
const (
GrafanaComProviderName = "grafana_com"
// legacy/old settings for the provider
GrafanaNetProviderName = "grafananet"
)
var ExtraGrafanaComSettingKeys = []string{allowedOrganizationsKey}
type SocialGrafanaCom struct {
*SocialBase
@@ -39,11 +45,11 @@ func NewGrafanaComProvider(settings map[string]any, cfg *setting.Cfg, features *
info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token"
info.AuthStyle = "inheader"
config := createOAuthConfig(info, cfg, grafanaComProviderName)
config := createOAuthConfig(info, cfg, GrafanaComProviderName)
provider := &SocialGrafanaCom{
SocialBase: newSocialBase(grafanaComProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(GrafanaComProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
url: cfg.GrafanaComURL,
allowedOrganizations: util.SplitString(info.Extra["allowed_organizations"]),
allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]),
skipOrgRoleSync: cfg.GrafanaComSkipOrgRoleSync,
// FIXME: Move skipOrgRoleSync to OAuthInfo
// skipOrgRoleSync: info.SkipOrgRoleSync

View File

@@ -15,7 +15,7 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
const oktaProviderName = "okta"
const OktaProviderName = "okta"
type SocialOkta struct {
*SocialBase
@@ -49,9 +49,9 @@ func NewOktaProvider(settings map[string]any, cfg *setting.Cfg, features *featur
return nil, err
}
config := createOAuthConfig(info, cfg, oktaProviderName)
config := createOAuthConfig(info, cfg, OktaProviderName)
provider := &SocialOkta{
SocialBase: newSocialBase(oktaProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
SocialBase: newSocialBase(OktaProviderName, config, info, cfg.AutoAssignOrgRole, cfg.OAuthSkipOrgRoleUpdateSync, *features),
apiUrl: info.ApiUrl,
allowedGroups: info.AllowedGroups,
// FIXME: Move skipOrgRoleSync to OAuthInfo

View File

@@ -33,6 +33,7 @@ import (
const (
OfflineAccessScope = "offline_access"
RoleGrafanaAdmin = "GrafanaAdmin" // For AzureAD for example this value cannot contain spaces
)
type SocialService struct {
@@ -43,40 +44,50 @@ type SocialService struct {
}
type OAuthInfo struct {
AllowAssignGrafanaAdmin bool `mapstructure:"allow_assign_grafana_admin" toml:"allow_assign_grafana_admin" json:"allowAssignGrafanaAdmin"`
AllowSignup bool `mapstructure:"allow_sign_up" toml:"allow_sign_up" json:"allowSignup"`
AllowedDomains []string `mapstructure:"allowed_domains" toml:"allowed_domains" json:"allowedDomains"`
AllowedGroups []string `mapstructure:"allowed_groups" toml:"allowed_groups" json:"allowedGroups"`
ApiUrl string `mapstructure:"api_url" toml:"api_url" json:"apiUrl"`
AuthUrl string `mapstructure:"auth_url" toml:"auth_url" json:"authUrl"`
AuthStyle string `mapstructure:"auth_style" toml:"auth_style" json:"authStyle"`
AuthUrl string `mapstructure:"auth_url" toml:"auth_url" json:"authUrl"`
AutoLogin bool `mapstructure:"auto_login" toml:"auto_login" json:"autoLogin"`
ClientId string `mapstructure:"client_id" toml:"client_id" json:"clientId"`
ClientSecret string `mapstructure:"client_secret" toml:"-" json:"clientSecret"`
EmailAttributeName string `mapstructure:"email_attribute_name" toml:"email_attribute_name" json:"emailAttributeName"`
EmailAttributePath string `mapstructure:"email_attribute_path" toml:"email_attribute_path" json:"emailAttributePath"`
EmptyScopes bool `mapstructure:"empty_scopes" toml:"empty_scopes" json:"emptyScopes"`
Enabled bool `mapstructure:"enabled" toml:"enabled" json:"enabled"`
GroupsAttributePath string `mapstructure:"groups_attribute_path" toml:"groups_attribute_path" json:"groupsAttributePath"`
HostedDomain string `mapstructure:"hosted_domain" toml:"hosted_domain" json:"hostedDomain"`
Icon string `mapstructure:"icon" toml:"icon" json:"icon"`
Name string `mapstructure:"name" toml:"name" json:"name"`
RoleAttributePath string `mapstructure:"role_attribute_path" toml:"role_attribute_path" json:"roleAttributePath"`
RoleAttributeStrict bool `mapstructure:"role_attribute_strict" toml:"role_attribute_strict" json:"roleAttributeStrict"`
Scopes []string `mapstructure:"scopes" toml:"scopes" json:"scopes"`
SignoutRedirectUrl string `mapstructure:"signout_redirect_url" toml:"signout_redirect_url" json:"signoutRedirectUrl"`
SkipOrgRoleSync bool `mapstructure:"skip_org_role_sync" toml:"skip_org_role_sync" json:"skipOrgRoleSync"`
TeamIdsAttributePath string `mapstructure:"team_ids_attribute_path" toml:"team_ids_attribute_path" json:"teamIdsAttributePath"`
TeamsUrl string `mapstructure:"teams_url" toml:"teams_url" json:"teamsUrl"`
TlsClientCa string `mapstructure:"tls_client_ca" toml:"tls_client_ca" json:"tlsClientCa"`
TlsClientCert string `mapstructure:"tls_client_cert" toml:"tls_client_cert" json:"tlsClientCert"`
TlsClientKey string `mapstructure:"tls_client_key" toml:"tls_client_key" json:"tlsClientKey"`
TokenUrl string `mapstructure:"token_url" toml:"token_url" json:"tokenUrl"`
AllowedDomains []string `mapstructure:"allowed_domains" toml:"allowed_domains" json:"allowedDomains"`
AllowedGroups []string `mapstructure:"allowed_groups" toml:"allowed_groups" json:"allowedGroups"`
Scopes []string `mapstructure:"scopes" toml:"scopes" json:"scopes"`
AllowAssignGrafanaAdmin bool `mapstructure:"allow_assign_grafana_admin" toml:"allow_assign_grafana_admin" json:"allowAssignGrafanaAdmin"`
AllowSignup bool `mapstructure:"allow_sign_up" toml:"allow_sign_up" json:"allowSignup"`
AutoLogin bool `mapstructure:"auto_login" toml:"auto_login" json:"autoLogin"`
Enabled bool `mapstructure:"enabled" toml:"enabled" json:"enabled"`
RoleAttributeStrict bool `mapstructure:"role_attribute_strict" toml:"role_attribute_strict" json:"roleAttributeStrict"`
TlsSkipVerify bool `mapstructure:"tls_skip_verify_insecure" toml:"tls_skip_verify_insecure" json:"tlsSkipVerify"`
TokenUrl string `mapstructure:"token_url" toml:"token_url" json:"tokenUrl"`
UsePKCE bool `mapstructure:"use_pkce" toml:"use_pkce" json:"usePKCE"`
UseRefreshToken bool `mapstructure:"use_refresh_token" toml:"use_refresh_token" json:"useRefreshToken"`
SignoutRedirectUrl string `mapstructure:"signout_redirect_url" toml:"signout_redirect_url" json:"signoutRedirectUrl"`
Extra map[string]string `mapstructure:",remain" toml:"extra,omitempty" json:"extra"`
}
func NewOAuthInfo() *OAuthInfo {
return &OAuthInfo{
Scopes: []string{},
AllowedDomains: []string{},
AllowedGroups: []string{},
Extra: map[string]string{},
}
}
func ProvideService(cfg *setting.Cfg,
features *featuremgmt.FeatureManager,
usageStats usagestats.Service,
@@ -105,8 +116,8 @@ func ProvideService(cfg *setting.Cfg,
continue
}
if name == "grafananet" {
name = grafanaCom
if name == GrafanaNetProviderName {
name = GrafanaComProviderName
}
conn, err := ss.createOAuthConnector(name, settingsKVs, cfg, features, cache)
@@ -177,15 +188,11 @@ func (e Error) Error() string {
return e.s
}
const (
grafanaCom = "grafana_com"
RoleGrafanaAdmin = "GrafanaAdmin" // For AzureAD for example this value cannot contain spaces
)
var (
SocialBaseUrl = "/login/"
SocialMap = make(map[string]SocialConnector)
allOauthes = []string{"github", "gitlab", "google", "generic_oauth", "grafananet", grafanaCom, "azuread", "okta"}
allOauthes = []string{GitHubProviderName, GitlabProviderName, GoogleProviderName, GenericOAuthProviderName, GrafanaNetProviderName,
GrafanaComProviderName, AzureADProviderName, OktaProviderName}
)
type Service interface {
@@ -507,19 +514,19 @@ func (s *SocialBase) retrieveRawIDToken(idToken any) ([]byte, error) {
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:
case AzureADProviderName:
return NewAzureADProvider(settings, cfg, features, cache)
case genericOAuthProviderName:
case GenericOAuthProviderName:
return NewGenericOAuthProvider(settings, cfg, features)
case gitHubProviderName:
case GitHubProviderName:
return NewGitHubProvider(settings, cfg, features)
case gitlabProviderName:
case GitlabProviderName:
return NewGitLabProvider(settings, cfg, features)
case googleProviderName:
case GoogleProviderName:
return NewGoogleProvider(settings, cfg, features)
case grafanaComProviderName:
case GrafanaComProviderName:
return NewGrafanaComProvider(settings, cfg, features)
case oktaProviderName:
case OktaProviderName:
return NewOktaProvider(settings, cfg, features)
default:
return nil, fmt.Errorf("unknown oauth provider: %s", name)

View File

@@ -37,13 +37,13 @@ type SSOSettings struct {
}
type SSOSettingsDTO struct {
ID string `xorm:"id pk" json:"id"`
Provider string `xorm:"provider" json:"provider"`
Settings map[string]interface{} `xorm:"settings" json:"settings"`
Created time.Time `xorm:"created" json:"-"`
Updated time.Time `xorm:"updated" json:"-"`
IsDeleted bool `xorm:"is_deleted" json:"-"`
Source SettingsSource `xorm:"-" json:"source"`
ID string `xorm:"id pk" json:"id"`
Provider string `xorm:"provider" json:"provider"`
Settings map[string]any `xorm:"settings" json:"settings"`
Created time.Time `xorm:"created" json:"-"`
Updated time.Time `xorm:"updated" json:"-"`
IsDeleted bool `xorm:"is_deleted" json:"-"`
Source SettingsSource `xorm:"-" json:"source"`
}
// TableName returns the table name (needed for Xorm)
@@ -79,7 +79,7 @@ func (s SSOSettings) ToSSOSettingsDTO() (*SSOSettingsDTO, error) {
return nil, err
}
var settings map[string]interface{}
var settings map[string]any
err = json.Unmarshal(settingsEncoded, &settings)
if err != nil {
return nil, err

View File

@@ -3,6 +3,7 @@ package ssosettings
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"
)
@@ -12,7 +13,7 @@ var (
// TODO: make it configurable
ConfigurableOAuthProviders = []string{"github", "gitlab", "google", "generic_oauth", "azuread", "okta"}
AllOAuthProviders = []string{"github", "gitlab", "google", "generic_oauth", "grafana_com", "azuread", "okta"}
AllOAuthProviders = []string{social.GitHubProviderName, social.GitlabProviderName, social.GoogleProviderName, social.GenericOAuthProviderName, social.GrafanaComProviderName, social.AzureADProviderName, social.OktaProviderName}
)
// Service is a SSO settings service
@@ -28,7 +29,7 @@ type Service interface {
// Delete deletes the SSO settings for a given provider (soft delete)
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]interface{}) error
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
@@ -45,7 +46,7 @@ type Reloadable interface {
// using the config file and/or environment variables. Used mostly for backwards compatibility.
type FallbackStrategy interface {
IsMatch(provider string) bool
ParseConfigFromSystem(ctx context.Context) (map[string]interface{}, error)
GetProviderConfig(ctx context.Context, provider string) (any, error)
}
// Store is a SSO settings store
@@ -55,6 +56,6 @@ type Store interface {
Get(ctx context.Context, provider string) (*models.SSOSettings, error)
List(ctx context.Context) ([]*models.SSOSettings, error)
Upsert(ctx context.Context, settings models.SSOSettings) error
Patch(ctx context.Context, provider string, data map[string]interface{}) error
Patch(ctx context.Context, provider string, data map[string]any) error
Delete(ctx context.Context, provider string) error
}

View File

@@ -121,7 +121,7 @@ func (s *SSOSettingsService) Upsert(ctx context.Context, settings models.SSOSett
return nil
}
func (s *SSOSettingsService) Patch(ctx context.Context, provider string, data map[string]interface{}) error {
func (s *SSOSettingsService) Patch(ctx context.Context, provider string, data map[string]any) error {
panic("not implemented") // TODO: Implement
}
@@ -147,21 +147,21 @@ func (s *SSOSettingsService) loadSettingsUsingFallbackStrategy(ctx context.Conte
return nil, errors.New("no fallback strategy found for provider: " + provider)
}
settingsFromSystem, err := loadStrategy.ParseConfigFromSystem(ctx)
settingsFromSystem, err := loadStrategy.GetProviderConfig(ctx, provider)
if err != nil {
return nil, err
}
oAuthInfo, err := social.CreateOAuthInfoFromKeyValues(settingsFromSystem)
if err != nil {
return nil, err
switch settingsFromSystem := settingsFromSystem.(type) {
case *social.OAuthInfo:
return &models.SSOSettings{
Provider: provider,
Source: models.System,
OAuthSettings: settingsFromSystem,
}, nil
default:
return nil, errors.New("could not parse settings from system")
}
return &models.SSOSettings{
Provider: provider,
Source: models.System,
OAuthSettings: oAuthInfo,
}, nil
}
func getSettingsByProvider(provider string, settings []*models.SSOSettings) []*models.SSOSettings {

View File

@@ -52,9 +52,7 @@ func TestSSOSettingsService_GetForProvider(t *testing.T) {
setup: func(env testEnv) {
env.store.ExpectedError = ssosettings.ErrNotFound
env.fallbackStrategy.ExpectedIsMatch = true
env.fallbackStrategy.ExpectedConfig = map[string]interface{}{
"enabled": true,
}
env.fallbackStrategy.ExpectedConfig = &social.OAuthInfo{Enabled: true}
},
want: &models.SSOSettings{
Provider: "github",
@@ -150,9 +148,7 @@ func TestSSOSettingsService_List(t *testing.T) {
},
}
env.fallbackStrategy.ExpectedIsMatch = true
env.fallbackStrategy.ExpectedConfig = map[string]interface{}{
"enabled": false,
}
env.fallbackStrategy.ExpectedConfig = &social.OAuthInfo{Enabled: false}
},
identity: defaultIdentity,
want: []*models.SSOSettings{
@@ -210,9 +206,7 @@ func TestSSOSettingsService_List(t *testing.T) {
},
}
env.fallbackStrategy.ExpectedIsMatch = true
env.fallbackStrategy.ExpectedConfig = map[string]interface{}{
"enabled": false,
}
env.fallbackStrategy.ExpectedConfig = &social.OAuthInfo{Enabled: false}
},
identity: scopedIdentity,
want: []*models.SSOSettings{
@@ -241,9 +235,7 @@ func TestSSOSettingsService_List(t *testing.T) {
setup: func(env testEnv) {
env.store.ExpectedSSOSettings = []*models.SSOSettings{}
env.fallbackStrategy.ExpectedIsMatch = true
env.fallbackStrategy.ExpectedConfig = map[string]interface{}{
"enabled": false,
}
env.fallbackStrategy.ExpectedConfig = &social.OAuthInfo{Enabled: false}
},
identity: defaultIdentity,
want: []*models.SSOSettings{

View File

@@ -4,7 +4,7 @@ import context "context"
type FakeFallbackStrategy struct {
ExpectedIsMatch bool
ExpectedConfig map[string]interface{}
ExpectedConfig any
ExpectedError error
}
@@ -17,6 +17,6 @@ func (f *FakeFallbackStrategy) IsMatch(provider string) bool {
return f.ExpectedIsMatch
}
func (f *FakeFallbackStrategy) ParseConfigFromSystem(ctx context.Context) (map[string]interface{}, error) {
func (f *FakeFallbackStrategy) GetProviderConfig(ctx context.Context, provider string) (any, error) {
return f.ExpectedConfig, f.ExpectedError
}

View File

@@ -31,7 +31,7 @@ func (f *FakeStore) Upsert(ctx context.Context, settings models.SSOSettings) err
return f.ExpectedError
}
func (f *FakeStore) Patch(ctx context.Context, provider string, data map[string]interface{}) error {
func (f *FakeStore) Patch(ctx context.Context, provider string, data map[string]any) error {
return f.ExpectedError
}

View File

@@ -2,73 +2,101 @@ package strategies
import (
"context"
"regexp"
"strings"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/ssosettings"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
type OAuthStrategy struct {
provider string
cfg *setting.Cfg
supportedProvidersRegex *regexp.Regexp
cfg *setting.Cfg
settingsByProvider map[string]*social.OAuthInfo
}
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,
}
var _ ssosettings.FallbackStrategy = (*OAuthStrategy)(nil)
func NewOAuthStrategy(cfg *setting.Cfg) *OAuthStrategy {
compiledRegex := regexp.MustCompile(`^` + strings.Join(ssosettings.AllOAuthProviders, "|") + `$`)
return &OAuthStrategy{
cfg: cfg,
supportedProvidersRegex: compiledRegex,
oauthStrategy := &OAuthStrategy{
cfg: cfg,
settingsByProvider: make(map[string]*social.OAuthInfo),
}
oauthStrategy.loadAllSettings()
return oauthStrategy
}
func (s *OAuthStrategy) IsMatch(provider string) bool {
return s.supportedProvidersRegex.MatchString(provider)
_, ok := s.settingsByProvider[provider]
return ok
}
func (s *OAuthStrategy) ParseConfigFromSystem(_ context.Context) (map[string]any, error) {
section := s.cfg.SectionWithEnvOverrides("auth." + s.provider)
// TODO: load the provider specific keys separately
result := map[string]any{
"client_id": section.Key("client_id").Value(),
"client_secret": section.Key("client_secret").Value(),
"scopes": section.Key("scopes").Value(),
"auth_url": section.Key("auth_url").Value(),
"token_url": section.Key("token_url").Value(),
"api_url": section.Key("api_url").Value(),
"teams_url": section.Key("teams_url").Value(),
"enabled": section.Key("enabled").MustBool(false),
"email_attribute_name": section.Key("email_attribute_name").Value(),
"email_attribute_path": section.Key("email_attribute_path").Value(),
"role_attribute_path": section.Key("role_attribute_path").Value(),
"role_attribute_strict": section.Key("role_attribute_strict").MustBool(false),
"groups_attribute_path": section.Key("groups_attribute_path").Value(),
"team_ids_attribute_path": section.Key("team_ids_attribute_path").Value(),
"allowed_domains": section.Key("allowed_domains").Value(),
"hosted_domain": section.Key("hosted_domain").Value(),
"allow_sign_up": section.Key("allow_sign_up").MustBool(true),
"name": section.Key("name").MustString("default name"), // TODO: change this default value
"icon": section.Key("icon").Value(),
// TODO: @mgyongyosi move skipOrgRoleSync here in a separate PR
// "skip_org_role_sync": section.Key("skip_org_role_sync").MustBool(false),
"tls_client_cert": section.Key("tls_client_cert").Value(),
"tls_client_key": section.Key("tls_client_key").Value(),
"tls_client_ca": section.Key("tls_client_ca").Value(),
"tls_skip_verify_insecure": section.Key("tls_skip_verify_insecure").MustBool(false),
"use_pkce": section.Key("use_pkce").MustBool(true),
"use_refresh_token": section.Key("use_refresh_token").MustBool(false),
"allow_assign_grafana_admin": section.Key("allow_assign_grafana_admin").MustBool(false),
"auto_login": section.Key("auto_login").MustBool(false),
"allowed_groups": section.Key("allowed_groups").Value(),
}
// when empty_scopes parameter exists and is true, overwrite scope with empty value
if section.Key("empty_scopes").MustBool(false) {
result["scopes"] = []string{}
}
return result, nil
func (s *OAuthStrategy) GetProviderConfig(_ context.Context, provider string) (any, error) {
return s.settingsByProvider[provider], nil
}
func (s *OAuthStrategy) loadAllSettings() {
allProviders := append(ssosettings.AllOAuthProviders, social.GrafanaNetProviderName)
for _, provider := range allProviders {
settings := s.loadSettingsForProvider(provider)
if provider == social.GrafanaNetProviderName {
provider = social.GrafanaComProviderName
}
s.settingsByProvider[provider] = settings
}
}
func (s *OAuthStrategy) loadSettingsForProvider(provider string) *social.OAuthInfo {
section := s.cfg.SectionWithEnvOverrides("auth." + provider)
result := &social.OAuthInfo{
AllowAssignGrafanaAdmin: section.Key("allow_assign_grafana_admin").MustBool(false),
AllowSignup: section.Key("allow_sign_up").MustBool(false),
AllowedDomains: util.SplitString(section.Key("allowed_domains").Value()),
AllowedGroups: util.SplitString(section.Key("allowed_groups").Value()),
ApiUrl: section.Key("api_url").Value(),
AuthStyle: section.Key("auth_style").Value(),
AuthUrl: section.Key("auth_url").Value(),
AutoLogin: section.Key("auto_login").MustBool(false),
ClientId: section.Key("client_id").Value(),
ClientSecret: section.Key("client_secret").Value(),
EmailAttributeName: section.Key("email_attribute_name").Value(),
EmailAttributePath: section.Key("email_attribute_path").Value(),
EmptyScopes: section.Key("empty_scopes").MustBool(false),
Enabled: section.Key("enabled").MustBool(false),
GroupsAttributePath: section.Key("groups_attribute_path").Value(),
HostedDomain: section.Key("hosted_domain").Value(),
Icon: section.Key("icon").Value(),
Name: section.Key("name").Value(),
RoleAttributePath: section.Key("role_attribute_path").Value(),
RoleAttributeStrict: section.Key("role_attribute_strict").MustBool(false),
Scopes: util.SplitString(section.Key("scopes").Value()),
SignoutRedirectUrl: section.Key("signout_redirect_url").Value(),
SkipOrgRoleSync: section.Key("skip_org_role_sync").MustBool(false),
TeamIdsAttributePath: section.Key("team_ids_attribute_path").Value(),
TeamsUrl: section.Key("teams_url").Value(),
TlsClientCa: section.Key("tls_client_ca").Value(),
TlsClientCert: section.Key("tls_client_cert").Value(),
TlsClientKey: section.Key("tls_client_key").Value(),
TlsSkipVerify: section.Key("tls_skip_verify_insecure").MustBool(false),
TokenUrl: section.Key("token_url").Value(),
UsePKCE: section.Key("use_pkce").MustBool(false),
UseRefreshToken: section.Key("use_refresh_token").MustBool(false),
Extra: map[string]string{},
}
extraFields := extraKeysByProvider[provider]
for _, key := range extraFields {
result.Extra[key] = section.Key(key).Value()
}
return result
}

View File

@@ -0,0 +1,196 @@
package strategies
import (
"context"
"testing"
"gopkg.in/ini.v1"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
var (
iniContent = `
[auth.generic_oauth]
name = OAuth
icon = signin
enabled = true
allow_sign_up = false
auto_login = true
client_id = test_client_id
client_secret = test_client_secret
scopes = ["openid", "profile", "email"]
empty_scopes = false
email_attribute_name = email:primary
email_attribute_path = email
login_attribute_path = login
name_attribute_path = name
role_attribute_path = role
role_attribute_strict = true
groups_attribute_path = groups
id_token_attribute_name = id_token
team_ids_attribute_path = team_ids
auth_style = inheader
auth_url = test_auth_url
token_url = test_token_url
api_url = test_api_url
teams_url = test_teams_url
allowed_domains = domain1.com
allowed_groups =
team_ids = first, second
allowed_organizations = org1, org2
tls_skip_verify_insecure = true
tls_client_cert =
tls_client_key =
tls_client_ca =
use_pkce = false
allow_assign_grafana_admin = true
skip_org_role_sync = true
use_refresh_token = true
empty_scopes =
hosted_domain = test_hosted_domain
signout_redirect_url = test_signout_redirect_url
`
expectedOAuthInfo = &social.OAuthInfo{
Name: "OAuth",
Icon: "signin",
Enabled: true,
AllowSignup: false,
AutoLogin: true,
ClientId: "test_client_id",
ClientSecret: "test_client_secret",
Scopes: []string{"openid", "profile", "email"},
EmptyScopes: false,
EmailAttributeName: "email:primary",
EmailAttributePath: "email",
RoleAttributePath: "role",
RoleAttributeStrict: true,
GroupsAttributePath: "groups",
TeamIdsAttributePath: "team_ids",
AuthUrl: "test_auth_url",
TokenUrl: "test_token_url",
ApiUrl: "test_api_url",
TeamsUrl: "test_teams_url",
AllowedDomains: []string{"domain1.com"},
AllowedGroups: []string{},
TlsSkipVerify: true,
TlsClientCert: "",
TlsClientKey: "",
TlsClientCa: "",
UsePKCE: false,
AuthStyle: "inheader",
AllowAssignGrafanaAdmin: true,
UseRefreshToken: true,
HostedDomain: "test_hosted_domain",
SkipOrgRoleSync: true,
SignoutRedirectUrl: "test_signout_redirect_url",
Extra: map[string]string{
"allowed_organizations": "org1, org2",
"id_token_attribute_name": "id_token",
"login_attribute_path": "login",
"name_attribute_path": "name",
"team_ids": "first, second",
},
}
)
func TestGetProviderConfig_EnvVarsOnly(t *testing.T) {
setupEnvVars(t)
cfg := setting.NewCfg()
strategy := NewOAuthStrategy(cfg)
result, err := strategy.GetProviderConfig(context.Background(), "generic_oauth")
require.NoError(t, err)
oauthInfo, ok := result.(*social.OAuthInfo)
require.True(t, ok)
require.Equal(t, expectedOAuthInfo, oauthInfo)
}
func TestGetProviderConfig_IniFileOnly(t *testing.T) {
iniFile, err := ini.Load([]byte(iniContent))
require.NoError(t, err)
cfg := setting.NewCfg()
cfg.Raw = iniFile
strategy := NewOAuthStrategy(cfg)
result, err := strategy.GetProviderConfig(context.Background(), "generic_oauth")
require.NoError(t, err)
oauthInfo, ok := result.(*social.OAuthInfo)
require.True(t, ok)
require.Equal(t, expectedOAuthInfo, oauthInfo)
}
func TestGetProviderConfig_EnvVarsOverrideIniFileSettings(t *testing.T) {
t.Setenv("GF_AUTH_GENERIC_OAUTH_ENABLED", "false")
t.Setenv("GF_AUTH_GENERIC_OAUTH_SKIP_ORG_ROLE_SYNC", "false")
iniFile, err := ini.Load([]byte(iniContent))
require.NoError(t, err)
cfg := setting.NewCfg()
cfg.Raw = iniFile
strategy := NewOAuthStrategy(cfg)
result, err := strategy.GetProviderConfig(context.Background(), "generic_oauth")
require.NoError(t, err)
oauthInfo, ok := result.(*social.OAuthInfo)
require.True(t, ok)
expectedOAuthInfoWithOverrides := *expectedOAuthInfo
expectedOAuthInfoWithOverrides.Enabled = false
expectedOAuthInfoWithOverrides.SkipOrgRoleSync = false
require.Equal(t, expectedOAuthInfoWithOverrides, *oauthInfo)
}
func setupEnvVars(t *testing.T) {
t.Setenv("GF_AUTH_GENERIC_OAUTH_NAME", "OAuth")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ICON", "signin")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ENABLED", "true")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP", "false")
t.Setenv("GF_AUTH_GENERIC_OAUTH_AUTO_LOGIN", "true")
t.Setenv("GF_AUTH_GENERIC_OAUTH_CLIENT_ID", "test_client_id")
t.Setenv("GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET", "test_client_secret")
t.Setenv("GF_AUTH_GENERIC_OAUTH_SCOPES", `["openid", "profile", "email"]`)
t.Setenv("GF_AUTH_GENERIC_OAUTH_EMPTY_SCOPES", "")
t.Setenv("GF_AUTH_GENERIC_OAUTH_EMAIL_ATTRIBUTE_NAME", "email:primary")
t.Setenv("GF_AUTH_GENERIC_OAUTH_EMAIL_ATTRIBUTE_PATH", "email")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH", "role")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT", "true")
t.Setenv("GF_AUTH_GENERIC_OAUTH_GROUPS_ATTRIBUTE_PATH", "groups")
t.Setenv("GF_AUTH_GENERIC_OAUTH_TEAM_IDS_ATTRIBUTE_PATH", "team_ids")
t.Setenv("GF_AUTH_GENERIC_OAUTH_AUTH_URL", "test_auth_url")
t.Setenv("GF_AUTH_GENERIC_OAUTH_TOKEN_URL", "test_token_url")
t.Setenv("GF_AUTH_GENERIC_OAUTH_API_URL", "test_api_url")
t.Setenv("GF_AUTH_GENERIC_OAUTH_TEAMS_URL", "test_teams_url")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ALLOWED_DOMAINS", "domain1.com")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ALLOWED_GROUPS", "")
t.Setenv("GF_AUTH_GENERIC_OAUTH_TLS_SKIP_VERIFY_INSECURE", "true")
t.Setenv("GF_AUTH_GENERIC_OAUTH_TLS_CLIENT_CERT", "")
t.Setenv("GF_AUTH_GENERIC_OAUTH_TLS_CLIENT_KEY", "")
t.Setenv("GF_AUTH_GENERIC_OAUTH_TLS_CLIENT_CA", "")
t.Setenv("GF_AUTH_GENERIC_OAUTH_USE_PKCE", "false")
t.Setenv("GF_AUTH_GENERIC_OAUTH_AUTH_STYLE", "inheader")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ALLOW_ASSIGN_GRAFANA_ADMIN", "true")
t.Setenv("GF_AUTH_GENERIC_OAUTH_SKIP_ORG_ROLE_SYNC", "true")
t.Setenv("GF_AUTH_GENERIC_OAUTH_USE_REFRESH_TOKEN", "true")
t.Setenv("GF_AUTH_GENERIC_OAUTH_HOSTED_DOMAIN", "test_hosted_domain")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ALLOWED_ORGANIZATIONS", "org1, org2")
t.Setenv("GF_AUTH_GENERIC_OAUTH_ID_TOKEN_ATTRIBUTE_NAME", "id_token")
t.Setenv("GF_AUTH_GENERIC_OAUTH_LOGIN_ATTRIBUTE_PATH", "login")
t.Setenv("GF_AUTH_GENERIC_OAUTH_NAME_ATTRIBUTE_PATH", "name")
t.Setenv("GF_AUTH_GENERIC_OAUTH_TEAM_IDS", "first, second")
t.Setenv("GF_AUTH_GENERIC_OAUTH_SIGNOUT_REDIRECT_URL", "test_signout_redirect_url")
}