Merge pull request #724 from navi-infra/infra-3971-jit
INFRA-3971 | Harinder | Integrating JustInTime access in deployment portal frontend
This commit is contained in:
@@ -15,6 +15,7 @@ import ManifestVersionHistory from '@components/ManifestVersionHistory/ManifestV
|
||||
import ImportManifestJson from '@components/manifest/ImportManifestJson';
|
||||
import GocdYamlGenerator from '@components/gocd-yaml/PipelineBaseForm';
|
||||
import TokenGeneratorPage from '@components/token-generator/TokenGeneratorPage';
|
||||
import JustInTimeAccessPage from '@components/just-in-time-access/JustInTimeAccessPage';
|
||||
import GenerateForm from '@components/manifest/GenerateForm';
|
||||
import ReportGeneratorPage from '@components/report-generator/ReportGeneratorPage';
|
||||
import ShowDeploymentManifest from '@components/manifest/ShowDeploymentManifest';
|
||||
@@ -107,6 +108,12 @@ function App() {
|
||||
<ProtectedRoute loginRedirect={loginRedirect} component={TokenGeneratorPage} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/justInTimeAccess/"
|
||||
element={
|
||||
<ProtectedRoute loginRedirect={loginRedirect} component={JustInTimeAccessPage} />
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/databaseAccessToken/"
|
||||
element={
|
||||
|
||||
233
src/components/just-in-time-access/JustInTimeAccessPage.tsx
Normal file
233
src/components/just-in-time-access/JustInTimeAccessPage.tsx
Normal file
@@ -0,0 +1,233 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import { withCookies } from 'react-cookie';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '@src/store';
|
||||
import { justInTimeAccessValidationSchema } from './JustInTimeAccessValidation';
|
||||
import { FormikTextField } from '../common/FormikTextField';
|
||||
import Header from '../layout/Header';
|
||||
import { httpClient } from '../../helper/api-client';
|
||||
import { getIn, useFormikContext } from 'formik';
|
||||
|
||||
import { Button, Card, Grid, Typography, InputLabel, FormControl } from '@material-ui/core/';
|
||||
import { FormikAutocomplete } from '../common/FormikAutocomplete';
|
||||
import _, { get, set, values } from 'lodash';
|
||||
import { DropDownListProps, JitRequest } from './structs';
|
||||
import { useStyles } from './styles';
|
||||
import { submitJustInTimeAccessRequest } from './utils';
|
||||
import { ReviewsComponent } from './ReviewsComponent';
|
||||
import { RequestsComponent } from './RequestsComponent';
|
||||
import {
|
||||
RESOURCE_TYPE_LIST,
|
||||
RESOURCE_ACTION_MAP,
|
||||
CUSTOM_RESOURCE_ACTION_LIST_MAP,
|
||||
AWS_CUSTOM_RESOURCE_TYPE,
|
||||
} from './constants';
|
||||
|
||||
const JustInTimeAccessPage = () => {
|
||||
const [jit, setJit] = useState<JitRequest>({
|
||||
team: '',
|
||||
vertical: '',
|
||||
environment: '',
|
||||
resourceType: '',
|
||||
resourceAction: '',
|
||||
awsResourceType: '',
|
||||
awsResourceNames: [''],
|
||||
justification: '',
|
||||
grantWindow: 1,
|
||||
grantAt: 0,
|
||||
});
|
||||
const [verticalList, setVerticalList] = useState<Array<string>>([]);
|
||||
const [environmentList, setEnvironmentList] = useState<Array<string>>([]);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
let isResourceTypeAWSCustom = false;
|
||||
let resourceActionList = Array<string>();
|
||||
let awsCustomResourceTypeList = Array<string>();
|
||||
|
||||
const teamList = useSelector((state: RootState) => state.initial.teamList);
|
||||
|
||||
const fetchAllVerticals = () => {
|
||||
httpClient(`/api/vertical`)
|
||||
.then(response => response.json())
|
||||
.then(verticals => {
|
||||
setVerticalList(verticals);
|
||||
})
|
||||
.catch(console.log);
|
||||
return [];
|
||||
};
|
||||
|
||||
const fetchAllEnvironments = () => {
|
||||
httpClient(`/api/environment`)
|
||||
.then(response => response.json())
|
||||
.then(environments => {
|
||||
setEnvironmentList(environments);
|
||||
})
|
||||
.catch(console.log);
|
||||
return [];
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
fetchAllVerticals();
|
||||
fetchAllEnvironments();
|
||||
}, []);
|
||||
|
||||
const DropDownList: React.FC<DropDownListProps> = ({ label, fieldName, list, style }) => {
|
||||
const classes = useStyles();
|
||||
const { values }: { values: any } = useFormikContext();
|
||||
const resourceTypeField = 'resourceType';
|
||||
|
||||
if (typeof getIn(values, resourceTypeField) !== undefined && values[resourceTypeField] !== '') {
|
||||
if (values[resourceTypeField] === 'AWS-CUSTOM') {
|
||||
resourceActionList = CUSTOM_RESOURCE_ACTION_LIST_MAP[values['awsResourceType']];
|
||||
awsCustomResourceTypeList = AWS_CUSTOM_RESOURCE_TYPE;
|
||||
isResourceTypeAWSCustom = true;
|
||||
} else {
|
||||
isResourceTypeAWSCustom = false;
|
||||
}
|
||||
resourceActionList = RESOURCE_ACTION_MAP[values[resourceTypeField]];
|
||||
// }
|
||||
}
|
||||
|
||||
return (
|
||||
<FormControl className={classes.field}>
|
||||
<InputLabel shrink>{label}</InputLabel>
|
||||
<Field
|
||||
name={fieldName}
|
||||
component={FormikAutocomplete}
|
||||
getOptionSelected={(option, value) => option === value || value === ''}
|
||||
getOptionLabel={(option: string) => (_.isString(option) ? option : '')}
|
||||
options={list}
|
||||
autoSelect
|
||||
className={style}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header name="Just In Time Access" />
|
||||
<Grid item direction="row" className={classes.gridRow} justifyContent="space-evenly">
|
||||
<Grid item direction="column" justifyContent="flex-start">
|
||||
<Card className={classes.bigCard}>
|
||||
<Formik
|
||||
enableReinitialize
|
||||
initialValues={jit}
|
||||
validateOnChange={true}
|
||||
validationSchema={justInTimeAccessValidationSchema}
|
||||
onSubmit={values => {
|
||||
values.grantAt = new Date(values.grantAt).getTime();
|
||||
const tempList = values.awsResourceNames.toString();
|
||||
values.awsResourceNames = tempList.split(',');
|
||||
submitJustInTimeAccessRequest(values);
|
||||
}}
|
||||
>
|
||||
{({ resetForm, handleSubmit, isSubmitting, setFieldValue }) => {
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<br />
|
||||
<Typography variant="h6" className={classes.leftHeading}>
|
||||
{' '}
|
||||
{'New Request'}{' '}
|
||||
</Typography>
|
||||
<Grid container direction="column" alignItems="center">
|
||||
<DropDownList
|
||||
label="Resource Owning Team"
|
||||
fieldName="team"
|
||||
list={teamList.map((team: any) => team.name)}
|
||||
style={classes.field}
|
||||
/>
|
||||
<DropDownList
|
||||
label="Vertical"
|
||||
fieldName="vertical"
|
||||
list={verticalList.filter((vertical: string) => vertical !== 'ALL')}
|
||||
style={classes.field}
|
||||
/>
|
||||
<DropDownList
|
||||
label="Environment"
|
||||
fieldName="environment"
|
||||
list={environmentList}
|
||||
style={classes.field}
|
||||
/>
|
||||
<DropDownList
|
||||
label="Resource Type"
|
||||
fieldName="resourceType"
|
||||
list={RESOURCE_TYPE_LIST}
|
||||
style={classes.field}
|
||||
/>
|
||||
{isResourceTypeAWSCustom ? (
|
||||
<DropDownList
|
||||
label="AWS Resource Type"
|
||||
fieldName="awsResourceType"
|
||||
list={AWS_CUSTOM_RESOURCE_TYPE}
|
||||
style={classes.field}
|
||||
/>
|
||||
) : null}
|
||||
<DropDownList
|
||||
label="Resource Action"
|
||||
fieldName="resourceAction"
|
||||
list={resourceActionList}
|
||||
style={classes.field}
|
||||
/>
|
||||
{isResourceTypeAWSCustom ? (
|
||||
<FormikTextField
|
||||
multiline
|
||||
label="AWS Resource Names"
|
||||
className={classes.field}
|
||||
name="awsResourceNames"
|
||||
id="awsResourceNames"
|
||||
/>
|
||||
) : null}
|
||||
<FormikTextField
|
||||
multiline
|
||||
className={classes.field}
|
||||
label="Justification for access"
|
||||
name="justification"
|
||||
/>
|
||||
<FormikTextField
|
||||
name="grantWindow"
|
||||
id="grantWindow"
|
||||
label="Grant Window"
|
||||
type="number"
|
||||
className={classes.field}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
/>
|
||||
<FormikTextField
|
||||
name="grantAt"
|
||||
id="grantAt"
|
||||
label="Start Grant At"
|
||||
type="datetime-local"
|
||||
className={classes.field}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
className={classes.submitButton}
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Grid>
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item direction="column" justifyContent="flex-end">
|
||||
<RequestsComponent />
|
||||
<ReviewsComponent />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withCookies(JustInTimeAccessPage);
|
||||
@@ -0,0 +1,16 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
export const justInTimeAccessValidationSchema = yup.object({
|
||||
team: yup.string().required('is Required'),
|
||||
vertical: yup.string().required('is Required'),
|
||||
environment: yup.string().required('is Required'),
|
||||
resourceType: yup.string().required('is Required'),
|
||||
resourceAction: yup.string().required('is Required'),
|
||||
justification: yup.string().required('is Required'),
|
||||
grantWindow: yup
|
||||
.number()
|
||||
.required('is Required')
|
||||
.positive('should be positive')
|
||||
.integer('should be integer')
|
||||
.max(24, 'should be less than or equal to 24'),
|
||||
});
|
||||
151
src/components/just-in-time-access/RequestsComponent.tsx
Normal file
151
src/components/just-in-time-access/RequestsComponent.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import {
|
||||
Button,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
Container,
|
||||
Typography,
|
||||
CardContent,
|
||||
} from '@material-ui/core';
|
||||
import { Clear } from '@material-ui/icons';
|
||||
import { TableRow, Paper, Table, TableBody, Card } from '@material-ui/core/';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import LoadingScreen from '../common/LoadingScreen';
|
||||
import { RequestsInfo, RequestTableContent } from './structs';
|
||||
import { useStyles } from './styles';
|
||||
import { renderResourceTypeAndAction, renderGrantDuration, reviewRequest } from './utils';
|
||||
|
||||
export const RequestsComponent = () => {
|
||||
const [requests, setRequests] = React.useState<RequestsInfo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`/api/jit/list/requests`)
|
||||
.then(response => response.json())
|
||||
.then(setRequests)
|
||||
.then(() => setLoading(false))
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const tableContent: RequestTableContent[] = [
|
||||
{
|
||||
key: '',
|
||||
value: () => '',
|
||||
},
|
||||
{
|
||||
key: 'ID',
|
||||
value: ({ id }: RequestsInfo) => id,
|
||||
},
|
||||
{
|
||||
key: 'Vertical',
|
||||
value: ({ vertical }: RequestsInfo) => vertical,
|
||||
},
|
||||
{
|
||||
key: 'Team',
|
||||
value: ({ team }: RequestsInfo) => team,
|
||||
},
|
||||
{
|
||||
key: 'Environment',
|
||||
value: ({ environment }: RequestsInfo) => environment,
|
||||
},
|
||||
{
|
||||
key: 'Requested For',
|
||||
value: ({ resourceType, awsResourceType, resourceAction, awsResourceNames }: RequestsInfo) =>
|
||||
renderResourceTypeAndAction(
|
||||
resourceType,
|
||||
awsResourceType,
|
||||
resourceAction,
|
||||
awsResourceNames,
|
||||
classes,
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'Justification',
|
||||
value: ({ justification }: RequestsInfo) => justification,
|
||||
},
|
||||
{
|
||||
key: 'Grant Duration',
|
||||
value: ({ grantAt, grantWindow }: RequestsInfo) =>
|
||||
renderGrantDuration(grantAt, grantWindow, classes),
|
||||
},
|
||||
{
|
||||
key: 'Status',
|
||||
value: ({ status }: RequestsInfo) => status,
|
||||
},
|
||||
{
|
||||
key: 'Close',
|
||||
value: ({ id, status }: RequestsInfo) => (
|
||||
<div className={classes.buttonContainer}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.clearButton}
|
||||
startIcon={<Clear />}
|
||||
onClick={() => {
|
||||
reviewRequest(id, 'close');
|
||||
}}
|
||||
disabled={status === 'APPROVED' || status === 'REJECTED' || status === 'CLOSED'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const renderHeaderRows = () =>
|
||||
tableContent.map(header => <TableCell key={header.key}>{header.key}</TableCell>);
|
||||
|
||||
const renderBodyRows = () =>
|
||||
requests.map((row: RequestsInfo) => (
|
||||
<TableRow key={row.id}>
|
||||
{tableContent.map(header => (
|
||||
<TableCell key={header.key}>{header.value(row)}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
));
|
||||
|
||||
const renderTable = () => (
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label="table">
|
||||
<TableHead>
|
||||
<TableRow>{renderHeaderRows()}</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{renderBodyRows()}</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
const showNoResult = () => (
|
||||
<Container component="main" className={classes.main} maxWidth="sm">
|
||||
<Typography variant="h2" component="h1" gutterBottom>
|
||||
{`No requests found.`}
|
||||
</Typography>
|
||||
<Typography variant="h5" component="h2" gutterBottom>
|
||||
{'No requests found'}
|
||||
{'Get Started with JIT. Refer https://navihq.atlassian.net/wiki/x/4gIZNg'}
|
||||
</Typography>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const showRequestsTable = () => {
|
||||
if (loading) return <LoadingScreen containerClass={classes.main} />;
|
||||
if (!requests || requests.length === 0) return showNoResult();
|
||||
return renderTable();
|
||||
};
|
||||
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<>
|
||||
<Card className={classes.halfCardTop}>
|
||||
<CardContent>
|
||||
<div>
|
||||
<Typography variant="h6" className={classes.heading}>
|
||||
{' '}
|
||||
{'My Accesses'}{' '}
|
||||
</Typography>
|
||||
{showRequestsTable()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
157
src/components/just-in-time-access/ReviewsComponent.tsx
Normal file
157
src/components/just-in-time-access/ReviewsComponent.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
Button,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
Container,
|
||||
Typography,
|
||||
CardContent,
|
||||
} from '@material-ui/core';
|
||||
import { Check, Clear } from '@material-ui/icons';
|
||||
import { TableRow, Paper, Table, TableBody, Card } from '@material-ui/core/';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import LoadingScreen from '../common/LoadingScreen';
|
||||
import { ReviewsInfo, ReviewsTableContent } from './structs';
|
||||
import { useStyles } from './styles';
|
||||
import { renderResourceTypeAndAction, renderGrantDuration, reviewRequest } from './utils';
|
||||
|
||||
export const ReviewsComponent = () => {
|
||||
const [requests, setRequests] = React.useState<ReviewsInfo[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(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) => (
|
||||
<div className={classes.buttonContainer}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={classes.checkButton}
|
||||
startIcon={<Check />}
|
||||
onClick={() => reviewRequest(jitApprovalId, 'approve')}
|
||||
disabled={status !== 'PENDING'}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.clearButton}
|
||||
startIcon={<Clear />}
|
||||
onClick={() => reviewRequest(jitApprovalId, 'reject')}
|
||||
disabled={status !== 'PENDING'}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const renderHeaderRows = () =>
|
||||
tableContent.map(header => <TableCell key={header.key}>{header.key}</TableCell>);
|
||||
|
||||
const renderBodyRows = () =>
|
||||
requests.map((row: ReviewsInfo) => (
|
||||
<TableRow key={row.jitApprovalId}>
|
||||
{tableContent.map(header => (
|
||||
<TableCell key={header.key}>{header.value(row)}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
));
|
||||
|
||||
const renderTable = () => (
|
||||
<TableContainer component={Paper}>
|
||||
<Table aria-label="table">
|
||||
<TableHead>
|
||||
<TableRow>{renderHeaderRows()}</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>{renderBodyRows()}</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
const showNoResult = () => (
|
||||
<Container component="main" className={classes.main} maxWidth="sm">
|
||||
<Typography variant="h2" component="h1" gutterBottom>
|
||||
{`No reviews found`}
|
||||
</Typography>
|
||||
<Typography variant="h5" component="h2" gutterBottom>
|
||||
{"If you're not receiving any reviews, please get TEAM_JITREVIEWER role."}
|
||||
{'Get Started with JIT. Refer https://navihq.atlassian.net/wiki/x/4gIZNg'}
|
||||
</Typography>
|
||||
</Container>
|
||||
);
|
||||
|
||||
const showReviewsTable = () => {
|
||||
if (loading) return <LoadingScreen containerClass={classes.main} />;
|
||||
if (!requests || requests.length === 0) return showNoResult();
|
||||
return renderTable();
|
||||
};
|
||||
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<>
|
||||
<Card className={classes.halfCardBottom}>
|
||||
<CardContent>
|
||||
<div>
|
||||
<Typography variant="h6" className={classes.heading}>
|
||||
{' '}
|
||||
{'Reviews'}{' '}
|
||||
</Typography>
|
||||
{showReviewsTable()}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
31
src/components/just-in-time-access/constants.tsx
Normal file
31
src/components/just-in-time-access/constants.tsx
Normal file
@@ -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'],
|
||||
};
|
||||
62
src/components/just-in-time-access/structs.tsx
Normal file
62
src/components/just-in-time-access/structs.tsx
Normal file
@@ -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<string>;
|
||||
justification: string;
|
||||
grantWindow: number;
|
||||
grantAt: number;
|
||||
};
|
||||
|
||||
export type RequestsInfo = {
|
||||
id: number;
|
||||
team: string;
|
||||
vertical: string;
|
||||
environment: string;
|
||||
resourceType: string;
|
||||
awsResourceType: string;
|
||||
awsResourceNames: Array<string>;
|
||||
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<string>;
|
||||
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;
|
||||
};
|
||||
104
src/components/just-in-time-access/styles.tsx
Normal file
104
src/components/just-in-time-access/styles.tsx
Normal file
@@ -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',
|
||||
},
|
||||
}));
|
||||
77
src/components/just-in-time-access/utils.tsx
Normal file
77
src/components/just-in-time-access/utils.tsx
Normal file
@@ -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 (
|
||||
<div className={classes.grantDuration}>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderResourceTypeAndAction = (
|
||||
resourceType: string,
|
||||
awsResourceType: string,
|
||||
resourceAction: string,
|
||||
awsResourceNames: Array<string>,
|
||||
classes: ClassNameMap,
|
||||
): React.ReactElement => {
|
||||
const message =
|
||||
resourceType === 'AWS-CUSTOM'
|
||||
? `${resourceType} - ${awsResourceType} with ${resourceAction} on ${awsResourceNames.join(
|
||||
', ',
|
||||
)}`
|
||||
: `${resourceType} - ${resourceAction}`;
|
||||
|
||||
return (
|
||||
<div className={classes.resourceTypeAndAction}>
|
||||
<div>{message}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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');
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -28,6 +28,9 @@ const PortalMenu = props => {
|
||||
<MenuItem component={Link} onClick={props.handleClose} to="/generateToken">
|
||||
App Access
|
||||
</MenuItem>
|
||||
<MenuItem component={Link} onClick={props.handleClose} to="/justInTimeAccess">
|
||||
Just In Time Access
|
||||
</MenuItem>
|
||||
<MenuItem component={Link} onClick={props.handleClose} to="/databaseAccessToken">
|
||||
Database Access
|
||||
</MenuItem>
|
||||
|
||||
Reference in New Issue
Block a user