Compare commits

..

9 Commits
8.3.1 ... 8.3.2

Author SHA1 Message Date
Yuri Kuznetsov
5b0787474e 8.3.2 2024-06-26 07:53:49 +03:00
Yuri Kuznetsov
8c87f20374 comment 2024-06-26 07:50:19 +03:00
Yuri Kuznetsov
ba35115a48 inline edit: revert initial attributes on error 2024-06-26 07:44:44 +03:00
Yuri Kuznetsov
069010d0fe log opened only for system user 2024-06-26 07:22:09 +03:00
Yuri Kuznetsov
fe5878fd99 2fa auth log records on fail 2024-06-25 20:39:44 +03:00
Yuri Kuznetsov
839ceea142 root api endpoint fix type 2024-06-25 08:35:56 +03:00
Yuri Kuznetsov
9cce9d7347 comment 2024-06-24 11:16:45 +03:00
Yuri Kuznetsov
69d0dbbf1c email collapse fix 2024-06-24 11:09:29 +03:00
Yuri Kuznetsov
68ef9ce4ac disable email field layouts 2024-06-24 10:54:48 +03:00
13 changed files with 101 additions and 24 deletions

View File

@@ -29,10 +29,14 @@
namespace Espo\Controllers;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Utils\Json;
class ApiIndex
{
public function getActionIndex(): string
public function getActionIndex(Request $request, Response $response): void
{
return "EspoCRM REST API";
$response->writeBody(Json::encode("EspoCRM REST API"));
}
}

View File

@@ -195,8 +195,12 @@ class Auth
throw new BadRequest("Auth: Bad authorization string provided.");
}
/** @var array{string, string} */
return explode(':', $stringDecoded, 2);
[$username, $password] = explode(':', $stringDecoded, 2);
$username = trim($username);
$password = trim($password);
return [$username, $password];
}
private function handleSecondStepRequired(Response $response, Result $result): void
@@ -316,6 +320,14 @@ class Auth
$username = $request->getServerParam('PHP_AUTH_USER');
$password = $request->getServerParam('PHP_AUTH_PW');
if (is_string($username)) {
$username = trim($username);
}
if (is_string($password)) {
$password = trim($password);
}
return [$username, $password];
}

View File

@@ -241,7 +241,7 @@ class Authentication
$result = $this->processTwoFactor($result, $request);
if ($result->isFail()) {
return $this->processFail($result, $data, $request);
return $this->processTwoFactorFail($result, $data, $request, $authLogRecord);
}
}
@@ -818,4 +818,22 @@ class Authentication
throw new Forbidden();
}
private function processTwoFactorFail(
Result $result,
AuthenticationData $data,
Request $request,
?AuthLogRecord $authLogRecord
): Result {
if ($authLogRecord) {
$authLogRecord
->setIsDenied()
->setDenialReason(AuthLogRecord::DENIAL_REASON_WRONG_CODE);
$this->entityManager->saveEntity($authLogRecord);
}
return $this->processFail($result, $data, $request);
}
}

View File

@@ -68,6 +68,8 @@ class FailedAttemptsLimit implements BeforeLogin
return;
}
$isSecondStep = $request->getHeader('Espo-Authorization-Code') !== null;
$failedAttemptsPeriod = $this->configDataProvider->getFailedAttemptsPeriod();
$maxFailedAttempts = $this->configDataProvider->getMaxFailedAttemptNumber();
@@ -80,14 +82,21 @@ class FailedAttemptsLimit implements BeforeLogin
throw new RuntimeException($e->getMessage());
}
$apAddress = $this->util->obtainIpFromRequest($request);
$ipAddress = $this->util->obtainIpFromRequest($request);
$where = [
'requestTime>' => $requestTimeFrom->format('U'),
'ipAddress' => $apAddress,
'isDenied' => true,
];
if ($isSecondStep) {
$where['username'] = $data->getUsername();
}
if (!$isSecondStep) {
$where['ipAddress'] = $ipAddress;
}
$wasFailed = (bool) $this->entityManager
->getRDBRepository(AuthLogRecord::ENTITY_TYPE)
->select(['id'])
@@ -107,6 +116,12 @@ class FailedAttemptsLimit implements BeforeLogin
return;
}
throw new Forbidden("Max failed login attempts exceeded for IP address $apAddress.");
if ($isSecondStep) {
$username = $data->getUsername() ?? '';
throw new Forbidden("Max failed 2FA login attempts exceeded for username '$username'.");
}
throw new Forbidden("Max failed login attempts exceeded for IP address $ipAddress.");
}
}

View File

@@ -36,6 +36,7 @@ class AuthLogRecord extends Entity
public const ENTITY_TYPE = 'AuthLogRecord';
public const DENIAL_REASON_CREDENTIALS = 'CREDENTIALS';
public const DENIAL_REASON_WRONG_CODE = 'WRONG_CODE';
public const DENIAL_REASON_INACTIVE_USER = 'INACTIVE_USER';
public const DENIAL_REASON_IS_PORTAL_USER = 'IS_PORTAL_USER';
public const DENIAL_REASON_IS_NOT_PORTAL_USER = 'IS_NOT_PORTAL_USER';

View File

@@ -29,6 +29,7 @@
namespace Espo\Modules\Crm\EntryPoints;
use Espo\Entities\User;
use Espo\Modules\Crm\Entities\Campaign;
use Espo\Modules\Crm\Entities\EmailQueueItem;
use Espo\Modules\Crm\Entities\MassEmail;
@@ -47,7 +48,8 @@ class CampaignTrackOpened implements EntryPoint
public function __construct(
private EntityManager $entityManager,
private LogService $service
private LogService $service,
private User $user,
) {}
/**
@@ -105,7 +107,9 @@ class CampaignTrackOpened implements EntryPoint
return;
}
$this->service->logOpened($campaignId, $queueItem);
if ($this->user->isSystem()) {
$this->service->logOpened($campaignId, $queueItem);
}
header('Content-Type: image/png');

View File

@@ -27,6 +27,7 @@
"options": {
"denialReason": {
"CREDENTIALS": "Invalid credentials",
"WRONG_CODE": "Wrong code",
"INACTIVE_USER": "Inactive user",
"IS_PORTAL_USER": "Portal user",
"IS_NOT_PORTAL_USER": "Not a portal user",

View File

@@ -81,8 +81,10 @@
"view": "views/email/fields/from-address-varchar",
"textFilterDisabled": true,
"customizationDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail",
"filters"
]
},
"to": {
"type": "varchar",
@@ -97,8 +99,10 @@
],
"textFilterDisabled": true,
"customizationDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail",
"filters"
]
},
"cc": {
"type": "varchar",
@@ -111,8 +115,9 @@
],
"customizationDisabled": true,
"textFilterDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail"
]
},
"bcc": {
"type": "varchar",
@@ -125,8 +130,9 @@
],
"customizationDisabled": true,
"textFilterDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail"
]
},
"replyTo": {
"type": "varchar",
@@ -135,8 +141,9 @@
"view": "views/email/fields/email-address-varchar",
"textFilterDisabled": true,
"customizationDisabled": true,
"layoutDefaultSidePanelDisabled": true,
"layoutMassUpdateDisabled": true
"layoutAvailabilityList": [
"detail"
]
},
"personStringData": {
"type": "varchar",

View File

@@ -69,6 +69,10 @@ function init(langSets) {
const options = context.options;
const view = /** @type {import('view').default} */options.espoView;
if (!view) {
return;
}
context.memo('button.cellParams', () => {
return ui.button({
className: '',
@@ -144,6 +148,12 @@ function init(langSets) {
const options = context.options;
const view = /** @type {import('view').default} */options.espoView;
if (!view) {
// Prevents an issue with a collapsed modal.
// @todo Revise.
return;
}
context.memo('button.tableParams', () => {
return ui.button({
className: '',

View File

@@ -1253,6 +1253,7 @@ class BaseFieldView extends View {
if (isInvalid) {
Espo.Ui.error(this.translate('Not valid'));
// @todo Revise.
model.set(prev, {silent: true});
return;
@@ -1273,6 +1274,7 @@ class BaseFieldView extends View {
.catch(() => {
Espo.Ui.error(this.translate('Error occurred'));
// @todo Revise.
model.set(prev, {silent: true});
this.reRender();

View File

@@ -2085,7 +2085,10 @@ class DetailRecordView extends BaseRecordView {
return;
}
view.inlineEdit();
const initialAttributes = {...view.initialAttributes};
view.inlineEdit()
.then(() => view.initialAttributes = initialAttributes);
}
});
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "espocrm",
"version": "8.3.1",
"version": "8.3.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "espocrm",
"version": "8.3.1",
"version": "8.3.2",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "espocrm",
"version": "8.3.1",
"version": "8.3.2",
"description": "Open-source CRM.",
"repository": {
"type": "git",