blob: 5a0eeb602d0a32aa679c1615ff1da604b6ff4f1c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { useState, useCallback, useRef, useEffect } from "react";
import {
Link,
useParams,
useNavigate,
useLocation,
useSearchParams
} from "react-router-dom";
import { Badge, Button, Col, Row, Modal, Alert } from "react-bootstrap";
import moment from "moment-timezone";
import { toast } from "react-toastify";
import {
pick,
indexOf,
isUndefined,
isEmpty,
map,
sortBy,
find,
concat,
camelCase,
union
} from "lodash";
import { fetchApi } from "Utils/fetchAPI";
import XATableLayout from "Components/XATableLayout";
import {
showGroupsOrUsersOrRolesForPolicy,
QueryParamsName
} from "Utils/XAUtils";
import { MoreLess, scrollToNewData } from "Components/CommonComponents";
import {} from "Utils/XAUtils";
import PolicyViewDetails from "../AuditEvent/AdminLogs/PolicyViewDetails";
import StructuredFilter from "../../components/structured-filter/react-typeahead/tokenizer";
import {
isAuditor,
isKMSAuditor,
isPolicyExpired,
isSystemAdmin,
isKeyAdmin,
isUser,
parseSearchFilter,
getResourcesDefVal
} from "../../utils/XAUtils";
import {
alertMessage,
ResourcesOverrideInfoMsg,
ServerAttrName
} from "../../utils/XAEnums";
import {
BlockUi,
CustomPopover,
Loader
} from "../../components/CommonComponents";
function PolicyListing(props) {
const { serviceDef, serviceData, serviceZone } = props;
const { state } = useLocation();
const [policyListingData, setPolicyData] = useState([]);
const [loader, setLoader] = useState(true);
const [pageCount, setPageCount] = useState(
state && state.showLastPage ? state.addPageData.totalPage : 0
);
const [currentpageIndex, setCurrentPageIndex] = useState(
state && state.showLastPage ? state.addPageData.totalPage - 1 : 0
);
const [currentpageSize, setCurrentPageSize] = useState(
state && state.showLastPage ? state.addPageData.pageSize : 25
);
const [totalCount, setTotalCount] = useState(0);
const [tblpageData, setTblPageData] = useState({
totalPage: 0,
pageRecords: 0,
pageSize: 25
});
const fetchIdRef = useRef(0);
const [deletePolicyModal, setConfirmModal] = useState({
policyDetails: {},
showSyncDetails: false
});
const [policyviewmodal, setPolicyViewModal] = useState(false);
const [policyParamsData, setPolicyParamsData] = useState(null);
const [updateTable, setUpdateTable] = useState(moment.now());
const [currentPage, setCurrentPage] = useState(1);
const [searchFilterParams, setSearchFilterParams] = useState([]);
const [searchParams, setSearchParams] = useSearchParams();
const [defaultSearchFilterParams, setDefaultSearchFilterParams] = useState(
[]
);
const [pageLoader, setPageLoader] = useState(true);
const [resetPage, setResetpage] = useState({ page: 0 });
const [show, setShow] = useState(true);
const [blockUI, setBlockUI] = useState(false);
let navigate = useNavigate();
let { serviceId, policyType } = useParams();
useEffect(() => {
let searchFilterParam = {};
let searchParam = {};
let defaultSearchFilterParam = [];
// Get Search Filter Params from current search params
const currentParams = Object.fromEntries([...searchParams]);
for (const param in currentParams) {
let searchFilterObj = find(getSearchFilterOptions(), {
urlLabel: param
});
if (!isUndefined(searchFilterObj)) {
let category = searchFilterObj.category;
let value = currentParams[param];
if (searchFilterObj.type == "textoptions") {
let textOptionObj = find(searchFilterObj.options(), {
label: value
});
value = textOptionObj !== undefined ? textOptionObj.value : value;
}
searchFilterParam[category] = value;
defaultSearchFilterParam.push({
category: category,
value: value
});
}
}
// Updating the states for search params, search filter and default search filter
setSearchParams({ ...currentParams, ...searchParam }, { replace: true });
if (
JSON.stringify(searchFilterParams) !== JSON.stringify(searchFilterParam)
) {
setSearchFilterParams(searchFilterParam);
}
setDefaultSearchFilterParams(defaultSearchFilterParam);
setPageLoader(false);
localStorage.setItem("newDataAdded", state && state.showLastPage);
}, [searchParams]);
useEffect(() => {
if (localStorage.getItem("newDataAdded") == "true") {
scrollToNewData(policyListingData);
}
}, [totalCount]);
const getTableSortBy = (sortArr = []) => {
return sortArr
.map(({ id }) => {
return QueryParamsName(id);
})
.join(",");
};
const getTableSortType = (sortArr = []) => {
return sortArr.map(({ desc }) => (desc ? "desc" : "asc")).join(",");
};
const fetchPolicyInfo = useCallback(
async ({ pageSize, pageIndex, sortBy, gotoPage }) => {
setLoader(true);
let policyData = [];
let policyResp = [];
let totalCount = 0;
let page =
state && state.showLastPage
? state.addPageData.totalPage - 1
: pageIndex;
let totalPageCount = 0;
const fetchId = ++fetchIdRef.current;
let params = { ...searchFilterParams };
if (fetchId === fetchIdRef.current) {
params["page"] = page;
params["startIndex"] =
state && state.showLastPage
? (state.addPageData.totalPage - 1) * pageSize
: pageIndex * pageSize;
params["pageSize"] = pageSize;
params["policyType"] = policyType;
if (sortBy.length > 0) {
params["sortBy"] = getTableSortBy(sortBy);
params["sortType"] = getTableSortType(sortBy);
}
if (serviceZone !== null) {
params["zoneName"] = serviceZone.label;
}
try {
policyResp = await fetchApi({
url: `plugins/policies/service/${serviceId}`,
params: params
});
policyData = policyResp.data.policies;
totalCount = policyResp.data.totalCount;
totalPageCount = Math.ceil(totalCount / pageSize);
} catch (error) {
console.error(`Error occurred while fetching Policies ! ${error}`);
}
if (state) {
state["showLastPage"] = false;
}
setPolicyData(policyData);
setTblPageData({
totalPage: totalPageCount,
pageRecords: policyResp?.data?.totalCount,
pageSize: 25
});
setTotalCount(totalCount);
setPageCount(totalPageCount);
setCurrentPageIndex(page);
setCurrentPageSize(pageSize);
setResetpage({ page: gotoPage });
setLoader(false);
}
},
[updateTable, searchFilterParams]
);
const toggleConfirmModalForDelete = (policyID, policyName) => {
setConfirmModal({
policyDetails: { policyID: policyID, policyName: policyName },
showPopup: true
});
};
const toggleClose = () => {
setConfirmModal({
policyDetails: {},
showPopup: false
});
};
const handleClosePolicyId = () => setPolicyViewModal(false);
const openModal = (policyDetails) => {
let policyId = pick(policyDetails, ["id"]);
setPolicyViewModal(true);
setPolicyParamsData(policyDetails);
fetchVersions(policyId.id);
};
const fetchVersions = async (policyId) => {
let versionsResp = {};
try {
versionsResp = await fetchApi({
url: `plugins/policy/${policyId}/versionList`
});
} catch (error) {
console.error(
`Error occurred while fetching Policy Version or CSRF headers! ${error}`
);
}
setCurrentPage(
versionsResp.data.value
.split(",")
.map(Number)
.sort(function (a, b) {
return a - b;
})
);
setLoader(false);
};
const handleDeleteClick = async (policyID) => {
toggleClose();
try {
setBlockUI(true);
await fetchApi({
url: `plugins/policies/${policyID}`,
method: "DELETE"
});
setBlockUI(false);
toast.success(" Success! Policy deleted successfully");
} catch (error) {
setBlockUI(false);
let errorMsg = "Failed to delete policy : ";
if (error?.response?.data?.msgDesc) {
errorMsg += error.response.data.msgDesc;
}
toast.error(errorMsg);
console.error("Error occurred during deleting policy : " + error);
}
if (policyListingData.length == 1 && currentpageIndex > 0) {
let page = currentpageIndex - currentpageIndex;
if (typeof resetPage?.page === "function") {
resetPage.page(page);
}
} else {
setUpdateTable(moment.now());
}
};
const previousVersion = (e) => {
if (e.currentTarget.classList.contains("active")) {
let curr = policyParamsData && policyParamsData.version;
let policyVersionList = currentPage;
var previousVal =
policyVersionList[
(indexOf(policyVersionList, curr) - 1) % policyVersionList.length
];
}
let prevVal = {};
prevVal.version = previousVal;
prevVal.id = policyParamsData.id;
prevVal.isChangeVersion = true;
setPolicyParamsData(prevVal);
};
const nextVersion = (e) => {
if (e.currentTarget.classList.contains("active")) {
let curr = policyParamsData && policyParamsData.version;
let policyVersionList = currentPage;
var nextValue =
policyVersionList[
(indexOf(policyVersionList, curr) + 1) % policyVersionList.length
];
}
let nextVal = {};
nextVal.version = nextValue;
nextVal.id = policyParamsData.id;
nextVal.isChangeVersion = true;
setPolicyParamsData(nextVal);
};
const revert = (e) => {
e.preventDefault();
let version = policyParamsData && policyParamsData.version;
let revertVal = {};
revertVal.version = version;
revertVal.id = policyParamsData.id;
revertVal.isRevert = true;
setPolicyParamsData(revertVal);
setPolicyViewModal(false);
};
const updateServices = () => {
setUpdateTable(moment.now());
};
const columns = React.useMemo(
() => [
{
Header: "Policy ID",
accessor: "id",
Cell: (rawValue) => {
if (isAuditor() || isKMSAuditor()) {
if (
!isEmpty(rawValue.row.original.validitySchedules) &&
isPolicyExpired(rawValue.row.original)
) {
return (
<div className="position-relative text-center">
<i
className="fa-fw fa fa-exclamation-circle policy-expire-icon"
title="Policy expired"
></i>
{rawValue.value}
</div>
);
} else {
return (
<div className="position-relative text-center">
{rawValue.value}
</div>
);
}
} else {
if (
!isEmpty(rawValue.row.original.validitySchedules) &&
isPolicyExpired(rawValue.row.original)
) {
return (
<div className="position-relative text-center">
<i
className="fa-fw fa fa-exclamation-circle policy-expire-icon"
title="Policy expired"
></i>
<Link
title="Edit"
to={`/service/${serviceId}/policies/${rawValue.value}/edit`}
>
{rawValue.value}
</Link>
</div>
);
} else {
return (
<div className="position-relative text-center">
<Link
title="Edit"
to={`/service/${serviceId}/policies/${rawValue.value}/edit`}
>
{rawValue.value}
</Link>
</div>
);
}
}
},
width: 90
},
{
Header: "Policy Name",
accessor: "name",
Cell: (val) => {
return (
<span
className="text-truncate"
title={val.value}
style={{ maxWidth: "240px", display: "inline-block" }}
>
{val.value}
</span>
);
},
width: 250
},
{
Header: "Policy Label",
accessor: "policyLabels",
Cell: (rawValue) => {
return !isEmpty(rawValue.value) ? (
<MoreLess data={rawValue.value} />
) : (
<div className="text-center">--</div>
);
},
width: 130,
disableSortBy: true
},
{
Header: "Status",
accessor: "isEnabled",
Cell: (rawValue) => {
if (rawValue.value)
return (
<h6>
<Badge variant="success">Enabled</Badge>
</h6>
);
else
return (
<h6>
<Badge variant="danger">Disabled</Badge>
</h6>
);
},
width: 100,
disableSortBy: true
},
{
Header: "Audit Logging",
accessor: "isAuditEnabled",
Cell: (rawValue) => {
if (rawValue.value) {
return (
<h6>
<Badge variant="success">Enabled</Badge>
</h6>
);
} else
return (
<h6>
<Badge variant="danger">Disabled</Badge>
</h6>
);
},
width: 110,
disableSortBy: true
},
{
Header: "Roles",
accessor: "roles",
Cell: (rawValue) => {
let rolesData = showGroupsOrUsersOrRolesForPolicy(
"roles",
rawValue.row.original,
policyType
);
return !isEmpty(rolesData) ? (
<MoreLess data={rolesData} key={rawValue.row.original.id} />
) : (
<div className="text-center">--</div>
);
},
minWidth: 190,
disableSortBy: true
},
{
Header: "Groups",
accessor: "groups",
Cell: (rawValue) => {
let groupsData = showGroupsOrUsersOrRolesForPolicy(
"groups",
rawValue.row.original,
policyType
);
return !isEmpty(groupsData) ? (
<MoreLess data={groupsData} key={rawValue.row.original.id} />
) : (
<div className="text-center">--</div>
);
},
minWidth: 190,
disableSortBy: true
},
{
Header: "Users",
accessor: "users",
Cell: (rawValue) => {
let usersData = showGroupsOrUsersOrRolesForPolicy(
"users",
rawValue.row.original,
policyType
);
return !isEmpty(usersData) ? (
<MoreLess data={usersData} key={rawValue.row.original.id} />
) : (
<div className="text-center">--</div>
);
},
minWidth: 190,
disableSortBy: true
},
{
Header: "Actions",
accessor: "actions",
Cell: ({ row: { original } }) => {
return (
<div>
<Button
variant="outline-dark"
size="sm"
className="mr-2"
title="View"
onClick={(e) => {
e.stopPropagation();
openModal(original);
}}
data-name="viewPolicy"
data-id={original.id}
>
<i className="fa-fw fa fa-eye fa-fw fa fa-large"></i>
</Button>
{(isSystemAdmin() || isKeyAdmin() || isUser()) && (
<>
<Link
className="btn btn-outline-dark btn-sm mr-2"
title="Edit"
to={`/service/${serviceId}/policies/${original.id}/edit`}
>
<i className="fa-fw fa fa-edit"></i>
</Link>
<Button
variant="danger"
size="sm"
title="Delete"
onClick={() =>
toggleConfirmModalForDelete(original.id, original.name)
}
data-name="deletePolicy"
data-id={original.id}
data-cy={original.id}
>
<i className="fa-fw fa fa-trash fa-fw fa fa-large"></i>
</Button>
</>
)}
</div>
);
},
disableSortBy: true
}
],
[]
);
const addPolicy = () => {
navigate(`/service/${serviceId}/policies/create/${policyType}`, {
state: { tblpageData: tblpageData }
});
};
const searchFilterOptions = [
{
category: "group",
label: "Group Name",
urlLabel: "groupName",
type: "text"
},
{
category: "policyLabelsPartial",
label: "Policy Label",
urlLabel: "policyLabel",
type: "text"
},
{
category: "policyNamePartial",
label: "Policy Name",
urlLabel: "policyName",
type: "text"
},
{
category: "role",
label: "Role Name",
urlLabel: "roleName",
type: "text"
},
{
category: "isEnabled",
label: "Status",
urlLabel: "status",
type: "textoptions",
options: () => {
return [
{ value: "true", label: "Enabled" },
{ value: "false", label: "Disabled" }
];
}
},
{
category: "user",
label: "User Name",
urlLabel: "userName",
type: "text"
}
];
const getSearchFilterOptions = () => {
let currentServiceDef = serviceDef;
if (currentServiceDef !== undefined) {
let serviceDefResource = getResourcesDefVal(
currentServiceDef,
policyType
);
let serviceDefResourceOption = serviceDefResource?.map((obj) => ({
category: "resource:" + obj.name,
label: obj.label,
urlLabel: camelCase(obj.label),
type: "text"
}));
return sortBy(concat(searchFilterOptions, serviceDefResourceOption), [
"label"
]);
}
return sortBy(searchFilterOptions, ["label"]);
};
const getSearchInfoContent = () => {
let resources = [];
let resourceSearchOpt = [];
let serverRsrcAttrName = [];
let policySearchInfoMsg = [];
resources = getResourcesDefVal(serviceDef, policyType);
resourceSearchOpt = map(resources, function (resource) {
return {
name: resource.name,
label: resource.label,
description: resource.description
};
});
serverRsrcAttrName = map(resourceSearchOpt, function (opt) {
return {
text: opt.label,
info: !isUndefined(opt?.description)
? opt.description
: ResourcesOverrideInfoMsg[opt.name]
};
});
policySearchInfoMsg = union(ServerAttrName, serverRsrcAttrName);
return (
<div className="policy-search-info">
<p className="m-0">
Wildcard searches ( for example using * or ? ) are not currently
supported.
</p>
{policySearchInfoMsg?.map((m, index) => {
return (
<p className="m-0" key={index}>
<span className="font-weight-bold">{m.text}: </span>
<span>{m.info}</span>
</p>
);
})}
</div>
);
};
const updateSearchFilter = (filter) => {
let { searchFilterParam, searchParam } = parseSearchFilter(
filter,
getSearchFilterOptions()
);
setSearchFilterParams(searchFilterParam);
setSearchParams(searchParam, { replace: true });
if (typeof resetPage?.page === "function") {
resetPage.page(0);
}
};
return (
<div className="wrap">
{(serviceData.type == "hdfs" || serviceData.type == "yarn") && show && (
<Alert variant="warning" onClose={() => setShow(false)} dismissible>
<i className="fa-fw fa fa-info-circle d-inline text-dark"></i>
<p className="pd-l-10 d-inline">
{`By default, fallback to ${
alertMessage[serviceData.type].label
} ACLs are enabled. If access cannot be
determined by Ranger policies, authorization will fall back to
${
alertMessage[serviceData.type].label
} ACLs. If this behavior needs to be changed, modify ${
alertMessage[serviceData.type].label
}
plugin config - ${
alertMessage[serviceData.type].configs
}-authorization.`}
</p>
</Alert>
)}
{pageLoader ? (
<Loader />
) : (
<React.Fragment>
<BlockUi isUiBlock={blockUI} />
<div className="policy-listing">
<Row className="mb-3">
<Col sm={10}>
<div className="filter-icon-wrap">
<StructuredFilter
key="policy-listing-search-filter"
placeholder="Search for your policy..."
options={getSearchFilterOptions()}
onChange={updateSearchFilter}
defaultSelected={defaultSearchFilterParams}
/>
<CustomPopover
icon="fa-fw fa fa-info-circle info-icon"
title={
<span style={{ fontSize: "14px" }}>
Search Filter Hints
</span>
}
content={getSearchInfoContent()}
placement="bottom"
trigger={["hover", "focus"]}
/>
</div>
</Col>
<Col sm={2}>
<div className="pull-right mb-1">
{(isSystemAdmin() || isKeyAdmin() || isUser()) && (
<Button
variant="primary"
size="sm"
onClick={addPolicy}
data-js="addNewPolicy"
data-cy="addNewPolicy"
>
Add New Policy
</Button>
)}
</div>
</Col>
</Row>
<XATableLayout
data={policyListingData}
columns={columns}
fetchData={fetchPolicyInfo}
totalCount={totalCount}
pagination
pageCount={pageCount}
currentpageIndex={currentpageIndex}
currentpageSize={currentpageSize}
loading={loader}
columnSort={true}
/>
</div>
<Modal show={deletePolicyModal.showPopup} onHide={toggleClose}>
<Modal.Header closeButton>
<span className="text-word-break">
Are you sure you want to delete policy&nbsp;"
<b>{deletePolicyModal?.policyDetails?.policyName}</b>" ?
</span>
</Modal.Header>
<Modal.Footer>
<Button variant="secondary" size="sm" onClick={toggleClose}>
Close
</Button>
<Button
variant="primary"
size="sm"
onClick={() =>
handleDeleteClick(deletePolicyModal.policyDetails.policyID)
}
>
OK
</Button>
</Modal.Footer>
</Modal>
<Modal show={policyviewmodal} onHide={handleClosePolicyId} size="xl">
<Modal.Header closeButton>
<Modal.Title>Policy Details</Modal.Title>
</Modal.Header>
<Modal.Body>
<PolicyViewDetails
paramsData={policyParamsData}
policyInfo={fetchPolicyInfo}
totalCount={totalCount}
policyView={true}
updateServices={updateServices}
/>
</Modal.Body>
<Modal.Footer>
<div className="policy-version text-left">
<span>
<i
className={
policyParamsData && policyParamsData.version > 1
? "fa-fw fa fa-chevron-left active"
: "fa-fw fa fa-chevron-left"
}
onClick={(e) =>
e.currentTarget.classList.contains("active") &&
previousVersion(e)
}
></i>
<span>{`Version ${
policyParamsData && policyParamsData.version
}`}</span>
<i
className={
!isUndefined(
currentPage[
indexOf(
currentPage,
policyParamsData && policyParamsData.version
) + 1
]
)
? "fa-fw fa fa-chevron-right active"
: "fa-fw fa fa-chevron-right"
}
onClick={(e) =>
e.currentTarget.classList.contains("active") &&
nextVersion(e)
}
></i>
</span>
{!isUndefined(
currentPage[
indexOf(
currentPage,
policyParamsData && policyParamsData.version
) + 1
]
) && (
<Button
variant="primary"
size="sm"
onClick={(e) => revert(e)}
>
Revert
</Button>
)}
</div>
<Button variant="primary" size="sm" onClick={handleClosePolicyId}>
OK
</Button>
</Modal.Footer>
</Modal>
</React.Fragment>
)}
</div>
);
}
export default PolicyListing;