Tighten organisation admin permissions & password reset workflow

This commit is contained in:
2023-09-07 11:53:39 +01:00
parent f3af94a285
commit dead350676
7 changed files with 200 additions and 4 deletions

View File

@@ -9,6 +9,7 @@
namespace UserFrosting\Sprinkle\Organisations\Controller;
use Carbon\Carbon;
use Illuminate\Database\Capsule\Manager as Capsule;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
@@ -918,6 +919,81 @@ class OrganisationMembersController extends SimpleController
return $response->withRedirect($this->ci->router->pathFor('uri_organisation', ['slug' => $organisation->slug]));
}
/**
* Processes the request to send a user a password reset email.
*
* Processes the request from the user update form, checking that:
* 1. The target user's new email address, if specified, is not already in use;
* 2. The logged-in user has the necessary permissions to update the posted field(s);
* 3. We're not trying to disable the master account;
* 4. The submitted data is valid.
* This route requires authentication.
*
* Request type: POST
*
* @param Request $request
* @param Response $response
* @param string[] $args
*
* @throws NotFoundException If user is not found
* @throws ForbiddenException If user is not authorized to access page
*/
public function createPasswordReset(Request $request, Response $response, array $args)
{
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
$authorizer = $this->ci->authorizer;
/** @var \UserFrosting\Support\Repository\Repository $config */
$config = $this->ci->config;
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
$currentUser = $this->ci->currentUser;
/** @var \UserFrosting\Sprinkle\Core\Alert\AlertStream $ms */
$ms = $this->ci->alerts;
// Get the username from the URL
$user = $this->getUserFromParams($args);
if (!$user) {
throw new NotFoundException();
}
// Access-controlled resource - check that currentUser has permission to edit "password" for this user
if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
'user' => $user,
'fields' => ['password'],
])) {
throw new ForbiddenException();
}
// Begin transaction - DB will be rolled back if an exception occurs
Capsule::transaction(function () use ($user, $config) {
// Create a password reset and shoot off an email
$passwordReset = $this->ci->repoPasswordReset->create($user, $config['password_reset.timeouts.reset']);
// Create and send welcome email with password set link
$message = new TwigMailMessage($this->ci->view, 'mail/password-reset.html.twig');
$message->from($config['address_book.admin'])
->addEmailRecipient(new EmailRecipient($user->email, $user->full_name))
->addParams([
'user' => $user,
'token' => $passwordReset->getToken(),
'request_date' => Carbon::now()->format('Y-m-d H:i:s'),
]);
$this->ci->mailer->send($message);
});
$ms->addMessageTranslated('success', 'ORGANISATION.MEMBER.PASSWORD_LINK_SENT', [
'email' => $user->email,
]);
return $response->withJson([], 200);
}
/**
* Returns a list of organisation members.
@@ -1359,6 +1435,60 @@ class OrganisationMembersController extends SimpleController
]);
}
/**
* Renders the modal form for sending a password reset to a member.
*
* This does NOT render a complete page. Instead, it renders the HTML for the form, which can be embedded in other pages.
* This page requires authentication.
*
* Request type: GET
*
* @param Request $request
* @param Response $response
* @param string[] $args
*
* @throws NotFoundException If user is not found
* @throws ForbiddenException If user is not authorized to access page
*/
public function getModalResetPassword(Request $request, Response $response, array $args)
{
/** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager $authorizer */
$authorizer = $this->ci->authorizer;
/** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */
$currentUser = $this->ci->currentUser;
/** @var \UserFrosting\Support\Repository\Repository $config */
$config = $this->ci->config;
// GET parameters
$params = $request->getQueryParams();
$user = $this->getUserFromParams($params);
$organisation = $this->getOrganisationFromParams($params);
// Check organisation & user exists
if (!$organisation || !$user) {
throw new NotFoundException();
}
// Access-controlled resource - check that currentUser has permission to edit "password" field for this user
if (!$authorizer->checkAccess($currentUser, 'update_user_field', [
'organisation' => $organisation,
'user' => $user,
'fields' => ['password'],
])) {
throw new ForbiddenException();
}
return $this->ci->view->render($response, 'modals/member-reset-password.html.twig', [
'user' => $user,
'organisation' => $organisation,
'page' => [],
]);
}
/**
* Send approval email for specified organisation and confirmation to user.

View File

@@ -241,7 +241,7 @@ class OrganisationPermissions extends BaseSeed
'update_user_field_organisation' => new Permission([
'slug' => 'update_user_field',
'name' => 'Edit organisation user',
'conditions' => "has_matching_organisation(self.id,user.id,true) && !is_master(user.id) && !has_role(user.id,{$roleIds['site-admin']}) && (!has_role(user.id,{$roleIds['organisations-admin']}) || equals_num(self.id,user.id)) && subset(fields,['name','email','locale','flag_enabled','flag_verified','password'])",
'conditions' => "has_matching_organisation(self.id,user.id,1) && !is_master(user.id) && !has_role(user.id,{$roleIds['site-admin']}) && (!has_role(user.id,{$roleIds['organisations-admin']}) || equals_num(self.id,user.id)) && subset(fields,['name','email','locale','flag_enabled','flag_verified','password'])",
'description' => 'Edit users in your own organisation who are not Site or (global) Organisation Administrators, except yourself.',
]),
];