github.com/minio/console@v1.4.1/web-app/src/screens/Console/Users/ListUsers.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 { useNavigate } from "react-router-dom"; 19 import { 20 AddIcon, 21 Button, 22 DeleteIcon, 23 GroupsIcon, 24 HelpBox, 25 PageLayout, 26 UsersIcon, 27 DataTable, 28 Grid, 29 ProgressBar, 30 ActionLink, 31 } from "mds"; 32 33 import { User, UsersList } from "./types"; 34 import { usersSort } from "../../../utils/sortFunctions"; 35 import { actionsTray } from "../Common/FormComponents/common/styleLibrary"; 36 import { ErrorResponseHandler } from "../../../common/types"; 37 import { encodeURLString } from "../../../common/utils"; 38 import { 39 addUserToGroupPermissions, 40 CONSOLE_UI_RESOURCE, 41 deleteUserPermissions, 42 IAM_PAGES, 43 IAM_SCOPES, 44 listUsersPermissions, 45 permissionTooltipHelper, 46 S3_ALL_RESOURCES, 47 viewUserPermissions, 48 } from "../../../common/SecureComponent/permissions"; 49 import api from "../../../common/api"; 50 import SearchBox from "../Common/SearchBox"; 51 import withSuspense from "../Common/Components/withSuspense"; 52 53 import { 54 hasPermission, 55 SecureComponent, 56 } from "../../../common/SecureComponent"; 57 import { setErrorSnackMessage, setHelpName } from "../../../systemSlice"; 58 import { useAppDispatch } from "../../../store"; 59 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 60 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 61 import HelpMenu from "../HelpMenu"; 62 63 const DeleteUser = withSuspense(React.lazy(() => import("./DeleteUser"))); 64 const AddToGroup = withSuspense(React.lazy(() => import("./BulkAddToGroup"))); 65 66 const ListUsers = () => { 67 const dispatch = useAppDispatch(); 68 const navigate = useNavigate(); 69 70 const [records, setRecords] = useState<User[]>([]); 71 const [loading, setLoading] = useState<boolean>(true); 72 const [deleteOpen, setDeleteOpen] = useState<boolean>(false); 73 const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false); 74 const [filter, setFilter] = useState<string>(""); 75 const [checkedUsers, setCheckedUsers] = useState<string[]>([]); 76 77 const displayListUsers = hasPermission( 78 CONSOLE_UI_RESOURCE, 79 listUsersPermissions, 80 ); 81 82 const viewUser = hasPermission(CONSOLE_UI_RESOURCE, viewUserPermissions); 83 84 const addUserToGroup = hasPermission( 85 CONSOLE_UI_RESOURCE, 86 addUserToGroupPermissions, 87 ); 88 89 const deleteUser = hasPermission(CONSOLE_UI_RESOURCE, deleteUserPermissions); 90 91 const closeDeleteModalAndRefresh = (refresh: boolean) => { 92 setDeleteOpen(false); 93 if (refresh) { 94 setLoading(true); 95 setCheckedUsers([]); 96 } 97 }; 98 99 const closeAddGroupBulk = (unCheckAll: boolean = false) => { 100 setAddGroupOpen(false); 101 if (unCheckAll) { 102 setCheckedUsers([]); 103 } 104 }; 105 106 useEffect(() => { 107 if (loading) { 108 if (displayListUsers) { 109 api 110 .invoke("GET", `/api/v1/users`) 111 .then((res: UsersList) => { 112 const users = res.users === null ? [] : res.users; 113 114 setLoading(false); 115 setRecords(users.sort(usersSort)); 116 }) 117 .catch((err: ErrorResponseHandler) => { 118 setLoading(false); 119 dispatch(setErrorSnackMessage(err)); 120 }); 121 } else { 122 setLoading(false); 123 } 124 } 125 }, [loading, dispatch, displayListUsers]); 126 127 const filteredRecords = records.filter((elementItem) => 128 elementItem.accessKey.includes(filter), 129 ); 130 131 const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { 132 const { target: { value = "", checked = false } = {} } = e; 133 134 let elements: string[] = [...checkedUsers]; // We clone the checkedUsers array 135 136 if (checked) { 137 // If the user has checked this field we need to push this to checkedUsersList 138 elements.push(value); 139 } else { 140 // User has unchecked this field, we need to remove it from the list 141 elements = elements.filter((element) => element !== value); 142 } 143 144 setCheckedUsers(elements); 145 146 return elements; 147 }; 148 149 const viewAction = (selectionElement: any): void => { 150 navigate( 151 `${IAM_PAGES.USERS}/${encodeURLString(selectionElement.accessKey)}`, 152 ); 153 }; 154 155 const tableActions = [ 156 { 157 type: "view", 158 onClick: viewAction, 159 disableButtonFunction: () => !viewUser, 160 }, 161 { 162 type: "edit", 163 onClick: viewAction, 164 disableButtonFunction: () => !viewUser, 165 }, 166 ]; 167 168 useEffect(() => { 169 dispatch(setHelpName("list_users")); 170 // eslint-disable-next-line react-hooks/exhaustive-deps 171 }, []); 172 173 return ( 174 <Fragment> 175 {deleteOpen && ( 176 <DeleteUser 177 deleteOpen={deleteOpen} 178 selectedUsers={checkedUsers} 179 closeDeleteModalAndRefresh={(refresh: boolean) => { 180 closeDeleteModalAndRefresh(refresh); 181 }} 182 /> 183 )} 184 {addGroupOpen && ( 185 <AddToGroup 186 open={addGroupOpen} 187 checkedUsers={checkedUsers} 188 closeModalAndRefresh={(close: boolean) => { 189 closeAddGroupBulk(close); 190 }} 191 /> 192 )} 193 <PageHeaderWrapper label={"Users"} actions={<HelpMenu />} /> 194 195 <PageLayout> 196 <Grid container> 197 <Grid item xs={12} sx={actionsTray.actionsTray}> 198 <SearchBox 199 placeholder={"Search Users"} 200 onChange={setFilter} 201 value={filter} 202 sx={{ 203 marginRight: "auto", 204 maxWidth: 380, 205 }} 206 /> 207 <SecureComponent 208 resource={CONSOLE_UI_RESOURCE} 209 scopes={[IAM_SCOPES.ADMIN_DELETE_USER]} 210 matchAll 211 errorProps={{ disabled: true }} 212 > 213 <TooltipWrapper 214 tooltip={ 215 hasPermission("console", [IAM_SCOPES.ADMIN_DELETE_USER]) 216 ? checkedUsers.length === 0 217 ? "Select Users to delete" 218 : "Delete Selected" 219 : permissionTooltipHelper( 220 [IAM_SCOPES.ADMIN_DELETE_USER], 221 "delete users", 222 ) 223 } 224 > 225 <Button 226 id={"delete-selected-users"} 227 onClick={() => { 228 setDeleteOpen(true); 229 }} 230 label={"Delete Selected"} 231 icon={<DeleteIcon />} 232 disabled={checkedUsers.length === 0} 233 variant={"secondary"} 234 aria-label="delete-selected-users" 235 /> 236 </TooltipWrapper> 237 </SecureComponent> 238 <SecureComponent 239 scopes={[IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP]} 240 resource={CONSOLE_UI_RESOURCE} 241 errorProps={{ disabled: true }} 242 > 243 <TooltipWrapper 244 tooltip={ 245 hasPermission("console", [IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP]) 246 ? checkedUsers.length === 0 247 ? "Select Users to group" 248 : "Add to Group" 249 : permissionTooltipHelper( 250 [IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP], 251 "add users to groups", 252 ) 253 } 254 > 255 <Button 256 id={"add-to-group"} 257 label={"Add to Group"} 258 icon={<GroupsIcon />} 259 disabled={checkedUsers.length <= 0} 260 onClick={() => { 261 if (checkedUsers.length > 0) { 262 setAddGroupOpen(true); 263 } 264 }} 265 variant={"regular"} 266 /> 267 </TooltipWrapper> 268 </SecureComponent> 269 <SecureComponent 270 scopes={[ 271 IAM_SCOPES.ADMIN_CREATE_USER, 272 IAM_SCOPES.ADMIN_LIST_USER_POLICIES, 273 IAM_SCOPES.ADMIN_LIST_GROUPS, 274 ]} 275 resource={S3_ALL_RESOURCES} 276 matchAll 277 errorProps={{ disabled: true }} 278 > 279 <TooltipWrapper 280 tooltip={ 281 hasPermission( 282 "console-ui", 283 [ 284 IAM_SCOPES.ADMIN_CREATE_USER, 285 IAM_SCOPES.ADMIN_LIST_USER_POLICIES, 286 IAM_SCOPES.ADMIN_LIST_GROUPS, 287 IAM_SCOPES.ADMIN_ATTACH_USER_OR_GROUP_POLICY, 288 ], 289 true, 290 ) 291 ? "Create User" 292 : permissionTooltipHelper( 293 [ 294 IAM_SCOPES.ADMIN_CREATE_USER, 295 IAM_SCOPES.ADMIN_LIST_USER_POLICIES, 296 IAM_SCOPES.ADMIN_LIST_GROUPS, 297 IAM_SCOPES.ADMIN_ATTACH_USER_OR_GROUP_POLICY, 298 ], 299 "create users", 300 ) 301 } 302 > 303 <Button 304 id={"create-user"} 305 label={"Create User"} 306 icon={<AddIcon />} 307 onClick={() => { 308 navigate(`${IAM_PAGES.USER_ADD}`); 309 }} 310 variant={"callAction"} 311 disabled={ 312 !hasPermission( 313 "console-ui", 314 [ 315 IAM_SCOPES.ADMIN_CREATE_USER, 316 IAM_SCOPES.ADMIN_LIST_USER_POLICIES, 317 IAM_SCOPES.ADMIN_LIST_GROUPS, 318 IAM_SCOPES.ADMIN_ATTACH_USER_OR_GROUP_POLICY, 319 ], 320 true, 321 ) 322 } 323 /> 324 </TooltipWrapper> 325 </SecureComponent> 326 </Grid> 327 328 {loading && <ProgressBar />} 329 {!loading && ( 330 <Fragment> 331 {records.length > 0 && ( 332 <Fragment> 333 <Grid item xs={12} sx={{ marginBottom: 15 }}> 334 <SecureComponent 335 scopes={[IAM_SCOPES.ADMIN_LIST_USERS]} 336 resource={CONSOLE_UI_RESOURCE} 337 errorProps={{ disabled: true }} 338 > 339 <DataTable 340 itemActions={tableActions} 341 columns={[ 342 { label: "Access Key", elementKey: "accessKey" }, 343 ]} 344 onSelect={ 345 addUserToGroup || deleteUser 346 ? selectionChanged 347 : undefined 348 } 349 selectedItems={checkedUsers} 350 isLoading={loading} 351 records={filteredRecords} 352 entityName="Users" 353 idField="accessKey" 354 /> 355 </SecureComponent> 356 </Grid> 357 <HelpBox 358 title={"Users"} 359 iconComponent={<UsersIcon />} 360 help={ 361 <Fragment> 362 A MinIO user consists of a unique access key (username) 363 and corresponding secret key (password). Clients must 364 authenticate their identity by specifying both a valid 365 access key (username) and the corresponding secret key 366 (password) of an existing MinIO user. 367 <br /> 368 Groups provide a simplified method for managing shared 369 permissions among users with common access patterns and 370 workloads. 371 <br /> 372 <br /> 373 Users inherit access permissions to data and resources 374 through the groups they belong to. 375 <br /> 376 MinIO uses Policy-Based Access Control (PBAC) to define 377 the authorized actions and resources to which an 378 authenticated user has access. Each policy describes one 379 or more actions and conditions that outline the 380 permissions of a user or group of users. 381 <br /> 382 <br /> 383 Each user can access only those resources and operations 384 which are explicitly granted by the built-in role. MinIO 385 denies access to any other resource or action by 386 default. 387 <br /> 388 <br /> 389 You can learn more at our{" "} 390 <a 391 href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/minio-user-management.html?ref=con" 392 target="_blank" 393 rel="noopener" 394 > 395 documentation 396 </a> 397 . 398 </Fragment> 399 } 400 /> 401 </Fragment> 402 )} 403 {records.length === 0 && ( 404 <Grid container> 405 <Grid item xs={8}> 406 <HelpBox 407 title={"Users"} 408 iconComponent={<UsersIcon />} 409 help={ 410 <Fragment> 411 A MinIO user consists of a unique access key 412 (username) and corresponding secret key (password). 413 Clients must authenticate their identity by specifying 414 both a valid access key (username) and the 415 corresponding secret key (password) of an existing 416 MinIO user. 417 <br /> 418 Groups provide a simplified method for managing shared 419 permissions among users with common access patterns 420 and workloads. 421 <br /> 422 <br /> 423 Users inherit access permissions to data and resources 424 through the groups they belong to. 425 <br /> 426 MinIO uses Policy-Based Access Control (PBAC) to 427 define the authorized actions and resources to which 428 an authenticated user has access. Each policy 429 describes one or more actions and conditions that 430 outline the permissions of a user or group of users. 431 <br /> 432 <br /> 433 Each user can access only those resources and 434 operations which are explicitly granted by the 435 built-in role. MinIO denies access to any other 436 resource or action by default. 437 <SecureComponent 438 scopes={[ 439 IAM_SCOPES.ADMIN_CREATE_USER, 440 IAM_SCOPES.ADMIN_LIST_USER_POLICIES, 441 IAM_SCOPES.ADMIN_LIST_GROUPS, 442 ]} 443 matchAll 444 resource={CONSOLE_UI_RESOURCE} 445 > 446 <br /> 447 <br /> 448 To get started,{" "} 449 <ActionLink 450 onClick={() => { 451 navigate(`${IAM_PAGES.USER_ADD}`); 452 }} 453 > 454 Create a User 455 </ActionLink> 456 . 457 </SecureComponent> 458 </Fragment> 459 } 460 /> 461 </Grid> 462 </Grid> 463 )} 464 </Fragment> 465 )} 466 </Grid> 467 </PageLayout> 468 </Fragment> 469 ); 470 }; 471 472 export default ListUsers;