INFRA-4031 | Ankit Bhardwaj | add new bucket policy fields (#728)

* INFRA-4031 | Ankit Bhardwaj | add new buckey policy fiels

* INFRA-4031 | Ankit Bhardwaj | add old s3_bucket_policies

* INFRA-4031 | Ankit Bhardwaj | add conditional render of s3_bucket policy
This commit is contained in:
Ankit Bhardwaj Bhardwaj
2024-12-25 01:54:21 +05:30
committed by GitHub
parent 330d67b096
commit 5b3b802c5d
6 changed files with 228 additions and 11 deletions

View File

@@ -22,6 +22,7 @@ export const FormikTable = (props: {
headers: Array<string>;
className?: string;
children: Function;
tableRowClass?: string;
newItem: Function;
label?: string;
addOnClick?: string;
@@ -29,8 +30,18 @@ export const FormikTable = (props: {
deleteAllButton?: boolean;
disableAdd?: boolean;
}) => {
const { name, headers, newItem, label, children, className, addOnClick, reverse, disableAdd } =
props;
const {
name,
headers,
newItem,
label,
children,
className,
tableRowClass,
addOnClick,
reverse,
disableAdd,
} = props;
const isReverse = (values: Array<any>) => {
if (reverse) {
return values.reverse();
@@ -41,7 +52,7 @@ export const FormikTable = (props: {
const classes = makeStyles({
deleteButton: {
marginLeft: 210,
marginTop: 20,
padding: 0,
},
})();
@@ -77,11 +88,14 @@ export const FormikTable = (props: {
<TableHead>
<TableRow>
{headers.map((header, i) => (
<TableCell key={i}>{header}</TableCell>
<TableCell className={tableRowClass} key={i}>
<b>{header}</b>
</TableCell>
))}
<TableCell align="right">
<TableCell className={tableRowClass} align="right">
{' '}
<IconButton
className={tableRowClass}
onClick={() =>
addOnClick == 'push' ? push(newItem()) : insert(0, newItem())
}
@@ -98,15 +112,16 @@ export const FormikTable = (props: {
getIn(form.values, name, []).map((_, i) => (
<TableRow key={i}>
{children(i, form.values, insert, form.setFieldValue, push)}
<TableCell>
<TableCell className={tableRowClass}>
<IconButton
className={tableRowClass}
onClick={async () => {
await remove(i);
form.setFieldTouched(`environmentVariables.0.name`, true, true);
}}
>
{' '}
<DeleteIcon />{' '}
<DeleteIcon className={tableRowClass} />{' '}
</IconButton>
</TableCell>
</TableRow>

View File

@@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import MenuItem from '@material-ui/core/MenuItem';
import { Grid, Typography, Box, FormControlLabel, TableCell } from '@material-ui/core';
import { Grid, Typography, Box, FormControlLabel, TableCell, Radio } from '@material-ui/core';
import { useField, useFormikContext } from 'formik';
import * as manifest from '../../models/Manifest';
import { FormikTextField } from '../../components/common/FormikTextField';
@@ -24,9 +24,20 @@ import {
getBucketsDeploymentStatus,
S3StorageClassOptions,
disasterRecovery,
newBucketPolicyEnvs,
} from './constants';
import { useStyles } from './useStyles';
import { path } from '../../models/Manifest';
import {
newBucketPolicyStatement,
newS3BucketPolicyAction,
newS3BucketPolicyItem,
newS3BucketPolicyPrincipal,
path,
} from '../../models/Manifest';
import * as _m from '@src/models/Manifest';
import { FormikRadioGroup } from '@components/common/FormikRadioGroup';
import { values } from 'lodash';
import { elasticCacheAlertFields } from '@src/coreform/elasticcache/constant';
const PolicyBox = (props: {
policyHeading: string;
@@ -206,6 +217,13 @@ const BucketMetadata = (props: {
);
};
interface S3BucketPolicyTableProps {
policy: string;
type: 'actions' | 'resource' | 'principal';
header: string;
newItem: any;
}
const S3BucketPolicy = (props: { s3Bucket: string }): React.ReactNode => {
const classes = useStyles();
const { s3Bucket } = props;
@@ -233,6 +251,109 @@ const S3BucketPolicy = (props: { s3Bucket: string }): React.ReactNode => {
);
};
const S3BucketPolicyTable = ({ policy, type, header, newItem }: S3BucketPolicyTableProps) => {
const classes = useStyles();
return (
<FormikTable
name={`${policy}.${type}`}
headers={[header]}
newItem={newItem}
className={classes.policyStatement}
tableRowClass={classes.policyStatementTableRow}
>
{i => (
<TableCell>
<FormikTextField
name={`${policy}.${type}.${i}`}
className={classes.policyStatementTableField}
/>
</TableCell>
)}
</FormikTable>
);
};
const getIsPIIBucket = (values, bucketIndex) => {
const bucketDataSensitivity =
values?.extraResources?.s3_buckets?.[bucketIndex]?.metadata?.DataSensitivity;
const globalDataSensitivity = values?.metadata?.dataSensitivity;
return (
bucketDataSensitivity === 'PII_SPI' ||
(bucketDataSensitivity === undefined && globalDataSensitivity === 'PII_SPI')
);
};
const BucketPolicy = (props: { s3Bucket: string; bucketIndex: number }): React.ReactNode => {
const classes = useStyles();
const { s3Bucket, bucketIndex } = props;
const { values }: { values } = useFormikContext();
const isPIIBucket = getIsPIIBucket(values, bucketIndex);
return (
<CardLayout heading="Bucket Policy" expanded={false}>
<Grid container direction="column" className={classes.policyStatement}>
<FormikCardList
name={`${s3Bucket}.bucketPolicyStatements`}
newItem={_m.newBucketPolicyStatement}
newItemLabel="Add new Policy Statement"
>
{i => (
<>
<div>
{!isPIIBucket ? (
<FormikRadioGroup name={`${s3Bucket}.bucketPolicyStatements.${i}.effect`} row>
<FormControlLabel
disabled={isPIIBucket}
value={'Allow'}
control={<Radio />}
label="Allow"
/>
<FormControlLabel
disabled={isPIIBucket}
value={'Deny'}
control={<Radio />}
label="Deny"
/>
</FormikRadioGroup>
) : null}
{isPIIBucket ? (
<FormControlLabel
control={
<FormikCheckbox
disabled={!isPIIBucket}
name={`${s3Bucket}.bucketPolicyStatements.${i}.crossAccount`}
label="Cross Account"
/>
}
label="Cross account role"
/>
) : null}
</div>
<S3BucketPolicyTable
policy={`${s3Bucket}.bucketPolicyStatements.${i}`}
type="principal"
header="Principal"
newItem={_m.newS3BucketPolicyItem}
/>
<S3BucketPolicyTable
policy={`${s3Bucket}.bucketPolicyStatements.${i}`}
type="actions"
header="Actions"
newItem={_m.newS3BucketPolicyItem}
/>
<S3BucketPolicyTable
policy={`${s3Bucket}.bucketPolicyStatements.${i}`}
type="resource"
header="Resources"
newItem={_m.newS3BucketPolicyItem}
/>
</>
)}
</FormikCardList>
</Grid>
</CardLayout>
);
};
const CorsPolicy = (props: { s3Bucket: string }): React.ReactNode => {
const { s3Bucket } = props;
return (
@@ -323,7 +444,11 @@ const S3BucketForm = (): React.ReactNode => {
/>
<LifecycleRules s3Bucket={`extraResources.s3_buckets.${i}`} />
<CorsPolicy s3Bucket={`extraResources.s3_buckets.${i}`} />
<S3BucketPolicy s3Bucket={`extraResources.s3_buckets.${i}`} />
{newBucketPolicyEnvs.has(values.environment) ? (
<BucketPolicy s3Bucket={`extraResources.s3_buckets.${i}`} bucketIndex={i} />
) : (
<S3BucketPolicy s3Bucket={`extraResources.s3_buckets.${i}`} />
)}
<FormControlLabel
control={
<FormikCheckbox name={`extraResources.s3_buckets.${i}.enableAccessLog`} />

View File

@@ -28,3 +28,12 @@ export function getBucketsDeploymentStatus(Value: any): boolean[] {
}
return status;
}
export const newBucketPolicyEnvs = new Set([
'local',
'perf',
'dev',
'qa',
'data-platform-nonprod',
'uat',
]);

View File

@@ -29,4 +29,17 @@ export const useStyles = makeStyles({
width: 450,
height: 280,
},
policyStatement: {
width: 450,
padding: 0,
},
policyStatementTableRow: {
height: 17,
padding: 1,
},
policyStatementTableField: {
width: 375,
height: 20,
padding: 1,
},
});

View File

@@ -546,10 +546,24 @@ export const newAwsAccessPolicy = () => {
};
};
export const newBucketPolicyStatement = () => {
return {
effect: 'Allow',
crossAccount: false,
actions: [],
principal: [],
resource: [],
};
};
export const newAwsAccessPolicyAction = () => {
return '';
};
export const newS3BucketPolicyItem = () => {
return '';
};
export const hasS3Bucket = (manifest: any) => {
return has(manifest, path.s3Buckets);
};
@@ -1000,6 +1014,9 @@ export const newSecondaryRegionReadReplica = () => {
export const newS3Bucket = () => {
return {
anonymizedBucketName: '',
metadata: {
DataSensitivity: 'Internal',
},
bucketTag: '',
lifecycleRules: [],
corsPolicy: [],

View File

@@ -30,6 +30,7 @@ function isS3WildcardAction(action: string | string[]): boolean {
function createContextError(context: any, message: string): boolean {
return context.createError({ message });
}
function isPrincipalRestrictive(principal: any): boolean {
return principal === '*' || principal?.AWS === '*' || principal?.AWS?.includes('*');
}
@@ -94,6 +95,15 @@ const validateExpirationDays = (value?: number | null | undefined, context?: any
}
};
const validateS3BucketPolicyStatements = (value: any, context: any): boolean => {
if (value.effect === 'Deny' && value.actions.includes('s3:*') && value.principal.includes('*')) {
return context.createError({
message: 'Bucket policy is too restrictive, denying all actions for all resources.',
});
}
return true;
};
export const s3BucketsValidationSchema = yup
.array()
.of(
@@ -105,6 +115,34 @@ export const s3BucketsValidationSchema = yup
name: 'bucketPolicy',
test: validateS3BucketPolicy,
}),
bucketPolicyStatements: yup.array().of(
yup
.object({
effect: yup.string().required('is required'),
actions: yup.array().of(yup.string().required('can not be empty')).min(1),
resource: yup.array().of(yup.string().required('can not be empty')).min(1),
principal: yup
.array()
.of(yup.string().required('can not be empty'))
.min(1)
.test(
'validate-principal-based-on-metadata',
'Only one principal is allowed for PII_SPI bucket',
function (principal) {
const parent = this.from[1].value;
if (parent.metadata?.DataSensitivity === 'PII_SPI') {
return principal.length <= 1;
}
return true;
},
),
})
.test(
'is-too-restrictive',
'Bucket policy is too restrictive, denying all actions for all resources.',
validateS3BucketPolicyStatements,
),
),
anonymizedBucketName: yup
.string()
.required('is required')
@@ -123,7 +161,7 @@ export const s3BucketsValidationSchema = yup
.nullable(),
metadata: yup
.object({
DataSensitivity: yup.string(),
DataSensitivity: yup.string().required('is required'),
DisasterRecovery: yup.string(),
})
.nullable(),