(true);
+
+ useEffect(() => {
+ fetch(`/api/jit/list/reviews`)
+ .then(response => response.json())
+ .then(setRequests)
+ .then(() => setLoading(false))
+ .catch(console.error);
+ }, []);
+
+ const tableContent: ReviewsTableContent[] = [
+ {
+ key: '',
+ value: () => '',
+ },
+ {
+ key: 'ID',
+ value: ({ jitRequestId }: ReviewsInfo) => jitRequestId,
+ },
+ {
+ key: 'User',
+ value: ({ requestedFor }: ReviewsInfo) => requestedFor,
+ },
+ {
+ key: 'Vertical',
+ value: ({ vertical }: ReviewsInfo) => vertical,
+ },
+ {
+ key: 'Review As',
+ value: ({ team }: ReviewsInfo) => team,
+ },
+ {
+ key: 'Environment',
+ value: ({ environment }: ReviewsInfo) => environment,
+ },
+ {
+ key: 'Requested For',
+ value: ({ resourceType, awsResourceType, resourceAction, awsResourceNames }: ReviewsInfo) =>
+ renderResourceTypeAndAction(
+ resourceType,
+ awsResourceType,
+ resourceAction,
+ awsResourceNames,
+ classes,
+ ),
+ },
+ {
+ key: 'Justification',
+ value: ({ justification }: ReviewsInfo) => justification,
+ },
+ {
+ key: 'Grant Duration',
+ value: ({ grantAt, grantWindow }: ReviewsInfo) =>
+ renderGrantDuration(grantAt, grantWindow, classes),
+ },
+ {
+ key: 'Actions',
+ value: ({ jitApprovalId, status }: ReviewsInfo) => (
+
+ }
+ onClick={() => reviewRequest(jitApprovalId, 'approve')}
+ disabled={status !== 'PENDING'}
+ />
+ }
+ onClick={() => reviewRequest(jitApprovalId, 'reject')}
+ disabled={status !== 'PENDING'}
+ />
+
+ ),
+ },
+ ];
+
+ const renderHeaderRows = () =>
+ tableContent.map(header => {header.key});
+
+ const renderBodyRows = () =>
+ requests.map((row: ReviewsInfo) => (
+
+ {tableContent.map(header => (
+ {header.value(row)}
+ ))}
+
+ ));
+
+ const renderTable = () => (
+
+
+
+ {renderHeaderRows()}
+
+ {renderBodyRows()}
+
+
+ );
+
+ const showNoResult = () => (
+
+
+ {`No reviews found`}
+
+
+ {"If you're not receiving any reviews, please get TEAM_JITREVIEWER role."}
+ {'Get Started with JIT. Refer https://navihq.atlassian.net/wiki/x/4gIZNg'}
+
+
+ );
+
+ const showReviewsTable = () => {
+ if (loading) return ;
+ if (!requests || requests.length === 0) return showNoResult();
+ return renderTable();
+ };
+
+ const classes = useStyles();
+ return (
+ <>
+
+
+
+
+ {' '}
+ {'Reviews'}{' '}
+
+ {showReviewsTable()}
+
+
+
+ >
+ );
+};
diff --git a/src/components/just-in-time-access/constants.tsx b/src/components/just-in-time-access/constants.tsx
new file mode 100644
index 0000000..a764ffe
--- /dev/null
+++ b/src/components/just-in-time-access/constants.tsx
@@ -0,0 +1,31 @@
+export const RESOURCE_TYPE_LIST = ['DB', 'KUBERNETES', 'AWS', 'AWS-CUSTOM', 'GOCD'];
+export const KUBERNETES_ACTION_LIST = ['read', 'write', 'admin'];
+export const DB_ACTION_LIST = ['read', 'write', 'master', 'manager'];
+export const AWS_ACTION_LIST = ['App-Developers', 'App-Admin', 'App-Owner'];
+export const AWS_CUSTOM_RESOURCE_TYPE = ['S3', 'DYNAMODB', 'RDS', 'DOCDB', 'ELASTICACHE'];
+export const GOCD_ACTION_LIST = ['deploy-prod', 'prod-resource', 'hotfix'];
+
+export const resourceActionMap = {
+ KUBERNETES: KUBERNETES_ACTION_LIST,
+ DB: DB_ACTION_LIST,
+ AWS: AWS_ACTION_LIST,
+ 'AWS-CUSTOM': ['read'],
+ GOCD: GOCD_ACTION_LIST,
+};
+
+export const RESOURCE_ACTION_MAP = {
+ KUBERNETES: KUBERNETES_ACTION_LIST,
+ DB: DB_ACTION_LIST,
+ AWS: AWS_ACTION_LIST,
+ 'AWS-CUSTOM': ['read'],
+ GOCD: GOCD_ACTION_LIST,
+};
+
+export const CUSTOM_RESOURCE_ACTION_LIST_MAP = {
+ S3: ['read', 'write'],
+ DYNAMODB: ['read'],
+ RDS: ['read', 'write'],
+ DOCDB: ['read', 'write'],
+ ELASTICACHE: ['read'],
+ // CUSTOM: ['read', 'write', 'custom'],
+};
diff --git a/src/components/just-in-time-access/structs.tsx b/src/components/just-in-time-access/structs.tsx
new file mode 100644
index 0000000..7aa15c4
--- /dev/null
+++ b/src/components/just-in-time-access/structs.tsx
@@ -0,0 +1,62 @@
+export interface DropDownListProps {
+ label: string;
+ fieldName: string;
+ list: any[];
+ style: string;
+}
+
+export type JitRequest = {
+ team: string;
+ vertical: string;
+ environment: string;
+ resourceType: string;
+ resourceAction: string;
+ awsResourceType: string;
+ awsResourceNames: Array;
+ justification: string;
+ grantWindow: number;
+ grantAt: number;
+};
+
+export type RequestsInfo = {
+ id: number;
+ team: string;
+ vertical: string;
+ environment: string;
+ resourceType: string;
+ awsResourceType: string;
+ awsResourceNames: Array;
+ resourceAction: string;
+ justification: string;
+ grantWindow: number;
+ grantAt: number;
+ status: string;
+};
+
+export type ReviewsInfo = {
+ jitApprovalId: number;
+ jitRequestId: number;
+ requestedFor: string;
+ team: string;
+ vertical: string;
+ environment: string;
+ resourceType: string;
+ awsResourceType: string;
+ awsResourceNames: Array;
+ resourceAction: string;
+ grantWindow: number;
+ grantAt: number;
+ justification: string;
+ action: string;
+ status: string;
+};
+
+export type RequestTableContent = {
+ key: string;
+ value: (row: RequestsInfo) => any;
+};
+
+export type ReviewsTableContent = {
+ key: string;
+ value: (row: ReviewsInfo) => any;
+};
diff --git a/src/components/just-in-time-access/styles.tsx b/src/components/just-in-time-access/styles.tsx
new file mode 100644
index 0000000..29f94bb
--- /dev/null
+++ b/src/components/just-in-time-access/styles.tsx
@@ -0,0 +1,104 @@
+import { makeStyles } from '@material-ui/core/styles';
+
+export const useStyles = makeStyles(theme => ({
+ main: {
+ marginTop: theme.spacing(8),
+ marginLeft: theme.spacing(2),
+ marginBottom: theme.spacing(2),
+ },
+ formContainer: {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ buttonContainer: {
+ display: 'flex',
+ justifyContent: 'center',
+ },
+ checkButton: {
+ marginRight: 10,
+ paddingRight: 0,
+ },
+ clearButton: {
+ paddingRight: 5,
+ },
+ box: {
+ marginLeft: 40,
+ marginBottom: 20,
+ marginTop: 60,
+ },
+ bigCard: {
+ width: 400,
+ height: 800,
+ border: '5px solid #E8E8E8',
+ padding: 5,
+ overflowY: 'auto',
+ },
+ halfCardTop: {
+ width: 1200,
+ height: 390,
+ border: '5px solid #E8E8E8',
+ padding: 5,
+ overflowY: 'auto',
+ marginLeft: 10,
+ marginBottom: 10,
+ },
+ halfCardBottom: {
+ width: 1200,
+ height: 380,
+ border: '5px solid #E8E8E8',
+ padding: 5,
+ overflowY: 'auto',
+ marginLeft: 10,
+ },
+ field: {
+ marginTop: 20,
+ width: 350,
+ padding: 5,
+ },
+ leftField: {
+ marginTop: 20,
+ width: 170,
+ marginRight: 10,
+ },
+ rightField: {
+ marginTop: 20,
+ width: 170,
+ },
+ gridRow: {
+ width: 1600,
+ display: 'flex',
+ marginTop: 20,
+ marginLeft: 75,
+ },
+ heading: {
+ backgroundColor: 'grey',
+ color: 'white',
+ paddingLeft: 20,
+ paddingTop: 5,
+ paddingBottom: 5,
+ },
+ leftHeading: {
+ backgroundColor: 'grey',
+ color: 'white',
+ paddingLeft: 50,
+ paddingTop: 5,
+ paddingBottom: 5,
+ },
+ submitButton: {
+ backgroundColor: '#fd7700',
+ alignItems: 'flex-end',
+ color: '#ffffff',
+ '&:hover': {
+ backgroundColor: '#ff7700',
+ color: '#ffffff',
+ },
+ },
+ grantDuration: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+ resourceTypeAndAction: {
+ display: 'flex',
+ alignItems: 'center',
+ },
+}));
diff --git a/src/components/just-in-time-access/utils.tsx b/src/components/just-in-time-access/utils.tsx
new file mode 100644
index 0000000..a81aa89
--- /dev/null
+++ b/src/components/just-in-time-access/utils.tsx
@@ -0,0 +1,77 @@
+import { toast } from 'react-toastify';
+import { ClassNameMap } from '@material-ui/core/styles/withStyles';
+import { parseDate } from '../../helper/date';
+import { httpClient, post } from '../../helper/api-client';
+import React from 'react';
+import { JitRequest } from './structs';
+import { updateToastError, updateToastSuccess } from '@src/helper/updateToast';
+
+export const renderGrantDuration = (
+ grantAt: number,
+ grantWindow: number,
+ classes: ClassNameMap,
+): React.ReactElement => {
+ const message = parseDate(grantAt * 1000) + ' for ' + grantWindow + ' hours';
+ return (
+
+ );
+};
+
+export const renderResourceTypeAndAction = (
+ resourceType: string,
+ awsResourceType: string,
+ resourceAction: string,
+ awsResourceNames: Array,
+ classes: ClassNameMap,
+): React.ReactElement => {
+ const message =
+ resourceType === 'AWS-CUSTOM'
+ ? `${resourceType} - ${awsResourceType} with ${resourceAction} on ${awsResourceNames.join(
+ ', ',
+ )}`
+ : `${resourceType} - ${resourceAction}`;
+
+ return (
+
+ );
+};
+
+export const submitJustInTimeAccessRequest = async (values: JitRequest) => {
+ const toastId = toast.info('In Progress ...', {
+ autoClose: 10000,
+ });
+ await post(values, '/api/jit/portal').then(response => {
+ if (response.status === 201) {
+ updateToastSuccess(toastId, 'Request submitted successfully');
+ } else if (response.status === 400) {
+ updateToastError(toastId, 'Invalid request or reviewer not found');
+ } else if (response.status === 409) {
+ updateToastError(toastId, 'Request already exists. Please wait for approval');
+ } else {
+ updateToastError(toastId, 'Internal Server Error');
+ }
+ });
+};
+
+export const reviewRequest = (id: number, action: string) => {
+ const toastId = toast.info('Reviewing ...', {
+ autoClose: 10000,
+ });
+ httpClient(`/api/jit/${action}/portal/${id}`, {
+ method: 'POST',
+ }).then(response => {
+ if (response.ok) {
+ const message =
+ action === 'reject' ? `${action}ed request ${id}` : `${action}d request ${id}`;
+ updateToastSuccess(toastId, message);
+ } else if (response.status === 401) {
+ updateToastError(toastId, 'You are not authorized to perform this action');
+ } else {
+ updateToastError(toastId, 'Internal Server Error');
+ }
+ });
+};
diff --git a/src/components/layout/PortalMenu.tsx b/src/components/layout/PortalMenu.tsx
index 14fea9c..556cc2c 100644
--- a/src/components/layout/PortalMenu.tsx
+++ b/src/components/layout/PortalMenu.tsx
@@ -28,6 +28,9 @@ const PortalMenu = props => {
+