user password ref
This commit is contained in:
@@ -40,7 +40,6 @@ use Espo\Entities\PasswordChangeRequest;
|
||||
class PasswordChangeRequests implements Cleanup
|
||||
{
|
||||
private Config $config;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
|
||||
private string $cleanupPeriod = '30 days';
|
||||
|
||||
@@ -36,17 +36,23 @@ use Espo\Core\Exceptions\BadRequest;
|
||||
|
||||
use Espo\Services\User as Service;
|
||||
|
||||
use Espo\Core\{
|
||||
Controllers\Record,
|
||||
Api\Request,
|
||||
Select\SearchParams,
|
||||
Select\Where\Item as WhereItem,
|
||||
};
|
||||
use Espo\Core\Api\Request;
|
||||
use Espo\Core\Controllers\Record;
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Core\Password\Recovery;
|
||||
use Espo\Core\Select\SearchParams;
|
||||
use Espo\Core\Select\Where\Item as WhereItem;
|
||||
|
||||
use Espo\Tools\UserSecurity\Password\Service as PasswordService;
|
||||
use stdClass;
|
||||
|
||||
class User extends Record
|
||||
{
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function getActionAcl(Request $request): stdClass
|
||||
{
|
||||
$userId = $request->getQueryParam('id');
|
||||
@@ -55,40 +61,53 @@ class User extends Record
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
if (!$this->user->isAdmin() && $this->user->getId() !== $userId) {
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
$this->user->getId() !== $userId
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$user = $this->getEntityManager()->getEntity('User', $userId);
|
||||
$user = $this->entityManager->getEntityById(\Espo\Entities\User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (empty($user)) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
return $this->getAclManager()->getMapData($user);
|
||||
return $this->aclManager->getMapData($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionChangeOwnPassword(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
$password = $data->password ?? null;
|
||||
$currentPassword = $data->currentPassword ?? null;
|
||||
|
||||
if (
|
||||
!property_exists($data, 'password') ||
|
||||
!property_exists($data, 'currentPassword')
|
||||
!is_string($password) ||
|
||||
!is_string($currentPassword)
|
||||
) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getUserService()->changePassword(
|
||||
$this->user->getId(),
|
||||
$data->password,
|
||||
true,
|
||||
$data->currentPassword
|
||||
);
|
||||
$this->getPasswordService()->changePasswordWithCheck($this->user->getId(), $password, $currentPassword);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionChangePasswordByRequest(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -97,31 +116,41 @@ class User extends Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
return $this->getUserService()->changePasswordByRequest($data->requestId, $data->password);
|
||||
$url = $this->getPasswordService()->changePasswordByRecovery($data->requestId, $data->password);
|
||||
|
||||
return (object) [
|
||||
'url' => $url,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
public function postActionPasswordChangeRequest(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
|
||||
if (empty($data->userName) || empty($data->emailAddress)) {
|
||||
$userName = $data->userName ?? null;
|
||||
$emailAddress = $data->emailAddress ?? null;
|
||||
$url = $data->url ?? null;
|
||||
|
||||
if (!$userName || !$emailAddress) {
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$userName = $data->userName;
|
||||
$emailAddress = $data->emailAddress;
|
||||
|
||||
$url = null;
|
||||
|
||||
if (!empty($data->url)) {
|
||||
$url = $data->url;
|
||||
}
|
||||
|
||||
$this->getUserService()->passwordChangeRequest($userName, $emailAddress, $url);
|
||||
$this->injectableFactory
|
||||
->create(Recovery::class)
|
||||
->request($emailAddress, $userName, $url);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionGenerateNewApiKey(Request $request): stdClass
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -139,6 +168,14 @@ class User extends Record
|
||||
->getValueMap();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws SendingError
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function postActionGenerateNewPassword(Request $request): bool
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
@@ -151,11 +188,17 @@ class User extends Record
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->getUserService()->generateNewPasswordForUser($data->id);
|
||||
$this->getPasswordService()->generateAndSendNewPasswordForUser($data->id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function postActionSendPasswordChangeLink(Request $request): bool
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
@@ -168,7 +211,7 @@ class User extends Record
|
||||
throw new BadRequest();
|
||||
}
|
||||
|
||||
$this->getUserService()->sendPasswordChangeLink($id);
|
||||
$this->getPasswordService()->createAndSendPasswordRecovery($id);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -210,6 +253,11 @@ class User extends Record
|
||||
);
|
||||
}
|
||||
|
||||
private function getPasswordService(): PasswordService
|
||||
{
|
||||
return $this->injectableFactory->create(PasswordService::class);
|
||||
}
|
||||
|
||||
private function getUserService(): Service
|
||||
{
|
||||
/** @var Service */
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Password\Jobs;
|
||||
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Entities\User;
|
||||
|
||||
use Espo\Core\Job\Job;
|
||||
@@ -37,20 +38,23 @@ use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use Espo\Services\User as Service;
|
||||
use Espo\Tools\UserSecurity\Password\Service as PasswordService;
|
||||
|
||||
class SendAccessInfo implements Job
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
private PasswordService $passwordService;
|
||||
|
||||
private Service $service;
|
||||
|
||||
public function __construct(EntityManager $entityManager, Service $service)
|
||||
public function __construct(EntityManager $entityManager, PasswordService $passwordService)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->service = $service;
|
||||
$this->passwordService = $passwordService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SendingError
|
||||
* @throws Error
|
||||
*/
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$userId = $data->getTargetId();
|
||||
@@ -59,12 +63,13 @@ class SendAccessInfo implements Job
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new Error("User '{$userId}' not found.");
|
||||
}
|
||||
|
||||
$this->service->sendAccessInfoNew($user);
|
||||
$this->passwordService->sendAccessInfoForNewUser($user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Core\Password;
|
||||
|
||||
use Espo\Core\Job\JobSchedulerFactory;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\Core\Utils\Json;
|
||||
|
||||
@@ -44,41 +45,31 @@ use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\Field\DateTime;
|
||||
|
||||
use Espo\Core\{
|
||||
Authentication\Logins\Espo as EspoLogin,
|
||||
ORM\EntityManager,
|
||||
Utils\Config,
|
||||
Mail\EmailSender,
|
||||
Htmlizer\HtmlizerFactory as HtmlizerFactory,
|
||||
Utils\TemplateFileManager,
|
||||
Utils\Log,
|
||||
Job\QueueName,
|
||||
};
|
||||
use Espo\Core\Authentication\Logins\Espo as EspoLogin;
|
||||
use Espo\Core\Htmlizer\HtmlizerFactory as HtmlizerFactory;
|
||||
use Espo\Core\Job\QueueName;
|
||||
use Espo\Core\Mail\EmailSender;
|
||||
use Espo\Core\ORM\EntityManager;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Utils\TemplateFileManager;
|
||||
use Espo\Tools\UserSecurity\Password\Jobs\RemoveRecoveryRequest;
|
||||
|
||||
class Recovery
|
||||
{
|
||||
/**
|
||||
* Milliseconds.
|
||||
*/
|
||||
/** Milliseconds. */
|
||||
private const REQUEST_DELAY = 3000;
|
||||
|
||||
private const REQUEST_LIFETIME = '3 hours';
|
||||
|
||||
private const NEW_USER_REQUEST_LIFETIME = '2 days';
|
||||
|
||||
private const EXISTING_USER_REQUEST_LIFETIME = '2 days';
|
||||
|
||||
protected EntityManager $entityManager;
|
||||
|
||||
protected Config $config;
|
||||
|
||||
protected EmailSender $emailSender;
|
||||
|
||||
protected HtmlizerFactory $htmlizerFactory;
|
||||
|
||||
protected TemplateFileManager $templateFileManager;
|
||||
|
||||
private EntityManager $entityManager;
|
||||
private Config $config;
|
||||
private EmailSender $emailSender;
|
||||
private HtmlizerFactory $htmlizerFactory;
|
||||
private TemplateFileManager $templateFileManager;
|
||||
private Log $log;
|
||||
private JobSchedulerFactory $jobSchedulerFactory;
|
||||
|
||||
public function __construct(
|
||||
EntityManager $entityManager,
|
||||
@@ -86,7 +77,8 @@ class Recovery
|
||||
EmailSender $emailSender,
|
||||
HtmlizerFactory $htmlizerFactory,
|
||||
TemplateFileManager $templateFileManager,
|
||||
Log $log
|
||||
Log $log,
|
||||
JobSchedulerFactory $jobSchedulerFactory
|
||||
) {
|
||||
$this->entityManager = $entityManager;
|
||||
$this->config = $config;
|
||||
@@ -94,8 +86,14 @@ class Recovery
|
||||
$this->htmlizerFactory = $htmlizerFactory;
|
||||
$this->templateFileManager = $templateFileManager;
|
||||
$this->log = $log;
|
||||
$this->jobSchedulerFactory = $jobSchedulerFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function getRequest(string $id): PasswordChangeRequest
|
||||
{
|
||||
$config = $this->config;
|
||||
@@ -137,6 +135,11 @@ class Recovery
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function request(string $emailAddress, string $userName, ?string $url): bool
|
||||
{
|
||||
$config = $this->config;
|
||||
@@ -148,7 +151,7 @@ class Recovery
|
||||
}
|
||||
|
||||
$user = $this->entityManager
|
||||
->getRDBRepository('User')
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->where([
|
||||
'userName' => $userName,
|
||||
'emailAddress' => $emailAddress,
|
||||
@@ -269,6 +272,9 @@ class Recovery
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function createAndSendRequestForExistingUser(User $user, ?string $url = null): PasswordChangeRequest
|
||||
{
|
||||
$this->checkUser($user);
|
||||
@@ -283,7 +289,8 @@ class Recovery
|
||||
|
||||
$this->entityManager->saveEntity($entity);
|
||||
|
||||
$lifetime = $this->config->get('passwordChangeRequestExistingUserLifetime') ??
|
||||
$lifetime =
|
||||
$this->config->get('passwordChangeRequestExistingUserLifetime') ??
|
||||
self::EXISTING_USER_REQUEST_LIFETIME;
|
||||
|
||||
$this->createCleanupRequestJob($entity->getId(), $lifetime);
|
||||
@@ -312,15 +319,17 @@ class Recovery
|
||||
|
||||
private function createCleanupRequestJob(string $id, string $lifetime): void
|
||||
{
|
||||
$this->entityManager->createEntity('Job', [
|
||||
'serviceName' => 'User',
|
||||
'methodName' => 'removeChangePasswordRequestJob',
|
||||
'data' => ['id' => $id],
|
||||
'executeTime' => DateTime::createNow()
|
||||
->modify('+' . $lifetime)
|
||||
->getString(),
|
||||
'queue' => QueueName::Q1,
|
||||
]);
|
||||
$this->jobSchedulerFactory
|
||||
->create()
|
||||
->setClassName(RemoveRecoveryRequest::class)
|
||||
->setData(['id' => $id])
|
||||
->setTime(
|
||||
DateTime::createNow()
|
||||
->modify('+' . $lifetime)
|
||||
->getDateTime()
|
||||
)
|
||||
->setQueue(QueueName::Q1)
|
||||
->schedule();
|
||||
}
|
||||
|
||||
private function getDelay(): int
|
||||
|
||||
@@ -29,12 +29,31 @@
|
||||
|
||||
namespace Espo\Entities;
|
||||
|
||||
class PasswordChangeRequest extends \Espo\Core\ORM\Entity
|
||||
use Espo\Core\ORM\Entity;
|
||||
use UnexpectedValueException;
|
||||
|
||||
class PasswordChangeRequest extends Entity
|
||||
{
|
||||
public const ENTITY_TYPE = 'PasswordChangeRequest';
|
||||
|
||||
public function getUrl(): ?string
|
||||
{
|
||||
return $this->get('url');
|
||||
}
|
||||
|
||||
public function getRequestId(): string
|
||||
{
|
||||
return $this->get('requestId');
|
||||
}
|
||||
|
||||
public function getUserId(): string
|
||||
{
|
||||
$userId = $this->get('userId');
|
||||
|
||||
if ($userId === null) {
|
||||
throw new UnexpectedValueException();
|
||||
}
|
||||
|
||||
return $userId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,33 +29,27 @@
|
||||
|
||||
namespace Espo\Services;
|
||||
|
||||
use Espo\Core\ApplicationUser;
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Entities\Team as TeamEntity;
|
||||
use Espo\Entities\User as UserEntity;
|
||||
use Espo\Entities\Email as EmailEntity;
|
||||
use Espo\Entities\PasswordChangeRequest;
|
||||
use Espo\Entities\Portal as PortalEntity;
|
||||
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\Repositories\Portal as PortalRepository;
|
||||
|
||||
use Espo\Core\Mail\Sender;
|
||||
|
||||
use Espo\Core\{Acl\Cache\Clearer as AclCacheClearer,
|
||||
Exceptions\Forbidden,
|
||||
Exceptions\Error,
|
||||
Exceptions\NotFound,
|
||||
Exceptions\BadRequest,
|
||||
Utils\Util,
|
||||
Utils\PasswordHash,
|
||||
Utils\ApiKey as ApiKeyUtil,
|
||||
Di,
|
||||
Password\Recovery,
|
||||
Record\CreateParams,
|
||||
Record\UpdateParams,
|
||||
Record\DeleteParams};
|
||||
|
||||
use Espo\Core\Acl\Cache\Clearer as AclCacheClearer;
|
||||
use Espo\Core\Di;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Record\CreateParams;
|
||||
use Espo\Core\Record\DeleteParams;
|
||||
use Espo\Core\Record\UpdateParams;
|
||||
use Espo\Core\Utils\ApiKey as ApiKeyUtil;
|
||||
use Espo\Core\Utils\PasswordHash;
|
||||
use Espo\Core\Utils\Util;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
use Espo\Tools\UserSecurity\Password\Checker as PasswordChecker;
|
||||
use Espo\Tools\UserSecurity\Password\Sender as PasswordSender;
|
||||
use Espo\Tools\UserSecurity\Password\Service as PasswordService;
|
||||
use stdClass;
|
||||
use Exception;
|
||||
|
||||
@@ -64,23 +58,12 @@ use Exception;
|
||||
*/
|
||||
class User extends Record implements
|
||||
|
||||
Di\TemplateFileManagerAware,
|
||||
Di\EmailSenderAware,
|
||||
Di\HtmlizerFactoryAware,
|
||||
Di\FileManagerAware,
|
||||
Di\DataManagerAware
|
||||
{
|
||||
use Di\TemplateFileManagerSetter;
|
||||
use Di\EmailSenderSetter;
|
||||
use Di\HtmlizerFactorySetter;
|
||||
use Di\FileManagerSetter;
|
||||
use Di\DataManagerSetter;
|
||||
|
||||
private const AUTHENTICATION_METHOD_ESPO = 'Espo';
|
||||
private const AUTHENTICATION_METHOD_HMAC = 'Hmac';
|
||||
|
||||
private const ID_SYSTEM = 'system';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
@@ -105,7 +88,7 @@ class User extends Record implements
|
||||
*/
|
||||
public function getEntity(string $id): ?Entity
|
||||
{
|
||||
if ($id === self::ID_SYSTEM) {
|
||||
if ($id === ApplicationUser::SYSTEM_USER_ID) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -123,181 +106,7 @@ class User extends Record implements
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function changePassword(
|
||||
string $userId,
|
||||
string $password,
|
||||
bool $checkCurrentPassword = false,
|
||||
?string $currentPassword = null
|
||||
): void {
|
||||
|
||||
/** @var ?UserEntity $user */
|
||||
$user = $this->entityManager->getEntityById(UserEntity::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if ($user->isSuperAdmin() && !$this->user->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$authenticationMethod = $this->config->get('authenticationMethod', self::AUTHENTICATION_METHOD_ESPO);
|
||||
|
||||
if (!$user->isAdmin() && $authenticationMethod !== self::AUTHENTICATION_METHOD_ESPO) {
|
||||
throw new Forbidden("Authentication method is not `Espo`.");
|
||||
}
|
||||
|
||||
if (empty($password)) {
|
||||
throw new Error("Password can't be empty.");
|
||||
}
|
||||
|
||||
if ($checkCurrentPassword) {
|
||||
$u = $this->entityManager
|
||||
->getRDBRepository(UserEntity::ENTITY_TYPE)
|
||||
->where([
|
||||
'id' => $user->getId(),
|
||||
'password' => $this->createPasswordHashUtil()->hash($currentPassword ?? ''),
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$u) {
|
||||
throw new Forbidden("Wrong password.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->checkPasswordStrength($password)) {
|
||||
throw new Forbidden("Password is weak.");
|
||||
}
|
||||
|
||||
$validLength = $this->fieldValidationManager
|
||||
->check($user, 'password', 'maxLength', (object) ['password' => $password]);
|
||||
|
||||
if (!$validLength) {
|
||||
throw new Forbidden("Password exceeds max length.");
|
||||
}
|
||||
|
||||
$user->set('password', $this->hashPassword($password));
|
||||
|
||||
$this->entityManager->saveEntity($user);
|
||||
}
|
||||
|
||||
public function checkPasswordStrength(string $password): bool
|
||||
{
|
||||
$minLength = $this->config->get('passwordStrengthLength');
|
||||
|
||||
if ($minLength) {
|
||||
if (mb_strlen($password) < $minLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$requiredLetterCount = $this->config->get('passwordStrengthLetterCount');
|
||||
|
||||
if ($requiredLetterCount) {
|
||||
$letterCount = 0;
|
||||
|
||||
foreach (str_split($password) as $c) {
|
||||
if (ctype_alpha($c)) {
|
||||
$letterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($letterCount < $requiredLetterCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$requiredNumberCount = $this->config->get('passwordStrengthNumberCount');
|
||||
|
||||
if ($requiredNumberCount) {
|
||||
$numberCount = 0;
|
||||
|
||||
foreach (str_split($password) as $c) {
|
||||
if (is_numeric($c)) {
|
||||
$numberCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($numberCount < $requiredNumberCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$bothCases = $this->config->get('passwordStrengthBothCases');
|
||||
|
||||
if ($bothCases) {
|
||||
$ucCount = 0;
|
||||
$lcCount = 0;
|
||||
|
||||
foreach (str_split($password) as $c) {
|
||||
if (ctype_alpha($c) && $c === mb_strtoupper($c)) {
|
||||
$ucCount++;
|
||||
}
|
||||
|
||||
if (ctype_alpha($c) && $c === mb_strtolower($c)) {
|
||||
$lcCount++;
|
||||
}
|
||||
}
|
||||
if (!$ucCount || !$lcCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createRecoveryService(): Recovery
|
||||
{
|
||||
return $this->injectableFactory->create(Recovery::class);
|
||||
}
|
||||
|
||||
public function passwordChangeRequest(string $userName, string $emailAddress, ?string $url = null): void
|
||||
{
|
||||
$this->createRecoveryService()->request($emailAddress, $userName, $url);
|
||||
}
|
||||
|
||||
public function changePasswordByRequest(string $requestId, string $password): stdClass
|
||||
{
|
||||
$recovery = $this->createRecoveryService();
|
||||
|
||||
$request = $recovery->getRequest($requestId);
|
||||
|
||||
$userId = $request->get('userId');
|
||||
|
||||
if (!$userId) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
$this->changePassword($userId, $password);
|
||||
|
||||
$recovery->removeRequest($requestId);
|
||||
|
||||
return (object) [
|
||||
'url' => $request->get('url'),
|
||||
];
|
||||
}
|
||||
|
||||
public function removeChangePasswordRequestJob(stdClass $data): void
|
||||
{
|
||||
if (empty($data->id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $data->id;
|
||||
|
||||
$p = $this->entityManager->getEntity(PasswordChangeRequest::ENTITY_TYPE, $id);
|
||||
|
||||
if ($p) {
|
||||
$this->entityManager->removeEntity($p);
|
||||
}
|
||||
}
|
||||
|
||||
protected function hashPassword(string $password): string
|
||||
private function hashPassword(string $password): string
|
||||
{
|
||||
$passwordHash = $this->injectableFactory->create(PasswordHash::class);
|
||||
|
||||
@@ -332,7 +141,7 @@ class User extends Record implements
|
||||
}
|
||||
|
||||
if ($newPassword !== null) {
|
||||
if (!$this->checkPasswordStrength($newPassword)) {
|
||||
if (!$this->createPasswordChecker()->checkStrength($newPassword)) {
|
||||
throw new Forbidden("Password is weak.");
|
||||
}
|
||||
|
||||
@@ -355,7 +164,7 @@ class User extends Record implements
|
||||
return $user;
|
||||
}
|
||||
|
||||
$this->sendAccessInfoNew($user);
|
||||
$this->getPasswordService()->sendAccessInfoForNewUser($user);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
$this->log->error("Could not send user access info. " . $e->getMessage());
|
||||
@@ -366,7 +175,7 @@ class User extends Record implements
|
||||
|
||||
public function update(string $id, stdClass $data, UpdateParams $params): Entity
|
||||
{
|
||||
if ($id === self::ID_SYSTEM) {
|
||||
if ($id === ApplicationUser::SYSTEM_USER_ID) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -375,7 +184,7 @@ class User extends Record implements
|
||||
if (property_exists($data, 'password')) {
|
||||
$newPassword = $data->password;
|
||||
|
||||
if (!$this->checkPasswordStrength($newPassword)) {
|
||||
if (!$this->createPasswordChecker()->checkStrength($newPassword)) {
|
||||
throw new Forbidden("Password is weak.");
|
||||
}
|
||||
|
||||
@@ -403,6 +212,22 @@ class User extends Record implements
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function getPasswordService(): PasswordService
|
||||
{
|
||||
return $this->injectableFactory->create(PasswordService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SendingError
|
||||
* @throws Error
|
||||
*/
|
||||
private function sendPassword(UserEntity $user, string $password): void
|
||||
{
|
||||
$this->injectableFactory
|
||||
->create(PasswordSender::class)
|
||||
->sendPassword($user, $password);
|
||||
}
|
||||
|
||||
public function prepareEntityForOutput(Entity $entity)
|
||||
{
|
||||
assert($entity instanceof UserEntity);
|
||||
@@ -467,132 +292,6 @@ class User extends Record implements
|
||||
return $entity;
|
||||
}
|
||||
|
||||
private function generatePassword(): string
|
||||
{
|
||||
$length = $this->config->get('passwordStrengthLength');
|
||||
$letterCount = $this->config->get('passwordStrengthLetterCount');
|
||||
$numberCount = $this->config->get('passwordStrengthNumberCount');
|
||||
|
||||
$generateLength = $this->config->get('passwordGenerateLength', 10);
|
||||
$generateLetterCount = $this->config->get('passwordGenerateLetterCount', 4);
|
||||
$generateNumberCount = $this->config->get('passwordGenerateNumberCount', 2);
|
||||
|
||||
$length = is_null($length) ? $generateLength : $length;
|
||||
$letterCount = is_null($letterCount) ? $generateLetterCount : $letterCount;
|
||||
$numberCount = is_null($letterCount) ? $generateNumberCount : $numberCount;
|
||||
|
||||
if ($length < $generateLength) {
|
||||
$length = $generateLength;
|
||||
}
|
||||
|
||||
if ($letterCount < $generateLetterCount) {
|
||||
$letterCount = $generateLetterCount;
|
||||
}
|
||||
|
||||
if ($numberCount < $generateNumberCount) {
|
||||
$numberCount = $generateNumberCount;
|
||||
}
|
||||
|
||||
return Util::generatePassword($length, $letterCount, $numberCount, true);
|
||||
}
|
||||
|
||||
public function sendPasswordChangeLink(string $id, bool $allowNonAdmin = false): void
|
||||
{
|
||||
if (!$allowNonAdmin && !$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @var UserEntity|null $user */
|
||||
$user = $this->entityManager->getEntityById(UserEntity::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$user->isActive()) {
|
||||
throw new Forbidden("User is not active.");
|
||||
}
|
||||
|
||||
if (
|
||||
!$user->isRegular() &&
|
||||
!$user->isAdmin() &&
|
||||
!$user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->createRecoveryService()->createAndSendRequestForExistingUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws \Espo\Core\Mail\Exceptions\SendingError
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function generateNewPasswordForUser(string $id, bool $allowNonAdmin = false): void
|
||||
{
|
||||
if (!$allowNonAdmin) {
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
}
|
||||
|
||||
/** @var ?UserEntity $user */
|
||||
$user = $this->getEntity($id);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if ($user->isApi()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($user->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($user->isSystem()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!$user->getEmailAddress()) {
|
||||
throw new Forbidden(
|
||||
"Generate new password: Can't process because user doesn't have email address."
|
||||
);
|
||||
}
|
||||
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
throw new Forbidden(
|
||||
"Generate new password: Can't process because SMTP is not configured."
|
||||
);
|
||||
}
|
||||
|
||||
$password = $this->generatePassword();
|
||||
|
||||
$this->sendPassword($user, $password);
|
||||
|
||||
$this->saveUserPassword($user, $password);
|
||||
}
|
||||
|
||||
private function isSmtpConfigured(): bool
|
||||
{
|
||||
return $this->emailSender->hasSystemSmtp() || $this->config->get('internalSmtpServer');
|
||||
}
|
||||
|
||||
private function saveUserPassword(UserEntity $user, string $password, bool $silent = false): void
|
||||
{
|
||||
$user->set('password', $this->createPasswordHashUtil()->hash($password));
|
||||
|
||||
$this->entityManager->saveEntity($user, ['silent' => $silent]);
|
||||
}
|
||||
|
||||
private function createPasswordHashUtil(): PasswordHash
|
||||
{
|
||||
return $this->injectableFactory->create(PasswordHash::class);
|
||||
}
|
||||
|
||||
protected function getInternalUserCount(): int
|
||||
{
|
||||
return $this->entityManager
|
||||
@@ -755,135 +454,9 @@ class User extends Record implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?string,?string,?array<string,mixed>}
|
||||
*/
|
||||
private function getAccessInfoTemplateData(
|
||||
UserEntity $user,
|
||||
?string $password = null,
|
||||
?PasswordChangeRequest $passwordChangeRequest = null
|
||||
): array {
|
||||
|
||||
$data = [];
|
||||
|
||||
if ($password !== null) {
|
||||
$data['password'] = $password;
|
||||
}
|
||||
|
||||
$urlSuffix = '';
|
||||
|
||||
if ($passwordChangeRequest !== null) {
|
||||
$urlSuffix = '?entryPoint=changePassword&id=' . $passwordChangeRequest->getRequestId();
|
||||
}
|
||||
|
||||
$siteUrl = $this->config->getSiteUrl() . '/' . $urlSuffix;
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$subjectTpl = $this->templateFileManager
|
||||
->getTemplate('accessInfoPortal', 'subject', UserEntity::ENTITY_TYPE);
|
||||
$bodyTpl = $this->templateFileManager
|
||||
->getTemplate('accessInfoPortal', 'body', UserEntity::ENTITY_TYPE);
|
||||
|
||||
$urlList = [];
|
||||
|
||||
$portalList = $this->entityManager
|
||||
->getRDBRepository(PortalEntity::ENTITY_TYPE)
|
||||
->distinct()
|
||||
->join('users')
|
||||
->where([
|
||||
'isActive' => true,
|
||||
'users.id' => $user->getId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($portalList as $portal) {
|
||||
/** @var PortalEntity $portal */
|
||||
$this->getPortalRepository()->loadUrlField($portal);
|
||||
|
||||
$urlList[] = $portal->getUrl() . $urlSuffix;
|
||||
}
|
||||
|
||||
if (count($urlList) === 0) {
|
||||
return [null, null, null];
|
||||
}
|
||||
|
||||
$data['siteUrlList'] = $urlList;
|
||||
|
||||
return [$subjectTpl, $bodyTpl, $data];
|
||||
}
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('accessInfo', 'subject', UserEntity::ENTITY_TYPE);
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('accessInfo', 'body', UserEntity::ENTITY_TYPE);
|
||||
|
||||
$data['siteUrl'] = $siteUrl;
|
||||
|
||||
return [$subjectTpl, $bodyTpl, $data];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Espo\Core\Mail\Exceptions\SendingError
|
||||
* @throws Error
|
||||
*/
|
||||
protected function sendPassword(UserEntity $user, string $password): void
|
||||
{
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
|
||||
if (empty($emailAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var EmailEntity $email */
|
||||
$email = $this->entityManager->getNewEntity(EmailEntity::ENTITY_TYPE);
|
||||
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$subjectTpl, $bodyTpl, $data] = $this->getAccessInfoTemplateData($user, $password);
|
||||
|
||||
if ($data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$htmlizer = $this->htmlizerFactory->createNoAcl();
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl ?? '', null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl ?? '', null, $data, true);
|
||||
|
||||
$email->set([
|
||||
'subject' => $subject,
|
||||
'body' => $body,
|
||||
'to' => $emailAddress,
|
||||
]);
|
||||
|
||||
$this->getEmailSenderForAccessInfo()->send($email);
|
||||
}
|
||||
|
||||
private function getEmailSenderForAccessInfo(): Sender
|
||||
{
|
||||
$sender = $this->emailSender->create();
|
||||
|
||||
if (!$this->emailSender->hasSystemSmtp()) {
|
||||
$sender->withSmtpParams([
|
||||
'server' => $this->config->get('internalSmtpServer'),
|
||||
'port' => $this->config->get('internalSmtpPort'),
|
||||
'auth' => $this->config->get('internalSmtpAuth'),
|
||||
'username' => $this->config->get('internalSmtpUsername'),
|
||||
'password' => $this->config->get('internalSmtpPassword'),
|
||||
'security' => $this->config->get('internalSmtpSecurity'),
|
||||
'fromAddress' => $this->config->get(
|
||||
'internalOutboundEmailFromAddress',
|
||||
$this->config->get('outboundEmailFromAddress')
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return $sender;
|
||||
}
|
||||
|
||||
public function delete(string $id, DeleteParams $params): void
|
||||
{
|
||||
if ($id === self::ID_SYSTEM) {
|
||||
if ($id === ApplicationUser::SYSTEM_USER_ID) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
@@ -962,56 +535,9 @@ class User extends Record implements
|
||||
$this->dataManager->updateCacheTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws \Espo\Core\Mail\Exceptions\SendingError
|
||||
*/
|
||||
public function sendAccessInfoNew(UserEntity $user): void
|
||||
private function createPasswordChecker(): PasswordChecker
|
||||
{
|
||||
$primaryAddress = $user->getEmailAddressGroup()->getPrimary();
|
||||
|
||||
if ($primaryAddress === null) {
|
||||
throw new Error("Can't send access info for user '{$user->getId()}' w/o email address.");
|
||||
}
|
||||
|
||||
$emailAddress = $primaryAddress->getAddress();
|
||||
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
throw new Error("Can't send access info. SMTP is not configured.");
|
||||
}
|
||||
|
||||
$stubPassword = $this->generatePassword();
|
||||
|
||||
$this->saveUserPassword($user, $stubPassword, true);
|
||||
|
||||
$request = $this->createRecoveryService()->createRequestForNewUser($user);
|
||||
|
||||
[$subjectTpl, $bodyTpl, $data] = $this->getAccessInfoTemplateData($user, null, $request);
|
||||
|
||||
if ($data === null) {
|
||||
throw new Error("Could not send access info.");
|
||||
}
|
||||
|
||||
/** @var EmailEntity $email */
|
||||
$email = $this->entityManager->getEntity(EmailEntity::ENTITY_TYPE);
|
||||
|
||||
$htmlizer = $this->htmlizerFactory->createNoAcl();
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl ?? '', null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl ?? '', null, $data, true);
|
||||
|
||||
$email
|
||||
->addToAddress($emailAddress)
|
||||
->setSubject($subject)
|
||||
->setBody($body);
|
||||
|
||||
$this->getEmailSenderForAccessInfo()->send($email);
|
||||
}
|
||||
|
||||
private function getPortalRepository(): PortalRepository
|
||||
{
|
||||
/** @var PortalRepository */
|
||||
return $this->entityManager->getRDBRepository(PortalEntity::ENTITY_TYPE);
|
||||
return $this->injectableFactory->create(PasswordChecker::class);
|
||||
}
|
||||
|
||||
private function createAclCacheClearer(): AclCacheClearer
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\UserSecurity\Password;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class Checker
|
||||
{
|
||||
private Config $config;
|
||||
|
||||
public function __construct(
|
||||
Config $config
|
||||
) {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function checkStrength(string $password): bool
|
||||
{
|
||||
$minLength = $this->config->get('passwordStrengthLength');
|
||||
|
||||
if ($minLength) {
|
||||
if (mb_strlen($password) < $minLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$requiredLetterCount = $this->config->get('passwordStrengthLetterCount');
|
||||
|
||||
if ($requiredLetterCount) {
|
||||
$letterCount = 0;
|
||||
|
||||
foreach (str_split($password) as $c) {
|
||||
if (ctype_alpha($c)) {
|
||||
$letterCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($letterCount < $requiredLetterCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$requiredNumberCount = $this->config->get('passwordStrengthNumberCount');
|
||||
|
||||
if ($requiredNumberCount) {
|
||||
$numberCount = 0;
|
||||
|
||||
foreach (str_split($password) as $c) {
|
||||
if (is_numeric($c)) {
|
||||
$numberCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($numberCount < $requiredNumberCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$bothCases = $this->config->get('passwordStrengthBothCases');
|
||||
|
||||
if ($bothCases) {
|
||||
$ucCount = 0;
|
||||
$lcCount = 0;
|
||||
|
||||
foreach (str_split($password) as $c) {
|
||||
if (ctype_alpha($c) && $c === mb_strtoupper($c)) {
|
||||
$ucCount++;
|
||||
}
|
||||
|
||||
if (ctype_alpha($c) && $c === mb_strtolower($c)) {
|
||||
$lcCount++;
|
||||
}
|
||||
}
|
||||
if (!$ucCount || !$lcCount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\UserSecurity\Password;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Util;
|
||||
|
||||
/**
|
||||
* A password generator.
|
||||
*
|
||||
* @todo Use an interface with binding.
|
||||
*/
|
||||
class Generator
|
||||
{
|
||||
private Config $config;
|
||||
|
||||
public function __construct(
|
||||
Config $config
|
||||
) {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a password.
|
||||
*/
|
||||
public function generate(): string
|
||||
{
|
||||
$length = $this->config->get('passwordStrengthLength');
|
||||
$letterCount = $this->config->get('passwordStrengthLetterCount');
|
||||
$numberCount = $this->config->get('passwordStrengthNumberCount');
|
||||
|
||||
$generateLength = $this->config->get('passwordGenerateLength', 10);
|
||||
$generateLetterCount = $this->config->get('passwordGenerateLetterCount', 4);
|
||||
$generateNumberCount = $this->config->get('passwordGenerateNumberCount', 2);
|
||||
|
||||
$length = is_null($length) ? $generateLength : $length;
|
||||
$letterCount = is_null($letterCount) ? $generateLetterCount : $letterCount;
|
||||
$numberCount = is_null($letterCount) ? $generateNumberCount : $numberCount;
|
||||
|
||||
if ($length < $generateLength) {
|
||||
$length = $generateLength;
|
||||
}
|
||||
|
||||
if ($letterCount < $generateLetterCount) {
|
||||
$letterCount = $generateLetterCount;
|
||||
}
|
||||
|
||||
if ($numberCount < $generateNumberCount) {
|
||||
$numberCount = $generateNumberCount;
|
||||
}
|
||||
|
||||
return Util::generatePassword($length, $letterCount, $numberCount, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\UserSecurity\Password\Jobs;
|
||||
|
||||
use Espo\Core\Job\Job;
|
||||
use Espo\Core\Job\Job\Data;
|
||||
use Espo\Entities\PasswordChangeRequest;
|
||||
use Espo\ORM\EntityManager;
|
||||
use RuntimeException;
|
||||
|
||||
class RemoveRecoveryRequest implements Job
|
||||
{
|
||||
private EntityManager $entityManager;
|
||||
|
||||
public function __construct(EntityManager $entityManager)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
}
|
||||
|
||||
public function run(Data $data): void
|
||||
{
|
||||
$id = $data->get('id');
|
||||
|
||||
if (!$id) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$entity = $this->entityManager->getEntity(PasswordChangeRequest::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->entityManager->removeEntity($entity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\UserSecurity\Password;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Htmlizer\HtmlizerFactory;
|
||||
use Espo\Core\Mail\EmailSender;
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Core\Mail\Sender as EmailSenderSender;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\TemplateFileManager;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\PasswordChangeRequest;
|
||||
use Espo\Entities\Portal;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\Portal as PortalRepository;
|
||||
|
||||
class Sender
|
||||
{
|
||||
private Config $config;
|
||||
private EmailSender $emailSender;
|
||||
private EntityManager $entityManager;
|
||||
private HtmlizerFactory $htmlizerFactory;
|
||||
private TemplateFileManager $templateFileManager;
|
||||
|
||||
public function __construct(
|
||||
Config $config,
|
||||
EmailSender $emailSender,
|
||||
EntityManager $entityManager,
|
||||
HtmlizerFactory $htmlizerFactory,
|
||||
TemplateFileManager $templateFileManager
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->emailSender = $emailSender;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->htmlizerFactory = $htmlizerFactory;
|
||||
$this->templateFileManager = $templateFileManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send access info for a new user.
|
||||
*
|
||||
* @throws Error
|
||||
* @throws NoSmtp
|
||||
* @throws SendingError
|
||||
*/
|
||||
public function sendAccessInfo(User $user, PasswordChangeRequest $request): void
|
||||
{
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
|
||||
if (!$emailAddress) {
|
||||
throw new Error("No email address.");
|
||||
}
|
||||
|
||||
[$subjectTpl, $bodyTpl, $data] = $this->getAccessInfoTemplateData($user, null, $request);
|
||||
|
||||
if ($data === null) {
|
||||
throw new Error("Could not send access info.");
|
||||
}
|
||||
|
||||
/** @var Email $email */
|
||||
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
|
||||
|
||||
$htmlizer = $this->htmlizerFactory->createNoAcl();
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl ?? '', null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl ?? '', null, $data, true);
|
||||
|
||||
$email
|
||||
->addToAddress($emailAddress)
|
||||
->setSubject($subject)
|
||||
->setBody($body);
|
||||
|
||||
$this->createSender()->send($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a plain password in email.
|
||||
*
|
||||
* @throws SendingError
|
||||
* @throws Error
|
||||
*/
|
||||
public function sendPassword(User $user, string $password): void
|
||||
{
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
|
||||
if (empty($emailAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Email $email */
|
||||
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
|
||||
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$subjectTpl, $bodyTpl, $data] = $this->getAccessInfoTemplateData($user, $password);
|
||||
|
||||
if ($data === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$htmlizer = $this->htmlizerFactory->createNoAcl();
|
||||
|
||||
$subject = $htmlizer->render($user, $subjectTpl ?? '', null, $data, true);
|
||||
$body = $htmlizer->render($user, $bodyTpl ?? '', null, $data, true);
|
||||
|
||||
$email
|
||||
->setSubject($subject)
|
||||
->setBody($body)
|
||||
->addToAddress($emailAddress);
|
||||
|
||||
$this->createSender()->send($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
private function createSender(): EmailSenderSender
|
||||
{
|
||||
$sender = $this->emailSender->create();
|
||||
|
||||
$smtpParams = $this->getSmtpParams();
|
||||
|
||||
if ($smtpParams) {
|
||||
$sender = $sender->withSmtpParams($smtpParams);
|
||||
}
|
||||
|
||||
return $sender;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{?string, ?string, ?array<string, mixed>}
|
||||
*/
|
||||
private function getAccessInfoTemplateData(
|
||||
User $user,
|
||||
?string $password = null,
|
||||
?PasswordChangeRequest $passwordChangeRequest = null
|
||||
): array {
|
||||
|
||||
$data = [];
|
||||
|
||||
if ($password !== null) {
|
||||
$data['password'] = $password;
|
||||
}
|
||||
|
||||
$urlSuffix = '';
|
||||
|
||||
if ($passwordChangeRequest !== null) {
|
||||
$urlSuffix = '?entryPoint=changePassword&id=' . $passwordChangeRequest->getRequestId();
|
||||
}
|
||||
|
||||
$siteUrl = $this->config->getSiteUrl() . '/' . $urlSuffix;
|
||||
|
||||
if ($user->isPortal()) {
|
||||
$subjectTpl = $this->templateFileManager
|
||||
->getTemplate('accessInfoPortal', 'subject', User::ENTITY_TYPE);
|
||||
$bodyTpl = $this->templateFileManager
|
||||
->getTemplate('accessInfoPortal', 'body', User::ENTITY_TYPE);
|
||||
|
||||
$urlList = [];
|
||||
|
||||
$portalList = $this->entityManager
|
||||
->getRDBRepositoryByClass(Portal::class)
|
||||
->distinct()
|
||||
->join('users')
|
||||
->where([
|
||||
'isActive' => true,
|
||||
'users.id' => $user->getId(),
|
||||
])
|
||||
->find();
|
||||
|
||||
foreach ($portalList as $portal) {
|
||||
/** @var Portal $portal */
|
||||
$this->getPortalRepository()->loadUrlField($portal);
|
||||
|
||||
$urlList[] = $portal->getUrl() . $urlSuffix;
|
||||
}
|
||||
|
||||
if (count($urlList) === 0) {
|
||||
return [null, null, null];
|
||||
}
|
||||
|
||||
$data['siteUrlList'] = $urlList;
|
||||
|
||||
return [$subjectTpl, $bodyTpl, $data];
|
||||
}
|
||||
|
||||
$subjectTpl = $this->templateFileManager->getTemplate('accessInfo', 'subject', User::ENTITY_TYPE);
|
||||
$bodyTpl = $this->templateFileManager->getTemplate('accessInfo', 'body', User::ENTITY_TYPE);
|
||||
|
||||
$data['siteUrl'] = $siteUrl;
|
||||
|
||||
return [$subjectTpl, $bodyTpl, $data];
|
||||
}
|
||||
|
||||
private function isSmtpConfigured(): bool
|
||||
{
|
||||
return
|
||||
$this->emailSender->hasSystemSmtp() ||
|
||||
$this->config->get('internalSmtpServer');
|
||||
}
|
||||
|
||||
private function getPortalRepository(): PortalRepository
|
||||
{
|
||||
/** @var PortalRepository */
|
||||
return $this->entityManager->getRDBRepository(Portal::ENTITY_TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
private function getSmtpParams(): ?SmtpParams
|
||||
{
|
||||
if ($this->emailSender->hasSystemSmtp()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$server = $this->config->get('internalSmtpServer');
|
||||
|
||||
if (!$server) {
|
||||
throw new NoSmtp("No SMTP configured to send access info.");
|
||||
}
|
||||
|
||||
/** @var int $port */
|
||||
$port = $this->config->get('internalSmtpPort');
|
||||
|
||||
return SmtpParams
|
||||
::create($server, $port)
|
||||
->withAuth($this->config->get('internalSmtpAuth'))
|
||||
->withUsername($this->config->get('internalSmtpUsername'))
|
||||
->withPassword($this->config->get('internalSmtpPassword'))
|
||||
->withSecurity($this->config->get('internalSmtpSecurity'))
|
||||
->withFromAddress(
|
||||
$this->config->get('internalOutboundEmailFromAddress') ??
|
||||
$this->config->get('outboundEmailFromAddress')
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* This file is part of EspoCRM.
|
||||
*
|
||||
* EspoCRM - Open Source CRM application.
|
||||
* Copyright (C) 2014-2022 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
|
||||
* Website: https://www.espocrm.com
|
||||
*
|
||||
* EspoCRM is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* EspoCRM is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
|
||||
*
|
||||
* The interactive user interfaces in modified source and object code versions
|
||||
* of this program must display Appropriate Legal Notices, as required under
|
||||
* Section 5 of the GNU General Public License version 3.
|
||||
*
|
||||
* In accordance with Section 7(b) of the GNU General Public License version 3,
|
||||
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\UserSecurity\Password;
|
||||
|
||||
use Espo\Core\Authentication\Logins\Espo;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\FieldValidation\FieldValidationManager;
|
||||
use Espo\Core\Mail\EmailSender;
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
use Espo\Core\Password\Recovery;
|
||||
use Espo\Core\Record\ServiceContainer;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\PasswordHash;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class Service
|
||||
{
|
||||
private User $user;
|
||||
private ServiceContainer $serviceContainer;
|
||||
private EmailSender $emailSender;
|
||||
private Config $config;
|
||||
private Generator $generator;
|
||||
private Sender $sender;
|
||||
private PasswordHash $passwordHash;
|
||||
private EntityManager $entityManager;
|
||||
private Recovery $recovery;
|
||||
private FieldValidationManager $fieldValidationManager;
|
||||
private Checker $checker;
|
||||
|
||||
public function __construct(
|
||||
User $user,
|
||||
ServiceContainer $serviceContainer,
|
||||
EmailSender $emailSender,
|
||||
Config $config,
|
||||
Generator $generator,
|
||||
Sender $sender,
|
||||
PasswordHash $passwordHash,
|
||||
EntityManager $entityManager,
|
||||
Recovery $recovery,
|
||||
FieldValidationManager $fieldValidationManager,
|
||||
Checker $checker
|
||||
) {
|
||||
$this->user = $user;
|
||||
$this->serviceContainer = $serviceContainer;
|
||||
$this->emailSender = $emailSender;
|
||||
$this->config = $config;
|
||||
$this->generator = $generator;
|
||||
$this->sender = $sender;
|
||||
$this->passwordHash = $passwordHash;
|
||||
$this->entityManager = $entityManager;
|
||||
$this->recovery = $recovery;
|
||||
$this->fieldValidationManager = $fieldValidationManager;
|
||||
$this->checker = $checker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and send a password recovery link in an email. Only for admin.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function createAndSendPasswordRecovery(string $id): void
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (!$user->isActive()) {
|
||||
throw new Forbidden("User is not active.");
|
||||
}
|
||||
|
||||
if (
|
||||
!$user->isRegular() &&
|
||||
!$user->isAdmin() &&
|
||||
!$user->isPortal()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$this->recovery->createAndSendRequestForExistingUser($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a password by a recovery request.
|
||||
*
|
||||
* @param string $requestId A request ID.
|
||||
* @param string $password A new password.
|
||||
*
|
||||
* @return ?string A URL to suggest to a user.
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function changePasswordByRecovery(string $requestId, string $password): ?string
|
||||
{
|
||||
$request = $this->recovery->getRequest($requestId);
|
||||
|
||||
$this->changePassword($request->getUserId(), $password);
|
||||
$this->recovery->removeRequest($requestId);
|
||||
|
||||
return $request->getUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a password with a current password check.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
public function changePasswordWithCheck(string $userId, string $password, string $currentPassword): void
|
||||
{
|
||||
$this->changePasswordInternal($userId, $password, true, $currentPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a password.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws Error
|
||||
*/
|
||||
private function changePassword(string $userId, string $password): void
|
||||
{
|
||||
$this->changePasswordInternal($userId, $password);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws NotFound
|
||||
*/
|
||||
private function changePasswordInternal(
|
||||
string $userId,
|
||||
string $password,
|
||||
bool $checkCurrentPassword = false,
|
||||
?string $currentPassword = null
|
||||
): void {
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if (
|
||||
$user->isSuperAdmin() &&
|
||||
!$this->user->isSuperAdmin()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$authenticationMethod = $this->config->get('authenticationMethod', Espo::NAME);
|
||||
|
||||
if (!$user->isAdmin() && $authenticationMethod !== Espo::NAME) {
|
||||
throw new Forbidden("Authentication method is not `Espo`.");
|
||||
}
|
||||
|
||||
if (empty($password)) {
|
||||
throw new Error("Password can't be empty.");
|
||||
}
|
||||
|
||||
if ($checkCurrentPassword) {
|
||||
$u = $this->entityManager
|
||||
->getRDBRepository(User::ENTITY_TYPE)
|
||||
->where([
|
||||
'id' => $user->getId(),
|
||||
'password' => $this->passwordHash->hash($currentPassword ?? ''),
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$u) {
|
||||
throw new Forbidden("Wrong password.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$this->checker->checkStrength($password)) {
|
||||
throw new Forbidden("Password is weak.");
|
||||
}
|
||||
|
||||
$validLength = $this->fieldValidationManager->check(
|
||||
$user,
|
||||
'password',
|
||||
'maxLength',
|
||||
(object) ['password' => $password]
|
||||
);
|
||||
|
||||
if (!$validLength) {
|
||||
throw new Forbidden("Password exceeds max length.");
|
||||
}
|
||||
|
||||
$user->set('password', $this->passwordHash->hash($password));
|
||||
|
||||
$this->entityManager->saveEntity($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send access info for a new user.
|
||||
*
|
||||
* @throws Error
|
||||
* @throws SendingError
|
||||
*/
|
||||
public function sendAccessInfoForNewUser(User $user): void
|
||||
{
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
|
||||
if ($emailAddress === null) {
|
||||
throw new Error("Can't send access info for user '{$user->getId()}' w/o email address.");
|
||||
}
|
||||
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
throw new Error("Can't send access info. SMTP is not configured.");
|
||||
}
|
||||
|
||||
$stubPassword = $this->generator->generate();
|
||||
|
||||
$this->savePasswordSilent($user, $stubPassword);
|
||||
|
||||
$request = $this->recovery->createRequestForNewUser($user);
|
||||
|
||||
$this->sender->sendAccessInfo($user, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new password and send it in an email. Only for admin.
|
||||
*
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @throws SendingError
|
||||
* @throws Error
|
||||
*/
|
||||
public function generateAndSendNewPasswordForUser(string $id): void
|
||||
{
|
||||
if (!$this->user->isAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @var ?User $user */
|
||||
$user = $this->serviceContainer
|
||||
->get(User::ENTITY_TYPE)
|
||||
->getEntity($id);
|
||||
|
||||
if (!$user) {
|
||||
throw new NotFound();
|
||||
}
|
||||
|
||||
if ($user->isApi()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($user->isSuperAdmin()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($user->isSystem()) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!$user->getEmailAddress()) {
|
||||
throw new Forbidden("Generate new password: Can't process because user doesn't have email address.");
|
||||
}
|
||||
|
||||
if (!$this->isSmtpConfigured()) {
|
||||
throw new Forbidden("Generate new password: Can't process because SMTP is not configured.");
|
||||
}
|
||||
|
||||
$password = $this->generator->generate();
|
||||
|
||||
$this->sender->sendPassword($user, $password);
|
||||
$this->savePassword($user, $password);
|
||||
}
|
||||
|
||||
private function savePassword(User $user, string $password): void
|
||||
{
|
||||
$user->set('password', $this->passwordHash->hash($password));
|
||||
|
||||
$this->entityManager->saveEntity($user);
|
||||
}
|
||||
|
||||
private function savePasswordSilent(User $user, string $password): void
|
||||
{
|
||||
$user->set('password', $this->passwordHash->hash($password));
|
||||
|
||||
$this->entityManager->saveEntity($user, ['silent' => true]);
|
||||
}
|
||||
|
||||
private function isSmtpConfigured(): bool
|
||||
{
|
||||
return
|
||||
$this->emailSender->hasSystemSmtp() ||
|
||||
$this->config->get('internalSmtpServer');
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
namespace Espo\Tools\UserSecurity;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
@@ -105,7 +106,7 @@ class Service
|
||||
|
||||
/**
|
||||
* @throws BadRequest
|
||||
* @throws \Espo\Core\Exceptions\Error
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
@@ -171,6 +172,11 @@ class Service
|
||||
return $clientData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
*/
|
||||
public function update(string $id, stdClass $data): stdClass
|
||||
{
|
||||
if (!$this->user->isAdmin() && $id !== $this->user->getId()) {
|
||||
|
||||
Reference in New Issue
Block a user