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;