Skip to content

Commit 028720f

Browse files
authored
Merge pull request #28 from kdambekalns/feature/edit-roles
FEATURE: Allow to edit roles assigned to users
2 parents 62c78ff + f5c2e19 commit 028720f

File tree

3 files changed

+147
-23
lines changed

3 files changed

+147
-23
lines changed

Classes/Controller/ModuleController.php

Lines changed: 123 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use Neos\Flow\Persistence\Exception\IllegalObjectTypeException;
2020
use Neos\Flow\Security\Account;
2121
use Neos\Flow\Security\AccountRepository;
22+
use Neos\Flow\Security\Policy\PolicyService;
23+
use Neos\Flow\Security\Policy\Role;
2224
use Neos\Neos\Controller\Module\AbstractModuleController;
2325
use Neos\Neos\Domain\Exception as NeosDomainException;
2426
use Neos\Neos\Domain\Model\User;
@@ -35,6 +37,12 @@ class ModuleController extends AbstractModuleController
3537
*/
3638
protected $userService;
3739

40+
/**
41+
* @Flow\Inject
42+
* @var PolicyService
43+
*/
44+
protected $policyService;
45+
3846
/**
3947
* @Flow\Inject
4048
* @var AccountRepository
@@ -96,14 +104,13 @@ public function showAction(User $user): void
96104
/**
97105
* Renders a form for creating a new user
98106
*
99-
* @param User $user
100107
* @return void
101108
*/
102-
public function newAction(User $user = null): void
109+
public function newAction(): void
103110
{
104111
$this->view->assignMultiple([
105112
'currentUser' => $this->currentUser,
106-
'user' => $user
113+
'availableRoles' => $this->getFrontendUserRoles()
107114
]);
108115
}
109116

@@ -113,24 +120,31 @@ public function newAction(User $user = null): void
113120
* @param string $username The user name (ie. account identifier) of the new user
114121
* @param array $password Expects an array in the format array('<password>', '<password confirmation>')
115122
* @param User $user The user to create
116-
* @param \DateTime $expirationDate
123+
* @param \DateTime|null $expirationDate
124+
* @param array $roleIdentifiers Identifiers of roles to assign to account
117125
* @return void
118126
* @Flow\Validate(argumentName="username", type="\Neos\Flow\Validation\Validator\NotEmptyValidator")
119127
* @Flow\Validate(argumentName="username", type="\Neos\Neos\Validation\Validator\UserDoesNotExistValidator")
120128
* @Flow\Validate(argumentName="password", type="\Neos\Neos\Validation\Validator\PasswordValidator", options={ "allowEmpty"=0, "minimum"=1, "maximum"=255 })
121129
*/
122-
public function createAction($username, array $password, User $user, ?\DateTime $expirationDate): void
130+
public function createAction(string $username, array $password, User $user, ?\DateTime $expirationDate, array $roleIdentifiers = []): void
123131
{
124-
$user = $this->userService->addUser($username, $password[0], $user, [self::$roleIdentifier], self::$authenticationProviderName);
132+
// make sure self::$roleIdentifier is always added
133+
$roleIdentifiersToSet = array_unique(array_merge($roleIdentifiers, [self::$roleIdentifier]));
134+
if ($this->onlyFrontendRoles($roleIdentifiersToSet)) {
135+
$user = $this->userService->addUser($username, $password[0], $user, $roleIdentifiersToSet, self::$authenticationProviderName);
125136

126-
if ($expirationDate !== null) {
127-
/** @var Account $account */
128-
$account = $user->getAccounts()->first();
129-
$expirationDate->setTime(0, 0, 0);
130-
$account->setExpirationDate($expirationDate);
131-
}
137+
if ($expirationDate !== null) {
138+
/** @var Account $account */
139+
$account = $user->getAccounts()->first();
140+
$expirationDate->setTime(0, 0);
141+
$account->setExpirationDate($expirationDate);
142+
}
132143

133-
$this->addFlashMessage('The user "%s" has been created.', 'User created', Message::SEVERITY_OK, [htmlspecialchars($username)], 1416225561);
144+
$this->addFlashMessage('The user "%s" has been created.', 'User created', Message::SEVERITY_OK, [htmlspecialchars($username)], 1416225561);
145+
} else {
146+
$this->throwStatus(403, 'Not allowed to assign the given roles');
147+
}
134148
$this->redirect('index');
135149
}
136150

@@ -209,7 +223,8 @@ public function editAccountAction(Account $account): void
209223
$this->view->assignMultiple([
210224
'account' => $account,
211225
'user' => $this->userService->getUser($account->getAccountIdentifier(), $account->getAuthenticationProviderName()),
212-
'expirationDate' => $account->getExpirationDate()
226+
'expirationDate' => $account->getExpirationDate(),
227+
'availableRoles' => $this->getFrontendUserRoles()
213228
]);
214229
} else {
215230
$this->throwStatus(403, 'Not allowed to edit that account');
@@ -220,25 +235,37 @@ public function editAccountAction(Account $account): void
220235
* Update a given account
221236
*
222237
* @param Account $account The account to update
238+
* @param array $roleIdentifiers Identifiers of roles to assign to account
223239
* @param array $password Expects an array in the format array('<password>', '<password confirmation>')
224240
* @Flow\Validate(argumentName="password", type="\Neos\Neos\Validation\Validator\PasswordValidator", options={ "allowEmpty"=1, "minimum"=1, "maximum"=255 })
225241
* @return void
226242
* @throws NeosDomainException
227243
* @throws IllegalObjectTypeException
228244
* @throws UnsupportedRequestTypeException
229245
*/
230-
public function updateAccountAction(Account $account, array $password = []): void
246+
public function updateAccountAction(Account $account, array $roleIdentifiers = [], array $password = []): void
231247
{
232248
if ($this->checkAccount($account)) {
233-
$user = $this->userService->getUser($account->getAccountIdentifier(), $account->getAuthenticationProviderName());
234-
$password = array_shift($password);
235-
if (trim((string)$password) !== '') {
236-
$this->userService->setUserPassword($user, $password);
237-
}
238-
$this->accountRepository->update($account);
249+
// make sure self::$roleIdentifier is always added
250+
$roleIdentifiersToSet = array_unique(array_merge($roleIdentifiers, [self::$roleIdentifier]));
251+
252+
if ($this->onlyFrontendRoles($roleIdentifiersToSet)) {
253+
// add any non-FE roles from the current roles to keep them unchanged
254+
$roleIdentifiersToSet = $this->addExistingNonFrontendUserRoles($roleIdentifiersToSet, $account);
239255

240-
$this->addFlashMessage('The account has been updated.', 'Account updated', Message::SEVERITY_OK);
241-
$this->redirect('edit', null, null, ['user' => $user]);
256+
$this->userService->setRolesForAccount($account, $roleIdentifiersToSet);
257+
$user = $this->userService->getUser($account->getAccountIdentifier(), $account->getAuthenticationProviderName());
258+
$password = array_shift($password);
259+
if (trim((string)$password) !== '') {
260+
$this->userService->setUserPassword($user, $password);
261+
}
262+
$this->accountRepository->update($account);
263+
264+
$this->addFlashMessage('The account has been updated.', 'Account updated');
265+
$this->redirect('edit', null, null, ['user' => $user]);
266+
} else {
267+
$this->throwStatus(403, 'Not allowed to assign the given roles');
268+
}
242269
} else {
243270
$this->throwStatus(403, 'Not allowed to update that account');
244271
}
@@ -293,4 +320,77 @@ protected function checkAccount(Account $account): bool
293320

294321
return false;
295322
}
323+
324+
/**
325+
* Returns all roles that are (indirect) heirs of self::$roleIdentifier
326+
*
327+
* @return Role[] indexed by role identifier
328+
*/
329+
protected function getFrontendUserRoles(): array
330+
{
331+
$availableRoles = $this->policyService->getRoles();
332+
return array_filter($availableRoles, static function (Role $role) {
333+
return $role->getIdentifier() === self::$roleIdentifier || array_key_exists(self::$roleIdentifier, $role->getAllParentRoles());
334+
});
335+
}
336+
337+
/**
338+
* Returns an array with all roles of a user's accounts, including parent roles, the "Everybody" role and the
339+
* "AuthenticatedUser" role, assuming that the user is logged in.
340+
*
341+
* @param Account $account
342+
* @return Role[] indexed by role identifier
343+
*/
344+
private function getAllRolesForAccount(Account $account): array
345+
{
346+
$roles = [];
347+
$accountRoles = $account->getRoles();
348+
foreach ($accountRoles as $currentRole) {
349+
if (!in_array($currentRole, $roles, true)) {
350+
$roles[$currentRole->getIdentifier()] = $currentRole;
351+
}
352+
foreach ($currentRole->getAllParentRoles() as $currentParentRole) {
353+
if (!in_array($currentParentRole, $roles, true)) {
354+
$roles[$currentParentRole->getIdentifier()] = $currentParentRole;
355+
}
356+
}
357+
}
358+
359+
return $roles;
360+
}
361+
362+
/**
363+
* Returns whether it is allowed to add/remove the changed roles.
364+
*
365+
* - Roles not being "FE user roles" can never be set
366+
*
367+
* @param array $roleIdentifiersToSet
368+
* @return bool
369+
*/
370+
private function onlyFrontendRoles(array $roleIdentifiersToSet): bool
371+
{
372+
$frontendUserRoleIdentifiers = array_keys($this->getFrontendUserRoles());
373+
374+
return array_diff($roleIdentifiersToSet, $frontendUserRoleIdentifiers) === [];
375+
}
376+
377+
/**
378+
* Add any non-FE roles from the current $account roles to $roleIdentifiersToSet
379+
*
380+
* @param array $roleIdentifiersToSet
381+
* @param Account $account
382+
* @return array
383+
*/
384+
protected function addExistingNonFrontendUserRoles(array $roleIdentifiersToSet, Account $account): array
385+
{
386+
return array_unique(array_merge(
387+
$roleIdentifiersToSet,
388+
array_keys(array_filter(
389+
$this->getAllRolesForAccount($account),
390+
static function (Role $role) {
391+
return !$role->isAbstract() && !($role->getIdentifier() === self::$roleIdentifier || array_key_exists(self::$roleIdentifier, $role->getAllParentRoles()));
392+
}
393+
))
394+
));
395+
}
296396
}

Resources/Private/Templates/Module/EditAccount.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,18 @@ <h2>{neos:backend.translate(source: 'Modules', package: 'Neos.Neos', id: 'users.
4343
<f:render partial="Module/Shared/FieldValidationResults" arguments="{fieldname: 'expirationDate'}"/>
4444
</div>
4545
</div>
46+
47+
<div class="neos-control-group">
48+
<label class="neos-control-label">{neos:backend.translate(id: 'users.roles', source: 'Modules', package: 'Neos.Neos')}</label>
49+
<f:for each="{availableRoles}" as="role" iteration="rolesIteration">
50+
<div class="neos-controls">
51+
<label for="roles-{rolesIteration.cycle}" class="neos-checkbox" title="{role.packageKey}" data-neos-toggle="tooltip" data-placement="right">
52+
<f:form.checkbox name="roleIdentifiers" multiple="true" value="{role.identifier}" id="roles-{rolesIteration.cycle}" checked="{f:security.ifHasRole(role: role, account: account, then: true, else: false)}" disabled="{role.identifier} == 'Flowpack.Neos.FrontendLogin:User'}"/><span></span>
53+
{role.name}
54+
</label>
55+
</div>
56+
</f:for>
57+
</div>
4658
</fieldset>
4759
</div>
4860

Resources/Private/Templates/Module/New.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ <h2>{neos:backend.translate(id: 'user.new.subtitle', value: 'Create a new user',
4242
<f:render partial="Module/Shared/FieldValidationResults" arguments="{fieldname: 'expirationDate'}"/>
4343
</div>
4444
</div>
45+
46+
<div class="neos-control-group">
47+
<label class="neos-control-label">{neos:backend.translate(id: 'users.roles', source: 'Modules', package: 'Neos.Neos')}</label>
48+
<f:for each="{availableRoles}" as="role" iteration="rolesIteration">
49+
<div class="neos-controls">
50+
<label for="roles-{rolesIteration.cycle}" class="neos-checkbox" title="{role.packageKey}" data-neos-toggle="tooltip" data-placement="right">
51+
<f:form.checkbox name="roleIdentifiers" multiple="true" value="{role.identifier}" id="roles-{rolesIteration.cycle}" checked="{f:if(condition: '{role.identifier} == \'Flowpack.Neos.FrontendLogin:User\'', then: true, else: false)}" disabled="{role.identifier} == 'Flowpack.Neos.FrontendLogin:User'}"/><span></span>
52+
{role.name}
53+
</label>
54+
</div>
55+
</f:for>
56+
</div>
4557
</fieldset>
4658

4759
<fieldset class="neos-span5 neos-offset1">

0 commit comments

Comments
 (0)