mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Adds support for GitHub, Azure AD, Google and SAML auth to the UI
This commit is contained in:
parent
8e46166313
commit
889eb2331c
@ -25,7 +25,7 @@ class BaseRedirectView(RedirectView):
|
||||
def get_redirect_url(self, *args, **kwargs):
|
||||
last_path = self.request.COOKIES.get('lastPath', '')
|
||||
last_path = urllib.parse.quote(urllib.parse.unquote(last_path).strip('"'))
|
||||
url = reverse('ui:index')
|
||||
url = reverse('ui_next:index')
|
||||
if last_path:
|
||||
return '%s#%s' % (url, last_path)
|
||||
else:
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import AdHocCommands from './models/AdHocCommands';
|
||||
import Applications from './models/Applications';
|
||||
import Auth from './models/Auth';
|
||||
import Config from './models/Config';
|
||||
import CredentialInputSources from './models/CredentialInputSources';
|
||||
import CredentialTypes from './models/CredentialTypes';
|
||||
@ -40,6 +41,7 @@ import WorkflowJobs from './models/WorkflowJobs';
|
||||
|
||||
const AdHocCommandsAPI = new AdHocCommands();
|
||||
const ApplicationsAPI = new Applications();
|
||||
const AuthAPI = new Auth();
|
||||
const ConfigAPI = new Config();
|
||||
const CredentialInputSourcesAPI = new CredentialInputSources();
|
||||
const CredentialTypesAPI = new CredentialTypes();
|
||||
@ -81,6 +83,7 @@ const WorkflowJobsAPI = new WorkflowJobs();
|
||||
export {
|
||||
AdHocCommandsAPI,
|
||||
ApplicationsAPI,
|
||||
AuthAPI,
|
||||
ConfigAPI,
|
||||
CredentialInputSourcesAPI,
|
||||
CredentialTypesAPI,
|
||||
|
||||
10
awx/ui_next/src/api/models/Auth.js
Normal file
10
awx/ui_next/src/api/models/Auth.js
Normal file
@ -0,0 +1,10 @@
|
||||
import Base from '../Base';
|
||||
|
||||
class Auth extends Base {
|
||||
constructor(http) {
|
||||
super(http);
|
||||
this.baseUrl = '/api/v2/auth/';
|
||||
}
|
||||
}
|
||||
|
||||
export default Auth;
|
||||
@ -4,9 +4,20 @@ import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Formik } from 'formik';
|
||||
import styled from 'styled-components';
|
||||
import { LoginForm, LoginPage as PFLoginPage } from '@patternfly/react-core';
|
||||
import {
|
||||
LoginMainFooterLinksItem,
|
||||
LoginForm,
|
||||
LoginPage as PFLoginPage,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
AzureIcon,
|
||||
GoogleIcon,
|
||||
GithubIcon,
|
||||
UserCircleIcon,
|
||||
} from '@patternfly/react-icons';
|
||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||
import { RootAPI } from '../../api';
|
||||
import { AuthAPI, RootAPI } from '../../api';
|
||||
import AlertModal from '../../components/AlertModal';
|
||||
import ErrorDetail from '../../components/ErrorDetail';
|
||||
|
||||
@ -23,7 +34,7 @@ function AWXLogin({ alt, i18n, isAuthenticated }) {
|
||||
isLoading: isCustomLoginInfoLoading,
|
||||
error: customLoginInfoError,
|
||||
request: fetchCustomLoginInfo,
|
||||
result: { brandName, logo, loginInfo },
|
||||
result: { brandName, logo, loginInfo, socialAuthOptions },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [
|
||||
@ -33,7 +44,12 @@ function AWXLogin({ alt, i18n, isAuthenticated }) {
|
||||
{
|
||||
data: { BRAND_NAME },
|
||||
},
|
||||
] = await Promise.all([RootAPI.read(), RootAPI.readAssetVariables()]);
|
||||
{ data: authData },
|
||||
] = await Promise.all([
|
||||
RootAPI.read(),
|
||||
RootAPI.readAssetVariables(),
|
||||
AuthAPI.read(),
|
||||
]);
|
||||
const logoSrc = custom_logo
|
||||
? `data:image/jpeg;${custom_logo}`
|
||||
: loginLogoSrc;
|
||||
@ -41,9 +57,15 @@ function AWXLogin({ alt, i18n, isAuthenticated }) {
|
||||
brandName: BRAND_NAME,
|
||||
logo: logoSrc,
|
||||
loginInfo: custom_login_info,
|
||||
socialAuthOptions: authData,
|
||||
};
|
||||
}, []),
|
||||
{ brandName: null, logo: loginLogoSrc, loginInfo: null }
|
||||
{
|
||||
brandName: null,
|
||||
logo: loginLogoSrc,
|
||||
loginInfo: null,
|
||||
socialAuthOptions: {},
|
||||
}
|
||||
);
|
||||
|
||||
const {
|
||||
@ -100,6 +122,79 @@ function AWXLogin({ alt, i18n, isAuthenticated }) {
|
||||
: ''
|
||||
}
|
||||
textContent={loginInfo}
|
||||
socialMediaLoginContent={
|
||||
<>
|
||||
{socialAuthOptions &&
|
||||
Object.keys(socialAuthOptions).map(authKey => {
|
||||
const loginUrl = socialAuthOptions[authKey].login_url;
|
||||
if (authKey === 'azuread-oauth2') {
|
||||
return (
|
||||
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
|
||||
<Tooltip content={i18n._(t`Sign in with Azure AD`)}>
|
||||
<AzureIcon />
|
||||
</Tooltip>
|
||||
</LoginMainFooterLinksItem>
|
||||
);
|
||||
}
|
||||
if (authKey === 'github') {
|
||||
return (
|
||||
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
|
||||
<Tooltip content={i18n._(t`Sign in with GitHub`)}>
|
||||
<GithubIcon />
|
||||
</Tooltip>
|
||||
</LoginMainFooterLinksItem>
|
||||
);
|
||||
}
|
||||
if (authKey === 'github-org') {
|
||||
return (
|
||||
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
|
||||
<Tooltip
|
||||
content={i18n._(t`Sign in with GitHub Organizations`)}
|
||||
>
|
||||
<GithubIcon />
|
||||
</Tooltip>
|
||||
</LoginMainFooterLinksItem>
|
||||
);
|
||||
}
|
||||
if (authKey === 'github-team') {
|
||||
return (
|
||||
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
|
||||
<Tooltip content={i18n._(t`Sign in with GitHub Teams`)}>
|
||||
<GithubIcon />
|
||||
</Tooltip>
|
||||
</LoginMainFooterLinksItem>
|
||||
);
|
||||
}
|
||||
if (authKey === 'google-oauth2') {
|
||||
return (
|
||||
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
|
||||
<Tooltip content={i18n._(t`Sign in with Google`)}>
|
||||
<GoogleIcon />
|
||||
</Tooltip>
|
||||
</LoginMainFooterLinksItem>
|
||||
);
|
||||
}
|
||||
if (authKey.startsWith('saml')) {
|
||||
const samlIDP = authKey.split(':')[1] || null;
|
||||
return (
|
||||
<LoginMainFooterLinksItem href={loginUrl} key={authKey}>
|
||||
<Tooltip
|
||||
content={
|
||||
samlIDP
|
||||
? i18n._(t`Sign in with SAML ${samlIDP}`)
|
||||
: i18n._(t`Sign in with SAML`)
|
||||
}
|
||||
>
|
||||
<UserCircleIcon />
|
||||
</Tooltip>
|
||||
</LoginMainFooterLinksItem>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Formik
|
||||
initialValues={{
|
||||
@ -109,30 +204,28 @@ function AWXLogin({ alt, i18n, isAuthenticated }) {
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{formik => (
|
||||
<>
|
||||
<LoginForm
|
||||
className={authError ? 'pf-m-error' : ''}
|
||||
helperText={helperText}
|
||||
isLoginButtonDisabled={isAuthenticating}
|
||||
isValidPassword={!authError}
|
||||
isValidUsername={!authError}
|
||||
loginButtonLabel={i18n._(t`Log In`)}
|
||||
onChangePassword={val => {
|
||||
formik.setFieldValue('password', val);
|
||||
dismissAuthError();
|
||||
}}
|
||||
onChangeUsername={val => {
|
||||
formik.setFieldValue('username', val);
|
||||
dismissAuthError();
|
||||
}}
|
||||
onLoginButtonClick={formik.handleSubmit}
|
||||
passwordLabel={i18n._(t`Password`)}
|
||||
passwordValue={formik.values.password}
|
||||
showHelperText={authError}
|
||||
usernameLabel={i18n._(t`Username`)}
|
||||
usernameValue={formik.values.username}
|
||||
/>
|
||||
</>
|
||||
<LoginForm
|
||||
className={authError ? 'pf-m-error' : ''}
|
||||
helperText={helperText}
|
||||
isLoginButtonDisabled={isAuthenticating}
|
||||
isValidPassword={!authError}
|
||||
isValidUsername={!authError}
|
||||
loginButtonLabel={i18n._(t`Log In`)}
|
||||
onChangePassword={val => {
|
||||
formik.setFieldValue('password', val);
|
||||
dismissAuthError();
|
||||
}}
|
||||
onChangeUsername={val => {
|
||||
formik.setFieldValue('username', val);
|
||||
dismissAuthError();
|
||||
}}
|
||||
onLoginButtonClick={formik.handleSubmit}
|
||||
passwordLabel={i18n._(t`Password`)}
|
||||
passwordValue={formik.values.password}
|
||||
showHelperText={authError}
|
||||
usernameLabel={i18n._(t`Username`)}
|
||||
usernameValue={formik.values.username}
|
||||
/>
|
||||
)}
|
||||
</Formik>
|
||||
{loginInfoError && (
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { RootAPI } from '../../api';
|
||||
import { AuthAPI, RootAPI } from '../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
@ -16,6 +16,10 @@ RootAPI.readAssetVariables.mockResolvedValue({
|
||||
},
|
||||
});
|
||||
|
||||
AuthAPI.read.mockResolvedValue({
|
||||
data: {},
|
||||
});
|
||||
|
||||
describe('<Login />', () => {
|
||||
async function findChildren(wrapper) {
|
||||
const [
|
||||
@ -268,4 +272,111 @@ describe('<Login />', () => {
|
||||
await waitForElement(wrapper, 'Redirect', el => el.props().to === '/');
|
||||
done();
|
||||
});
|
||||
|
||||
test('GitHub auth buttons shown', async done => {
|
||||
AuthAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
github: {
|
||||
login_url: '/sso/login/github/',
|
||||
complete_url: 'https://localhost:8043/sso/complete/github/',
|
||||
},
|
||||
'github-org': {
|
||||
login_url: '/sso/login/github-org/',
|
||||
complete_url: 'https://localhost:8043/sso/complete/github-org/',
|
||||
},
|
||||
'github-team': {
|
||||
login_url: '/sso/login/github-team/',
|
||||
complete_url: 'https://localhost:8043/sso/complete/github-team/',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => false} />);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('GithubIcon').length).toBe(3);
|
||||
expect(wrapper.find('AzureIcon').length).toBe(0);
|
||||
expect(wrapper.find('GoogleIcon').length).toBe(0);
|
||||
expect(wrapper.find('UserCircleIcon').length).toBe(0);
|
||||
done();
|
||||
});
|
||||
|
||||
test('Google auth button shown', async done => {
|
||||
AuthAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
'google-oauth2': {
|
||||
login_url: '/sso/login/google-oauth2/',
|
||||
complete_url: 'https://localhost:8043/sso/complete/google-oauth2/',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => false} />);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('GithubIcon').length).toBe(0);
|
||||
expect(wrapper.find('AzureIcon').length).toBe(0);
|
||||
expect(wrapper.find('GoogleIcon').length).toBe(1);
|
||||
expect(wrapper.find('UserCircleIcon').length).toBe(0);
|
||||
done();
|
||||
});
|
||||
|
||||
test('Azure AD auth button shown', async done => {
|
||||
AuthAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
'azuread-oauth2': {
|
||||
login_url: '/sso/login/azuread-oauth2/',
|
||||
complete_url: 'https://localhost:8043/sso/complete/azuread-oauth2/',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => false} />);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('GithubIcon').length).toBe(0);
|
||||
expect(wrapper.find('AzureIcon').length).toBe(1);
|
||||
expect(wrapper.find('GoogleIcon').length).toBe(0);
|
||||
expect(wrapper.find('UserCircleIcon').length).toBe(0);
|
||||
done();
|
||||
});
|
||||
|
||||
test('SAML auth buttons shown', async done => {
|
||||
AuthAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
saml: {
|
||||
login_url: '/sso/login/saml/',
|
||||
complete_url: 'https://localhost:8043/sso/complete/saml/',
|
||||
metadata_url: '/sso/metadata/saml/',
|
||||
},
|
||||
'saml:onelogin': {
|
||||
login_url: '/sso/login/saml/?idp=onelogin',
|
||||
complete_url: 'https://localhost:8043/sso/complete/saml/',
|
||||
metadata_url: '/sso/metadata/saml/',
|
||||
},
|
||||
'saml:someotheridp': {
|
||||
login_url: '/sso/login/saml/?idp=someotheridp',
|
||||
complete_url: 'https://localhost:8043/sso/complete/saml/',
|
||||
metadata_url: '/sso/metadata/saml/',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => false} />);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('GithubIcon').length).toBe(0);
|
||||
expect(wrapper.find('AzureIcon').length).toBe(0);
|
||||
expect(wrapper.find('GoogleIcon').length).toBe(0);
|
||||
expect(wrapper.find('UserCircleIcon').length).toBe(3);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@ const TARGET = process.env.TARGET || 'https://localhost:8043';
|
||||
|
||||
module.exports = app => {
|
||||
app.use(
|
||||
createProxyMiddleware(['/api', '/websocket'], {
|
||||
createProxyMiddleware(['/api', '/websocket', '/sso'], {
|
||||
target: TARGET,
|
||||
secure: false,
|
||||
ws: true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user