diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts index daec07d57..0a7e2a1f5 100644 --- a/portal-ui/src/common/SecureComponent/permissions.ts +++ b/portal-ui/src/common/SecureComponent/permissions.ts @@ -120,6 +120,7 @@ export const IAM_PAGES = { IDENTITY: "/identity", USERS: "/identity/users", USERS_VIEW: "/identity/users/:userName+", + USER_ADD: "/identity/add-user", GROUPS: "/identity/groups", GROUPS_ADD: "/identity/create-group", GROUPS_VIEW: "/identity/groups/:groupName+", @@ -313,6 +314,8 @@ export const IAM_PAGES_PERMISSIONS = { IAM_SCOPES.ADMIN_DISABLE_USER, IAM_SCOPES.ADMIN_DELETE_USER, ], + [IAM_PAGES.USER_ADD]: [ + IAM_SCOPES.ADMIN_CREATE_USER,], // displays create user button [IAM_PAGES.ACCOUNT_ADD]: [ IAM_SCOPES.ADMIN_CREATE_SERVICEACCOUNT, ], diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index 847b26afe..01935b087 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.tsx @@ -281,6 +281,10 @@ const Console = ({ component: Users, path: IAM_PAGES.USERS_VIEW, }, + { + component: Users, + path: IAM_PAGES.USER_ADD, + }, { component: Users, path: IAM_PAGES.USERS, diff --git a/portal-ui/src/screens/Console/Policies/PolicySelectors.tsx b/portal-ui/src/screens/Console/Policies/PolicySelectors.tsx index 62a325222..25224674f 100644 --- a/portal-ui/src/screens/Console/Policies/PolicySelectors.tsx +++ b/portal-ui/src/screens/Console/Policies/PolicySelectors.tsx @@ -145,7 +145,7 @@ const PolicySelectors = ({ Assign Policies
{ setFilter(value); }} diff --git a/portal-ui/src/screens/Console/Users/AddUser.tsx b/portal-ui/src/screens/Console/Users/AddUser.tsx deleted file mode 100644 index 9b1f19f4f..000000000 --- a/portal-ui/src/screens/Console/Users/AddUser.tsx +++ /dev/null @@ -1,311 +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, { useCallback, useEffect, useState } from "react"; -import { connect } from "react-redux"; -import Grid from "@mui/material/Grid"; -import { Button, LinearProgress, Tab, Tabs } from "@mui/material"; -import { Theme } from "@mui/material/styles"; -import createStyles from "@mui/styles/createStyles"; -import withStyles from "@mui/styles/withStyles"; -import { - formFieldStyles, - modalStyleUtils, - spacingUtils, -} from "../Common/FormComponents/common/styleLibrary"; -import { User } from "./types"; -import { setModalErrorSnackMessage } from "../../../actions"; -import { ErrorResponseHandler } from "../../../common/types"; -import api from "../../../common/api"; -import GroupsSelectors from "./GroupsSelectors"; -import ModalWrapper from "../Common/ModalWrapper/ModalWrapper"; -import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; -import FormSwitchWrapper from "../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; -import PredefinedList from "../Common/FormComponents/PredefinedList/PredefinedList"; -import PolicySelectors from "../Policies/PolicySelectors"; -import { TabPanel } from "../../shared/tabs"; -import { CreateUserIcon } from "../../../icons"; - -const styles = (theme: Theme) => - createStyles({ - tabsHeader: { - marginBottom: "1rem", - }, - ...modalStyleUtils, - ...formFieldStyles, - ...spacingUtils, - }); - -interface IAddUserContentProps { - classes: any; - closeModalAndRefresh: () => void; - selectedUser: User | null; - open: boolean; - setModalErrorSnackMessage: typeof setModalErrorSnackMessage; -} - -const AddUser = ({ - classes, - closeModalAndRefresh, - selectedUser, - open, - setModalErrorSnackMessage, -}: IAddUserContentProps) => { - const [addLoading, setAddLoading] = useState(false); - const [accessKey, setAccessKey] = useState(""); - const [secretKey, setSecretKey] = useState(""); - const [enabled, setEnabled] = useState(false); - const [selectedGroups, setSelectedGroups] = useState([]); - const [selectedPolicies, setSelectedPolicies] = useState([]); - const [currentGroups, setCurrentGroups] = useState([]); - const [currenTab, setCurrenTab] = useState(0); - - const getUserInformation = useCallback(() => { - if (!selectedUser) { - return null; - } - - api - .invoke("GET", `/api/v1/user?name=${encodeURI(selectedUser.accessKey)}`) - .then((res) => { - setAddLoading(false); - setAccessKey(res.accessKey); - setSelectedGroups(res.memberOf || []); - setCurrentGroups(res.memberOf || []); - setEnabled(res.status === "enabled"); - }) - .catch((err: ErrorResponseHandler) => { - setAddLoading(false); - setModalErrorSnackMessage(err); - }); - }, [selectedUser, setModalErrorSnackMessage]); - - useEffect(() => { - if (selectedUser === null) { - setAccessKey(""); - setSecretKey(""); - setSelectedGroups([]); - } else { - getUserInformation(); - } - }, [selectedUser, getUserInformation]); - - const saveRecord = (event: React.FormEvent) => { - event.preventDefault(); - - if (secretKey.length < 8) { - setModalErrorSnackMessage({ - errorMessage: "Passwords must be at least 8 characters long", - detailedError: "", - }); - setAddLoading(false); - return; - } - - if (addLoading) { - return; - } - setAddLoading(true); - if (selectedUser !== null) { - api - .invoke( - "PUT", - `/api/v1/user?name=${encodeURI(selectedUser.accessKey)}`, - { - status: enabled ? "enabled" : "disabled", - groups: selectedGroups, - policies: selectedPolicies, - } - ) - .then((res) => { - setAddLoading(false); - closeModalAndRefresh(); - }) - .catch((err: ErrorResponseHandler) => { - setAddLoading(false); - setModalErrorSnackMessage(err); - }); - } else { - api - .invoke("POST", "/api/v1/users", { - accessKey, - secretKey, - groups: selectedGroups, - policies: selectedPolicies, - }) - .then((res) => { - setAddLoading(false); - closeModalAndRefresh(); - }) - .catch((err: ErrorResponseHandler) => { - setAddLoading(false); - setModalErrorSnackMessage(err); - }); - } - }; - - const resetForm = () => { - if (selectedUser !== null) { - setSelectedGroups([]); - return; - } - setAccessKey(""); - setSecretKey(""); - setSelectedGroups([]); - }; - - const sendEnabled = - accessKey.trim() !== "" && - ((secretKey.trim() !== "" && selectedUser === null) || - selectedUser !== null); - return ( - { - closeModalAndRefresh(); - }} - modalOpen={open} - title={selectedUser !== null ? "Edit User" : "Create User"} - titleIcon={} - > - {selectedUser !== null && ( -
- { - setEnabled(e.target.checked); - }} - switchOnly - /> -
- )} - - -
) => { - saveRecord(e); - }} - > - - -
- ) => { - setAccessKey(e.target.value); - }} - disabled={selectedUser !== null} - /> -
- - {selectedUser !== null ? ( - - ) : ( -
- ) => { - setSecretKey(e.target.value); - }} - autoComplete="current-password" - /> -
- )} - - { - setCurrenTab(nv); - }} - > - - - - - - - - - - - - { - setSelectedGroups(elements); - }} - /> - - - - {addLoading && ( - - - - )} -
- - - - - -
-
-
-
- ); -}; - -const mapDispatchToProps = { - setModalErrorSnackMessage, -}; - -const connector = connect(null, mapDispatchToProps); - -export default withStyles(styles)(connector(AddUser)); diff --git a/portal-ui/src/screens/Console/Users/AddUserHelpBox.tsx b/portal-ui/src/screens/Console/Users/AddUserHelpBox.tsx new file mode 100644 index 000000000..5fc962482 --- /dev/null +++ b/portal-ui/src/screens/Console/Users/AddUserHelpBox.tsx @@ -0,0 +1,119 @@ +// 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, + UsersIcon, + ChangeAccessPolicyIcon, + GroupsIcon, +} from "../../../icons"; + +const FeatureItem = ({ + icon, + description, +}: { + icon: any; + description: string; +}) => { + return ( + + {icon}{" "} +
+ {description} +
+
+ ); +}; +const AddUserHelpBox = ({ hasMargin = true }: { hasMargin?: boolean }) => { + return ( + + + +
Learn more about the Users feature
+
+ + A MinIO user consists of a unique access key (username) and + corresponding secret key (password). Clients must authenticate their + identity by specifying both a valid access key (username) and the + corresponding secret key (password) of an existing MinIO user. +
+
+ Each user can have one or more assigned policies that explicitly list + the actions and resources to which that user has access. Users can also + inherit policies from the groups in which they have membership. +
+
+ + + } description={`Create Users`} /> + } description={`Manage Groups`} /> + } + description={`Assign Policies`} + /> + +
+ ); +}; + +export default AddUserHelpBox; diff --git a/portal-ui/src/screens/Console/Users/AddUserScreen.tsx b/portal-ui/src/screens/Console/Users/AddUserScreen.tsx new file mode 100644 index 000000000..956d317c8 --- /dev/null +++ b/portal-ui/src/screens/Console/Users/AddUserScreen.tsx @@ -0,0 +1,304 @@ +// 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 } 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, LinearProgress, Box } from "@mui/material"; +import { CreateUserIcon } from "../../../icons"; + +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 AddUserHelpBox from "./AddUserHelpBox"; +import PolicySelectors from "../Policies/PolicySelectors"; +import BackLink from "../../../common/BackLink"; +import GroupsSelectors from "./GroupsSelectors"; +import { connect } from "react-redux"; +import { User } from "./types"; + +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 { setErrorSnackMessage } from "../../../../src/actions"; + +interface IAddUserProps { + classes: any; + setErrorSnackMessage: typeof setErrorSnackMessage; + selectedUser: User | null; +} + +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: 16, + paddingLeft: 8, + }, + ...formFieldStyles, + ...modalStyleUtils, + }); + +const AddUser = ({ + classes, + setErrorSnackMessage, +}: IAddUserProps) => { + const [addLoading, setAddLoading] = useState(false); + const [accessKey, setAccessKey] = useState(""); + const [secretKey, setSecretKey] = useState(""); + const [selectedGroups, setSelectedGroups] = useState([]); + const [selectedPolicies, setSelectedPolicies] = useState([]); + const [showPassword, setShowPassword] = useState(false); + + const sendEnabled = accessKey.trim() !== ""; + + const saveRecord = (event: React.FormEvent) => { + event.preventDefault(); + + if (secretKey.length < 8) { + setErrorSnackMessage({ + errorMessage: "Passwords must be at least 8 characters long", + detailedError: "", + }); + setAddLoading(false); + return; + } + + if (addLoading) { + return; + } + setAddLoading(true); + api + .invoke("POST", "/api/v1/users", { + accessKey, + secretKey, + groups: selectedGroups, + policies: selectedPolicies, + }) + .then((res) => { + setAddLoading(false); + history.push(`${IAM_PAGES.USERS}`); + }) + .catch((err: ErrorResponseHandler) => { + setAddLoading(false); + setErrorSnackMessage(err); + }); + }; + + const resetForm = () => { + setSelectedGroups([]); + setAccessKey(""); + setSecretKey(""); + setSelectedPolicies([]); + setShowPassword(false); + }; + + return ( + + + } /> + + + + + + + Create User + + + + + + +
) => { + saveRecord(e); + }} + > + + +
+ + ) => { + setAccessKey(e.target.value); + }} + /> +
+
+ + ) => { + setSecretKey(e.target.value); + }} + autoComplete="current-password" + overlayIcon={ + showPassword ? ( + + ) : ( + + ) + } + overlayAction={() => setShowPassword(!showPassword)} + /> +
+ + + + + + { + setSelectedGroups(elements); + }} + /> + + + {addLoading && ( + + + + )} +
+ + + + + +
+
+
+
+ + + + + +
+
+
+
+ ); +}; + +const mapDispatchToProps = { + setErrorSnackMessage, +}; + +const connector = connect(null, mapDispatchToProps); + +export default withStyles(styles)(connector(AddUser)); diff --git a/portal-ui/src/screens/Console/Users/GroupsSelectors.tsx b/portal-ui/src/screens/Console/Users/GroupsSelectors.tsx index f112113e0..7bbb363c3 100644 --- a/portal-ui/src/screens/Console/Users/GroupsSelectors.tsx +++ b/portal-ui/src/screens/Console/Users/GroupsSelectors.tsx @@ -145,7 +145,7 @@ const GroupsSelectors = ({
import("./AddUser"))); const SetPolicy = withSuspense( React.lazy(() => import("../Policies/SetPolicy")) ); @@ -83,8 +82,6 @@ interface IUsersProps { const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); - const [addScreenOpen, setAddScreenOpen] = useState(false); - const [deleteOpen, setDeleteOpen] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [addGroupOpen, setAddGroupOpen] = useState(false); @@ -108,12 +105,7 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP, ]); - const closeAddModalAndRefresh = () => { - setAddScreenOpen(false); - setLoading(true); - }; - - const closeDeleteModalAndRefresh = (refresh: boolean) => { + const closeDeleteModalAndRefresh = (refresh: boolean) => { setDeleteOpen(false); if (refresh) { setLoading(true); @@ -201,15 +193,6 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { return ( - {addScreenOpen && ( - { - closeAddModalAndRefresh(); - }} - /> - )} {policyOpen && ( { icon={} color="primary" onClick={() => { - setAddScreenOpen(true); - setSelectedUser(null); + history.push(`${IAM_PAGES.USER_ADD}`); }} variant={"contained"} /> @@ -389,8 +371,7 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { To get started,{" "} { - setAddScreenOpen(true); - setSelectedUser(null); + history.push(`${IAM_PAGES.USER_ADD}`); }} > Create a User diff --git a/portal-ui/src/screens/Console/Users/Users.tsx b/portal-ui/src/screens/Console/Users/Users.tsx index 1749a861e..ef92bf5f0 100644 --- a/portal-ui/src/screens/Console/Users/Users.tsx +++ b/portal-ui/src/screens/Console/Users/Users.tsx @@ -25,6 +25,7 @@ import NotFoundPage from "../../NotFoundPage"; import ListUsers from "./ListUsers"; import UserDetails from "./UserDetails"; import { IAM_PAGES } from "../../../common/SecureComponent/permissions"; +import AddUserScreen from "./AddUserScreen"; const mapState = (state: AppState) => ({ open: state.system.sidebarOpen, @@ -38,6 +39,7 @@ const Users = () => { +