Skip to content

Commit

Permalink
Auth: add the missing fields for all SSO providers (#83813)
Browse files Browse the repository at this point in the history
* add the missing fields for sso providers

* remove fields array

* add the validate_hd field for google

* submit only fields defined on the provider

* fix unit tests

* add unit tests for the new fields/sections from the form

* add hosted_domain field for the google provider

* reorder fields in user mapping

* remove authStyle field from gitlab and okta

---------

Co-authored-by: Mihaly Gyongyosi <mgyongyosi@users.noreply.github.com>
  • Loading branch information
dmihai and mgyongyosi committed Mar 19, 2024
1 parent c1c9ccb commit 6febfdf
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 107 deletions.
95 changes: 76 additions & 19 deletions public/app/features/auth-config/ProviderConfigForm.test.tsx
Expand Up @@ -77,24 +77,54 @@ describe('ProviderConfigForm', () => {
jest.clearAllMocks();
});

it('renders all fields correctly', async () => {
it('renders all general settings fields correctly', async () => {
setup(<ProviderConfigForm config={testConfig} provider={testConfig.provider} />);
expect(screen.getByRole('textbox', { name: /Client ID/i })).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: /Team IDs/i })).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: /Allowed organizations/i })).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: /Client secret/i })).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: /Scopes/i })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /Allow Sign Up/i })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /Auto login/i })).toBeInTheDocument();
expect(screen.getByRole('textbox', { name: /Sign out redirect URL/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Save/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /Discard/i })).toBeInTheDocument();
});

it('renders all user mapping fields correctly', async () => {
const { user } = setup(<ProviderConfigForm config={testConfig} provider={testConfig.provider} />);
await user.click(screen.getByText('User mapping'));
expect(screen.getByRole('textbox', { name: /Role attribute path/i })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /Role attribute strict mode/i })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /Skip organization role sync/i })).toBeInTheDocument();
});

it('renders all extra security fields correctly', async () => {
const { user } = setup(<ProviderConfigForm config={testConfig} provider={testConfig.provider} />);
await user.click(screen.getByText('Extra security measures'));
expect(screen.getByRole('combobox', { name: /Allowed organizations/i })).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: /Allowed domains/i })).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: /Team Ids/i })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /Use PKCE/i })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /Use refresh token/i })).toBeInTheDocument();
expect(screen.getByRole('checkbox', { name: /TLS skip verify/i })).toBeInTheDocument();
});

it('should save and enable on form submit', async () => {
const { user } = setup(<ProviderConfigForm config={emptyConfig} provider={emptyConfig.provider} />);

await user.type(screen.getByRole('textbox', { name: /Client ID/i }), 'test-client-id');
await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret');
// Type a team name and press enter to select it
await user.type(screen.getByRole('combobox', { name: /Team IDs/i }), '12324{enter}');
// Add two orgs
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org1{enter}');
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{enter}');
// Type a scope and press enter to select it
await user.type(screen.getByRole('combobox', { name: /Scopes/i }), 'user:email{enter}');
await user.click(screen.getByRole('checkbox', { name: /Auto login/i }));

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.click(screen.getByText('Extra security measures'));
await user.type(screen.getByRole('combobox', { name: /Allowed domains/i }), 'grafana.com{enter}');
await user.click(screen.getByRole('checkbox', { name: /Use PKCE/i }));

await user.click(screen.getByRole('button', { name: /Save and enable/i }));

await waitFor(() => {
Expand All @@ -104,12 +134,27 @@ describe('ProviderConfigForm', () => {
id: '300f9b7c-0488-40db-9763-a22ce8bf6b3e',
provider: 'github',
settings: {
name: 'GitHub',
allowedOrganizations: 'test-org1,test-org2',
allowAssignGrafanaAdmin: false,
allowSignUp: false,
allowedDomains: 'grafana.com',
allowedOrganizations: '',
autoLogin: true,
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
teamIds: '12324',
enabled: true,
name: 'GitHub',
roleAttributePath: 'new-attribute-path',
roleAttributeStrict: true,
scopes: 'user:email',
signoutRedirectUrl: '',
skipOrgRoleSync: false,
teamIds: '',
tlsClientCa: '',
tlsClientCert: '',
tlsClientKey: '',
tlsSkipVerifyInsecure: false,
usePkce: true,
useRefreshToken: false,
},
},
{ showErrorAlert: false }
Expand All @@ -126,11 +171,9 @@ describe('ProviderConfigForm', () => {
const { user } = setup(<ProviderConfigForm config={emptyConfig} provider={emptyConfig.provider} />);
await user.type(screen.getByRole('textbox', { name: /Client ID/i }), 'test-client-id');
await user.type(screen.getByLabelText(/Client secret/i), 'test-client-secret');
// Type a team name and press enter to select it
await user.type(screen.getByRole('combobox', { name: /Team IDs/i }), '12324{enter}');
// Add two orgs
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org1{enter}');
await user.type(screen.getByRole('combobox', { name: /Allowed organizations/i }), 'test-org2{enter}');
// Type a scope and press enter to select it
await user.type(screen.getByRole('combobox', { name: /Scopes/i }), 'user:email{enter}');
await user.click(screen.getByRole('checkbox', { name: /Auto login/i }));
await user.click(screen.getByText('Save'));

await waitFor(() => {
Expand All @@ -140,12 +183,26 @@ describe('ProviderConfigForm', () => {
id: '300f9b7c-0488-40db-9763-a22ce8bf6b3e',
provider: 'github',
settings: {
name: 'GitHub',
allowedOrganizations: 'test-org1,test-org2',
allowAssignGrafanaAdmin: false,
allowSignUp: false,
allowedDomains: '',
allowedOrganizations: '',
autoLogin: true,
clientId: 'test-client-id',
clientSecret: 'test-client-secret',
teamIds: '12324',
enabled: false,
name: 'GitHub',
roleAttributePath: '',
roleAttributeStrict: false,
scopes: 'user:email',
signoutRedirectUrl: '',
skipOrgRoleSync: false,
teamIds: '',
tlsClientCa: '',
tlsClientCert: '',
tlsClientKey: '',
usePkce: false,
useRefreshToken: false,
},
},
{ showErrorAlert: false }
Expand Down
74 changes: 26 additions & 48 deletions public/app/features/auth-config/ProviderConfigForm.tsx
Expand Up @@ -21,7 +21,7 @@ import { FormPrompt } from '../../core/components/FormPrompt/FormPrompt';
import { Page } from '../../core/components/Page/Page';

import { FieldRenderer } from './FieldRenderer';
import { fields, sectionFields } from './fields';
import { sectionFields } from './fields';
import { SSOProvider, SSOProviderDTO } from './types';
import { dataToDTO, dtoToData } from './utils/data';

Expand All @@ -45,7 +45,6 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf
formState: { errors, dirtyFields, isSubmitted },
} = useForm({ defaultValues: dataToDTO(config), mode: 'onSubmit', reValidateMode: 'onChange' });
const [isSaving, setIsSaving] = useState(false);
const providerFields = fields[provider];
const [submitError, setSubmitError] = useState(false);
const dataSubmitted = isSubmitted && !submitError;
const sections = sectionFields[provider];
Expand Down Expand Up @@ -155,55 +154,34 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf
<Field label="Enabled" hidden={true}>
<Switch {...register('enabled')} id="enabled" label={'Enabled'} />
</Field>
{sections ? (
<Stack gap={2} direction={'column'}>
{sections
.filter((section) => !section.hidden)
.map((section, index) => {
return (
<CollapsableSection label={section.name} isOpen={index === 0} key={section.name}>
{section.fields
.filter((field) => (typeof field !== 'string' ? !field.hidden : true))
.map((field) => {
return (
<FieldRenderer
key={typeof field === 'string' ? field : field.name}
field={field}
control={control}
errors={errors}
setValue={setValue}
register={register}
watch={watch}
unregister={unregister}
provider={provider}
secretConfigured={!!config?.settings.clientSecret}
/>
);
})}
</CollapsableSection>
);
})}
</Stack>
) : (
<>
{providerFields.map((field) => {
<Stack gap={2} direction={'column'}>
{sections
.filter((section) => !section.hidden)
.map((section, index) => {
return (
<FieldRenderer
key={field}
field={field}
control={control}
errors={errors}
setValue={setValue}
register={register}
watch={watch}
unregister={unregister}
provider={provider}
secretConfigured={!!config?.settings.clientSecret}
/>
<CollapsableSection label={section.name} isOpen={index === 0} key={section.name}>
{section.fields
.filter((field) => (typeof field !== 'string' ? !field.hidden : true))
.map((field) => {
return (
<FieldRenderer
key={typeof field === 'string' ? field : field.name}
field={field}
control={control}
errors={errors}
setValue={setValue}
register={register}
watch={watch}
unregister={unregister}
provider={provider}
secretConfigured={!!config?.settings.clientSecret}
/>
);
})}
</CollapsableSection>
);
})}
</>
)}
</Stack>
<Box display={'flex'} gap={2} marginTop={5}>
<Stack alignItems={'center'} gap={2}>
<Button
Expand Down
136 changes: 118 additions & 18 deletions public/app/features/auth-config/fields.tsx
Expand Up @@ -9,24 +9,6 @@ import { FieldData, SSOProvider, SSOSettingsField } from './types';
import { isSelectableValue } from './utils/guards';
import { isUrlValid } from './utils/url';

/** Map providers to their settings */
export const fields: Record<SSOProvider['provider'], Array<keyof SSOProvider['settings']>> = {
github: ['name', 'clientId', 'clientSecret', 'teamIds', 'allowedOrganizations'],
google: ['name', 'clientId', 'clientSecret', 'allowedDomains'],
gitlab: ['name', 'clientId', 'clientSecret', 'allowedOrganizations', 'teamIds'],
okta: [
'name',
'clientId',
'clientSecret',
'authUrl',
'tokenUrl',
'apiUrl',
'roleAttributePath',
'allowedGroups',
'allowedDomains',
],
};

type Section = Record<
SSOProvider['provider'],
Array<{
Expand Down Expand Up @@ -131,6 +113,113 @@ export const sectionFields: Section = {
],
},
],
google: [
{
name: 'General settings',
id: 'general',
fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'],
},
{
name: 'User mapping',
id: 'user',
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
},
{
name: 'Extra security measures',
id: 'extra',
fields: [
'validateHd',
'hostedDomain',
'allowedDomains',
'allowedGroups',
'usePkce',
'useRefreshToken',
'tlsSkipVerifyInsecure',
'tlsClientCert',
'tlsClientKey',
'tlsClientCa',
],
},
],
github: [
{
name: 'General settings',
id: 'general',
fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'],
},
{
name: 'User mapping',
id: 'user',
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
},
{
name: 'Extra security measures',
id: 'extra',
fields: [
'allowedOrganizations',
'allowedDomains',
'teamIds',
'usePkce',
'useRefreshToken',
'tlsSkipVerifyInsecure',
'tlsClientCert',
'tlsClientKey',
'tlsClientCa',
],
},
],
gitlab: [
{
name: 'General settings',
id: 'general',
fields: ['name', 'clientId', 'clientSecret', 'scopes', 'allowSignUp', 'autoLogin', 'signoutRedirectUrl'],
},
{
name: 'User mapping',
id: 'user',
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
},
{
name: 'Extra security measures',
id: 'extra',
fields: [
'allowedDomains',
'allowedGroups',
'usePkce',
'useRefreshToken',
'tlsSkipVerifyInsecure',
'tlsClientCert',
'tlsClientKey',
'tlsClientCa',
],
},
],
okta: [
{
name: 'General settings',
id: 'general',
fields: ['name', 'clientId', 'clientSecret', 'scopes', 'authUrl', 'tokenUrl', 'apiUrl', 'signoutRedirectUrl'],
},
{
name: 'User mapping',
id: 'user',
fields: ['roleAttributePath', 'roleAttributeStrict', 'allowAssignGrafanaAdmin', 'skipOrgRoleSync'],
},
{
name: 'Extra security measures',
id: 'extra',
fields: [
'allowedDomains',
'allowedGroups',
'usePkce',
'useRefreshToken',
'tlsSkipVerifyInsecure',
'tlsClientCert',
'tlsClientKey',
'tlsClientCa',
],
},
],
};

/**
Expand Down Expand Up @@ -485,6 +574,17 @@ export function fieldMap(provider: string): Record<string, FieldData> {
}
: undefined,
},
hostedDomain: {
label: 'Hosted domain',
description: 'The domain under which Grafana is hosted and accessible.',
type: 'text',
},
validateHd: {
label: 'Validate hosted domain',
description:
'If enabled, Grafana will match the Hosted Domain retrieved from the Google ID Token against the Allowed Domains list specified by the user.',
type: 'checkbox',
},
};
}

Expand Down

0 comments on commit 6febfdf

Please sign in to comment.