Add User Service Account screen (#1947)
This commit is contained in:
@@ -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]: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
@@ -317,6 +317,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => {
|
||||
<UserServiceAccountsPanel
|
||||
user={userName}
|
||||
hasPolicy={hasPolicy}
|
||||
history={history}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user