mirror of
https://github.com/ansible/awx.git
synced 2026-01-09 23:12:08 -03:30
rebasing
This commit is contained in:
parent
bcd018707a
commit
9f3396d867
@ -379,6 +379,7 @@ AUTHENTICATION_BACKENDS = (
|
||||
'social_core.backends.github_enterprise.GithubEnterpriseOAuth2',
|
||||
'social_core.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2',
|
||||
'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2',
|
||||
'social_core.backends.open_id_connect.OpenIdConnectAuth',
|
||||
'social_core.backends.azuread.AzureADOAuth2',
|
||||
'awx.sso.backends.SAMLAuth',
|
||||
'awx.main.backends.AWXModelBackend',
|
||||
|
||||
@ -1215,6 +1215,54 @@ register(
|
||||
placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER,
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# Generic OIDC AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_OIDC_KEY',
|
||||
field_class=fields.CharField,
|
||||
allow_null=False,
|
||||
default=None,
|
||||
label=_('OIDC Key'),
|
||||
help_text='The OIDC key (Client ID) from your IDP.',
|
||||
category=_('Generic OIDC'),
|
||||
category_slug='oidc',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_OIDC_SECRET',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('OIDC Secret'),
|
||||
help_text=_('The OIDC secret (Client Secret) from your IDP.'),
|
||||
category=_('Generic OIDC'),
|
||||
category_slug='oidc',
|
||||
encrypted=True,
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT',
|
||||
field_class=fields.CharField,
|
||||
allow_blank=True,
|
||||
default='',
|
||||
label=_('OIDC Provider URL'),
|
||||
help_text=_('The URL for your OIDC provider including the path up to /.well-known/openid-configuration'),
|
||||
category=_('Generic OIDC'),
|
||||
category_slug='oidc',
|
||||
)
|
||||
|
||||
register(
|
||||
'SOCIAL_AUTH_OIDC_VERIFY_SSL',
|
||||
field_class=fields.BooleanField,
|
||||
default=True,
|
||||
label=_('Verify OIDC Provider Certificate'),
|
||||
help_text=_('Verify the OIDV provider ssl certificate.'),
|
||||
category=_('Generic OIDC'),
|
||||
category_slug='oidc',
|
||||
)
|
||||
|
||||
###############################################################################
|
||||
# SAML AUTHENTICATION SETTINGS
|
||||
###############################################################################
|
||||
|
||||
@ -149,6 +149,7 @@ class AuthenticationBackendsField(fields.StringListField):
|
||||
('awx.sso.backends.RADIUSBackend', ['RADIUS_SERVER']),
|
||||
('social_core.backends.google.GoogleOAuth2', ['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET']),
|
||||
('social_core.backends.github.GithubOAuth2', ['SOCIAL_AUTH_GITHUB_KEY', 'SOCIAL_AUTH_GITHUB_SECRET']),
|
||||
('social_core.backends.open_id_connect.OpenIdConnectAuth', ['SOCIAL_AUTH_OIDC_KEY', 'SOCIAL_AUTH_OIDC_SECRET', 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT']),
|
||||
(
|
||||
'social_core.backends.github.GithubOrganizationOAuth2',
|
||||
['SOCIAL_AUTH_GITHUB_ORG_KEY', 'SOCIAL_AUTH_GITHUB_ORG_SECRET', 'SOCIAL_AUTH_GITHUB_ORG_NAME'],
|
||||
|
||||
@ -346,6 +346,20 @@ function AWXLogin({ alt, isAuthenticated }) {
|
||||
</LoginMainFooterLinksItem>
|
||||
);
|
||||
}
|
||||
if (authKey === 'oidc') {
|
||||
return (
|
||||
<LoginMainFooterLinksItem
|
||||
data-cy="social-auth-oidc"
|
||||
href={loginUrl}
|
||||
key={authKey}
|
||||
onClick={setSessionRedirect}
|
||||
>
|
||||
<Tooltip content={t`Sign in with OIDC`}>
|
||||
<UserCircleIcon size="lg" />
|
||||
</Tooltip>
|
||||
</LoginMainFooterLinksItem>
|
||||
);
|
||||
}
|
||||
if (authKey.startsWith('saml')) {
|
||||
const samlIDP = authKey.split(':')[1] || null;
|
||||
return (
|
||||
|
||||
34
awx/ui/src/screens/Setting/OIDC/OIDC.js
Normal file
34
awx/ui/src/screens/Setting/OIDC/OIDC.js
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Link, Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { PageSection, Card } from '@patternfly/react-core';
|
||||
import ContentError from 'components/ContentError';
|
||||
import OIDCDetail from './OIDCDetail';
|
||||
import OIDCEdit from './OIDCEdit';
|
||||
|
||||
function OIDC() {
|
||||
const baseURL = '/settings/oidc';
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<Switch>
|
||||
<Redirect from={baseURL} to={`${baseURL}/details`} exact />
|
||||
<Route path={`${baseURL}/details`}>
|
||||
<OIDCDetail />
|
||||
</Route>
|
||||
<Route path={`${baseURL}/edit`}>
|
||||
<OIDCEdit />
|
||||
</Route>
|
||||
<Route key="not-found" path={`${baseURL}/*`}>
|
||||
<ContentError isNotFound>
|
||||
<Link to={`${baseURL}/details`}>{t`View OIDC settings`}</Link>
|
||||
</ContentError>
|
||||
</Route>
|
||||
</Switch>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default OIDC;
|
||||
75
awx/ui/src/screens/Setting/OIDC/OIDC.test.js
Normal file
75
awx/ui/src/screens/Setting/OIDC/OIDC.test.js
Normal file
@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { SettingsProvider } from 'contexts/Settings';
|
||||
import { SettingsAPI } from 'api';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import mockAllOptions from '../shared/data.allSettingOptions.json';
|
||||
import OIDC from './OIDC';
|
||||
|
||||
jest.mock('../../../api');
|
||||
|
||||
describe('<OIDC />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
SettingsAPI.readCategory.mockResolvedValue({
|
||||
data: {
|
||||
SOCIAL_AUTH_OIDC_KEY: 'mock key',
|
||||
SOCIAL_AUTH_OIDC_SECRET: '$encrypted$',
|
||||
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: 'https://example.com',
|
||||
SOCIAL_AUTH_OIDC_VERIFY_SSL: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render OIDC details', async () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/settings/oidc/details'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SettingsProvider value={mockAllOptions.actions}>
|
||||
<OIDC />
|
||||
</SettingsProvider>,
|
||||
{
|
||||
context: { router: { history } },
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('OIDCDetail').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should render OIDC edit', async () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/settings/oidc/edit'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SettingsProvider value={mockAllOptions.actions}>
|
||||
<OIDC />
|
||||
</SettingsProvider>,
|
||||
{
|
||||
context: { router: { history } },
|
||||
}
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('OIDCEdit').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should show content error when user navigates to erroneous route', async () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/settings/oidc/foo'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<OIDC />, {
|
||||
context: { router: { history } },
|
||||
});
|
||||
});
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
});
|
||||
98
awx/ui/src/screens/Setting/OIDC/OIDCDetail/OIDCDetail.js
Normal file
98
awx/ui/src/screens/Setting/OIDC/OIDCDetail/OIDCDetail.js
Normal file
@ -0,0 +1,98 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { Button } from '@patternfly/react-core';
|
||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
import { CardBody, CardActionsRow } from 'components/Card';
|
||||
import ContentLoading from 'components/ContentLoading';
|
||||
import ContentError from 'components/ContentError';
|
||||
import RoutedTabs from 'components/RoutedTabs';
|
||||
import { SettingsAPI } from 'api';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
import { DetailList } from 'components/DetailList';
|
||||
import { useConfig } from 'contexts/Config';
|
||||
import { useSettings } from 'contexts/Settings';
|
||||
import { SettingDetail } from '../../shared';
|
||||
|
||||
function OIDCDetail() {
|
||||
const { me } = useConfig();
|
||||
const { GET: options } = useSettings();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
request,
|
||||
result: OIDC,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await SettingsAPI.readCategory('oidc');
|
||||
return data;
|
||||
}, []),
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
request();
|
||||
}, [request]);
|
||||
|
||||
const tabsArray = [
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<CaretLeftIcon />
|
||||
{t`Back to Settings`}
|
||||
</>
|
||||
),
|
||||
link: `/settings`,
|
||||
id: 99,
|
||||
},
|
||||
{
|
||||
name: t`Details`,
|
||||
link: `/settings/oidc/details`,
|
||||
id: 0,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<RoutedTabs tabsArray={tabsArray} />
|
||||
<CardBody>
|
||||
{isLoading && <ContentLoading />}
|
||||
{!isLoading && error && <ContentError error={error} />}
|
||||
{!isLoading && OIDC && (
|
||||
<DetailList>
|
||||
{Object.keys(OIDC).map((key) => {
|
||||
const record = options?.[key];
|
||||
return (
|
||||
<SettingDetail
|
||||
key={key}
|
||||
id={key}
|
||||
helpText={record?.help_text}
|
||||
label={record?.label}
|
||||
type={record?.type}
|
||||
unit={record?.unit}
|
||||
value={OIDC?.[key]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</DetailList>
|
||||
)}
|
||||
{me?.is_superuser && (
|
||||
<CardActionsRow>
|
||||
<Button
|
||||
ouiaId="oidc-detail-edit-button"
|
||||
aria-label={t`Edit`}
|
||||
component={Link}
|
||||
to="/settings/oidc/edit"
|
||||
>
|
||||
{t`Edit`}
|
||||
</Button>
|
||||
</CardActionsRow>
|
||||
)}
|
||||
</CardBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default OIDCDetail;
|
||||
@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { SettingsProvider } from 'contexts/Settings';
|
||||
import { SettingsAPI } from 'api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../../testUtils/enzymeHelpers';
|
||||
import {
|
||||
assertDetail,
|
||||
assertVariableDetail,
|
||||
} from '../../shared/settingTestUtils';
|
||||
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||
import OIDCDetail from './OIDCDetail';
|
||||
|
||||
jest.mock('../../../../api');
|
||||
|
||||
describe('<OIDCDetail />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
SettingsAPI.readCategory.mockResolvedValue({
|
||||
data: {
|
||||
SOCIAL_AUTH_OIDC_KEY: 'mock key',
|
||||
SOCIAL_AUTH_OIDC_SECRET: '$encrypted$',
|
||||
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: 'https://example.com',
|
||||
SOCIAL_AUTH_OIDC_VERIFY_SSL: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SettingsProvider value={mockAllOptions.actions}>
|
||||
<OIDCDetail />
|
||||
</SettingsProvider>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders without crashing', () => {
|
||||
expect(wrapper.find('OIDCDetail').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should render expected tabs', () => {
|
||||
const expectedTabs = ['Back to Settings', 'Details'];
|
||||
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should render expected details', () => {
|
||||
assertDetail(wrapper, 'OIDC Key', 'mock key');
|
||||
assertDetail(wrapper, 'OIDC Secret', 'Encrypted');
|
||||
assertDetail(wrapper, 'OIDC Provider URL', 'https://example.com');
|
||||
assertDetail(wrapper, 'Verify OIDC Provider Certificate', 'On');
|
||||
});
|
||||
|
||||
test('should hide edit button from non-superusers', async () => {
|
||||
const config = {
|
||||
me: {
|
||||
is_superuser: false,
|
||||
},
|
||||
};
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SettingsProvider value={mockAllOptions.actions}>
|
||||
<OIDCDetail />
|
||||
</SettingsProvider>,
|
||||
{
|
||||
context: { config },
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
expect(wrapper.find('Button[aria-label="Edit"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('should display content error when api throws error on initial render', async () => {
|
||||
SettingsAPI.readCategory.mockRejectedValue(new Error());
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SettingsProvider value={mockAllOptions.actions}>
|
||||
<OIDCDetail />
|
||||
</SettingsProvider>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
});
|
||||
1
awx/ui/src/screens/Setting/OIDC/OIDCDetail/index.js
Normal file
1
awx/ui/src/screens/Setting/OIDC/OIDCDetail/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './OIDCDetail';
|
||||
147
awx/ui/src/screens/Setting/OIDC/OIDCEdit/OIDCEdit.js
Normal file
147
awx/ui/src/screens/Setting/OIDC/OIDCEdit/OIDCEdit.js
Normal file
@ -0,0 +1,147 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Formik } from 'formik';
|
||||
import { Form } from '@patternfly/react-core';
|
||||
import { CardBody } from 'components/Card';
|
||||
import ContentError from 'components/ContentError';
|
||||
import ContentLoading from 'components/ContentLoading';
|
||||
import { FormSubmitError } from 'components/FormField';
|
||||
import { FormColumnLayout } from 'components/FormLayout';
|
||||
import { useSettings } from 'contexts/Settings';
|
||||
import useModal from 'hooks/useModal';
|
||||
import useRequest from 'hooks/useRequest';
|
||||
import { SettingsAPI } from 'api';
|
||||
import { RevertAllAlert, RevertFormActionGroup } from '../../shared';
|
||||
import {
|
||||
EncryptedField,
|
||||
InputField,
|
||||
BooleanField,
|
||||
} from '../../shared/SharedFields';
|
||||
|
||||
function OIDCEdit() {
|
||||
const history = useHistory();
|
||||
const { isModalOpen, toggleModal, closeModal } = useModal();
|
||||
const { PUT: options } = useSettings();
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
error,
|
||||
request: fetchOIDC,
|
||||
result: OIDC,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await SettingsAPI.readCategory('oidc');
|
||||
const mergedData = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (!options[key]) {
|
||||
return;
|
||||
}
|
||||
mergedData[key] = options[key];
|
||||
mergedData[key].value = data[key];
|
||||
});
|
||||
return mergedData;
|
||||
}, [options]),
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchOIDC();
|
||||
}, [fetchOIDC]);
|
||||
|
||||
const { error: submitError, request: submitForm } = useRequest(
|
||||
useCallback(
|
||||
async (values) => {
|
||||
await SettingsAPI.updateAll(values);
|
||||
history.push('/settings/oidc/details');
|
||||
},
|
||||
[history]
|
||||
),
|
||||
null
|
||||
);
|
||||
|
||||
const { error: revertError, request: revertAll } = useRequest(
|
||||
useCallback(async () => {
|
||||
await SettingsAPI.revertCategory('oidc');
|
||||
}, []),
|
||||
null
|
||||
);
|
||||
|
||||
const handleSubmit = async (form) => {
|
||||
await submitForm({
|
||||
...form,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRevertAll = async () => {
|
||||
await revertAll();
|
||||
|
||||
closeModal();
|
||||
|
||||
history.push('/settings/oidc/details');
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push('/settings/oidc/details');
|
||||
};
|
||||
|
||||
const initialValues = (fields) =>
|
||||
Object.keys(fields).reduce((acc, key) => {
|
||||
if (fields[key].type === 'list' || fields[key].type === 'nested object') {
|
||||
acc[key] = fields[key].value
|
||||
? JSON.stringify(fields[key].value, null, 2)
|
||||
: null;
|
||||
} else {
|
||||
acc[key] = fields[key].value ?? '';
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
{isLoading && <ContentLoading />}
|
||||
{!isLoading && error && <ContentError error={error} />}
|
||||
{!isLoading && OIDC && (
|
||||
<Formik initialValues={initialValues(OIDC)} onSubmit={handleSubmit}>
|
||||
{(formik) => (
|
||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
||||
<FormColumnLayout>
|
||||
<InputField
|
||||
name="SOCIAL_AUTH_OIDC_KEY"
|
||||
config={OIDC.SOCIAL_AUTH_OIDC_KEY}
|
||||
/>
|
||||
<EncryptedField
|
||||
name="SOCIAL_AUTH_OIDC_SECRET"
|
||||
config={OIDC.SOCIAL_AUTH_OIDC_SECRET}
|
||||
/>
|
||||
<InputField
|
||||
name="SOCIAL_AUTH_OIDC_OIDC_ENDPOINT"
|
||||
config={OIDC.SOCIAL_AUTH_OIDC_OIDC_ENDPOINT}
|
||||
type="url"
|
||||
/>
|
||||
<BooleanField
|
||||
name="SOCIAL_AUTH_OIDC_VERIFY_SSL"
|
||||
config={OIDC.SOCIAL_AUTH_OIDC_VERIFY_SSL}
|
||||
/>
|
||||
{submitError && <FormSubmitError error={submitError} />}
|
||||
{revertError && <FormSubmitError error={revertError} />}
|
||||
</FormColumnLayout>
|
||||
<RevertFormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
onRevert={toggleModal}
|
||||
/>
|
||||
{isModalOpen && (
|
||||
<RevertAllAlert
|
||||
onClose={closeModal}
|
||||
onRevertAll={handleRevertAll}
|
||||
/>
|
||||
)}
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
)}
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
export default OIDCEdit;
|
||||
161
awx/ui/src/screens/Setting/OIDC/OIDCEdit/OIDCEdit.test.js
Normal file
161
awx/ui/src/screens/Setting/OIDC/OIDCEdit/OIDCEdit.test.js
Normal file
@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { SettingsProvider } from 'contexts/Settings';
|
||||
import { SettingsAPI } from 'api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../../testUtils/enzymeHelpers';
|
||||
import mockAllOptions from '../../shared/data.allSettingOptions.json';
|
||||
import OIDCEdit from './OIDCEdit';
|
||||
|
||||
jest.mock('../../../../api');
|
||||
|
||||
describe('<OIDCEdit />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
|
||||
beforeEach(() => {
|
||||
SettingsAPI.revertCategory.mockResolvedValue({});
|
||||
SettingsAPI.updateAll.mockResolvedValue({});
|
||||
SettingsAPI.readCategory.mockResolvedValue({
|
||||
data: {
|
||||
SOCIAL_AUTH_OIDC_KEY: 'mock key',
|
||||
SOCIAL_AUTH_OIDC_SECRET: '$encrypted$',
|
||||
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: 'https://example.com',
|
||||
SOCIAL_AUTH_OIDC_VERIFY_SSL: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/settings/oidc/edit'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SettingsProvider value={mockAllOptions.actions}>
|
||||
<OIDCEdit />
|
||||
</SettingsProvider>,
|
||||
{
|
||||
context: { router: { history } },
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
});
|
||||
|
||||
test('initially renders without crashing', () => {
|
||||
expect(wrapper.find('OIDCEdit').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should display expected form fields', async () => {
|
||||
expect(wrapper.find('FormGroup[label="OIDC Key"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="OIDC Secret"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="OIDC Provider URL"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper.find('FormGroup[label="Verify OIDC Provider Certificate"]').length
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
test('should successfully send default values to api on form revert all', async () => {
|
||||
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(0);
|
||||
expect(wrapper.find('RevertAllAlert')).toHaveLength(0);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('button[aria-label="Revert all to default"]')
|
||||
.invoke('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('RevertAllAlert')).toHaveLength(1);
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('RevertAllAlert button[aria-label="Confirm revert all"]')
|
||||
.invoke('onClick')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(SettingsAPI.revertCategory).toHaveBeenCalledTimes(1);
|
||||
expect(SettingsAPI.revertCategory).toHaveBeenCalledWith('oidc');
|
||||
});
|
||||
|
||||
test('should successfully send request to api on form submission', async () => {
|
||||
act(() => {
|
||||
wrapper
|
||||
.find(
|
||||
'FormGroup[fieldId="SOCIAL_AUTH_OIDC_SECRET"] button[aria-label="Revert"]'
|
||||
)
|
||||
.invoke('onClick')();
|
||||
wrapper.find('input#SOCIAL_AUTH_OIDC_KEY').simulate('change', {
|
||||
target: { value: 'new key', name: 'SOCIAL_AUTH_OIDC_KEY' },
|
||||
});
|
||||
wrapper.find('input#SOCIAL_AUTH_OIDC_OIDC_ENDPOINT').simulate('change', {
|
||||
target: {
|
||||
value: 'https://example.com',
|
||||
name: 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT',
|
||||
},
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('Form').invoke('onSubmit')();
|
||||
});
|
||||
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||
expect(SettingsAPI.updateAll).toHaveBeenCalledWith({
|
||||
SOCIAL_AUTH_OIDC_KEY: 'new key',
|
||||
SOCIAL_AUTH_OIDC_SECRET: '',
|
||||
SOCIAL_AUTH_OIDC_OIDC_ENDPOINT: 'https://example.com',
|
||||
SOCIAL_AUTH_OIDC_VERIFY_SSL: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('should navigate to OIDC detail on successful submission', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('Form').invoke('onSubmit')();
|
||||
});
|
||||
expect(history.location.pathname).toEqual('/settings/oidc/details');
|
||||
});
|
||||
|
||||
test('should navigate to OIDC detail when cancel is clicked', async () => {
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||
});
|
||||
expect(history.location.pathname).toEqual('/settings/oidc/details');
|
||||
});
|
||||
|
||||
test('should display error message on unsuccessful submission', async () => {
|
||||
const error = {
|
||||
response: {
|
||||
data: { detail: 'An error occurred' },
|
||||
},
|
||||
};
|
||||
SettingsAPI.updateAll.mockImplementation(() => Promise.reject(error));
|
||||
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(0);
|
||||
await act(async () => {
|
||||
wrapper.find('Form').invoke('onSubmit')();
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||
expect(SettingsAPI.updateAll).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should display ContentError on throw', async () => {
|
||||
SettingsAPI.readCategory.mockImplementationOnce(() =>
|
||||
Promise.reject(new Error())
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SettingsProvider value={mockAllOptions.actions}>
|
||||
<OIDCEdit />
|
||||
</SettingsProvider>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
});
|
||||
1
awx/ui/src/screens/Setting/OIDC/OIDCEdit/index.js
Normal file
1
awx/ui/src/screens/Setting/OIDC/OIDCEdit/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './OIDCEdit';
|
||||
1
awx/ui/src/screens/Setting/OIDC/index.js
Normal file
1
awx/ui/src/screens/Setting/OIDC/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './OIDC';
|
||||
@ -81,6 +81,10 @@ function SettingList() {
|
||||
title: t`TACACS+ settings`,
|
||||
path: '/settings/tacacs',
|
||||
},
|
||||
{
|
||||
title: t`Generic OIDC settings`,
|
||||
path: '/settings/oidc',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@ -12,6 +12,7 @@ import useRequest from 'hooks/useRequest';
|
||||
import AzureAD from './AzureAD';
|
||||
import GitHub from './GitHub';
|
||||
import GoogleOAuth2 from './GoogleOAuth2';
|
||||
import OIDC from './OIDC';
|
||||
import Jobs from './Jobs';
|
||||
import LDAP from './LDAP';
|
||||
import Subscription from './Subscription';
|
||||
@ -68,6 +69,9 @@ function Settings() {
|
||||
'/settings/google_oauth2': t`Google OAuth2`,
|
||||
'/settings/google_oauth2/details': t`Details`,
|
||||
'/settings/google_oauth2/edit': t`Edit Details`,
|
||||
'/settings/oidc': t`Generic OIDC`,
|
||||
'/settings/oidc/details': t`Details`,
|
||||
'/settings/oidc/edit': t`Edit Details`,
|
||||
'/settings/jobs': t`Jobs`,
|
||||
'/settings/jobs/details': t`Details`,
|
||||
'/settings/jobs/edit': t`Edit Details`,
|
||||
@ -153,6 +157,9 @@ function Settings() {
|
||||
<Route path="/settings/google_oauth2">
|
||||
<GoogleOAuth2 />
|
||||
</Route>
|
||||
<Route path="/settings/oidc">
|
||||
<OIDC />
|
||||
</Route>
|
||||
<Route path="/settings/jobs">
|
||||
<Jobs />
|
||||
</Route>
|
||||
|
||||
@ -840,6 +840,39 @@
|
||||
"read_only": false
|
||||
}
|
||||
},
|
||||
"SOCIAL_AUTH_OIDC_KEY": {
|
||||
"type": "string",
|
||||
"label": "OIDC Key",
|
||||
"help_text": "The OIDC key (Client ID) from your IDP.",
|
||||
"category": "Generic OIDC",
|
||||
"category_slug": "oidc",
|
||||
"default": ""
|
||||
},
|
||||
"SOCIAL_AUTH_OIDC_SECRET": {
|
||||
"type": "string",
|
||||
"label": "OIDC Secret",
|
||||
"help_text": "The OIDC secret (Client Secret) from your IDP.",
|
||||
"category": "Generic OIDC",
|
||||
"category_slug": "oidc",
|
||||
"default": ""
|
||||
},
|
||||
"SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": {
|
||||
"type": "string",
|
||||
"label": "OIDC Provider URL",
|
||||
"help_text": "The URL for your OIDC provider, e.g.: http(s)://hostname/.",
|
||||
"category": "Generic OIDC",
|
||||
"category_slug": "oidc",
|
||||
"default": ""
|
||||
},
|
||||
"SOCIAL_AUTH_OIDC_VERIFY_SSL": {
|
||||
"type": "boolean",
|
||||
"required": false,
|
||||
"label": "Verify OIDC Provider Certificate",
|
||||
"help_text": "Verify the OIDV provider ssl certificate.",
|
||||
"category": "Generic OIDC",
|
||||
"category_slug": "oidc",
|
||||
"default": true
|
||||
},
|
||||
"AUTH_LDAP_SERVER_URI": {
|
||||
"type": "string",
|
||||
"required": false,
|
||||
@ -4485,6 +4518,38 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"SOCIAL_AUTH_OIDC_KEY": {
|
||||
"type": "string",
|
||||
"label": "OIDC Key",
|
||||
"help_text": "The OIDC key (Client ID) from your IDP.",
|
||||
"category": "Generic OIDC",
|
||||
"category_slug": "oidc",
|
||||
"default": ""
|
||||
},
|
||||
"SOCIAL_AUTH_OIDC_SECRET": {
|
||||
"type": "string",
|
||||
"label": "OIDC Secret",
|
||||
"help_text": "The OIDC secret (Client Secret) from your IDP.",
|
||||
"category": "Generic OIDC",
|
||||
"category_slug": "oidc",
|
||||
"default": ""
|
||||
},
|
||||
"SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": {
|
||||
"type": "string",
|
||||
"label": "OIDC Provider URL",
|
||||
"help_text": "The URL for your OIDC provider, e.g.: http(s)://hostname/.",
|
||||
"category": "Generic OIDC",
|
||||
"category_slug": "oidc",
|
||||
"default": ""
|
||||
},
|
||||
"SOCIAL_AUTH_OIDC_VERIFY_SSL": {
|
||||
"type": "boolean",
|
||||
"label": "Verify OIDC Provider Certificate",
|
||||
"help_text": "Verify the OIDV provider ssl certificate.",
|
||||
"category": "Generic OIDC",
|
||||
"category_slug": "oidc",
|
||||
"default": true
|
||||
},
|
||||
"AUTH_LDAP_SERVER_URI": {
|
||||
"type": "string",
|
||||
"label": "LDAP Server URI",
|
||||
|
||||
@ -253,6 +253,10 @@
|
||||
"SOCIAL_AUTH_SAML_ORGANIZATION_ATTR":{},
|
||||
"SOCIAL_AUTH_SAML_TEAM_ATTR":{},
|
||||
"SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR":{},
|
||||
"SOCIAL_AUTH_OIDC_KEY":"",
|
||||
"SOCIAL_AUTH_OIDC_SECRET":"",
|
||||
"SOCIAL_AUTH_OIDC_OIDC_ENDPOINT":"",
|
||||
"SOCIAL_AUTH_OIDC_VERIFY_SSL":true,
|
||||
"NAMED_URL_FORMATS":{
|
||||
"organizations":"<name>",
|
||||
"teams":"<name>++<organization.name>",
|
||||
|
||||
24
docs/licenses/ecdsa.txt
Normal file
24
docs/licenses/ecdsa.txt
Normal file
@ -0,0 +1,24 @@
|
||||
"python-ecdsa" Copyright (c) 2010 Brian Warner
|
||||
|
||||
Portions written in 2005 by Peter Pearson and placed in the public domain.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
||||
21
docs/licenses/python-jose.txt
Normal file
21
docs/licenses/python-jose.txt
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Michael Davis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -47,7 +47,7 @@ python-ldap>=3.4.0 # https://github.com/ansible/awx/security/dependabot/20
|
||||
pyyaml>=5.4.1 # minimum to fix https://github.com/yaml/pyyaml/issues/478
|
||||
receptorctl==1.2.3
|
||||
schedule==0.6.0
|
||||
social-auth-core==4.2.0 # see UPGRADE BLOCKERs
|
||||
social-auth-core[openidconnect]==4.3.0 # see UPGRADE BLOCKERs
|
||||
social-auth-app-django==5.0.0 # see UPGRADE BLOCKERs
|
||||
redis
|
||||
requests
|
||||
|
||||
@ -130,6 +130,8 @@ djangorestframework-yaml==2.0.0
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
docutils==0.16
|
||||
# via python-daemon
|
||||
ecdsa==0.18.0
|
||||
# via python-jose
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements_git.txt
|
||||
# django-radius
|
||||
@ -246,6 +248,7 @@ ptyprocess==0.6.0
|
||||
pyasn1==0.4.8
|
||||
# via
|
||||
# pyasn1-modules
|
||||
# python-jose
|
||||
# python-ldap
|
||||
# rsa
|
||||
# service-identity
|
||||
@ -283,6 +286,8 @@ python-dateutil==2.8.1
|
||||
# receptorctl
|
||||
python-dsv-sdk==0.0.1
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
python-jose==3.3.0
|
||||
# via social-auth-core
|
||||
python-ldap==3.4.0
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
@ -334,7 +339,9 @@ requests-oauthlib==1.3.1
|
||||
# msrest
|
||||
# social-auth-core
|
||||
rsa==4.7.2
|
||||
# via google-auth
|
||||
# via
|
||||
# google-auth
|
||||
# python-jose
|
||||
schedule==0.6.0
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
semantic-version==2.9.0
|
||||
@ -351,6 +358,7 @@ six==1.14.0
|
||||
# automat
|
||||
# django-extensions
|
||||
# django-pglocks
|
||||
# ecdsa
|
||||
# google-auth
|
||||
# isodate
|
||||
# jaraco-collections
|
||||
@ -372,7 +380,7 @@ smmap==3.0.1
|
||||
# via gitdb
|
||||
social-auth-app-django==5.0.0
|
||||
# via -r /awx_devel/requirements/requirements.in
|
||||
social-auth-core==4.2.0
|
||||
social-auth-core[openidconnect]==4.3.0
|
||||
# via
|
||||
# -r /awx_devel/requirements/requirements.in
|
||||
# social-auth-app-django
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user