github.com/minio/console@v1.4.1/web-app/src/screens/Console/Account/Account.tsx (about) 1 // This file is part of MinIO Console Server 2 // Copyright (c) 2021 MinIO, Inc. 3 // 4 // This program is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Affero General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // This program is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Affero General Public License for more details. 13 // 14 // You should have received a copy of the GNU Affero General Public License 15 // along with this program. If not, see <http://www.gnu.org/licenses/>. 16 17 import React, { Fragment, useEffect, useState } from "react"; 18 import { 19 AccountIcon, 20 AddIcon, 21 Box, 22 Button, 23 DataTable, 24 DeleteIcon, 25 Grid, 26 HelpBox, 27 PageLayout, 28 PasswordKeyIcon, 29 } from "mds"; 30 import { useSelector } from "react-redux"; 31 import { useNavigate } from "react-router-dom"; 32 import { actionsTray } from "../Common/FormComponents/common/styleLibrary"; 33 34 import ChangePasswordModal from "./ChangePasswordModal"; 35 import SearchBox from "../Common/SearchBox"; 36 import withSuspense from "../Common/Components/withSuspense"; 37 38 import { selectSAs } from "../Configurations/utils"; 39 import DeleteMultipleServiceAccounts from "../Users/DeleteMultipleServiceAccounts"; 40 import EditServiceAccount from "./EditServiceAccount"; 41 42 import { selFeatures } from "../consoleSlice"; 43 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 44 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 45 import { api } from "api"; 46 import { errorToHandler } from "api/errors"; 47 import HelpMenu from "../HelpMenu"; 48 import { ACCOUNT_TABLE_COLUMNS } from "./AccountUtils"; 49 import { useAppDispatch } from "store"; 50 import { ServiceAccounts } from "api/consoleApi"; 51 import { 52 setErrorSnackMessage, 53 setHelpName, 54 setSnackBarMessage, 55 } from "systemSlice"; 56 import { usersSort } from "utils/sortFunctions"; 57 import { SecureComponent } from "common/SecureComponent"; 58 import { 59 CONSOLE_UI_RESOURCE, 60 IAM_PAGES, 61 IAM_SCOPES, 62 } from "common/SecureComponent/permissions"; 63 64 const DeleteServiceAccount = withSuspense( 65 React.lazy(() => import("./DeleteServiceAccount")), 66 ); 67 68 const Account = () => { 69 const dispatch = useAppDispatch(); 70 const navigate = useNavigate(); 71 72 const features = useSelector(selFeatures); 73 74 const [records, setRecords] = useState<ServiceAccounts>([]); 75 const [loading, setLoading] = useState<boolean>(false); 76 const [filter, setFilter] = useState<string>(""); 77 const [deleteOpen, setDeleteOpen] = useState<boolean>(false); 78 const [selectedServiceAccount, setSelectedServiceAccount] = useState< 79 string | null 80 >(null); 81 const [changePasswordModalOpen, setChangePasswordModalOpen] = 82 useState<boolean>(false); 83 const [selectedSAs, setSelectedSAs] = useState<string[]>([]); 84 const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false); 85 const [isEditOpen, setIsEditOpen] = useState<boolean>(false); 86 87 const userIDP = (features && features.includes("external-idp")) || false; 88 89 useEffect(() => { 90 fetchRecords(); 91 }, []); 92 93 useEffect(() => { 94 dispatch(setHelpName("accessKeys")); 95 // eslint-disable-next-line react-hooks/exhaustive-deps 96 }, []); 97 98 useEffect(() => { 99 if (loading) { 100 api.serviceAccounts 101 .listUserServiceAccounts() 102 .then((res) => { 103 setLoading(false); 104 const sortedRows = res.data.sort(usersSort); 105 setRecords(sortedRows); 106 }) 107 .catch((res) => { 108 dispatch( 109 setErrorSnackMessage( 110 errorToHandler(res?.error || "Error retrieving access keys"), 111 ), 112 ); 113 setLoading(false); 114 }); 115 } 116 }, [loading, setLoading, setRecords, dispatch]); 117 118 const fetchRecords = () => { 119 setLoading(true); 120 }; 121 122 const closeDeleteModalAndRefresh = (refresh: boolean) => { 123 setDeleteOpen(false); 124 125 if (refresh) { 126 setSelectedSAs([]); 127 fetchRecords(); 128 } 129 }; 130 131 const closeDeleteMultipleModalAndRefresh = (refresh: boolean) => { 132 setDeleteMultipleOpen(false); 133 if (refresh) { 134 dispatch(setSnackBarMessage(`Access keys deleted successfully.`)); 135 setSelectedSAs([]); 136 setLoading(true); 137 } 138 }; 139 140 const editModalOpen = (selectedServiceAccount: string) => { 141 setSelectedServiceAccount(selectedServiceAccount); 142 setIsEditOpen(true); 143 }; 144 145 const closePolicyModal = () => { 146 setIsEditOpen(false); 147 setLoading(true); 148 }; 149 150 const confirmDeleteServiceAccount = (selectedServiceAccount: string) => { 151 setSelectedServiceAccount(selectedServiceAccount); 152 setDeleteOpen(true); 153 }; 154 155 const tableActions = [ 156 { 157 type: "view", 158 onClick: (value: any) => { 159 if (value) { 160 editModalOpen(value.accessKey); 161 } 162 }, 163 }, 164 { 165 type: "delete", 166 onClick: (value: any) => { 167 if (value) { 168 confirmDeleteServiceAccount(value.accessKey); 169 } 170 }, 171 }, 172 { 173 type: "edit", 174 onClick: (value: any) => { 175 if (value) { 176 editModalOpen(value.accessKey); 177 } 178 }, 179 }, 180 ]; 181 182 const filteredRecords = records.filter((elementItem) => 183 elementItem?.accessKey?.toLowerCase().includes(filter.toLowerCase()), 184 ); 185 186 return ( 187 <React.Fragment> 188 {deleteOpen && ( 189 <DeleteServiceAccount 190 deleteOpen={deleteOpen} 191 selectedServiceAccount={selectedServiceAccount} 192 closeDeleteModalAndRefresh={(refresh: boolean) => { 193 closeDeleteModalAndRefresh(refresh); 194 }} 195 /> 196 )} 197 {deleteMultipleOpen && ( 198 <DeleteMultipleServiceAccounts 199 deleteOpen={deleteMultipleOpen} 200 selectedSAs={selectedSAs} 201 closeDeleteModalAndRefresh={closeDeleteMultipleModalAndRefresh} 202 /> 203 )} 204 205 {isEditOpen && ( 206 <EditServiceAccount 207 open={isEditOpen} 208 selectedAccessKey={selectedServiceAccount} 209 closeModalAndRefresh={closePolicyModal} 210 /> 211 )} 212 <ChangePasswordModal 213 open={changePasswordModalOpen} 214 closeModal={() => setChangePasswordModalOpen(false)} 215 /> 216 <PageHeaderWrapper label="Access Keys" actions={<HelpMenu />} /> 217 218 <PageLayout> 219 <Grid container> 220 <Grid item xs={12} sx={{ ...actionsTray.actionsTray }}> 221 <SearchBox 222 placeholder={"Search Access Keys"} 223 onChange={setFilter} 224 sx={{ marginRight: "auto", maxWidth: 380 }} 225 value={filter} 226 /> 227 <Box 228 sx={{ 229 display: "flex", 230 flexWrap: "nowrap", 231 gap: 5, 232 }} 233 > 234 <TooltipWrapper tooltip={"Delete Selected"}> 235 <Button 236 id={"delete-selected-accounts"} 237 onClick={() => { 238 setDeleteMultipleOpen(true); 239 }} 240 label={"Delete Selected"} 241 icon={<DeleteIcon />} 242 disabled={selectedSAs.length === 0} 243 variant={"secondary"} 244 /> 245 </TooltipWrapper> 246 <SecureComponent 247 scopes={[IAM_SCOPES.ADMIN_CREATE_USER]} 248 resource={CONSOLE_UI_RESOURCE} 249 matchAll 250 errorProps={{ disabled: true }} 251 > 252 <Button 253 id={"change-password"} 254 onClick={() => setChangePasswordModalOpen(true)} 255 label={`Change Password`} 256 icon={<PasswordKeyIcon />} 257 variant={"regular"} 258 disabled={userIDP} 259 /> 260 </SecureComponent> 261 <SecureComponent 262 scopes={[IAM_SCOPES.ADMIN_CREATE_SERVICEACCOUNT]} 263 resource={CONSOLE_UI_RESOURCE} 264 matchAll 265 errorProps={{ disabled: true }} 266 > 267 <Button 268 id={"create-service-account"} 269 onClick={() => { 270 navigate(`${IAM_PAGES.ACCOUNT_ADD}`); 271 }} 272 label={`Create access key`} 273 icon={<AddIcon />} 274 variant={"callAction"} 275 /> 276 </SecureComponent> 277 </Box> 278 </Grid> 279 280 <Grid item xs={12}> 281 <DataTable 282 itemActions={tableActions} 283 entityName={"Access Keys"} 284 columns={ACCOUNT_TABLE_COLUMNS} 285 onSelect={(e) => selectSAs(e, setSelectedSAs, selectedSAs)} 286 selectedItems={selectedSAs} 287 isLoading={loading} 288 records={filteredRecords} 289 idField="accessKey" 290 /> 291 </Grid> 292 <Grid item xs={12} sx={{ marginTop: 15 }}> 293 <HelpBox 294 title={"Learn more about ACCESS KEYS"} 295 iconComponent={<AccountIcon />} 296 help={ 297 <Fragment> 298 MinIO access keys are child identities of an authenticated 299 MinIO user, including externally managed identities. Each 300 access key inherits its privileges based on the policies 301 attached to it’s parent user or those groups in which the 302 parent user has membership. Access Keys also support an 303 optional inline policy which further restricts access to a 304 subset of actions and resources available to the parent user. 305 <br /> 306 <br /> 307 You can learn more at our{" "} 308 <a 309 href="https://min.io/docs/minio/linux/administration/identity-access-management/minio-user-management.html?ref=con#id3" 310 target="_blank" 311 rel="noopener" 312 > 313 documentation 314 </a> 315 . 316 </Fragment> 317 } 318 /> 319 </Grid> 320 </Grid> 321 </PageLayout> 322 </React.Fragment> 323 ); 324 }; 325 326 export default Account;