From 9e6078f654ed0a54e9d41b12c3a748449c6079ea Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Fri, 13 Dec 2024 23:41:40 +0530 Subject: [PATCH] supported editUser permission in user tab for team page (#18987) * supported editUser permission in user tab for team page * remove edit all permission check in teams add/remove user api * added playwright test for the editUser permission * Added playwright test for data consumer user and remove no used field from the advance api call --------- Co-authored-by: sonikashah Co-authored-by: sonika-shah <58761340+sonika-shah@users.noreply.github.com> --- .../service/resources/teams/TeamResource.java | 9 - .../ui/playwright/constant/permission.ts | 9 + .../ui/playwright/e2e/Pages/Teams.spec.ts | 235 +++++++++++++++++- .../ui/playwright/support/team/TeamClass.ts | 27 +- .../resources/ui/playwright/utils/team.ts | 37 +++ .../TeamDetails/UserTab/UserTab.component.tsx | 19 +- .../ui/src/pages/TeamsPage/TeamsPage.test.tsx | 1 - .../ui/src/pages/TeamsPage/TeamsPage.tsx | 1 - 8 files changed, 315 insertions(+), 23 deletions(-) diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java index c30c3178bb2..6f0070dff28 100644 --- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java +++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/teams/TeamResource.java @@ -72,7 +72,6 @@ import org.openmetadata.service.limits.Limits; import org.openmetadata.service.resources.Collection; import org.openmetadata.service.resources.EntityResource; import org.openmetadata.service.security.Authorizer; -import org.openmetadata.service.security.policyevaluator.OperationContext; import org.openmetadata.service.util.CSVExportResponse; import org.openmetadata.service.util.EntityUtil; import org.openmetadata.service.util.JsonUtils; @@ -687,10 +686,6 @@ public class TeamResource extends EntityResource { @Context SecurityContext securityContext, @PathParam("teamId") UUID teamId, List users) { - - OperationContext operationContext = - new OperationContext(entityType, MetadataOperation.EDIT_ALL); - authorizer.authorize(securityContext, operationContext, getResourceContextById(teamId)); return repository .updateTeamUsers(securityContext.getUserPrincipal().getName(), teamId, users) .toResponse(); @@ -721,10 +716,6 @@ public class TeamResource extends EntityResource { @Parameter(description = "Id of the user being removed", schema = @Schema(type = "string")) @PathParam("userId") String userId) { - - OperationContext operationContext = - new OperationContext(entityType, MetadataOperation.EDIT_ALL); - authorizer.authorize(securityContext, operationContext, getResourceContextById(teamId)); return repository .deleteTeamUser( securityContext.getUserPrincipal().getName(), teamId, UUID.fromString(userId)) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts b/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts index b4dccdce803..7c810b833c7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts @@ -77,6 +77,15 @@ export const DATA_CONSUMER_RULES: PolicyRulesType[] = [ }, ]; +export const EDIT_USER_FOR_TEAM_RULES: PolicyRulesType[] = [ + { + name: 'EditUserTeams-EditRule', + resources: ['team'], + operations: ['EditUsers'], + effect: 'allow', + }, +]; + export const ORGANIZATION_POLICY_RULES: PolicyRulesType[] = [ { name: 'OrganizationPolicy-NoOwner-Rule', diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Teams.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Teams.spec.ts index 2436b924d16..0e8a26acec8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Teams.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Teams.spec.ts @@ -10,12 +10,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import test, { expect } from '@playwright/test'; +import { expect, Page, test as base } from '@playwright/test'; +import { EDIT_USER_FOR_TEAM_RULES } from '../../constant/permission'; import { GlobalSettingOptions } from '../../constant/settings'; +import { PolicyClass } from '../../support/access-control/PoliciesClass'; +import { RolesClass } from '../../support/access-control/RolesClass'; import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; import { TableClass } from '../../support/entity/TableClass'; import { TeamClass } from '../../support/team/TeamClass'; import { UserClass } from '../../support/user/UserClass'; +import { performAdminLogin } from '../../utils/admin'; import { createNewPage, descriptionBox, @@ -28,6 +32,7 @@ import { addMultiOwner } from '../../utils/entity'; import { settingClick } from '../../utils/sidebar'; import { addTeamOwnerToEntity, + addUserInTeam, createTeam, hardDeleteTeam, searchTeam, @@ -35,10 +40,15 @@ import { verifyAssetsInTeamsPage, } from '../../utils/team'; -// use the admin user to login -test.use({ storageState: 'playwright/.auth/admin.json' }); - +const id = uuid(); +const dataConsumerUser = new UserClass(); +const editOnlyUser = new UserClass(); // this user will have only editUser permission in team +let team = new TeamClass(); +const team2 = new TeamClass(); +const policy = new PolicyClass(); +const role = new RolesClass(); const user = new UserClass(); +const user2 = new UserClass(); const userName = user.data.email.split('@')[0]; let teamDetails: { @@ -55,7 +65,28 @@ let teamDetails: { updatedEmail: `pwteamUpdated${uuid()}@example.com`, }; +const test = base.extend<{ + editOnlyUserPage: Page; + dataConsumerPage: Page; +}>({ + editOnlyUserPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await editOnlyUser.login(page); + await use(page); + await page.close(); + }, + dataConsumerPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await dataConsumerUser.login(page); + await use(page); + await page.close(); + }, +}); + test.describe('Teams Page', () => { + // use the admin user to login + test.use({ storageState: 'playwright/.auth/admin.json' }); + test.slow(true); test.beforeAll('Setup pre-requests', async ({ browser }) => { @@ -636,3 +667,199 @@ test.describe('Teams Page', () => { await afterAction(); }); }); + +test.describe('Teams Page with EditUser Permission', () => { + test.slow(true); + + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await editOnlyUser.create(apiContext); + + const id = uuid(); + await policy.create(apiContext, EDIT_USER_FOR_TEAM_RULES); + await role.create(apiContext, [policy.responseData.name]); + + team = new TeamClass({ + name: `PW%edit-user-team-${id}`, + displayName: `PW Edit User Team ${id}`, + description: 'playwright edit user team description', + teamType: 'Group', + users: [editOnlyUser.responseData.id], + defaultRoles: role.responseData.id ? [role.responseData.id] : [], + }); + await team.create(apiContext); + await team2.create(apiContext); + await user.create(apiContext); + await user2.create(apiContext); + await afterAction(); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await user.delete(apiContext); + await user2.delete(apiContext); + await editOnlyUser.delete(apiContext); + await team.delete(apiContext); + await team2.delete(apiContext); + await policy.delete(apiContext); + await role.delete(apiContext); + await afterAction(); + }); + + test.beforeEach('Visit Home Page', async ({ editOnlyUserPage }) => { + await redirectToHomePage(editOnlyUserPage); + await team2.visitTeamPage(editOnlyUserPage); + }); + + test('Add and Remove User for Team', async ({ editOnlyUserPage }) => { + await test.step('Add user in Team from the placeholder', async () => { + await addUserInTeam(editOnlyUserPage, user); + }); + + await test.step('Add user in Team for the header manage area', async () => { + await addUserInTeam(editOnlyUserPage, user2); + }); + + await test.step('Remove user from Team', async () => { + await editOnlyUserPage + .getByRole('row', { + name: `${user.data.firstName.slice(0, 1).toUpperCase()} ${ + user.data.firstName + }.`, + }) + .getByTestId('remove-user-btn') + .click(); + + const userResponse = editOnlyUserPage.waitForResponse( + '/api/v1/users?fields=**' + ); + await editOnlyUserPage.getByRole('button', { name: 'Confirm' }).click(); + await userResponse; + + await expect( + editOnlyUserPage.locator(`[data-testid="${userName.toLowerCase()}"]`) + ).not.toBeVisible(); + }); + }); +}); + +test.describe('Teams Page with Data Consumer User', () => { + test.slow(true); + + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await dataConsumerUser.create(apiContext); + await user.create(apiContext); + await policy.create(apiContext, EDIT_USER_FOR_TEAM_RULES); + await role.create(apiContext, [policy.responseData.name]); + + team = new TeamClass({ + name: `PW%-data-consumer-team-${id}`, + displayName: `PW Data Consumer Team ${id}`, + description: 'playwright data consumer team description', + teamType: 'Group', + users: [user.responseData.id], + defaultRoles: role.responseData.id ? [role.responseData.id] : [], + }); + await team.create(apiContext); + await team2.create(apiContext); + await afterAction(); + }); + + test.afterAll('Cleanup', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await dataConsumerUser.delete(apiContext); + await user.delete(apiContext); + await team.delete(apiContext); + await team2.delete(apiContext); + await afterAction(); + }); + + test.beforeEach('Visit Home Page', async ({ dataConsumerPage }) => { + await redirectToHomePage(dataConsumerPage); + }); + + test('Should not have edit access on team page with no data available', async ({ + dataConsumerPage, + }) => { + await team2.visitTeamPage(dataConsumerPage); + + await expect( + dataConsumerPage.getByTestId('edit-team-name') + ).not.toBeVisible(); + await expect(dataConsumerPage.getByTestId('add-domain')).not.toBeVisible(); + await expect(dataConsumerPage.getByTestId('edit-owner')).not.toBeVisible(); + await expect(dataConsumerPage.getByTestId('edit-email')).not.toBeVisible(); + await expect( + dataConsumerPage.getByTestId('edit-team-subscription') + ).not.toBeVisible(); + await expect( + dataConsumerPage.getByTestId('manage-button') + ).not.toBeVisible(); + + await expect(dataConsumerPage.getByTestId('join-teams')).toBeVisible(); + + // User Tab + await expect( + dataConsumerPage.getByTestId('add-new-user') + ).not.toBeVisible(); + await expect( + dataConsumerPage.getByTestId('permission-error-placeholder') + ).toBeVisible(); + + // Asset Tab + const assetResponse = dataConsumerPage.waitForResponse( + '/api/v1/search/query?**' + ); + await dataConsumerPage.getByTestId('assets').click(); + await assetResponse; + + await expect( + dataConsumerPage.getByTestId('add-placeholder-button') + ).not.toBeVisible(); + await expect( + dataConsumerPage.getByTestId('no-data-placeholder') + ).toBeVisible(); + + // Role Tab + await dataConsumerPage.getByTestId('roles').click(); + + await expect( + dataConsumerPage.getByTestId('add-placeholder-button') + ).not.toBeVisible(); + await expect( + dataConsumerPage.getByTestId('permission-error-placeholder') + ).toBeVisible(); + + // Policies Tab + await dataConsumerPage.getByTestId('policies').click(); + + await expect( + dataConsumerPage.getByTestId('add-placeholder-button') + ).not.toBeVisible(); + await expect( + dataConsumerPage.getByTestId('permission-error-placeholder') + ).toBeVisible(); + }); + + test('Should not have edit access on team page with data available', async ({ + dataConsumerPage, + }) => { + await team.visitTeamPage(dataConsumerPage); + + // User Tab + await expect( + dataConsumerPage.getByTestId('add-new-user') + ).not.toBeVisible(); + + // Role Tab + await dataConsumerPage.getByTestId('roles').click(); + + await expect(dataConsumerPage.getByTestId('add-role')).not.toBeVisible(); + + // Policies Tab + await dataConsumerPage.getByTestId('policies').click(); + + await expect(dataConsumerPage.getByTestId('add-policy')).not.toBeVisible(); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/team/TeamClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/team/TeamClass.ts index d580c59c8b7..a6553bc4eda 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/team/TeamClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/team/TeamClass.ts @@ -10,8 +10,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { APIRequestContext } from '@playwright/test'; +import { APIRequestContext, expect, Page } from '@playwright/test'; +import { GlobalSettingOptions } from '../../constant/settings'; import { uuid } from '../../utils/common'; +import { settingClick } from '../../utils/sidebar'; +import { searchTeam } from '../../utils/team'; type ResponseDataType = { name: string; displayName: string; @@ -46,6 +49,28 @@ export class TeamClass { return this.responseData; } + async visitTeamPage(page: Page) { + // complete url since we are making basic and advance call to get the details of the team + const fetchOrganizationResponse = page.waitForResponse( + `/api/v1/teams/name/Organization?fields=users%2CdefaultRoles%2Cpolicies%2CchildrenCount%2Cdomains&include=all` + ); + await settingClick(page, GlobalSettingOptions.TEAMS); + await fetchOrganizationResponse; + + await searchTeam(page, this.responseData?.['displayName']); + + await page + .locator(`[data-row-key="${this.data.name}"]`) + .getByRole('link') + .click(); + + await page.waitForLoadState('networkidle'); + + await expect(page.getByTestId('team-heading')).toHaveText( + this.data.displayName + ); + } + async create(apiContext: APIRequestContext) { const response = await apiContext.post('/api/v1/teams', { data: this.data, 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 382f5c2f014..f77452977e7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts @@ -13,6 +13,7 @@ import { APIRequestContext, expect, Page } from '@playwright/test'; import { TableClass } from '../support/entity/TableClass'; import { TeamClass } from '../support/team/TeamClass'; +import { UserClass } from '../support/user/UserClass'; import { descriptionBox, toastNotification, uuid } from './common'; import { addOwner } from './entity'; import { validateFormNameFieldInput } from './form'; @@ -316,3 +317,39 @@ export const verifyAssetsInTeamsPage = async ( page.getByTestId('assets').getByTestId('filter-count') ).toContainText(assetCount.toString()); }; + +export const addUserInTeam = async (page: Page, user: UserClass) => { + const userName = user.data.email.split('@')[0]; + const fetchUsersResponse = page.waitForResponse( + '/api/v1/users?limit=25&isBot=false' + ); + await page.locator('[data-testid="add-new-user"]').click(); + await fetchUsersResponse; + + // Search and select the user + await page + .locator('[data-testid="selectable-list"] [data-testid="searchbar"]') + .fill(user.getUserName()); + + await page + .locator(`[data-testid="selectable-list"] [title="${user.getUserName()}"]`) + .click(); + + await expect( + page.locator( + `[data-testid="selectable-list"] [title="${user.getUserName()}"]` + ) + ).toHaveClass(/active/); + + const updateTeamResponse = page.waitForResponse('/api/v1/users*'); + + // Update the team with the new user + await page.locator('[data-testid="selectable-list-update-btn"]').click(); + await updateTeamResponse; + + // Verify the user is added to the team + + await expect( + page.locator(`[data-testid="${userName.toLowerCase()}"]`) + ).toBeVisible(); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/UserTab/UserTab.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/UserTab/UserTab.component.tsx index b4a4b0d1645..0205cf9c0a7 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/UserTab/UserTab.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/UserTab/UserTab.component.tsx @@ -105,6 +105,11 @@ export const UserTab = ({ [currentTeam.teamType] ); + const editUserPermission = useMemo( + () => permission.EditAll || permission.EditUsers, + [permission.EditAll, permission.EditUsers] + ); + /** * Make API call to fetch current team user data */ @@ -213,13 +218,13 @@ export const UserTab = ({