Allow Asset add and remove operation for editTag permission user (#18786)

* Allow Asset add and remove operation for editTag permission user

* remove edit all permission check in bulkAssetTag api for tag

* fix the permission check by checking specific and the index used to exclude

* check editTag permission before updating assets by bulkTagAsset api

* fix the button not visible for the non admin user

* added playwright for the limited user check aroud asset

* restrict add asset button in certification page

* minor fix

* cleanup aroud tag page and fix usertab count around Team page

* use entity type instead of search index

---------

Co-authored-by: sonikashah <sonikashah94@gmail.com>
Co-authored-by: karanh37 <karanh37@gmail.com>
This commit is contained in:
Ashish Gupta 2024-11-28 16:15:07 +05:30 committed by GitHub
parent 9a21e77e15
commit 7877d5c14c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 616 additions and 67 deletions

View File

@ -220,6 +220,13 @@ public final class CatalogExceptionMessage {
"Principal: CatalogPrincipal{name='%s'} operations %s not allowed", user, operations);
}
public static String resourcePermissionNotAllowed(
String user, List<MetadataOperation> operations, List<String> resources) {
return String.format(
"Principal: CatalogPrincipal{name='%s'} operations %s not allowed for resources {%s}.",
user, operations, resources);
}
public static String domainPermissionNotAllowed(
String user, String domainName, List<MetadataOperation> operations) {
return String.format(

View File

@ -19,6 +19,7 @@ import static org.openmetadata.common.utils.CommonUtil.nullOrEmpty;
import static org.openmetadata.schema.type.EventType.ENTITY_CREATED;
import static org.openmetadata.schema.type.MetadataOperation.CREATE;
import static org.openmetadata.schema.type.MetadataOperation.VIEW_BASIC;
import static org.openmetadata.service.security.DefaultAuthorizer.getSubjectContext;
import static org.openmetadata.service.util.EntityUtil.createOrUpdateOperation;
import java.io.IOException;
@ -29,6 +30,7 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
import javax.json.JsonPatch;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
@ -42,6 +44,8 @@ import org.openmetadata.schema.type.EntityHistory;
import org.openmetadata.schema.type.EntityReference;
import org.openmetadata.schema.type.Include;
import org.openmetadata.schema.type.MetadataOperation;
import org.openmetadata.schema.type.Permission;
import org.openmetadata.schema.type.ResourcePermission;
import org.openmetadata.schema.type.api.BulkOperationResult;
import org.openmetadata.schema.type.csv.CsvImportResult;
import org.openmetadata.service.Entity;
@ -52,11 +56,13 @@ import org.openmetadata.service.jdbi3.ListFilter;
import org.openmetadata.service.limits.Limits;
import org.openmetadata.service.search.SearchListFilter;
import org.openmetadata.service.search.SearchSortFilter;
import org.openmetadata.service.security.AuthorizationException;
import org.openmetadata.service.security.Authorizer;
import org.openmetadata.service.security.policyevaluator.CreateResourceContext;
import org.openmetadata.service.security.policyevaluator.OperationContext;
import org.openmetadata.service.security.policyevaluator.ResourceContext;
import org.openmetadata.service.security.policyevaluator.ResourceContextInterface;
import org.openmetadata.service.security.policyevaluator.SubjectContext;
import org.openmetadata.service.util.AsyncService;
import org.openmetadata.service.util.BulkAssetsOperationResponse;
import org.openmetadata.service.util.CSVExportResponse;
@ -418,9 +424,37 @@ public abstract class EntityResource<T extends EntityInterface, K extends Entity
public Response bulkAddToAssetsAsync(
SecurityContext securityContext, UUID entityId, BulkAssetsRequestInterface request) {
OperationContext operationContext =
new OperationContext(entityType, MetadataOperation.EDIT_ALL);
authorizer.authorize(securityContext, operationContext, getResourceContextById(entityId));
SubjectContext subjectContext = getSubjectContext(securityContext);
String user = subjectContext.user().getName();
Set<String> editPermissibleResources =
authorizer.listPermissions(securityContext, user).stream()
.filter(
permission ->
permission.getPermissions().stream()
.anyMatch(
perm ->
MetadataOperation.EDIT_TAGS.equals(perm.getOperation())
&& Permission.Access.ALLOW.equals(perm.getAccess())))
.map(ResourcePermission::getResource)
.collect(Collectors.toSet());
// Validate if all entity types in the request are in the permissible resources
List<String> unauthorizedEntityTypes =
request.getAssets().stream()
.map(EntityReference::getType)
.filter(entityType -> !editPermissibleResources.contains(entityType))
.distinct()
.toList();
if (!unauthorizedEntityTypes.isEmpty()
&& !subjectContext.isAdmin()
&& !subjectContext.isBot()) {
throw new AuthorizationException(
CatalogExceptionMessage.resourcePermissionNotAllowed(
user, List.of(MetadataOperation.EDIT_TAGS), unauthorizedEntityTypes));
}
String jobId = UUID.randomUUID().toString();
ExecutorService executorService = AsyncService.getInstance().getExecutorService();
executorService.submit(
@ -443,9 +477,34 @@ public abstract class EntityResource<T extends EntityInterface, K extends Entity
public Response bulkRemoveFromAssetsAsync(
SecurityContext securityContext, UUID entityId, BulkAssetsRequestInterface request) {
OperationContext operationContext =
new OperationContext(entityType, MetadataOperation.EDIT_ALL);
authorizer.authorize(securityContext, operationContext, getResourceContextById(entityId));
SubjectContext subjectContext = getSubjectContext(securityContext);
String user = subjectContext.user().getName();
Set<String> editPermissibleResources =
authorizer.listPermissions(securityContext, user).stream()
.filter(
permission ->
permission.getPermissions().stream()
.anyMatch(
perm ->
MetadataOperation.EDIT_TAGS.equals(perm.getOperation())
&& Permission.Access.ALLOW.equals(perm.getAccess())))
.map(ResourcePermission::getResource)
.collect(Collectors.toSet());
List<String> unauthorizedEntityTypes =
request.getAssets().stream()
.map(EntityReference::getType)
.filter(entityType -> !editPermissibleResources.contains(entityType))
.distinct()
.toList();
if (!unauthorizedEntityTypes.isEmpty()
&& !subjectContext.isAdmin()
&& !subjectContext.isBot()) {
throw new AuthorizationException(
CatalogExceptionMessage.resourcePermissionNotAllowed(
user, List.of(MetadataOperation.EDIT_TAGS), unauthorizedEntityTypes));
}
String jobId = UUID.randomUUID().toString();
ExecutorService executorService = AsyncService.getInstance().getExecutorService();
executorService.submit(

View File

@ -11,28 +11,29 @@
* limitations under the License.
*/
import { expect, Page, test as base } from '@playwright/test';
import { DATA_STEWARD_RULES } from '../../constant/permission';
import { PolicyClass } from '../../support/access-control/PoliciesClass';
import { RolesClass } from '../../support/access-control/RolesClass';
import { ClassificationClass } from '../../support/tag/ClassificationClass';
import { TagClass } from '../../support/tag/TagClass';
import { TeamClass } from '../../support/team/TeamClass';
import { UserClass } from '../../support/user/UserClass';
import { performAdminLogin } from '../../utils/admin';
import { redirectToHomePage } from '../../utils/common';
import { getApiContext, redirectToHomePage, uuid } from '../../utils/common';
import {
addAssetsToTag,
checkAssetsCount,
editTagPageDescription,
LIMITED_USER_RULES,
removeAssetsFromTag,
setupAssetsForTag,
verifyCertificationTagPageUI,
verifyTagPageUI,
} from '../../utils/tag';
const adminUser = new UserClass();
const dataConsumerUser = new UserClass();
const dataStewardUser = new UserClass();
const policy = new PolicyClass();
const role = new RolesClass();
const limitedAccessUser = new UserClass();
const classification = new ClassificationClass({
provider: 'system',
mutuallyExclusive: true,
@ -45,6 +46,7 @@ const test = base.extend<{
adminPage: Page;
dataConsumerPage: Page;
dataStewardPage: Page;
limitedAccessPage: Page;
}>({
adminPage: async ({ browser }, use) => {
const adminPage = await browser.newPage();
@ -64,6 +66,12 @@ const test = base.extend<{
await use(page);
await page.close();
},
limitedAccessPage: async ({ browser }, use) => {
const page = await browser.newPage();
await limitedAccessUser.login(page);
await use(page);
await page.close();
},
});
base.beforeAll('Setup pre-requests', async ({ browser }) => {
@ -73,8 +81,7 @@ base.beforeAll('Setup pre-requests', async ({ browser }) => {
await dataConsumerUser.create(apiContext);
await dataStewardUser.create(apiContext);
await dataStewardUser.setDataStewardRole(apiContext);
await policy.create(apiContext, DATA_STEWARD_RULES);
await role.create(apiContext, [policy.responseData.name]);
await limitedAccessUser.create(apiContext);
await classification.create(apiContext);
await tag.create(apiContext);
await afterAction();
@ -85,8 +92,7 @@ base.afterAll('Cleanup', async ({ browser }) => {
await adminUser.delete(apiContext);
await dataConsumerUser.delete(apiContext);
await dataStewardUser.delete(apiContext);
await policy.delete(apiContext);
await role.delete(apiContext);
await limitedAccessUser.delete(apiContext);
await classification.delete(apiContext);
await tag.delete(apiContext);
await afterAction();
@ -99,6 +105,12 @@ test.describe('Tag Page with Admin Roles', () => {
await verifyTagPageUI(adminPage, classification.data.name, tag);
});
test('Certification Page should not have Asset button', async ({
adminPage,
}) => {
await verifyCertificationTagPageUI(adminPage);
});
test('Rename Tag name', async ({ adminPage }) => {
await redirectToHomePage(adminPage);
const res = adminPage.waitForResponse(`/api/v1/tags/name/*`);
@ -202,22 +214,15 @@ test.describe('Tag Page with Admin Roles', () => {
test('Add and Remove Assets', async ({ adminPage }) => {
await redirectToHomePage(adminPage);
const { assets } = await setupAssetsForTag(adminPage);
const { assets, assetCleanup } = await setupAssetsForTag(adminPage);
await test.step('Add Asset', async () => {
const res = adminPage.waitForResponse(`/api/v1/tags/name/*`);
await tag.visitPage(adminPage);
await res;
await addAssetsToTag(adminPage, assets);
await test.step('Add Asset ', async () => {
await addAssetsToTag(adminPage, assets, tag);
});
await test.step('Delete Asset', async () => {
const res = adminPage.waitForResponse(`/api/v1/tags/name/*`);
await tag.visitPage(adminPage);
await res;
await removeAssetsFromTag(adminPage, assets);
await checkAssetsCount(adminPage, 0);
await removeAssetsFromTag(adminPage, assets, tag);
await assetCleanup();
});
});
});
@ -234,11 +239,34 @@ test.describe('Tag Page with Data Consumer Roles', () => {
);
});
test('Edit Tag Description or Data Consumer', async ({
test('Certification Page should not have Asset button for Data Consumer', async ({
dataConsumerPage,
}) => {
await verifyCertificationTagPageUI(dataConsumerPage);
});
test('Edit Tag Description for Data Consumer', async ({
dataConsumerPage,
}) => {
await editTagPageDescription(dataConsumerPage, tag);
});
test('Add and Remove Assets for Data Consumer', async ({
adminPage,
dataConsumerPage,
}) => {
const { assets, assetCleanup } = await setupAssetsForTag(adminPage);
await redirectToHomePage(dataConsumerPage);
await test.step('Add Asset ', async () => {
await addAssetsToTag(dataConsumerPage, assets, tag);
});
await test.step('Delete Asset', async () => {
await removeAssetsFromTag(dataConsumerPage, assets, tag);
await assetCleanup();
});
});
});
test.describe('Tag Page with Data Steward Roles', () => {
@ -248,7 +276,82 @@ test.describe('Tag Page with Data Steward Roles', () => {
await verifyTagPageUI(dataStewardPage, classification.data.name, tag, true);
});
test('Certification Page should not have Asset button for Data Steward', async ({
dataStewardPage,
}) => {
await verifyCertificationTagPageUI(dataStewardPage);
});
test('Edit Tag Description for Data Steward', async ({ dataStewardPage }) => {
await editTagPageDescription(dataStewardPage, tag);
});
test('Add and Remove Assets for Data Steward', async ({
adminPage,
dataStewardPage,
}) => {
const { assets, assetCleanup } = await setupAssetsForTag(adminPage);
await redirectToHomePage(dataStewardPage);
await test.step('Add Asset ', async () => {
await addAssetsToTag(dataStewardPage, assets, tag);
});
await test.step('Delete Asset', async () => {
await removeAssetsFromTag(dataStewardPage, assets, tag);
await assetCleanup();
});
});
});
test.describe('Tag Page with Limited EditTag Permission', () => {
test.slow(true);
test('Add and Remove Assets and Check Restricted Entity', async ({
adminPage,
limitedAccessPage,
}) => {
const { apiContext, afterAction } = await getApiContext(adminPage);
const { assets, otherAsset, assetCleanup } = await setupAssetsForTag(
adminPage
);
const id = uuid();
const policy = new PolicyClass();
const role = new RolesClass();
let limitedAccessTeam: TeamClass | null = null;
try {
await policy.create(apiContext, LIMITED_USER_RULES);
await role.create(apiContext, [policy.responseData.name]);
limitedAccessTeam = new TeamClass({
name: `PW%limited_user_access_team-${id}`,
displayName: `PW Limited User Access Team ${id}`,
description: 'playwright data steward team description',
teamType: 'Group',
users: [limitedAccessUser.responseData.id],
defaultRoles: role.responseData.id ? [role.responseData.id] : [],
});
await limitedAccessTeam.create(apiContext);
await redirectToHomePage(limitedAccessPage);
await test.step('Add Asset ', async () => {
await addAssetsToTag(limitedAccessPage, assets, tag, otherAsset);
});
await test.step('Delete Asset', async () => {
await removeAssetsFromTag(limitedAccessPage, assets, tag);
});
} finally {
await tag.delete(apiContext);
await policy.delete(apiContext);
await role.delete(apiContext);
if (limitedAccessTeam) {
await limitedAccessTeam.delete(apiContext);
}
await assetCleanup();
await afterAction();
}
});
});

View File

@ -11,10 +11,13 @@
* limitations under the License.
*/
import { expect, Page } from '@playwright/test';
import { get } from 'lodash';
import { get, isUndefined } from 'lodash';
import { SidebarItem } from '../constant/sidebar';
import { PolicyRulesType } from '../support/access-control/PoliciesClass';
import { DashboardClass } from '../support/entity/DashboardClass';
import { EntityClass } from '../support/entity/EntityClass';
import { MlModelClass } from '../support/entity/MlModelClass';
import { PipelineClass } from '../support/entity/PipelineClass';
import { TableClass } from '../support/entity/TableClass';
import { TopicClass } from '../support/entity/TopicClass';
import { TagClass } from '../support/tag/TagClass';
@ -51,20 +54,54 @@ export const visitClassificationPage = async (
await expect(page.locator('.activeCategory')).toContainText(
classificationName
);
await page.waitForSelector('[data-testid="loader"]', { state: 'detached' });
};
export const addAssetsToTag = async (page: Page, assets: EntityClass[]) => {
// Other asset type that should not get from the search in explore, they are not added to the tag
export const addAssetsToTag = async (
page: Page,
assets: EntityClass[],
tag: TagClass,
otherAsset?: EntityClass[]
) => {
const res = page.waitForResponse(`/api/v1/tags/name/*`);
await tag.visitPage(page);
await res;
await page.getByTestId('assets').click();
const initialFetchResponse = page.waitForResponse(
'/api/v1/search/query?q=&index=all&from=0&size=25&deleted=false**'
);
await page.getByTestId('data-classification-add-button').click();
await initialFetchResponse;
await expect(page.getByRole('dialog')).toBeVisible();
if (!isUndefined(otherAsset)) {
for (const asset of otherAsset) {
const name = get(asset, 'entityResponseData.name');
const searchRes = page.waitForResponse(
`/api/v1/search/query?q=${name}&index=all&from=0&size=25&**`
);
await page
.getByTestId('asset-selection-modal')
.getByTestId('searchbar')
.fill(name);
await searchRes;
await expect(page.getByText(name)).not.toBeVisible();
}
}
for (const asset of assets) {
const name = get(asset, 'entityResponseData.name');
const fqn = get(asset, 'entityResponseData.fullyQualifiedName');
const searchRes = page.waitForResponse(
`/api/v1/search/query?q=${name}&index=all&from=0&size=25&*`
`/api/v1/search/query?q=${name}&index=all&from=0&size=25&**`
);
await page
.getByTestId('asset-selection-modal')
@ -82,8 +119,13 @@ export const addAssetsToTag = async (page: Page, assets: EntityClass[]) => {
export const removeAssetsFromTag = async (
page: Page,
assets: EntityClass[]
assets: EntityClass[],
tag: TagClass
) => {
const res = page.waitForResponse(`/api/v1/tags/name/*`);
await tag.visitPage(page);
await res;
await page.getByTestId('assets').click();
for (const asset of assets) {
const fqn = get(asset, 'entityResponseData.fullyQualifiedName');
@ -94,6 +136,8 @@ export const removeAssetsFromTag = async (
await page.getByTestId('delete-all-button').click();
await assetsRemoveRes;
await checkAssetsCount(page, 0);
};
export const checkAssetsCount = async (page: Page, count: number) => {
@ -107,10 +151,14 @@ export const setupAssetsForTag = async (page: Page) => {
const table = new TableClass();
const topic = new TopicClass();
const dashboard = new DashboardClass();
const mlModel = new MlModelClass();
const pipeline = new PipelineClass();
await Promise.all([
table.create(apiContext),
topic.create(apiContext),
dashboard.create(apiContext),
mlModel.create(apiContext),
pipeline.create(apiContext),
]);
const assetCleanup = async () => {
@ -118,12 +166,15 @@ export const setupAssetsForTag = async (page: Page) => {
table.delete(apiContext),
topic.delete(apiContext),
dashboard.delete(apiContext),
mlModel.delete(apiContext),
pipeline.delete(apiContext),
]);
await afterAction();
};
return {
assets: [table, topic, dashboard],
otherAsset: [mlModel, pipeline],
assetCleanup,
};
};
@ -246,17 +297,13 @@ export const verifyTagPageUI = async (
);
await expect(page.getByText(tag.data.description)).toBeVisible();
await expect(
page.getByTestId('data-classification-add-button')
).toBeVisible();
if (limitedAccess) {
await expect(
page.getByTestId('data-classification-add-button')
).not.toBeVisible();
await expect(page.getByTestId('manage-button')).not.toBeVisible();
await expect(page.getByTestId('add-domain')).not.toBeVisible();
// Asset tab should show no data placeholder and not add asset button
await page.getByTestId('assets').click();
await expect(page.getByTestId('no-data-placeholder')).toBeVisible();
}
const classificationTable = page.waitForResponse(
@ -295,3 +342,88 @@ export const editTagPageDescription = async (page: Page, tag: TagClass) => {
`This is updated test description for tag ${tag.data.name}.`
);
};
export const verifyCertificationTagPageUI = async (page: Page) => {
await redirectToHomePage(page);
const res = page.waitForResponse(`/api/v1/tags/name/*`);
await visitClassificationPage(page, 'Certification');
await page.getByTestId('Gold').click();
await res;
await page.getByTestId('assets').click();
await expect(
page.getByTestId('data-classification-add-button')
).not.toBeVisible();
await expect(page.getByTestId('no-data-placeholder')).toBeVisible();
};
export const LIMITED_USER_RULES: PolicyRulesType[] = [
{
name: 'limitedUserEditTagRole',
resources: [
'apiCollection',
'apiEndpoint',
'apiService',
'app',
'appMarketPlaceDefinition',
'bot',
'chart',
'classification',
'container',
'dashboardDataModel',
'dashboardService',
'database',
'databaseSchema',
'databaseService',
'dataInsightChart',
'dataInsightCustomChart',
'dataInsightDashboard',
'dataProduct',
'document',
'domain',
'entityReportData',
'eventsubscription',
'feed',
'glossary',
'glossaryTerm',
'ingestionPipeline',
'kpi',
'messagingService',
'metadataService',
'metric',
'mlmodel',
'mlmodelService',
'page',
'persona',
'pipeline',
'pipelineService',
'policy',
'query',
'report',
'role',
'searchIndex',
'searchService',
'storageService',
'storedProcedure',
'suggestion',
'tag',
'team',
'testCase',
'testCaseResolutionStatus',
'testCaseResult',
'testConnectionDefinition',
'testDefinition',
'testSuite',
'type',
'user',
'webAnalyticEvent',
'workflow',
'workflowDefinition',
'workflowInstance',
'workflowInstanceState',
],
operations: ['EditTags'],
effect: 'deny',
},
];

View File

@ -30,7 +30,7 @@ export const getTabs = (
},
users: {
name: t('label.user-plural'),
count: currentTeam.userCount ?? 0,
count: currentTeam.users?.length ?? 0,
key: TeamsPageTab.USERS,
},
assets: {

View File

@ -94,13 +94,12 @@ export const UserTab = ({
showPagination,
} = usePaging(PAGE_SIZE_MEDIUM);
const usersList = useMemo(() => {
return users.map((item) =>
getEntityReferenceFromEntity(item, EntityType.USER)
);
}, [users]);
const isGroupType = useMemo(
() => currentTeam.teamType === TeamType.Group,
[currentTeam.teamType]
@ -379,7 +378,7 @@ export const UserTab = ({
onSearch={handleUsersSearchAction}
/>
</Col>
{!currentTeam.deleted && (
{!currentTeam.deleted && isGroupType && (
<Col>
<Space>
{users.length > 0 && permission.EditAll && (

View File

@ -31,7 +31,7 @@ export interface EsTermQuery {
}
export type EsTermsQuery = {
[property: string]: string;
[property: string]: string | string[];
};
export interface EsExistsQuery {

View File

@ -23,7 +23,7 @@ import {
import { ItemType } from 'antd/lib/menu/hooks/useItems';
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { cloneDeep } from 'lodash';
import { cloneDeep, isEmpty, startsWith } from 'lodash';
import React, {
useCallback,
useEffect,
@ -92,7 +92,8 @@ import {
getEncodedFqn,
} from '../../utils/StringsUtils';
import {
getQueryFilterToExcludeTerms,
getExcludedIndexesBasedOnEntityTypeEditTagPermission,
getQueryFilterToExcludeTermsAndEntities,
getTagAssetsQueryFilter,
} from '../../utils/TagsUtils';
import { showErrorToast, showSuccessToast } from '../../utils/ToastUtils';
@ -104,7 +105,7 @@ const TagPage = () => {
const { fqn: tagFqn } = useFqn();
const history = useHistory();
const { tab: activeTab = TagTabs.OVERVIEW } = useParams<{ tab?: string }>();
const { getEntityPermission } = usePermissionProvider();
const { permissions, getEntityPermission } = usePermissionProvider();
const [isLoading, setIsLoading] = useState(false);
const [tagItem, setTagItem] = useState<Tag>();
const [assetModalVisible, setAssetModalVisible] = useState(false);
@ -161,6 +162,23 @@ const TagPage = () => {
return { editTagsPermission: false, editDescriptionPermission: false };
}, [tagPermissions, tagItem?.deleted]);
const editEntitiesTagPermission = useMemo(
() => getExcludedIndexesBasedOnEntityTypeEditTagPermission(permissions),
[permissions]
);
const haveAssetEditPermission = useMemo(
() =>
editTagsPermission ||
!isEmpty(editEntitiesTagPermission.entitiesHavingPermission),
[editTagsPermission, editEntitiesTagPermission.entitiesHavingPermission]
);
const isCertificationClassification = useMemo(
() => startsWith(tagFqn, 'Certification.'),
[tagFqn]
);
const fetchCurrentTagPermission = async () => {
if (!tagItem?.id) {
return;
@ -477,7 +495,16 @@ const TagPage = () => {
assetCount={assetCount}
entityFqn={tagItem?.fullyQualifiedName ?? ''}
isSummaryPanelOpen={Boolean(previewAsset)}
permissions={tagPermissions}
permissions={
{
Create:
haveAssetEditPermission &&
!isCertificationClassification,
EditAll:
haveAssetEditPermission &&
!isCertificationClassification,
} as OperationPermission
}
ref={assetTabRef}
type={AssetsOfEntity.TAG}
onAddAsset={() => setAssetModalVisible(true)}
@ -591,17 +618,19 @@ const TagPage = () => {
titleColor={tagItem.style?.color ?? BLACK_COLOR}
/>
</Col>
{editTagsPermission && (
{haveAssetEditPermission && (
<Col className="p-x-md">
<div className="d-flex self-end">
<Button
data-testid="data-classification-add-button"
type="primary"
onClick={() => setAssetModalVisible(true)}>
{t('label.add-entity', {
entity: t('label.asset-plural'),
})}
</Button>
{!isCertificationClassification && (
<Button
data-testid="data-classification-add-button"
type="primary"
onClick={() => setAssetModalVisible(true)}>
{t('label.add-entity', {
entity: t('label.asset-plural'),
})}
</Button>
)}
{manageButtonContent.length > 0 && (
<Dropdown
align={{ targetOffset: [-12, 0] }}
@ -687,7 +716,10 @@ const TagPage = () => {
<AssetSelectionModal
entityFqn={tagItem.fullyQualifiedName}
open={assetModalVisible}
queryFilter={getQueryFilterToExcludeTerms(tagItem.fullyQualifiedName)}
queryFilter={getQueryFilterToExcludeTermsAndEntities(
tagItem.fullyQualifiedName,
editEntitiesTagPermission.entitiesNotHavingPermission
)}
type={AssetsOfEntity.TAG}
onCancel={() => setAssetModalVisible(false)}
onSave={handleAssetSave}

View File

@ -52,6 +52,31 @@ export const checkPermission = (
return hasPermission;
};
/**
*
* @param operation operation like Edit, Delete
* @param resourceType Resource type like "bot", "table"
* @param permissions UIPermission
* @param checkEditAllPermission boolean to check EditALL permission as well
* @returns boolean - true/false
*/
export const checkPermissionEntityResource = (
operation: Operation,
resourceType: ResourceEntity,
permissions: UIPermission,
checkEditAllPermission = false
) => {
const entityResource = permissions?.[resourceType];
let hasPermission = entityResource && entityResource[operation];
if (checkEditAllPermission) {
hasPermission =
hasPermission || (entityResource && entityResource[Operation.EditAll]);
}
return hasPermission;
};
/**
*
* @param permission ResourcePermission

View File

@ -24,6 +24,10 @@ import Loader from '../components/common/Loader/Loader';
import RichTextEditorPreviewer from '../components/common/RichTextEditor/RichTextEditorPreviewer';
import { FQN_SEPARATOR_CHAR } from '../constants/char.constants';
import { getExplorePath } from '../constants/constants';
import {
ResourceEntity,
UIPermission,
} from '../context/PermissionProvider/PermissionProvider.interface';
import { SettledStatus } from '../enums/Axios.enum';
import { EntityType } from '../enums/entity.enum';
import { ExplorePageTabs } from '../enums/Explore.enum';
@ -32,6 +36,7 @@ import { Classification } from '../generated/entity/classification/classificatio
import { Tag } from '../generated/entity/classification/tag';
import { GlossaryTerm } from '../generated/entity/data/glossaryTerm';
import { Column } from '../generated/entity/data/table';
import { Operation } from '../generated/entity/policies/policy';
import { Paging } from '../generated/type/paging';
import { LabelType, State, TagLabel } from '../generated/type/tagLabel';
import { searchQuery } from '../rest/searchAPI';
@ -41,6 +46,7 @@ import {
getTags,
} from '../rest/tagAPI';
import { getQueryFilterToIncludeApprovedTerm } from './GlossaryUtils';
import { checkPermissionEntityResource } from './PermissionsUtils';
import { getTagsWithoutTier } from './TableUtils';
export const getClassifications = async (
@ -318,7 +324,10 @@ export const createTagObject = (tags: EntityTags[]) => {
);
};
export const getQueryFilterToExcludeTerms = (fqn: string) => ({
export const getQueryFilterToExcludeTermsAndEntities = (
fqn: string,
excludeEntityIndex: string[] = []
) => ({
query: {
bool: {
must: [
@ -337,13 +346,17 @@ export const getQueryFilterToExcludeTerms = (fqn: string) => ({
bool: {
must_not: [
{
term: {
entityType: EntityType.TAG,
},
},
{
term: {
entityType: EntityType.DATA_PRODUCT,
terms: {
entityType: [
EntityType.CLASSIFICATION,
EntityType.TEST_SUITE,
EntityType.TEST_CASE,
EntityType.TEST_CASE_RESOLUTION_STATUS,
EntityType.TEST_CASE_RESULT,
EntityType.TAG,
EntityType.DATA_PRODUCT,
...excludeEntityIndex,
],
},
},
],
@ -354,6 +367,185 @@ export const getQueryFilterToExcludeTerms = (fqn: string) => ({
},
});
export const getExcludedIndexesBasedOnEntityTypeEditTagPermission = (
permissions: UIPermission
) => {
const entityPermission = {
[EntityType.TABLE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.TABLE,
permissions,
true
),
[EntityType.TOPIC]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.TOPIC,
permissions,
true
),
[EntityType.DASHBOARD]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.DASHBOARD,
permissions,
true
),
[EntityType.MLMODEL]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.ML_MODEL,
permissions,
true
),
[EntityType.PIPELINE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.PIPELINE,
permissions,
true
),
[EntityType.CONTAINER]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.CONTAINER,
permissions,
true
),
[EntityType.SEARCH_INDEX]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.SEARCH_INDEX,
permissions,
true
),
[EntityType.API_SERVICE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.API_SERVICE,
permissions,
true
),
[EntityType.API_ENDPOINT]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.API_ENDPOINT,
permissions,
true
),
[EntityType.API_COLLECTION]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.API_COLLECTION,
permissions,
true
),
[EntityType.DASHBOARD_DATA_MODEL]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.DASHBOARD_DATA_MODEL,
permissions,
true
),
[EntityType.STORED_PROCEDURE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.STORED_PROCEDURE,
permissions,
true
),
[EntityType.DATABASE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.DATABASE,
permissions,
true
),
[EntityType.DATABASE_SERVICE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.DATABASE_SERVICE,
permissions,
true
),
[EntityType.DATABASE_SCHEMA]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.DATABASE_SCHEMA,
permissions,
true
),
[EntityType.MESSAGING_SERVICE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.PIPELINE_SERVICE,
permissions,
true
),
[EntityType.DASHBOARD_SERVICE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.DASHBOARD_SERVICE,
permissions,
true
),
[EntityType.MLMODEL_SERVICE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.ML_MODEL_SERVICE,
permissions,
true
),
[EntityType.PIPELINE_SERVICE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.PIPELINE_SERVICE,
permissions,
true
),
[EntityType.STORAGE_SERVICE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.STORAGE_SERVICE,
permissions,
true
),
[EntityType.SEARCH_SERVICE]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.SEARCH_SERVICE,
permissions,
true
),
[EntityType.GLOSSARY]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.GLOSSARY,
permissions,
true
),
[EntityType.GLOSSARY_TERM]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.GLOSSARY_TERM,
permissions,
true
),
[EntityType.DOMAIN]: checkPermissionEntityResource(
Operation.EditTags,
ResourceEntity.DOMAIN,
permissions,
true
),
};
return (Object.keys(entityPermission) as EntityType[]).reduce(
(
acc: {
entitiesHavingPermission: EntityType[];
entitiesNotHavingPermission: EntityType[];
},
cv: EntityType
) => {
const currentEntityPermission =
entityPermission[cv as keyof typeof entityPermission];
if (currentEntityPermission) {
return {
...acc,
entitiesHavingPermission: [...acc.entitiesHavingPermission, cv],
};
}
return {
...acc,
entitiesNotHavingPermission: [...acc.entitiesNotHavingPermission, cv],
};
},
{
entitiesHavingPermission: [],
entitiesNotHavingPermission: [],
}
);
};
export const getTagAssetsQueryFilter = (fqn: string) => {
if (fqn.includes('Tier.')) {
return `(tier.tagFQN:"${fqn}")`;