github.com/minio/console@v1.4.1/web-app/src/screens/Console/Users/UserDetails.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, useCallback, useEffect, useState } from "react"; 18 import { useNavigate, useParams } from "react-router-dom"; 19 import { 20 AddIcon, 21 BackLink, 22 Box, 23 Button, 24 DataTable, 25 Grid, 26 IAMPoliciesIcon, 27 PageLayout, 28 PasswordKeyIcon, 29 ScreenTitle, 30 SectionTitle, 31 Switch, 32 Tabs, 33 TrashIcon, 34 UsersIcon, 35 } from "mds"; 36 import { IPolicyItem } from "./types"; 37 import { ErrorResponseHandler } from "../../../common/types"; 38 import { decodeURLString, encodeURLString } from "../../../common/utils"; 39 import { setHelpName, setModalErrorSnackMessage } from "../../../systemSlice"; 40 import { 41 assignGroupPermissions, 42 assignIAMPolicyPermissions, 43 CONSOLE_UI_RESOURCE, 44 deleteUserPermissions, 45 disableUserPermissions, 46 editServiceAccountPermissions, 47 enableDisableUserPermissions, 48 enableUserPermissions, 49 getGroupPermissions, 50 IAM_PAGES, 51 permissionTooltipHelper, 52 } from "../../../common/SecureComponent/permissions"; 53 import { hasPermission } from "../../../common/SecureComponent"; 54 import { useAppDispatch } from "../../../store"; 55 import { policyDetailsSort } from "../../../utils/sortFunctions"; 56 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 57 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 58 import HelpMenu from "../HelpMenu"; 59 import api from "../../../common/api"; 60 import ChangeUserGroups from "./ChangeUserGroups"; 61 import SetUserPolicies from "./SetUserPolicies"; 62 import UserServiceAccountsPanel from "./UserServiceAccountsPanel"; 63 import ChangeUserPasswordModal from "../Account/ChangeUserPasswordModal"; 64 import DeleteUser from "./DeleteUser"; 65 66 interface IGroupItem { 67 group: string; 68 } 69 70 const UserDetails = () => { 71 const dispatch = useAppDispatch(); 72 const params = useParams(); 73 const navigate = useNavigate(); 74 75 const [loading, setLoading] = useState<boolean>(false); 76 const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false); 77 const [policyOpen, setPolicyOpen] = useState<boolean>(false); 78 const [addLoading, setAddLoading] = useState<boolean>(false); 79 const [enabled, setEnabled] = useState<boolean>(false); 80 const [selectedGroups, setSelectedGroups] = useState<string[]>([]); 81 const [currentGroups, setCurrentGroups] = useState<IGroupItem[]>([]); 82 const [currentPolicies, setCurrentPolicies] = useState<IPolicyItem[]>([]); 83 const [changeUserPasswordModalOpen, setChangeUserPasswordModalOpen] = 84 useState<boolean>(false); 85 const [deleteOpen, setDeleteOpen] = useState<boolean>(false); 86 const [hasPolicy, setHasPolicy] = useState<boolean>(false); 87 const [selectedTab, setSelectedTab] = useState<string>("groups"); 88 89 const enableEnabled = 90 hasPermission(CONSOLE_UI_RESOURCE, enableUserPermissions) && !enabled; 91 const disableEnabled = 92 hasPermission(CONSOLE_UI_RESOURCE, disableUserPermissions) && enabled; 93 94 const userName = decodeURLString(params.userName || ""); 95 96 const changeUserPassword = () => { 97 setChangeUserPasswordModalOpen(true); 98 }; 99 100 const deleteUser = () => { 101 setDeleteOpen(true); 102 }; 103 104 const userLoggedIn = localStorage.getItem("userLoggedIn") || ""; 105 const canAssignPolicy = hasPermission( 106 CONSOLE_UI_RESOURCE, 107 assignIAMPolicyPermissions, 108 true, 109 ); 110 const canAssignGroup = hasPermission( 111 CONSOLE_UI_RESOURCE, 112 assignGroupPermissions, 113 true, 114 ); 115 116 const viewGroup = hasPermission(CONSOLE_UI_RESOURCE, getGroupPermissions); 117 118 const getUserInformation = useCallback(() => { 119 if (userName === "") { 120 return null; 121 } 122 setLoading(true); 123 api 124 .invoke("GET", `/api/v1/user/${encodeURLString(userName)}`) 125 .then((res) => { 126 setAddLoading(false); 127 const memberOf = res.memberOf || []; 128 setSelectedGroups(memberOf); 129 130 const currentGroups: IGroupItem[] = memberOf.map((group: string) => { 131 return { 132 group: group, 133 }; 134 }); 135 136 setCurrentGroups(currentGroups); 137 const currentPolicies: IPolicyItem[] = res.policy.map( 138 (policy: string) => { 139 return { 140 policy: policy, 141 }; 142 }, 143 ); 144 145 currentPolicies.sort(policyDetailsSort); 146 147 setCurrentPolicies(currentPolicies); 148 setEnabled(res.status === "enabled"); 149 setHasPolicy(res.hasPolicy); 150 setLoading(false); 151 }) 152 .catch((err: ErrorResponseHandler) => { 153 setAddLoading(false); 154 setLoading(false); 155 dispatch(setModalErrorSnackMessage(err)); 156 }); 157 }, [userName, dispatch]); 158 159 const saveRecord = (isEnabled: boolean) => { 160 if (addLoading) { 161 return; 162 } 163 setAddLoading(true); 164 api 165 .invoke("PUT", `/api/v1/user/${encodeURLString(userName)}`, { 166 status: isEnabled ? "enabled" : "disabled", 167 groups: selectedGroups, 168 }) 169 .then((_) => { 170 setAddLoading(false); 171 }) 172 .catch((err: ErrorResponseHandler) => { 173 setAddLoading(false); 174 dispatch(setModalErrorSnackMessage(err)); 175 }); 176 }; 177 178 useEffect(() => { 179 dispatch(setHelpName("user_details_groups")); 180 // eslint-disable-next-line react-hooks/exhaustive-deps 181 }, []); 182 183 useEffect(() => { 184 getUserInformation(); 185 }, [getUserInformation]); 186 187 const closeDeleteModalAndRefresh = (refresh: boolean) => { 188 setDeleteOpen(false); 189 if (refresh) { 190 getUserInformation(); 191 } 192 }; 193 194 const groupViewAction = (group: any) => { 195 navigate(`${IAM_PAGES.GROUPS}/${encodeURLString(group.group)}`); 196 }; 197 198 const groupTableActions = [ 199 { 200 type: "view", 201 onClick: groupViewAction, 202 disableButtonFunction: () => !viewGroup, 203 }, 204 ]; 205 206 return ( 207 <Fragment> 208 {addGroupOpen && ( 209 <ChangeUserGroups 210 open={addGroupOpen} 211 selectedUser={userName} 212 closeModalAndRefresh={() => { 213 setAddGroupOpen(false); 214 getUserInformation(); 215 }} 216 /> 217 )} 218 {policyOpen && ( 219 <SetUserPolicies 220 open={policyOpen} 221 selectedUser={userName} 222 currentPolicies={currentPolicies} 223 closeModalAndRefresh={() => { 224 setPolicyOpen(false); 225 getUserInformation(); 226 }} 227 /> 228 )} 229 {deleteOpen && ( 230 <DeleteUser 231 deleteOpen={deleteOpen} 232 selectedUsers={[userName]} 233 closeDeleteModalAndRefresh={(refresh: boolean) => { 234 closeDeleteModalAndRefresh(refresh); 235 }} 236 /> 237 )} 238 {changeUserPasswordModalOpen && ( 239 <ChangeUserPasswordModal 240 open={changeUserPasswordModalOpen} 241 userName={userName} 242 closeModal={() => setChangeUserPasswordModalOpen(false)} 243 /> 244 )} 245 <PageHeaderWrapper 246 label={ 247 <Fragment> 248 <BackLink 249 label={"Users"} 250 onClick={() => navigate(IAM_PAGES.USERS)} 251 /> 252 </Fragment> 253 } 254 actions={<HelpMenu />} 255 /> 256 <PageLayout> 257 <Grid container> 258 <Grid item xs={12}> 259 <ScreenTitle 260 icon={<UsersIcon width={40} />} 261 title={userName} 262 subTitle={""} 263 actions={ 264 <Fragment> 265 <span 266 style={{ 267 fontSize: ".8rem", 268 marginRight: ".5rem", 269 }} 270 > 271 User Status: 272 </span> 273 <span 274 style={{ 275 fontWeight: "bold", 276 fontSize: ".9rem", 277 marginRight: ".5rem", 278 }} 279 > 280 {enabled ? "Enabled" : "Disabled"} 281 </span> 282 <TooltipWrapper 283 tooltip={ 284 enableEnabled || disableEnabled 285 ? "" 286 : hasPermission( 287 CONSOLE_UI_RESOURCE, 288 enableUserPermissions, 289 ) 290 ? permissionTooltipHelper( 291 disableUserPermissions, 292 "disable users", 293 ) 294 : hasPermission( 295 CONSOLE_UI_RESOURCE, 296 disableUserPermissions, 297 ) 298 ? permissionTooltipHelper( 299 enableUserPermissions, 300 "enable users", 301 ) 302 : permissionTooltipHelper( 303 enableDisableUserPermissions, 304 "enable or disable users", 305 ) 306 } 307 > 308 <Switch 309 indicatorLabels={["Enabled", "Disabled"]} 310 checked={enabled} 311 value={"group_enabled"} 312 id="group-status" 313 name="group-status" 314 onChange={() => { 315 setEnabled(!enabled); 316 saveRecord(!enabled); 317 }} 318 switchOnly 319 disabled={!enableEnabled && !disableEnabled} 320 /> 321 </TooltipWrapper> 322 <TooltipWrapper 323 tooltip={ 324 hasPermission(CONSOLE_UI_RESOURCE, deleteUserPermissions) 325 ? userLoggedIn === userName 326 ? "You cannot delete the currently logged in User" 327 : "Delete User" 328 : permissionTooltipHelper( 329 deleteUserPermissions, 330 "delete user", 331 ) 332 } 333 > 334 <Button 335 id={"delete-user"} 336 onClick={deleteUser} 337 icon={<TrashIcon />} 338 variant={"secondary"} 339 disabled={ 340 !hasPermission( 341 CONSOLE_UI_RESOURCE, 342 deleteUserPermissions, 343 ) || userLoggedIn === userName 344 } 345 /> 346 </TooltipWrapper> 347 348 <TooltipWrapper tooltip={"Change Password"}> 349 <Button 350 id={"change-user-password"} 351 onClick={changeUserPassword} 352 icon={<PasswordKeyIcon />} 353 variant={"regular"} 354 disabled={userLoggedIn === userName} 355 /> 356 </TooltipWrapper> 357 </Fragment> 358 } 359 sx={{ marginBottom: 15 }} 360 /> 361 </Grid> 362 363 <Grid item xs={12}> 364 <Tabs 365 currentTabOrPath={selectedTab} 366 onTabClick={setSelectedTab} 367 options={[ 368 { 369 tabConfig: { 370 id: "groups", 371 label: "Groups", 372 disabled: !canAssignGroup, 373 }, 374 content: ( 375 <Fragment> 376 <Box 377 onMouseMove={() => 378 dispatch(setHelpName("user_details_groups")) 379 } 380 > 381 <SectionTitle 382 separator 383 sx={{ marginBottom: 15 }} 384 actions={ 385 <TooltipWrapper 386 tooltip={ 387 canAssignGroup 388 ? "Assign groups" 389 : permissionTooltipHelper( 390 assignGroupPermissions, 391 "add users to groups", 392 ) 393 } 394 > 395 <Button 396 id={"add-groups"} 397 label={"Add to Groups"} 398 onClick={() => { 399 setAddGroupOpen(true); 400 }} 401 icon={<AddIcon />} 402 variant={"callAction"} 403 disabled={!canAssignGroup} 404 /> 405 </TooltipWrapper> 406 } 407 > 408 Groups 409 </SectionTitle> 410 </Box> 411 <Grid 412 item 413 xs={12} 414 onMouseMove={() => 415 dispatch(setHelpName("user_details_groups")) 416 } 417 > 418 <DataTable 419 itemActions={groupTableActions} 420 columns={[{ label: "Name", elementKey: "group" }]} 421 isLoading={loading} 422 records={currentGroups} 423 entityName="Groups" 424 idField="group" 425 /> 426 </Grid> 427 </Fragment> 428 ), 429 }, 430 { 431 tabConfig: { 432 id: "service_accounts", 433 label: "Service Accounts", 434 disabled: !hasPermission( 435 CONSOLE_UI_RESOURCE, 436 editServiceAccountPermissions, 437 ), 438 }, 439 content: ( 440 <UserServiceAccountsPanel 441 user={userName} 442 hasPolicy={hasPolicy} 443 /> 444 ), 445 }, 446 { 447 tabConfig: { 448 id: "policies", 449 label: "Policies", 450 disabled: !canAssignPolicy, 451 }, 452 content: ( 453 <Fragment> 454 <Box 455 onMouseMove={() => 456 dispatch(setHelpName("user_details_policies")) 457 } 458 > 459 <SectionTitle 460 separator 461 sx={{ marginBottom: 15 }} 462 actions={ 463 <TooltipWrapper 464 tooltip={ 465 canAssignPolicy 466 ? "Assign Policies" 467 : permissionTooltipHelper( 468 assignIAMPolicyPermissions, 469 "assign policies", 470 ) 471 } 472 > 473 <Button 474 id={"assign-policies"} 475 label={"Assign Policies"} 476 onClick={() => { 477 setPolicyOpen(true); 478 }} 479 icon={<IAMPoliciesIcon />} 480 variant={"callAction"} 481 disabled={!canAssignPolicy} 482 /> 483 </TooltipWrapper> 484 } 485 > 486 Policies 487 </SectionTitle> 488 </Box> 489 <Box> 490 <DataTable 491 itemActions={[ 492 { 493 type: "view", 494 onClick: (policy: IPolicyItem) => { 495 navigate( 496 `${IAM_PAGES.POLICIES}/${encodeURLString( 497 policy.policy, 498 )}`, 499 ); 500 }, 501 }, 502 ]} 503 columns={[{ label: "Name", elementKey: "policy" }]} 504 isLoading={loading} 505 records={currentPolicies} 506 entityName="Policies" 507 idField="policy" 508 /> 509 </Box> 510 </Fragment> 511 ), 512 }, 513 ]} 514 /> 515 </Grid> 516 </Grid> 517 </PageLayout> 518 </Fragment> 519 ); 520 }; 521 522 export default UserDetails;