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:
parent
d5b21f77aa
commit
b174c1310a
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -57,9 +57,18 @@ const testConfig: SSOProvider = {
|
||||
allowedDomains: '',
|
||||
allowedGroups: '',
|
||||
scopes: '',
|
||||
orgMapping: '',
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('app/core/core', () => {
|
||||
return {
|
||||
contextSrv: {
|
||||
isGrafanaAdmin: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const emptyConfig = {
|
||||
...testConfig,
|
||||
settings: { ...testConfig.settings, enabled: false, clientId: '', clientSecret: '' },
|
||||
@ -120,6 +129,8 @@ describe('ProviderConfigForm', () => {
|
||||
await user.click(screen.getByText('User mapping'));
|
||||
await user.type(screen.getByRole('textbox', { name: /Role attribute path/i }), 'new-attribute-path');
|
||||
await user.click(screen.getByRole('checkbox', { name: /Role attribute strict mode/i }));
|
||||
await user.type(screen.getByRole('combobox', { name: /Organization mapping/i }), 'Group A:1:Editor{enter}');
|
||||
await user.type(screen.getByRole('combobox', { name: /Organization mapping/i }), 'Group B:2:Admin{enter}');
|
||||
|
||||
await user.click(screen.getByText('Extra security measures'));
|
||||
await user.type(screen.getByRole('combobox', { name: /Allowed domains/i }), 'grafana.com{enter}');
|
||||
@ -143,6 +154,7 @@ describe('ProviderConfigForm', () => {
|
||||
clientSecret: 'test-client-secret',
|
||||
enabled: true,
|
||||
name: 'GitHub',
|
||||
orgMapping: '["Group A:1:Editor","Group B:2:Admin"]',
|
||||
roleAttributePath: 'new-attribute-path',
|
||||
roleAttributeStrict: true,
|
||||
scopes: 'user:email',
|
||||
@ -203,6 +215,7 @@ describe('ProviderConfigForm', () => {
|
||||
tlsClientKey: '',
|
||||
usePkce: false,
|
||||
useRefreshToken: false,
|
||||
orgMapping: '',
|
||||
},
|
||||
},
|
||||
{ showErrorAlert: false }
|
||||
|
@ -38,7 +38,7 @@ export const sectionFields: Section = {
|
||||
{
|
||||
name: 'User mapping',
|
||||
id: 'user',
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
},
|
||||
{
|
||||
name: 'Extra security measures',
|
||||
@ -86,6 +86,8 @@ export const sectionFields: Section = {
|
||||
'idTokenAttributeName',
|
||||
'roleAttributePath',
|
||||
'roleAttributeStrict',
|
||||
'orgMapping',
|
||||
'orgAttributePath',
|
||||
'allowAssignGrafanaAdmin',
|
||||
'skipOrgRoleSync',
|
||||
],
|
||||
@ -121,7 +123,7 @@ export const sectionFields: Section = {
|
||||
{
|
||||
name: 'User mapping',
|
||||
id: 'user',
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
},
|
||||
{
|
||||
name: 'Extra security measures',
|
||||
@ -149,7 +151,7 @@ export const sectionFields: Section = {
|
||||
{
|
||||
name: 'User mapping',
|
||||
id: 'user',
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
},
|
||||
{
|
||||
name: 'Extra security measures',
|
||||
@ -176,7 +178,7 @@ export const sectionFields: Section = {
|
||||
{
|
||||
name: 'User mapping',
|
||||
id: 'user',
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'orgMapping', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
},
|
||||
{
|
||||
name: 'Extra security measures',
|
||||
@ -213,7 +215,14 @@ export const sectionFields: Section = {
|
||||
{
|
||||
name: 'User mapping',
|
||||
id: 'user',
|
||||
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
|
||||
fields: [
|
||||
'roleAttributePath',
|
||||
'roleAttributeStrict',
|
||||
'orgMapping',
|
||||
'orgAttributePath',
|
||||
'allowAssignGrafanaAdmin',
|
||||
'skipOrgRoleSync',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Extra security measures',
|
||||
@ -448,6 +457,22 @@ export function fieldMap(provider: string): Record<string, FieldData> {
|
||||
description: 'Prevent synchronizing users’ organization roles from your IdP.',
|
||||
type: 'switch',
|
||||
},
|
||||
orgMapping: {
|
||||
label: 'Organization mapping',
|
||||
description: orgMappingDescription(provider),
|
||||
type: 'select',
|
||||
hidden: !contextSrv.isGrafanaAdmin,
|
||||
multi: true,
|
||||
allowCustomValue: true,
|
||||
options: [],
|
||||
placeholder: 'Enter mappings (my-team:1:Viewer...) and press Enter to add',
|
||||
},
|
||||
orgAttributePath: {
|
||||
label: 'Organization attribute path',
|
||||
description: 'JMESPath expression to use for organization lookup.',
|
||||
type: 'text',
|
||||
hidden: !['generic_oauth', 'okta'].includes(provider),
|
||||
},
|
||||
defineAllowedGroups: {
|
||||
label: 'Define allowed groups',
|
||||
type: 'switch',
|
||||
@ -602,3 +627,19 @@ export function fieldMap(provider: string): Record<string, FieldData> {
|
||||
function isNumeric(value: string) {
|
||||
return /^-?\d+$/.test(value);
|
||||
}
|
||||
|
||||
function orgMappingDescription(provider: string): string {
|
||||
switch (provider) {
|
||||
case 'azuread':
|
||||
return 'List of "<GroupID>:<OrgIdOrName>:<Role>" mappings.';
|
||||
case 'github':
|
||||
return 'List of "<GitHubTeamName>:<OrgIdOrName>:<Role>" mappings.';
|
||||
case 'gitlab':
|
||||
return 'List of "<GitlabGroupName>:<OrgIdOrName>:<Role>';
|
||||
case 'google':
|
||||
return 'List of "<GoogleGroupName>:<OrgIdOrName>:<Role>';
|
||||
default:
|
||||
// Generic OAuth, Okta
|
||||
return 'List of "<ExternalName>:<OrgIdOrName>:<Role>" mappings.';
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ export type SSOProviderSettingsBase = {
|
||||
roleAttributeStrict?: boolean;
|
||||
signoutRedirectUrl?: string;
|
||||
skipOrgRoleSync?: boolean;
|
||||
orgAttributePath?: string;
|
||||
teamIdsAttributePath?: string;
|
||||
teamsUrl?: string;
|
||||
tlsClientCa?: string;
|
||||
@ -70,6 +71,7 @@ export type SSOProvider = {
|
||||
allowedDomains?: string;
|
||||
allowedGroups?: string;
|
||||
scopes?: string;
|
||||
orgMapping?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@ -80,6 +82,7 @@ export type SSOProviderDTO = Partial<SSOProviderSettingsBase> & {
|
||||
allowedDomains?: Array<SelectableValue<string>>;
|
||||
allowedGroups?: Array<SelectableValue<string>>;
|
||||
scopes?: Array<SelectableValue<string>>;
|
||||
orgMapping?: Array<SelectableValue<string>>;
|
||||
};
|
||||
|
||||
export interface AuthConfigState {
|
||||
|
@ -51,6 +51,11 @@ const strToValue = (val: string | string[]): SelectableValue[] => {
|
||||
if (Array.isArray(val)) {
|
||||
return val.map((v) => ({ label: v, value: v }));
|
||||
}
|
||||
// Stored as JSON Array
|
||||
if (val.startsWith('[') && val.endsWith(']')) {
|
||||
return JSON.parse(val).map((v: string) => ({ label: v, value: v }));
|
||||
}
|
||||
|
||||
return val.split(/[\s,]/).map((s) => ({ label: s, value: s }));
|
||||
};
|
||||
|
||||
@ -70,7 +75,11 @@ export function dataToDTO(data?: SSOProvider): SSOProviderDTO {
|
||||
}
|
||||
|
||||
const valuesToString = (values: Array<SelectableValue<string>>) => {
|
||||
return values.map(({ value }) => value).join(',');
|
||||
if (values.length <= 1) {
|
||||
return values.map(({ value }) => value).join(',');
|
||||
}
|
||||
// Store as JSON array if there are multiple values
|
||||
return JSON.stringify(values.map(({ value }) => value));
|
||||
};
|
||||
|
||||
const getFieldsForProvider = (provider: string) => {
|
||||
|
Loading…
Reference in New Issue
Block a user