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 <sonikashah94@gmail.com>
Co-authored-by: sonika-shah <58761340+sonika-shah@users.noreply.github.com>
This commit is contained in:
Ashish Gupta 2024-12-13 23:41:40 +05:30 committed by GitHub
parent ea5a246a44
commit 9e6078f654
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 315 additions and 23 deletions

View File

@ -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<Team, TeamRepository> {
@Context SecurityContext securityContext,
@PathParam("teamId") UUID teamId,
List<EntityReference> 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<Team, TeamRepository> {
@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))

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = ({
<Tooltip
placement="left"
title={
permission.EditAll
editUserPermission
? t('label.remove')
: t('message.no-permission-for-action')
}>
<Button
data-testid="remove-user-btn"
disabled={!permission.EditAll}
disabled={!editUserPermission}
icon={
<IconRemove height={16} name={t('label.remove')} width={16} />
}
@ -235,7 +240,7 @@ export const UserTab = ({
return tabColumns.filter((column) =>
column.key === 'actions' ? !isTeamDeleted : true
);
}, [handleRemoveClick, permission, isTeamDeleted]);
}, [handleRemoveClick, editUserPermission, isTeamDeleted]);
const sortedUser = useMemo(() => orderBy(users, ['name'], 'asc'), [users]);
@ -329,10 +334,10 @@ export const UserTab = ({
<Button
ghost
className={classNames({
'p-x-lg': permission.EditAll && !isTeamDeleted,
'p-x-lg': editUserPermission && !isTeamDeleted,
})}
data-testid="add-new-user"
disabled={!permission.EditAll || isTeamDeleted}
disabled={!editUserPermission || isTeamDeleted}
icon={<PlusOutlined />}
type="primary">
{t('label.add')}
@ -352,7 +357,7 @@ export const UserTab = ({
}
className="mt-0-important"
heading={t('label.user')}
permission={permission.EditAll}
permission={editUserPermission}
type={ERROR_PLACEHOLDER_TYPE.ASSIGN}
/>
) : (
@ -382,7 +387,7 @@ export const UserTab = ({
{!currentTeam.deleted && isGroupType && (
<Col>
<Space>
{users.length > 0 && permission.EditAll && (
{users.length > 0 && editUserPermission && (
<UserSelectableList
hasPermission
selectedUsers={currentTeam?.users ?? []}

View File

@ -164,7 +164,6 @@ describe('Test Teams Page', () => {
{
fields: [
'users',
'userCount',
'defaultRoles',
'policies',
'childrenCount',

View File

@ -244,7 +244,6 @@ const TeamsPage = () => {
const data = await getTeamByName(name, {
fields: [
TabSpecificField.USERS,
TabSpecificField.USER_COUNT,
TabSpecificField.DEFAULT_ROLES,
TabSpecificField.POLICIES,
TabSpecificField.CHILDREN_COUNT,