Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/web/entity/icon/EditIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import {type ExtendedDynamicIconProps} from 'web/components/icon/createIconComponents';
import useCapabilities from 'web/hooks/useCapabilities';
import useTranslation from 'web/hooks/useTranslation';
import useUserName from 'web/hooks/useUserName';

interface EntityEdit extends WithEntityType {
userCapabilities: {
Expand Down Expand Up @@ -45,6 +46,7 @@
}: EntityEditIconProps<TEntity>) => {
const [_] = useTranslation();
const capabilities = useCapabilities();
const username = useUserName();
if (!isDefined(name)) {
name = getEntityType(entity);
}
Expand All @@ -54,7 +56,9 @@
}

const mayEdit =
capabilities?.mayEdit(name) && entity.userCapabilities.mayEdit(name);
capabilities?.mayEdit(name) &&
(entity.userCapabilities.mayEdit(name) ||
(name === 'user' && entity.name === username));

Check failure on line 61 in src/web/entity/icon/EditIcon.tsx

View workflow job for this annotation

GitHub Actions / Typecheck (24)

Property 'name' does not exist on type 'TEntity'.

Check failure on line 61 in src/web/entity/icon/EditIcon.tsx

View workflow job for this annotation

GitHub Actions / Typecheck (22)

Property 'name' does not exist on type 'TEntity'.

const active = mayEdit && entity.isWritable() && !disabled;

Expand Down
43 changes: 39 additions & 4 deletions src/web/entity/icon/__tests__/EditIcon.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {describe, test, expect, testing} from '@gsa/testing';
import {rendererWith, fireEvent} from 'web/testing';
import Capabilities from 'gmp/capabilities/capabilities';
import Task from 'gmp/models/task';
import User from 'gmp/models/user';
import {createSession} from 'gmp/testing';
import EditIcon from 'web/entity/icon/EditIcon';

describe('Entity EditIcon component tests', () => {
Expand All @@ -17,7 +19,10 @@ describe('Entity EditIcon component tests', () => {
});
const clickHandler = testing.fn();

const {render} = rendererWith({capabilities: caps});
const {render} = rendererWith({
capabilities: caps,
gmp: {session: createSession({username: 'admin'})},
});

const {element} = render(
<EditIcon entity={entity} onClick={clickHandler} />,
Expand All @@ -39,7 +44,10 @@ describe('Entity EditIcon component tests', () => {
});
const clickHandler = testing.fn();

const {render} = rendererWith({capabilities: caps});
const {render} = rendererWith({
capabilities: caps,
gmp: {session: createSession({username: 'admin'})},
});

const {element} = render(
<EditIcon entity={entity} onClick={clickHandler} />,
Expand All @@ -60,7 +68,10 @@ describe('Entity EditIcon component tests', () => {
});
const clickHandler = testing.fn();

const {render} = rendererWith({capabilities: caps});
const {render} = rendererWith({
capabilities: caps,
gmp: {session: createSession({username: 'admin'})},
});

const {element} = render(
<EditIcon entity={entity} onClick={clickHandler} />,
Expand All @@ -82,7 +93,10 @@ describe('Entity EditIcon component tests', () => {
});
const clickHandler = testing.fn();

const {render} = rendererWith({capabilities: caps});
const {render} = rendererWith({
capabilities: caps,
gmp: {session: createSession({username: 'admin'})},
});

const {element} = render(
<EditIcon disabled={true} entity={entity} onClick={clickHandler} />,
Expand All @@ -96,4 +110,25 @@ describe('Entity EditIcon component tests', () => {
expect(element).toHaveAttribute('disabled');
expect(element).toHaveAttribute('data-disabled', 'true');
});

test('should allow the current user to edit their own account', () => {
const caps = new Capabilities(['modify_user']);
const entity = User.fromElement({
_id: 'user-id',
name: 'admin-2',
});
const clickHandler = testing.fn();
const gmp = {session: createSession({username: 'admin-2'})};

const {render} = rendererWith({capabilities: caps, gmp});

const {element} = render(
<EditIcon entity={entity} name="user" onClick={clickHandler} />,
);

fireEvent.click(element);

expect(clickHandler).toHaveBeenCalled();
expect(element).not.toHaveAttribute('disabled');
});
});
2 changes: 1 addition & 1 deletion src/web/pages/users/UsersDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const UsersDialog = ({
* or you have already confirmed that you want to save the user data
* without any role.
*/
if (isDefined(user) && username === user.name) {
if (isDefined(user) && username === user.name && user.isSuperAdmin()) {
/*
* You reach this point only as a Super Admin, when you try to save your
* own personal user data. The confirmation dialog opens. The data can
Expand Down
65 changes: 63 additions & 2 deletions src/web/pages/users/__tests__/UserComponent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ authSettings.set('method:radius_connect', {enabled: false});
const groups = [Group.fromElement({_id: 'group1', name: 'Group 1'})];
const roles = [Role.fromElement({_id: 'role1', name: 'Admin'})];

const createGmp = () => ({
const createGmp = (username = 'admin') => ({
user: {
create: testing.fn().mockResolvedValue({data: {id: 'created'}}),
save: testing.fn().mockResolvedValue({data: {id: 'saved'}}),
Expand All @@ -52,7 +52,7 @@ const createGmp = () => ({
roles: {
getAll: testing.fn().mockResolvedValue({data: roles}),
},
session: createSession({username: 'admin'}),
session: createSession({username}),
});

describe('UserComponent', () => {
Expand Down Expand Up @@ -108,6 +108,67 @@ describe('UserComponent', () => {
});
});

test('should allow a non-superadmin to edit their own user account', async () => {
const gmp = createGmp('admin-2');
const admin = User.fromElement({
_id: 'admin-2-id',
name: 'admin-2',
role: {_id: 'role1', name: 'Admin'},
groups: {
group: [{_id: 'group1', name: 'Special group'}],
},
});
const {render} = rendererWith({gmp, capabilities: true, store: true});

render(
<UserComponent>
{({edit}) => <Button data-testid="open" onClick={() => edit(admin)} />}
</UserComponent>,
);

fireEvent.click(screen.getByTestId('open'));
await screen.findByText('Edit User admin-2');

fireEvent.click(screen.getDialogSaveButton());
await waitFor(() => {
expect(gmp.user.save).toHaveBeenCalled();
});
expect(screen.queryByText('Save Super Admin User')).toBeNull();
});

test('should confirm before a superadmin edits their own user account', async () => {
const gmp = createGmp('superadmin');
const superadmin = User.fromElement({
_id: 'superadmin-id',
name: 'superadmin',
role: {
_id: '9c5a6ec6-6fe2-11e4-8cb6-406186ea4fc5',
name: 'Super Admin',
},
});
const {render} = rendererWith({gmp, capabilities: true, store: true});

render(
<UserComponent>
{({edit}) => (
<Button data-testid="open" onClick={() => edit(superadmin)} />
)}
</UserComponent>,
);

fireEvent.click(screen.getByTestId('open'));
await screen.findByText('Edit User superadmin');

fireEvent.click(screen.getDialogSaveButton());
await screen.findByText('Save Super Admin User');
expect(gmp.user.save).not.toHaveBeenCalled();

fireEvent.click(screen.getByRole('button', {name: 'OK'}));
await waitFor(() => {
expect(gmp.user.save).toHaveBeenCalled();
});
});

test('should report errors while loading the user dialog', async () => {
const gmp = createGmp();
const error = new Error('Unable to load authentication settings');
Expand Down
Loading