Auth: Add organization mapping configuration to the UI (#90003)

* Add org_mapping and org_attribute_path to the UI

* Add validators, allow setting org mapping to only Grafana Admins

* comment

* Address feedback, improve validation, fix FE test, lint
This commit is contained in:
Misi
2024-07-04 16:00:56 +02:00
committed by GitHub
parent d5b21f77aa
commit b174c1310a
14 changed files with 313 additions and 44 deletions

View File

@@ -189,13 +189,18 @@ func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettin
return nil
}
func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSettings, _ ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
func (s *SocialAzureAD) Validate(ctx context.Context, newSettings ssoModels.SSOSettings, oldSettings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(newSettings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info, requester)
oldInfo, err := CreateOAuthInfoFromKeyValues(oldSettings.Settings)
if err != nil {
oldInfo = &social.OAuthInfo{}
}
err = validateInfo(info, oldInfo, requester)
if err != nil {
return err
}

View File

@@ -75,13 +75,18 @@ func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMa
return provider
}
func (s *SocialGenericOAuth) Validate(ctx context.Context, settings ssoModels.SSOSettings, _ ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
func (s *SocialGenericOAuth) Validate(ctx context.Context, newSettings ssoModels.SSOSettings, oldSettings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(newSettings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info, requester)
oldInfo, err := CreateOAuthInfoFromKeyValues(oldSettings.Settings)
if err != nil {
oldInfo = &social.OAuthInfo{}
}
err = validateInfo(info, oldInfo, requester)
if err != nil {
return err
}

View File

@@ -82,13 +82,18 @@ func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *
return provider
}
func (s *SocialGithub) Validate(ctx context.Context, settings ssoModels.SSOSettings, _ ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
func (s *SocialGithub) Validate(ctx context.Context, newSettings ssoModels.SSOSettings, oldSettings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(newSettings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info, requester)
oldInfo, err := CreateOAuthInfoFromKeyValues(oldSettings.Settings)
if err != nil {
oldInfo = &social.OAuthInfo{}
}
err = validateInfo(info, oldInfo, requester)
if err != nil {
return err
}

View File

@@ -64,13 +64,18 @@ func NewGitLabProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *
return provider
}
func (s *SocialGitlab) Validate(ctx context.Context, settings ssoModels.SSOSettings, _ ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
func (s *SocialGitlab) Validate(ctx context.Context, newSettings ssoModels.SSOSettings, oldSettings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(newSettings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info, requester)
oldInfo, err := CreateOAuthInfoFromKeyValues(oldSettings.Settings)
if err != nil {
oldInfo = &social.OAuthInfo{}
}
err = validateInfo(info, oldInfo, requester)
if err != nil {
return err
}

View File

@@ -65,13 +65,17 @@ func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *
return provider
}
func (s *SocialGoogle) Validate(ctx context.Context, settings ssoModels.SSOSettings, _ ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
func (s *SocialGoogle) Validate(ctx context.Context, newSettings ssoModels.SSOSettings, oldSettings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(newSettings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
oldInfo, err := CreateOAuthInfoFromKeyValues(oldSettings.Settings)
if err != nil {
oldInfo = &social.OAuthInfo{}
}
err = validateInfo(info, requester)
err = validateInfo(info, oldInfo, requester)
if err != nil {
return err
}

View File

@@ -54,13 +54,18 @@ func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapp
return provider
}
func (s *SocialGrafanaCom) Validate(ctx context.Context, settings ssoModels.SSOSettings, _ ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
func (s *SocialGrafanaCom) Validate(ctx context.Context, newSettings ssoModels.SSOSettings, oldSettings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(newSettings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info, requester)
oldInfo, err := CreateOAuthInfoFromKeyValues(oldSettings.Settings)
if err != nil {
oldInfo = &social.OAuthInfo{}
}
err = validateInfo(info, oldInfo, requester)
if err != nil {
return err
}

View File

@@ -61,13 +61,18 @@ func NewOktaProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *Or
return provider
}
func (s *SocialOkta) Validate(ctx context.Context, settings ssoModels.SSOSettings, _ ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(settings.Settings)
func (s *SocialOkta) Validate(ctx context.Context, newSettings ssoModels.SSOSettings, oldSettings ssoModels.SSOSettings, requester identity.Requester) error {
info, err := CreateOAuthInfoFromKeyValues(newSettings.Settings)
if err != nil {
return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err)
}
err = validateInfo(info, requester)
oldInfo, err := CreateOAuthInfoFromKeyValues(oldSettings.Settings)
if err != nil {
oldInfo = &social.OAuthInfo{}
}
err = validateInfo(info, oldInfo, requester)
if err != nil {
return err
}

View File

@@ -259,9 +259,11 @@ func getRoleFromSearch(role string) (org.RoleType, bool) {
return org.RoleType(cases.Title(language.Und).String(role)), false
}
func validateInfo(info *social.OAuthInfo, requester identity.Requester) error {
func validateInfo(info *social.OAuthInfo, oldInfo *social.OAuthInfo, requester identity.Requester) error {
return validation.Validate(info, requester,
validation.RequiredValidator(info.ClientId, "Client Id"),
validation.AllowAssignGrafanaAdminValidator,
validation.SkipOrgRoleSyncAllowAssignGrafanaAdminValidator)
validation.AllowAssignGrafanaAdminValidator(info, oldInfo, requester),
validation.SkipOrgRoleSyncAllowAssignGrafanaAdminValidator,
validation.OrgAttributePathValidator(info, oldInfo, requester),
validation.OrgMappingValidator(info, oldInfo, requester))
}

View File

@@ -3,6 +3,7 @@ package validation
import (
"fmt"
"net/url"
"slices"
"strings"
"github.com/grafana/grafana/pkg/apimachinery/identity"
@@ -10,11 +11,34 @@ import (
"github.com/grafana/grafana/pkg/services/ssosettings"
)
func AllowAssignGrafanaAdminValidator(info *social.OAuthInfo, requester identity.Requester) error {
if info.AllowAssignGrafanaAdmin && !requester.GetIsGrafanaAdmin() {
return ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin can only be updated by Grafana Server Admins.")
func AllowAssignGrafanaAdminValidator(info *social.OAuthInfo, oldInfo *social.OAuthInfo, requester identity.Requester) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
hasChanged := info.AllowAssignGrafanaAdmin != oldInfo.AllowAssignGrafanaAdmin
if hasChanged && !requester.GetIsGrafanaAdmin() {
return ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin can only be updated by Grafana Server Admins.")
}
return nil
}
}
func OrgMappingValidator(info *social.OAuthInfo, oldInfo *social.OAuthInfo, requester identity.Requester) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
hasChanged := !slices.Equal(oldInfo.OrgMapping, info.OrgMapping)
if hasChanged && !requester.GetIsGrafanaAdmin() {
return ssosettings.ErrInvalidOAuthConfig("Organization mapping can only be updated by Grafana Server Admins.")
}
return nil
}
}
func OrgAttributePathValidator(info *social.OAuthInfo, oldInfo *social.OAuthInfo, requester identity.Requester) ssosettings.ValidateFunc[social.OAuthInfo] {
return func(info *social.OAuthInfo, requester identity.Requester) error {
hasChanged := info.OrgAttributePath != oldInfo.OrgAttributePath
if hasChanged && !requester.GetIsGrafanaAdmin() {
return ssosettings.ErrInvalidOAuthConfig("Organization attribute path can only be updated by Grafana Server Admins.")
}
return nil
}
return nil
}
func SkipOrgRoleSyncAllowAssignGrafanaAdminValidator(info *social.OAuthInfo, requester identity.Requester) error {

View File

@@ -12,10 +12,11 @@ import (
)
type testCase struct {
name string
input *social.OAuthInfo
requester identity.Requester
wantErr error
name string
input *social.OAuthInfo
oldSettings *social.OAuthInfo
requester identity.Requester
wantErr error
}
func TestUrlValidator(t *testing.T) {
@@ -81,20 +82,39 @@ func TestRequiredValidator(t *testing.T) {
func TestAllowAssignGrafanaAdminValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when user is grafana admin and allow assign grafana admin is true",
name: "passes when user is Grafana Admin and Allow assign Grafana Admin was changed",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
},
oldSettings: &social.OAuthInfo{
AllowAssignGrafanaAdmin: false,
},
requester: &user.SignedInUser{
IsGrafanaAdmin: true,
},
wantErr: nil,
},
{
name: "fails when user is not grafana admin and allow assign grafana admin is true",
name: "passess when user is not Grafana Admin and Allow assign Grafana Admin was not changed",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
},
oldSettings: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
},
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
wantErr: nil,
},
{
name: "fails when user is not Grafana Admin and Allow assign Grafana Admin was changed",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
},
oldSettings: &social.OAuthInfo{
AllowAssignGrafanaAdmin: false,
},
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
@@ -104,7 +124,7 @@ func TestAllowAssignGrafanaAdminValidator(t *testing.T) {
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
err := AllowAssignGrafanaAdminValidator(tt.input, tt.requester)
err := AllowAssignGrafanaAdminValidator(tt.input, tt.oldSettings, tt.requester)(tt.input, tt.requester)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
@@ -117,7 +137,7 @@ func TestAllowAssignGrafanaAdminValidator(t *testing.T) {
func TestSkipOrgRoleSyncAllowAssignGrafanaAdminValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when allow assign grafana admin is set, but skip org role sync is not set",
name: "passes when allow assign Grafana Admin is set, but skip org role sync is not set",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
SkipOrgRoleSync: false,
@@ -125,7 +145,7 @@ func TestSkipOrgRoleSyncAllowAssignGrafanaAdminValidator(t *testing.T) {
wantErr: nil,
},
{
name: "passes when allow assign grafana admin is not set, but skip org role sync is set",
name: "passes when allow assign Grafana Admin is not set, but skip org role sync is set",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: false,
SkipOrgRoleSync: true,
@@ -133,7 +153,7 @@ func TestSkipOrgRoleSyncAllowAssignGrafanaAdminValidator(t *testing.T) {
wantErr: nil,
},
{
name: "fails when both allow assign grafana admin and skip org role sync is set",
name: "fails when both allow assign Grafana Admin and skip org role sync is set",
input: &social.OAuthInfo{
AllowAssignGrafanaAdmin: true,
SkipOrgRoleSync: true,
@@ -153,3 +173,126 @@ func TestSkipOrgRoleSyncAllowAssignGrafanaAdminValidator(t *testing.T) {
})
}
}
func TestOrgMappingValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when user is Grafana Admin and Org mapping was changed",
input: &social.OAuthInfo{
OrgMapping: []string{"group1:1:Viewer"},
},
oldSettings: &social.OAuthInfo{
OrgMapping: []string{"group1:2:Viewer"},
},
requester: &user.SignedInUser{
IsGrafanaAdmin: true,
},
wantErr: nil,
},
{
name: "passes when user is not Grafana Admin and Org mapping was not changed",
input: &social.OAuthInfo{
OrgMapping: []string{"group1:1:Viewer"},
},
oldSettings: &social.OAuthInfo{
OrgMapping: []string{"group1:1:Viewer"},
},
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
wantErr: nil,
},
{
name: "fails when user is not Grafana Admin and Org mapping was changed",
input: &social.OAuthInfo{
OrgMapping: []string{"group1:1:Viewer"},
},
oldSettings: &social.OAuthInfo{
OrgMapping: []string{},
},
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Organization mapping can only be updated by Grafana Server Admins."),
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
err := OrgMappingValidator(tt.input, tt.oldSettings, tt.requester)(tt.input, tt.requester)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}
func TestOrgAttributePathValidator(t *testing.T) {
tc := []testCase{
{
name: "passes when user is Grafana Admin and Org attribute path was changed",
input: &social.OAuthInfo{
OrgAttributePath: "path",
},
oldSettings: &social.OAuthInfo{
OrgAttributePath: "old-path",
},
requester: &user.SignedInUser{
IsGrafanaAdmin: true,
},
wantErr: nil,
},
{
name: "passes when user is Grafana Admin and Org attribute path was not changed",
input: &social.OAuthInfo{
OrgAttributePath: "path",
},
oldSettings: &social.OAuthInfo{
OrgAttributePath: "path",
},
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
wantErr: nil,
},
{
name: "fails when user is not Grafana Admin and Org attribute path casing was changed",
input: &social.OAuthInfo{
OrgAttributePath: "path",
},
oldSettings: &social.OAuthInfo{
OrgAttributePath: "Path",
},
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Organization attribute path can only be updated by Grafana Server Admins."),
},
{
name: "fails when user is not Grafana Admin and Org attribute path was changed",
input: &social.OAuthInfo{
OrgAttributePath: "path",
},
oldSettings: &social.OAuthInfo{
OrgAttributePath: "old-path",
},
requester: &user.SignedInUser{
IsGrafanaAdmin: false,
},
wantErr: ssosettings.ErrInvalidOAuthConfig("Organization attribute path can only be updated by Grafana Server Admins."),
},
}
for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
err := OrgAttributePathValidator(tt.input, tt.oldSettings, tt.requester)(tt.input, tt.requester)
if tt.wantErr != nil {
require.ErrorIs(t, err, tt.wantErr)
return
}
require.NoError(t, err)
})
}
}