From e5a64b75abb96792902fa9ceaba1464e6970b468 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Wed, 11 Sep 2024 11:54:36 +0530 Subject: [PATCH 1/5] playwright: remove organisation policy and role before staring the playwright test --- .../resources/ui/playwright/e2e/auth.setup.ts | 2 ++ .../resources/ui/playwright/utils/team.ts | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/auth.setup.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/auth.setup.ts index 4c8c825b4b78..8894062b2f7e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/auth.setup.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/auth.setup.ts @@ -15,6 +15,7 @@ import { JWT_EXPIRY_TIME_MAP } from '../constant/login'; import { AdminClass } from '../support/user/AdminClass'; import { getApiContext } from '../utils/common'; import { updateJWTTokenExpiryTime } from '../utils/login'; +import { removeOrganizationPolicyAndRole } from '../utils/team'; const adminFile = 'playwright/.auth/admin.json'; setup('authenticate as admin', async ({ page }) => { @@ -25,6 +26,7 @@ setup('authenticate as admin', async ({ page }) => { await page.waitForURL('**/my-data'); const { apiContext, afterAction } = await getApiContext(page); await updateJWTTokenExpiryTime(apiContext, JWT_EXPIRY_TIME_MAP['4 hours']); + await removeOrganizationPolicyAndRole(apiContext); await afterAction(); await admin.logout(page); await page.waitForURL('**/signin'); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts index 10cf7db398e5..fc4d5293fd9c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts @@ -10,7 +10,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { expect, Page } from '@playwright/test'; +import { APIRequestContext, expect, Page } from '@playwright/test'; import { descriptionBox, uuid } from './common'; import { validateFormNameFieldInput } from './form'; @@ -208,3 +208,29 @@ export const addTeamHierarchy = async ( await page.click('[form="add-team-form"]'); await saveTeamResponse; }; + +export const removeOrganizationPolicyAndRole = async ( + apiContext: APIRequestContext +) => { + const organizationTeamResponse = await apiContext + .get(`/api/v1/teams/name/Organization`) + .then((res) => res.json()); + + await apiContext.patch(`/api/v1/teams/${organizationTeamResponse.id}`, { + data: [ + { + op: 'replace', + path: '/policies', + value: [], + }, + { + op: 'replace', + path: '/defaultRoles', + value: [], + }, + ], + headers: { + 'Content-Type': 'application/json-patch+json', + }, + }); +}; From 4c8385487d6cb98ba331d1763cc9204c37be15af Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Wed, 11 Sep 2024 15:51:06 +0530 Subject: [PATCH 2/5] added default role as data consumer --- .../e2e/Pages/EntityDataConsumer.spec.ts | 21 ------------------- .../ui/playwright/support/user/UserClass.ts | 8 ++++++- 2 files changed, 7 insertions(+), 22 deletions(-) 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 cd16a765f006..f04ce40727dd 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 @@ -63,27 +63,6 @@ entities.forEach((EntityClass) => { await user.create(apiContext); - const dataConsumerRoleResponse = await apiContext.get( - '/api/v1/roles/name/DataConsumer' - ); - - const dataConsumerRole = await dataConsumerRoleResponse.json(); - - await user.patch({ - apiContext, - patchData: [ - { - op: 'add', - path: '/roles/0', - value: { - id: dataConsumerRole.id, - type: 'role', - name: dataConsumerRole.name, - }, - }, - ], - }); - await EntityDataClass.preRequisitesForTests(apiContext); await entity.create(apiContext); await afterAction(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index 6174d901dbc9..11f8b4e5a9aa 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -48,8 +48,14 @@ export class UserClass { } async create(apiContext: APIRequestContext) { + const dataConsumerRoleResponse = await apiContext.get( + '/api/v1/roles/name/DataConsumer' + ); + + const dataConsumerRole = await dataConsumerRoleResponse.json(); + const response = await apiContext.post('/api/v1/users/signup', { - data: this.data, + data: { ...this.data, roles: [dataConsumerRole.id] }, }); this.responseData = await response.json(); From 532790236bd8764e99e02817d827faeac27268ec Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Wed, 11 Sep 2024 18:00:20 +0530 Subject: [PATCH 3/5] fixed failing test --- .../ui/playwright/support/user/UserClass.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index 11f8b4e5a9aa..6e25fdb660ea 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -55,12 +55,26 @@ export class UserClass { const dataConsumerRole = await dataConsumerRoleResponse.json(); const response = await apiContext.post('/api/v1/users/signup', { - data: { ...this.data, roles: [dataConsumerRole.id] }, + data: this.data, }); this.responseData = await response.json(); + const { entity } = await this.patch({ + apiContext, + patchData: [ + { + op: 'add', + path: '/roles/0', + value: { + id: dataConsumerRole.id, + type: 'role', + name: dataConsumerRole.name, + }, + }, + ], + }); - return response.body; + return entity; } async patch({ From e8cdb129e60e4d390ecbbac80482749d8d9a4c9d Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Wed, 11 Sep 2024 22:01:02 +0530 Subject: [PATCH 4/5] keeping org policies as it is. --- .../src/main/resources/ui/playwright/utils/team.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts index 28c30317605f..9adddd6d8a54 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts @@ -214,11 +214,6 @@ export const removeOrganizationPolicyAndRole = async ( await apiContext.patch(`/api/v1/teams/${organizationTeamResponse.id}`, { data: [ - { - op: 'replace', - path: '/policies', - value: [], - }, { op: 'replace', path: '/defaultRoles', From 6fff580cbc73de43f617441d00e50040900f87b0 Mon Sep 17 00:00:00 2001 From: Shailesh Parmar Date: Thu, 12 Sep 2024 19:31:56 +0530 Subject: [PATCH 5/5] migrate permission spec --- .../ui/cypress/e2e/Pages/Permission.spec.ts | 439 ------------------ .../e2e/Features/Permission.spec.ts | 271 +++++++++++ .../support/access-control/PoliciesClass.ts | 17 + .../playwright/support/entity/TableClass.ts | 20 + .../ui/playwright/utils/permission.ts | 119 +++++ 5 files changed, 427 insertions(+), 439 deletions(-) delete mode 100644 openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Permission.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permission.spec.ts create mode 100644 openmetadata-ui/src/main/resources/ui/playwright/utils/permission.ts diff --git a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Permission.spec.ts b/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Permission.spec.ts deleted file mode 100644 index 196dec909cd8..000000000000 --- a/openmetadata-ui/src/main/resources/ui/cypress/e2e/Pages/Permission.spec.ts +++ /dev/null @@ -1,439 +0,0 @@ -/* - * 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 { - interceptURL, - uuid, - verifyResponseStatusCode, -} from '../../common/common'; -import UsersTestClass from '../../common/Entities/UserClass'; -import { hardDeleteService } from '../../common/EntityUtils'; -import { - createEntityTableViaREST, - visitEntityDetailsPage, -} from '../../common/Utils/Entity'; -import { getToken } from '../../common/Utils/LocalStorage'; -import { EntityType } from '../../constants/Entity.interface'; -import { DATABASE_SERVICE, USER_DETAILS } from '../../constants/EntityConstant'; -import { SERVICE_CATEGORIES } from '../../constants/service.constants'; - -type RoleType = { - name: string; - policies: string[]; - id?: string; -}; -type PolicyType = { - name: string; - rules: { - name: string; - resources: string[]; - operations: string[]; - effect: string; - }[]; - id?: string; -}; -type OrganizationTeamType = { - id: string; - policies: { - id: string; - type: string; - }[]; - defaultRoles: { - id: string; - type: string; - }[]; -}; -const entity = new UsersTestClass(); -const policy: PolicyType = { - name: `cy-permission-policy-${uuid()}`, - rules: [ - { - name: `cy-permission-rule-${uuid()}`, - resources: ['All'], - operations: ['ViewBasic'], - effect: 'allow', - }, - ], -}; - -const role: RoleType = { - name: `cy-permission-role-${uuid()}`, - policies: [policy.name], -}; -const tableFqn = `${DATABASE_SERVICE.entity.databaseSchema}.${DATABASE_SERVICE.entity.name}`; -const testSuite = { - name: `${tableFqn}.testSuite`, - executableEntityReference: tableFqn, -}; -const testCase = { - name: `user_tokens_table_column_name_to_exist_${uuid()}`, - entityLink: `<#E::table::${testSuite.executableEntityReference}>`, - parameterValues: [{ name: 'columnName', value: 'id' }], - testDefinition: 'tableColumnNameToExist', - description: 'test case description', - testSuite: testSuite.name, -}; - -let organizationTeam = {} as OrganizationTeamType; -let userId = ''; -let teamId = ''; - -const viewPermissions = [ - { - title: 'ViewBasic, ViewSampleData & ViewQueries permission', - data: { - patch: [ - { op: 'add', path: '/rules/0/operations/1', value: 'ViewSampleData' }, - { op: 'add', path: '/rules/0/operations/2', value: 'ViewQueries' }, - ], - permission: { viewSampleData: true, viewQueries: true }, - }, - }, - { - title: 'ViewBasic, ViewSampleData, ViewQueries & ViewTests permission', - data: { - patch: [{ op: 'add', path: '/rules/0/operations/3', value: 'ViewTests' }], - permission: { - viewSampleData: true, - viewQueries: true, - viewTests: true, - }, - }, - }, - { - title: 'EditDisplayName permission', - data: { - patch: [ - { op: 'add', path: '/rules/0/operations/4', value: 'EditDisplayName' }, - ], - permission: { - viewSampleData: true, - viewQueries: true, - viewTests: true, - editDisplayName: true, - }, - }, - }, -]; - -const createViewBasicRoleViaREST = ({ token }) => { - cy.request({ - method: 'POST', - url: `/api/v1/policies`, - headers: { Authorization: `Bearer ${token}` }, - body: policy, - }).then((policyResponse) => { - policy.id = policyResponse.body.id; - cy.request({ - method: 'POST', - url: `/api/v1/roles`, - headers: { Authorization: `Bearer ${token}` }, - body: role, - }).then((roleResponse) => { - role.id = roleResponse.body.id; - cy.request({ - method: 'GET', - url: `/api/v1/teams/name/Organization?fields=defaultRoles,policies`, - headers: { Authorization: `Bearer ${token}` }, - }).then((orgResponse) => { - organizationTeam = orgResponse.body; - cy.request({ - method: 'PATCH', - url: `/api/v1/teams/${orgResponse.body.id}`, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json-patch+json', - }, - body: [ - { - op: 'replace', - path: '/policies', - value: [ - { - id: policyResponse.body.id, - type: 'policy', - }, - ], - }, - { - op: 'replace', - path: '/defaultRoles', - value: [ - { - id: roleResponse.body.id, - type: 'role', - }, - ], - }, - ], - }); - - cy.request({ - method: 'POST', - url: `/api/v1/users/signup`, - headers: { Authorization: `Bearer ${token}` }, - body: USER_DETAILS, - }).then((userResponse) => { - userId = userResponse.body.id; - cy.request({ - method: 'POST', - url: `/api/v1/teams`, - headers: { Authorization: `Bearer ${token}` }, - body: { - name: `teamBasic-${uuid()}`, - description: 'teamBasic', - teamType: 'Group', - defaultRoles: [roleResponse.body.id], - policies: [policyResponse.body.id], - users: [userResponse.body.id], - }, - }).then((teamResponse) => { - teamId = teamResponse.body.id; - }); - }); - }); - }); - }); -}; - -const preRequisite = () => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - createViewBasicRoleViaREST({ - token, - }); - - createEntityTableViaREST({ - token, - ...DATABASE_SERVICE, - tables: [], - }); - cy.request({ - method: 'POST', - url: `/api/v1/tables`, - headers: { Authorization: `Bearer ${token}` }, - body: DATABASE_SERVICE.entity, - }).then((response) => { - cy.request({ - method: 'POST', - url: `/api/v1/queries`, - headers: { Authorization: `Bearer ${token}` }, - body: { - query: `select * from dim_address_${uuid()}`, - queryUsedIn: [{ id: response.body.id, type: 'table' }], - queryDate: Date.now(), - service: 'sample_data', - }, - }); - cy.request({ - method: 'POST', - url: `/api/v1/dataQuality/testSuites/executable`, - headers: { Authorization: `Bearer ${token}` }, - body: testSuite, - }).then(() => { - cy.request({ - method: 'POST', - url: `/api/v1/dataQuality/testCases`, - headers: { Authorization: `Bearer ${token}` }, - body: testCase, - }); - }); - }); - }); - cy.logout(); -}; - -const cleanUp = () => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - hardDeleteService({ - token, - serviceFqn: DATABASE_SERVICE.service.name, - serviceType: SERVICE_CATEGORIES.DATABASE_SERVICES, - }); - cy.request({ - method: 'DELETE', - url: `/api/v1/roles/${role.id}?hardDelete=true&recursive=false`, - headers: { Authorization: `Bearer ${token}` }, - }); - cy.request({ - method: 'DELETE', - url: `/api/v1/policies/${policy.id}?hardDelete=true&recursive=false`, - headers: { Authorization: `Bearer ${token}` }, - }); - - cy.request({ - method: 'PATCH', - url: `/api/v1/teams/${organizationTeam.id}`, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json-patch+json', - }, - body: [ - { - op: 'replace', - path: '/policies', - value: organizationTeam.policies, - }, - - { - op: 'add', - path: '/defaultRoles', - value: organizationTeam.defaultRoles, - }, - ], - }); - // Delete created user - cy.request({ - method: 'DELETE', - url: `/api/v1/users/${userId}?hardDelete=true&recursive=false`, - headers: { Authorization: `Bearer ${token}` }, - }); - // Delete created team - cy.request({ - method: 'DELETE', - url: `/api/v1/teams/${teamId}?hardDelete=true&recursive=false`, - headers: { Authorization: `Bearer ${token}` }, - }); - }); -}; - -const checkPermission = (permission?: { - viewSampleData?: boolean; - viewQueries?: boolean; - viewTests?: boolean; - editDisplayName?: boolean; -}) => { - cy.login(USER_DETAILS.email, USER_DETAILS.password); - visitEntityDetailsPage({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - }); - entity.viewPermissions(permission); - cy.logout(); -}; -const updatePolicy = ( - patch: { op: string; path: string; value: unknown }[] -) => { - cy.login(); - cy.getAllLocalStorage().then((data) => { - const token = getToken(data); - cy.request({ - method: 'PATCH', - url: `/api/v1/policies/${policy.id}`, - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/json-patch+json', - }, - body: patch, - }); - }); - cy.logout(); - cy.reload(); -}; - -describe('Permissions', { tags: 'Settings' }, () => { - before(preRequisite); - after(cleanUp); - - it('ViewBasic permission', () => { - checkPermission(); - }); - - viewPermissions.forEach((permissionData) => { - it(`check ${permissionData.title}`, () => { - updatePolicy(permissionData.data.patch); - checkPermission(permissionData.data.permission); - }); - }); - - it('EditQuery permission', () => { - updatePolicy([ - { - op: 'add', - path: '/rules/1', - value: { - name: `cy-edit-query-rule-${uuid()}`, - resources: ['query'], - operations: ['ViewAll', 'EditAll'], - effect: 'allow', - }, - }, - { op: 'add', path: '/rules/0/operations/5', value: 'EditQueries' }, - ]); - - cy.login(USER_DETAILS.email, USER_DETAILS.password); - visitEntityDetailsPage({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - }); - interceptURL( - 'GET', - '/api/v1/search/query?q=*&index=query_search_index*', - 'getQueries' - ); - cy.get('[data-testid="table_queries"]').click(); - verifyResponseStatusCode('@getQueries', 200); - cy.get('[data-testid="query-btn"]').click(); - cy.get('[data-menu-id*="edit-query"]').click(); - interceptURL('PATCH', '/api/v1/queries/*', 'updateQuery'); - cy.get('.CodeMirror-line').click().type('updated'); - cy.get('[data-testid="save-query-btn"]').click(); - verifyResponseStatusCode('@updateQuery', 200); - cy.logout(); - }); - - it('EditTest permission', () => { - updatePolicy([ - { op: 'add', path: '/rules/1/operations/6', value: 'EditTests' }, - { - op: 'add', - path: '/rules/2', - value: { - name: `cy-edit-test-case-rule-${uuid()}`, - resources: ['testCase'], - operations: ['ViewAll', 'EditAll'], - effect: 'allow', - }, - }, - ]); - - cy.login(USER_DETAILS.email, USER_DETAILS.password); - visitEntityDetailsPage({ - term: DATABASE_SERVICE.entity.name, - serviceName: DATABASE_SERVICE.service.name, - entity: EntityType.Table, - }); - interceptURL( - 'GET', - '/api/v1/dataQuality/testCases/search/list?fields=*', - 'testCase' - ); - cy.get('[data-testid="profiler"]').click(); - cy.get('[data-testid="profiler-tab-left-panel"]') - .contains('Data Quality') - .click(); - verifyResponseStatusCode('@testCase', 200); - cy.get(`[data-testid="edit-${testCase.name}"]`).click(); - cy.get('#tableTestForm_params_columnName') - .scrollIntoView() - .clear() - .type('test'); - interceptURL('PATCH', '/api/v1/dataQuality/testCases/*', 'updateTest'); - cy.get('.ant-modal-footer').contains('Submit').click(); - verifyResponseStatusCode('@updateTest', 200); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permission.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permission.spec.ts new file mode 100644 index 000000000000..c274bc70d320 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Permission.spec.ts @@ -0,0 +1,271 @@ +/* + * 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 { Operation } from 'fast-json-patch'; +import { PolicyClass } from '../../support/access-control/PoliciesClass'; +import { RolesClass } from '../../support/access-control/RolesClass'; +import { TableClass } from '../../support/entity/TableClass'; +import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; +import { getApiContext, redirectToHomePage, uuid } from '../../utils/common'; +import { validateViewPermissions } from '../../utils/permission'; + +const policy = new PolicyClass(); +const policy2 = new PolicyClass(); +const role = new RolesClass(); +const role2 = new RolesClass(); +const user = new UserClass(); +const table = new TableClass(); + +const viewPermissionsData = [ + { + title: 'ViewBasic, ViewSampleData & ViewQueries permission', + data: { + patch: [ + { op: 'add', path: '/rules/0/operations/1', value: 'ViewSampleData' }, + { op: 'add', path: '/rules/0/operations/2', value: 'ViewQueries' }, + ], + permission: { viewSampleData: true, viewQueries: true }, + }, + }, + { + title: 'ViewBasic, ViewSampleData, ViewQueries & ViewTests permission', + data: { + patch: [{ op: 'add', path: '/rules/0/operations/3', value: 'ViewTests' }], + permission: { + viewSampleData: true, + viewQueries: true, + viewTests: true, + }, + }, + }, + { + title: 'EditDisplayName permission', + data: { + patch: [ + { op: 'add', path: '/rules/0/operations/4', value: 'EditDisplayName' }, + ], + permission: { + viewSampleData: true, + viewQueries: true, + viewTests: true, + editDisplayName: true, + }, + }, + }, +]; + +const test = base.extend<{ + adminPage: Page; + userPage: Page; +}>({ + adminPage: async ({ browser }, use) => { + const { page } = await performAdminLogin(browser); + await use(page); + await page.close(); + }, + userPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await user.login(page); + await use(page); + await page.close(); + }, +}); + +test.beforeAll(async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await user.create(apiContext); + const policyResponse = await policy.create(apiContext, [ + { + name: `pw-permission-rule-${uuid()}`, + resources: ['All'], + operations: ['ViewBasic'], + effect: 'allow', + }, + ]); + const policyResponse2 = await policy2.create(apiContext, [ + { + name: `pw-permission-rule-${uuid()}`, + resources: ['All'], + operations: ['EditOwners'], + effect: 'deny', + }, + ]); + await table.create(apiContext); + await table.createTestCase(apiContext); + await table.createQuery(apiContext); + const roleResponse = await role.create(apiContext, [ + policyResponse.fullyQualifiedName, + ]); + const roleResponse2 = await role2.create(apiContext, [ + policyResponse2.fullyQualifiedName, + ]); + await user.patch({ + apiContext, + patchData: [ + { + op: 'replace', + path: '/roles', + value: [ + { + id: roleResponse.id, + type: 'role', + name: roleResponse.name, + }, + { + id: roleResponse2.id, + type: 'role', + name: roleResponse2.name, + }, + ], + }, + ], + }); + await afterAction(); +}); + +test.afterAll(async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await user.delete(apiContext); + await role.delete(apiContext); + await policy.delete(apiContext); + await table.delete(apiContext); + await afterAction(); +}); + +test('Permissions', async ({ userPage, adminPage }) => { + test.slow(); + + await redirectToHomePage(userPage); + + await test.step('ViewBasic permission', async () => { + await table.visitEntityPage(userPage); + await userPage.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + await validateViewPermissions(userPage); + }); + + for (const viewPermission of viewPermissionsData) { + await test.step(viewPermission.title, async () => { + const { apiContext, afterAction } = await getApiContext(adminPage); + await policy.patch(apiContext, viewPermission.data.patch as Operation[]); + await afterAction(); + await redirectToHomePage(userPage); + await userPage.reload(); + const permissionResponse = userPage.waitForResponse( + `/api/v1/permissions/table/name/${encodeURIComponent( + table.entityResponseData?.['fullyQualifiedName'] + )}` + ); + await table.visitEntityPage(userPage); + await permissionResponse; + await userPage.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + await validateViewPermissions(userPage, viewPermission.data.permission); + }); + } + + await test.step('EditQuery permission', async () => { + const { apiContext, afterAction } = await getApiContext(adminPage); + await policy.patch(apiContext, [ + { + op: 'add', + path: '/rules/1', + value: { + name: `pw-edit-query-rule-${uuid()}`, + resources: ['query'], + operations: ['ViewAll', 'EditAll'], + effect: 'allow', + }, + }, + { op: 'add', path: '/rules/0/operations/5', value: 'EditQueries' }, + ]); + await afterAction(); + await redirectToHomePage(userPage); + await userPage.reload(); + const permissionResponse = userPage.waitForResponse( + `/api/v1/permissions/table/name/${encodeURIComponent( + table.entityResponseData?.['fullyQualifiedName'] + )}` + ); + await table.visitEntityPage(userPage); + await permissionResponse; + await userPage.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + const queryListResponse = userPage.waitForResponse( + '/api/v1/search/query?q=*&index=query_search_index*' + ); + await userPage.click('[data-testid="table_queries"]'); + await queryListResponse; + await userPage.click('[data-testid="query-btn"]'); + await userPage.click('[data-menu-id*="edit-query"]'); + await userPage.locator('.CodeMirror-line').click(); + await userPage.keyboard.type('updated'); + const saveQueryResponse = userPage.waitForResponse('/api/v1/queries/*'); + await userPage.click('[data-testid="save-query-btn"]'); + await saveQueryResponse; + }); + + await test.step('EditTest permission', async () => { + const testCaseName = table.testCasesResponseData[0]?.['name']; + const { apiContext, afterAction } = await getApiContext(adminPage); + await policy.patch(apiContext, [ + { op: 'add', path: '/rules/1/operations/6', value: 'EditTests' }, + { + op: 'add', + path: '/rules/2', + value: { + name: `cy-edit-test-case-rule-${uuid()}`, + resources: ['testCase'], + operations: ['ViewAll', 'EditAll'], + effect: 'allow', + }, + }, + ]); + await afterAction(); + await redirectToHomePage(userPage); + await userPage.reload(); + const permissionResponse = userPage.waitForResponse( + `/api/v1/permissions/table/name/${encodeURIComponent( + table.entityResponseData?.['fullyQualifiedName'] + )}` + ); + await table.visitEntityPage(userPage); + await permissionResponse; + await userPage.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await userPage.getByTestId('profiler').click(); + const testCaseResponse = userPage.waitForResponse( + '/api/v1/dataQuality/testCases/search/list?fields=*' + ); + await userPage + .getByTestId('profiler-tab-left-panel') + .getByText('Data Quality') + .click(); + await testCaseResponse; + + await userPage.getByTestId(`edit-${testCaseName}`).click(); + await userPage.locator('#tableTestForm_displayName').clear(); + await userPage.fill('#tableTestForm_displayName', 'Update_display_name'); + const saveTestResponse = userPage.waitForResponse( + '/api/v1/dataQuality/testCases/*' + ); + await userPage.locator('.ant-modal-footer').getByText('Submit').click(); + await saveTestResponse; + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/PoliciesClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/PoliciesClass.ts index b9916862fe69..08853690bcf0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/PoliciesClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/access-control/PoliciesClass.ts @@ -11,6 +11,7 @@ * limitations under the License. */ import { APIRequestContext } from '@playwright/test'; +import { Operation } from 'fast-json-patch'; import { uuid } from '../../utils/common'; type ResponseDataType = { @@ -55,6 +56,22 @@ export class PolicyClass { return data; } + async patch(apiContext: APIRequestContext, patchData: Operation[]) { + const response = await apiContext.patch( + `/api/v1/policies/${this.responseData.id}`, + { + data: patchData, + headers: { + 'Content-Type': 'application/json-patch+json', + }, + } + ); + const data = await response.json(); + this.responseData = data; + + return data; + } + async delete(apiContext: APIRequestContext) { const response = await apiContext.delete( `/api/v1/policies/${this.responseData.id}?hardDelete=true&recursive=true` diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index 6e3c88b79217..ea2aed51702e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -110,6 +110,7 @@ export class TableClass extends EntityClass { testSuiteResponseData: unknown; testSuitePipelineResponseData: unknown[] = []; testCasesResponseData: unknown[] = []; + queryResponseData: unknown[] = []; constructor(name?: string) { super(EntityTypeEndpoint.Table); @@ -172,6 +173,25 @@ export class TableClass extends EntityClass { }); } + async createQuery(apiContext: APIRequestContext, queryText?: string) { + const queryResponse = await apiContext.post('/api/v1/queries', { + data: { + query: + queryText ?? + `select * from ${this.entityResponseData?.['fullyQualifiedName']}`, + queryUsedIn: [{ id: this.entityResponseData?.['id'], type: 'table' }], + queryDate: Date.now(), + service: this.serviceResponseData?.['name'], + }, + }); + + const query = await queryResponse.json(); + + this.queryResponseData.push(query); + + return query; + } + async createTestSuiteAndPipelines( apiContext: APIRequestContext, testSuite?: TestSuiteData diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/permission.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/permission.ts new file mode 100644 index 000000000000..f5dc8f2ddd88 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/permission.ts @@ -0,0 +1,119 @@ +/* + * 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'; + +export const checkNoPermissionPlaceholder = async ( + page: Page, + label: string | RegExp, + permission = false +) => { + const placeholder = page + .getByLabel(label) + .locator('[data-testid="permission-error-placeholder"]'); + + if (permission) { + await expect(placeholder).not.toBeVisible(); + } else { + await expect(placeholder).toBeVisible(); + await expect(placeholder).toContainText( + 'You don’t have access, please check with the admin to get permissions' + ); + } +}; + +export const validateViewPermissions = async ( + page: Page, + permission?: { + viewSampleData?: boolean; + viewQueries?: boolean; + viewTests?: boolean; + editDisplayName?: boolean; + } +) => { + // check Add domain permission + await expect(page.locator('[data-testid="add-domain"]')).not.toBeVisible(); + + await expect( + page.locator('[data-testid="edit-displayName-button"]') + ).toHaveCount(permission?.editDisplayName ? 6 : 0); + + // check edit owner permission + await expect(page.locator('[data-testid="edit-owner"]')).not.toBeVisible(); + // check edit description permission + await expect( + page.locator('[data-testid="edit-description"]') + ).not.toBeVisible(); + // check edit tier permission + await expect(page.locator('[data-testid="edit-tier"]')).not.toBeVisible(); + + // check add tags button + await expect( + page.locator( + '[data-testid="tags-container"] > [data-testid="entity-tags"] .ant-tag' + ) + ).not.toBeVisible(); + // check add glossary term button + await expect( + page.locator( + '[data-testid="glossary-container"] > [data-testid="entity-tags"] .ant-tag' + ) + ).not.toBeVisible(); + + // check manage button + await expect(page.locator('[data-testid="manage-button"]')).toHaveCount( + permission?.editDisplayName ? 1 : 0 + ); + + if (permission?.editDisplayName) { + await page.click('[data-testid="manage-button"]'); + await page.click('[data-testid="rename-button"]'); + await page.fill('#displayName', 'updated-table-name'); + const updateDisplayNameResponse = page.waitForResponse( + (response) => + response.url().includes('api/v1/tables/') && response.status() === 200 + ); + await page.click('[data-testid="save-button"]'); + + await updateDisplayNameResponse; + + await expect( + page.locator('[data-testid="entity-header-display-name"]') + ).toContainText('updated-table-name'); + } + + await page.click('[data-testid="sample_data"]'); + await page.waitForLoadState('domcontentloaded'); + await checkNoPermissionPlaceholder( + page, + /Sample Data/, + permission?.viewSampleData + ); + await page.click('[data-testid="table_queries"]'); + await page.waitForLoadState('domcontentloaded'); + await checkNoPermissionPlaceholder(page, /Queries/, permission?.viewQueries); + await page.click('[data-testid="profiler"]'); + await page.waitForLoadState('domcontentloaded'); + await checkNoPermissionPlaceholder( + page, + /Profiler & Data Quality/, + permission?.viewTests + ); + await page.click('[data-testid="lineage"]'); + await page.waitForLoadState('domcontentloaded'); + + await expect(page.locator('[data-testid="edit-lineage"]')).toBeDisabled(); + + await page.click('[data-testid="custom_properties"]'); + await page.waitForLoadState('domcontentloaded'); + await checkNoPermissionPlaceholder(page, /Custom Properties/); +};