From 16b03eca4e0f22b41f97fd111a4d1390ba736ab0 Mon Sep 17 00:00:00 2001 From: Ashish Gupta Date: Fri, 19 Sep 2025 20:48:43 +0530 Subject: [PATCH] Modify the team entity specific permission for create and editUser (#22821) * modify the team entity specific permission * cleanup around unused data * added playwright test in support of owner as team permisison (cherry picked from commit 54f47854f29308869d8f2c84e9e24ddbae8c69a8) --- .../ui/playwright/constant/permission.ts | 10 + .../ui/playwright/e2e/Pages/Teams.spec.ts | 214 +++++++++++++----- .../ui/playwright/support/team/TeamClass.ts | 2 + .../resources/ui/playwright/utils/team.ts | 134 ++++++++++- .../Team/TeamDetails/TeamDetailsV1.tsx | 21 +- 5 files changed, 308 insertions(+), 73 deletions(-) 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 218ebd54bff..ed884faf22f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/constant/permission.ts @@ -117,6 +117,16 @@ export const EDIT_USER_FOR_TEAM_RULES: PolicyRulesType[] = [ }, ]; +export const OWNER_TEAM_RULES: PolicyRulesType[] = [ + { + name: 'Owner-EditRule', + resources: ['team'], + operations: ['Create', 'EditAll'], + effect: 'allow', + condition: 'isOwner()', + }, +]; + 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 01d78b4d763..4a4d92593f4 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 @@ -11,10 +11,15 @@ * limitations under the License. */ import { expect, Page, test as base } from '@playwright/test'; -import { EDIT_USER_FOR_TEAM_RULES } from '../../constant/permission'; +import { + EDIT_USER_FOR_TEAM_RULES, + OWNER_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 { DataProduct } from '../../support/domain/DataProduct'; +import { Domain } from '../../support/domain/Domain'; import { EntityTypeEndpoint } from '../../support/entity/Entity.interface'; import { TableClass } from '../../support/entity/TableClass'; import { TeamClass } from '../../support/team/TeamClass'; @@ -34,10 +39,14 @@ import { import { addMultiOwner } from '../../utils/entity'; import { settingClick } from '../../utils/sidebar'; import { + addEmailTeam, addTeamOwnerToEntity, addUserInTeam, + addUserTeam, checkTeamTabCount, createTeam, + executionOnOwnerGroupTeam, + executionOnOwnerTeam, hardDeleteTeam, searchTeam, softDeleteTeam, @@ -47,13 +56,19 @@ import { const id = uuid(); const dataConsumerUser = new UserClass(); const editOnlyUser = new UserClass(); // this user will have only editUser permission in team +const ownerUser = new UserClass(); + let team = new TeamClass(); -const team2 = new TeamClass(); +let team2 = new TeamClass(); +let team3 = new TeamClass(); +let team4 = 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]; +const domain = new Domain(); +const dataProduct = new DataProduct([domain]); let teamDetails: { name?: string; @@ -72,6 +87,7 @@ let teamDetails: { const test = base.extend<{ editOnlyUserPage: Page; dataConsumerPage: Page; + ownerUserPage: Page; }>({ editOnlyUserPage: async ({ browser }, use) => { const page = await browser.newPage(); @@ -85,6 +101,12 @@ const test = base.extend<{ await use(page); await page.close(); }, + ownerUserPage: async ({ browser }, use) => { + const page = await browser.newPage(); + await ownerUser.login(page); + await use(page); + await page.close(); + }, }); test.describe('Teams Page', () => { @@ -146,64 +168,11 @@ test.describe('Teams Page', () => { }); await test.step('Update email of created team', async () => { - // Edit email - await page.locator('[data-testid="edit-email"]').click(); - await page - .locator('[data-testid="email-input"]') - .fill(teamDetails.updatedEmail); - - const saveEditEmailResponse = page.waitForResponse('/api/v1/teams/*'); - await page.locator('[data-testid="save-edit-email"]').click(); - await saveEditEmailResponse; - - // Reload the page - await page.reload(); - - // Check for updated email - - await expect(page.locator('[data-testid="email-value"]')).toContainText( - teamDetails.updatedEmail - ); + await addEmailTeam(page, teamDetails.updatedEmail); }); await test.step('Add user to created team', async () => { - // Navigate to users tab and add new user - await page.locator('[data-testid="users"]').click(); - - 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(); + await addUserTeam(page, user, userName); }); await test.step('Remove added user from created team', async () => { @@ -904,3 +873,134 @@ test.describe('Teams Page with Data Consumer User', () => { await expect(dataConsumerPage.getByTestId('add-policy')).not.toBeVisible(); }); }); + +test.describe('Teams Page action as Owner of Team', () => { + test.slow(true); + + let teamNoOwner = new TeamClass(); + + test.beforeAll('Setup pre-requests', async ({ browser }) => { + const { apiContext, afterAction } = await performAdminLogin(browser); + await ownerUser.create(apiContext); + await user.create(apiContext); + await policy.create(apiContext, OWNER_TEAM_RULES); + await role.create(apiContext, [policy.responseData.name]); + + const ownerDataEntityReference = { + displayName: ownerUser.responseData.displayName, + fullyQualifiedName: ownerUser.responseData.fullyQualifiedName, + id: ownerUser.responseData.id, + name: ownerUser.responseData.name, + type: 'user', + }; + + const teamID = uuid(); + const team2ID = uuid(); + const team3ID = uuid(); + const team4ID = uuid(); + const teamNoOwnerId = uuid(); + + team = new TeamClass({ + name: `PW%-data-owner-team-${teamID}`, + displayName: `PW Data Owner Team ${teamID}`, + description: 'playwright data consumer team description', + teamType: 'BusinessUnit', + users: [user.responseData.id, ownerDataEntityReference.id], + owners: [ownerDataEntityReference], + defaultRoles: role.responseData.id ? [role.responseData.id] : [], + }); + team2 = new TeamClass({ + name: `PW%-data-owner-team-${team2ID}`, + displayName: `PW Data Owner Team ${team2ID}`, + description: 'playwright data consumer team description', + teamType: 'Department', + users: [user.responseData.id, ownerDataEntityReference.id], + owners: [ownerDataEntityReference], + defaultRoles: role.responseData.id ? [role.responseData.id] : [], + }); + team3 = new TeamClass({ + name: `PW%-data-owner-team-${team3ID}`, + displayName: `PW Data Owner Team ${team3ID}`, + description: 'playwright data consumer team description', + teamType: 'Division', + users: [user.responseData.id, ownerDataEntityReference.id], + owners: [ownerDataEntityReference], + defaultRoles: role.responseData.id ? [role.responseData.id] : [], + }); + team4 = new TeamClass({ + name: `PW%-data-owner-team-${team4ID}`, + displayName: `PW Data Owner Team ${team4ID}`, + description: 'playwright data consumer team description', + teamType: 'Group', + owners: [ownerDataEntityReference], + defaultRoles: role.responseData.id ? [role.responseData.id] : [], + }); + teamNoOwner = new TeamClass({ + name: `PW%-data-owner-team-${teamNoOwnerId}`, + displayName: `PW Data Owner Team ${teamNoOwnerId}`, + description: 'playwright data consumer team description', + teamType: 'BusinessUnit', + }); + + await team.create(apiContext); + await team2.create(apiContext); + await team3.create(apiContext); + await team4.create(apiContext); + await teamNoOwner.create(apiContext); + await domain.create(apiContext); + await dataProduct.create(apiContext); + await afterAction(); + }); + + test.beforeEach('Visit Home Page', async ({ ownerUserPage }) => { + await redirectToHomePage(ownerUserPage); + }); + + test('User as not owner should not have edit/create permission on Team', async ({ + ownerUserPage, + }) => { + await teamNoOwner.visitTeamPage(ownerUserPage); + + await expect(ownerUserPage.getByTestId('manage-button')).not.toBeVisible(); + + await expect(ownerUserPage.getByTestId('add-domain')).not.toBeVisible(); + + await expect(ownerUserPage.getByTestId('edit-owner')).not.toBeVisible(); + + await expect(ownerUserPage.getByTestId('edit-email')).not.toBeVisible(); + + await expect( + ownerUserPage.getByTestId('add-placeholder-button') + ).not.toBeVisible(); + }); + + test(`Add New Team in BusinessUnit Team`, async ({ ownerUserPage }) => { + await executionOnOwnerTeam(ownerUserPage, team, { + domain: domain, + email: teamDetails.updatedEmail, + }); + }); + + test(`Add New Team in Department Team`, async ({ ownerUserPage }) => { + await executionOnOwnerTeam(ownerUserPage, team2, { + domain: domain, + email: teamDetails.updatedEmail, + }); + }); + + test(`Add New Team in Division Team`, async ({ ownerUserPage }) => { + await executionOnOwnerTeam(ownerUserPage, team3, { + domain: domain, + email: teamDetails.updatedEmail, + }); + }); + + test(`Add New User in Group Team`, async ({ ownerUserPage }) => { + await executionOnOwnerGroupTeam(ownerUserPage, team4, { + domain: domain, + email: teamDetails.updatedEmail, + user, + userName, + }); + }); +}); 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 21271f8a5e7..a67a4fc9b7d 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 @@ -15,6 +15,7 @@ import { GlobalSettingOptions } from '../../constant/settings'; import { uuid } from '../../utils/common'; import { settingClick } from '../../utils/sidebar'; import { searchTeam } from '../../utils/team'; +import { EntityReference } from '../entity/Entity.interface'; type ResponseDataType = { name: string; displayName: string; @@ -25,6 +26,7 @@ type ResponseDataType = { users?: string[]; defaultRoles?: string[]; policies?: string[]; + owners?: EntityReference[]; }; export class TeamClass { 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 8c19b0f8e7f..c0d62edc47f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/team.ts @@ -12,10 +12,16 @@ */ import { APIRequestContext, expect, Page } from '@playwright/test'; import { GlobalSettingOptions } from '../constant/settings'; +import { Domain } from '../support/domain/Domain'; 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 { + assignDomain, + descriptionBox, + toastNotification, + uuid, +} from './common'; import { addOwner } from './entity'; import { validateFormNameFieldInput } from './form'; import { settingClick } from './sidebar'; @@ -368,3 +374,129 @@ export const checkTeamTabCount = async (page: Page) => { ) ).toContainText(jsonRes.childrenCount.toString()); }; + +export const addEmailTeam = async (page: Page, email: string) => { + // Edit email + await page.locator('[data-testid="edit-email"]').click(); + await page.locator('[data-testid="email-input"]').fill(email); + + const saveEditEmailResponse = page.waitForResponse('/api/v1/teams/*'); + await page.locator('[data-testid="save-edit-email"]').click(); + await saveEditEmailResponse; + + // Reload the page + await page.reload(); + + await page.waitForLoadState('networkidle'); + + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + // Check for updated email + await expect(page.locator('[data-testid="email-value"]')).toContainText( + email + ); +}; + +export const addUserTeam = async ( + page: Page, + user: UserClass, + userName: string +) => { + // Navigate to users tab and add new user + await page.locator('[data-testid="users"]').click(); + + 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(); +}; + +export const executionOnOwnerTeam = async ( + page: Page, + team: TeamClass, + data: { + domain: Domain; + email: string; + } +) => { + await team.visitTeamPage(page); + + await expect(page.getByTestId('manage-button')).toBeVisible(); + + await expect(page.getByTestId('edit-team-subscription')).toBeVisible(); + await expect(page.getByTestId('edit-team-type-icon')).toBeVisible(); + + await assignDomain(page, data.domain.responseData); + + await addEmailTeam(page, data.email); + + await page.getByTestId('add-placeholder-button').click(); + + const newTeamData = await createTeam(page); + + await page.waitForLoadState('networkidle'); + + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await expect( + page.getByRole('cell', { name: newTeamData.displayName }) + ).toBeVisible(); +}; + +export const executionOnOwnerGroupTeam = async ( + page: Page, + team: TeamClass, + data: { + domain: Domain; + email: string; + user: UserClass; + userName: string; + } +) => { + await team.visitTeamPage(page); + + await expect( + page.getByTestId('team-details-collapse').getByTestId('manage-button') + ).toBeVisible(); + + await expect(page.getByTestId('edit-team-subscription')).toBeVisible(); + await expect(page.getByTestId('edit-team-type-icon')).not.toBeVisible(); + + await assignDomain(page, data.domain.responseData); + + await addEmailTeam(page, data.email); + + await addUserTeam(page, data.user, data.userName); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx index c8599025050..9dbb1e3e92b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Team/TeamDetails/TeamDetailsV1.tsx @@ -50,13 +50,10 @@ import { GlobalSettingOptions, GlobalSettingsMenuCategory, } from '../../../../constants/GlobalSettings.constants'; -import { usePermissionProvider } from '../../../../context/PermissionProvider/PermissionProvider'; -import { ResourceEntity } from '../../../../context/PermissionProvider/PermissionProvider.interface'; import { ERROR_PLACEHOLDER_TYPE } from '../../../../enums/common.enum'; import { EntityAction, EntityType } from '../../../../enums/entity.enum'; import { SearchIndex } from '../../../../enums/search.enum'; import { OwnerType } from '../../../../enums/user.enum'; -import { Operation } from '../../../../generated/entity/policies/policy'; import { Team, TeamType } from '../../../../generated/entity/teams/team'; import { EntityReference as UserTeams, @@ -73,7 +70,6 @@ import { exportTeam, restoreTeam } from '../../../../rest/teamsAPI'; import { Transi18next } from '../../../../utils/CommonUtils'; import { getEntityName } from '../../../../utils/EntityUtils'; import { getSettingPageEntityBreadCrumb } from '../../../../utils/GlobalSettingsUtils'; -import { checkPermission } from '../../../../utils/PermissionsUtils'; import { getSettingsPathWithFqn, getTeamsWithFqnPath, @@ -153,7 +149,6 @@ const TeamDetailsV1 = ({ state: false, leave: false, }; - const { permissions } = usePermissionProvider(); const currentTab = useMemo(() => { if (activeTab) { return activeTab; @@ -211,16 +206,12 @@ const TeamDetailsV1 = ({ navigate({ search: Qs.stringify({ activeTab: key }) }); }; - const { createTeamPermission, editUserPermission } = useMemo(() => { + const { editUserPermission } = useMemo(() => { return { - createTeamPermission: - !isEmpty(permissions) && - checkPermission(Operation.Create, ResourceEntity.TEAM, permissions), editUserPermission: - checkPermission(Operation.EditAll, ResourceEntity.TEAM, permissions) || - checkPermission(Operation.EditUsers, ResourceEntity.TEAM, permissions), + entityPermissions.EditAll || entityPermissions.EditUsers, }; - }, [permissions]); + }, [entityPermissions]); /** * Take user id as input to find out the user data and set it for delete @@ -674,7 +665,7 @@ const TeamDetailsV1 = ({ ) : ( handleAddTeam(true), - permission: createTeamPermission, + permission: entityPermissions.Create, heading: t('label.team-plural'), doc: TEAMS_DOCS, });