Auth: Validate admin assignment in SSO Settings (#82233)

* Add validation for allowAssignGrafanaAdmin

* Update default values

* Do not render hidden fields

* Change error message

* Improve tests

---------

Co-authored-by: Clarity-89 <homes89@ukr.net>
This commit is contained in:
Misi 2024-02-09 13:10:23 +01:00 committed by GitHub
parent 5a5520b5da
commit b1dc505a2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 271 additions and 88 deletions

View File

@ -18,6 +18,7 @@ import (
"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/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
@ -185,13 +186,13 @@ func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettin
return nil
}
func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info)
err = validateInfo(info, requester)
if err != nil {
return err
}

View File

@ -18,10 +18,12 @@ import (
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "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"
)
@ -992,9 +994,10 @@ func TestSocialAzureAD_InitializeExtraFields(t *testing.T) {
func TestSocialAzureAD_Validate(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
wantErr error
name string
settings ssoModels.SSOSettings
requester identity.Requester
wantErr error
}{
{
name: "SSOSettings is valid",
@ -1052,13 +1055,29 @@ func TestSocialAzureAD_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewAzureADProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil)
err := s.Validate(context.Background(), tc.settings)
if tc.requester == nil {
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
}
err := s.Validate(context.Background(), tc.settings, tc.requester)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return

View File

@ -13,6 +13,7 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
@ -67,13 +68,13 @@ func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettin
return provider
}
func (s *SocialGenericOAuth) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
func (s *SocialGenericOAuth) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info)
err = validateInfo(info, requester)
if err != nil {
return err
}

View File

@ -13,11 +13,13 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "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"
)
@ -920,17 +922,20 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) {
func TestSocialGenericOAuth_Validate(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
wantErr error
name string
settings ssoModels.SSOSettings
requester identity.Requester
wantErr error
}{
{
name: "SSOSettings is valid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
},
{
name: "fails if settings map contains an invalid field",
@ -969,13 +974,30 @@ func TestSocialGenericOAuth_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGenericOAuthProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Validate(context.Background(), tc.settings)
if tc.requester == nil {
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
}
err := s.Validate(context.Background(), tc.settings, tc.requester)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
@ -74,13 +75,13 @@ func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings sso
return provider
}
func (s *SocialGithub) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
func (s *SocialGithub) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info)
err = validateInfo(info, requester)
if err != nil {
return err
}

View File

@ -12,10 +12,12 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "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"
)
@ -346,17 +348,20 @@ func TestSocialGitHub_InitializeExtraFields(t *testing.T) {
func TestSocialGitHub_Validate(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
wantErr error
name string
settings ssoModels.SSOSettings
requester identity.Requester
wantErr error
}{
{
name: "SSOSettings is valid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
},
{
name: "fails if settings map contains an invalid field",
@ -405,13 +410,31 @@ func TestSocialGitHub_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGitHubProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Validate(context.Background(), tc.settings)
if tc.requester == nil {
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
}
err := s.Validate(context.Background(), tc.settings, tc.requester)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
@ -64,13 +65,13 @@ func NewGitLabProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings sso
return provider
}
func (s *SocialGitlab) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
func (s *SocialGitlab) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info)
err = validateInfo(info, requester)
if err != nil {
return err
}

View File

@ -16,11 +16,13 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "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"
)
@ -464,17 +466,20 @@ func TestSocialGitlab_GetGroupsNextPage(t *testing.T) {
func TestSocialGitlab_Validate(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
wantErr error
name string
settings ssoModels.SSOSettings
requester identity.Requester
wantErr error
}{
{
name: "SSOSettings is valid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
},
{
name: "fails if settings map contains an invalid field",
@ -513,13 +518,31 @@ func TestSocialGitlab_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGitLabProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Validate(context.Background(), tc.settings)
if tc.requester == nil {
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
}
err := s.Validate(context.Background(), tc.settings, tc.requester)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return

View File

@ -11,6 +11,7 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
@ -54,13 +55,13 @@ func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings sso
return provider
}
func (s *SocialGoogle) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
func (s *SocialGoogle) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info)
err = validateInfo(info, requester)
if err != nil {
return err
}

View File

@ -16,10 +16,12 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "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"
)
@ -669,17 +671,20 @@ func TestSocialGoogle_UserInfo(t *testing.T) {
func TestSocialGoogle_Validate(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
wantErr error
name string
settings ssoModels.SSOSettings
requester identity.Requester
wantErr error
}{
{
name: "SSOSettings is valid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
},
{
name: "fails if settings map contains an invalid field",
@ -718,13 +723,31 @@ func TestSocialGoogle_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGoogleProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Validate(context.Background(), tc.settings)
if tc.requester == nil {
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
}
err := s.Validate(context.Background(), tc.settings, tc.requester)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
@ -52,13 +53,13 @@ func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings
return provider
}
func (s *SocialGrafanaCom) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
func (s *SocialGrafanaCom) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info)
err = validateInfo(info, requester)
if err != nil {
return err
}

View File

@ -10,9 +10,11 @@ import (
"golang.org/x/oauth2"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
ssoModels "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"
)
@ -139,15 +141,18 @@ func TestSocialGrafanaCom_Validate(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
requester identity.Requester
expectError bool
}{
{
name: "SSOSettings is valid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
expectError: false,
},
{
@ -176,13 +181,31 @@ func TestSocialGrafanaCom_Validate(t *testing.T) {
},
expectError: true,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
},
},
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewGrafanaComProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Validate(context.Background(), tc.settings)
if tc.requester == nil {
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
}
err := s.Validate(context.Background(), tc.settings, tc.requester)
if tc.expectError {
require.Error(t, err)
} else {

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models"
@ -60,13 +61,13 @@ func NewOktaProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssose
return provider
}
func (s *SocialOkta) Validate(ctx context.Context, settings ssoModels.SSOSettings) error {
func (s *SocialOkta) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info)
err = validateInfo(info, requester)
if err != nil {
return err
}

View File

@ -14,10 +14,12 @@ import (
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/ssosettings"
ssoModels "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"
)
@ -137,17 +139,20 @@ func TestSocialOkta_UserInfo(t *testing.T) {
func TestSocialOkta_Validate(t *testing.T) {
testCases := []struct {
name string
settings ssoModels.SSOSettings
wantErr error
name string
settings ssoModels.SSOSettings
requester identity.Requester
wantErr error
}{
{
name: "SSOSettings is valid",
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
},
},
requester: &user.SignedInUser{IsGrafanaAdmin: true},
},
{
name: "fails if settings map contains an invalid field",
@ -186,13 +191,30 @@ func TestSocialOkta_Validate(t *testing.T) {
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
{
name: "fails if the user is not allowed to update allow assign grafana admin",
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
settings: ssoModels.SSOSettings{
Settings: map[string]any{
"client_id": "client-id",
"allow_assign_grafana_admin": "true",
"skip_org_role_sync": "true",
},
},
wantErr: ssosettings.ErrBaseInvalidOAuthConfig,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
s := NewOktaProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures())
err := s.Validate(context.Background(), tc.settings)
if tc.requester == nil {
tc.requester = &user.SignedInUser{IsGrafanaAdmin: false}
}
err := s.Validate(context.Background(), tc.settings, tc.requester)
if tc.wantErr != nil {
require.ErrorIs(t, err, tc.wantErr)
return

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/ssosettings"
@ -220,9 +221,13 @@ func getRoleFromSearch(role string) (org.RoleType, bool) {
return org.RoleType(cases.Title(language.Und).String(role)), false
}
func validateInfo(info *social.OAuthInfo) error {
func validateInfo(info *social.OAuthInfo, requester identity.Requester) error {
if info.ClientId == "" {
return ssosettings.ErrInvalidOAuthConfig("ClientId is empty")
return ssosettings.ErrInvalidOAuthConfig("Client Id is empty.")
}
if info.AllowAssignGrafanaAdmin && !requester.GetIsGrafanaAdmin() {
return ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin can only be updated by Grafana Server Admins.")
}
if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync {

View File

@ -178,7 +178,7 @@ func (api *Api) updateProviderSettings(c *contextmodel.ReqContext) response.Resp
settings.Provider = key
err := api.SSOSettingsService.Upsert(c.Req.Context(), &settings)
err := api.SSOSettingsService.Upsert(c.Req.Context(), &settings, c.SignedInUser)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update provider settings", err)
}

View File

@ -132,19 +132,21 @@ func TestSSOSettingsAPI_Update(t *testing.T) {
Settings: input.Settings,
}
signedInUser := &user.SignedInUser{
OrgRole: org.RoleAdmin,
OrgID: 1,
Permissions: getPermissionsForActionAndScope(tt.action, tt.scope),
}
service := ssosettingstests.NewMockService(t)
if tt.expectedServiceCall {
service.On("Upsert", mock.Anything, &settings).Return(tt.expectedError).Once()
service.On("Upsert", mock.Anything, &settings, signedInUser).Return(tt.expectedError).Once()
}
server := setupTests(t, service)
path := fmt.Sprintf("/api/v1/sso-settings/%s", tt.key)
req := server.NewRequest(http.MethodPut, path, bytes.NewBufferString(tt.body))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgRole: org.RoleEditor,
OrgID: 1,
Permissions: getPermissionsForActionAndScope(tt.action, tt.scope),
})
webtest.RequestWithSignedInUser(req, signedInUser)
res, err := server.SendJSON(req)
require.NoError(t, err)

View File

@ -4,6 +4,7 @@ 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"
)
@ -28,7 +29,7 @@ type Service interface {
// GetForProviderWithRedactedSecrets returns the SSO settings for a given provider (DB or config file) with secret values redacted
GetForProviderWithRedactedSecrets(ctx context.Context, provider string) (*models.SSOSettings, error)
// Upsert creates or updates the SSO settings for a given provider
Upsert(ctx context.Context, settings *models.SSOSettings) error
Upsert(ctx context.Context, settings *models.SSOSettings, requester identity.Requester) error
// 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
@ -44,7 +45,7 @@ type Service interface {
//go:generate mockery --name Reloadable --structname MockReloadable --outpkg ssosettingstests --filename reloadable_mock.go --output ./ssosettingstests/
type Reloadable interface {
Reload(ctx context.Context, settings models.SSOSettings) error
Validate(ctx context.Context, settings models.SSOSettings) error
Validate(ctx context.Context, settings models.SSOSettings, requester identity.Requester) error
}
// FallbackStrategy is an interface that can be implemented to allow a provider to load settings from a different source

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/usagestats"
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"
@ -164,7 +165,7 @@ func (s *Service) ListWithRedactedSecrets(ctx context.Context) ([]*models.SSOSet
return configurableSettings, nil
}
func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings) error {
func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings, requester identity.Requester) error {
if !s.isProviderConfigurable(settings.Provider) {
return ssosettings.ErrNotConfigurable
}
@ -174,7 +175,7 @@ func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings) erro
return ssosettings.ErrInvalidProvider.Errorf("provider %s not found in reloadables", settings.Provider)
}
err := social.Validate(ctx, *settings)
err := social.Validate(ctx, *settings, requester)
if err != nil {
return err
}

View File

@ -22,6 +22,7 @@ import (
"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"
)
@ -796,7 +797,7 @@ func TestService_Upsert(t *testing.T) {
wg.Add(1)
reloadable := ssosettingstests.NewMockReloadable(t)
reloadable.On("Validate", mock.Anything, settings).Return(nil)
reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil)
reloadable.On("Reload", mock.Anything, mock.MatchedBy(func(settings models.SSOSettings) bool {
defer wg.Done()
return settings.Provider == provider &&
@ -830,7 +831,7 @@ func TestService_Upsert(t *testing.T) {
},
}, nil
}
err := env.service.Upsert(context.Background(), &settings)
err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{})
require.NoError(t, err)
// Wait for the goroutine first to assert the Reload call
@ -859,7 +860,7 @@ func TestService_Upsert(t *testing.T) {
reloadable := ssosettingstests.NewMockReloadable(t)
env.reloadables[provider] = reloadable
err := env.service.Upsert(context.Background(), settings)
err := env.service.Upsert(context.Background(), settings, &user.SignedInUser{})
require.Error(t, err)
})
@ -883,7 +884,7 @@ func TestService_Upsert(t *testing.T) {
// the reloadable is available for other provider
env.reloadables["github"] = reloadable
err := env.service.Upsert(context.Background(), settings)
err := env.service.Upsert(context.Background(), settings, &user.SignedInUser{})
require.Error(t, err)
})
@ -904,10 +905,10 @@ func TestService_Upsert(t *testing.T) {
}
reloadable := ssosettingstests.NewMockReloadable(t)
reloadable.On("Validate", mock.Anything, settings).Return(errors.New("validation failed"))
reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(errors.New("validation failed"))
env.reloadables[provider] = reloadable
err := env.service.Upsert(context.Background(), &settings)
err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{})
require.Error(t, err)
})
@ -928,7 +929,7 @@ func TestService_Upsert(t *testing.T) {
env.fallbackStrategy.ExpectedIsMatch = false
err := env.service.Upsert(context.Background(), settings)
err := env.service.Upsert(context.Background(), settings, &user.SignedInUser{})
require.Error(t, err)
})
@ -949,11 +950,11 @@ func TestService_Upsert(t *testing.T) {
}
reloadable := ssosettingstests.NewMockReloadable(t)
reloadable.On("Validate", mock.Anything, settings).Return(nil)
reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil)
env.reloadables[provider] = reloadable
env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return(nil, errors.New("encryption failed")).Once()
err := env.service.Upsert(context.Background(), &settings)
err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{})
require.Error(t, err)
})
@ -981,13 +982,13 @@ func TestService_Upsert(t *testing.T) {
}
reloadable := ssosettingstests.NewMockReloadable(t)
reloadable.On("Validate", mock.Anything, settings).Return(nil)
reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil)
reloadable.On("Reload", mock.Anything, mock.Anything).Return(nil).Maybe()
env.reloadables[provider] = reloadable
env.secrets.On("Decrypt", mock.Anything, []byte("current-client-secret"), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once()
env.secrets.On("Encrypt", mock.Anything, []byte("encrypted-client-secret"), mock.Anything).Return([]byte("current-client-secret"), nil).Once()
err := env.service.Upsert(context.Background(), &settings)
err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{})
require.NoError(t, err)
settings.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("current-client-secret"))
@ -1011,7 +1012,7 @@ func TestService_Upsert(t *testing.T) {
}
reloadable := ssosettingstests.NewMockReloadable(t)
reloadable.On("Validate", mock.Anything, settings).Return(nil)
reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil)
env.reloadables[provider] = reloadable
env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once()
env.store.GetFn = func(ctx context.Context, provider string) (*models.SSOSettings, error) {
@ -1022,7 +1023,7 @@ func TestService_Upsert(t *testing.T) {
return errors.New("failed to upsert settings")
}
err := env.service.Upsert(context.Background(), &settings)
err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{})
require.Error(t, err)
})
@ -1043,12 +1044,12 @@ func TestService_Upsert(t *testing.T) {
}
reloadable := ssosettingstests.NewMockReloadable(t)
reloadable.On("Validate", mock.Anything, settings).Return(nil)
reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil)
reloadable.On("Reload", mock.Anything, mock.Anything).Return(errors.New("failed reloading new settings")).Maybe()
env.reloadables[provider] = reloadable
env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once()
err := env.service.Upsert(context.Background(), &settings)
err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{})
require.NoError(t, err)
settings.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("encrypted-client-secret"))

View File

@ -5,8 +5,10 @@ package ssosettingstests
import (
context "context"
models "github.com/grafana/grafana/pkg/services/ssosettings/models"
identity "github.com/grafana/grafana/pkg/services/auth/identity"
mock "github.com/stretchr/testify/mock"
models "github.com/grafana/grafana/pkg/services/ssosettings/models"
)
// MockReloadable is an autogenerated mock type for the Reloadable type
@ -32,17 +34,17 @@ func (_m *MockReloadable) Reload(ctx context.Context, settings models.SSOSetting
return r0
}
// Validate provides a mock function with given fields: ctx, settings
func (_m *MockReloadable) Validate(ctx context.Context, settings models.SSOSettings) error {
ret := _m.Called(ctx, settings)
// Validate provides a mock function with given fields: ctx, settings, requester
func (_m *MockReloadable) Validate(ctx context.Context, settings models.SSOSettings, requester identity.Requester) error {
ret := _m.Called(ctx, settings, requester)
if len(ret) == 0 {
panic("no return value specified for Validate")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, models.SSOSettings) error); ok {
r0 = rf(ctx, settings)
if rf, ok := ret.Get(0).(func(context.Context, models.SSOSettings, identity.Requester) error); ok {
r0 = rf(ctx, settings, requester)
} else {
r0 = ret.Error(0)
}

View File

@ -5,9 +5,11 @@ package ssosettingstests
import (
context "context"
models "github.com/grafana/grafana/pkg/services/ssosettings/models"
identity "github.com/grafana/grafana/pkg/services/auth/identity"
mock "github.com/stretchr/testify/mock"
models "github.com/grafana/grafana/pkg/services/ssosettings/models"
ssosettings "github.com/grafana/grafana/pkg/services/ssosettings"
)
@ -182,17 +184,17 @@ func (_m *MockService) Reload(ctx context.Context, provider string) {
_m.Called(ctx, provider)
}
// Upsert provides a mock function with given fields: ctx, settings
func (_m *MockService) Upsert(ctx context.Context, settings *models.SSOSettings) error {
ret := _m.Called(ctx, settings)
// Upsert provides a mock function with given fields: ctx, settings, requester
func (_m *MockService) Upsert(ctx context.Context, settings *models.SSOSettings, requester identity.Requester) error {
ret := _m.Called(ctx, settings, requester)
if len(ret) == 0 {
panic("no return value specified for Upsert")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *models.SSOSettings) error); ok {
r0 = rf(ctx, settings)
if rf, ok := ret.Get(0).(func(context.Context, *models.SSOSettings, identity.Requester) error); ok {
r0 = rf(ctx, settings, requester)
} else {
r0 = ret.Error(0)
}

View File

@ -47,6 +47,10 @@ export const FieldRenderer = ({
return null;
}
if (!!fieldData.hidden) {
return null;
}
// Dependant field means the field depends on another field's value and shouldn't be rendered if the parent field is false
if (isDependantField) {
const parentValue = watch(field.dependsOn);
@ -61,7 +65,7 @@ export const FieldRenderer = ({
error: fieldData.validation?.message,
key: name,
description: fieldData.description,
defaultValue: fieldData.defaultValue,
defaultValue: fieldData.defaultValue?.value,
};
switch (fieldData.type) {

View File

@ -2,6 +2,7 @@ import React from 'react';
import { validate as uuidValidate } from 'uuid';
import { TextLink } from '@grafana/ui';
import { contextSrv } from 'app/core/core';
import { FieldData, SSOProvider, SSOSettingsField } from './types';
import { isSelectableValue } from './utils/guards';
@ -151,7 +152,7 @@ export function fieldMap(provider: string): Record<string, FieldData> {
{ value: 'InParams', label: 'InParams' },
{ value: 'InHeader', label: 'InHeader' },
],
defaultValue: 'AutoDetect',
defaultValue: { value: 'AutoDetect', label: 'AutoDetect' },
},
tokenUrl: {
label: 'Token URL',
@ -280,6 +281,7 @@ export function fieldMap(provider: string): Record<string, FieldData> {
label: 'Allow assign Grafana admin',
description: 'If enabled, it will automatically sync the Grafana server administrator role.',
type: 'switch',
hidden: !contextSrv.isGrafanaAdmin,
},
skipOrgRoleSync: {
label: 'Skip organization role sync',

View File

@ -113,7 +113,8 @@ export type FieldData = {
allowCustomValue?: boolean;
options?: Array<SelectableValue<string>>;
placeholder?: string;
defaultValue?: string;
defaultValue?: SelectableValue<string>;
hidden?: boolean;
};
export type SSOSettingsField =