644 lines
20 KiB
TypeScript
644 lines
20 KiB
TypeScript
// Copyright (c) 2021 MinIO, Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import React, { Fragment, useEffect, useState } from "react";
|
|
import { IAMPolicy, IAMStatement, Policy } from "./types";
|
|
import { connect } from "react-redux";
|
|
import { Theme } from "@mui/material/styles";
|
|
import createStyles from "@mui/styles/createStyles";
|
|
import withStyles from "@mui/styles/withStyles";
|
|
import {
|
|
actionsTray,
|
|
containerForHeader,
|
|
modalBasic,
|
|
searchField,
|
|
} from "../Common/FormComponents/common/styleLibrary";
|
|
import Paper from "@mui/material/Paper";
|
|
import Grid from "@mui/material/Grid";
|
|
import { Button, LinearProgress, Tooltip } from "@mui/material";
|
|
import TableWrapper from "../Common/TableWrapper/TableWrapper";
|
|
import api from "../../../common/api";
|
|
import PageHeader from "../Common/PageHeader/PageHeader";
|
|
import DeletePolicy from "./DeletePolicy";
|
|
import { Link } from "react-router-dom";
|
|
import { setErrorSnackMessage, setSnackBarMessage } from "../../../actions";
|
|
import { ErrorResponseHandler } from "../../../common/types";
|
|
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
|
|
import history from "../../../history";
|
|
import InputAdornment from "@mui/material/InputAdornment";
|
|
import TextField from "@mui/material/TextField";
|
|
import ListItem from "@mui/material/ListItem";
|
|
import ListItemText from "@mui/material/ListItemText";
|
|
import List from "@mui/material/List";
|
|
import ScreenTitle from "../Common/ScreenTitle/ScreenTitle";
|
|
import IAMPoliciesIcon from "../../../icons/IAMPoliciesIcon";
|
|
import RefreshIcon from "../../../icons/RefreshIcon";
|
|
import SearchIcon from "../../../icons/SearchIcon";
|
|
import TrashIcon from "../../../icons/TrashIcon";
|
|
import BoxIconButton from "../Common/BoxIconButton";
|
|
|
|
interface IPolicyDetailsProps {
|
|
classes: any;
|
|
match: any;
|
|
setErrorSnackMessage: typeof setErrorSnackMessage;
|
|
setSnackBarMessage: typeof setSnackBarMessage;
|
|
}
|
|
|
|
const styles = (theme: Theme) =>
|
|
createStyles({
|
|
buttonContainer: {
|
|
textAlign: "right",
|
|
},
|
|
multiContainer: {
|
|
display: "flex",
|
|
alignItems: "center" as const,
|
|
justifyContent: "flex-start" as const,
|
|
},
|
|
sizeFactorContainer: {
|
|
marginLeft: 8,
|
|
},
|
|
containerHeader: {
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
},
|
|
paperContainer: {
|
|
padding: "15px 15px 15px 50px",
|
|
},
|
|
infoGrid: {
|
|
display: "grid",
|
|
gridTemplateColumns: "auto auto auto auto",
|
|
gridGap: 8,
|
|
"& div": {
|
|
display: "flex",
|
|
alignItems: "center",
|
|
},
|
|
"& div:nth-child(odd)": {
|
|
justifyContent: "flex-end",
|
|
fontWeight: 700,
|
|
},
|
|
"& div:nth-child(2n)": {
|
|
paddingRight: 35,
|
|
},
|
|
},
|
|
masterActions: {
|
|
width: "25%",
|
|
minWidth: "120px",
|
|
"& div": {
|
|
margin: "5px 0px",
|
|
},
|
|
},
|
|
updateButton: {
|
|
backgroundColor: "transparent",
|
|
border: 0,
|
|
padding: "0 6px",
|
|
cursor: "pointer",
|
|
"&:focus, &:active": {
|
|
outline: "none",
|
|
},
|
|
"& svg": {
|
|
height: 12,
|
|
},
|
|
},
|
|
noUnderLine: {
|
|
textDecoration: "none",
|
|
},
|
|
poolLabel: {
|
|
color: "#666666",
|
|
},
|
|
licenseContainer: {
|
|
position: "relative",
|
|
padding: "20px 52px 0px 28px",
|
|
background: "#032F51",
|
|
boxShadow: "0px 3px 7px #00000014",
|
|
"& h2": {
|
|
color: "#FFF",
|
|
marginBottom: 67,
|
|
},
|
|
"& a": {
|
|
textDecoration: "none",
|
|
},
|
|
"& h3": {
|
|
color: "#FFFFFF",
|
|
marginBottom: "30px",
|
|
fontWeight: "bold",
|
|
},
|
|
"& h6": {
|
|
color: "#FFFFFF !important",
|
|
},
|
|
},
|
|
licenseInfo: { color: "#FFFFFF", position: "relative" },
|
|
licenseInfoTitle: {
|
|
textTransform: "none",
|
|
color: "#BFBFBF",
|
|
fontSize: 11,
|
|
},
|
|
licenseInfoValue: {
|
|
textTransform: "none",
|
|
fontSize: 14,
|
|
fontWeight: "bold",
|
|
},
|
|
verifiedIcon: {
|
|
width: 96,
|
|
position: "absolute",
|
|
right: 0,
|
|
bottom: 29,
|
|
},
|
|
breadcrumLink: {
|
|
textDecoration: "none",
|
|
color: "black",
|
|
},
|
|
statement: {
|
|
border: "1px solid #DADADA",
|
|
padding: 8,
|
|
marginBottom: 8,
|
|
borderRadius: 4,
|
|
},
|
|
labelCol: {
|
|
fontWeight: "bold",
|
|
},
|
|
statementActions: {
|
|
textAlign: "right",
|
|
},
|
|
addStmt: {
|
|
color: theme.palette.primary.main,
|
|
},
|
|
listBox: {
|
|
border: "1px solid #DADADA",
|
|
height: 100,
|
|
},
|
|
...actionsTray,
|
|
...searchField,
|
|
...modalBasic,
|
|
...containerForHeader(theme.spacing(4)),
|
|
});
|
|
|
|
const PolicyDetails = ({
|
|
classes,
|
|
match,
|
|
setErrorSnackMessage,
|
|
setSnackBarMessage,
|
|
}: IPolicyDetailsProps) => {
|
|
const [selectedTab, setSelectedTab] = useState<number>(0);
|
|
const [policy, setPolicy] = useState<Policy | null>(null);
|
|
const [policyStatements, setPolicyStatements] = useState<IAMStatement[]>([]);
|
|
const [userList, setUserList] = useState<string[]>([]);
|
|
const [groupList, setGroupList] = useState<string[]>([]);
|
|
const [addLoading, setAddLoading] = useState<boolean>(false);
|
|
const [policyName, setPolicyName] = useState<string>(match.params[0]);
|
|
const [policyDefinition, setPolicyDefinition] = useState<string>("");
|
|
const [loadingPolicy, setLoadingPolicy] = useState<boolean>(true);
|
|
const [filterUsers, setFilterUsers] = useState<string>("");
|
|
const [loadingUsers, setLoadingUsers] = useState<boolean>(true);
|
|
const [filterGroups, setFilterGroups] = useState<string>("");
|
|
const [loadingGroups, setLoadingGroups] = useState<boolean>(true);
|
|
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
|
|
|
const saveRecord = (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
if (addLoading) {
|
|
return;
|
|
}
|
|
setAddLoading(true);
|
|
api
|
|
.invoke("POST", "/api/v1/policies", {
|
|
name: policyName,
|
|
policy: policyDefinition,
|
|
})
|
|
.then((_) => {
|
|
setAddLoading(false);
|
|
setSnackBarMessage("Policy successfully updated");
|
|
})
|
|
.catch((err: ErrorResponseHandler) => {
|
|
setAddLoading(false);
|
|
setErrorSnackMessage(err);
|
|
});
|
|
};
|
|
|
|
useEffect(() => {
|
|
const loadUsersForPolicy = () => {
|
|
if (loadingUsers) {
|
|
api
|
|
.invoke(
|
|
"GET",
|
|
`/api/v1/policies/${encodeURIComponent(policyName)}/users`
|
|
)
|
|
.then((result: any) => {
|
|
setUserList(result);
|
|
setLoadingUsers(false);
|
|
})
|
|
.catch((err: ErrorResponseHandler) => {
|
|
setErrorSnackMessage(err);
|
|
setLoadingUsers(false);
|
|
});
|
|
}
|
|
};
|
|
const loadGroupsForPolicy = () => {
|
|
if (loadingGroups) {
|
|
api
|
|
.invoke(
|
|
"GET",
|
|
`/api/v1/policies/${encodeURIComponent(policyName)}/groups`
|
|
)
|
|
.then((result: any) => {
|
|
setGroupList(result);
|
|
setLoadingGroups(false);
|
|
})
|
|
.catch((err: ErrorResponseHandler) => {
|
|
setErrorSnackMessage(err);
|
|
setLoadingGroups(false);
|
|
});
|
|
}
|
|
};
|
|
const loadPolicyDetails = () => {
|
|
if (loadingPolicy) {
|
|
api
|
|
.invoke(
|
|
"GET",
|
|
`/api/v1/policy?name=${encodeURIComponent(policyName)}`
|
|
)
|
|
.then((result: any) => {
|
|
if (result) {
|
|
setPolicy(result);
|
|
setPolicyDefinition(
|
|
result ? JSON.stringify(JSON.parse(result.policy), null, 4) : ""
|
|
);
|
|
const pol: IAMPolicy = JSON.parse(result.policy);
|
|
setPolicyStatements(pol.Statement);
|
|
}
|
|
setLoadingPolicy(false);
|
|
})
|
|
.catch((err: ErrorResponseHandler) => {
|
|
setErrorSnackMessage(err);
|
|
setLoadingPolicy(false);
|
|
});
|
|
}
|
|
};
|
|
|
|
if (loadingPolicy) {
|
|
loadPolicyDetails();
|
|
loadUsersForPolicy();
|
|
loadGroupsForPolicy();
|
|
}
|
|
}, [
|
|
policyName,
|
|
loadingPolicy,
|
|
loadingUsers,
|
|
loadingGroups,
|
|
setErrorSnackMessage,
|
|
setUserList,
|
|
setGroupList,
|
|
setPolicyDefinition,
|
|
setPolicy,
|
|
setLoadingUsers,
|
|
setLoadingGroups,
|
|
]);
|
|
|
|
const resetForm = () => {
|
|
setPolicyName("");
|
|
setPolicyDefinition("");
|
|
};
|
|
|
|
const validSave = policyName.trim() !== "";
|
|
|
|
const deletePolicy = () => {
|
|
setDeleteOpen(true);
|
|
};
|
|
|
|
const closeDeleteModalAndRefresh = (refresh: boolean) => {
|
|
setDeleteOpen(false);
|
|
history.push(`/policies`);
|
|
};
|
|
|
|
const userViewAction = (user: any) => {
|
|
history.push(`/users/${user}`);
|
|
};
|
|
const userTableActions = [{ type: "view", onClick: userViewAction }];
|
|
|
|
const filteredUsers = userList.filter((elementItem) =>
|
|
elementItem.includes(filterUsers)
|
|
);
|
|
|
|
const filteredGroups = groupList.filter((elementItem) =>
|
|
elementItem.includes(filterGroups)
|
|
);
|
|
|
|
return (
|
|
<Fragment>
|
|
{deleteOpen && (
|
|
<DeletePolicy
|
|
deleteOpen={deleteOpen}
|
|
selectedPolicy={policyName}
|
|
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
|
|
/>
|
|
)}
|
|
<PageHeader
|
|
label={
|
|
<Fragment>
|
|
<Link to={"/policies"} className={classes.breadcrumLink}>
|
|
Policy
|
|
</Link>
|
|
</Fragment>
|
|
}
|
|
/>
|
|
<Grid container className={classes.container}>
|
|
<Grid item xs={12}>
|
|
<ScreenTitle
|
|
icon={
|
|
<Fragment>
|
|
<IAMPoliciesIcon width={40} />
|
|
</Fragment>
|
|
}
|
|
title={policyName}
|
|
subTitle={<Fragment>IAM Policy</Fragment>}
|
|
actions={
|
|
<Fragment>
|
|
<Tooltip title="Delete Policy">
|
|
<BoxIconButton
|
|
color="primary"
|
|
aria-label="Delete Policy"
|
|
onClick={deletePolicy}
|
|
>
|
|
<TrashIcon />
|
|
</BoxIconButton>
|
|
</Tooltip>
|
|
|
|
<Tooltip title={"Refresh"}>
|
|
<BoxIconButton
|
|
color="primary"
|
|
aria-label="Refresh List"
|
|
onClick={() => {
|
|
setLoadingUsers(true);
|
|
setLoadingGroups(true);
|
|
setLoadingPolicy(true);
|
|
}}
|
|
size="large"
|
|
>
|
|
<RefreshIcon />
|
|
</BoxIconButton>
|
|
</Tooltip>
|
|
</Fragment>
|
|
}
|
|
/>
|
|
</Grid>
|
|
<Grid item xs={2}>
|
|
<List component="nav" dense={true}>
|
|
<ListItem
|
|
button
|
|
selected={selectedTab === 0}
|
|
onClick={() => {
|
|
setSelectedTab(0);
|
|
}}
|
|
>
|
|
<ListItemText primary="Summary" />
|
|
</ListItem>
|
|
<ListItem
|
|
button
|
|
selected={selectedTab === 1}
|
|
onClick={() => {
|
|
setSelectedTab(1);
|
|
}}
|
|
>
|
|
<ListItemText primary="Users" />
|
|
</ListItem>
|
|
<ListItem
|
|
button
|
|
selected={selectedTab === 2}
|
|
onClick={() => {
|
|
setSelectedTab(2);
|
|
}}
|
|
>
|
|
<ListItemText primary="Groups" />
|
|
</ListItem>
|
|
<ListItem
|
|
button
|
|
selected={selectedTab === 3}
|
|
onClick={() => {
|
|
setSelectedTab(3);
|
|
}}
|
|
>
|
|
<ListItemText primary="JSON" />
|
|
</ListItem>
|
|
</List>
|
|
</Grid>
|
|
<Grid item xs={10}>
|
|
{selectedTab === 0 && (
|
|
<Fragment>
|
|
<h1 className={classes.sectionTitle}>Summary</h1>
|
|
<Paper className={classes.paperContainer}>
|
|
<form
|
|
noValidate
|
|
autoComplete="off"
|
|
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
|
saveRecord(e);
|
|
}}
|
|
>
|
|
<Grid container>
|
|
<Grid item xs={8}>
|
|
<h4>Statements</h4>
|
|
</Grid>
|
|
<Grid item xs={4} />
|
|
|
|
<Fragment>
|
|
{policyStatements.map((stmt, i) => {
|
|
return (
|
|
<Grid
|
|
item
|
|
xs={12}
|
|
className={classes.statement}
|
|
key={`s-${i}`}
|
|
>
|
|
<Grid container>
|
|
<Grid item xs={2} className={classes.labelCol}>
|
|
Effect
|
|
</Grid>
|
|
<Grid item xs={4}>
|
|
<Fragment>{stmt.Effect}</Fragment>
|
|
</Grid>
|
|
<Grid item xs={2} className={classes.labelCol} />
|
|
<Grid item xs={4} />
|
|
<Grid item xs={2} className={classes.labelCol}>
|
|
Actions
|
|
</Grid>
|
|
<Grid item xs={4}>
|
|
<ul>
|
|
{stmt.Action &&
|
|
stmt.Action.map((act, actIndex) => (
|
|
<li key={`${i}-r-${actIndex}`}>{act}</li>
|
|
))}
|
|
</ul>
|
|
</Grid>
|
|
<Grid item xs={2} className={classes.labelCol}>
|
|
Resources
|
|
</Grid>
|
|
<Grid item xs={4}>
|
|
<ul>
|
|
{stmt.Resource &&
|
|
stmt.Resource.map((res, resIndex) => (
|
|
<li key={`${i}-r-${resIndex}`}>{res}</li>
|
|
))}
|
|
</ul>
|
|
</Grid>
|
|
</Grid>
|
|
</Grid>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
</Grid>
|
|
</form>
|
|
</Paper>
|
|
</Fragment>
|
|
)}
|
|
{selectedTab === 1 && (
|
|
<Fragment>
|
|
<h1 className={classes.sectionTitle}>Users</h1>
|
|
<Grid container>
|
|
<Grid item xs={12} className={classes.actionsTray}>
|
|
<TextField
|
|
placeholder="Search Users"
|
|
className={classes.searchField}
|
|
id="search-resource"
|
|
label=""
|
|
onChange={(val) => {
|
|
setFilterUsers(val.target.value);
|
|
}}
|
|
InputProps={{
|
|
disableUnderline: true,
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon />
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
variant="standard"
|
|
/>
|
|
</Grid>
|
|
<Grid item xs={12} className={classes.actionsTray}>
|
|
<br />
|
|
</Grid>
|
|
<TableWrapper
|
|
itemActions={userTableActions}
|
|
columns={[{ label: "Name", elementKey: "name" }]}
|
|
isLoading={loadingUsers}
|
|
records={filteredUsers}
|
|
entityName="Users"
|
|
idField="name"
|
|
/>
|
|
</Grid>
|
|
</Fragment>
|
|
)}
|
|
{selectedTab === 2 && (
|
|
<Fragment>
|
|
<h1 className={classes.sectionTitle}>Groups</h1>
|
|
<Grid container>
|
|
<Grid item xs={12} className={classes.actionsTray}>
|
|
<TextField
|
|
placeholder="Search Groups"
|
|
className={classes.searchField}
|
|
id="search-resource"
|
|
label=""
|
|
onChange={(val) => {
|
|
setFilterGroups(val.target.value);
|
|
}}
|
|
InputProps={{
|
|
disableUnderline: true,
|
|
startAdornment: (
|
|
<InputAdornment position="start">
|
|
<SearchIcon />
|
|
</InputAdornment>
|
|
),
|
|
}}
|
|
variant="standard"
|
|
/>
|
|
</Grid>
|
|
<Grid item xs={12} className={classes.actionsTray}>
|
|
<br />
|
|
</Grid>
|
|
<TableWrapper
|
|
itemActions={[]}
|
|
columns={[{ label: "Name", elementKey: "name" }]}
|
|
isLoading={loadingGroups}
|
|
records={filteredGroups}
|
|
entityName="Groups"
|
|
idField="name"
|
|
/>
|
|
</Grid>
|
|
</Fragment>
|
|
)}
|
|
{selectedTab === 3 && (
|
|
<Fragment>
|
|
<h1 className={classes.sectionTitle}>Raw Policy</h1>
|
|
<Paper className={classes.paperContainer}>
|
|
<form
|
|
noValidate
|
|
autoComplete="off"
|
|
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
|
saveRecord(e);
|
|
}}
|
|
>
|
|
<Grid container>
|
|
<Grid item xs={12} className={classes.formScrollable}>
|
|
<CodeMirrorWrapper
|
|
value={policyDefinition}
|
|
onBeforeChange={(editor, data, value) => {
|
|
setPolicyDefinition(value);
|
|
}}
|
|
/>
|
|
</Grid>
|
|
<Grid item xs={12} className={classes.buttonContainer}>
|
|
{!policy && (
|
|
<button
|
|
type="button"
|
|
color="primary"
|
|
className={classes.clearButton}
|
|
onClick={() => {
|
|
resetForm();
|
|
}}
|
|
>
|
|
Clear
|
|
</button>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
variant="contained"
|
|
color="primary"
|
|
disabled={addLoading || !validSave}
|
|
>
|
|
Save
|
|
</Button>
|
|
</Grid>
|
|
{addLoading && (
|
|
<Grid item xs={12}>
|
|
<LinearProgress />
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
</form>
|
|
</Paper>
|
|
</Fragment>
|
|
)}
|
|
</Grid>
|
|
</Grid>
|
|
</Fragment>
|
|
);
|
|
};
|
|
|
|
const connector = connect(null, {
|
|
setErrorSnackMessage,
|
|
setSnackBarMessage,
|
|
});
|
|
|
|
export default withStyles(styles)(connector(PolicyDetails));
|