diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/DomainPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/DomainPermissions.spec.ts new file mode 100644 index 00000000000..3d6ad6c7c9c --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/DomainPermissions.spec.ts @@ -0,0 +1,228 @@ +/* + * Copyright 2024 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 { expect, Page, test as base } from '@playwright/test'; +import { SidebarItem } from '../../../constant/sidebar'; +import { Domain } from '../../../support/domain/Domain'; +import { EntityDataClass } from '../../../support/entity/EntityDataClass'; +import { UserClass } from '../../../support/user/UserClass'; +import { performAdminLogin } from '../../../utils/admin'; +import { redirectToHomePage, uuid } from '../../../utils/common'; +import { addCustomPropertiesForEntity } from '../../../utils/customProperty'; +import { + assignRoleToUser, + initializePermissions, +} from '../../../utils/permission'; +import { + settingClick, + SettingOptionsType, + sidebarClick, +} from '../../../utils/sidebar'; + +const adminUser = new UserClass(); +const testUser = new UserClass(); + +const test = base.extend<{ + page: Page; + testUserPage: Page; +}>({ + page: async ({ browser }, use) => { + const adminPage = await browser.newPage(); + await adminUser.login(adminPage); + await use(adminPage); + await adminPage.close(); + }, + testUserPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await testUser.login(page); + await use(page); + await page.close(); + }, +}); + +test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await adminUser.create(apiContext); + await adminUser.setAdminRole(apiContext); + await testUser.create(apiContext); + await afterAction(); +}); + +const domain = new Domain(); +const customPropertyName = `pwDomainCustomProperty${uuid()}`; + +test.beforeAll('Setup domain and custom property', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await EntityDataClass.preRequisitesForTests(apiContext); + await domain.create(apiContext); + + // Create custom property for domain once + const page = await browser.newPage(); + await adminUser.login(page); + await settingClick(page, 'domains' as SettingOptionsType, true); + await addCustomPropertiesForEntity({ + page, + propertyName: customPropertyName, + customPropertyData: { description: 'Test domain custom property' }, + customType: 'String', + }); + await page.close(); + + await afterAction(); +}); + +test('Domain allow operations', async ({ testUserPage, browser }) => { + test.slow(true); + + // Setup allow permissions + const page = await browser.newPage(); + await adminUser.login(page); + await initializePermissions(page, 'allow', [ + 'EditDescription', + 'EditOwners', + 'EditTags', + 'Delete', + 'EditDisplayName', + 'Create', + 'Delete', + 'EditCustomFields', + ]); + await assignRoleToUser(page, testUser); + await page.close(); + + // Navigate to domain page + await redirectToHomePage(testUserPage); + await sidebarClick(testUserPage, SidebarItem.DOMAIN); + await domain.visitEntityPage(testUserPage); + + // Test that domain operation elements are visible + const directElements = [ + 'edit-description', + 'add-owner', + 'add-tag', + 'edit-icon-right-panel', + 'add-domain', + ]; + + const manageButtonElements = ['delete-button', 'rename-button']; + + await testUserPage.waitForLoadState('networkidle'); + + // Test direct elements first + for (const testId of directElements) { + let element; + if (testId === 'add-tag') { + // For add-tag, target the button within tags-container + element = testUserPage + .getByTestId('tags-container') + .getByTestId('add-tag'); + } else { + element = testUserPage.getByTestId(testId).first(); + } + + await expect(element).toBeVisible(); + } + + // Click manage button once and test elements inside it + const manageButton = testUserPage.getByTestId('manage-button'); + + if (await manageButton.isVisible()) { + await manageButton.click(); + + for (const testId of manageButtonElements) { + const element = testUserPage.getByTestId(testId); + + await expect(element).toBeVisible(); + } + } +}); + +test('Domain deny operations', async ({ testUserPage, browser }) => { + test.slow(true); + + // Setup deny permissions + const page = await browser.newPage(); + await adminUser.login(page); + await initializePermissions(page, 'deny', [ + 'EditDescription', + 'EditOwners', + 'EditTags', + 'Delete', + 'EditDisplayName', + 'Create', + 'Delete', + 'EditCustomFields', + ]); + await assignRoleToUser(page, testUser); + await page.close(); + + // Navigate to domain page + await redirectToHomePage(testUserPage); + await sidebarClick(testUserPage, SidebarItem.DOMAIN); + await domain.visitEntityPage(testUserPage); + + // Test that domain operation elements are visible + const directElements = [ + 'edit-description', + 'add-owner', + 'add-tag', + 'edit-icon-right-panel', + 'add-domain', + ]; + + const manageButtonElements = ['delete-button', 'rename-button']; + + await testUserPage.waitForLoadState('networkidle'); + + for (const testId of directElements) { + let element; + if (testId === 'add-tag') { + // For add-tag, target the button within tags-container + element = testUserPage + .getByTestId('tags-container') + .getByTestId('add-tag'); + } else { + element = testUserPage.getByTestId(testId).first(); + } + + await expect(element).not.toBeVisible(); + } + + // Click manage button once and test elements inside it + const manageButton = testUserPage.getByTestId('manage-button'); + + if (await manageButton.isVisible()) { + await manageButton.click(); + + for (const testId of manageButtonElements) { + const element = testUserPage.getByTestId(testId); + + await expect(element).not.toBeVisible(); + } + } +}); + +test.afterAll('Cleanup domain', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await domain.delete(apiContext); + await EntityDataClass.postRequisitesForTests(apiContext); + await afterAction(); +}); + +test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await adminUser.delete(apiContext); + await testUser.delete(apiContext); + + await afterAction(); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/EntityPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/EntityPermissions.spec.ts new file mode 100644 index 00000000000..d29539d6235 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/EntityPermissions.spec.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2024 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 { Page, test as base } from '@playwright/test'; +import { EntityClass } from '../../../support/entity/EntityClass'; +import { EntityDataClass } from '../../../support/entity/EntityDataClass'; +import { UserClass } from '../../../support/user/UserClass'; +import { performAdminLogin } from '../../../utils/admin'; +import { getApiContext, uuid } from '../../../utils/common'; +import { + ALL_OPERATIONS, + createCustomPropertyForEntity, + entityConfig, + runCommonPermissionTests, + runEntitySpecificPermissionTests, +} from '../../../utils/entityPermissionUtils'; +import { + assignRoleToUser, + cleanupPermissions, + initializePermissions, +} from '../../../utils/permission'; + +const adminUser = new UserClass(); +const testUser = new UserClass(); + +const test = base.extend<{ + page: Page; + testUserPage: Page; +}>({ + page: async ({ browser }, use) => { + const adminPage = await browser.newPage(); + await adminUser.login(adminPage); + await use(adminPage); + }, + testUserPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await testUser.login(page); + await use(page); + }, +}); + +test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await adminUser.create(apiContext); + await adminUser.setAdminRole(apiContext); + await testUser.create(apiContext); + await afterAction(); +}); + +Object.entries(entityConfig).forEach(([, config]) => { + const entity = new config.class(); + const entityType = entity.getType(); + + test.describe(`${entityType} Permissions`, () => { + const customPropertyName = `pw${entityType}CustomProperty${uuid()}`; + + test.beforeAll('Setup entity', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await EntityDataClass.preRequisitesForTests(apiContext); + await entity.create(apiContext); + + // Create custom property for this entity type + await createCustomPropertyForEntity( + browser, + entityType, + customPropertyName, + adminUser + ); + + await afterAction(); + }); + + // Allow permissions tests + test.describe('Allow permissions', () => { + test.beforeAll('Initialize allow permissions', async ({ browser }) => { + const page = await browser.newPage(); + await adminUser.login(page); + await initializePermissions(page, 'allow', ALL_OPERATIONS); + await assignRoleToUser(page, testUser); + }); + + test(`${entityType} allow common operations permissions`, async ({ + testUserPage, + }) => { + test.slow(true); + + await runCommonPermissionTests(testUserPage, entity, 'allow'); + }); + + // Entity-specific tests + if ('specificTest' in config && config.specificTest) { + test(`${entityType} allow entity-specific permission operations`, async ({ + testUserPage, + }) => { + test.slow(true); + + await runEntitySpecificPermissionTests( + testUserPage, + entity, + 'allow', + config.specificTest as ( + page: Page, + entity: EntityClass, + effect: 'allow' | 'deny' + ) => Promise + ); + }); + } + + test.afterAll('Cleanup allow permissions', async ({ browser }) => { + const page = await browser.newPage(); + await adminUser.login(page); + const { apiContext } = await getApiContext(page); + await cleanupPermissions(apiContext); + await page.close(); + }); + }); + + // Deny permissions tests + test.describe('Deny permissions', () => { + test.beforeAll('Initialize deny permissions', async ({ browser }) => { + const page = await browser.newPage(); + await adminUser.login(page); + await initializePermissions(page, 'deny', ALL_OPERATIONS); + await assignRoleToUser(page, testUser); + }); + + test(`${entityType} deny common operations permissions`, async ({ + testUserPage, + }) => { + test.slow(true); + + await runCommonPermissionTests(testUserPage, entity, 'deny'); + }); + + // Entity-specific tests + if ('specificTest' in config && config.specificTest) { + test(`${entityType} deny entity-specific permission operations`, async ({ + testUserPage, + }) => { + test.slow(true); + + await runEntitySpecificPermissionTests( + testUserPage, + entity, + 'deny', + config.specificTest as ( + page: Page, + entity: EntityClass, + effect: 'allow' | 'deny' + ) => Promise + ); + }); + } + + test.afterAll('Cleanup deny permissions', async ({ browser }) => { + const page = await browser.newPage(); + await adminUser.login(page); + const { apiContext } = await getApiContext(page); + await cleanupPermissions(apiContext); + await page.close(); + }); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/GlossaryPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/GlossaryPermissions.spec.ts new file mode 100644 index 00000000000..c29fbc89fc2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/GlossaryPermissions.spec.ts @@ -0,0 +1,203 @@ +/* + * Copyright 2024 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 { expect, Page, test as base } from '@playwright/test'; +import { SidebarItem } from '../../../constant/sidebar'; +import { EntityDataClass } from '../../../support/entity/EntityDataClass'; +import { Glossary } from '../../../support/glossary/Glossary'; +import { UserClass } from '../../../support/user/UserClass'; +import { performAdminLogin } from '../../../utils/admin'; +import { redirectToHomePage } from '../../../utils/common'; +import { + assignRoleToUser, + initializePermissions, +} from '../../../utils/permission'; +import { sidebarClick } from '../../../utils/sidebar'; + +const adminUser = new UserClass(); +const testUser = new UserClass(); + +const test = base.extend<{ + page: Page; + testUserPage: Page; +}>({ + page: async ({ browser }, use) => { + const adminPage = await browser.newPage(); + await adminUser.login(adminPage); + await use(adminPage); + await adminPage.close(); + }, + testUserPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await testUser.login(page); + await use(page); + await page.close(); + }, +}); + +test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await adminUser.create(apiContext); + await adminUser.setAdminRole(apiContext); + await testUser.create(apiContext); + await afterAction(); +}); + +const glossary = new Glossary(); + +test.beforeAll('Setup glossary', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await EntityDataClass.preRequisitesForTests(apiContext); + await glossary.create(apiContext); + await afterAction(); +}); + +test('Glossary allow operations', async ({ testUserPage, browser }) => { + test.slow(true); + + const page = await browser.newPage(); + await adminUser.login(page); + await initializePermissions(page, 'allow', [ + 'EditDescription', + 'EditOwners', + 'EditTags', + 'Delete', + 'EditDisplayName', + 'Create', + 'Delete', + 'EditReviewers', + ]); + await assignRoleToUser(page, testUser); + await page.close(); + + await redirectToHomePage(testUserPage); + await sidebarClick(testUserPage, SidebarItem.GLOSSARY); + await glossary.visitEntityPage(testUserPage); + + // Test that glossary operation elements are visible + const directElements = [ + 'edit-description', + 'add-owner', + 'add-tag', + 'Add', + 'add-glossary', + ]; + + const manageButtonElements = ['delete-button', 'rename-button']; + + await testUserPage.waitForLoadState('networkidle'); + + for (const testId of directElements) { + let element; + if (testId === 'add-tag') { + element = testUserPage + .getByTestId('tags-container') + .getByTestId('add-tag'); + } else { + element = testUserPage.getByTestId(testId).first(); + } + + await expect(element).toBeVisible(); + } + + const manageButton = testUserPage.getByTestId('manage-button'); + + if (await manageButton.isVisible()) { + await manageButton.click(); + + for (const testId of manageButtonElements) { + const element = testUserPage.getByTestId(testId); + + await expect(element).toBeVisible(); + } + } +}); + +test('Glossary deny operations', async ({ testUserPage, browser }) => { + test.slow(true); + + // Setup deny permissions + const page = await browser.newPage(); + await adminUser.login(page); + await initializePermissions(page, 'deny', [ + 'EditDescription', + 'EditOwners', + 'EditTags', + 'Delete', + 'EditDisplayName', + 'Create', + 'Delete', + 'EditReviewers', + ]); + await assignRoleToUser(page, testUser); + await page.close(); + + // Navigate to glossary page + await redirectToHomePage(testUserPage); + await sidebarClick(testUserPage, SidebarItem.GLOSSARY); + await glossary.visitEntityPage(testUserPage); + + // Test that glossary operation elements are visible + const directElements = [ + 'edit-description', + 'add-owner', + 'add-tag', + 'add-glossary', + ]; + + const manageButtonElements = ['delete-button', 'rename-button']; + + await testUserPage.waitForLoadState('networkidle'); + + for (const testId of directElements) { + let element; + if (testId === 'add-tag') { + // For add-tag, target the button within tags-container + element = testUserPage + .getByTestId('tags-container') + .getByTestId('add-tag'); + } else { + element = testUserPage.getByTestId(testId).first(); + } + + await expect(element).not.toBeVisible(); + } + + // Click manage button once and test elements inside it + const manageButton = testUserPage.getByTestId('manage-button'); + + if (await manageButton.isVisible()) { + await manageButton.click(); + + for (const testId of manageButtonElements) { + const element = testUserPage.getByTestId(testId); + + await expect(element).not.toBeVisible(); + } + } +}); + +test.afterAll('Cleanup glossary', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await glossary.delete(apiContext); + await EntityDataClass.postRequisitesForTests(apiContext); + await afterAction(); +}); + +test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await adminUser.delete(apiContext); + await testUser.delete(apiContext); + + await afterAction(); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/ServiceEntityPermissions.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/ServiceEntityPermissions.spec.ts new file mode 100644 index 00000000000..0160143fb15 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permissions/ServiceEntityPermissions.spec.ts @@ -0,0 +1,142 @@ +/* + * Copyright 2024 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 { Page, test as base } from '@playwright/test'; +import { EntityDataClass } from '../../../support/entity/EntityDataClass'; +import { ApiServiceClass } from '../../../support/entity/service/ApiServiceClass'; +import { DashboardServiceClass } from '../../../support/entity/service/DashboardServiceClass'; +import { DatabaseServiceClass } from '../../../support/entity/service/DatabaseServiceClass'; +import { MessagingServiceClass } from '../../../support/entity/service/MessagingServiceClass'; +import { MlmodelServiceClass } from '../../../support/entity/service/MlmodelServiceClass'; +import { PipelineServiceClass } from '../../../support/entity/service/PipelineServiceClass'; +import { SearchIndexServiceClass } from '../../../support/entity/service/SearchIndexServiceClass'; +import { StorageServiceClass } from '../../../support/entity/service/StorageServiceClass'; +import { UserClass } from '../../../support/user/UserClass'; +import { performAdminLogin } from '../../../utils/admin'; +import { getApiContext } from '../../../utils/common'; +import { + ALL_OPERATIONS, + runCommonPermissionTests, +} from '../../../utils/entityPermissionUtils'; +import { + assignRoleToUser, + cleanupPermissions, + initializePermissions, +} from '../../../utils/permission'; + +const adminUser = new UserClass(); +const testUser = new UserClass(); + +// Service entity classes +const serviceEntities = [ + ApiServiceClass, + DashboardServiceClass, + DatabaseServiceClass, + MessagingServiceClass, + MlmodelServiceClass, + PipelineServiceClass, + SearchIndexServiceClass, + StorageServiceClass, +] as const; + +const test = base.extend<{ + page: Page; + testUserPage: Page; +}>({ + page: async ({ browser }, use) => { + const adminPage = await browser.newPage(); + await adminUser.login(adminPage); + await use(adminPage); + await adminPage.close(); + }, + testUserPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await testUser.login(page); + await use(page); + await page.close(); + }, +}); + +test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await adminUser.create(apiContext); + await adminUser.setAdminRole(apiContext); + await testUser.create(apiContext); + await afterAction(); +}); + +serviceEntities.forEach((EntityClass) => { + const entity = new EntityClass(); + const entityType = entity.getType(); + + test.describe(`${entityType} Permissions`, () => { + test.beforeAll('Setup entity', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await EntityDataClass.preRequisitesForTests(apiContext); + await entity.create(apiContext); + await afterAction(); + }); + + test.describe('Allow permissions', () => { + test.beforeAll('Initialize allow permissions', async ({ browser }) => { + const page = await browser.newPage(); + await adminUser.login(page); + await initializePermissions(page, 'allow', ALL_OPERATIONS); + await assignRoleToUser(page, testUser); + await page.close(); + }); + + test(`${entityType} allow common operations permissions`, async ({ + testUserPage, + }) => { + test.slow(true); + + await runCommonPermissionTests(testUserPage, entity, 'allow'); + }); + + test.afterAll('Cleanup allow permissions', async ({ browser }) => { + const page = await browser.newPage(); + await adminUser.login(page); + const { apiContext } = await getApiContext(page); + await cleanupPermissions(apiContext); + await page.close(); + }); + }); + + test.describe('Deny permissions', () => { + test.beforeAll('Initialize deny permissions', async ({ browser }) => { + const page = await browser.newPage(); + await adminUser.login(page); + await initializePermissions(page, 'deny', ALL_OPERATIONS); + await assignRoleToUser(page, testUser); + await page.close(); + }); + + test(`${entityType} deny common operations permissions`, async ({ + testUserPage, + }) => { + test.slow(true); + + await runCommonPermissionTests(testUserPage, entity, 'deny'); + }); + + test.afterAll('Cleanup deny permissions', async ({ browser }) => { + const page = await browser.newPage(); + await adminUser.login(page); + const { apiContext } = await getApiContext(page); + await cleanupPermissions(apiContext); + await page.close(); + }); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts index 61d2cfb3304..a93d2c587ee 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Entity.spec.ts @@ -244,7 +244,7 @@ entities.forEach((EntityClass) => { ); }); - if (['Dashboard', 'Dashboard Data Model'].includes(entityName)) { + if (['Dashboard', 'DashboardDataModel'].includes(entityName)) { test(`${entityName} page should show the project name`, async ({ page, }) => { @@ -369,7 +369,7 @@ entities.forEach((EntityClass) => { }); }); - if (['Table', 'Dashboard Data Model'].includes(entity.type)) { + if (['Table', 'DashboardDataModel'].includes(entity.type)) { test('DisplayName Add, Update and Remove for child entities', async ({ page, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts index c1bd2a98a7b..806e1c64374 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataConsumer.spec.ts @@ -140,7 +140,7 @@ entities.forEach((EntityClass) => { }); }); - if (['Table', 'Dashboard Data Model'].includes(entity.type)) { + if (['Table', 'DashboardDataModel'].includes(entity.type)) { test('DisplayName edit for child entities should not be allowed', async ({ page, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataSteward.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataSteward.spec.ts index a1ecd5c8f7e..fd508f6d0be 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataSteward.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/EntityDataSteward.spec.ts @@ -173,7 +173,7 @@ entities.forEach((EntityClass) => { }); }); - if (['Table', 'Dashboard Data Model'].includes(entity.type)) { + if (['Table', 'DashboardDataModel'].includes(entity.type)) { test('DisplayName Add, Update and Remove for child entities', async ({ page, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts index d1b32af7d79..519cad91eb8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/DashboardDataModelClass.ts @@ -69,7 +69,7 @@ export class DashboardDataModelClass extends EntityClass { constructor(name?: string) { super(EntityTypeEndpoint.DataModel); this.service.name = name ?? this.service.name; - this.type = 'Dashboard Data Model'; + this.type = 'DashboardDataModel'; this.childrenTabId = 'model'; this.childrenSelectorId = this.children[0].name; this.serviceCategory = SERVICE_TYPE.Dashboard; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 18a2ee6c672..ee90c46f085 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -107,6 +107,8 @@ export const getApiContext = async (page: Page) => { return { apiContext, afterAction }; }; +const DASHBOARD_DATA_MODEL = 'DashboardDataModel'; + export const getEntityTypeSearchIndexMapping = (entityType: string) => { const entityMapping = { Table: 'table_search_index', @@ -118,7 +120,7 @@ export const getEntityTypeSearchIndexMapping = (entityType: string) => { SearchIndex: 'search_entity_search_index', ApiEndpoint: 'api_endpoint_search_index', Metric: 'metric_search_index', - 'Dashboard Data Model': 'dashboard_data_model_search_index', + [DASHBOARD_DATA_MODEL]: 'dashboard_data_model_search_index', }; return entityMapping[entityType as keyof typeof entityMapping]; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index f9bbe321276..ec9605edf3c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -1790,7 +1790,7 @@ export const checkExploreSearchFilter = async ( export const getEntityDataTypeDisplayPatch = (entity: EntityClass) => { switch (entity.getType()) { case 'Table': - case 'Dashboard Data Model': + case 'DashboardDataModel': return '/columns/0/dataTypeDisplay'; case 'ApiEndpoint': return '/requestSchema/schemaFields/0/dataTypeDisplay'; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entityPermissionUtils.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entityPermissionUtils.ts new file mode 100644 index 00000000000..59fa8913f07 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entityPermissionUtils.ts @@ -0,0 +1,594 @@ +/* + * Copyright 2024 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 { expect, Page } from '@playwright/test'; +import { ContainerClass } from '../support/entity/ContainerClass'; +import { DashboardClass } from '../support/entity/DashboardClass'; +import { DashboardDataModelClass } from '../support/entity/DashboardDataModelClass'; +import { EntityClass } from '../support/entity/EntityClass'; +import { MetricClass } from '../support/entity/MetricClass'; +import { MlModelClass } from '../support/entity/MlModelClass'; +import { PipelineClass } from '../support/entity/PipelineClass'; +import { SearchIndexClass } from '../support/entity/SearchIndexClass'; +import { TableClass } from '../support/entity/TableClass'; +import { TopicClass } from '../support/entity/TopicClass'; +import { UserClass } from '../support/user/UserClass'; +import { redirectToHomePage } from './common'; +import { addCustomPropertiesForEntity } from './customProperty'; +import { settingClick, SettingOptionsType } from './sidebar'; + +// All operations across all entities +export const ALL_OPERATIONS = [ + // Common operations + 'EditDescription', + 'EditOwners', + 'EditTier', + 'EditDisplayName', + 'EditTags', + 'EditGlossaryTerms', + 'EditCustomFields', + 'Delete', + + // Entity specific operations + 'ViewQueries', + 'ViewSampleData', + 'ViewDataProfile', + 'ViewTests', + 'ViewUsage', + 'EditQueries', + 'EditDataProfile', + 'EditSampleData', + 'EditTests', + 'EditStatus', + 'EditLineage', +]; + +// Helper function to check element visibility based on configuration +const checkElementVisibility = async ( + testUserPage: Page, + config: { + testId: string; + type: string; + containers?: string[]; + }, + effect: 'allow' | 'deny' +) => { + const { testId, type } = config; + + if (effect === 'allow') { + switch (type) { + case 'direct': { + await expect( + testUserPage.locator(`[data-testid="${testId}"]`).first() + ).toBeVisible(); + + break; + } + + case 'multiple-containers': { + // Handle elements that exist in multiple containers + const containerLocators = + config.containers?.map((container) => + testUserPage + .locator(`[data-testid="${container}"]`) + .locator(`button[data-testid="${testId}"]`) + ) || []; + + const containerVisibilityChecks = await Promise.all( + containerLocators.map((locator) => locator.isVisible()) + ); + + // In allow case: any one of the containers should have the element visible + expect( + containerVisibilityChecks.some((visible) => visible) + ).toBeTruthy(); + + break; + } + + case 'with-manage-button': { + const manageButton = testUserPage.locator( + '[data-testid="manage-button"]' + ); + if (await manageButton.isVisible()) { + await manageButton.click(); + + await expect( + testUserPage.locator(`[data-testid="${testId}"]`) + ).toBeVisible(); + } + + break; + } + case 'label': { + await expect(testUserPage.getByText(testId).first()).toBeVisible(); + + break; + } + + default: { + await expect( + testUserPage.locator(`[data-testid="${testId}"]`) + ).toBeVisible(); + } + } + } else { + // Deny effect + switch (type) { + case 'direct': { + await expect( + testUserPage.locator(`[data-testid="${testId}"]`).first() + ).not.toBeVisible(); + + break; + } + + case 'multiple-containers': { + // Handle elements that exist in multiple containers for deny case + const containerLocators = + config.containers?.map((container) => + testUserPage + .locator(`[data-testid="${container}"]`) + .locator(`button[data-testid="${testId}"]`) + ) || []; + + const containerVisibilityChecks = await Promise.all( + containerLocators.map((locator) => locator.isVisible()) + ); + + // In deny case: none of the containers should have the element visible + expect( + containerVisibilityChecks.every((visible) => !visible) + ).toBeTruthy(); + + break; + } + + case 'with-manage-button': { + const manageButton = testUserPage.locator( + '[data-testid="manage-button"]' + ); + if (await manageButton.isVisible()) { + await manageButton.click(); + + await expect( + testUserPage.locator(`[data-testid="${testId}"]`) + ).not.toBeVisible(); + } + + break; + } + case 'label': { + await expect(testUserPage.getByText(testId).first()).not.toBeVisible(); + + break; + } + + default: { + await expect( + testUserPage.locator(`[data-testid="${testId}"]`) + ).not.toBeVisible(); + } + } + } +}; + +// Test common operations for any entity +export const testCommonOperations = async ( + testUserPage: Page, + entity: EntityClass, + effect: 'allow' | 'deny' +) => { + // Navigate to entity page + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + // Define test configurations with special handling + const testIdsConfigs = [ + { testId: 'edit-description', type: 'direct' }, + { + testId: 'add-tag', + type: 'multiple-containers', + containers: ['tags-container', 'glossary-container'], + }, + { testId: 'edit-tier', type: 'direct' }, + { testId: 'edit-owner', type: 'direct' }, + { testId: 'rename-button', type: 'with-manage-button' }, + { testId: 'delete-button', type: 'with-manage-button' }, + ]; + + await expect( + testUserPage.locator('[data-testid="entity-header-title"]') + ).toBeVisible(); + + for (const config of testIdsConfigs) { + await checkElementVisibility(testUserPage, config, effect); + } + + // Check custom properties + const customPropertiesLocator = testUserPage.locator( + '[data-testid="custom_properties"]' + ); + if (await customPropertiesLocator.isVisible()) { + await customPropertiesLocator.click(); + if (effect === 'allow') { + await expect( + testUserPage + .locator('[data-testid="custom-properties-card"]') + .first() + .getByTestId('edit-icon') + .first() + ).toBeVisible(); + } else { + await expect( + testUserPage + .locator('[data-testid="custom-properties-card"]') + .first() + .getByTestId('edit-icon') + .first() + ).not.toBeVisible(); + } + } +}; + +// Helper function to test permission error visibility +export const testPermissionErrorVisibility = async ( + testUserPage: Page, + testId: string, + effect: 'allow' | 'deny', + expectedErrorMessage?: string +) => { + await testUserPage.locator(`[data-testid="${testId}"]`).click(); + + if (effect === 'deny') { + await expect( + testUserPage + .locator('[data-testid="permission-error-placeholder"]') + .getByText( + expectedErrorMessage || "You don't have necessary permissions." + ) + ).toBeVisible(); + } else { + await expect( + testUserPage + .locator('[data-testid="permission-error-placeholder"]') + .getByText( + expectedErrorMessage || "You don't have necessary permissions." + ) + ).not.toBeVisible(); + } +}; + +// Helper function to test profiler tab permissions +export const testProfilerTabPermission = async ( + testUserPage: Page, + tabName: string, + effect: 'allow' | 'deny', + expectedErrorMessage?: string +) => { + await testUserPage + .locator('[data-testid="profiler-tab-left-panel"]') + .getByText(tabName) + .click(); + + if (effect === 'deny') { + await expect( + testUserPage + .locator('[data-testid="permission-error-placeholder"]') + .getByText( + expectedErrorMessage || "You don't have necessary permissions." + ) + ).toBeVisible(); + } else { + await expect( + testUserPage.locator('[data-testid="permission-error-placeholder"]') + ).not.toBeVisible(); + } +}; + +// Entity-specific test functions +export const testTableSpecificOperations = async ( + testUserPage: Page, + entity: TableClass, + effect: 'allow' | 'deny' +) => { + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + // Test ViewQueries + await testPermissionErrorVisibility( + testUserPage, + 'table_queries', + effect, + "You don't have necessary permissions. Please check with the admin to get the View Queries permission." + ); + + // Test ViewSampleData + await testPermissionErrorVisibility( + testUserPage, + 'sample_data', + effect, + "You don't have necessary permissions. Please check with the admin to get the View Sample Data permission." + ); + + // Test ViewDataProfile + await testUserPage.locator('[data-testid="profiler"]').click(); + + // Test Table Profile + await testProfilerTabPermission( + testUserPage, + 'Table Profile', + effect, + "You don't have necessary permissions. Please check with the admin to get the View Data Observability permission." + ); + + // Test Column Profile + await testProfilerTabPermission( + testUserPage, + 'Column Profile', + effect, + "You don't have necessary permissions. Please check with the admin to get the ViewDataProfile permission." + ); + + // Test Data Quality + await testProfilerTabPermission( + testUserPage, + 'Data Quality', + effect, + "You don't have necessary permissions. Please check with the admin to get the View Data Observability permission." + ); + + await checkElementVisibility( + testUserPage, + { + testId: 'Usage', + type: 'label', + }, + effect + ); +}; + +export const testTopicSpecificOperations = async ( + testUserPage: Page, + entity: TopicClass, + effect: 'allow' | 'deny' +) => { + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + // Test ViewSampleData for Topic + await testPermissionErrorVisibility( + testUserPage, + 'sample_data', + effect, + "You don't have necessary permissions. Please check with the admin to get the View Sample Data permission." + ); +}; + +export const testPipelineSpecificOperations = async ( + testUserPage: Page, + entity: PipelineClass, + effect: 'allow' | 'deny' +) => { + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + // Test Edit Lineage for Pipeline + await testUserPage.getByRole('tab', { name: 'Lineage' }).click(); + await testUserPage.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + if (effect === 'allow') { + await expect(testUserPage.getByTestId('edit-lineage')).toBeVisible(); + } else { + await expect(testUserPage.getByTestId('edit-lineage')).toBeDisabled(); + } +}; + +export const testSearchIndexSpecificOperations = async ( + testUserPage: Page, + entity: SearchIndexClass, + effect: 'allow' | 'deny' +) => { + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + // Test ViewUsage for Search Index + await testPermissionErrorVisibility( + testUserPage, + 'sample_data', + effect, + "You don't have necessary permissions. Please check with the admin to get the View Sample Data permission." + ); +}; + +export const testStoredProcedureSpecificOperations = async ( + testUserPage: Page, + entity: TableClass, + effect: 'allow' | 'deny' +) => { + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + // Test ViewUsage for Stored Procedure + await testPermissionErrorVisibility( + testUserPage, + 'usage', + effect, + "You don't have necessary permissions. Please check with the admin to get the View Usage permission." + ); +}; + +export const testDashboardDataModelSpecificOperations = async ( + testUserPage: Page, + entity: DashboardDataModelClass, + effect: 'allow' | 'deny' +) => { + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + // Test Edit Lineage for Dashboard Data Model + await testUserPage.getByRole('tab', { name: 'Lineage' }).click(); + await testUserPage.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + if (effect === 'allow') { + await expect(testUserPage.getByTestId('edit-lineage')).toBeVisible(); + } else { + await expect(testUserPage.getByTestId('edit-lineage')).toBeDisabled(); + } +}; + +export const testDashboardSpecificOperations = async ( + testUserPage: Page, + entity: DashboardClass, + effect: 'allow' | 'deny' +) => { + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + await checkElementVisibility( + testUserPage, + { + testId: 'Usage', + type: 'label', + }, + effect + ); +}; + +export const testMlModelSpecificOperations = async ( + testUserPage: Page, + entity: MlModelClass, + effect: 'allow' | 'deny' +) => { + await redirectToHomePage(testUserPage); + await entity.visitEntityPage(testUserPage); + + await checkElementVisibility( + testUserPage, + { + testId: 'Usage', + type: 'label', + }, + effect + ); +}; + +// Helper function to run common permission tests +export const runCommonPermissionTests = async ( + testUserPage: Page, + entity: EntityClass, + effect: 'allow' | 'deny' +) => { + await testCommonOperations(testUserPage, entity, effect); +}; + +export const runEntitySpecificPermissionTests = async ( + testUserPage: Page, + entity: EntityClass, + effect: 'allow' | 'deny', + specificTest: ( + page: Page, + entity: EntityClass, + effect: 'allow' | 'deny' + ) => Promise +) => { + await specificTest(testUserPage, entity, effect); +}; + +// Entity configuration with their specific test functions +export const entityConfig = { + Table: { + class: TableClass, + specificTest: testTableSpecificOperations, + }, + Dashboard: { + class: DashboardClass, + specificTest: testDashboardSpecificOperations, + }, + Pipeline: { + class: PipelineClass, + specificTest: testPipelineSpecificOperations, + }, + Topic: { + class: TopicClass, + specificTest: testTopicSpecificOperations, + }, + MlModel: { + class: MlModelClass, + specificTest: testMlModelSpecificOperations, + }, + Container: { + class: ContainerClass, + }, + SearchIndex: { + class: SearchIndexClass, + specificTest: testSearchIndexSpecificOperations, + }, + DashboardDataModel: { + class: DashboardDataModelClass, + specificTest: testDashboardDataModelSpecificOperations, + }, + Metric: { + class: MetricClass, + }, +} as const; + +// Function to create custom properties for different entity types +export const createCustomPropertyForEntity = async ( + browser: any, + entityType: string, + customPropertyName: string, + adminUser: UserClass +) => { + const page = await browser.newPage(); + await adminUser.login(page); + + // Map entity types to their correct API types (same as used in working tests) + const entityTypeMapping: Record = { + Table: 'tables', + Dashboard: 'dashboards', + Pipeline: 'pipelines', + Topic: 'topics', + MlModel: 'mlmodels', + Container: 'containers', + SearchIndex: 'searchIndexes', + DashboardDataModel: 'dashboardDataModels', + Metric: 'metrics', + Database: 'databases', + DatabaseSchema: 'databaseSchemas', + StoredProcedure: 'storedProcedures', + GlossaryTerm: 'glossaryTerm', + Domain: 'domains', + ApiCollection: 'apiCollections', + ApiEndpoint: 'apiEndpoints', + DataProduct: 'dataProducts', + }; + + const entityApiType = + entityTypeMapping[entityType] || entityType.toLowerCase(); + + await settingClick(page, entityApiType as SettingOptionsType, true); + + await addCustomPropertiesForEntity({ + page, + propertyName: customPropertyName, + customPropertyData: { description: `Test ${entityType} custom property` }, + customType: 'String', + }); + + await page.close(); +}; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/permission.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/permission.ts index c7fa61ff462..ad550919538 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/permission.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/permission.ts @@ -14,7 +14,64 @@ import { APIRequestContext, expect, Page } from '@playwright/test'; import { DATA_CONSUMER_RULES, ORGANIZATION_POLICY_RULES, + VIEW_ALL_RULE, } from '../constant/permission'; +import { PolicyClass } from '../support/access-control/PoliciesClass'; +import { RolesClass } from '../support/access-control/RolesClass'; +import { UserClass } from '../support/user/UserClass'; +import { getApiContext, redirectToHomePage } from './common'; + +let policy: PolicyClass; +let role: RolesClass; + +export const initializePermissions = async ( + page: Page, + effect: 'allow' | 'deny', + operations: string[] +) => { + await redirectToHomePage(page); + const { apiContext } = await getApiContext(page); + + policy = new PolicyClass(); + + const policyRules = [ + ...VIEW_ALL_RULE, + { + name: `Global${effect}AllOperationsPolicy`, + resources: ['All'], + operations, + effect, + }, + ]; + + await policy.create(apiContext, policyRules); + + role = new RolesClass(); + await role.create(apiContext, [policy.responseData.name]); + + return { apiContext, policy, role }; +}; + +export const assignRoleToUser = async (page: Page, testUser: UserClass) => { + const { apiContext } = await getApiContext(page); + + await testUser.patch({ + apiContext, + patchData: [ + { + op: 'replace', + path: '/roles', + value: [ + { + id: role.responseData.id, + type: 'role', + name: role.responseData.name, + }, + ], + }, + ], + }); +}; export const checkNoPermissionPlaceholder = async ( page: Page, @@ -169,3 +226,12 @@ export const updateDefaultOrganizationPolicy = async ( }, }); }; + +export const cleanupPermissions = async (apiContext: APIRequestContext) => { + if (role && role.responseData?.id) { + await role.delete(apiContext); + } + if (policy && policy.responseData?.id) { + await policy.delete(apiContext); + } +};