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)
This commit is contained in:
Ashish Gupta 2025-09-19 20:48:43 +05:30
parent 41ec8d18de
commit 16b03eca4e
5 changed files with 308 additions and 73 deletions

View File

@ -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',

View File

@ -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,
});
});
});

View File

@ -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 {

View File

@ -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);
};

View File

@ -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 = ({
</ErrorPlaceHolder>
) : (
<TeamHierarchy
createTeamPermission={createTeamPermission}
createTeamPermission={entityPermissions.Create}
currentTeam={currentTeam}
data={childTeamList}
handleAddTeamButtonClick={handleAddTeamButtonClick}
@ -693,7 +684,7 @@ const TeamDetailsV1 = ({
currentTeam,
childTeamList,
showDeletedTeam,
createTeamPermission,
entityPermissions.Create,
isFetchingAllTeamAdvancedDetails,
onTeamExpand,
handleAddTeamButtonClick,
@ -1112,7 +1103,7 @@ const TeamDetailsV1 = ({
if (isEmpty(currentTeam)) {
return fetchErrorPlaceHolder({
onClick: () => handleAddTeam(true),
permission: createTeamPermission,
permission: entityPermissions.Create,
heading: t('label.team-plural'),
doc: TEAMS_DOCS,
});