diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 393b2f40682..4b6c22c3aff 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -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); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts index b5e4a11495d..ffe0f79c026 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/glossary.ts @@ -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, + }; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx index 0ad92cc57a0..7915e18cc05 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Glossary/GlossaryV1.component.tsx @@ -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 ( +
+ +
+ ); + } + + return ( + + ); + }, [ + glossaryPermission.ViewAll, + glossaryPermission.ViewBasic, + isTabExpanded, + isVersionsView, + onGlossaryDelete, + handleGlossaryUpdate, + updateVote, + ]); + return ( <> {(isLoading || isPermissionLoading) && } @@ -349,15 +406,7 @@ const GlossaryV1 = ({ !isPermissionLoading && !isEmpty(selectedData) && (isGlossaryActive ? ( - + glossaryContent ) : (