Add User Service Account screen (#1947)

This commit is contained in:
jinapurapu
2022-05-04 14:39:21 -07:00
committed by GitHub
parent 3c659a29ae
commit 0cdff7dc0e
6 changed files with 496 additions and 3 deletions

View File

@@ -126,6 +126,8 @@ export const IAM_PAGES = {
GROUPS_VIEW: "/identity/groups/:groupName+",
ACCOUNT: "/identity/account",
ACCOUNT_ADD: "/identity/new-account",
USER_ACCOUNT: "/identity/new-user-sa",
USER_ACCOUNT_ADD: "/identity/new-user-sa/:userName+",
/* Access */
POLICIES: "/access/policies",
POLICY_ADD: "/access/add-policy",
@@ -314,6 +316,12 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_DISABLE_USER,
IAM_SCOPES.ADMIN_DELETE_USER,
],
[IAM_PAGES.USER_ACCOUNT_ADD]: [
IAM_SCOPES.ADMIN_CREATE_SERVICEACCOUNT,
IAM_SCOPES.ADMIN_UPDATE_SERVICEACCOUNT,
IAM_SCOPES.ADMIN_REMOVE_SERVICEACCOUNT,
IAM_SCOPES.ADMIN_LIST_SERVICEACCOUNTS,
],
[IAM_PAGES.USER_ADD]: [IAM_SCOPES.ADMIN_CREATE_USER], // displays create user button
[IAM_PAGES.ACCOUNT_ADD]: [IAM_SCOPES.ADMIN_CREATE_SERVICEACCOUNT],
[IAM_PAGES.DASHBOARD]: [

View File

@@ -111,6 +111,9 @@ const Account = React.lazy(() => import("./Account/Account"));
const AccountCreate = React.lazy(
() => import("./Account/AddServiceAccountScreen")
);
const UserSACreate = React.lazy(
() => import("./Users/AddUserServiceAccountScreen")
);
const Users = React.lazy(() => import("./Users/Users"));
const Groups = React.lazy(() => import("./Groups/Groups"));
@@ -419,6 +422,11 @@ const Console = ({
path: IAM_PAGES.ACCOUNT_ADD,
forceDisplay: true, // user has implicit access to service-accounts
},
{
component: UserSACreate,
path: IAM_PAGES.USER_ACCOUNT_ADD,
forceDisplay: true, // user has implicit access to service-accounts
},
{
component: License,
path: IAM_PAGES.LICENSE,

View File

@@ -0,0 +1,142 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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 from "react";
import { Box } from "@mui/material";
import {
HelpIconFilled,
ServiceAccountIcon,
PasswordKeyIcon,
IAMPoliciesIcon,
} from "../../../icons";
const FeatureItem = ({
icon,
description,
}: {
icon: any;
description: string;
}) => {
return (
<Box
sx={{
display: "flex",
"& .min-icon": {
marginRight: "10px",
height: "23px",
width: "23px",
marginBottom: "10px",
},
}}
>
{icon}{" "}
<div style={{ fontSize: "14px", fontStyle: "italic", color: "#5E5E5E" }}>
{description}
</div>
</Box>
);
};
const AddUserServiceAccountHelpBox = () => {
return (
<Box
sx={{
flex: 1,
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "20px",
marginTop: {
xs: "0px",
},
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
marginBottom: "16px",
paddingBottom: "20px",
"& .min-icon": {
height: "21px",
width: "21px",
marginRight: "15px",
},
}}
>
<HelpIconFilled />
<div>Learn more about Service Accounts</div>
</Box>
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={<ServiceAccountIcon />}
description={`Create Service Accounts`}
/>
<Box sx={{ paddingTop: "20px" }}>
Service Accounts inherit the policies explicitly attached to the
parent user, and the policies attached to each group in which the
parent user has membership.
</Box>
</Box>
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={<PasswordKeyIcon />}
description={`Assign Custom Credentials`}
/>
<Box sx={{ paddingTop: "10px" }}>
Randomized access credentials are recommended, and provided by
default. You may use your own custom Access Key and Secret Key by
replacing the default values. After creation of any Service Account,
you will be given the opportunity to view and download the account
credentials.
</Box>
<Box sx={{ paddingTop: "10px" }}>
Service Accounts support programmatic access by applications. You
cannot use a Service Account to log into the MinIO Console.
</Box>
</Box>
<Box sx={{ paddingBottom: "20px" }}>
<FeatureItem
icon={<IAMPoliciesIcon />}
description={`Assign Access Policies`}
/>
<Box sx={{ paddingTop: "10px" }}>
You can specify an optional JSON-formatted IAM policy to further
restrict Service Account access to a subset of the actions and
resources explicitly allowed for the parent user. Additional access
beyond that of the parent user cannot be implemented through these
policies.
</Box>
<Box sx={{ paddingTop: "10px" }}>
You cannot modify the optional Service Account IAM policy after
saving.
</Box>
</Box>
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
></Box>
</Box>
);
};
export default AddUserServiceAccountHelpBox;

View File

@@ -0,0 +1,320 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 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, useState, useEffect } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
formFieldStyles,
modalStyleUtils,
} from "../Common/FormComponents/common/styleLibrary";
import Grid from "@mui/material/Grid";
import { Button, Box } from "@mui/material";
import { PasswordKeyIcon, ServiceAccountCredentialsIcon } from "../../../icons";
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
import PageHeader from "../Common/PageHeader/PageHeader";
import PageLayout from "../Common/Layout/PageLayout";
import history from "../../../../src/history";
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import BackLink from "../../../common/BackLink";
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
import { connect } from "react-redux";
import { IAMPoliciesIcon } from "../../../icons";
import RemoveRedEyeIcon from "@mui/icons-material/RemoveRedEye";
import VisibilityOffIcon from "@mui/icons-material/VisibilityOff";
import { IAM_PAGES } from "../../../common/SecureComponent/permissions";
import { ErrorResponseHandler } from "../../../../src/common/types";
import api from "../../../../src/common/api";
import CredentialsPrompt from "../Common/CredentialsPrompt/CredentialsPrompt";
import { setErrorSnackMessage } from "../../../../src/actions";
import SectionTitle from "../Common/SectionTitle";
import { getRandomString } from "../../../screens/Console/Tenants/utils";
import AddUserServiceAccountHelpBox from "./AddUserServiceAccountHelpBox";
interface IAddServiceAccountProps {
classes: any;
match: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
}
const styles = (theme: Theme) =>
createStyles({
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
margin: "auto",
justifyContent: "center",
"& div": {
width: 150,
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
},
...formFieldStyles,
...modalStyleUtils,
});
const AddServiceAccount = ({
classes,
match,
setErrorSnackMessage,
}: IAddServiceAccountProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [accessKey, setAccessKey] = useState<string>(getRandomString(16));
const [secretKey, setSecretKey] = useState<string>(getRandomString(32));
const [isRestrictedByPolicy, setIsRestrictedByPolicy] =
useState<boolean>(false);
const [newServiceAccount, setNewServiceAccount] =
useState<NewServiceAccount | null>(null);
const [showPassword, setShowPassword] = useState<boolean>(false);
const [policyJSON, setPolicyJSON] = useState<string>("");
const userName = match.params["userName"];
useEffect(() => {
if (addSending) {
api
.invoke(
"POST",
`/api/v1/user/${userName}/service-account-credentials`,
{
policy: policyJSON,
accessKey: accessKey,
secretKey: secretKey,
}
)
.then((res) => {
setAddSending(false);
setNewServiceAccount({
accessKey: res.accessKey || "",
secretKey: res.secretKey || "",
url: res.url || "",
});
})
.catch((err: ErrorResponseHandler) => {
setAddSending(false);
setErrorSnackMessage(err);
});
}
}, [
addSending,
setAddSending,
setErrorSnackMessage,
policyJSON,
userName,
accessKey,
secretKey,
]);
const addUserServiceAccount = (e: React.FormEvent) => {
e.preventDefault();
setAddSending(true);
};
const resetForm = () => {
setNewServiceAccount(null);
setAccessKey("");
setSecretKey("");
setShowPassword(false);
};
const closeCredentialsModal = () => {
setNewServiceAccount(null);
history.push(`${IAM_PAGES.USERS}/${userName}`);
};
return (
<Fragment>
{newServiceAccount !== null && (
<CredentialsPrompt
newServiceAccount={newServiceAccount}
open={newServiceAccount !== null}
closeModal={() => {
closeCredentialsModal();
}}
entity="Service Account"
/>
)}
<Grid item xs={12}>
<PageHeader
label={
<BackLink
to={`${IAM_PAGES.USERS}/${userName}`}
label={"User Details - " + userName}
/>
}
/>
<PageLayout>
<Box
sx={{
display: "grid",
padding: "25px",
gap: "25px",
gridTemplateColumns: {
md: "2fr 1.2fr",
xs: "1fr",
},
border: "1px solid #eaeaea",
}}
>
<Box>
<SectionTitle icon={<ServiceAccountCredentialsIcon />}>
Create Service Account for {userName}
</SectionTitle>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
addUserServiceAccount(e);
}}
>
<Grid container item spacing="20" sx={{ marginTop: 1 }}>
<Grid item xs={12}>
<Grid container item spacing="20">
<Grid item xs={12}>
<Grid container>
<Grid item xs={1}>
<PasswordKeyIcon />
</Grid>
<Grid item>
<Grid container item spacing="20">
<Grid item xs={12}>
{" "}
<div className={classes.stackedInputs}>
<InputBoxWrapper
value={accessKey}
label={"Access Key"}
id={"accessKey"}
name={"accessKey"}
placeholder={"Enter Access Key"}
onChange={(e) => {
setAccessKey(e.target.value);
}}
/>
</div>
</Grid>
<Grid item xs={12}>
<div className={classes.stackedInputs}>
<InputBoxWrapper
value={secretKey}
label={"Secret Key"}
id={"secretKey"}
name={"secretKey"}
type={showPassword ? "text" : "password"}
placeholder={"Enter Secret Key"}
onChange={(e) => {
setSecretKey(e.target.value);
}}
overlayIcon={
showPassword ? (
<VisibilityOffIcon />
) : (
<RemoveRedEyeIcon />
)
}
overlayAction={() =>
setShowPassword(!showPassword)
}
/>
</div>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
</Grid>
<Grid container item spacing="20">
<Grid item xs={12}>
<Grid container>
<Grid item xs={1}>
<IAMPoliciesIcon />
</Grid>
<Grid item xs={11}>
<FormSwitchWrapper
value="serviceAccountPolicy"
id="serviceAccountPolicy"
name="serviceAccountPolicy"
checked={isRestrictedByPolicy}
onChange={(
event: React.ChangeEvent<HTMLInputElement>
) => {
setIsRestrictedByPolicy(event.target.checked);
}}
label={"Restrict beyond user policy"}
tooltip={
"You can specify an optional JSON-formatted IAM policy to further restrict Service Account access to a subset of the actions and resources explicitly allowed for the parent user. Additional access beyond that of the parent user cannot be implemented through these policies."
}
/>
</Grid>
</Grid>
</Grid>
{isRestrictedByPolicy && (
<Grid
item
xs={12}
className={classes.codeMirrorContainer}
>
<Grid item xs={12} className={classes.formScrollable}>
<CodeMirrorWrapper
label={"Policy"}
value={policyJSON}
onBeforeChange={(editor, data, value) => {
setPolicyJSON(value);
}}
/>
</Grid>
</Grid>
)}
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
type="button"
variant="outlined"
color="primary"
onClick={resetForm}
>
Clear
</Button>
<Button type="submit" variant="contained" color="primary">
Create
</Button>
</Grid>
</Grid>
</form>
</Box>
<AddUserServiceAccountHelpBox />
</Box>
</PageLayout>
</Grid>
</Fragment>
);
};
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default withStyles(styles)(connector(AddServiceAccount));

View File

@@ -317,6 +317,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
<UserServiceAccountsPanel
user={userName}
hasPolicy={hasPolicy}
history={history}
/>
),
}}

View File

@@ -41,12 +41,17 @@ import RBIconButton from "../Buckets/BucketDetails/SummaryItems/RBIconButton";
import DeleteMultipleServiceAccounts from "./DeleteMultipleServiceAccounts";
import { selectSAs } from "../../Console/Configurations/utils";
import ServiceAccountPolicy from "../Account/ServiceAccountPolicy";
import { IAM_PAGES,
CONSOLE_UI_RESOURCE,
IAM_SCOPES } from "../../../common/SecureComponent/permissions";
import { SecureComponent } from "../../../common/SecureComponent";
interface IUserServiceAccountsProps {
classes: any;
user: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
hasPolicy: boolean;
history: any;
}
const styles = (theme: Theme) =>
@@ -64,6 +69,7 @@ const UserServiceAccountsPanel = ({
user,
setErrorSnackMessage,
hasPolicy,
history,
}: IUserServiceAccountsProps) => {
const [records, setRecords] = useState<string[]>([]);
const [loading, setLoading] = useState<boolean>(false);
@@ -228,6 +234,15 @@ const UserServiceAccountsPanel = ({
disabled={selectedSAs.length === 0}
variant={"outlined"}
/>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_CREATE_SERVICEACCOUNT,
IAM_SCOPES.ADMIN_UPDATE_SERVICEACCOUNT,
IAM_SCOPES.ADMIN_REMOVE_SERVICEACCOUNT,
IAM_SCOPES.ADMIN_LIST_SERVICEACCOUNTS]}
resource={CONSOLE_UI_RESOURCE}
matchAll
errorProps={{ disabled: true }}
>
<RBIconButton
tooltip={"Create service account"}
text={"Create service account"}
@@ -235,12 +250,11 @@ const UserServiceAccountsPanel = ({
color="primary"
icon={<AddIcon />}
onClick={() => {
setAddScreenOpen(true);
setAddScreenOpen(true);
setSelectedServiceAccount(null);
history.push(`${IAM_PAGES.USER_ACCOUNT}/${user}`);
}}
disabled={!hasPolicy}
/>
</SecureComponent>
</Box>
</div>
<div className={classes.tableBlock}>