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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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)
})
}
}

View File

@ -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 }

View File

@ -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.';
}
}

View File

@ -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 {

View File

@ -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) => {