diff --git a/src/web/entity/icon/EditIcon.tsx b/src/web/entity/icon/EditIcon.tsx index 2f69b84d2c..9ec1dd0035 100644 --- a/src/web/entity/icon/EditIcon.tsx +++ b/src/web/entity/icon/EditIcon.tsx @@ -14,6 +14,7 @@ import {EditIcon} from 'web/components/icon'; 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: { @@ -45,6 +46,7 @@ const EntityEditIcon = ({ }: EntityEditIconProps) => { const [_] = useTranslation(); const capabilities = useCapabilities(); + const username = useUserName(); if (!isDefined(name)) { name = getEntityType(entity); } @@ -54,7 +56,9 @@ const EntityEditIcon = ({ } const mayEdit = - capabilities?.mayEdit(name) && entity.userCapabilities.mayEdit(name); + capabilities?.mayEdit(name) && + (entity.userCapabilities.mayEdit(name) || + (name === 'user' && entity.name === username)); const active = mayEdit && entity.isWritable() && !disabled; diff --git a/src/web/entity/icon/__tests__/EditIcon.test.jsx b/src/web/entity/icon/__tests__/EditIcon.test.jsx index 4a526ecd44..f39bbe168e 100644 --- a/src/web/entity/icon/__tests__/EditIcon.test.jsx +++ b/src/web/entity/icon/__tests__/EditIcon.test.jsx @@ -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', () => { @@ -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( , @@ -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( , @@ -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( , @@ -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( , @@ -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( + , + ); + + fireEvent.click(element); + + expect(clickHandler).toHaveBeenCalled(); + expect(element).not.toHaveAttribute('disabled'); + }); }); diff --git a/src/web/pages/users/UsersDialog.tsx b/src/web/pages/users/UsersDialog.tsx index 6cd87969ff..80250b837d 100644 --- a/src/web/pages/users/UsersDialog.tsx +++ b/src/web/pages/users/UsersDialog.tsx @@ -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 diff --git a/src/web/pages/users/__tests__/UserComponent.test.tsx b/src/web/pages/users/__tests__/UserComponent.test.tsx index b40c6242ab..fe79df5614 100644 --- a/src/web/pages/users/__tests__/UserComponent.test.tsx +++ b/src/web/pages/users/__tests__/UserComponent.test.tsx @@ -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'}}), @@ -52,7 +52,7 @@ const createGmp = () => ({ roles: { getAll: testing.fn().mockResolvedValue({data: roles}), }, - session: createSession({username: 'admin'}), + session: createSession({username}), }); describe('UserComponent', () => { @@ -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( + + {({edit}) =>