Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth: add the missing fields for all SSO providers #83813

Merged
merged 14 commits into from Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add the validate_hd setting? #83229

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the new field validate_hd. Can you please check the description I added to it? https://github.com/grafana/grafana/blob/dmihai/sso-config-pages/public/app/features/auth-config/fields.tsx#L595-L600

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