Create service account screen (#1886)
This commit is contained in:
@@ -123,6 +123,7 @@ export const IAM_PAGES = {
|
||||
GROUPS: "/identity/groups",
|
||||
GROUPS_VIEW: "/identity/groups/:groupName+",
|
||||
ACCOUNT: "/identity/account",
|
||||
ACCOUNT_ADD: "/identity/new-account",
|
||||
/* Access */
|
||||
POLICIES: "/access/policies",
|
||||
POLICIES_VIEW: "/access/policies/*",
|
||||
@@ -306,6 +307,9 @@ export const IAM_PAGES_PERMISSIONS = {
|
||||
IAM_SCOPES.ADMIN_DISABLE_USER,
|
||||
IAM_SCOPES.ADMIN_DELETE_USER,
|
||||
],
|
||||
[IAM_PAGES.ACCOUNT_ADD]: [
|
||||
IAM_SCOPES.ADMIN_CREATE_SERVICEACCOUNT,
|
||||
],
|
||||
[IAM_PAGES.DASHBOARD]: [
|
||||
IAM_SCOPES.ADMIN_SERVER_INFO, // displays dashboard information
|
||||
],
|
||||
|
||||
@@ -30,7 +30,7 @@ const PasswordKeyIcon = (props: SVGProps<SVGSVGElement>) => {
|
||||
data-name="Trazado 7179"
|
||||
d="M141.421,148.182a4.5,4.5,0,0,0-4.3,5.805l-5.188,5.195v3h3l5.187-5.2a4.5,4.5,0,0,0,5.8-3.936,4.39,4.39,0,0,0-.823-3A4.492,4.492,0,0,0,141.421,148.182Zm.5,5a1,1,0,1,1,1-1A1,1,0,0,1,141.92,153.182Z"
|
||||
transform="translate(-131.934 -148.182)"
|
||||
fill="#5e5e5e"
|
||||
//fill="#5e5e5e"
|
||||
/>
|
||||
<rect
|
||||
id="Rectángulo_1090"
|
||||
|
||||
@@ -22,7 +22,6 @@ import withStyles from "@mui/styles/withStyles";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import api from "../../../common/api";
|
||||
import { Box } from "@mui/material";
|
||||
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
|
||||
import { setErrorSnackMessage, setSnackBarMessage } from "../../../actions";
|
||||
import {
|
||||
AccountIcon,
|
||||
@@ -39,6 +38,7 @@ import {
|
||||
searchField,
|
||||
tableStyles,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
|
||||
import { ErrorResponseHandler } from "../../../common/types";
|
||||
import ChangePasswordModal from "./ChangePasswordModal";
|
||||
import HelpBox from "../../../common/HelpBox";
|
||||
@@ -48,6 +48,7 @@ import withSuspense from "../Common/Components/withSuspense";
|
||||
import {
|
||||
CONSOLE_UI_RESOURCE,
|
||||
IAM_SCOPES,
|
||||
IAM_PAGES,
|
||||
} from "../../../common/SecureComponent/permissions";
|
||||
import { SecureComponent } from "../../../common/SecureComponent";
|
||||
import RBIconButton from "../Buckets/BucketDetails/SummaryItems/RBIconButton";
|
||||
@@ -55,15 +56,11 @@ import { selectSAs } from "../Configurations/utils";
|
||||
import DeleteMultipleServiceAccounts from "../Users/DeleteMultipleServiceAccounts";
|
||||
import ServiceAccountPolicy from "./ServiceAccountPolicy";
|
||||
|
||||
const AddServiceAccount = withSuspense(
|
||||
React.lazy(() => import("./AddServiceAccount"))
|
||||
);
|
||||
|
||||
const DeleteServiceAccount = withSuspense(
|
||||
React.lazy(() => import("./DeleteServiceAccount"))
|
||||
);
|
||||
const CredentialsPrompt = withSuspense(
|
||||
React.lazy(() => import("../Common/CredentialsPrompt/CredentialsPrompt"))
|
||||
);
|
||||
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -80,23 +77,21 @@ const styles = (theme: Theme) =>
|
||||
|
||||
interface IServiceAccountsProps {
|
||||
classes: any;
|
||||
history: any;
|
||||
displayErrorMessage: typeof setErrorSnackMessage;
|
||||
}
|
||||
|
||||
const Account = ({ classes, displayErrorMessage }: IServiceAccountsProps) => {
|
||||
const Account = ({
|
||||
classes,
|
||||
displayErrorMessage,
|
||||
history,
|
||||
}: IServiceAccountsProps) => {
|
||||
const [records, setRecords] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
const [addScreenOpen, setAddScreenOpen] = useState<boolean>(false);
|
||||
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
|
||||
const [selectedServiceAccount, setSelectedServiceAccount] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
|
||||
const [newServiceAccount, setNewServiceAccount] =
|
||||
useState<NewServiceAccount | null>(null);
|
||||
const [changePasswordModalOpen, setChangePasswordModalOpen] =
|
||||
useState<boolean>(false);
|
||||
const [selectedServiceAccount, setSelectedServiceAccount] = useState<string | null >(null);
|
||||
const [changePasswordModalOpen, setChangePasswordModalOpen] = useState<boolean>(false);
|
||||
const [selectedSAs, setSelectedSAs] = useState<string[]>([]);
|
||||
const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false);
|
||||
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
|
||||
@@ -126,22 +121,6 @@ const Account = ({ classes, displayErrorMessage }: IServiceAccountsProps) => {
|
||||
setLoading(true);
|
||||
};
|
||||
|
||||
const closeAddModalAndRefresh = (res: NewServiceAccount | null) => {
|
||||
setAddScreenOpen(false);
|
||||
fetchRecords();
|
||||
|
||||
if (res !== null) {
|
||||
const nsa: NewServiceAccount = {
|
||||
console: {
|
||||
accessKey: `${res.accessKey}`,
|
||||
secretKey: `${res.secretKey}`,
|
||||
url: `${res.url}`,
|
||||
},
|
||||
};
|
||||
setNewServiceAccount(nsa);
|
||||
setShowNewCredentials(true);
|
||||
}
|
||||
};
|
||||
|
||||
const closeDeleteModalAndRefresh = (refresh: boolean) => {
|
||||
setDeleteOpen(false);
|
||||
@@ -173,11 +152,6 @@ const Account = ({ classes, displayErrorMessage }: IServiceAccountsProps) => {
|
||||
setSelectedSAs(records);
|
||||
};
|
||||
|
||||
const closeCredentialsModal = () => {
|
||||
setShowNewCredentials(false);
|
||||
setNewServiceAccount(null);
|
||||
};
|
||||
|
||||
const closePolicyModal = () => {
|
||||
setPolicyOpen(false);
|
||||
setLoading(true);
|
||||
@@ -199,14 +173,6 @@ const Account = ({ classes, displayErrorMessage }: IServiceAccountsProps) => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{addScreenOpen && (
|
||||
<AddServiceAccount
|
||||
open={addScreenOpen}
|
||||
closeModalAndRefresh={(res: NewServiceAccount | null) => {
|
||||
closeAddModalAndRefresh(res);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteOpen && (
|
||||
<DeleteServiceAccount
|
||||
deleteOpen={deleteOpen}
|
||||
@@ -223,16 +189,7 @@ const Account = ({ classes, displayErrorMessage }: IServiceAccountsProps) => {
|
||||
closeDeleteModalAndRefresh={closeDeleteMultipleModalAndRefresh}
|
||||
/>
|
||||
)}
|
||||
{showNewCredentials && (
|
||||
<CredentialsPrompt
|
||||
newServiceAccount={newServiceAccount}
|
||||
open={showNewCredentials}
|
||||
closeModal={() => {
|
||||
closeCredentialsModal();
|
||||
}}
|
||||
entity="Service Account"
|
||||
/>
|
||||
)}
|
||||
|
||||
{policyOpen && (
|
||||
<ServiceAccountPolicy
|
||||
open={policyOpen}
|
||||
@@ -283,12 +240,12 @@ const Account = ({ classes, displayErrorMessage }: IServiceAccountsProps) => {
|
||||
icon={<PasswordKeyIcon />}
|
||||
color={"primary"}
|
||||
variant={"outlined"}
|
||||
disabled={selectedSAs.length === 0 }
|
||||
/>
|
||||
</SecureComponent>
|
||||
<RBIconButton
|
||||
onClick={() => {
|
||||
setAddScreenOpen(true);
|
||||
setSelectedServiceAccount(null);
|
||||
onClick={(e) => {
|
||||
history.push(`${IAM_PAGES.ACCOUNT_ADD}`);
|
||||
}}
|
||||
text={`Create service account`}
|
||||
icon={<AddIcon />}
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// 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, { useEffect, useState } from "react";
|
||||
import { connect } from "react-redux";
|
||||
import Grid from "@mui/material/Grid";
|
||||
import { Box, Button, LinearProgress } from "@mui/material";
|
||||
import { Theme } from "@mui/material/styles";
|
||||
import createStyles from "@mui/styles/createStyles";
|
||||
import withStyles from "@mui/styles/withStyles";
|
||||
import {
|
||||
modalStyleUtils,
|
||||
serviceAccountStyles,
|
||||
} from "../Common/FormComponents/common/styleLibrary";
|
||||
import { NewServiceAccount } from "../Common/CredentialsPrompt/types";
|
||||
import { setModalErrorSnackMessage } from "../../../actions";
|
||||
import { ErrorResponseHandler } from "../../../common/types";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import api from "../../../common/api";
|
||||
import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper";
|
||||
import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import { AccountIcon } from "../../../icons";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...serviceAccountStyles,
|
||||
...modalStyleUtils,
|
||||
});
|
||||
|
||||
interface IAddServiceAccountProps {
|
||||
classes: any;
|
||||
open: boolean;
|
||||
closeModalAndRefresh: (res: NewServiceAccount | null) => void;
|
||||
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
|
||||
}
|
||||
|
||||
const AddServiceAccount = ({
|
||||
classes,
|
||||
open,
|
||||
closeModalAndRefresh,
|
||||
setModalErrorSnackMessage,
|
||||
}: IAddServiceAccountProps) => {
|
||||
const [addSending, setAddSending] = useState<boolean>(false);
|
||||
const [policyDefinition, setPolicyDefinition] = useState<string>("");
|
||||
const [accessKey, setAccessKey] = useState<string>("");
|
||||
const [secretKey, setSecretKey] = useState<string>("");
|
||||
const [isRestrictedByPolicy, setIsRestrictedByPolicy] =
|
||||
useState<boolean>(false);
|
||||
const [addCredentials, setAddCredentials] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (addSending) {
|
||||
if (addCredentials) {
|
||||
api
|
||||
.invoke("POST", `/api/v1/service-account-credentials`, {
|
||||
policy: policyDefinition,
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
})
|
||||
.then((res) => {
|
||||
setAddSending(false);
|
||||
closeModalAndRefresh(res);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
setAddSending(false);
|
||||
setModalErrorSnackMessage(err);
|
||||
});
|
||||
} else {
|
||||
api
|
||||
.invoke("POST", `/api/v1/service-accounts`, {
|
||||
policy: policyDefinition,
|
||||
})
|
||||
.then((res) => {
|
||||
setAddSending(false);
|
||||
closeModalAndRefresh(res);
|
||||
})
|
||||
.catch((err: ErrorResponseHandler) => {
|
||||
setAddSending(false);
|
||||
setModalErrorSnackMessage(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [
|
||||
addSending,
|
||||
setAddSending,
|
||||
setModalErrorSnackMessage,
|
||||
policyDefinition,
|
||||
closeModalAndRefresh,
|
||||
addCredentials,
|
||||
accessKey,
|
||||
secretKey,
|
||||
]);
|
||||
|
||||
const addServiceAccount = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setAddSending(true);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setPolicyDefinition("");
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalWrapper
|
||||
modalOpen={open}
|
||||
onClose={() => {
|
||||
closeModalAndRefresh(null);
|
||||
}}
|
||||
title={`Create Service Account`}
|
||||
titleIcon={<AccountIcon />}
|
||||
>
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
addServiceAccount(e);
|
||||
}}
|
||||
>
|
||||
{addSending && (
|
||||
<Grid item xs={12}>
|
||||
<LinearProgress />
|
||||
</Grid>
|
||||
)}
|
||||
<Grid container className={classes.modalFormScrollable}>
|
||||
<Grid item xs={12}>
|
||||
<div className={classes.infoDetails}>
|
||||
Service Accounts inherit the policy explicitly attached to the
|
||||
parent user and the policy attached to each group in which the
|
||||
parent user has membership. You can specify an optional
|
||||
JSON-formatted policy below to restrict the Service Account access
|
||||
to a subset of actions and resources explicitly allowed for the
|
||||
parent user.
|
||||
<Box sx={{ paddingTop: "15px", paddingBottom: "15px" }}>
|
||||
You cannot modify the Service Account optional policy after
|
||||
saving.
|
||||
</Box>
|
||||
</div>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid item xs={12}>
|
||||
<FormSwitchWrapper
|
||||
value="locking"
|
||||
id="locking"
|
||||
name="locking"
|
||||
checked={addCredentials}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAddCredentials(event.target.checked);
|
||||
}}
|
||||
label={"Customize Credentials"}
|
||||
/>
|
||||
{addCredentials && (
|
||||
<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);
|
||||
}}
|
||||
/>
|
||||
<InputBoxWrapper
|
||||
value={secretKey}
|
||||
label={"Secret Key"}
|
||||
id={"secretKey"}
|
||||
name={"secretKey"}
|
||||
placeholder={"Enter Secret Key"}
|
||||
onChange={(e) => {
|
||||
setSecretKey(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<FormSwitchWrapper
|
||||
value="locking"
|
||||
id="locking"
|
||||
name="locking"
|
||||
checked={isRestrictedByPolicy}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsRestrictedByPolicy(event.target.checked);
|
||||
}}
|
||||
label={"Restrict with policy"}
|
||||
/>
|
||||
{isRestrictedByPolicy && (
|
||||
<Grid item xs={12} className={classes.codeMirrorContainer}>
|
||||
<CodeMirrorWrapper
|
||||
label={"Policy "}
|
||||
value={policyDefinition}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
setPolicyDefinition(value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container>
|
||||
<Grid item xs={12} className={classes.modalButtonBar}>
|
||||
<Button
|
||||
type="button"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
onClick={resetForm}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={addSending}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</form>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setModalErrorSnackMessage,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default withStyles(styles)(connector(AddServiceAccount));
|
||||
@@ -0,0 +1,134 @@
|
||||
// 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 AddUserHelpBox = ({ hasMargin = true }: { hasMargin?: boolean }) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
flex: 1,
|
||||
border: "1px solid #eaeaea",
|
||||
borderRadius: "2px",
|
||||
display: "flex",
|
||||
flexFlow: "column",
|
||||
padding: "20px",
|
||||
marginLeft: {
|
||||
xs: "0px",
|
||||
sm: "0px",
|
||||
md: hasMargin ? "30px" : "",
|
||||
},
|
||||
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 AddUserHelpBox;
|
||||
@@ -0,0 +1,342 @@
|
||||
// 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 AddServiceAccountHelpBox from "./AddServiceAccountHelpBox";
|
||||
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";
|
||||
|
||||
interface IAddServiceAccountProps {
|
||||
classes: any;
|
||||
setErrorSnackMessage: typeof setErrorSnackMessage;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
buttonContainer: {
|
||||
textAlign: "right",
|
||||
},
|
||||
bottomContainer: {
|
||||
display: "flex",
|
||||
flexGrow: 1,
|
||||
alignItems: "center",
|
||||
margin: "auto",
|
||||
justifyContent: "center",
|
||||
"& div": {
|
||||
width: 150,
|
||||
"@media (max-width: 900px)": {
|
||||
flexFlow: "column",
|
||||
},
|
||||
},
|
||||
},
|
||||
factorElements: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
marginLeft: 30,
|
||||
},
|
||||
sizeNumber: {
|
||||
fontSize: 35,
|
||||
fontWeight: 700,
|
||||
textAlign: "center",
|
||||
},
|
||||
sizeDescription: {
|
||||
fontSize: 14,
|
||||
color: "#777",
|
||||
textAlign: "center",
|
||||
},
|
||||
pageBox: {
|
||||
border: "1px solid #EAEAEA",
|
||||
borderTop: 0,
|
||||
},
|
||||
addPoolTitle: {
|
||||
border: "1px solid #EAEAEA",
|
||||
borderBottom: 0,
|
||||
},
|
||||
headTitle: {
|
||||
fontWeight: "bold",
|
||||
fontSize: 20,
|
||||
paddingLeft: 20,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 40,
|
||||
textAlign: "end",
|
||||
},
|
||||
headIcon: {
|
||||
fontWeight: "bold",
|
||||
size: "50",
|
||||
},
|
||||
...formFieldStyles,
|
||||
...modalStyleUtils,
|
||||
});
|
||||
|
||||
const AddServiceAccount = ({
|
||||
classes,
|
||||
setErrorSnackMessage,
|
||||
}: IAddServiceAccountProps) => {
|
||||
const [addSending, setAddSending] = useState<boolean>(false);
|
||||
const [policyDefinition, setPolicyDefinition] = useState<string>("");
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (addSending) {
|
||||
api
|
||||
.invoke("POST", `/api/v1/service-account-credentials`, {
|
||||
policy: policyDefinition,
|
||||
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,
|
||||
policyDefinition,
|
||||
accessKey,
|
||||
secretKey,
|
||||
]);
|
||||
|
||||
const addServiceAccount = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setAddSending(true);
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setPolicyDefinition("");
|
||||
setNewServiceAccount(null);
|
||||
setAccessKey("");
|
||||
setSecretKey("");
|
||||
setShowPassword(false);
|
||||
};
|
||||
|
||||
const closeCredentialsModal = () => {
|
||||
setNewServiceAccount(null);
|
||||
history.push(`${IAM_PAGES.ACCOUNT}`);
|
||||
};
|
||||
|
||||
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.ACCOUNT} label={"Service Accounts"} />}
|
||||
/>
|
||||
<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
|
||||
</SectionTitle>
|
||||
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
|
||||
addServiceAccount(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}
|
||||
>
|
||||
<CodeMirrorWrapper
|
||||
label={"Policy "}
|
||||
value={policyDefinition}
|
||||
onBeforeChange={(editor, data, value) => {
|
||||
setPolicyDefinition(value);
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<AddServiceAccountHelpBox />
|
||||
</Box>
|
||||
</PageLayout>
|
||||
</Grid>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setErrorSnackMessage,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
export default withStyles(styles)(connector(AddServiceAccount));
|
||||
@@ -156,9 +156,22 @@ const CredentialsPrompt = ({
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)}
|
||||
{(consoleCreds === null || consoleCreds === undefined) && (
|
||||
<>
|
||||
<CredentialItem
|
||||
label="Access Key"
|
||||
value={newServiceAccount.accessKey || ""}
|
||||
/>
|
||||
<CredentialItem
|
||||
label="Secret Key"
|
||||
value={newServiceAccount.secretKey || ""}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{idp ? (
|
||||
<div className={classes.warningBlock}>
|
||||
Please Login via the configured external identity provider.
|
||||
@@ -172,6 +185,7 @@ const CredentialsPrompt = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</Grid>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
{!idp && (
|
||||
@@ -207,6 +221,14 @@ const CredentialsPrompt = ({
|
||||
});
|
||||
consoleExtras = cCreds[0];
|
||||
}
|
||||
} else {
|
||||
consoleExtras = {
|
||||
url: newServiceAccount.url,
|
||||
accessKey: newServiceAccount.accessKey,
|
||||
secretKey: newServiceAccount.secretKey,
|
||||
api: "s3v4",
|
||||
path: "auto",
|
||||
}
|
||||
}
|
||||
|
||||
download(
|
||||
|
||||
@@ -105,6 +105,8 @@ const Policies = React.lazy(() => import("./Policies/Policies"));
|
||||
const Dashboard = React.lazy(() => import("./Dashboard/Dashboard"));
|
||||
|
||||
const Account = React.lazy(() => import("./Account/Account"));
|
||||
|
||||
const AccountCreate = React.lazy(() => import("./Account/AddServiceAccountScreen"));
|
||||
const Users = React.lazy(() => import("./Users/Users"));
|
||||
const Groups = React.lazy(() => import("./Groups/Groups"));
|
||||
|
||||
@@ -394,6 +396,11 @@ const Console = ({
|
||||
path: IAM_PAGES.ACCOUNT,
|
||||
forceDisplay: true, // user has implicit access to service-accounts
|
||||
},
|
||||
{
|
||||
component: AccountCreate,
|
||||
path: IAM_PAGES.ACCOUNT_ADD,
|
||||
forceDisplay: true, // user has implicit access to service-accounts
|
||||
},
|
||||
{
|
||||
component: License,
|
||||
path: IAM_PAGES.LICENSE,
|
||||
|
||||
Reference in New Issue
Block a user