mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user