github.com/minio/console@v1.4.1/web-app/src/screens/Console/Policies/PolicyDetails.tsx (about) 1 // Copyright (c) 2021 MinIO, Inc. 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as published by 5 // the Free Software Foundation, either version 3 of the License, or 6 // (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 import React, { Fragment, useEffect, useState } from "react"; 17 import { IAMPolicy, IAMStatement } from "./types"; 18 import { useSelector } from "react-redux"; 19 import { useNavigate, useParams } from "react-router-dom"; 20 import { 21 BackLink, 22 Box, 23 Button, 24 DataTable, 25 Grid, 26 IAMPoliciesIcon, 27 PageLayout, 28 ProgressBar, 29 RefreshIcon, 30 ScreenTitle, 31 SectionTitle, 32 Tabs, 33 TrashIcon, 34 HelpTip, 35 } from "mds"; 36 import { actionsTray } from "../Common/FormComponents/common/styleLibrary"; 37 38 import { ErrorResponseHandler } from "../../../common/types"; 39 import CodeMirrorWrapper from "../Common/FormComponents/CodeMirrorWrapper/CodeMirrorWrapper"; 40 41 import { 42 CONSOLE_UI_RESOURCE, 43 createPolicyPermissions, 44 deletePolicyPermissions, 45 getGroupPermissions, 46 IAM_PAGES, 47 IAM_SCOPES, 48 listGroupPermissions, 49 listUsersPermissions, 50 permissionTooltipHelper, 51 viewPolicyPermissions, 52 viewUserPermissions, 53 } from "../../../common/SecureComponent/permissions"; 54 import { 55 hasPermission, 56 SecureComponent, 57 } from "../../../common/SecureComponent"; 58 59 import withSuspense from "../Common/Components/withSuspense"; 60 61 import PolicyView from "./PolicyView"; 62 import { decodeURLString, encodeURLString } from "../../../common/utils"; 63 import { 64 setErrorSnackMessage, 65 setHelpName, 66 setSnackBarMessage, 67 } from "../../../systemSlice"; 68 import { selFeatures } from "../consoleSlice"; 69 import { useAppDispatch } from "../../../store"; 70 import TooltipWrapper from "../Common/TooltipWrapper/TooltipWrapper"; 71 import PageHeaderWrapper from "../Common/PageHeaderWrapper/PageHeaderWrapper"; 72 import { Policy } from "../../../api/consoleApi"; 73 import { api } from "../../../api"; 74 import HelpMenu from "../HelpMenu"; 75 import SearchBox from "../Common/SearchBox"; 76 77 const DeletePolicy = withSuspense(React.lazy(() => import("./DeletePolicy"))); 78 79 const PolicyDetails = () => { 80 const dispatch = useAppDispatch(); 81 const navigate = useNavigate(); 82 const params = useParams(); 83 84 const features = useSelector(selFeatures); 85 86 const [policy, setPolicy] = useState<Policy | null>(null); 87 const [policyStatements, setPolicyStatements] = useState<IAMStatement[]>([]); 88 const [userList, setUserList] = useState<string[]>([]); 89 const [groupList, setGroupList] = useState<string[]>([]); 90 const [addLoading, setAddLoading] = useState<boolean>(false); 91 92 const policyName = decodeURLString(params.policyName || ""); 93 94 const [policyDefinition, setPolicyDefinition] = useState<string>(""); 95 const [loadingPolicy, setLoadingPolicy] = useState<boolean>(true); 96 const [filterUsers, setFilterUsers] = useState<string>(""); 97 const [loadingUsers, setLoadingUsers] = useState<boolean>(true); 98 const [filterGroups, setFilterGroups] = useState<string>(""); 99 const [loadingGroups, setLoadingGroups] = useState<boolean>(true); 100 const [deleteOpen, setDeleteOpen] = useState<boolean>(false); 101 const [selectedTab, setSelectedTab] = useState<string>("summary"); 102 103 const ldapIsEnabled = (features && features.includes("ldap-idp")) || false; 104 105 const displayGroups = hasPermission( 106 CONSOLE_UI_RESOURCE, 107 listGroupPermissions, 108 true, 109 ); 110 111 const viewGroup = hasPermission( 112 CONSOLE_UI_RESOURCE, 113 getGroupPermissions, 114 true, 115 ); 116 117 const displayUsers = hasPermission( 118 CONSOLE_UI_RESOURCE, 119 listUsersPermissions, 120 true, 121 ); 122 123 const viewUser = hasPermission( 124 CONSOLE_UI_RESOURCE, 125 viewUserPermissions, 126 true, 127 ); 128 129 const displayPolicy = hasPermission( 130 CONSOLE_UI_RESOURCE, 131 viewPolicyPermissions, 132 true, 133 ); 134 135 const canDeletePolicy = hasPermission( 136 CONSOLE_UI_RESOURCE, 137 deletePolicyPermissions, 138 true, 139 ); 140 141 const canEditPolicy = hasPermission( 142 CONSOLE_UI_RESOURCE, 143 createPolicyPermissions, 144 true, 145 ); 146 147 const saveRecord = (event: React.FormEvent) => { 148 event.preventDefault(); 149 if (addLoading) { 150 return; 151 } 152 setAddLoading(true); 153 if (canEditPolicy) { 154 api.policies 155 .addPolicy({ 156 name: policyName, 157 policy: policyDefinition, 158 }) 159 .then((_) => { 160 setAddLoading(false); 161 dispatch(setSnackBarMessage("Policy successfully updated")); 162 refreshPolicyDetails(); 163 }) 164 .catch((err) => { 165 setAddLoading(false); 166 dispatch( 167 setErrorSnackMessage({ 168 errorMessage: "There was an error updating the Policy ", 169 detailedError: 170 "There was an error updating the Policy: " + 171 (err.error.detailedMessage || "") + 172 ". Please check Policy syntax.", 173 }), 174 ); 175 }); 176 } else { 177 setAddLoading(false); 178 } 179 }; 180 181 useEffect(() => { 182 const loadUsersForPolicy = () => { 183 if (loadingUsers) { 184 if (displayUsers && !ldapIsEnabled) { 185 api.policies 186 .listUsersForPolicy(encodeURLString(policyName)) 187 .then((result) => { 188 setUserList(result.data ?? []); 189 setLoadingUsers(false); 190 }) 191 .catch((err: ErrorResponseHandler) => { 192 dispatch(setErrorSnackMessage(err)); 193 setLoadingUsers(false); 194 }); 195 } else { 196 setLoadingUsers(false); 197 } 198 } 199 }; 200 201 const loadGroupsForPolicy = () => { 202 if (loadingGroups) { 203 if (displayGroups && !ldapIsEnabled) { 204 api.policies 205 .listGroupsForPolicy(encodeURLString(policyName)) 206 .then((result) => { 207 setGroupList(result.data ?? []); 208 setLoadingGroups(false); 209 }) 210 .catch((err: ErrorResponseHandler) => { 211 dispatch(setErrorSnackMessage(err)); 212 setLoadingGroups(false); 213 }); 214 } else { 215 setLoadingGroups(false); 216 } 217 } 218 }; 219 const loadPolicyDetails = () => { 220 if (loadingPolicy) { 221 if (displayPolicy) { 222 api.policy 223 .policyInfo(encodeURLString(policyName)) 224 .then((result) => { 225 if (result.data) { 226 setPolicy(result.data); 227 setPolicyDefinition( 228 result 229 ? JSON.stringify(JSON.parse(result.data?.policy!), null, 4) 230 : "", 231 ); 232 const pol: IAMPolicy = JSON.parse(result.data?.policy!); 233 setPolicyStatements(pol.Statement); 234 } 235 setLoadingPolicy(false); 236 }) 237 .catch((err: ErrorResponseHandler) => { 238 dispatch(setErrorSnackMessage(err)); 239 setLoadingPolicy(false); 240 }); 241 } else { 242 setLoadingPolicy(false); 243 } 244 } 245 }; 246 247 if (loadingPolicy) { 248 loadPolicyDetails(); 249 loadUsersForPolicy(); 250 loadGroupsForPolicy(); 251 } 252 }, [ 253 policyName, 254 loadingPolicy, 255 loadingUsers, 256 loadingGroups, 257 setUserList, 258 setGroupList, 259 setPolicyDefinition, 260 setPolicy, 261 setLoadingUsers, 262 setLoadingGroups, 263 displayUsers, 264 displayGroups, 265 displayPolicy, 266 ldapIsEnabled, 267 dispatch, 268 ]); 269 270 const resetForm = () => { 271 setPolicyDefinition("{}"); 272 }; 273 274 const validSave = policyName.trim() !== ""; 275 276 const deletePolicy = () => { 277 setDeleteOpen(true); 278 }; 279 280 const closeDeleteModalAndRefresh = (refresh: boolean) => { 281 setDeleteOpen(false); 282 navigate(IAM_PAGES.POLICIES); 283 }; 284 285 const userViewAction = (user: any) => { 286 navigate(`${IAM_PAGES.USERS}/${encodeURLString(user)}`); 287 }; 288 const userTableActions = [ 289 { 290 type: "view", 291 onClick: userViewAction, 292 disableButtonFunction: () => !viewUser, 293 }, 294 ]; 295 296 const filteredUsers = userList.filter((elementItem) => 297 elementItem.includes(filterUsers), 298 ); 299 300 const groupViewAction = (group: any) => { 301 navigate(`${IAM_PAGES.GROUPS}/${encodeURLString(group)}`); 302 }; 303 304 const groupTableActions = [ 305 { 306 type: "view", 307 onClick: groupViewAction, 308 disableButtonFunction: () => !viewGroup, 309 }, 310 ]; 311 312 const filteredGroups = groupList.filter((elementItem) => 313 elementItem.includes(filterGroups), 314 ); 315 316 const refreshPolicyDetails = () => { 317 setLoadingUsers(true); 318 setLoadingGroups(true); 319 setLoadingPolicy(true); 320 }; 321 322 useEffect(() => { 323 dispatch(setHelpName("policy_details_summary")); 324 325 // eslint-disable-next-line react-hooks/exhaustive-deps 326 }, []); 327 328 return ( 329 <Fragment> 330 {deleteOpen && ( 331 <DeletePolicy 332 deleteOpen={deleteOpen} 333 selectedPolicy={policyName} 334 closeDeleteModalAndRefresh={closeDeleteModalAndRefresh} 335 /> 336 )} 337 <PageHeaderWrapper 338 label={ 339 <Fragment> 340 <BackLink 341 label={"Policy"} 342 onClick={() => navigate(IAM_PAGES.POLICIES)} 343 /> 344 </Fragment> 345 } 346 actions={<HelpMenu />} 347 /> 348 <PageLayout> 349 <ScreenTitle 350 icon={<IAMPoliciesIcon width={40} />} 351 title={policyName} 352 subTitle={<Fragment>IAM Policy</Fragment>} 353 actions={ 354 <Fragment> 355 <SecureComponent 356 scopes={[IAM_SCOPES.ADMIN_DELETE_POLICY]} 357 resource={CONSOLE_UI_RESOURCE} 358 errorProps={{ disabled: true }} 359 > 360 <TooltipWrapper 361 tooltip={ 362 canDeletePolicy 363 ? "" 364 : permissionTooltipHelper( 365 deletePolicyPermissions, 366 "delete Policies", 367 ) 368 } 369 > 370 <Button 371 id={"delete-policy"} 372 label={"Delete Policy"} 373 variant="secondary" 374 icon={<TrashIcon />} 375 onClick={deletePolicy} 376 disabled={!canDeletePolicy} 377 /> 378 </TooltipWrapper> 379 </SecureComponent> 380 381 <TooltipWrapper tooltip={"Refresh"}> 382 <Button 383 id={"refresh-policy"} 384 label={"Refresh"} 385 variant="regular" 386 icon={<RefreshIcon />} 387 onClick={() => { 388 refreshPolicyDetails(); 389 }} 390 /> 391 </TooltipWrapper> 392 </Fragment> 393 } 394 sx={{ marginBottom: 15 }} 395 /> 396 <Box> 397 <Tabs 398 options={[ 399 { 400 tabConfig: { 401 label: "Summary", 402 disabled: !displayPolicy, 403 id: "summary", 404 }, 405 content: ( 406 <Fragment> 407 <Grid 408 onMouseMove={() => 409 dispatch(setHelpName("policy_details_summary")) 410 } 411 > 412 <SectionTitle separator sx={{ marginBottom: 15 }}> 413 Policy Summary 414 </SectionTitle> 415 <Box withBorders> 416 <PolicyView policyStatements={policyStatements} /> 417 </Box> 418 </Grid> 419 </Fragment> 420 ), 421 }, 422 { 423 tabConfig: { 424 label: "Users", 425 disabled: !displayUsers || ldapIsEnabled, 426 id: "users", 427 }, 428 content: ( 429 <Fragment> 430 <Grid 431 onMouseMove={() => 432 dispatch(setHelpName("policy_details_users")) 433 } 434 > 435 <SectionTitle separator sx={{ marginBottom: 15 }}> 436 Users 437 </SectionTitle> 438 <Grid container> 439 {userList.length > 0 && ( 440 <Grid 441 item 442 xs={12} 443 sx={{ 444 ...actionsTray.actionsTray, 445 marginBottom: 15, 446 }} 447 > 448 <SearchBox 449 value={filterUsers} 450 placeholder={"Search Users"} 451 id="search-resource" 452 onChange={(val) => { 453 setFilterUsers(val); 454 }} 455 /> 456 </Grid> 457 )} 458 <DataTable 459 itemActions={userTableActions} 460 columns={[{ label: "Name", elementKey: "name" }]} 461 isLoading={loadingUsers} 462 records={filteredUsers} 463 entityName="Users with this Policy associated" 464 idField="name" 465 customPaperHeight={"500px"} 466 /> 467 </Grid> 468 </Grid> 469 </Fragment> 470 ), 471 }, 472 { 473 tabConfig: { 474 label: "Groups", 475 disabled: !displayGroups || ldapIsEnabled, 476 id: "groups", 477 }, 478 content: ( 479 <Fragment> 480 <Grid 481 onMouseMove={() => 482 dispatch(setHelpName("policy_details_groups")) 483 } 484 > 485 <SectionTitle separator sx={{ marginBottom: 15 }}> 486 Groups 487 </SectionTitle> 488 <Grid container> 489 {groupList.length > 0 && ( 490 <Grid 491 item 492 xs={12} 493 sx={{ 494 ...actionsTray.actionsTray, 495 marginBottom: 15, 496 }} 497 > 498 <SearchBox 499 value={filterUsers} 500 placeholder={"Search Groups"} 501 id="search-resource" 502 onChange={(val) => { 503 setFilterGroups(val); 504 }} 505 /> 506 </Grid> 507 )} 508 <DataTable 509 itemActions={groupTableActions} 510 columns={[{ label: "Name", elementKey: "name" }]} 511 isLoading={loadingGroups} 512 records={filteredGroups} 513 entityName="Groups with this Policy associated" 514 idField="name" 515 customPaperHeight={"500px"} 516 /> 517 </Grid> 518 </Grid> 519 </Fragment> 520 ), 521 }, 522 { 523 tabConfig: { 524 label: "Raw Policy", 525 disabled: !displayPolicy, 526 id: "raw-policy", 527 }, 528 content: ( 529 <Fragment> 530 <Grid 531 onMouseMove={() => 532 dispatch(setHelpName("policy_details_policy")) 533 } 534 > 535 <HelpTip 536 content={ 537 <Fragment> 538 <a 539 target="blank" 540 href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/policy-based-access-control.html#policy-document-structure" 541 > 542 Guide to access policy structure 543 </a> 544 </Fragment> 545 } 546 placement="right" 547 > 548 <SectionTitle>Raw Policy</SectionTitle> 549 </HelpTip> 550 <form 551 noValidate 552 autoComplete="off" 553 onSubmit={(e: React.FormEvent<HTMLFormElement>) => { 554 saveRecord(e); 555 }} 556 > 557 <Grid container> 558 <Grid item xs={12}> 559 <CodeMirrorWrapper 560 value={policyDefinition} 561 onChange={(value) => { 562 if (canEditPolicy) { 563 setPolicyDefinition(value); 564 } 565 }} 566 editorHeight={"350px"} 567 helptip={ 568 <Fragment> 569 <a 570 target="blank" 571 href="https://min.io/docs/minio/kubernetes/upstream/administration/identity-access-management/policy-based-access-control.html#policy-document-structure" 572 > 573 Guide to access policy structure 574 </a> 575 </Fragment> 576 } 577 /> 578 </Grid> 579 <Grid 580 item 581 xs={12} 582 sx={{ 583 display: "flex", 584 justifyContent: "flex-end", 585 paddingTop: 16, 586 gap: 8, 587 }} 588 > 589 {!policy && ( 590 <Button 591 type="button" 592 variant={"regular"} 593 id={"clear-policy"} 594 onClick={() => { 595 resetForm(); 596 }} 597 > 598 Clear 599 </Button> 600 )} 601 <SecureComponent 602 scopes={[IAM_SCOPES.ADMIN_CREATE_POLICY]} 603 resource={CONSOLE_UI_RESOURCE} 604 errorProps={{ disabled: true }} 605 > 606 <TooltipWrapper 607 tooltip={ 608 canEditPolicy 609 ? "" 610 : permissionTooltipHelper( 611 createPolicyPermissions, 612 "edit a Policy", 613 ) 614 } 615 > 616 <Button 617 id={"save"} 618 type="submit" 619 variant="callAction" 620 color="primary" 621 disabled={ 622 addLoading || !validSave || !canEditPolicy 623 } 624 label={"Save"} 625 /> 626 </TooltipWrapper> 627 </SecureComponent> 628 </Grid> 629 {addLoading && ( 630 <Grid item xs={12}> 631 <ProgressBar /> 632 </Grid> 633 )} 634 </Grid> 635 </form> 636 </Grid> 637 </Fragment> 638 ), 639 }, 640 ]} 641 currentTabOrPath={selectedTab} 642 onTabClick={(tab) => setSelectedTab(tab)} 643 /> 644 </Box> 645 </PageLayout> 646 </Fragment> 647 ); 648 }; 649 650 export default PolicyDetails;