diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts index 6bce0f4a4..116dd8558 100644 --- a/portal-ui/src/common/SecureComponent/permissions.ts +++ b/portal-ui/src/common/SecureComponent/permissions.ts @@ -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]: [ diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index d4c449933..9c89c549d 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.tsx @@ -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, diff --git a/portal-ui/src/screens/Console/Users/AddUserServiceAccountHelpBox.tsx b/portal-ui/src/screens/Console/Users/AddUserServiceAccountHelpBox.tsx new file mode 100644 index 000000000..5841fe1da --- /dev/null +++ b/portal-ui/src/screens/Console/Users/AddUserServiceAccountHelpBox.tsx @@ -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 . +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 AddUserServiceAccountHelpBox = () => { + 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 AddUserServiceAccountHelpBox; diff --git a/portal-ui/src/screens/Console/Users/AddUserServiceAccountScreen.tsx b/portal-ui/src/screens/Console/Users/AddUserServiceAccountScreen.tsx new file mode 100644 index 000000000..4c5a66ed3 --- /dev/null +++ b/portal-ui/src/screens/Console/Users/AddUserServiceAccountScreen.tsx @@ -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 . + +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(false); + 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); + const [policyJSON, setPolicyJSON] = useState(""); + + 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 ( + + {newServiceAccount !== null && ( + { + closeCredentialsModal(); + }} + entity="Service Account" + /> + )} + + + } + /> + + + + }> + Create Service Account for {userName} + + +
) => { + addUserServiceAccount(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 && ( + + + + { + setPolicyJSON(value); + }} + /> + + + )} + + + + + + +
+
+
+ +
+
+
+
+ ); +}; + +const mapDispatchToProps = { + setErrorSnackMessage, +}; + +const connector = connect(null, mapDispatchToProps); + +export default withStyles(styles)(connector(AddServiceAccount)); diff --git a/portal-ui/src/screens/Console/Users/UserDetails.tsx b/portal-ui/src/screens/Console/Users/UserDetails.tsx index 02f2df1ba..b5a36caa1 100644 --- a/portal-ui/src/screens/Console/Users/UserDetails.tsx +++ b/portal-ui/src/screens/Console/Users/UserDetails.tsx @@ -317,6 +317,7 @@ const UserDetails = ({ classes, match }: IUserDetailsProps) => { ), }} diff --git a/portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx b/portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx index 6573b4257..99863257d 100644 --- a/portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx +++ b/portal-ui/src/screens/Console/Users/UserServiceAccountsPanel.tsx @@ -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([]); const [loading, setLoading] = useState(false); @@ -228,6 +234,15 @@ const UserServiceAccountsPanel = ({ disabled={selectedSAs.length === 0} variant={"outlined"} /> + } onClick={() => { - setAddScreenOpen(true); - setAddScreenOpen(true); - setSelectedServiceAccount(null); + history.push(`${IAM_PAGES.USER_ACCOUNT}/${user}`); }} disabled={!hasPolicy} /> +