mirror of
https://github.com/ansible/awx.git
synced 2026-03-01 08:48:46 -03:30
Update logout/login redirect for different users
* Logout as User A and Login as User B redirects to `/home' * Logout as User A and Login as User A redirects to `/home' * Allow session to timeout as User A and Login as User A redirects to User A's last location See: https://github.com/ansible/awx/issues/11167
This commit is contained in:
@@ -101,9 +101,9 @@ const AuthorizedRoutes = ({ routeConfig }) => {
|
|||||||
export function ProtectedRoute({ children, ...rest }) {
|
export function ProtectedRoute({ children, ...rest }) {
|
||||||
const {
|
const {
|
||||||
authRedirectTo,
|
authRedirectTo,
|
||||||
setAuthRedirectTo,
|
|
||||||
loginRedirectOverride,
|
|
||||||
isUserBeingLoggedOut,
|
isUserBeingLoggedOut,
|
||||||
|
loginRedirectOverride,
|
||||||
|
setAuthRedirectTo,
|
||||||
} = useSession();
|
} = useSession();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
|
|||||||
@@ -11,3 +11,4 @@ export const JOB_TYPE_URL_SEGMENTS = {
|
|||||||
export const SESSION_TIMEOUT_KEY = 'awx-session-timeout';
|
export const SESSION_TIMEOUT_KEY = 'awx-session-timeout';
|
||||||
export const SESSION_REDIRECT_URL = 'awx-redirect-url';
|
export const SESSION_REDIRECT_URL = 'awx-redirect-url';
|
||||||
export const PERSISTENT_FILTER_KEY = 'awx-persistent-filter';
|
export const PERSISTENT_FILTER_KEY = 'awx-persistent-filter';
|
||||||
|
export const SESSION_USER_ID = 'awx-session-user-id';
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { DateTime } from 'luxon';
|
|||||||
import { RootAPI, MeAPI } from 'api';
|
import { RootAPI, MeAPI } from 'api';
|
||||||
import { isAuthenticated } from 'util/auth';
|
import { isAuthenticated } from 'util/auth';
|
||||||
import useRequest from 'hooks/useRequest';
|
import useRequest from 'hooks/useRequest';
|
||||||
import { SESSION_TIMEOUT_KEY } from '../constants';
|
import { SESSION_TIMEOUT_KEY, SESSION_USER_ID } from '../constants';
|
||||||
|
|
||||||
// The maximum supported timeout for setTimeout(), in milliseconds,
|
// The maximum supported timeout for setTimeout(), in milliseconds,
|
||||||
// is the highest number you can represent as a signed 32bit
|
// is the highest number you can represent as a signed 32bit
|
||||||
@@ -101,6 +101,7 @@ function SessionProvider({ children }) {
|
|||||||
setIsUserBeingLoggedOut(true);
|
setIsUserBeingLoggedOut(true);
|
||||||
if (!isSessionExpired.current) {
|
if (!isSessionExpired.current) {
|
||||||
setAuthRedirectTo('/logout');
|
setAuthRedirectTo('/logout');
|
||||||
|
window.localStorage.setItem(SESSION_USER_ID, null);
|
||||||
}
|
}
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
await RootAPI.logout();
|
await RootAPI.logout();
|
||||||
@@ -167,21 +168,21 @@ function SessionProvider({ children }) {
|
|||||||
|
|
||||||
const sessionValue = useMemo(
|
const sessionValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
isUserBeingLoggedOut,
|
|
||||||
loginRedirectOverride,
|
|
||||||
authRedirectTo,
|
authRedirectTo,
|
||||||
handleSessionContinue,
|
handleSessionContinue,
|
||||||
isSessionExpired,
|
isSessionExpired,
|
||||||
|
isUserBeingLoggedOut,
|
||||||
|
loginRedirectOverride,
|
||||||
logout,
|
logout,
|
||||||
sessionCountdown,
|
sessionCountdown,
|
||||||
setAuthRedirectTo,
|
setAuthRedirectTo,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
isUserBeingLoggedOut,
|
|
||||||
loginRedirectOverride,
|
|
||||||
authRedirectTo,
|
authRedirectTo,
|
||||||
handleSessionContinue,
|
handleSessionContinue,
|
||||||
isSessionExpired,
|
isSessionExpired,
|
||||||
|
isUserBeingLoggedOut,
|
||||||
|
loginRedirectOverride,
|
||||||
logout,
|
logout,
|
||||||
sessionCountdown,
|
sessionCountdown,
|
||||||
setAuthRedirectTo,
|
setAuthRedirectTo,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable react/jsx-no-useless-fragment */
|
/* eslint-disable react/jsx-no-useless-fragment */
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Redirect, withRouter } from 'react-router-dom';
|
import { Redirect, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -31,7 +31,8 @@ import { AuthAPI, RootAPI } from 'api';
|
|||||||
import AlertModal from 'components/AlertModal';
|
import AlertModal from 'components/AlertModal';
|
||||||
import ErrorDetail from 'components/ErrorDetail';
|
import ErrorDetail from 'components/ErrorDetail';
|
||||||
import { useSession } from 'contexts/Session';
|
import { useSession } from 'contexts/Session';
|
||||||
import { SESSION_REDIRECT_URL } from '../../constants';
|
import { getCurrentUserId } from 'util/auth';
|
||||||
|
import { SESSION_REDIRECT_URL, SESSION_USER_ID } from '../../constants';
|
||||||
|
|
||||||
const loginLogoSrc = 'static/media/logo-login.svg';
|
const loginLogoSrc = 'static/media/logo-login.svg';
|
||||||
|
|
||||||
@@ -43,6 +44,8 @@ const Login = styled(PFLogin)`
|
|||||||
|
|
||||||
function AWXLogin({ alt, isAuthenticated }) {
|
function AWXLogin({ alt, isAuthenticated }) {
|
||||||
const { authRedirectTo, isSessionExpired, setAuthRedirectTo } = useSession();
|
const { authRedirectTo, isSessionExpired, setAuthRedirectTo } = useSession();
|
||||||
|
const isNewUser = useRef(true);
|
||||||
|
const hasVerifiedUser = useRef(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isLoading: isCustomLoginInfoLoading,
|
isLoading: isCustomLoginInfoLoading,
|
||||||
@@ -112,8 +115,26 @@ function AWXLogin({ alt, isAuthenticated }) {
|
|||||||
if (isCustomLoginInfoLoading) {
|
if (isCustomLoginInfoLoading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (isAuthenticated(document.cookie)) {
|
|
||||||
return <Redirect to={authRedirectTo || '/'} />;
|
if (isAuthenticated(document.cookie) && !hasVerifiedUser.current) {
|
||||||
|
const currentUserId = getCurrentUserId(document.cookie);
|
||||||
|
const verifyIsNewUser = () => {
|
||||||
|
const previousUserId = JSON.parse(
|
||||||
|
window.localStorage.getItem(SESSION_USER_ID)
|
||||||
|
);
|
||||||
|
if (previousUserId === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return currentUserId.toString() !== previousUserId.toString();
|
||||||
|
};
|
||||||
|
isNewUser.current = verifyIsNewUser();
|
||||||
|
hasVerifiedUser.current = true;
|
||||||
|
window.localStorage.setItem(SESSION_USER_ID, JSON.stringify(currentUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAuthenticated(document.cookie) && hasVerifiedUser.current) {
|
||||||
|
const redirect = isNewUser.current ? '/' : authRedirectTo;
|
||||||
|
return <Redirect to={redirect} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let helperText;
|
let helperText;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { AuthAPI, RootAPI } from 'api';
|
import { AuthAPI, RootAPI } from 'api';
|
||||||
import {
|
import {
|
||||||
@@ -7,9 +8,16 @@ import {
|
|||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
|
|
||||||
import AWXLogin from './Login';
|
import AWXLogin from './Login';
|
||||||
|
import { getCurrentUserId } from 'util/auth';
|
||||||
|
|
||||||
|
import { SESSION_USER_ID } from '../../constants';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
|
|
||||||
|
jest.mock('util/auth', () => ({
|
||||||
|
getCurrentUserId: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
RootAPI.readAssetVariables.mockResolvedValue({
|
RootAPI.readAssetVariables.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
BRAND_NAME: 'AWX',
|
BRAND_NAME: 'AWX',
|
||||||
@@ -72,6 +80,13 @@ describe('<Login />', () => {
|
|||||||
custom_logo: 'images/foo.jpg',
|
custom_logo: 'images/foo.jpg',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
Object.defineProperty(window, 'localStorage', {
|
||||||
|
value: {
|
||||||
|
getItem: jest.fn(() => '42'),
|
||||||
|
setItem: jest.fn(() => null),
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -276,15 +291,77 @@ describe('<Login />', () => {
|
|||||||
expect(RootAPI.login).toHaveBeenCalledWith('un', 'pw');
|
expect(RootAPI.login).toHaveBeenCalledWith('un', 'pw');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('render Redirect to / when already authenticated', async () => {
|
test('render Redirect to / when already authenticated as a new user', async () => {
|
||||||
|
getCurrentUserId.mockReturnValue(1);
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/login'],
|
||||||
|
});
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => true} />);
|
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => true} />, {
|
||||||
|
context: {
|
||||||
|
router: { history },
|
||||||
|
session: {
|
||||||
|
authRedirectTo: '/projects',
|
||||||
|
handleSessionContinue: () => {},
|
||||||
|
isSessionExpired: false,
|
||||||
|
isUserBeingLoggedOut: false,
|
||||||
|
loginRedirectOverride: null,
|
||||||
|
logout: () => {},
|
||||||
|
sessionCountdown: 60,
|
||||||
|
setAuthRedirectTo: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
expect(window.localStorage.getItem).toHaveBeenCalledWith(SESSION_USER_ID);
|
||||||
|
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||||
|
SESSION_USER_ID,
|
||||||
|
'1'
|
||||||
|
);
|
||||||
await waitForElement(wrapper, 'Redirect', (el) => el.length === 1);
|
await waitForElement(wrapper, 'Redirect', (el) => el.length === 1);
|
||||||
await waitForElement(wrapper, 'Redirect', (el) => el.props().to === '/');
|
await waitForElement(wrapper, 'Redirect', (el) => el.props().to === '/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('render redirect to authRedirectTo when authenticated as a previous user', async () => {
|
||||||
|
getCurrentUserId.mockReturnValue(42);
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/login'],
|
||||||
|
});
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<AWXLogin isAuthenticated={() => true} />, {
|
||||||
|
context: {
|
||||||
|
router: { history },
|
||||||
|
session: {
|
||||||
|
authRedirectTo: '/projects',
|
||||||
|
handleSessionContinue: () => {},
|
||||||
|
isSessionExpired: false,
|
||||||
|
isUserBeingLoggedOut: false,
|
||||||
|
loginRedirectOverride: null,
|
||||||
|
logout: () => {},
|
||||||
|
sessionCountdown: 60,
|
||||||
|
setAuthRedirectTo: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
expect(window.localStorage.getItem).toHaveBeenCalledWith(SESSION_USER_ID);
|
||||||
|
expect(window.localStorage.setItem).toHaveBeenCalledWith(
|
||||||
|
SESSION_USER_ID,
|
||||||
|
'42'
|
||||||
|
);
|
||||||
|
wrapper.update();
|
||||||
|
await waitForElement(wrapper, 'Redirect', (el) => el.length === 1);
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Redirect',
|
||||||
|
(el) => el.props().to === '/projects'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('GitHub auth buttons shown', async () => {
|
test('GitHub auth buttons shown', async () => {
|
||||||
AuthAPI.read.mockResolvedValue({
|
AuthAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@@ -6,3 +6,29 @@ export function isAuthenticated(cookie) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCurrentUserId(cookie) {
|
||||||
|
if (!isAuthenticated(cookie)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const name = 'current_user';
|
||||||
|
let userId = null;
|
||||||
|
if (cookie && cookie !== '') {
|
||||||
|
const cookies = cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const parsedCookie = cookies[i].trim();
|
||||||
|
if (parsedCookie.substring(0, name.length + 1) === `${name}=`) {
|
||||||
|
userId = parseUserId(
|
||||||
|
decodeURIComponent(parsedCookie.substring(name.length + 1))
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUserId(decodedUserData) {
|
||||||
|
const userData = JSON.parse(decodedUserData);
|
||||||
|
return userData.id;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isAuthenticated } from './auth';
|
import { isAuthenticated, getCurrentUserId } from './auth';
|
||||||
|
|
||||||
const invalidCookie = 'invalid';
|
const invalidCookie = 'invalid';
|
||||||
const validLoggedOutCookie =
|
const validLoggedOutCookie =
|
||||||
@@ -19,3 +19,17 @@ describe('isAuthenticated', () => {
|
|||||||
expect(isAuthenticated(validLoggedInCookie)).toEqual(true);
|
expect(isAuthenticated(validLoggedInCookie)).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getCurrentUserId', () => {
|
||||||
|
test('returns null for invalid cookie', () => {
|
||||||
|
expect(getCurrentUserId(invalidCookie)).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns null for expired cookie', () => {
|
||||||
|
expect(getCurrentUserId(validLoggedOutCookie)).toEqual(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns current user id for valid authenticated cookie', () => {
|
||||||
|
expect(getCurrentUserId(validLoggedInCookie)).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user