fix glossary permission error (#21548)

* fix glossary permission error

* add permission tests

* fix sonar lint
This commit is contained in:
Karan Hotchandani 2025-06-04 20:56:00 +05:30 committed by GitHub
parent 8540884ab1
commit f0f1debb6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 200 additions and 20 deletions

View File

@ -63,6 +63,7 @@ import {
selectActiveGlossary,
selectActiveGlossaryTerm,
selectColumns,
setupGlossaryDenyPermissionTest,
toggleBulkActionColumnsSelection,
updateGlossaryReviewer,
updateGlossaryTermDataFromTree,
@ -1434,6 +1435,38 @@ test.describe('Glossary tests', () => {
}
});
test('Verify Glossary Deny Permission', async ({ browser }) => {
const { afterAction, apiContext } = await performAdminLogin(browser);
const { dataConsumerUser, glossary1, cleanup } =
await setupGlossaryDenyPermissionTest(apiContext);
const { page: dataConsumerPage, afterAction: consumerAfterAction } =
await performUserLogin(browser, dataConsumerUser);
await redirectToHomePage(dataConsumerPage);
await sidebarClick(dataConsumerPage, SidebarItem.GLOSSARY);
await selectActiveGlossary(
dataConsumerPage,
glossary1.data.displayName,
false
);
await expect(
dataConsumerPage.getByTestId('permission-error-placeholder')
).toBeVisible();
await expect(
dataConsumerPage.getByTestId('permission-error-placeholder')
).toHaveText(
"You don't have necessary permissions. Please check with the admin to get the View Glossary permission."
);
await consumerAfterAction();
await cleanup(apiContext);
await afterAction();
});
test.afterAll(async ({ browser }) => {
const { afterAction, apiContext } = await performAdminLogin(browser);
await user1.delete(apiContext);

View File

@ -14,6 +14,8 @@ import { expect, Page } from '@playwright/test';
import { get, isUndefined } from 'lodash';
import { SidebarItem } from '../constant/sidebar';
import { GLOSSARY_TERM_PATCH_PAYLOAD } from '../constant/version';
import { PolicyClass } from '../support/access-control/PoliciesClass';
import { RolesClass } from '../support/access-control/RolesClass';
import { DashboardClass } from '../support/entity/DashboardClass';
import { EntityTypeEndpoint } from '../support/entity/Entity.interface';
import { TableClass } from '../support/entity/TableClass';
@ -25,6 +27,9 @@ import {
UserTeamRef,
} from '../support/glossary/Glossary.interface';
import { GlossaryTerm } from '../support/glossary/GlossaryTerm';
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 {
clickOutside,
@ -36,6 +41,7 @@ import {
NAME_VALIDATION_ERROR,
redirectToHomePage,
toastNotification,
uuid,
} from './common';
import { addMultiOwner } from './entity';
import { sidebarClick } from './sidebar';
@ -55,16 +61,21 @@ export const checkName = async (page: Page, name: string) => {
export const selectActiveGlossary = async (
page: Page,
glossaryName: string
glossaryName: string,
bWaitForResponse = true
) => {
const menuItem = page.getByRole('menuitem', { name: glossaryName });
const isSelected = await menuItem.evaluate((element) => {
return element.classList.contains('ant-menu-item-selected');
});
if (!isSelected) {
const glossaryResponse = page.waitForResponse('/api/v1/glossaryTerms*');
await menuItem.click();
await glossaryResponse;
if (bWaitForResponse) {
const glossaryResponse = page.waitForResponse('/api/v1/glossaryTerms*');
await menuItem.click();
await glossaryResponse;
} else {
await menuItem.click();
}
} else {
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
@ -1478,3 +1489,90 @@ export const checkGlossaryTermDetails = async (
)
).toContainText(reviewer.responseData.displayName);
};
export const setupGlossaryDenyPermissionTest = async (apiContext: any) => {
// Create all necessary resources
const dataConsumerUser = new UserClass();
const id = uuid();
const glossary1 = new Glossary();
const glossaryTerm1 = new GlossaryTerm(glossary1);
await glossary1.create(apiContext);
await glossaryTerm1.create(apiContext);
const classification = new ClassificationClass({
provider: 'system',
mutuallyExclusive: true,
});
const tag = new TagClass({
classification: classification.data.name,
});
await dataConsumerUser.create(apiContext);
await classification.create(apiContext);
await tag.create(apiContext);
// Setup permissions
const dataConsumerPolicy = new PolicyClass();
const dataConsumerRole = new RolesClass();
// Create domain access policy
const matchTagRule = [
{
name: 'Hidden from Non Admins',
description: '',
resources: ['All'],
operations: ['All'],
effect: 'deny',
condition: `matchAllTags('${tag.responseData.fullyQualifiedName}')`,
},
];
await dataConsumerPolicy.create(apiContext, matchTagRule);
await dataConsumerRole.create(apiContext, [
dataConsumerPolicy.responseData.name,
]);
// Create team for the user
const dataConsumerTeam = new TeamClass({
name: `PW_data_consumer_team-${id}`,
displayName: `PW Data Consumer Team ${id}`,
description: 'playwright data consumer team description',
teamType: 'Group',
users: [dataConsumerUser.responseData.id ?? ''],
defaultRoles: [dataConsumerRole.responseData.id ?? ''],
});
await dataConsumerTeam.create(apiContext);
// Set domain ownership
await glossary1.patch(apiContext, [
{
op: 'add',
path: '/tags/0',
value: {
tagFQN: tag.responseData.fullyQualifiedName,
source: 'Classification',
},
},
]);
// Return cleanup function and all created resources
const cleanup = async (apiContext1: APIRequestContext) => {
await glossaryTerm1.delete(apiContext);
await glossary1.delete(apiContext);
await dataConsumerUser.delete(apiContext1);
await dataConsumerTeam.delete(apiContext1);
await dataConsumerPolicy.delete(apiContext1);
await dataConsumerRole.delete(apiContext1);
};
return {
dataConsumerUser,
glossary1,
glossaryTerm1,
dataConsumerTeam,
dataConsumerPolicy,
dataConsumerRole,
cleanup,
};
};

View File

@ -14,7 +14,7 @@
import { AxiosError } from 'axios';
import { compare } from 'fast-json-patch';
import { cloneDeep, isEmpty } from 'lodash';
import React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHistory, useParams } from 'react-router-dom';
import { withActivityFeed } from '../../components/AppRouter/withActivityFeed';
@ -23,6 +23,7 @@ import {
OperationPermission,
ResourceEntity,
} from '../../context/PermissionProvider/PermissionProvider.interface';
import { ERROR_PLACEHOLDER_TYPE, SIZE } from '../../enums/common.enum';
import { EntityAction, EntityTabs, EntityType } from '../../enums/entity.enum';
import { Glossary } from '../../generated/entity/data/glossary';
import { GlossaryTerm } from '../../generated/entity/data/glossaryTerm';
@ -40,6 +41,7 @@ import { updateGlossaryTermByFqn } from '../../utils/GlossaryUtils';
import { DEFAULT_ENTITY_PERMISSION } from '../../utils/PermissionsUtils';
import { getGlossaryTermDetailsPath } from '../../utils/RouterUtils';
import { showErrorToast } from '../../utils/ToastUtils';
import ErrorPlaceHolder from '../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import Loader from '../common/Loader/Loader';
import { GenericProvider } from '../Customization/GenericProvider/GenericProvider';
import EntityDeleteModal from '../Modals/EntityDeleteModal/EntityDeleteModal';
@ -126,8 +128,12 @@ const GlossaryV1 = ({
selectedData?.id as string
);
setGlossaryPermission(response);
return response;
} catch (error) {
showErrorToast(error as AxiosError);
throw error;
}
};
@ -138,8 +144,12 @@ const GlossaryV1 = ({
selectedData?.id as string
);
setGlossaryTermPermission(response);
return response;
} catch (error) {
showErrorToast(error as AxiosError);
throw error;
}
};
@ -298,21 +308,31 @@ const GlossaryV1 = ({
try {
if (isVersionsView) {
isGlossaryActive
? setGlossaryPermission(VERSION_VIEW_GLOSSARY_PERMISSION)
: setGlossaryTermPermission(VERSION_VIEW_GLOSSARY_PERMISSION);
const permission = VERSION_VIEW_GLOSSARY_PERMISSION;
setGlossaryPermission(permission);
setGlossaryTermPermission(permission);
return permission;
} else {
await permissionFetch();
return await permissionFetch();
}
} finally {
setIsPermissionLoading(false);
}
};
const initializeGlossary = async () => {
const permission = await initPermissions();
if (permission?.ViewAll || permission?.ViewBasic) {
loadGlossaryTerms();
} else {
setIsLoading(false);
}
};
useEffect(() => {
if (id && !action) {
loadGlossaryTerms();
initPermissions();
initializeGlossary();
}
}, [id, isGlossaryActive, isVersionsView, action]);
@ -330,6 +350,43 @@ const GlossaryV1 = ({
setIsTabExpanded(!isTabExpanded);
};
const glossaryContent = useMemo(() => {
if (!(glossaryPermission.ViewAll || glossaryPermission.ViewBasic)) {
return (
<div className="d-flex justify-center items-center full-height">
<ErrorPlaceHolder
className="mt-0-important border-none"
permissionValue={t('label.view-entity', {
entity: t('label.glossary'),
})}
size={SIZE.X_LARGE}
type={ERROR_PLACEHOLDER_TYPE.PERMISSION}
/>
</div>
);
}
return (
<GlossaryDetails
handleGlossaryDelete={onGlossaryDelete}
isTabExpanded={isTabExpanded}
isVersionView={isVersionsView}
permissions={glossaryPermission}
toggleTabExpanded={toggleTabExpanded}
updateGlossary={handleGlossaryUpdate}
updateVote={updateVote}
/>
);
}, [
glossaryPermission.ViewAll,
glossaryPermission.ViewBasic,
isTabExpanded,
isVersionsView,
onGlossaryDelete,
handleGlossaryUpdate,
updateVote,
]);
return (
<>
{(isLoading || isPermissionLoading) && <Loader />}
@ -349,15 +406,7 @@ const GlossaryV1 = ({
!isPermissionLoading &&
!isEmpty(selectedData) &&
(isGlossaryActive ? (
<GlossaryDetails
handleGlossaryDelete={onGlossaryDelete}
isTabExpanded={isTabExpanded}
isVersionView={isVersionsView}
permissions={glossaryPermission}
toggleTabExpanded={toggleTabExpanded}
updateGlossary={handleGlossaryUpdate}
updateVote={updateVote}
/>
glossaryContent
) : (
<GlossaryTermsV1
glossaryTerm={selectedData as GlossaryTerm}