From b567e4855fc22277c6a538ee2617d5dc374894c1 Mon Sep 17 00:00:00 2001 From: jinapurapu <65002498+jinapurapu@users.noreply.github.com> Date: Fri, 22 Apr 2022 17:49:24 -0700 Subject: [PATCH] Create service account screen (#1886) --- .../src/common/SecureComponent/permissions.ts | 4 + portal-ui/src/icons/PasswordKeyIcon.tsx | 2 +- .../src/screens/Console/Account/Account.tsx | 75 +--- .../Console/Account/AddServiceAccount.tsx | 248 ------------- .../Account/AddServiceAccountHelpBox.tsx | 134 +++++++ .../Account/AddServiceAccountScreen.tsx | 342 ++++++++++++++++++ .../CredentialsPrompt/CredentialsPrompt.tsx | 22 ++ portal-ui/src/screens/Console/Console.tsx | 7 + 8 files changed, 526 insertions(+), 308 deletions(-) delete mode 100644 portal-ui/src/screens/Console/Account/AddServiceAccount.tsx create mode 100644 portal-ui/src/screens/Console/Account/AddServiceAccountHelpBox.tsx create mode 100644 portal-ui/src/screens/Console/Account/AddServiceAccountScreen.tsx diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts index aabc35c3e..fb4f927be 100644 --- a/portal-ui/src/common/SecureComponent/permissions.ts +++ b/portal-ui/src/common/SecureComponent/permissions.ts @@ -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 ], diff --git a/portal-ui/src/icons/PasswordKeyIcon.tsx b/portal-ui/src/icons/PasswordKeyIcon.tsx index dd402c708..7ab0f67f3 100644 --- a/portal-ui/src/icons/PasswordKeyIcon.tsx +++ b/portal-ui/src/icons/PasswordKeyIcon.tsx @@ -30,7 +30,7 @@ const PasswordKeyIcon = (props: SVGProps) => { 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" /> 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([]); const [loading, setLoading] = useState(false); const [filter, setFilter] = useState(""); - const [addScreenOpen, setAddScreenOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); - const [selectedServiceAccount, setSelectedServiceAccount] = useState< - string | null - >(null); - const [showNewCredentials, setShowNewCredentials] = useState(false); - const [newServiceAccount, setNewServiceAccount] = - useState(null); - const [changePasswordModalOpen, setChangePasswordModalOpen] = - useState(false); + const [selectedServiceAccount, setSelectedServiceAccount] = useState(null); + const [changePasswordModalOpen, setChangePasswordModalOpen] = useState(false); const [selectedSAs, setSelectedSAs] = useState([]); const [deleteMultipleOpen, setDeleteMultipleOpen] = useState(false); const [policyOpen, setPolicyOpen] = useState(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 ( - {addScreenOpen && ( - { - closeAddModalAndRefresh(res); - }} - /> - )} {deleteOpen && ( { closeDeleteModalAndRefresh={closeDeleteMultipleModalAndRefresh} /> )} - {showNewCredentials && ( - { - closeCredentialsModal(); - }} - entity="Service Account" - /> - )} + {policyOpen && ( { icon={} color={"primary"} variant={"outlined"} + disabled={selectedSAs.length === 0 } /> { - setAddScreenOpen(true); - setSelectedServiceAccount(null); + onClick={(e) => { + history.push(`${IAM_PAGES.ACCOUNT_ADD}`); }} text={`Create service account`} icon={} diff --git a/portal-ui/src/screens/Console/Account/AddServiceAccount.tsx b/portal-ui/src/screens/Console/Account/AddServiceAccount.tsx deleted file mode 100644 index bf2b3992b..000000000 --- a/portal-ui/src/screens/Console/Account/AddServiceAccount.tsx +++ /dev/null @@ -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 . - -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(false); - const [policyDefinition, setPolicyDefinition] = useState(""); - const [accessKey, setAccessKey] = useState(""); - const [secretKey, setSecretKey] = useState(""); - const [isRestrictedByPolicy, setIsRestrictedByPolicy] = - useState(false); - const [addCredentials, setAddCredentials] = useState(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 ( - { - closeModalAndRefresh(null); - }} - title={`Create Service Account`} - titleIcon={} - > -
) => { - addServiceAccount(e); - }} - > - {addSending && ( - - - - )} - - -
- 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. - - You cannot modify the Service Account optional policy after - saving. - -
-
- - - ) => { - setAddCredentials(event.target.checked); - }} - label={"Customize Credentials"} - /> - {addCredentials && ( - -
- { - setAccessKey(e.target.value); - }} - /> - { - setSecretKey(e.target.value); - }} - /> -
-
- )} -
- - ) => { - setIsRestrictedByPolicy(event.target.checked); - }} - label={"Restrict with policy"} - /> - {isRestrictedByPolicy && ( - - { - setPolicyDefinition(value); - }} - /> - - )} - -
-
- - - - - - -
-
- ); -}; - -const mapDispatchToProps = { - setModalErrorSnackMessage, -}; - -const connector = connect(null, mapDispatchToProps); - -export default withStyles(styles)(connector(AddServiceAccount)); diff --git a/portal-ui/src/screens/Console/Account/AddServiceAccountHelpBox.tsx b/portal-ui/src/screens/Console/Account/AddServiceAccountHelpBox.tsx new file mode 100644 index 000000000..4eb9df345 --- /dev/null +++ b/portal-ui/src/screens/Console/Account/AddServiceAccountHelpBox.tsx @@ -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 . +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 ( + + {icon}{" "} +
+ {description} +
+
+ ); +}; +const AddUserHelpBox = ({ hasMargin = true }: { hasMargin?: boolean }) => { + return ( + + + +
Learn more about Service Accounts
+
+ + + } description={`Create Service Accounts`} /> + + 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. + + + + } description={`Assign Custom Credentials`} /> + + 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. + + + Service Accounts support programmatic access by applications. + You cannot use a Service Account to log into the MinIO Console. + + + + } description={`Assign Access Policies`}/> + + 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. + + + You cannot modify the optional Service Account IAM policy after + saving. + + + + + + + + +
+ ); +}; + +export default AddUserHelpBox; diff --git a/portal-ui/src/screens/Console/Account/AddServiceAccountScreen.tsx b/portal-ui/src/screens/Console/Account/AddServiceAccountScreen.tsx new file mode 100644 index 000000000..08c6c9802 --- /dev/null +++ b/portal-ui/src/screens/Console/Account/AddServiceAccountScreen.tsx @@ -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 . + +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(false); + const [policyDefinition, setPolicyDefinition] = useState(""); + const [accessKey, setAccessKey] = useState(getRandomString(16)); + const [secretKey, setSecretKey] = useState(getRandomString(32)); + const [isRestrictedByPolicy, setIsRestrictedByPolicy] = + useState(false); + const [newServiceAccount, setNewServiceAccount] = + useState(null); + const [showPassword, setShowPassword] = useState(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 ( + + {newServiceAccount !== null && ( + { + closeCredentialsModal(); + }} + entity="Service Account" + /> + )} + + } + /> + + + + }> + Create Service Account + + +
) => { + addServiceAccount(e); + }} + > + + + + + + + + + + + + {" "} +
+ { + setAccessKey(e.target.value); + }} + /> +
+
+ +
+ { + setSecretKey(e.target.value); + }} + overlayIcon={ + showPassword ? ( + + ) : ( + + ) + } + overlayAction={() => + setShowPassword(!showPassword) + } + /> +
+
+
+
+
+
+
+
+ + + + + + + + + ) => { + 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." + } + /> + + + + {isRestrictedByPolicy && ( + + { + setPolicyDefinition(value); + }} + /> + + )} + + + + + + +
+
+
+ +
+
+
+
+ ); +}; + +const mapDispatchToProps = { + setErrorSnackMessage, +}; + +const connector = connect(null, mapDispatchToProps); + +export default withStyles(styles)(connector(AddServiceAccount)); diff --git a/portal-ui/src/screens/Console/Common/CredentialsPrompt/CredentialsPrompt.tsx b/portal-ui/src/screens/Console/Common/CredentialsPrompt/CredentialsPrompt.tsx index 91308e957..c1f688657 100644 --- a/portal-ui/src/screens/Console/Common/CredentialsPrompt/CredentialsPrompt.tsx +++ b/portal-ui/src/screens/Console/Common/CredentialsPrompt/CredentialsPrompt.tsx @@ -156,9 +156,22 @@ const CredentialsPrompt = ({ /> )} +
)} + {(consoleCreds === null || consoleCreds === undefined) && ( + <> + + + + )} {idp ? (
Please Login via the configured external identity provider. @@ -172,6 +185,7 @@ const CredentialsPrompt = ({
)} + {!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( diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index 15f910cbf..bf4aa6fbd 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.tsx @@ -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,