Skip to content

Commit

Permalink
add impersonate function for developers
Browse files Browse the repository at this point in the history
  • Loading branch information
DaoDaoNoCode committed Jan 17, 2023
1 parent dd12e69 commit f9d9b9f
Show file tree
Hide file tree
Showing 13 changed files with 111 additions and 17 deletions.
7 changes: 7 additions & 0 deletions backend/src/devFlags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
let impersonating = false;

export const setImpersonate = (impersonate: boolean): void => {
impersonating = impersonate || false;
};

export const isImpersonating = (): boolean => impersonating;
9 changes: 9 additions & 0 deletions backend/src/routes/api/dev-impersonate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FastifyInstance, FastifyRequest } from 'fastify';
import { setImpersonate } from '../../../devFlags';

export default async (fastify: FastifyInstance): Promise<void> => {
fastify.post('/', async (request: FastifyRequest<{ Body: { impersonate: boolean } }>) => {
setImpersonate(request.body.impersonate);
return null;
});
};
4 changes: 4 additions & 0 deletions backend/src/routes/api/status/statusUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { KubeFastifyInstance, KubeStatus } from '../../../types';
import { getUserName } from '../../../utils/userUtils';
import { createCustomError } from '../../../utils/requestUtils';
import { isUserAdmin, isUserAllowed } from '../../../utils/adminUtils';
import { DEV_MODE } from '../../../utils/constants';
import { isImpersonating } from '../../../devFlags';

export const status = async (
fastify: KubeFastifyInstance,
Expand All @@ -16,6 +18,7 @@ export const status = async (
const userName = await getUserName(fastify, request);
const isAdmin = await isUserAdmin(fastify, userName, namespace);
const isAllowed = isAdmin ? true : await isUserAllowed(fastify, userName);
const impersonating = DEV_MODE ? isImpersonating() : undefined;

if (!kubeContext && !kubeContext.trim()) {
const error = createCustomError(
Expand All @@ -36,6 +39,7 @@ export const status = async (
isAdmin,
isAllowed,
serverURL: server,
isImpersonating: impersonating,
},
};
}
Expand Down
1 change: 1 addition & 0 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ export type KubeStatus = {
isAdmin: boolean;
isAllowed: boolean;
serverURL: string;
isImpersonating?: boolean;
};

export type KubeDecorator = KubeStatus & {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const IP = process.env.IP || '0.0.0.0';
export const LOG_LEVEL = process.env.FASTIFY_LOG_LEVEL || process.env.LOG_LEVEL || 'info';
export const DEV_MODE = process.env.APP_ENV === 'development';
/** Allows a user token to be used in place of the actual token for testing purposes */
export const DEV_TOKEN_AUTH = DEV_MODE ? process.env.DEV_TOKEN_AUTH : undefined;
export const DEV_IMPERSONATE_USER = DEV_MODE ? process.env.DEV_IMPERSONATE_USER : undefined;
export const APP_ENV = process.env.APP_ENV;

export const USER_ACCESS_TOKEN = 'x-forwarded-access-token';
Expand Down
13 changes: 10 additions & 3 deletions backend/src/utils/directCallUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { RequestOptions } from 'https';
import { DEV_MODE, DEV_TOKEN_AUTH, USER_ACCESS_TOKEN } from './constants';
import { DEV_IMPERSONATE_USER, DEV_MODE, USER_ACCESS_TOKEN } from './constants';
import { KubeFastifyInstance, OauthFastifyRequest } from '../types';
import { isImpersonating } from '../devFlags';

export const getDirectCallOptions = async (
fastify: KubeFastifyInstance,
Expand All @@ -19,10 +20,16 @@ export const getDirectCallOptions = async (
// In dev mode, we always are logged in fully -- no service accounts
headers = kubeHeaders;
// Fakes the call as another user to test permissions
if (DEV_TOKEN_AUTH) {
// if (DEV_TOKEN_AUTH) {
// headers = {
// ...headers,
// Authorization: `Bearer ${DEV_TOKEN_AUTH}`,
// };
// }
if (isImpersonating()) {
headers = {
...headers,
Authorization: `Bearer ${DEV_TOKEN_AUTH}`,
'Impersonate-User': DEV_IMPERSONATE_USER,
};
}
} else {
Expand Down
24 changes: 12 additions & 12 deletions backend/src/utils/userUtils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { FastifyRequest } from 'fastify';
import * as _ from 'lodash';
import { DEV_TOKEN_AUTH, USER_ACCESS_TOKEN } from './constants';
import { DEV_IMPERSONATE_USER, USER_ACCESS_TOKEN } from './constants';
import { KubeFastifyInstance } from '../types';
import { DEV_MODE } from './constants';
import { createCustomError } from './requestUtils';
import { isImpersonating } from '../devFlags';

export const usernameTranslate = (username: string): string => {
const encodedUsername = encodeURIComponent(username);
Expand Down Expand Up @@ -55,18 +56,14 @@ export const getUser = async (
fastify: KubeFastifyInstance,
request: FastifyRequest,
): Promise<OpenShiftUser> => {
let accessToken = request.headers[USER_ACCESS_TOKEN] as string;
const accessToken = request.headers[USER_ACCESS_TOKEN] as string;
if (!accessToken) {
if(DEV_TOKEN_AUTH){
accessToken = DEV_TOKEN_AUTH;
} else {
const error = createCustomError(
'Unauthorized',
`Error, missing x-forwarded-access-token header`,
401,
);
throw error;
}
const error = createCustomError(
'Unauthorized',
`Error, missing x-forwarded-access-token header`,
401,
);
throw error;
}
try {
const customObjectApiNoAuth = _.cloneDeep(fastify.kube.customObjectsApi);
Expand Down Expand Up @@ -100,6 +97,9 @@ export const getUserName = async (
return userOauth.metadata.name;
} catch (e) {
if (DEV_MODE) {
if (isImpersonating()) {
return DEV_IMPERSONATE_USER;
}
return (currentUser.username || currentUser.name)?.split('/')[0];
}
fastify.log.error(`Failed to retrieve username: ${e.response?.body?.message || e.message}`);
Expand Down
48 changes: 47 additions & 1 deletion frontend/src/app/HeaderTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,24 @@ import {
ToolbarContent,
ToolbarGroup,
ToolbarItem,
Button,
Tooltip,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon, QuestionCircleIcon } from '@patternfly/react-icons';
import { COMMUNITY_LINK, DOC_LINK, SUPPORT_LINK } from '../utilities/const';
import {
COMMUNITY_LINK,
DEV_IMPERSONATE_USER,
DEV_MODE,
DOC_LINK,
SUPPORT_LINK,
} from '../utilities/const';
import { AppNotification } from '../redux/types';
import AppLauncher from './AppLauncher';
import { useAppContext } from './AppContext';
import { logout } from './appUtils';
import { useAppSelector } from '../redux/hooks';
import { updateImpersonateSettings } from '../services/impersonateService';
import useNotification from '../utilities/useNotification';

interface HeaderToolsProps {
onNotificationsClick: () => void;
Expand All @@ -27,7 +37,9 @@ const HeaderTools: React.FC<HeaderToolsProps> = ({ onNotificationsClick }) => {
const [helpMenuOpen, setHelpMenuOpen] = React.useState(false);
const notifications: AppNotification[] = useAppSelector((state) => state.notifications);
const userName: string = useAppSelector((state) => state.user || '');
const isImpersonating: boolean = useAppSelector((state) => state.isImpersonating || false);
const { dashboardConfig } = useAppContext();
const notification = useNotification();

const newNotifications = React.useMemo(() => {
return notifications.filter((notification) => !notification.read).length;
Expand All @@ -47,6 +59,26 @@ const HeaderTools: React.FC<HeaderToolsProps> = ({ onNotificationsClick }) => {
</DropdownItem>,
];

if (DEV_MODE && !isImpersonating) {
userMenuItems.unshift(
<DropdownItem
key="impersonate"
onClick={() => {
if (!DEV_IMPERSONATE_USER) {
notification.error(
'Cannot impersonate user',
'Please check your .env or .env.local file to make sure you set DEV_IMPERSONATE_USER to the username you want to impersonate.',
);
} else {
updateImpersonateSettings(true).then(() => location.reload());
}
}}
>
Start impersonate
</DropdownItem>,
);
}

const handleHelpClick = () => {
setHelpMenuOpen(false);
};
Expand Down Expand Up @@ -125,6 +157,20 @@ const HeaderTools: React.FC<HeaderToolsProps> = ({ onNotificationsClick }) => {
</ToolbarItem>
) : null}
</ToolbarGroup>
{DEV_MODE && isImpersonating && (
<ToolbarItem>
<Tooltip
content={`You are impersonating as ${userName}, click to stop impersonating`}
position="bottom"
>
<Button
onClick={() => updateImpersonateSettings(false).then(() => location.reload())}
>
Stop impersonate
</Button>
</Tooltip>
</ToolbarItem>
)}
<ToolbarItem>
<Dropdown
removeFindDomNode
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/redux/actions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const getUserFulfilled = (response: {
isAdmin: boolean;
isAllowed: boolean;
namespace: string;
isImpersonating?: boolean;
};
}): GetUserAction => ({
type: Actions.GET_USER_FULFILLED,
Expand All @@ -27,6 +28,7 @@ export const getUserFulfilled = (response: {
isAdmin: response.kube.isAdmin,
isAllowed: response.kube.isAllowed,
dashboardNamespace: response.kube.namespace,
isImpersonating: response.kube.isImpersonating,
},
});

Expand Down
1 change: 1 addition & 0 deletions frontend/src/redux/reducers/appReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const appReducer = (state: AppState = initialState, action: GetUserAction): AppS
isAdmin: action.payload.isAdmin,
isAllowed: action.payload.isAllowed,
dashboardNamespace: action.payload.dashboardNamespace,
isImpersonating: action.payload.isImpersonating,
};
case Actions.GET_USER_REJECTED:
return {
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/redux/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface GetUserAction {
isAllowed?: boolean;
error?: Error | null;
notification?: AppNotification;
isImpersonating?: boolean;
};
}

Expand All @@ -42,6 +43,7 @@ export type AppState = {
user?: string;
userLoading: boolean;
userError?: Error | null;
isImpersonating?: boolean;

clusterID?: string;
clusterBranding?: string;
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/services/impersonateService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import axios from 'axios';

export const updateImpersonateSettings = (impersonate: boolean): Promise<void> => {
const url = '/api/dev-impersonate';
return axios
.post(url, { impersonate })
.then((response) => {
return response.data;
})
.catch((e) => {
throw new Error(e.response.data.message);
});
};
2 changes: 2 additions & 0 deletions frontend/src/utilities/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const SUPPORT_LINK = process.env.SUPPORT_LINK;
const ODH_LOGO = process.env.ODH_LOGO || 'odh-logo.svg';
const ODH_PRODUCT_NAME = process.env.ODH_PRODUCT_NAME;
const ODH_NOTEBOOK_REPO = process.env.ODH_NOTEBOOK_REPO;
const DEV_IMPERSONATE_USER = DEV_MODE ? process.env.DEV_IMPERSONATE_USER : undefined;

export {
DEV_MODE,
Expand All @@ -24,6 +25,7 @@ export {
ODH_LOGO,
ODH_PRODUCT_NAME,
ODH_NOTEBOOK_REPO,
DEV_IMPERSONATE_USER,
};

export const DOC_TYPE_TOOLTIPS = {
Expand Down

0 comments on commit f9d9b9f

Please sign in to comment.