feat(ui): update global settings page with new roles and policy (#7002)

* feat(ui): update global settings page with new roles and policy

* Modify the permission util and remove access prop from create bot and user page

* Fix typo

* Fix console errors

* fix unit tests

* Add getEntityPermission to provider

* Remove admin and auth disable check from global setting

* Add check for edit description.

Co-authored-by: Sachin Chaurasiya <sachinchaurasiyachotey87@gmail.com>
This commit is contained in:
Chirag Madlani 2022-08-29 16:35:42 +05:30 committed by GitHub
parent 15c83e3f2d
commit cc0449e506
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 771 additions and 257 deletions

View File

@ -13,9 +13,11 @@
import { AxiosResponse } from 'axios';
import { Operation } from 'fast-json-patch';
import { ResourceEntity } from '../components/PermissionProvider/PermissionProvider.interface';
import { CreatePolicy } from '../generated/api/policies/createPolicy';
import { CreateRole } from '../generated/api/teams/createRole';
import { ResourceDescriptor } from '../generated/entity/policies/accessControl/resourceDescriptor';
import { ResourcePermission } from '../generated/entity/policies/accessControl/resourcePermission';
import { Policy } from '../generated/entity/policies/policy';
import { Role } from '../generated/entity/teams/role';
import { Function } from '../generated/type/function';
@ -154,3 +156,14 @@ export const validateRuleCondition = async (condition: string) => {
*/
return response;
};
export const getEntityPermissionById = async (
resource: ResourceEntity,
entityId: string
) => {
const response = await APIClient.get<ResourcePermission>(
`/permissions/${resource}/${entityId}`
);
return response.data;
};

View File

@ -13,12 +13,19 @@
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from 'antd';
import { Store } from 'antd/lib/form/interface';
import classNames from 'classnames';
import cryptoRandomString from 'crypto-random-string-with-promisify-polyfill';
import { cloneDeep, isEqual, isNil } from 'lodash';
import { cloneDeep, isEmpty, isEqual, isNil } from 'lodash';
import { EditorContentRef } from 'Models';
import React, { FunctionComponent, useCallback, useRef, useState } from 'react';
import React, {
FunctionComponent,
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import { ROUTES, TERM_ALL } from '../../constants/constants';
import {
GlobalSettingOptions,
@ -27,6 +34,7 @@ import {
import {
CONFIGURE_SLACK_TEXT,
CONFIGURE_WEBHOOK_TEXT,
NO_PERMISSION_FOR_ACTION,
} from '../../constants/HelperTextUtil';
import { UrlEntityCharRegEx } from '../../constants/regex.constants';
import { FormSubmitType } from '../../enums/form.enum';
@ -37,12 +45,14 @@ import {
Filters,
} from '../../generated/api/events/createWebhook';
import { WebhookType } from '../../generated/entity/events/webhook';
import { Operation } from '../../generated/entity/policies/policy';
import {
errorMsg,
getSeparator,
isValidUrl,
requiredField,
} from '../../utils/CommonUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { Button } from '../buttons/Button/Button';
@ -52,6 +62,8 @@ import TitleBreadcrumb from '../common/title-breadcrumb/title-breadcrumb.compone
import PageLayout from '../containers/PageLayout';
import Loader from '../Loader/Loader';
import ConfirmationModal from '../Modals/ConfirmationModal/ConfirmationModal';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
import { AddWebhookProps } from './AddWebhook.interface';
import SelectComponent from './select-component';
import {
@ -163,6 +175,29 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
const [generatingSecret, setGeneratingSecret] = useState<boolean>(false);
const [isDelete, setIsDelete] = useState<boolean>(false);
const { permissions } = usePermissionProvider();
const editWebhookPermission = useMemo(
() =>
!isEmpty(permissions) &&
checkPermission(Operation.EditAll, ResourceEntity.WEBHOOK, permissions),
[permissions]
);
const addWebhookPermission = useMemo(
() =>
!isEmpty(permissions) &&
checkPermission(Operation.Create, ResourceEntity.WEBHOOK, permissions),
[permissions]
);
const deleteWebhookPermission = useMemo(
() =>
!isEmpty(permissions) &&
checkPermission(Operation.Delete, ResourceEntity.WEBHOOK, permissions),
[permissions]
);
const handleDelete = () => {
if (data) {
onDelete && onDelete(data.id);
@ -259,7 +294,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
};
const getDeleteButton = () => {
return allowAccess ? (
return (
<>
{deleteState === 'waiting' ? (
<Button
@ -271,24 +306,32 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
<Loader size="small" type="default" />
</Button>
) : (
<Button
className={classNames({
'tw-opacity-40': !allowAccess,
})}
data-testid="delete-webhook"
size="regular"
theme="primary"
variant="text"
onClick={() => setIsDelete(true)}>
Delete
</Button>
<Tooltip
placement="left"
title={
deleteWebhookPermission ? 'Delete' : NO_PERMISSION_FOR_ACTION
}>
<Button
data-testid="delete-webhook"
disabled={!deleteWebhookPermission}
size="regular"
theme="primary"
variant="text"
onClick={() => setIsDelete(true)}>
Delete
</Button>
</Tooltip>
)}
</>
) : null;
);
};
const getSaveButton = () => {
return allowAccess ? (
const savePermission =
(mode === 'add' && addWebhookPermission) ||
(mode === 'edit' && editWebhookPermission);
return (
<>
{saveState === 'waiting' ? (
<Button
@ -309,20 +352,23 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
<FontAwesomeIcon icon="check" />
</Button>
) : (
<Button
className={classNames('tw-w-16 tw-h-10', {
'tw-opacity-40': !allowAccess,
})}
data-testid="save-webhook"
size="regular"
theme="primary"
variant="contained"
onClick={handleSave}>
Save
</Button>
<Tooltip
placement="left"
title={savePermission ? 'Save' : NO_PERMISSION_FOR_ACTION}>
<Button
className={classNames('tw-w-16 tw-h-10')}
data-testid="save-webhook"
disabled={!savePermission}
size="regular"
theme="primary"
variant="contained"
onClick={handleSave}>
Save
</Button>
</Tooltip>
)}
</>
) : null;
);
};
const fetchRightPanel = useCallback(() => {
@ -631,7 +677,7 @@ const AddWebhook: FunctionComponent<AddWebhookProps> = ({
</div>
)}
</Field>
{data && isDelete && (
{data && isDelete && deleteWebhookPermission && (
<ConfirmationModal
bodyText={`You want to delete webhook ${data.name} permanently? This action cannot be reverted.`}
cancelText="Cancel"

View File

@ -34,7 +34,7 @@ import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { JWTTokenExpiry, User } from '../../generated/entity/teams/user';
import { EntityReference } from '../../generated/type/entityReference';
import { getEntityName, requiredField } from '../../utils/CommonUtils';
import { checkPemission } from '../../utils/PermissionsUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast } from '../../utils/ToastUtils';
@ -77,18 +77,18 @@ const BotDetails: FC<BotsDetailProp> = ({
const [generateToken, setGenerateToken] = useState<boolean>(false);
const [selectedExpiry, setSelectedExpiry] = useState('7');
const editAllPermission = checkPemission(
const editAllPermission = checkPermission(
Operation.EditAll,
ResourceEntity.BOT,
permissions
);
const displayNamePermission = checkPemission(
const displayNamePermission = checkPermission(
Operation.EditDisplayName,
ResourceEntity.BOT,
permissions
);
const descriptionPermission = checkPemission(
const descriptionPermission = checkPermission(
Operation.EditDescription,
ResourceEntity.BOT,
permissions

View File

@ -85,7 +85,7 @@ jest.mock('../PermissionProvider/PermissionProvider', () => ({
}));
jest.mock('../../utils/PermissionsUtils', () => ({
checkPemission: jest.fn().mockReturnValue(true),
checkPermission: jest.fn().mockReturnValue(true),
}));
jest.mock('../../axiosAPIs/userAPI', () => {

View File

@ -22,12 +22,13 @@ import {
INITIAL_PAGING_VALUE,
PAGE_SIZE,
} from '../../constants/constants';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import { EntityType } from '../../enums/entity.enum';
import { Bot } from '../../generated/entity/bot';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { Include } from '../../generated/type/include';
import { Paging } from '../../generated/type/paging';
import { checkPemission } from '../../utils/PermissionsUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import SVGIcons, { Icons } from '../../utils/SvgUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import DeleteWidgetModal from '../common/DeleteWidget/DeleteWidgetModal';
@ -45,7 +46,7 @@ const BotListV1 = ({ showDeleted }: BotListV1Props) => {
const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState<number>(INITIAL_PAGING_VALUE);
const deletePermission = checkPemission(
const deletePermission = checkPermission(
Operation.Delete,
ResourceEntity.BOT,
permissions
@ -103,11 +104,7 @@ const BotListV1 = ({ showDeleted }: BotListV1Props) => {
<Space align="center" size={8}>
<Tooltip
placement="bottom"
title={
deletePermission
? 'Delete'
: 'You do not have permissions to perform this action.'
}>
title={deletePermission ? 'Delete' : NO_PERMISSION_FOR_ACTION}>
<Button
disabled={!deletePermission}
icon={

View File

@ -36,7 +36,6 @@ import TeamsSelectable from '../TeamsSelectable/TeamsSelectable';
import { CreateUserProps } from './CreateUser.interface';
const CreateUser = ({
allowAccess,
roles,
saveState = 'initial',
onCancel,
@ -81,10 +80,6 @@ const CreateUser = ({
const handleValidation = (
event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
) => {
if (!allowAccess) {
return;
}
const value = event.target.value;
const eleName = event.target.name;
@ -188,7 +183,7 @@ const CreateUser = ({
* @returns Button
*/
const getSaveButton = () => {
return allowAccess ? (
return (
<>
{saveState === 'waiting' ? (
<Button
@ -210,9 +205,7 @@ const CreateUser = ({
</Button>
) : (
<Button
className={classNames('tw-w-16 tw-h-10', {
'tw-opacity-40': !allowAccess,
})}
className={classNames('tw-w-16 tw-h-10')}
data-testid="save-user"
size="regular"
theme="primary"
@ -222,7 +215,7 @@ const CreateUser = ({
</Button>
)}
</>
) : null;
);
};
return (
@ -310,10 +303,8 @@ const CreateUser = ({
className={classNames('toggle-switch', { open: isAdmin })}
data-testid="admin"
onClick={() => {
if (allowAccess) {
setIsAdmin((prev) => !prev);
setIsBot(false);
}
setIsAdmin((prev) => !prev);
setIsBot(false);
}}>
<div className="switch" />
</div>
@ -324,10 +315,8 @@ const CreateUser = ({
className={classNames('toggle-switch', { open: isBot })}
data-testid="bot"
onClick={() => {
if (allowAccess) {
setIsBot((prev) => !prev);
setIsAdmin(false);
}
setIsBot((prev) => !prev);
setIsAdmin(false);
}}>
<div className="switch" />
</div>

View File

@ -16,7 +16,6 @@ import { CreateUser } from '../../generated/api/teams/createUser';
import { Role } from '../../generated/entity/teams/role';
export interface CreateUserProps {
allowAccess: boolean;
saveState?: LoadingState;
roles: Array<Role>;
onSave: (data: CreateUser) => void;

View File

@ -37,7 +37,6 @@ jest.mock('../common/rich-text-editor/RichTextEditor', () => {
});
const propsValue: CreateUserProps = {
allowAccess: true,
saveState: 'initial',
roles: [],
forceBot: false,

View File

@ -16,23 +16,29 @@ import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { camelCase } from 'lodash';
import React from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { GLOBAL_SETTINGS_MENU } from '../../constants/globalSettings.constants';
import { useAuth } from '../../hooks/authHooks';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { getGlobalSettingMenus } from '../../utils/GlobalSettingsUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import { getSettingPath } from '../../utils/RouterUtils';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
const GlobalSettingLeftPanel = () => {
const { tab, settingCategory } = useParams<{ [key: string]: string }>();
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const isHasAccess = isAdminUser || isAuthDisabled;
const { permissions } = usePermissionProvider();
const viewAllPermission = checkPermission(
Operation.ViewAll,
ResourceEntity.ALL,
permissions
);
const history = useHistory();
const items: ItemType[] = GLOBAL_SETTINGS_MENU.filter(({ isProtected }) => {
if (isHasAccess) {
return isHasAccess;
if (viewAllPermission) {
return viewAllPermission;
}
return !isProtected;
@ -44,7 +50,7 @@ const GlobalSettingLeftPanel = () => {
'',
items,
'group',
isHasAccess
viewAllPermission
);
});

View File

@ -1,3 +1,17 @@
/*
* Copyright 2021 Collate
* Licensed 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 { AxiosError } from 'axios';
import { Operation } from '../../generated/entity/policies/accessControl/resourcePermission';
export type UIPermission = {
@ -13,35 +27,47 @@ export enum ResourceEntity {
BOT = 'bot',
CHART = 'chart',
DASHBOARD = 'dashboard',
DASHBOARDSERVICE = 'dashboardService',
DASHBOARD_SERVICE = 'dashboardService',
DATABASE = 'database',
DATABASESCHEMA = 'databaseSchema',
DATABASESERVICE = 'databaseService',
DATABASE_SCHEMA = 'databaseSchema',
DATABASE_SERVICE = 'databaseService',
EVENTS = 'events',
FEED = 'feed',
GLOSSARY = 'glossary',
GLOSSARYTERM = 'glossaryTerm',
INGESTIONPIPELINE = 'ingestionPipeline',
GLOSSARY_TERM = 'glossaryTerm',
INGESTION_PIPELINE = 'ingestionPipeline',
LOCATION = 'location',
MESSAGINGSERVICE = 'messagingService',
MESSAGING_SERVICE = 'messagingService',
METRICS = 'metrics',
MLMODEL = 'mlmodel',
MLMODELSERVICE = 'mlmodelService',
ML_MODEL = 'mlmodel',
ML_MODEL_SERVICE = 'mlmodelService',
PIPELINE = 'pipeline',
PIPELINESERVICE = 'pipelineService',
PIPELINE_SERVICE = 'pipelineService',
POLICY = 'policy',
REPORT = 'report',
ROLE = 'role',
STORAGESERVICE = 'storageService',
STORAGE_SERVICE = 'storageService',
TABLE = 'table',
TAG = 'tag',
TAGCATEGORY = 'tagCategory',
TAG_CATEGORY = 'tagCategory',
TEAM = 'team',
TESTCASE = 'testCase',
TESTDEFINITION = 'testDefinition',
TESTSUITE = 'testSuite',
TEST_CASE = 'testCase',
TEST_DEFINITION = 'testDefinition',
TEST_SUITE = 'testSuite',
TOPIC = 'topic',
TYPE = 'type',
USER = 'user',
WEBHOOK = 'webhook',
}
export interface PermissionContextType {
permissions: UIPermission;
getEntityPermission: (
resource: ResourceEntity,
entityId: string
) => Promise<OperationPermission | AxiosError>;
}
export interface EntityPermissionMap {
[key: string]: OperationPermission;
}

View File

@ -24,18 +24,27 @@ import React, {
} from 'react';
import AppState from '../../AppState';
import { getLoggedInUserPermissions } from '../../axiosAPIs/miscAPI';
import { getUIPermission } from '../../utils/PermissionsUtils';
import { getEntityPermissionById } from '../../axiosAPIs/rolesAPIV1';
import {
getOperationPermissions,
getUIPermission,
} from '../../utils/PermissionsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import { UIPermission } from './PermissionProvider.interface';
import {
EntityPermissionMap,
PermissionContextType,
ResourceEntity,
UIPermission,
} from './PermissionProvider.interface';
/**
* Permission Context
* Returns ResourcePermission List for loggedIn User
* @returns PermissionMap
*/
export const PermissionContext = createContext<{
permissions: UIPermission;
}>({ permissions: {} as UIPermission });
export const PermissionContext = createContext<PermissionContextType>(
{} as PermissionContextType
);
interface PermissionProviderProps {
children: ReactNode;
@ -51,6 +60,9 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
{} as UIPermission
);
const [entitiesPermission, setEntitiesPermission] =
useState<EntityPermissionMap>({} as EntityPermissionMap);
// Update current user details of AppState change
const currentUser = useMemo(() => {
return AppState.getCurrentUserDetails();
@ -68,6 +80,29 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
}
};
const fetchEntityPermission = async (
resource: ResourceEntity,
entityId: string
) => {
try {
const entityPermission = entitiesPermission[entityId];
if (entityPermission) {
return entityPermission;
} else {
const response = await getEntityPermissionById(resource, entityId);
const operationPermission = getOperationPermissions(response);
setEntitiesPermission((prev) => ({
...prev,
[entityId]: operationPermission,
}));
return operationPermission;
}
} catch (error) {
return error as AxiosError;
}
};
useEffect(() => {
/**
* Only fetch permission if user is logged In
@ -78,7 +113,11 @@ const PermissionProvider: FC<PermissionProviderProps> = ({ children }) => {
}, [currentUser]);
return (
<PermissionContext.Provider value={{ permissions }}>
<PermissionContext.Provider
value={{
permissions,
getEntityPermission: fetchEntityPermission,
}}>
{children}
</PermissionContext.Provider>
);

View File

@ -11,8 +11,9 @@
* limitations under the License.
*/
import { Card, Col, Row } from 'antd';
import React, { Fragment } from 'react';
import { Card, Col, Row, Tooltip } from 'antd';
import { isEmpty } from 'lodash';
import React, { Fragment, useMemo } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import {
@ -20,26 +21,32 @@ import {
PAGE_SIZE,
TITLE_FOR_NON_ADMIN_ACTION,
} from '../../constants/constants';
import { NO_PERMISSION_FOR_ACTION } from '../../constants/HelperTextUtil';
import {
NoDataFoundPlaceHolder,
servicesDisplayName,
} from '../../constants/services.const';
import { ServiceCategory } from '../../enums/service.enum';
import { Operation } from '../../generated/entity/policies/policy';
import { Paging } from '../../generated/type/paging';
import { useAuth } from '../../hooks/authHooks';
import { ServicesType } from '../../interface/service.interface';
import {
getEntityName,
getServiceLogo,
showPagination,
} from '../../utils/CommonUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import { getAddServicePath } from '../../utils/RouterUtils';
import { getOptionalFields } from '../../utils/ServiceUtils';
import {
getOptionalFields,
getResourceEntityFromServiceCategory,
} from '../../utils/ServiceUtils';
import { Button } from '../buttons/Button/Button';
import NextPrevious from '../common/next-previous/NextPrevious';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import RichTextEditorPreviewer from '../common/rich-text-editor/RichTextEditorPreviewer';
import { leftPanelAntCardStyle } from '../containers/PageLayout';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
interface ServicesProps {
serviceData: ServicesType[];
@ -56,33 +63,49 @@ const Services = ({
currentPage,
onPageChange,
}: ServicesProps) => {
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const history = useHistory();
const handleAddServiceClick = () => {
history.push(getAddServicePath(serviceName));
};
const { permissions } = usePermissionProvider();
const addServicePermission = useMemo(
() =>
!isEmpty(permissions) &&
checkPermission(
Operation.Create,
getResourceEntityFromServiceCategory(serviceName),
permissions
),
[permissions, serviceName]
);
return (
<Row data-testid="services-container">
{serviceData.length ? (
<Fragment>
<Col span={24}>
<div className="tw-flex tw-justify-end" data-testid="header">
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Tooltip
placement="left"
title={
addServicePermission
? 'Add Service'
: NO_PERMISSION_FOR_ACTION
}>
<Button
className="tw-h-8 tw-rounded tw-mb-2"
data-testid="add-new-service-button"
disabled={!isAdminUser && !isAuthDisabled}
disabled={!addServicePermission && !isAuthDisabled}
size="small"
theme="primary"
variant="contained"
onClick={handleAddServiceClick}>
Add New Service
</Button>
</NonAdminAction>
</Tooltip>
</div>
</Col>
<Col span={24}>

View File

@ -11,28 +11,28 @@
* limitations under the License.
*/
import { Card, Col, Row, Select, Space } from 'antd';
import { Card, Col, Row, Select, Space, Tooltip } from 'antd';
import classNames from 'classnames';
import { isNil } from 'lodash';
import { isEmpty, isNil } from 'lodash';
import React, { FC, useEffect, useMemo, useState } from 'react';
import { PAGE_SIZE } from '../../constants/constants';
import {
PAGE_SIZE,
TITLE_FOR_NON_ADMIN_ACTION,
} from '../../constants/constants';
import {
NO_PERMISSION_FOR_ACTION,
SLACK_LISTING_TEXT,
WEBHOOK_LISTING_TEXT,
} from '../../constants/HelperTextUtil';
import { WebhookType } from '../../generated/api/events/createWebhook';
import { Webhook } from '../../generated/entity/events/webhook';
import { useAuth } from '../../hooks/authHooks';
import { Operation } from '../../generated/entity/policies/policy';
import { checkPermission } from '../../utils/PermissionsUtils';
import { statuses } from '../AddWebhook/WebhookConstants';
import { Button } from '../buttons/Button/Button';
import ErrorPlaceHolder from '../common/error-with-placeholder/ErrorPlaceHolder';
import NextPrevious from '../common/next-previous/NextPrevious';
import NonAdminAction from '../common/non-admin-action/NonAdminAction';
import WebhookDataCard from '../common/webhook-data-card/WebhookDataCard';
import { leftPanelAntCardStyle } from '../containers/PageLayout';
import { usePermissionProvider } from '../PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../PermissionProvider/PermissionProvider.interface';
import { WebhooksV1Props } from './WebhooksV1.interface';
import './webhookV1.less';
@ -47,9 +47,17 @@ const WebhooksV1: FC<WebhooksV1Props> = ({
onStatusFilter,
currentPage,
}) => {
const { isAuthDisabled, isAdminUser } = useAuth();
const [filteredData, setFilteredData] = useState<Array<Webhook>>(data);
const { permissions } = usePermissionProvider();
const addWebhookPermission = useMemo(
() =>
!isEmpty(permissions) &&
checkPermission(Operation.Create, ResourceEntity.WEBHOOK, permissions),
[permissions]
);
const getFilteredWebhooks = () => {
return selectedStatus.length
? data.filter(
@ -79,21 +87,22 @@ const WebhooksV1: FC<WebhooksV1Props> = ({
<ErrorPlaceHolder>
<p className="tw-text-center">{message}</p>
<p className="tw-text-center">
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Tooltip
placement="left"
title={
addWebhookPermission ? 'Add Webhook' : NO_PERMISSION_FOR_ACTION
}>
<Button
className={classNames('tw-h-8 tw-rounded tw-my-3', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
className={classNames('tw-h-8 tw-rounded tw-my-3')}
data-testid="add-webhook-button"
disabled={!addWebhookPermission}
size="small"
theme="primary"
variant="contained"
onClick={onAddWebhook}>
Add {webhookType === WebhookType.Slack ? 'Slack' : 'Webhook'}
</Button>
</NonAdminAction>
</Tooltip>
</p>
</ErrorPlaceHolder>
);
@ -131,14 +140,17 @@ const WebhooksV1: FC<WebhooksV1Props> = ({
className="tw-w-full tw-justify-end"
size={16}>
{filteredData.length > 0 && (
<NonAdminAction
position="bottom"
title={TITLE_FOR_NON_ADMIN_ACTION}>
<Tooltip
placement="left"
title={
addWebhookPermission
? 'Add Webhook'
: NO_PERMISSION_FOR_ACTION
}>
<Button
className={classNames('tw-h-8 tw-rounded ', {
'tw-opacity-40': !isAdminUser && !isAuthDisabled,
})}
className={classNames('tw-h-8 tw-rounded ')}
data-testid="add-webhook-button"
disabled={!addWebhookPermission}
size="small"
theme="primary"
variant="contained"
@ -146,7 +158,7 @@ const WebhooksV1: FC<WebhooksV1Props> = ({
Add{' '}
{webhookType === WebhookType.Slack ? 'Slack' : 'Webhook'}
</Button>
</NonAdminAction>
</Tooltip>
)}
</Space>
</Col>

View File

@ -36,3 +36,8 @@ export const ADD_POLICY_TEXT = `
Policies are assigned to Teams. In OpenMetadata, a Policy is a collection of Rules, which define access based on certain conditions. We support rich SpEL (Spring Expression Language) based conditions. All the operations supported by an Entity are published. Use these fine grained operations to define the conditional Rules for each Policy.
Create well-defined policies based on conditional rules to build rich access control roles.
`;
export const NO_PERMISSION_FOR_ACTION =
'You do not have permissions to perform this action.';
export const NO_PERMISSION_TO_VIEW = 'You do not have permission to view data';

View File

@ -11,7 +11,7 @@
* limitations under the License.
*/
import { Button, Col, Empty, Row, Space, Switch } from 'antd';
import { Button, Col, Row, Space, Switch } from 'antd';
import React, { useState } from 'react';
import { useHistory } from 'react-router-dom';
import BotListV1 from '../../components/BotListV1/BotListV1.component';
@ -19,7 +19,7 @@ import { usePermissionProvider } from '../../components/PermissionProvider/Permi
import { ResourceEntity } from '../../components/PermissionProvider/PermissionProvider.interface';
import { getCreateUserPath } from '../../constants/constants';
import { Operation } from '../../generated/entity/policies/accessControl/rule';
import { checkPemission } from '../../utils/PermissionsUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
export const BotsPageV1 = () => {
const { permissions } = usePermissionProvider();
@ -34,13 +34,7 @@ export const BotsPageV1 = () => {
setShowDeleted(checked);
};
const viewAllPermission = checkPemission(
Operation.ViewAll,
ResourceEntity.BOT,
permissions
);
const createPermission = checkPemission(
const createPermission = checkPermission(
Operation.Create,
ResourceEntity.BOT,
permissions
@ -48,35 +42,29 @@ export const BotsPageV1 = () => {
return (
<Row gutter={[16, 16]}>
{viewAllPermission ? (
<>
<Col flex={1} />
<Col>
<Space size={16}>
<Space align="end" size={5}>
<Switch
checked={showDeleted}
id="switch-deleted"
size="small"
onClick={handleShowDeleted}
/>
<label htmlFor="switch-deleted">Show deleted</label>
</Space>
<Col flex={1} />
<Col>
<Space size={16}>
<Space align="end" size={5}>
<Switch
checked={showDeleted}
id="switch-deleted"
size="small"
onClick={handleShowDeleted}
/>
<label htmlFor="switch-deleted">Show deleted</label>
</Space>
{createPermission && (
<Button type="primary" onClick={handleAddBotClick}>
Add Bot
</Button>
)}
</Space>
</Col>
<Col span={24}>
<BotListV1 showDeleted={showDeleted} />
</Col>
</>
) : (
<Empty description="You do not have permission to view data" />
)}
{createPermission && (
<Button type="primary" onClick={handleAddBotClick}>
Add Bot
</Button>
)}
</Space>
</Col>
<Col span={24}>
<BotListV1 showDeleted={showDeleted} />
</Col>
</Row>
);
};

View File

@ -17,7 +17,6 @@ import { LoadingState } from 'Models';
import React, { useEffect, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import AppState from '../../AppState';
import { useAuthContext } from '../../authentication/auth-provider/AuthProvider';
import { createBot } from '../../axiosAPIs/botsAPI';
import { createUser } from '../../axiosAPIs/userAPI';
import PageContainerV1 from '../../components/containers/PageContainerV1';
@ -30,14 +29,11 @@ import { EntityType } from '../../enums/entity.enum';
import { CreateUser } from '../../generated/api/teams/createUser';
import { Bot } from '../../generated/entity/bot';
import { Role } from '../../generated/entity/teams/role';
import { useAuth } from '../../hooks/authHooks';
import jsonData from '../../jsons/en';
import { getSettingPath } from '../../utils/RouterUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
const CreateUserPage = () => {
const { isAdminUser } = useAuth();
const { isAuthDisabled } = useAuthContext();
const history = useHistory();
const [roles, setRoles] = useState<Array<Role>>([]);
@ -128,7 +124,6 @@ const CreateUserPage = () => {
<PageContainerV1>
<div className="tw-self-center">
<CreateUserComponent
allowAccess={isAdminUser || isAuthDisabled}
forceBot={Boolean(bot)}
roles={roles}
saveState={status}

View File

@ -11,10 +11,10 @@
* limitations under the License.
*/
import { Col, Row } from 'antd';
import { Col, Empty, Row } from 'antd';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { isUndefined } from 'lodash';
import { isEmpty, isUndefined } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory, useParams } from 'react-router-dom';
import { getTypeByFQN, updateType } from '../../axiosAPIs/metadataTypeAPI';
@ -23,11 +23,16 @@ import ErrorPlaceHolder from '../../components/common/error-with-placeholder/Err
import TabsPane from '../../components/common/TabsPane/TabsPane';
import { CustomPropertyTable } from '../../components/CustomEntityDetail/CustomPropertyTable';
import Loader from '../../components/Loader/Loader';
import { usePermissionProvider } from '../../components/PermissionProvider/PermissionProvider';
import SchemaEditor from '../../components/schema-editor/SchemaEditor';
import { getAddCustomPropertyPath } from '../../constants/constants';
import { customAttributesPath } from '../../constants/globalSettings.constants';
import { NO_PERMISSION_TO_VIEW } from '../../constants/HelperTextUtil';
import { Operation } from '../../generated/entity/policies/policy';
import { Type } from '../../generated/entity/type';
import jsonData from '../../jsons/en';
import { getResourceEntityFromCustomProperty } from '../../utils/CustomPropertyUtils';
import { checkPermission } from '../../utils/PermissionsUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import './CustomPropertiesPageV1.less';
@ -44,6 +49,19 @@ const CustomEntityDetailV1 = () => {
const tabAttributePath =
customAttributesPath[tab as keyof typeof customAttributesPath];
const { permissions } = usePermissionProvider();
const viewPermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(
Operation.ViewAll,
getResourceEntityFromCustomProperty(tab),
permissions
)
);
}, [permissions, tab]);
const fetchTypeDetail = async (typeFQN: string) => {
setIsLoading(true);
try {
@ -120,7 +138,7 @@ const CustomEntityDetailV1 = () => {
);
}
return (
return viewPermission ? (
<Row
className="tw-my-2"
data-testid="custom-entity-container"
@ -162,6 +180,12 @@ const CustomEntityDetailV1 = () => {
)}
</Col>
</Row>
) : (
<Row>
<Col span={24}>
<Empty description={NO_PERMISSION_TO_VIEW} />
</Col>
</Row>
);
};

View File

@ -41,15 +41,18 @@ import Description from '../../../components/common/description/Description';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
import Loader from '../../../components/Loader/Loader';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../components/PermissionProvider/PermissionProvider.interface';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../../constants/globalSettings.constants';
import { EntityType } from '../../../enums/entity.enum';
import { Rule } from '../../../generated/api/policies/createPolicy';
import { Policy } from '../../../generated/entity/policies/policy';
import { Operation, Policy } from '../../../generated/entity/policies/policy';
import { EntityReference } from '../../../generated/type/entityReference';
import { getEntityName } from '../../../utils/CommonUtils';
import { checkPermission } from '../../../utils/PermissionsUtils';
import {
getAddPolicyRulePath,
getEditPolicyRulePath,
@ -147,6 +150,7 @@ const List = ({
const PoliciesDetailPage = () => {
const history = useHistory();
const { fqn } = useParams<{ fqn: string }>();
const { permissions } = usePermissionProvider();
const [policy, setPolicy] = useState<Policy>({} as Policy);
const [isLoading, setLoading] = useState<boolean>(false);
@ -159,6 +163,17 @@ const PoliciesDetailPage = () => {
GlobalSettingOptions.POLICIES
);
const editDescriptionPermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(
Operation.EditDescription,
ResourceEntity.ROLE,
permissions
)
);
}, [permissions]);
const breadcrumb = useMemo(
() => [
{
@ -393,6 +408,7 @@ const PoliciesDetailPage = () => {
entityFqn={policy.fullyQualifiedName}
entityName={getEntityName(policy)}
entityType={EntityType.POLICY}
hasEditAccess={editDescriptionPermission}
isEdit={editDescription}
onCancel={() => setEditDescription(false)}
onDescriptionEdit={() => setEditDescription(true)}

View File

@ -11,18 +11,24 @@
* limitations under the License.
*/
import { Button, Popover, Space, Table, Tag } from 'antd';
import { Button, Popover, Space, Table, Tag, Tooltip } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { isUndefined, uniqueId } from 'lodash';
import { isEmpty, isUndefined, uniqueId } from 'lodash';
import React, { FC, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import DeleteWidgetModal from '../../../components/common/DeleteWidget/DeleteWidgetModal';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../components/PermissionProvider/PermissionProvider.interface';
import {
NO_PERMISSION_FOR_ACTION,
NO_PERMISSION_TO_VIEW,
} from '../../../constants/HelperTextUtil';
import { EntityType } from '../../../enums/entity.enum';
import { Policy } from '../../../generated/entity/policies/policy';
import { Operation, Policy } from '../../../generated/entity/policies/policy';
import { Paging } from '../../../generated/type/paging';
import { getEntityName } from '../../../utils/CommonUtils';
import { LIST_CAP } from '../../../utils/PermissionsUtils';
import { checkPermission, LIST_CAP } from '../../../utils/PermissionsUtils';
import {
getPolicyWithFqnPath,
getRoleWithFqnPath,
@ -36,6 +42,23 @@ interface PolicyListProps {
const PoliciesList: FC<PolicyListProps> = ({ policies, fetchPolicies }) => {
const [selectedPolicy, setSelectedPolicy] = useState<Policy>();
const { permissions } = usePermissionProvider();
const deletePolicyPermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(Operation.Delete, ResourceEntity.POLICY, permissions)
);
}, [permissions]);
const viewRolePermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(Operation.ViewAll, ResourceEntity.ROLE, permissions)
);
}, [permissions]);
const columns: ColumnsType<Policy> = useMemo(() => {
return [
{
@ -71,27 +94,39 @@ const PoliciesList: FC<PolicyListProps> = ({ policies, fetchPolicies }) => {
return record.roles?.length ? (
<Space wrap data-testid="role-link" size={4}>
{record.roles.slice(0, LIST_CAP).map((role) => (
<Link
key={uniqueId()}
to={getRoleWithFqnPath(role.fullyQualifiedName || '')}>
{getEntityName(role)}
</Link>
))}
{record.roles.slice(0, LIST_CAP).map((role) =>
viewRolePermission ? (
<Link
key={uniqueId()}
to={getRoleWithFqnPath(role.fullyQualifiedName || '')}>
{getEntityName(role)}
</Link>
) : (
<Tooltip title={NO_PERMISSION_TO_VIEW}>
{getEntityName(role)}
</Tooltip>
)
)}
{hasMore && (
<Popover
className="tw-cursor-pointer"
content={
<Space wrap size={4}>
{record.roles.slice(LIST_CAP).map((role) => (
<Link
key={uniqueId()}
to={getRoleWithFqnPath(
role.fullyQualifiedName || ''
)}>
{getEntityName(role)}
</Link>
))}
{record.roles.slice(LIST_CAP).map((role) =>
viewRolePermission ? (
<Link
key={uniqueId()}
to={getRoleWithFqnPath(
role.fullyQualifiedName || ''
)}>
{getEntityName(role)}
</Link>
) : (
<Tooltip title={NO_PERMISSION_TO_VIEW}>
{getEntityName(role)}
</Tooltip>
)
)}
</Space>
}
overlayClassName="tw-w-40 tw-text-center"
@ -114,12 +149,19 @@ const PoliciesList: FC<PolicyListProps> = ({ policies, fetchPolicies }) => {
key: 'actions',
render: (_, record) => {
return (
<Button
data-testid={`delete-action-${getEntityName(record)}`}
type="text"
onClick={() => setSelectedPolicy(record)}>
<SVGIcons alt="delete" icon={Icons.DELETE} width="18px" />
</Button>
<Tooltip
placement="left"
title={
deletePolicyPermission ? 'Delete' : NO_PERMISSION_FOR_ACTION
}>
<Button
data-testid={`delete-action-${getEntityName(record)}`}
disabled={!deletePolicyPermission}
type="text"
onClick={() => setSelectedPolicy(record)}>
<SVGIcons alt="delete" icon={Icons.DELETE} width="18px" />
</Button>
</Tooltip>
);
},
},
@ -135,7 +177,7 @@ const PoliciesList: FC<PolicyListProps> = ({ policies, fetchPolicies }) => {
pagination={false}
size="middle"
/>
{selectedPolicy && (
{selectedPolicy && deletePolicyPermission && (
<DeleteWidgetModal
afterDeleteAction={fetchPolicies}
allowSoftDelete={false}

View File

@ -11,20 +11,25 @@
* limitations under the License.
*/
import { Button, Col, Row, Space } from 'antd';
import { Button, Col, Row, Space, Tooltip } from 'antd';
import { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import { isEmpty } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { getPolicies } from '../../../axiosAPIs/rolesAPIV1';
import NextPrevious from '../../../components/common/next-previous/NextPrevious';
import Loader from '../../../components/Loader/Loader';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../components/PermissionProvider/PermissionProvider.interface';
import {
INITIAL_PAGING_VALUE,
PAGE_SIZE,
ROUTES,
} from '../../../constants/constants';
import { Policy } from '../../../generated/entity/policies/policy';
import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil';
import { Operation, Policy } from '../../../generated/entity/policies/policy';
import { Paging } from '../../../generated/type/paging';
import { checkPermission } from '../../../utils/PermissionsUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import PoliciesList from './PoliciesList';
import './PoliciesList.less';
@ -36,6 +41,15 @@ const PoliciesListPage = () => {
const [paging, setPaging] = useState<Paging>();
const [currentPage, setCurrentPage] = useState<number>(INITIAL_PAGING_VALUE);
const { permissions } = usePermissionProvider();
const addPolicyPermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(Operation.Create, ResourceEntity.POLICY, permissions)
);
}, [permissions]);
const fetchPolicies = async (paging?: Paging) => {
setIsLoading(true);
try {
@ -73,12 +87,19 @@ const PoliciesListPage = () => {
<Row className="policies-list-container" gutter={[16, 16]}>
<Col span={24}>
<Space align="center" className="tw-w-full tw-justify-end" size={16}>
<Button
data-testid="add-policy"
type="primary"
onClick={handleAddPolicy}>
Add Policy
</Button>
<Tooltip
placement="left"
title={
addPolicyPermission ? 'Add Policy' : NO_PERMISSION_FOR_ACTION
}>
<Button
data-testid="add-policy"
disabled={!addPolicyPermission}
type="primary"
onClick={handleAddPolicy}>
Add Policy
</Button>
</Tooltip>
</Space>
</Col>
<Col span={24}>

View File

@ -26,14 +26,18 @@ import Description from '../../../components/common/description/Description';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import TitleBreadcrumb from '../../../components/common/title-breadcrumb/title-breadcrumb.component';
import Loader from '../../../components/Loader/Loader';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../components/PermissionProvider/PermissionProvider.interface';
import { getUserPath } from '../../../constants/constants';
import {
GlobalSettingOptions,
GlobalSettingsMenuCategory,
} from '../../../constants/globalSettings.constants';
import { EntityType } from '../../../enums/entity.enum';
import { Operation } from '../../../generated/entity/policies/policy';
import { Role } from '../../../generated/entity/teams/role';
import { getEntityName } from '../../../utils/CommonUtils';
import { checkPermission } from '../../../utils/PermissionsUtils';
import {
getPolicyWithFqnPath,
getSettingPath,
@ -139,6 +143,7 @@ const List = ({
const RolesDetailPage = () => {
const history = useHistory();
const { permissions } = usePermissionProvider();
const { fqn } = useParams<{ fqn: string }>();
const [role, setRole] = useState<Role>({} as Role);
@ -154,6 +159,17 @@ const RolesDetailPage = () => {
GlobalSettingOptions.ROLES
);
const editDescriptionPermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(
Operation.EditDescription,
ResourceEntity.ROLE,
permissions
)
);
}, [permissions]);
const breadcrumb = useMemo(
() => [
{
@ -319,6 +335,7 @@ const RolesDetailPage = () => {
entityFqn={role.fullyQualifiedName}
entityName={getEntityName(role)}
entityType={EntityType.ROLE}
hasEditAccess={editDescriptionPermission}
isEdit={editDescription}
onCancel={() => setEditDescription(false)}
onDescriptionEdit={() => setEditDescription(true)}

View File

@ -11,18 +11,25 @@
* limitations under the License.
*/
import { Button, Popover, Space, Table, Tag } from 'antd';
import { Button, Popover, Space, Table, Tag, Tooltip } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { isUndefined, uniqueId } from 'lodash';
import { isEmpty, isUndefined, uniqueId } from 'lodash';
import React, { FC, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import DeleteWidgetModal from '../../../components/common/DeleteWidget/DeleteWidgetModal';
import RichTextEditorPreviewer from '../../../components/common/rich-text-editor/RichTextEditorPreviewer';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../components/PermissionProvider/PermissionProvider.interface';
import {
NO_PERMISSION_FOR_ACTION,
NO_PERMISSION_TO_VIEW,
} from '../../../constants/HelperTextUtil';
import { EntityType } from '../../../enums/entity.enum';
import { Operation } from '../../../generated/entity/policies/policy';
import { Role } from '../../../generated/entity/teams/role';
import { Paging } from '../../../generated/type/paging';
import { getEntityName } from '../../../utils/CommonUtils';
import { LIST_CAP } from '../../../utils/PermissionsUtils';
import { checkPermission, LIST_CAP } from '../../../utils/PermissionsUtils';
import {
getPolicyWithFqnPath,
getRoleWithFqnPath,
@ -37,6 +44,22 @@ interface RolesListProps {
const RolesList: FC<RolesListProps> = ({ roles, fetchRoles }) => {
const [selectedRole, setSelectedRole] = useState<Role>();
const { permissions } = usePermissionProvider();
const viewPolicyPermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(Operation.ViewAll, ResourceEntity.POLICY, permissions)
);
}, [permissions]);
const deleteRolePermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(Operation.Delete, ResourceEntity.ROLE, permissions)
);
}, [permissions]);
const columns: ColumnsType<Role> = useMemo(() => {
return [
{
@ -72,27 +95,39 @@ const RolesList: FC<RolesListProps> = ({ roles, fetchRoles }) => {
return record.policies?.length ? (
<Space wrap data-testid="policy-link" size={4}>
{record.policies.slice(0, LIST_CAP).map((policy) => (
<Link
key={uniqueId()}
to={getPolicyWithFqnPath(policy.fullyQualifiedName || '')}>
{getEntityName(policy)}
</Link>
))}
{record.policies.slice(0, LIST_CAP).map((policy) =>
viewPolicyPermission ? (
<Link
key={uniqueId()}
to={getPolicyWithFqnPath(policy.fullyQualifiedName || '')}>
{getEntityName(policy)}
</Link>
) : (
<Tooltip title={NO_PERMISSION_TO_VIEW}>
{getEntityName(policy)}
</Tooltip>
)
)}
{hasMore && (
<Popover
className="tw-cursor-pointer"
content={
<Space wrap size={4}>
{record.policies.slice(LIST_CAP).map((policy) => (
<Link
key={uniqueId()}
to={getPolicyWithFqnPath(
policy.fullyQualifiedName || ''
)}>
{getEntityName(policy)}
</Link>
))}
{record.policies.slice(LIST_CAP).map((policy) =>
viewPolicyPermission ? (
<Link
key={uniqueId()}
to={getPolicyWithFqnPath(
policy.fullyQualifiedName || ''
)}>
{getEntityName(policy)}
</Link>
) : (
<Tooltip title={NO_PERMISSION_TO_VIEW}>
{getEntityName(policy)}
</Tooltip>
)
)}
</Space>
}
overlayClassName="tw-w-40 tw-text-center"
@ -115,12 +150,21 @@ const RolesList: FC<RolesListProps> = ({ roles, fetchRoles }) => {
key: 'actions',
render: (_, record) => {
return (
<Button
data-testid={`delete-action-${getEntityName(record)}`}
type="text"
onClick={() => setSelectedRole(record)}>
<SVGIcons alt="delete" icon={Icons.DELETE} width="18px" />
</Button>
<Tooltip
placement="left"
title={
deleteRolePermission ? 'Delete' : NO_PERMISSION_FOR_ACTION
}>
<Button
data-testid={`delete-action-${getEntityName(record)}`}
disabled={!deleteRolePermission}
icon={
<SVGIcons alt="delete" icon={Icons.DELETE} width="18px" />
}
type="text"
onClick={() => setSelectedRole(record)}
/>
</Tooltip>
);
},
},

View File

@ -42,7 +42,27 @@ jest.mock('./RolesList', () =>
jest.fn().mockReturnValue(<div data-testid="roles-list">RolesList</div>)
);
describe('Test Roled List Page', () => {
jest.mock('../../../utils/PermissionsUtils', () => ({
checkPermission: jest.fn().mockReturnValue(true),
}));
jest.mock('../../../components/PermissionProvider/PermissionProvider', () => ({
usePermissionProvider: jest.fn().mockReturnValue({
permissions: {
role: {
Create: true,
Delete: true,
ViewAll: true,
EditAll: true,
EditDescription: true,
EditDisplayName: true,
EditCustomFields: true,
},
},
}),
}));
describe('Test Roles List Page', () => {
it('Should render the list component', async () => {
render(<RolesListPage />);

View File

@ -11,20 +11,26 @@
* limitations under the License.
*/
import { Button, Col, Row, Space } from 'antd';
import { Button, Col, Row, Space, Tooltip } from 'antd';
import { AxiosError } from 'axios';
import React, { useEffect, useState } from 'react';
import { isEmpty } from 'lodash';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { getRoles } from '../../../axiosAPIs/rolesAPIV1';
import NextPrevious from '../../../components/common/next-previous/NextPrevious';
import Loader from '../../../components/Loader/Loader';
import { usePermissionProvider } from '../../../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../../../components/PermissionProvider/PermissionProvider.interface';
import {
INITIAL_PAGING_VALUE,
PAGE_SIZE,
ROUTES,
} from '../../../constants/constants';
import { NO_PERMISSION_FOR_ACTION } from '../../../constants/HelperTextUtil';
import { Operation } from '../../../generated/entity/policies/policy';
import { Role } from '../../../generated/entity/teams/role';
import { Paging } from '../../../generated/type/paging';
import { checkPermission } from '../../../utils/PermissionsUtils';
import { showErrorToast } from '../../../utils/ToastUtils';
import RolesList from './RolesList';
import './RolesList.less';
@ -37,6 +43,15 @@ const RolesListPage = () => {
const [paging, setPaging] = useState<Paging>();
const [currentPage, setCurrentPage] = useState<number>(INITIAL_PAGING_VALUE);
const { permissions } = usePermissionProvider();
const addRolePermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(Operation.Create, ResourceEntity.ROLE, permissions)
);
}, [permissions]);
const fetchRoles = async (paging?: Paging) => {
setIsLoading(true);
try {
@ -77,9 +92,17 @@ const RolesListPage = () => {
gutter={[16, 16]}>
<Col span={24}>
<Space align="center" className="tw-w-full tw-justify-end" size={16}>
<Button data-testid="add-role" type="primary" onClick={handleAddRole}>
Add Role
</Button>
<Tooltip
placement="left"
title={addRolePermission ? 'Add Role' : NO_PERMISSION_FOR_ACTION}>
<Button
data-testid="add-role"
disabled={!addRolePermission}
type="primary"
onClick={handleAddRole}>
Add Role
</Button>
</Tooltip>
</Space>
</Col>
<Col span={24}>

View File

@ -887,7 +887,7 @@ const ServicePage: FunctionComponent = () => {
};
return (
<Row>
<Row className="tw-my-4" gutter={[16, 16]}>
{isLoading ? (
<Loader />
) : isError ? (

View File

@ -11,19 +11,26 @@
* limitations under the License.
*/
import { Col, Empty, Row } from 'antd';
import { AxiosError } from 'axios';
import { isEmpty } from 'lodash';
import { ServiceCategory } from 'Models';
import React, { useEffect, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { useParams } from 'react-router-dom';
import { getServices } from '../../axiosAPIs/serviceAPI';
import Loader from '../../components/Loader/Loader';
import { usePermissionProvider } from '../../components/PermissionProvider/PermissionProvider';
import Services from '../../components/Services/Services';
import { pagingObject } from '../../constants/constants';
import { NO_PERMISSION_TO_VIEW } from '../../constants/HelperTextUtil';
import { SERVICE_CATEGORY } from '../../constants/services.const';
import { ServiceCategory as Category } from '../../enums/service.enum';
import { Operation } from '../../generated/entity/policies/policy';
import { Paging } from '../../generated/type/paging';
import { ServicesType } from '../../interface/service.interface';
import jsonData from '../../jsons/en';
import { checkPermission } from '../../utils/PermissionsUtils';
import { getResourceEntityFromServiceCategory } from '../../utils/ServiceUtils';
import { showErrorToast } from '../../utils/ToastUtils';
const ServicesPage = () => {
@ -37,6 +44,19 @@ const ServicesPage = () => {
);
const [currentPage, setCurrentPage] = useState<number>(1);
const { permissions } = usePermissionProvider();
const viewAllPermission = useMemo(() => {
return (
!isEmpty(permissions) &&
checkPermission(
Operation.ViewAll,
getResourceEntityFromServiceCategory(tab),
permissions
)
);
}, [permissions]);
const getServiceDetails = async (type: string) => {
setIsLoading(true);
try {
@ -78,7 +98,7 @@ const ServicesPage = () => {
return <Loader />;
}
return (
return viewAllPermission ? (
<Services
currentPage={currentPage}
paging={paging}
@ -86,6 +106,12 @@ const ServicesPage = () => {
serviceName={serviceName}
onPageChange={handlePageChange}
/>
) : (
<Row>
<Col span={24}>
<Empty description={NO_PERMISSION_TO_VIEW} />
</Col>
</Row>
);
};

View File

@ -15,7 +15,12 @@ import { isEmpty } from 'lodash';
import React, { FunctionComponent } from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import AppState from '../AppState';
import { usePermissionProvider } from '../components/PermissionProvider/PermissionProvider';
import { ResourceEntity } from '../components/PermissionProvider/PermissionProvider.interface';
import { ROUTES } from '../constants/constants';
import { Operation } from '../generated/entity/policies/policy';
import { checkPermission } from '../utils/PermissionsUtils';
import AdminProtectedRoute from './AdminProtectedRoute';
import withSuspenseFallback from './withSuspenseFallback';
const GlobalSettingPage = withSuspenseFallback(
@ -70,9 +75,6 @@ const TourPageComponent = withSuspenseFallback(
const UserPage = withSuspenseFallback(
React.lazy(() => import('../pages/UserPage/UserPage.component'))
);
const AdminProtectedRoute = withSuspenseFallback(
React.lazy(() => import('./AdminProtectedRoute'))
);
const AddGlossaryPage = withSuspenseFallback(
React.lazy(() => import('../pages/AddGlossary/AddGlossaryPage.component'))
@ -190,6 +192,8 @@ const EditRulePage = withSuspenseFallback(
);
const AuthenticatedAppRouter: FunctionComponent = () => {
const { permissions } = usePermissionProvider();
return (
<Switch>
<Route exact component={MyDataPage} path={ROUTES.MY_DATA} />
@ -208,11 +212,21 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
<AdminProtectedRoute
exact
component={AddIngestionPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.INGESTION_PIPELINE,
permissions
)}
path={ROUTES.ADD_INGESTION}
/>
<AdminProtectedRoute
exact
component={EditIngestionPage}
hasPermission={checkPermission(
Operation.EditAll,
ResourceEntity.INGESTION_PIPELINE,
permissions
)}
path={ROUTES.EDIT_INGESTION}
/>
<Route exact component={SignupPage} path={ROUTES.SIGNUP}>
@ -290,47 +304,92 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
<AdminProtectedRoute
exact
component={AddGlossaryPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.GLOSSARY,
permissions
)}
path={ROUTES.ADD_GLOSSARY}
/>
<AdminProtectedRoute
exact
component={AddGlossaryTermPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.GLOSSARY_TERM,
permissions
)}
path={ROUTES.ADD_GLOSSARY_TERMS_CHILD}
/>
<AdminProtectedRoute
exact
component={AddGlossaryTermPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.GLOSSARY_TERM,
permissions
)}
path={ROUTES.ADD_GLOSSARY_TERMS}
/>
<AdminProtectedRoute
exact
component={AddWebhookPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.WEBHOOK,
permissions
)}
path={ROUTES.ADD_WEBHOOK_WITH_TYPE}
/>
<AdminProtectedRoute
exact
component={AddWebhookPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.WEBHOOK,
permissions
)}
path={ROUTES.ADD_WEBHOOK}
/>
<AdminProtectedRoute
exact
component={CreateUserPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.USER,
permissions
)}
path={ROUTES.CREATE_USER}
/>
<AdminProtectedRoute
exact
component={CreateUserPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.BOT,
permissions
)}
path={ROUTES.CREATE_USER_WITH_BOT}
/>
<AdminProtectedRoute
exact
component={BotDetailsPage}
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.BOT,
permissions
)}
path={ROUTES.BOTS_PROFILE}
/>
<AdminProtectedRoute
exact
component={AddCustomProperty}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.TYPE,
permissions
)}
path={ROUTES.ADD_CUSTOM_PROPERTY}
/>
<Route
@ -352,21 +411,41 @@ const AuthenticatedAppRouter: FunctionComponent = () => {
<AdminProtectedRoute
exact
component={AddRolePage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.ROLE,
permissions
)}
path={ROUTES.ADD_ROLE}
/>
<AdminProtectedRoute
exact
component={AddPolicyPage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.POLICY,
permissions
)}
path={ROUTES.ADD_POLICY}
/>
<AdminProtectedRoute
exact
component={AddRulePage}
hasPermission={checkPermission(
Operation.Create,
ResourceEntity.POLICY,
permissions
)}
path={ROUTES.ADD_POLICY_RULE}
/>
<AdminProtectedRoute
exact
component={EditRulePage}
hasPermission={checkPermission(
Operation.EditAll,
ResourceEntity.POLICY,
permissions
)}
path={ROUTES.EDIT_POLICY_RULE}
/>

View File

@ -21,7 +21,7 @@ import {
} from '../constants/globalSettings.constants';
import { Operation } from '../generated/entity/policies/policy';
import TeamsPage from '../pages/teams/TeamsPage';
import { checkPemission } from '../utils/PermissionsUtils';
import { checkPermission } from '../utils/PermissionsUtils';
import { getSettingCategoryPath, getSettingPath } from '../utils/RouterUtils';
import AdminProtectedRoute from './AdminProtectedRoute';
import withSuspenseFallback from './withSuspenseFallback';
@ -103,7 +103,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={RolesListPage}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.ROLE,
permissions
@ -117,7 +117,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={RolesDetailPage}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.ROLE,
permissions
@ -135,7 +135,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={PoliciesListPage}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.POLICY,
permissions
@ -148,7 +148,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={PoliciesDetailPage}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.POLICY,
permissions
@ -162,7 +162,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={UserListPageV1}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.USER,
permissions
@ -173,7 +173,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={WebhooksPageV1}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.WEBHOOK,
permissions
@ -186,7 +186,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={BotsPageV1}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.BOT,
permissions
@ -200,7 +200,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={SlackSettingsPage}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.WEBHOOK,
permissions
@ -220,7 +220,7 @@ const GlobalSettingRouter = () => {
<AdminProtectedRoute
exact
component={CustomPropertiesPageV1}
hasPermission={checkPemission(
hasPermission={checkPermission(
Operation.ViewAll,
ResourceEntity.ALL,
permissions

View File

@ -29,3 +29,4 @@
@import url('./components/table.less');
@import url('./components/toggle-switch.less');
@import url('./components/button.less');

View File

@ -0,0 +1,7 @@
.ant-btn {
&:disabled {
img {
opacity: 0.5;
}
}
}

View File

@ -0,0 +1,22 @@
import { ResourceEntity } from '../components/PermissionProvider/PermissionProvider.interface';
export const getResourceEntityFromCustomProperty = (property: string) => {
switch (property) {
case 'tables':
return ResourceEntity.TABLE;
case 'topics':
return ResourceEntity.TOPIC;
case 'dashboards':
return ResourceEntity.DASHBOARD;
case 'pipelines':
return ResourceEntity.PIPELINE;
case 'mlModels':
return ResourceEntity.ML_MODEL;
}
return ResourceEntity.TABLE;
};

View File

@ -11,6 +11,7 @@
* limitations under the License.
*/
import AppState from '../AppState';
import {
OperationPermission,
ResourceEntity,
@ -25,6 +26,7 @@ import {
import { Operation } from '../generated/entity/policies/policy';
/**
* @deprecated
* TODO: Remove this method once we have new permission structure everywhere
*/
export const hasPemission = (
@ -50,22 +52,26 @@ export const hasPemission = (
* @param permissions UIPermission
* @returns boolean - true/false
*/
export const checkPemission = (
export const checkPermission = (
operation: Operation,
resourceType: ResourceEntity,
permissions: UIPermission
) => {
const allResource = permissions.all;
const entityResource = permissions[resourceType];
const isAuthDisabled = AppState.authDisabled;
const allResource = permissions?.all;
const entityResource = permissions?.[resourceType];
let hasPemission = isAuthDisabled;
/**
* If allresource is present then check for permission and return it
*/
if (allResource) {
return allResource.All || allResource[operation];
if (allResource && !hasPemission) {
hasPemission = allResource.All || allResource[operation];
}
return entityResource[operation];
hasPemission = hasPemission || (entityResource && entityResource[operation]);
return hasPemission;
};
/**

View File

@ -22,6 +22,7 @@ import {
} from 'Models';
import React from 'react';
import { getEntityCount } from '../axiosAPIs/miscAPI';
import { ResourceEntity } from '../components/PermissionProvider/PermissionProvider.interface';
import { GlobalSettingOptions } from '../constants/globalSettings.constants';
import {
addLineageIngestionGuide,
@ -743,3 +744,31 @@ export const getServiceRouteFromServiceType = (type: ServiceTypes) => {
return GlobalSettingOptions.DATABASES;
};
export const getResourceEntityFromServiceCategory = (
category: string | ServiceCategory
) => {
switch (category) {
case 'dashboards':
case ServiceCategory.DASHBOARD_SERVICES:
return ResourceEntity.DASHBOARD_SERVICE;
case 'databases':
case ServiceCategory.DATABASE_SERVICES:
return ResourceEntity.DATABASE_SERVICE;
case 'mlModels':
case ServiceCategory.ML_MODAL_SERVICES:
return ResourceEntity.ML_MODEL_SERVICE;
case 'messaging':
case ServiceCategory.MESSAGING_SERVICES:
return ResourceEntity.MESSAGING_SERVICE;
case 'pipelines':
case ServiceCategory.PIPELINE_SERVICES:
return ResourceEntity.PIPELINE_SERVICE;
}
return ResourceEntity.DATABASE_SERVICE;
};