mirror of
https://github.com/open-metadata/OpenMetadata.git
synced 2025-10-27 00:31:42 +00:00
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:
parent
9a21e77e15
commit
7877d5c14c
@ -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(
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -31,7 +31,7 @@ export interface EsTermQuery {
|
||||
}
|
||||
|
||||
export type EsTermsQuery = {
|
||||
[property: string]: string;
|
||||
[property: string]: string | string[];
|
||||
};
|
||||
|
||||
export interface EsExistsQuery {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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}")`;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user