diff --git a/CHANGELOG.md b/CHANGELOG.md index 55412183d4..c8641f6fe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,60 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.8.0] - 2024-05-21 +### Added +- PB-33071 As an administrator I can purge the action logs table with a dedicated command +- PB-33231 As an administrator I want to know if a custom certificate is in use for SMTP +- PB-32579 As an administrator I can view email_queue records via passbolt command + +### Improved +- PB-32888 As an admin I should not get a time-out on health checks on air-gapped network +- PB-32983 Access email settings only when emails are sent + +### Fixed +- PB-33451 Fix 500 error on authentication when nonce is not a string +- PB-33073 As a user logging in, invalid login operation should not be logged as success in the audit logs +- PB-33234 The application should not throw an error if the JWT public key is not parsable + +### Maintenance +- PB-30314 Bump passbolt/passbolt-test-data to v4.8 + +## [4.8.0-rc.1] - 2024-05-17 +### Added +- PB-33071 As an administrator I can purge the action logs table with a dedicated command +- PB-33231 As an administrator I want to know if a custom certificate is in use for SMTP +- PB-32579 As an administrator I can view email_queue records via passbolt command + +### Improved +- PB-32888 As an admin I should not get a time-out on health checks on air-gapped network +- PB-32983 Access email settings only when emails are sent + +### Fixed +- PB-33451 Fix 500 error on authentication when nonce is not a string +- PB-33073 As a user logging in, invalid login operation should not be logged as success in the audit logs +- PB-33234 The application should not throw an error if the JWT public key is not parsable + +### Maintenance +- PB-30314 Bump passbolt/passbolt-test-data to v4.8 + +## [4.8.0-test.1] - 2024-05-16 +### Added +- PB-33071 As an administrator I can purge the action logs table with a dedicated command +- PB-33231 As an administrator I want to know if a custom certificate is in use for SMTP +- PB-32579 As an administrator I can view email_queue records via passbolt command + +### Improved +- PB-32888 As an admin I should not get a time-out on health checks on air-gapped network +- PB-32983 Access email settings only when emails are sent + +### Fixed +- PB-33451 Fix 500 error on authentication when nonce is not a string +- PB-33073 As a user logging in, invalid login operation should not be logged as success in the audit logs +- PB-33234 The application should not throw an error if the JWT public key is not parsable + +### Maintenance +- PB-30314 Bump passbolt/passbolt-test-data to v4.8 + ## [4.7.0] - 2024-04-30 ### Added - PB-30330 Add HTTP HEAD method support to /healthcheck/status.json to support more uptime monitoring tools (GITHUB #507) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a90e22641d..fde202a045 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,28 +1,42 @@ -Release song: https://youtu.be/3L4YrGaR8E4 +Release song: https://youtu.be/hbe3CQamF8k -Passbolt Community Edition v4.7 is a maintenance release that resolves multiple issues identified by the community. Furthermore, this release supports the commitment to improving customization options and integration features, making it easier for organizations to tailor the system to their specific needs. +Passbolt v4.8.0 is a maintenance release focusing on the migration of the browser extension to the latest MV3 +architecture and adding tools for administrators to help them manage their instance. -A key enhancement in this release is the ability to use custom SSL certificates for SMTP server connections. This long-awaited feature is particularly beneficial for organizations operating in air-gapped environments or those using their own root CAs, enabling passbolt to more securely integrate with internal tools. +This release marks the introduction of the first version of the MV3 extension for Chrome. The transition to MV3 has been +in progress since last year, with changes rolled out progressively until now. The base code between MV2 and MV3 is +nearly identical, and both extensions will continue to be maintained in parallel. A detailed blog post explaining our +migration process will be coming soon. -## [4.7.0] - 2024-04-30 +A new feature allowing administrators to purge audit logs from the command line was added. This will help reclaim database +space for logs that are no longer relevant, improving the performance of long-running instances while keeping necessary +logs for forensic and audit activities. + +A new command has also been added to help administrators debug issues with their SMTP server. Email functionality is +crucial for Passbolt, and diagnosing connection problems is not always straightforward. This new command aims to simplify +the process when connecting to a new SMTP server as well as understand errors that could occur on existing integration. + +As passbolt moves towards supporting more content types this year, significant work has been done to enhance performance +across the entire stack, from the database to the API and the browser extension. This release includes some of these +improvements, with more enhancements on the way in the next coming release v4.9.0. + +We hope these updates enhance your experience with Passbolt. Your feedback is always valuable to us. + + +## [4.8.0] - 2024-05-21 ### Added -- PB-30330 Add HTTP HEAD method support to /healthcheck/status.json to support more uptime monitoring tools (GITHUB #507) -- PB-26156 As an administrator I can configure SMTP to use TLS with a self-signed cert on my mail server (GITHUB #498) +- PB-33071 As an administrator I can purge the action logs table with a dedicated command +- PB-33231 As an administrator I want to know if a custom certificate is in use for SMTP +- PB-32579 As an administrator I can view email_queue records via passbolt command -### Security -- PB-30255 As an authenticated user I cannot access to the healthcheck endpoint when debug is on +### Improved +- PB-32888 As an admin I should not get a time-out on health checks on air-gapped network +- PB-32983 Access email settings only when emails are sent ### Fixed -- PB-30379 As an authenticating user I should not get a 500 if the gpg_auth is not an array -- PB-32889 As an administrator I should not get an exception when running core healthcheck and the host cannot be resolved -- PB-32928 As user I should see the accurate URL in the email footer when passbolt runs on multiple instances -- PB-32566 As a user setting up my account I should not get an unexpected 500 -- PB-32903 Fix deprecation error on password expiry settings validation +- PB-33451 Fix 500 error on authentication when nonce is not a string +- PB-33073 As a user logging in, invalid login operation should not be logged as success in the audit logs +- PB-33234 The application should not throw an error if the JWT public key is not parsable ### Maintenance -- PB-29983 Refactor health check code domain for better maintenance -- PB-30394 Moves code in ActionLogsModelListener into a dedicated service -- PB-32881 Disable by default all plugins in integration tests -- PB-32978 Use dependency proxy to reduce docker pull limit -- PB-22605 Refactor ShareSearchControllerTest, SecretViewControllerTest and GroupsDeleteControllerTest with fixture factories -- PB-32594 Add tests for SecretCreateService +- PB-30314 Bump passbolt/passbolt-test-data to v4.8 diff --git a/composer.json b/composer.json index 9a0950c62d..8456225c00 100644 --- a/composer.json +++ b/composer.json @@ -109,7 +109,7 @@ "phpunit/phpunit": "~9.5.2", "cakephp/cakephp-codesniffer": "^4.5", "passbolt/passbolt-selenium-api": "^4.5", - "passbolt/passbolt-test-data": "^4.4", + "passbolt/passbolt-test-data": "^4.8", "vierge-noire/cakephp-fixture-factories": "^v2.9.3", "cakephp/localized": "4.0.0", "vimeo/psalm": "^5.0.0", diff --git a/composer.lock b/composer.lock index 0e49aac9a2..1242c018cb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e1503e29ce2b01ba38ef82c344f9a888", + "content-hash": "44be8bfbf90b6efc7be940cc4a97f095", "packages": [ { "name": "bacon/bacon-qr-code", @@ -6160,20 +6160,20 @@ }, { "name": "passbolt/passbolt-test-data", - "version": "4.4.0", + "version": "4.8.0", "source": { "type": "git", "url": "https://github.com/passbolt/passbolt-test-data", - "reference": "cc1e4e665c84677ac6ee46f0d9ea597e3793f632" + "reference": "b290d06a00fdeaae9270f9fdb28c4ce90c8288c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/passbolt/passbolt-test-data/zipball/cc1e4e665c84677ac6ee46f0d9ea597e3793f632", - "reference": "cc1e4e665c84677ac6ee46f0d9ea597e3793f632", + "url": "https://api.github.com/repos/passbolt/passbolt-test-data/zipball/b290d06a00fdeaae9270f9fdb28c4ce90c8288c6", + "reference": "b290d06a00fdeaae9270f9fdb28c4ce90c8288c6", "shasum": "" }, "require": { - "cakephp/cakephp": "^4.0", + "cakephp/cakephp": "^4.3", "ext-json": "*" }, "require-dev": { @@ -6211,7 +6211,7 @@ "help": "https://www.passbolt.com/help", "source": "https://github.com/passbolt/passbolt" }, - "time": "2023-11-02T14:20:48+00:00" + "time": "2024-05-07T08:55:35+00:00" }, { "name": "phar-io/manifest", diff --git a/config/version.php b/config/version.php index dd50e3a4e3..619767d96c 100644 --- a/config/version.php +++ b/config/version.php @@ -1,8 +1,8 @@ [ - 'version' => '4.7.0', - 'name' => 'Bulls On Parade', + 'version' => '4.8.0', + 'name' => 'Angel', ], 'php' => [ 'minVersion' => '7.4', diff --git a/package-lock.json b/package-lock.json index 36e5136b9f..ee72038e6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "jquery": "^3.5.1", "lockfile-lint": "^4.12.1", "openpgp": "5.2.1", - "passbolt-styleguide": "^4.7.0" + "passbolt-styleguide": "^4.8.0" }, "engines": { "node": ">=16.14.0", @@ -2239,9 +2239,9 @@ } }, "node_modules/passbolt-styleguide": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/passbolt-styleguide/-/passbolt-styleguide-4.7.0.tgz", - "integrity": "sha512-8myUPLOQIWUoeTqoWJSGjvQKIOzAF4h0nW1fYqNUALMFuBcC7JjIUrpD7qAqp1tOgCKU5SoSoe22GSf4z7xKag==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/passbolt-styleguide/-/passbolt-styleguide-4.8.0.tgz", + "integrity": "sha512-n4lyIZHxzPM9/aNFgwoEr6rQienzb06S28TE0WLmPOKPhdBDbc1AWPRceZixN/+8ZXUmoIjUzLzbGVowN+VrAA==", "dev": true, "dependencies": { "debounce-promise": "^3.1.2", @@ -4976,9 +4976,9 @@ "dev": true }, "passbolt-styleguide": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/passbolt-styleguide/-/passbolt-styleguide-4.7.0.tgz", - "integrity": "sha512-8myUPLOQIWUoeTqoWJSGjvQKIOzAF4h0nW1fYqNUALMFuBcC7JjIUrpD7qAqp1tOgCKU5SoSoe22GSf4z7xKag==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/passbolt-styleguide/-/passbolt-styleguide-4.8.0.tgz", + "integrity": "sha512-n4lyIZHxzPM9/aNFgwoEr6rQienzb06S28TE0WLmPOKPhdBDbc1AWPRceZixN/+8ZXUmoIjUzLzbGVowN+VrAA==", "dev": true, "requires": { "debounce-promise": "^3.1.2", diff --git a/package.json b/package.json index 4dd025912c..0815dbe878 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "jquery": "^3.5.1", "lockfile-lint": "^4.12.1", "openpgp": "5.2.1", - "passbolt-styleguide": "^4.7.0" + "passbolt-styleguide": "^4.8.0" }, "scripts": { "lint": "npm run lint:lockfile", diff --git a/plugins/PassboltCe/EmailDigest/tests/Factory/EmailQueueFactory.php b/plugins/PassboltCe/EmailDigest/tests/Factory/EmailQueueFactory.php index c64963e94e..62a18c9307 100644 --- a/plugins/PassboltCe/EmailDigest/tests/Factory/EmailQueueFactory.php +++ b/plugins/PassboltCe/EmailDigest/tests/Factory/EmailQueueFactory.php @@ -55,7 +55,7 @@ protected function setDefaultTemplate(): void return [ 'email' => $email, - 'subject' => $faker->word(), + 'subject' => $faker->sentence(3), 'config' => 'default', 'template' => 'test_email', 'layout' => 'default', diff --git a/plugins/PassboltCe/EmailNotificationSettings/src/Utility/EmailNotificationSettings.php b/plugins/PassboltCe/EmailNotificationSettings/src/Utility/EmailNotificationSettings.php index 65f28caccb..cd22425f53 100644 --- a/plugins/PassboltCe/EmailNotificationSettings/src/Utility/EmailNotificationSettings.php +++ b/plugins/PassboltCe/EmailNotificationSettings/src/Utility/EmailNotificationSettings.php @@ -32,27 +32,13 @@ class EmailNotificationSettings { public const NAMESPACE = 'emailNotification'; - /** - * The settings. - * - * @var array|null - */ - private static $settings; + private static ?array $settings = null; - /** - * @var \Passbolt\EmailNotificationSettings\Utility\NotificationSettingsSource\ConfigEmailNotificationSettingsSource|null - */ - private static $configSettingsSource = null; + private static ?ConfigEmailNotificationSettingsSource $configSettingsSource = null; - /** - * @var \Passbolt\EmailNotificationSettings\Utility\NotificationSettingsSource\DbEmailNotificationSettingsSource|null - */ - private static $dbSettingsSource = null; + private static ?DbEmailNotificationSettingsSource $dbSettingsSource = null; - /** - * @var \Passbolt\EmailNotificationSettings\Utility\NotificationSettingsSource\DefaultEmailNotificationSettingsSource|null - */ - private static $defaultSettingsSource = null; + private static ?DefaultEmailNotificationSettingsSource $defaultSettingsSource = null; /** * Flush the cache version of the settings. @@ -74,7 +60,7 @@ public static function flushCache() * 2. configuration file * 3. the defaults in this function * - * @param string $key (optional) Key to lookup. If not provided, return all the settings. + * @param ?string $key (optional) Key to lookup. If not provided, return all the settings. * @return mixed */ public static function get(?string $key = null) @@ -100,7 +86,7 @@ public static function get(?string $key = null) * * @return array */ - protected static function getSettings() + protected static function getSettings(): array { $settings = static::getSettingsFromConfig(); $settings['sources'] = [ @@ -108,13 +94,11 @@ protected static function getSettings() 'file' => static::isDefaultSettingsAreOverridden(), ]; - if (static::getDbSettingsSource()->isAvailable()) { - try { - $dbSettings = static::getSettingsFromDb(); - $settings['sources']['database'] = true; - $settings = array_replace_recursive($settings, $dbSettings); - } catch (RecordNotFoundException $exception) { - } + try { + $dbSettings = static::getSettingsFromDb(); + $settings['sources']['database'] = true; + $settings = array_replace_recursive($settings, $dbSettings); + } catch (RecordNotFoundException $exception) { } return $settings; @@ -125,7 +109,7 @@ protected static function getSettings() * * @return array */ - protected static function getSettingsFromConfig() + protected static function getSettingsFromConfig(): array { return static::sanitizeSettings(static::getConfigSettingsSource()->read()); } @@ -151,7 +135,7 @@ protected static function sanitizeSettings(array $settings) /** * @return \Passbolt\EmailNotificationSettings\Utility\NotificationSettingsSource\ConfigEmailNotificationSettingsSource */ - protected static function getConfigSettingsSource() + protected static function getConfigSettingsSource(): ConfigEmailNotificationSettingsSource { if (!isset(static::$configSettingsSource)) { static::$configSettingsSource = new ConfigEmailNotificationSettingsSource(); @@ -167,7 +151,7 @@ protected static function getConfigSettingsSource() * @throws \Cake\Datasource\Exception\RecordNotFoundException If a matching DB config doesn't exist * @throws \Cake\Http\Exception\InternalErrorException If the DB config is not valid json string */ - protected static function getSettingsFromDb() + protected static function getSettingsFromDb(): array { return static::sanitizeSettings(static::getDbSettingsSource()->read()); } @@ -175,7 +159,7 @@ protected static function getSettingsFromDb() /** * @return \Passbolt\EmailNotificationSettings\Utility\NotificationSettingsSource\DbEmailNotificationSettingsSource */ - protected static function getDbSettingsSource() + protected static function getDbSettingsSource(): DbEmailNotificationSettingsSource { if (!isset(self::$dbSettingsSource)) { self::$dbSettingsSource = new DbEmailNotificationSettingsSource(); @@ -189,7 +173,7 @@ protected static function getDbSettingsSource() * * @return array */ - protected static function getSettingsFromDefault() + protected static function getSettingsFromDefault(): array { return static::getDefaultSettingsSource()->read(); } @@ -197,7 +181,7 @@ protected static function getSettingsFromDefault() /** * @return \Passbolt\EmailNotificationSettings\Utility\NotificationSettingsSource\DefaultEmailNotificationSettingsSource */ - protected static function getDefaultSettingsSource() + protected static function getDefaultSettingsSource(): DefaultEmailNotificationSettingsSource { if (!isset(static::$defaultSettingsSource)) { static::$defaultSettingsSource = DefaultEmailNotificationSettingsSource::fromCakeForm( @@ -232,7 +216,7 @@ protected static function isDefaultSettingsAreOverridden(): bool * @param bool $force Force saving even if the key is invalid/not yet registered (useful for testing purposes) * @return void */ - public static function save(array $configs, UserAccessControl $accessControl, bool $force = false) + public static function save(array $configs, UserAccessControl $accessControl, bool $force = false): void { // strip all non notification keys if ($force === false) { @@ -253,7 +237,7 @@ public static function save(array $configs, UserAccessControl $accessControl, bo * @param string $key The key to check. * @return bool */ - public static function isConfigKeyValid(string $key) + public static function isConfigKeyValid(string $key): bool { return Hash::check(static::getSettingsFromDefault(), static::underscoreToDottedFormat($key)); } @@ -264,7 +248,7 @@ public static function isConfigKeyValid(string $key) * @param string $key Key to normalize * @return string */ - public static function underscoreToDottedFormat(string $key) + public static function underscoreToDottedFormat(string $key): string { return str_replace('_', '.', $key); } diff --git a/plugins/PassboltCe/EmailNotificationSettings/src/Utility/NotificationSettingsSource/DbEmailNotificationSettingsSource.php b/plugins/PassboltCe/EmailNotificationSettings/src/Utility/NotificationSettingsSource/DbEmailNotificationSettingsSource.php index 5a5a1e6c51..eca9dbec97 100644 --- a/plugins/PassboltCe/EmailNotificationSettings/src/Utility/NotificationSettingsSource/DbEmailNotificationSettingsSource.php +++ b/plugins/PassboltCe/EmailNotificationSettings/src/Utility/NotificationSettingsSource/DbEmailNotificationSettingsSource.php @@ -20,7 +20,6 @@ use App\Utility\UserAccessControl; use Cake\Http\Exception\InternalErrorException; use Cake\ORM\TableRegistry; -use Exception; use Passbolt\EmailNotificationSettings\Utility\EmailNotificationSettings; use function json_decode; @@ -46,7 +45,7 @@ public function __construct() * @param \App\Utility\UserAccessControl $userAccessControl Instance of user access control * @return void */ - public function write(array $notificationSettingsData, UserAccessControl $userAccessControl) + public function write(array $notificationSettingsData, UserAccessControl $userAccessControl): void { $data = json_encode($notificationSettingsData); @@ -72,7 +71,7 @@ public function write(array $notificationSettingsData, UserAccessControl $userAc * @throws \Cake\Datasource\Exception\RecordNotFoundException If a matching DB config doesn't exist * @throws \Cake\Http\Exception\InternalErrorException If the DB config is not valid json string */ - public function read() + public function read(): array { $notificationSettingFromDb = $this->organizationSettingsTable->getFirstSettingOrFail(static::NAMESPACE); $settings = json_decode($notificationSettingFromDb->get('value'), true); @@ -84,22 +83,4 @@ public function read() return $settings; } - - /** - * Check if the table exists with a simple query to the database. - * This check must be done before using this notification settings source to avoid - * DB exception raised during installation because of queries run against the table when it does exist . - * - * @return bool - */ - public function isAvailable() - { - try { - $this->organizationSettingsTable->exists([]); - } catch (Exception $exception) { - return false; - } - - return true; - } } diff --git a/plugins/PassboltCe/EmailNotificationSettings/src/Utility/NotificationSettingsSource/DefaultEmailNotificationSettingsSource.php b/plugins/PassboltCe/EmailNotificationSettings/src/Utility/NotificationSettingsSource/DefaultEmailNotificationSettingsSource.php index b59b9de570..6d37a6d21f 100644 --- a/plugins/PassboltCe/EmailNotificationSettings/src/Utility/NotificationSettingsSource/DefaultEmailNotificationSettingsSource.php +++ b/plugins/PassboltCe/EmailNotificationSettings/src/Utility/NotificationSettingsSource/DefaultEmailNotificationSettingsSource.php @@ -66,7 +66,7 @@ public static function fromSettingsFormDefinition(EmailNotificationSettingsDefin /** * @return array */ - public function read() + public function read(): array { $defaultSettings = []; $fieldsList = $this->schema->fields(); diff --git a/plugins/PassboltCe/EmailNotificationSettings/tests/TestCase/Controllers/EmailNotificationSettingsBasicActionsControllerTest.php b/plugins/PassboltCe/EmailNotificationSettings/tests/TestCase/Controllers/EmailNotificationSettingsBasicActionsControllerTest.php new file mode 100644 index 0000000000..76e88b34e8 --- /dev/null +++ b/plugins/PassboltCe/EmailNotificationSettings/tests/TestCase/Controllers/EmailNotificationSettingsBasicActionsControllerTest.php @@ -0,0 +1,60 @@ +setField('value', $invalidJsonString)->persist(); + $this->get('/settings.json'); + $this->assertResponseOk(); + } + + public function testEmailNotificationSettingsBasicActionsController_View_Avatar_On_Invalid_Email_Settings(): void + { + $invalidJsonString = '{foo: '; + EmailNotificationSettingFactory::make()->setField('value', $invalidJsonString)->persist(); + $this->get('/avatars/view/75dc2799-a85c-47c1-8dff-c4ab3efb19ca/small.jpg'); + $this->assertResponseOk(); + } + + /** + * This action triggers an email. Therefore, if the settings in the DB are not consistent, an error + * is triggered + */ + public function testEmailNotificationSettingsBasicActionsController_Create_Resource_On_Invalid_Email_Settings(): void + { + $invalidJsonString = '{foo: '; + EmailNotificationSettingFactory::make()->setField('value', $invalidJsonString)->persist(); + + $this->logInAsUser(); + $data = $this->getDummyResourcesPostData([ + 'name' => '新的專用資源名稱', + 'username' => 'username@domain.com', + 'uri' => 'https://www.域.com', + 'description' => '新的資源描述', + ]); + $this->postJson('/resources.json', $data); + $this->assertResponseError('Could not validate resource data.'); + } +} diff --git a/plugins/PassboltCe/Folders/src/Notification/Email/CreateFolderEmailRedactor.php b/plugins/PassboltCe/Folders/src/Notification/Email/CreateFolderEmailRedactor.php index 91eb8884c8..2a399decc8 100644 --- a/plugins/PassboltCe/Folders/src/Notification/Email/CreateFolderEmailRedactor.php +++ b/plugins/PassboltCe/Folders/src/Notification/Email/CreateFolderEmailRedactor.php @@ -62,6 +62,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.folder.create'; + } + /** * @param \Cake\Event\Event $event Event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/Folders/src/Notification/Email/DeleteFolderEmailRedactor.php b/plugins/PassboltCe/Folders/src/Notification/Email/DeleteFolderEmailRedactor.php index 21b2007d33..a94a2a2b80 100644 --- a/plugins/PassboltCe/Folders/src/Notification/Email/DeleteFolderEmailRedactor.php +++ b/plugins/PassboltCe/Folders/src/Notification/Email/DeleteFolderEmailRedactor.php @@ -64,6 +64,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.folder.delete'; + } + /** * @param \Cake\Event\Event $event Event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/Folders/src/Notification/Email/FoldersEmailRedactorPool.php b/plugins/PassboltCe/Folders/src/Notification/Email/FoldersEmailRedactorPool.php index 70603afbb1..46ea549c9d 100644 --- a/plugins/PassboltCe/Folders/src/Notification/Email/FoldersEmailRedactorPool.php +++ b/plugins/PassboltCe/Folders/src/Notification/Email/FoldersEmailRedactorPool.php @@ -28,23 +28,10 @@ class FoldersEmailRedactorPool extends AbstractSubscribedEmailRedactorPool */ public function getSubscribedRedactors(): array { - $redactors = []; - - if ($this->isRedactorEnabled('send.folder.create')) { - $redactors[] = new CreateFolderEmailRedactor(); - } - - if ($this->isRedactorEnabled('send.folder.update')) { - $redactors[] = new UpdateFolderEmailRedactor(); - } - - if ($this->isRedactorEnabled('send.folder.delete')) { - $redactors[] = new DeleteFolderEmailRedactor(); - } - - if ($this->isRedactorEnabled('send.folder.share')) { - $redactors[] = new ShareFolderEmailRedactor(); - } + $redactors[] = new CreateFolderEmailRedactor(); + $redactors[] = new UpdateFolderEmailRedactor(); + $redactors[] = new DeleteFolderEmailRedactor(); + $redactors[] = new ShareFolderEmailRedactor(); return $redactors; } diff --git a/plugins/PassboltCe/Folders/src/Notification/Email/ShareFolderEmailRedactor.php b/plugins/PassboltCe/Folders/src/Notification/Email/ShareFolderEmailRedactor.php index 8db256bbfd..3422b36483 100644 --- a/plugins/PassboltCe/Folders/src/Notification/Email/ShareFolderEmailRedactor.php +++ b/plugins/PassboltCe/Folders/src/Notification/Email/ShareFolderEmailRedactor.php @@ -63,6 +63,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.folder.share'; + } + /** * @param \Cake\Event\Event $event Event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/Folders/src/Notification/Email/UpdateFolderEmailRedactor.php b/plugins/PassboltCe/Folders/src/Notification/Email/UpdateFolderEmailRedactor.php index 2407c7401f..550add071d 100644 --- a/plugins/PassboltCe/Folders/src/Notification/Email/UpdateFolderEmailRedactor.php +++ b/plugins/PassboltCe/Folders/src/Notification/Email/UpdateFolderEmailRedactor.php @@ -71,6 +71,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.folder.update'; + } + /** * @param \Cake\Event\Event $event Event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/JwtAuthentication/src/Notification/Email/Redactor/JwtAuthenticationAttackEmailRedactor.php b/plugins/PassboltCe/JwtAuthentication/src/Notification/Email/Redactor/JwtAuthenticationAttackEmailRedactor.php index 7f6eb730e3..a7a3e46b0b 100644 --- a/plugins/PassboltCe/JwtAuthentication/src/Notification/Email/Redactor/JwtAuthenticationAttackEmailRedactor.php +++ b/plugins/PassboltCe/JwtAuthentication/src/Notification/Email/Redactor/JwtAuthenticationAttackEmailRedactor.php @@ -73,6 +73,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return null; + } + /** * @param \Cake\Event\Event $event User register event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwksGetService.php b/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwksGetService.php index 7a2f9a45bd..62d2212f36 100644 --- a/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwksGetService.php +++ b/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwksGetService.php @@ -17,15 +17,13 @@ namespace Passbolt\JwtAuthentication\Service\AccessToken; use Firebase\JWT\JWT; +use Passbolt\JwtAuthentication\Error\Exception\AccessToken\InvalidJwtKeyPairException; class JwksGetService extends JwtAbstractService { public const PUBLIC_KEY_PATH = self::JWT_CONFIG_DIR . 'jwt.pem'; - /** - * @var string - */ - protected $keyPath = self::PUBLIC_KEY_PATH; + protected string $keyPath = self::PUBLIC_KEY_PATH; /** * @return string[] @@ -33,19 +31,46 @@ class JwksGetService extends JwtAbstractService */ public function getPublicKey(): array { - $pubKey = $this->readKeyFileContent(); - $res = openssl_pkey_get_public($pubKey); - $detail = openssl_pkey_get_details($res); + $details = $this->getDetails(); return [ 'kty' => 'RSA', 'alg' => JwtTokenCreateService::JWT_ALG, 'use' => 'sig', - 'e' => JWT::urlsafeB64Encode($detail['rsa']['e']), - 'n' => JWT::urlsafeB64Encode($detail['rsa']['n']), + 'e' => JWT::urlsafeB64Encode($details['rsa']['e']), + 'n' => JWT::urlsafeB64Encode($details['rsa']['n']), ]; } + /** + * @return int + */ + public function getSecretKeySize(): int + { + $details = $this->getDetails(); + + return $details['bits'] ?? 0; + } + + /** + * @return array + * @throws \Passbolt\JwtAuthentication\Error\Exception\AccessToken\InvalidJwtKeyPairException if the public key file is not parsable. + */ + private function getDetails(): array + { + $pubKey = $this->readKeyFileContent(); + $res = openssl_pkey_get_public($pubKey); + if ($res === false) { + throw new InvalidJwtKeyPairException(__('The JWT public key could not be extracted.')); + } + $details = openssl_pkey_get_details($res); + if ($details === false) { + throw new InvalidJwtKeyPairException(__('The JWT public key details could not be read.')); + } + + return $details; + } + /** * @return string * @throws \Passbolt\JwtAuthentication\Error\Exception\AccessToken\InvalidJwtKeyPairException if the public key file is not found or not readable. diff --git a/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtAbstractService.php b/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtAbstractService.php index c42a87bbc5..71296916d1 100644 --- a/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtAbstractService.php +++ b/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtAbstractService.php @@ -23,10 +23,7 @@ abstract class JwtAbstractService public const USER_ACCESS_TOKEN_KEY = 'access_token'; public const JWT_CONFIG_DIR = CONFIG . 'jwt' . DS; - /** - * @var string - */ - protected $keyPath; + protected string $keyPath; /** * @param string $path Path to the secret/private key file diff --git a/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtKeyPairService.php b/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtKeyPairService.php index 43b3af784c..ce330d4427 100644 --- a/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtKeyPairService.php +++ b/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtKeyPairService.php @@ -24,20 +24,11 @@ class JwtKeyPairService { - /** - * @var \Passbolt\JwtAuthentication\Service\AccessToken\JwtTokenCreateService - */ - protected $secretService; + protected JwtTokenCreateService $secretService; - /** - * @var \Passbolt\JwtAuthentication\Service\AccessToken\JwksGetService - */ - protected $publicService; + protected JwksGetService $publicService; - /** - * @var int Key length - */ - protected $keyLength = JwtTokenCreateService::JWT_KEY_LENGTH; + protected int $keyLength = JwtTokenCreateService::JWT_KEY_LENGTH; /** * CreateJwtKeysService constructor. @@ -124,11 +115,7 @@ public function validateKeyPair(?string $uuid = null) throw new \Exception(__('The JWT public key could not be read or is not valid.')); } $publicKey = file_get_contents($this->publicService->getKeyPath()); - $details = openssl_pkey_get_details( - openssl_pkey_get_public($publicKey) - ); - - $secretKeySize = $details['bits'] ?? 0; + $secretKeySize = $this->publicService->getSecretKeySize(); if ($secretKeySize === 0) { throw new \Exception(__('The JWT public key could not be read or is not valid.')); diff --git a/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtTokenCreateService.php b/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtTokenCreateService.php index 81cd7c8f5d..7a017bd34f 100644 --- a/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtTokenCreateService.php +++ b/plugins/PassboltCe/JwtAuthentication/src/Service/AccessToken/JwtTokenCreateService.php @@ -31,10 +31,7 @@ class JwtTokenCreateService extends JwtAbstractService public const JWT_KEY_LENGTH = 4096; public const JWT_EXPIRY_CONFIG_KEY = 'passbolt.auth.token.access_token.expiry'; - /** - * @var string - */ - protected $keyPath = self::JWT_SECRET_KEY_PATH; + protected string $keyPath = self::JWT_SECRET_KEY_PATH; /** * @param string $userId The id of the user successfully logging in. diff --git a/plugins/PassboltCe/JwtAuthentication/tests/TestCase/Service/AccessToken/JwksGetServiceTest.php b/plugins/PassboltCe/JwtAuthentication/tests/TestCase/Service/AccessToken/JwksGetServiceTest.php new file mode 100644 index 0000000000..2bf9aa943b --- /dev/null +++ b/plugins/PassboltCe/JwtAuthentication/tests/TestCase/Service/AccessToken/JwksGetServiceTest.php @@ -0,0 +1,57 @@ +jwtDir = TMP . 'jwt' . rand(1, 10000); + DirectoryUtility::removeRecursively(TMP . 'jwt'); + mkdir($this->jwtDir); + $this->service = new JwksGetService(); + } + + public function tearDown(): void + { + parent::tearDown(); + DirectoryUtility::removeRecursively($this->jwtDir); + unset($this->service); + } + + public function testJwksGetService_Invalid_Public_Key() + { + $publicKeyContent = 'foo'; + $fileName = $this->jwtDir . DS . 'jwt.pem'; + $this->service->setKeyPath($fileName); + file_put_contents($fileName, $publicKeyContent); + + $this->expectException(InvalidJwtKeyPairException::class); + $this->expectExceptionMessage('The JWT public key could not be extracted.'); + $this->service->getPublicKey(); + } +} diff --git a/plugins/PassboltCe/Log/src/Command/ActionLogsPurgeCommand.php b/plugins/PassboltCe/Log/src/Command/ActionLogsPurgeCommand.php new file mode 100644 index 0000000000..0e2e4a7fbe --- /dev/null +++ b/plugins/PassboltCe/Log/src/Command/ActionLogsPurgeCommand.php @@ -0,0 +1,124 @@ +processUserService = $processUserService; + $this->purgeService = new ActionLogsPurgeService(); + } + + /** + * @inheritDoc + */ + public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser + { + $parser + ->setDescription([ + __('Purge action logs.'), + '' . + __('The performance of your instance might be degraded while the command is running.') + . '', + ]) + ->addOption('retention-in-days', [ + 'short' => 'r', + 'required' => true, + 'help' => __('Retention period in days.'), + ]) + ->addOption('dry-run', [ + 'short' => 'd', + 'boolean' => true, + 'default' => false, + 'help' => __('Dry run mode.'), + ]) + ->addOption('verbose', [ + 'short' => 'v', + 'boolean' => true, + 'default' => false, + 'help' => __('Display the count of logs grouped by actions before and after the purge.'), + ]); + + return $parser; + } + + /** + * @inheritDoc + */ + public function execute(Arguments $args, ConsoleIo $io): ?int + { + parent::execute($args, $io); + + // Root user is not allowed to execute this command. + $this->assertCurrentProcessUser($io, $this->processUserService); + + $retentionInDays = (int)$args->getOption('retention-in-days'); + $dryRun = $args->getOption('dry-run'); + if ($dryRun) { + $this->getDryRun($retentionInDays, $io); + } else { + $this->purge($retentionInDays, $io); + } + + return $this->successCode(); + } + + /** + * @param int $retentionInDays retention in days + * @param \Cake\Console\ConsoleIo $io Console IO + * @return void + */ + private function getDryRun(int $retentionInDays, ConsoleIo $io): void + { + $logs = $this->purgeService->dryRun($retentionInDays); + $data[] = ['Action', 'Count']; + /** @var \Passbolt\Log\Model\Entity\ActionLog $log */ + foreach ($logs as $log) { + $data[] = [$log['action'], $log['count']]; + } + $io->helper('Table')->output($data); + } + + /** + * @param int $retentionInDays retention in days + * @param \Cake\Console\ConsoleIo $io Console IO + * @return void + */ + private function purge(int $retentionInDays, ConsoleIo $io): void + { + $nEntriesDeleted = $this->purgeService->purge($retentionInDays); + $io->success(__('{0} action logs entries were deleted.', $nEntriesDeleted)); + } +} diff --git a/plugins/PassboltCe/Log/src/LogPlugin.php b/plugins/PassboltCe/Log/src/LogPlugin.php index 198eab475b..ba4c59e7f2 100644 --- a/plugins/PassboltCe/Log/src/LogPlugin.php +++ b/plugins/PassboltCe/Log/src/LogPlugin.php @@ -16,8 +16,11 @@ */ namespace Passbolt\Log; +use App\Service\Command\ProcessUserService; use Cake\Core\BasePlugin; +use Cake\Core\ContainerInterface; use Cake\Core\PluginApplicationInterface; +use Passbolt\Log\Command\ActionLogsPurgeCommand; use Passbolt\Log\Events\ActionLogsAfterCreateListener; use Passbolt\Log\Events\ActionLogsBeforeRenderListener; use Passbolt\Log\Events\ActionLogsModelListener; @@ -35,4 +38,13 @@ public function bootstrap(PluginApplicationInterface $app): void ->on(new ActionLogsBeforeRenderListener()) ->on(new ActionLogsModelListener()); } + + /** + * @inheritDoc + */ + public function services(ContainerInterface $container): void + { + $container->add(ActionLogsPurgeCommand::class) + ->addArgument(ProcessUserService::class); + } } diff --git a/plugins/PassboltCe/Log/src/Service/ActionLogs/ActionLogsPurgeService.php b/plugins/PassboltCe/Log/src/Service/ActionLogs/ActionLogsPurgeService.php new file mode 100644 index 0000000000..5bb1d43ebb --- /dev/null +++ b/plugins/PassboltCe/Log/src/Service/ActionLogs/ActionLogsPurgeService.php @@ -0,0 +1,178 @@ +get('Passbolt/Log.ActionLogs'); + try { + return $ActionLogsTable->deleteAll([ + 'ActionLogs.id IN' => $this->getActionLogsToPurge($retentionInDays)->select('id'), + ]); + } catch (\PDOException $exception) { + $createdBefore = FrozenDate::now()->subDays($retentionInDays); + $entitiesHistory = $ActionLogsTable->getAssociation('EntitiesHistory') + ->subquery() + ->select('EntitiesHistory.action_log_id') + ->where(['EntitiesHistory.created <' => $createdBefore]); + + return $ActionLogsTable->deleteQuery() + ->whereInList('ActionLogs.action_id', array_keys($this->getActionUuidsToPurge())) + ->where([ + 'ActionLogs.id NOT IN' => $entitiesHistory, + 'ActionLogs.created < ' => $createdBefore, + ]) + ->execute() + ->rowCount(); + } + } + + /** + * Dry run of the purge + * + * @param int $retentionInDays retention in days + * @return \Cake\ORM\Query + */ + public function dryRun(int $retentionInDays): Query + { + $total = $this->getActionLogsToPurge($retentionInDays) + ->select([ + 'count' => 'COUNT(*)', + 'action_id', + ]) + ->group('ActionLogs.action_id') + ->orderDesc('count'); + + $total->formatResults(function (CollectionInterface $results) { + $actionsToPurge = $this->getActionUuidsToPurge(); + $results = $results->map(function (ActionLog $entity) use ($actionsToPurge): ActionLog { + $entity->set('action', $actionsToPurge[$entity->action_id] ?? null); + + return $entity; + }); + $totalCount = $results->sumOf('count'); + + return $results->appendItem([ + 'count' => $totalCount, + 'action' => 'Total', + ]); + }); + + return $total; + } + + /** + * @param int $retentionInDays retention in days + * @return \Cake\ORM\Query + */ + private function getActionLogsToPurge(int $retentionInDays): Query + { + $createdBefore = FrozenDate::now()->subDays($retentionInDays); + $ActionLogsTable = TableRegistry::getTableLocator()->get('Passbolt/Log.ActionLogs'); + + return $ActionLogsTable->find() + ->leftJoinWith('EntitiesHistory') + ->whereInList('ActionLogs.action_id', array_keys($this->getActionUuidsToPurge())) + ->whereNull('EntitiesHistory.id') + ->where(['ActionLogs.created < ' => $createdBefore]); + } + + /** + * @return array + */ + private function getActionUuidsToPurge(): array + { + $uuids = []; + foreach ($this->getActionList() as $action) { + $uuids[UuidFactory::uuid($action)] = $action; + } + + return $uuids; + } + + /** + * List of actions to be purged + * + * @return string[] + */ + public function getActionList(): array + { + return [ + 'shell', + 'AccountRecoveryOrganizationPoliciesGet.get', + 'AccountSettingsIndex.index', + 'AuthLogin.loginGet', + 'AuthVerify.verifyGet', + 'CommentsView.view', + 'DirectorySettings.view', + 'FolderLogs.view', + 'FoldersIndex.index', + 'FoldersView.view', + 'GetCsrfToken.get', + 'GpgkeysIndex.index', + 'GroupsView.view', + 'GroupsIndex.index', + 'HealthcheckIndex.index', + 'Home.apiExtApp', + 'Home.apiApp', + 'Home.view', + 'MfaOrgSettingsGet.get', + 'MfaSetupSelectProvider.get', + 'NotificationOrgSettingsGet.get', + 'PasswordGeneratorSettings.index', + 'PermissionsView.viewAcoPermissions', + 'ResourceLogs.view', + 'ResourceTypesIndex.index', + 'ResourceTypesView.view', + 'ResourcesIndex.index', + 'ResourcesView.view', + 'RolesIndex.index', + 'SettingsIndex.index', + 'Share.dryRun', + 'ShareSearch.searchArosToShareWith', + 'SubscriptionsView.view', + 'TagsIndex.index', + 'TotpSetupGet.get', + 'TotpVerifyGet.get', + 'ThemesIndex.index', + 'UserLogs.viewByFolder', + 'UserLogs.viewByResource', + 'UsersIndex.index', + 'UsersView.view', + ]; + } +} diff --git a/plugins/PassboltCe/Log/tests/Factory/ActionLogFactory.php b/plugins/PassboltCe/Log/tests/Factory/ActionLogFactory.php index c6080b26ce..f309369d8e 100644 --- a/plugins/PassboltCe/Log/tests/Factory/ActionLogFactory.php +++ b/plugins/PassboltCe/Log/tests/Factory/ActionLogFactory.php @@ -62,6 +62,9 @@ protected function setDefaultTemplate(): void }); } + /** + * @return $this + */ public function setActionId(string $actionName) { return $this->setField('action_id', UuidFactory::uuid($actionName)); diff --git a/plugins/PassboltCe/Log/tests/TestCase/Command/ActionLogsPurgeCommandTest.php b/plugins/PassboltCe/Log/tests/TestCase/Command/ActionLogsPurgeCommandTest.php new file mode 100644 index 0000000000..12b28dc4d2 --- /dev/null +++ b/plugins/PassboltCe/Log/tests/TestCase/Command/ActionLogsPurgeCommandTest.php @@ -0,0 +1,102 @@ +useCommandRunner(); + $this->mockProcessUserService('www-data'); + $this->enableFeaturePlugin(LogPlugin::class); + } + + public function testActionLogsPurgeCommandHelp() + { + $this->exec('passbolt action_logs_purge -h'); + $this->assertExitSuccess(); + $this->assertOutputContains('Purge action logs.'); + $this->assertOutputContains('The performance of your instance might be degraded'); + $this->assertOutputContains('--dry-run, -d'); + $this->assertOutputContains('--retention-in-days'); + } + + public function testActionLogsPurgeCommand_Purge() + { + $action = 'AuthLogin.loginGet'; + $retentionPeriodInDays = 10; + [$actionToRetain] = ActionLogFactory::make([ + ['created' => FrozenDate::now()->subDays($retentionPeriodInDays)], + ['created' => FrozenDate::now()->subDays($retentionPeriodInDays + 1)], + ]) + ->setActionId($action) + ->persist(); + $this->exec('passbolt action_logs_purge -r ' . $retentionPeriodInDays); + $this->assertExitSuccess(); + $this->assertOutputContains('1 action logs entries were deleted.'); + $this->assertSame(1, ActionLogFactory::count()); + $this->assertSame($actionToRetain->get('id'), ActionLogFactory::firstOrFail()->get('id')); + } + + public function testActionLogsPurgeCommand_Dry_Run() + { + $action1 = 'AuthLogin.loginGet'; + $action2 = 'ResourceTypesIndex.index'; + $retentionPeriodInDays = 10; + ActionLogFactory::make([ + ['created' => FrozenDate::now()->subDays($retentionPeriodInDays)], + ['created' => FrozenDate::now()->subDays($retentionPeriodInDays + 1)], + ]) + ->setActionId($action1) + ->persist(); + + ActionLogFactory::make([ + ['created' => FrozenDate::now()->subDays($retentionPeriodInDays)], + ['created' => FrozenDate::now()->subDays($retentionPeriodInDays + 1)], + ['created' => FrozenDate::now()->subDays($retentionPeriodInDays + 2)], + ]) + ->setActionId($action2) + ->persist(); + + $this->exec('passbolt action_logs_purge -d -r ' . $retentionPeriodInDays); + $this->assertOutputContains('Action'); + $this->assertOutputContains('Count'); + $this->assertOutputContains('Total'); + $this->assertOutputContains($action1); + $this->assertOutputContains($action2); + $this->assertOutputContains('1'); // count for action1 + $this->assertOutputContains('2'); // count for action2 + $this->assertOutputContains('3'); // count for total + $this->assertExitSuccess(); + } +} diff --git a/plugins/PassboltCe/Log/tests/TestCase/Service/ActionLogs/ActionLogsPurgeServiceTest.php b/plugins/PassboltCe/Log/tests/TestCase/Service/ActionLogs/ActionLogsPurgeServiceTest.php new file mode 100644 index 0000000000..f411761274 --- /dev/null +++ b/plugins/PassboltCe/Log/tests/TestCase/Service/ActionLogs/ActionLogsPurgeServiceTest.php @@ -0,0 +1,93 @@ + [true], + 'purge' => [false], + ]; + } + + /** + * @dataProvider dataForTest + */ + public function testActionLogsPurgeService(bool $isDryRun) + { + $service = new ActionLogsPurgeService(); + + $actionsToDelete = $service->getActionList(); + // Limit the number of action logs persisted to 5 in order to speed up the test + $randKeys = array_rand($actionsToDelete, 5); + $totalCountToDelete = 0; + $totalCountToIgnore = rand(2, 5); + $entitiesHistoryCount = 0; + $retentionPeriodInDays = 30; + ActionLogFactory::make($totalCountToIgnore)->persist(); + foreach ($randKeys as $k) { + $action = $actionsToDelete[$k]; + $toDelete = rand(1, 5); + ActionLogFactory::make($toDelete) + ->setActionId($action) + ->created(FrozenDate::now()->subDays($retentionPeriodInDays + $toDelete)) + ->persist(); + $totalCountToDelete += $toDelete; + + // Ignore actions within retention period + $toIgnore = rand(1, 5); + ActionLogFactory::make($toIgnore) + ->setActionId($action) + ->created(FrozenDate::now()->subDays($toIgnore)) + ->persist(); + $totalCountToIgnore += $toIgnore; + + // Skip actions associated to some entity history + ActionLogFactory::make() + ->setActionId($action) + ->created(FrozenDate::now()->subDays($retentionPeriodInDays + $toDelete)) + ->with('EntitiesHistory') + ->persist(); + $totalCountToIgnore++; + $entitiesHistoryCount++; + } + + if ($isDryRun) { + $result = $service->dryRun($retentionPeriodInDays); + $expectedCount = count($randKeys) + 1; // Add one as we add a line with the total + $this->assertSame($expectedCount, $result->all()->count()); + $this->assertSame($totalCountToDelete + $totalCountToIgnore, ActionLogFactory::count()); + } else { + $result = $service->purge($retentionPeriodInDays); + $this->assertSame($result, $totalCountToDelete); + $this->assertSame($totalCountToIgnore, ActionLogFactory::count()); + } + $this->assertSame($entitiesHistoryCount, EntitiesHistoryFactory::count()); + } +} diff --git a/plugins/PassboltCe/MultiFactorAuthentication/src/Notification/Email/MfaUserSettingsResetEmailRedactor.php b/plugins/PassboltCe/MultiFactorAuthentication/src/Notification/Email/MfaUserSettingsResetEmailRedactor.php index 0d1222d37c..e1dd1a557f 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/src/Notification/Email/MfaUserSettingsResetEmailRedactor.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/src/Notification/Email/MfaUserSettingsResetEmailRedactor.php @@ -52,6 +52,14 @@ public function __construct() $this->Users = $this->fetchTable('Users'); } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return null; + } + /** * @param \App\Model\Entity\User $user user who got their settings deleted * @param \App\Utility\UserAccessControl $uac of user who deleted the settings diff --git a/plugins/PassboltCe/MultiFactorAuthentication/tests/Factory/MfaOrganizationSettingFactory.php b/plugins/PassboltCe/MultiFactorAuthentication/tests/Factory/MfaOrganizationSettingFactory.php index d9333eb4e2..6274657525 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/tests/Factory/MfaOrganizationSettingFactory.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/tests/Factory/MfaOrganizationSettingFactory.php @@ -124,6 +124,8 @@ protected function getDuoDefaultSettings( ?string $clientId = null, ?string $clientSecret = null ) { + // SEC-5652 Note to security researchers: these are not leaked credentials + // They look valid as they should pass validation, but are fake return [ MfaOrgSettings::DUO_CLIENT_ID => $clientId ?? 'DICPIC33F13IWF1FR52J', MfaOrgSettings::DUO_CLIENT_SECRET => $clientSecret ?? '7TkYNgK8AGAuv3KW12qhsJLeIc1mJjHDHC1siNYX', diff --git a/plugins/PassboltCe/MultiFactorAuthentication/tests/Lib/MfaOrgSettingsTestTrait.php b/plugins/PassboltCe/MultiFactorAuthentication/tests/Lib/MfaOrgSettingsTestTrait.php index 7cfa9f8f1a..1720dae61d 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/tests/Lib/MfaOrgSettingsTestTrait.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/tests/Lib/MfaOrgSettingsTestTrait.php @@ -64,6 +64,8 @@ public function getDefaultMfaOrgSettings(): array */ public function getDefaultDuoV2OrgSettings(): array { + // SEC-5652 Note to security researchers: these are not leaked credentials + // They look valid as they should pass validation, but are fake return [ 'salt' => 'pG2y71Uu0wx3PsnWvtGom2CK9AGouV5oW84VHtwQ', 'integrationKey' => 'UICPIC93F14RWR5F55SJ', @@ -79,6 +81,8 @@ public function getDefaultDuoV2OrgSettings(): array */ public function getDefaultDuoV4OrgSettings(): array { + // SEC-5652 Note to security researchers: these are not leaked credentials + // They look valid as they should pass validation, but are fake return [ 'clientId' => 'UICPIC93F14RWR5F55SJ', 'clientSecret' => '8tkYNgi8aGAqa3KW1eqhsJLfjc1nJnHDYC1siNYX', diff --git a/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Utility/MfaOrgSettingsDuoTraitTest.php b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Utility/MfaOrgSettingsDuoTraitTest.php index 0481fc43bb..b41d5763c8 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Utility/MfaOrgSettingsDuoTraitTest.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Utility/MfaOrgSettingsDuoTraitTest.php @@ -34,6 +34,8 @@ class MfaOrgSettingsDuoTraitTest extends MfaIntegrationTestCase 'providers' => [ MfaSettings::PROVIDER_DUO => true, ], + // SEC-5652 Note to security researchers: these are not leaked credentials + // They look valid as they should pass validation, but are fake MfaSettings::PROVIDER_DUO => [ 'clientId' => 'UICPIC93F14RWR5F55SJ', 'clientSecret' => '8tkYNgi8aGAqa3KW1eqhsJLfjc1nJnHDYC1siNYX', diff --git a/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Utility/MfaOrgSettingsTest.php b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Utility/MfaOrgSettingsTest.php index bd7a51eb06..79a1674ca2 100644 --- a/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Utility/MfaOrgSettingsTest.php +++ b/plugins/PassboltCe/MultiFactorAuthentication/tests/TestCase/Utility/MfaOrgSettingsTest.php @@ -311,6 +311,8 @@ public function testMfaOrgSettingsValidateDuoSettings_Success() { $duoSettings = new MfaOrgSettingsDuoService( [ + // SEC-5652 Note to security researchers: these are not leaked credentials + // They look valid as they should pass validation, but are fake MfaSettings::PROVIDER_DUO => [ MfaOrgSettings::DUO_CLIENT_ID => 'DICPIC33F13IWF1FR52J', MfaOrgSettings::DUO_API_HOSTNAME => 'api-42e9f2fe.duosecurity.com', diff --git a/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryExpiredResourcesEmailRedactor.php b/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryExpiredResourcesEmailRedactor.php index 27a8877bef..23a3538dc8 100644 --- a/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryExpiredResourcesEmailRedactor.php +++ b/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryExpiredResourcesEmailRedactor.php @@ -51,6 +51,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.password.expire'; + } + /** * @param \Cake\Event\Event $event Event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryPasswordMarkedExpiredEmailRedactor.php b/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryPasswordMarkedExpiredEmailRedactor.php index 76bdb0e533..9910ca6063 100644 --- a/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryPasswordMarkedExpiredEmailRedactor.php +++ b/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryPasswordMarkedExpiredEmailRedactor.php @@ -53,6 +53,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.password.expire'; + } + /** * @param \Cake\Event\Event $event Event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryRedactorPool.php b/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryRedactorPool.php index 3ec269f3f1..66da0a7ee7 100644 --- a/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryRedactorPool.php +++ b/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpiryRedactorPool.php @@ -29,10 +29,8 @@ class PasswordExpiryRedactorPool extends AbstractSubscribedEmailRedactorPool public function getSubscribedRedactors(): array { $redactors[] = new PasswordExpirySettingsUpdatedEmailRedactor(); - if ($this->isRedactorEnabled('send.password.expire')) { - $redactors[] = new PasswordExpiryExpiredResourcesEmailRedactor(); - $redactors[] = new PasswordExpiryPasswordMarkedExpiredEmailRedactor(); - } + $redactors[] = new PasswordExpiryExpiredResourcesEmailRedactor(); + $redactors[] = new PasswordExpiryPasswordMarkedExpiredEmailRedactor(); return $redactors; } diff --git a/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpirySettingsUpdatedEmailRedactor.php b/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpirySettingsUpdatedEmailRedactor.php index 262fcc870a..f6c7aa4a41 100644 --- a/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpirySettingsUpdatedEmailRedactor.php +++ b/plugins/PassboltCe/PasswordExpiry/src/Notification/Email/PasswordExpirySettingsUpdatedEmailRedactor.php @@ -50,6 +50,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return null; + } + /** * @param \Cake\Event\Event $event Event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/PasswordExpiry/tests/TestCase/Controller/Users/PasswordExpiryUsersEditDisableControllerTest.php b/plugins/PassboltCe/PasswordExpiry/tests/TestCase/Controller/Users/PasswordExpiryUsersEditDisableControllerTest.php index ccc9ade412..f404a39aba 100644 --- a/plugins/PassboltCe/PasswordExpiry/tests/TestCase/Controller/Users/PasswordExpiryUsersEditDisableControllerTest.php +++ b/plugins/PassboltCe/PasswordExpiry/tests/TestCase/Controller/Users/PasswordExpiryUsersEditDisableControllerTest.php @@ -22,6 +22,7 @@ use App\Test\Factory\UserFactory; use App\Test\Lib\AppIntegrationTestCase; use App\Test\Lib\Model\EmailQueueTrait; +use App\Utility\Purifier; use Cake\I18n\FrozenTime; use Passbolt\Log\Test\Factory\SecretAccessFactory; use Passbolt\PasswordExpiry\PasswordExpiryPlugin; @@ -45,7 +46,9 @@ public function setUp(): void public function testPasswordExpiryUsersEditDisableController_Success_Admin_Disable_User(): void { [$admin1, $admin2] = UserFactory::make(2)->admin()->persist(); - [$userToDisable, $ownerWithResourceShared1, $ownerWithGroupShared1] = UserFactory::make(3)->user()->persist(); + /** @var \App\Model\Entity\User $userToDisable */ + $userToDisable = UserFactory::make(['profile' => ['last_name' => 'O\'Conner']])->user()->persist(); + [$ownerWithResourceShared1, $ownerWithGroupShared1] = UserFactory::make(2)->user()->persist(); [$resourceSharedViewed, $resourceSharedNotViewed] = ResourceFactory::make(2) ->withPermissionsFor([$userToDisable, $ownerWithResourceShared1]) ->withSecretsFor([$userToDisable, $ownerWithResourceShared1]) @@ -88,15 +91,16 @@ public function testPasswordExpiryUsersEditDisableController_Success_Admin_Disab $this->assertFalse($resourcesSharedViaGroupNotViewed->isExpired()); $this->assertEmailQueueCount(4); + $userFullName = h(Purifier::clean($userFullName)); $this->assertEmailInBatchContains( "The user {$userFullName} has been suspended.", - h($admin1->username), + $admin1->username, '', false ); $this->assertEmailInBatchContains( "The user {$userFullName} has been suspended.", - h($admin2->username), + $admin2->username, '', false ); diff --git a/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/SelfRegistrationEmailRedactorPool.php b/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/SelfRegistrationEmailRedactorPool.php index ab55f407d3..20151027ed 100644 --- a/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/SelfRegistrationEmailRedactorPool.php +++ b/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/SelfRegistrationEmailRedactorPool.php @@ -32,10 +32,7 @@ public function getSubscribedRedactors(): array // This setting cannot be deactivated $redactors[] = new SelfRegistrationSettingsAdminEmailRedactor(); - - if ($this->isRedactorEnabled('send.admin.user.register.complete')) { - $redactors[] = new SelfRegistrationAdminEmailRedactor(); - } + $redactors[] = new SelfRegistrationAdminEmailRedactor(); return $redactors; } diff --git a/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/Settings/SelfRegistrationSettingsAdminEmailRedactor.php b/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/Settings/SelfRegistrationSettingsAdminEmailRedactor.php index 259a9c08a4..7df4c7600c 100644 --- a/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/Settings/SelfRegistrationSettingsAdminEmailRedactor.php +++ b/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/Settings/SelfRegistrationSettingsAdminEmailRedactor.php @@ -48,6 +48,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return null; + } + /** * @param \Cake\Event\Event $event User register event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/User/SelfRegistrationAdminEmailRedactor.php b/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/User/SelfRegistrationAdminEmailRedactor.php index 0a07ef5e88..246c7a46ee 100644 --- a/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/User/SelfRegistrationAdminEmailRedactor.php +++ b/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/User/SelfRegistrationAdminEmailRedactor.php @@ -47,6 +47,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.admin.user.register.complete'; + } + /** * @param \Cake\Event\Event $event User register event * @return \App\Notification\Email\EmailCollection diff --git a/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/User/SelfRegistrationUserEmailRedactor.php b/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/User/SelfRegistrationUserEmailRedactor.php index efbaa16cca..4746f3a147 100644 --- a/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/User/SelfRegistrationUserEmailRedactor.php +++ b/plugins/PassboltCe/SelfRegistration/src/Notification/Email/Redactor/User/SelfRegistrationUserEmailRedactor.php @@ -108,4 +108,12 @@ private function createEmailSelfRegister(User $user, AuthenticationToken $uac): static::EMAIL_TEMPLATE ); } + + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.user.create'; + } } diff --git a/plugins/PassboltCe/SmtpSettings/src/Service/Healthcheck/CustomSslOptionsSmtpSettingsHealthcheck.php b/plugins/PassboltCe/SmtpSettings/src/Service/Healthcheck/CustomSslOptionsSmtpSettingsHealthcheck.php new file mode 100644 index 0000000000..89a7936435 --- /dev/null +++ b/plugins/PassboltCe/SmtpSettings/src/Service/Healthcheck/CustomSslOptionsSmtpSettingsHealthcheck.php @@ -0,0 +1,143 @@ +sslOptions = $service->get(); + $default = $service->isDefault(); + + if ($default) { + $this->status = true; + + return $this; + } + + return $this; + } + + /** + * @inheritDoc + */ + public function domain(): string + { + return HealthcheckServiceCollector::DOMAIN_SMTP_SETTINGS; + } + + /** + * @inheritDoc + */ + public function isPassed(): bool + { + return $this->status; + } + + /** + * @inheritDoc + */ + public function level(): string + { + $level = HealthcheckServiceCollector::LEVEL_WARNING; + + if (!$this->status && !$this->isSslVerificationDisabled($this->sslOptions)) { + $level = HealthcheckServiceCollector::LEVEL_NOTICE; + } + + return $level; + } + + /** + * @inheritDoc + */ + public function getSuccessMessage(): string + { + return __('No custom SSL configuration for SMTP server.'); + } + + /** + * @inheritDoc + */ + public function getFailureMessage(): string + { + $failureMessage = __('Custom SSL certificate options for SMTP server is in use.'); + if ($this->isSslVerificationDisabled($this->sslOptions)) { + $failureMessage = __('SSL certification validation for SMTP server is disabled.'); + } + + return $failureMessage; + } + + /** + * @inheritDoc + */ + public function getHelpMessage(): ?string + { + return null; + } + + /** + * CLI Option for this check. + * + * @return string + */ + public function cliOption(): string + { + return HealthcheckServiceCollector::DOMAIN_SMTP_SETTINGS; + } + + /** + * @inheritDoc + */ + public function getLegacyArrayKey(): string + { + return 'customSslOptions'; + } + + /** + * @param array $sslOptions SSL options to check. + * @return bool + */ + private function isSslVerificationDisabled(array $sslOptions): bool + { + return (isset($sslOptions['verify_peer']) && !$sslOptions['verify_peer']) + || (isset($sslOptions['verify_peer_name']) && !$sslOptions['verify_peer_name']); + } +} diff --git a/plugins/PassboltCe/SmtpSettings/src/Service/SmtpSettingsSslOptionsGetService.php b/plugins/PassboltCe/SmtpSettings/src/Service/SmtpSettingsSslOptionsGetService.php index 2bc069c9e3..61ae7073c8 100644 --- a/plugins/PassboltCe/SmtpSettings/src/Service/SmtpSettingsSslOptionsGetService.php +++ b/plugins/PassboltCe/SmtpSettings/src/Service/SmtpSettingsSslOptionsGetService.php @@ -20,6 +20,13 @@ class SmtpSettingsSslOptionsGetService { + /** + * Is configuration options are default or not. + * + * @var bool|null + */ + private ?bool $default = null; + /** * Returns empty array if values set are defaults as settings those again won't have any effect. * Otherwise, returns set configurations mapped as PHP SSL context options format (https://www.php.net/manual/en/context.ssl.php#refsect1-context.ssl-options). @@ -28,24 +35,50 @@ class SmtpSettingsSslOptionsGetService */ public function get(): array { - $configSslOptions = Configure::read('passbolt.plugins.smtpSettings.security', []); + $configSslOptions = $this->getConfigOptions(); - if ($this->isDefault($configSslOptions)) { + if ($this->checkDefaultOptions($configSslOptions)) { return []; } return $this->getMappedOptions($configSslOptions); } + /** + * Returns `true` if SSL options set in configuration are default, `false` otherwise. + * + * @return bool|null + */ + public function isDefault(): ?bool + { + if (is_null($this->default)) { + $configSslOptions = $this->getConfigOptions(); + + $this->checkDefaultOptions($configSslOptions); + } + + return $this->default; + } + + /** + * @return array + */ + private function getConfigOptions(): array + { + return Configure::read('passbolt.plugins.smtpSettings.security', []); + } + /** * Checks if SSL options set in configuration are defaults. * * @param array $configOptions SSL options set in configuration. * @return bool */ - private function isDefault(array $configOptions): bool + private function checkDefaultOptions(array $configOptions): bool { if (count($configOptions) !== 4) { + $this->default = false; + return false; } @@ -58,7 +91,9 @@ private function isDefault(array $configOptions): bool $result = $this->getMappedOptions($configOptions); - return empty(array_diff_assoc($defaults, $result)); + $this->default = empty(array_diff_assoc($defaults, $result)); + + return $this->default; } /** diff --git a/plugins/PassboltCe/SmtpSettings/src/SmtpSettingsPlugin.php b/plugins/PassboltCe/SmtpSettings/src/SmtpSettingsPlugin.php index e0a2fed887..9a698969ce 100644 --- a/plugins/PassboltCe/SmtpSettings/src/SmtpSettingsPlugin.php +++ b/plugins/PassboltCe/SmtpSettings/src/SmtpSettingsPlugin.php @@ -21,6 +21,7 @@ use Cake\Core\ContainerInterface; use Cake\Core\PluginApplicationInterface; use Passbolt\SmtpSettings\Event\SmtpTransportBeforeSendEventListener; +use Passbolt\SmtpSettings\Service\Healthcheck\CustomSslOptionsSmtpSettingsHealthcheck; use Passbolt\SmtpSettings\Service\Healthcheck\SettingsValidationSmtpSettingsHealthcheck; use Passbolt\SmtpSettings\Service\Healthcheck\SmtpSettingsEndpointsDisabledHealthcheck; use Passbolt\SmtpSettings\Service\Healthcheck\SmtpSettingsSettingsSourceHealthcheck; @@ -46,10 +47,12 @@ public function services(ContainerInterface $container): void $container->add(SettingsValidationSmtpSettingsHealthcheck::class); $container->add(SmtpSettingsSettingsSourceHealthcheck::class); $container->add(SmtpSettingsEndpointsDisabledHealthcheck::class); + $container->add(CustomSslOptionsSmtpSettingsHealthcheck::class); $container ->extend(HealthcheckServiceCollector::class) ->addMethodCall('addService', [SettingsValidationSmtpSettingsHealthcheck::class]) ->addMethodCall('addService', [SmtpSettingsSettingsSourceHealthcheck::class]) - ->addMethodCall('addService', [SmtpSettingsEndpointsDisabledHealthcheck::class]); + ->addMethodCall('addService', [SmtpSettingsEndpointsDisabledHealthcheck::class]) + ->addMethodCall('addService', [CustomSslOptionsSmtpSettingsHealthcheck::class]); } } diff --git a/plugins/PassboltCe/SmtpSettings/tests/TestCase/Service/Healthcheck/CustomSslOptionsSmtpSettingsHealthcheckTest.php b/plugins/PassboltCe/SmtpSettings/tests/TestCase/Service/Healthcheck/CustomSslOptionsSmtpSettingsHealthcheckTest.php new file mode 100644 index 0000000000..d8bc9583cd --- /dev/null +++ b/plugins/PassboltCe/SmtpSettings/tests/TestCase/Service/Healthcheck/CustomSslOptionsSmtpSettingsHealthcheckTest.php @@ -0,0 +1,92 @@ +service = new CustomSslOptionsSmtpSettingsHealthcheck(); + } + + public function tearDown(): void + { + unset($this->service); + + parent::tearDown(); + } + + public function testCustomSslOptionsSmtpSettingsHealthcheck_Pass_WithDefaultValues(): void + { + $this->service->check(); + + $this->assertTrue($this->service->isPassed()); + $this->assertSame(HealthcheckServiceCollector::LEVEL_WARNING, $this->service->level()); + $this->assertTextContains('No custom SSL configuration for SMTP server', $this->service->getSuccessMessage()); + $this->assertSame(HealthcheckServiceCollector::DOMAIN_SMTP_SETTINGS, $this->service->domain()); + $this->assertSame(HealthcheckServiceCollector::DOMAIN_SMTP_SETTINGS, $this->service->cliOption()); + } + + public function testCustomSslOptionsSmtpSettingsHealthcheck_Fail_WithInfoIfUsingCustomSSLOptions(): void + { + Configure::write('passbolt.plugins.smtpSettings.security', [ + 'sslVerifyPeer' => true, + 'sslVerifyPeerName' => true, + 'sslAllowSelfSigned' => true, + 'sslCafile' => '/path/to/rootCA.crt', + ]); + + $this->service->check(); + + $this->assertFalse($this->service->isPassed()); + $this->assertSame(HealthcheckServiceCollector::LEVEL_NOTICE, $this->service->level()); + $this->assertTextContains('Custom SSL certificate options for SMTP server is in use', $this->service->getFailureMessage()); + } + + public function testCustomSslOptionsSmtpSettingsHealthcheck_Fail_WithWarningIfSslVerificationIsDisabled(): void + { + Configure::write('passbolt.plugins.smtpSettings.security', [ + 'sslVerifyPeer' => false, + 'sslVerifyPeerName' => false, + 'sslAllowSelfSigned' => true, + ]); + + $this->service->check(); + + $this->assertFalse($this->service->isPassed()); + $this->assertSame(HealthcheckServiceCollector::LEVEL_WARNING, $this->service->level()); + $this->assertTextContains('SSL certification validation for SMTP server is disabled', $this->service->getFailureMessage()); + } +} diff --git a/plugins/PassboltCe/SmtpSettings/tests/TestCase/Service/SmtpSettingsSslOptionsGetServiceTest.php b/plugins/PassboltCe/SmtpSettings/tests/TestCase/Service/SmtpSettingsSslOptionsGetServiceTest.php index d6f4bd8af0..02eeaff6ca 100644 --- a/plugins/PassboltCe/SmtpSettings/tests/TestCase/Service/SmtpSettingsSslOptionsGetServiceTest.php +++ b/plugins/PassboltCe/SmtpSettings/tests/TestCase/Service/SmtpSettingsSslOptionsGetServiceTest.php @@ -55,6 +55,7 @@ public function testSmtpSettingsSslOptionsGetService() $result = $this->service->get(); $this->assertSame([], $result); + $this->assertTrue($this->service->isDefault()); } public function testSmtpSettingsSslOptionsGetService_Overridden_DefaultValues() @@ -70,6 +71,7 @@ public function testSmtpSettingsSslOptionsGetService_Overridden_DefaultValues() $result = $this->service->get(); $this->assertSame([], $result); + $this->assertTrue($this->service->isDefault()); } public function testSmtpSettingsSslOptionsGetService_Overridden_SpecificKeys() @@ -87,6 +89,7 @@ public function testSmtpSettingsSslOptionsGetService_Overridden_SpecificKeys() 'verify_peer_name' => false, 'allow_self_signed' => false, ], $result); + $this->assertFalse($this->service->isDefault()); } public function testSmtpSettingsSslOptionsGetService_Overridden_CAFile() @@ -102,6 +105,7 @@ public function testSmtpSettingsSslOptionsGetService_Overridden_CAFile() 'allow_self_signed' => true, 'cafile' => '/path/to/cafile.crt', ], $result); + $this->assertFalse($this->service->isDefault()); } public function testSmtpSettingsSslOptionsGetService_Overridden_AllValues() @@ -121,5 +125,22 @@ public function testSmtpSettingsSslOptionsGetService_Overridden_AllValues() 'allow_self_signed' => true, 'cafile' => '/etc/ssl/certs/mailpit/cert.pem', ], $result); + $this->assertFalse($this->service->isDefault()); + } + + public function testSmtpSettingsSslOptionsGetService_IsDefault_True() + { + $result = $this->service->isDefault(); + + $this->assertTrue($result); + } + + public function testSmtpSettingsSslOptionsGetService_IsDefault_False() + { + Configure::write('passbolt.plugins.smtpSettings.security', ['sslVerifyPeer' => false]); + + $result = $this->service->isDefault(); + + $this->assertFalse($result); } } diff --git a/resources/locales/ko_KR/default.po b/resources/locales/ko_KR/default.po index 5766cae345..5ab7f6684f 100644 --- a/resources/locales/ko_KR/default.po +++ b/resources/locales/ko_KR/default.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: 41c2572bd9bd4cc908d3e09e0cbed6e5\n" "POT-Creation-Date: 2024-04-24 04:35+0000\n" -"PO-Revision-Date: 2024-04-27 07:12\n" +"PO-Revision-Date: 2024-05-02 04:25\n" "Last-Translator: NAME \n" "Language-Team: Korean\n" "MIME-Version: 1.0\n" @@ -1259,7 +1259,7 @@ msgid "Or set the PASSBOLT_EMAIL_VALIDATE_MX environment variable to true." msgstr "또는 PASSBOLT_EMAIL_VALIDATE_MX 환경 변수를 true로 설정합니다." msgid "Or set passbolt.email.validate.mx to true in {0}." -msgstr "또는 {0}에서 passbolt.email.validate.mx 을 true로 설정합니다." +msgstr "또는 {0}에서 passbolt.email.validate.mx를 true로 설정합니다." msgid "Serving the compiled version of the javascript app." msgstr "자바스크립트 앱의 컴파일된 버전을 제공합니다." @@ -1328,7 +1328,7 @@ msgid "Enable the plugin in order to define self registration settings." msgstr "자체 등록 설정을 정의하려면 플러그인을 사용합니다." msgid "Registration is closed, only administrators can add users." -msgstr "가입이 종료되었습니다. 사용자를 추가할 수 있는 권한은 관리자에게만 있습니다." +msgstr "등록이 종료되었습니다. 사용자를 추가할 수 있는 권한은 관리자에게만 있습니다." msgid "The self registration provider is: {0}." msgstr "자체 등록 제공자 : {0}." @@ -1760,13 +1760,13 @@ msgid "SSL access is not enabled. You can still proceed, but it is highly recomm msgstr "SSL 접근을 사용할 수 없습니다. 계속 진행할 수 있지만 계속하기 전에 HTTPS를 사용하도록 웹 서버를 구성하는 것이 좋습니다." msgid "Not using a self-signed certificate." -msgstr "자체 서명된 인증서를 사용하지 않습니다." +msgstr "자체 서명 인증서를 사용하지 않습니다." msgid "Using a self-signed certificate." msgstr "자체 서명된 인증서를 사용합니다." msgid "SSL peer certificate validates." -msgstr "SSL 피어 인증서가 검증됩니다." +msgstr "SSL 피어 인증서가 유효합니다." msgid "SSL peer certificate does not validate." msgstr "SSL 피어 인증서가 검증되지 않습니다." @@ -3098,7 +3098,7 @@ msgid "{0} resources were affected." msgstr "{0} 개의 리소스가 영향을 받았습니다." msgid "It would be too much to list them here, but you can go check them on passbolt." -msgstr "여기에 나열하는 건 무리겠지만, 패스볼트로 확인하시면 됩니다." +msgstr "여기에 나열하는 건 무리겠지만, 패스볼트에서 확인하면 됩니다." msgid "Change them in passbolt" msgstr "패스볼트에서 변경" diff --git a/resources/locales/pl_PL/default.po b/resources/locales/pl_PL/default.po index 760e934f91..e9ab759b3d 100644 --- a/resources/locales/pl_PL/default.po +++ b/resources/locales/pl_PL/default.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: 41c2572bd9bd4cc908d3e09e0cbed6e5\n" "POT-Creation-Date: 2024-04-24 04:35+0000\n" -"PO-Revision-Date: 2024-04-24 14:20\n" +"PO-Revision-Date: 2024-05-03 20:48\n" "Last-Translator: NAME \n" "Language-Team: Polish\n" "MIME-Version: 1.0\n" @@ -956,7 +956,7 @@ msgid "The key should not already be expired." msgstr "Klucz nie może być już nieważny." msgid "The date could not be parsed." -msgstr "" +msgstr "Nie udało się sparsować danych." msgid "The email should be a valid email address." msgstr "E-mail powinien być prawidłowym adresem e-mail." @@ -983,7 +983,7 @@ msgid "{0} updated your memberships in several groups" msgstr "Użytkownik {0} zaktualizował Twoje członkostwo w kilku grupach" msgid "{0} deleted several group memberships" -msgstr "" +msgstr "Użytkownik {0} usunął kilka członkostw grupowych" msgid "You made changes on several resources" msgstr "" @@ -1241,19 +1241,19 @@ msgid "Could not validate group user data." msgstr "Nie udało się zweryfikować danych użytkownika grupy." msgid "All email notifications will be sent." -msgstr "" +msgstr "Wszystkie powiadomienia e-mail zostaną wysłane." msgid "Some email notifications are disabled by the administrator." -msgstr "" +msgstr "Niektóre powiadomienia e-mail są wyłączone przez administratora." msgid "Host availability will be checked." -msgstr "" +msgstr "Dostępność hosta zostanie sprawdzona." msgid "Host availability checking is disabled." -msgstr "" +msgstr "Sprawdzanie dostępności hosta jest wyłączone." msgid "Make sure this instance is not publicly available on the internet." -msgstr "" +msgstr "Upewnij się, że ta instancja nie jest publicznie dostępna w Internecie." msgid "Or set the PASSBOLT_EMAIL_VALIDATE_MX environment variable to true." msgstr "" @@ -1280,7 +1280,7 @@ msgid "Could not read tag information on github repository" msgstr "Nie udało się odczytać informacji o tagu w repozytorium github" msgid "Using latest passbolt version ({0})." -msgstr "" +msgstr "Używasz najnowszej wersji passbolt ({0})." msgid "Could connect to passbolt repository to check versions." msgstr "" @@ -1292,19 +1292,19 @@ msgid "Could not connect to passbolt repository to check versions" msgstr "" msgid "It is not possible check if your version is up to date." -msgstr "" +msgstr "Nie można sprawdzić, czy Twoja wersja jest aktualna." msgid "See https://www.passbolt.com/help/tech/update" -msgstr "" +msgstr "Sprawdź https://www.passbolt.com/help/tech/update" msgid "Check the network configuration to allow this script to check for updates." -msgstr "" +msgstr "Sprawdź konfigurację sieci, aby umożliwić temu skryptowi sprawdzanie aktualizacji." msgid "Search engine robots are told not to index content." -msgstr "" +msgstr "Roboty wyszukiwarek mają nie indeksować treści." msgid "Search engine robots are not told not to index content." -msgstr "" +msgstr "Roboty wyszukiwarek nie mają nie indeksować treści." msgid "Set passbolt.meta.robots to false in {0}." msgstr "" @@ -1352,13 +1352,13 @@ msgid "Set passbolt.ssl.force to true in {0}." msgstr "" msgid "App.fullBaseUrl is set to HTTPS." -msgstr "" +msgstr "App.fullBaseUrl jest ustawiony na HTTPS." msgid "App.fullBaseUrl is not set to HTTPS." -msgstr "" +msgstr "App.fullBaseUrl nie jest ustawiony na HTTPS." msgid "Check App.fullBaseUrl url scheme in {0}." -msgstr "" +msgstr "Sprawdź schemat adresu URL dla App.fullBaseUrl w {0}." msgid "The application config file is present" msgstr "" @@ -1367,7 +1367,7 @@ msgid "The application config file is missing in {0}" msgstr "" msgid "Copy {0} to {1}" -msgstr "" +msgstr "Skopiuj {0} do {1}" msgid "The passbolt config file is present" msgstr "" @@ -1379,19 +1379,19 @@ msgid "The passbolt config file is not required if passbolt is configured with e msgstr "" msgid "Cache is working." -msgstr "" +msgstr "Pamięć podręczna działa." msgid "Cache is NOT working." -msgstr "" +msgstr "Pamięć podręczna NIE działa." msgid "Check the settings in {0}" -msgstr "" +msgstr "Sprawdź ustawienia w {0}" msgid "Debug mode is off." -msgstr "" +msgstr "Tryb debugowania jest wyłączony." msgid "Debug mode is on." -msgstr "" +msgstr "Tryb debugowania jest włączony." msgid "Set debug to false in {0}" msgstr "" @@ -1403,7 +1403,7 @@ msgid "Full base url is not set. The application is using: {0}." msgstr "" msgid "Edit App.fullBaseUrl in {0}" -msgstr "" +msgstr "Edytuj App.fullBaseUrl w {0}" msgid "/healthcheck/status is reachable." msgstr "" @@ -1412,25 +1412,25 @@ msgid "Could not reach the /healthcheck/status with the url specified in App.ful msgstr "" msgid "Check that the domain name is correct in {0}" -msgstr "" +msgstr "Sprawdź, czy nazwa domeny jest poprawna w {0}" msgid "Check the network settings" -msgstr "" +msgstr "Sprawdź ustawienia sieci" msgid "Unique value set for security.salt" -msgstr "" +msgstr "Unikalna wartość ustawiona dla security.salt" msgid "Default value found for security.salt" -msgstr "" +msgstr "Znaleziono domyślną wartość dla security.salt" msgid "Edit the security.salt in {0}" -msgstr "" +msgstr "Edytuj security.salt w {0}" msgid "App.fullBaseUrl validation OK." -msgstr "" +msgstr "Test poprawności App.fullBaseUrl zakończony pomyślnie." msgid "App.fullBaseUrl does not validate. {0}." -msgstr "" +msgstr "App.fullBaseUrl nie przechodzi testu poprawności. {0}." msgid "Select a valid domain name as defined by section 2.3.1 of http://www.ietf.org/rfc/rfc1035.txt" msgstr "" @@ -1457,10 +1457,10 @@ msgid "Some default content is present." msgstr "" msgid "No default content found." -msgstr "" +msgstr "Nie znaleziono domyślnych treści." msgid "Run the install script to install the database tables" -msgstr "" +msgstr "Uruchom skrypt instalacyjny, aby zainstalować tabele bazy danych" msgid "The database schema up to date." msgstr "" @@ -1472,10 +1472,10 @@ msgid "Run the migration scripts:" msgstr "" msgid "{0} tables found." -msgstr "" +msgstr "Znalezione tabele: {0}" msgid "No table found." -msgstr "" +msgstr "Nie znaleziono tabeli." msgid "GD or Imagick extension is installed." msgstr "" @@ -1484,19 +1484,19 @@ msgid "You must enable the gd or imagick extensions to use Passbolt." msgstr "" msgid "See. https://secure.php.net/manual/en/book.image.php" -msgstr "" +msgstr "Sprawdź https://secure.php.net/manual/en/book.image.php" msgid "See. https://secure.php.net/manual/en/book.imagick.php" -msgstr "" +msgstr "Sprawdź https://secure.php.net/manual/en/book.imagick.php" msgid "Intl extension is installed." -msgstr "" +msgstr "Rozszerzenie Intl jest zainstalowane." msgid "You must enable the intl extension to use Passbolt." -msgstr "" +msgstr "Musisz włączyć rozszerzenie intl, aby korzystać z Passbolt." msgid "See. https://secure.php.net/manual/en/book.intl.php" -msgstr "" +msgstr "Sprawdź https://secure.php.net/manual/pl/book.intl.php" msgid "The logs directory and its content are writable." msgstr "" @@ -1511,10 +1511,10 @@ msgid "you can try:" msgstr "możesz spróbować:" msgid "Mbstring extension is installed." -msgstr "" +msgstr "Rozszerzenie mbstring jest zainstalowane." msgid "You must enable the mbstring extension to use Passbolt." -msgstr "" +msgstr "Musisz włączyć rozszerzenie mbstring, aby korzystać z Passbolt." msgid "See. https://secure.php.net/manual/en/book.mbstring.php" msgstr "" @@ -1535,10 +1535,10 @@ msgid "Recompile PCRE with Unicode support by adding --enable-unicode-properties msgstr "" msgid "PHP version {0}." -msgstr "" +msgstr "Wersja PHP {0}." msgid "PHP version is too low, passbolt need PHP {0} or higher." -msgstr "" +msgstr "Wersja PHP jest za stara, passbolt potrzebuje wersji PHP {0} lub wyższej." msgid "The temporary directory and its content are writable and not executable." msgstr "" @@ -1550,46 +1550,46 @@ msgid "Ensure the temporary directory and its content are writable by the webser msgstr "" msgid "The private key can be used to decrypt a message." -msgstr "" +msgstr "Klucz prywatny może być użyty do odszyfrowania wiadomości." msgid "The private key cannot be used to decrypt a message" -msgstr "" +msgstr "Klucz prywatny nie może być użyty do odszyfrowania wiadomości" msgid "The private key can be used to decrypt and verify a message." -msgstr "" +msgstr "Klucz prywatny może być użyty do odszyfrowania i zweryfikowania wiadomości." msgid "The private key cannot be used to decrypt and verify a message" -msgstr "" +msgstr "Klucz prywatny nie może być użyty do odszyfrowania i zweryfikowania wiadomości" msgid "The public key can be used to encrypt a message." -msgstr "" +msgstr "Klucz prywatny może być użyty do zaszyfrowania wiadomości." msgid "The public key cannot be used to encrypt a message" -msgstr "" +msgstr "Klucz prywatny nie może być użyty do zaszyfrowania wiadomości" msgid "Make sure that the server private key is valid and that there is no passphrase." -msgstr "" +msgstr "Upewnij się, że klucz prywatny serwera jest prawidłowy i że nie ma hasła dostępu." msgid "Make sure you imported the private server key in the keyring of the webserver user." -msgstr "" +msgstr "Upewnij się, że zaimportowałeś klucz prywatny serwera do pęku kluczy użytkownika serwera web." msgid "The public and private keys can be used to encrypt and sign a message." -msgstr "" +msgstr "Publiczne i prywatne klucze nie mogą być użyte do zaszyfrowania i podpisania wiadomości." msgid "The public and private keys cannot be used to encrypt and sign a message" -msgstr "" +msgstr "Publiczne i prywatne klucze nie mogą być użyte do zaszyfrowania i podpisania wiadomości" msgid "The private key can be used to sign a message." -msgstr "" +msgstr "Klucz prywatny może być użyty do podpisania wiadomości." msgid "The private key cannot be used to sign a message" -msgstr "" +msgstr "Klucz prywatny nie może być użyty do podpisania wiadomości" msgid "The public key can be used to verify a signature." -msgstr "" +msgstr "Klucz publiczny może być użyty do zweryfikowania podpisu." msgid "The public key cannot be used to verify a signature." -msgstr "" +msgstr "Klucz publiczny nie może być użyty do zweryfikowania podpisu." msgid "The server key fingerprint matches the one defined in {0}." msgstr "" @@ -1709,19 +1709,19 @@ msgid "Ensure the public key defined in {0} exists and is accessible by the webs msgstr "" msgid "Environment" -msgstr "" +msgstr "Środowisko" msgid "Config files" -msgstr "" +msgstr "Pliki konfiguracyjne" msgid "Core config" msgstr "" msgid "SMTP settings" -msgstr "" +msgstr "Ustawienia SMTP" msgid "Application configuration" -msgstr "" +msgstr "Konfiguracja aplikacji" msgid "Database" msgstr "Baza danych" @@ -1730,19 +1730,19 @@ msgid "GPG Configuration" msgstr "Konfiguracja GPG" msgid "JWT Authentication" -msgstr "" +msgstr "Uwierzytelnianie JWT" msgid "SSL Certificate" -msgstr "" +msgstr "Certyfikat SSL" msgid "The {0} plugin is enabled." -msgstr "" +msgstr "Wtyczka {0} jest włączona." msgid "The {0} plugin is disabled." -msgstr "" +msgstr "Wtyczka {0} jest wyłączona." msgid "Set the environment variable {0} to true" -msgstr "" +msgstr "Ustaw zmienną środowiskową {0} na wartość true" msgid "Enable the plugin in order to define SMTP settings in the database." msgstr "" @@ -2378,10 +2378,10 @@ msgid "The {0} directory should not be writable." msgstr "" msgid "A valid JWT key pair was found." -msgstr "" +msgstr "Znaleziono prawidłową parę kluczy JWT." msgid "A valid JWT key pair is missing." -msgstr "" +msgstr "Brakuje prawidłowej pary kluczy JWT." msgid "Run the create JWT keys script to create a valid JWT secret and public key pair:" msgstr "" @@ -3068,7 +3068,7 @@ msgid "{0} marked several passwords as expired" msgstr "" msgid "Some of your passwords expired" -msgstr "" +msgstr "Niektóre twoje hasła wygasły" msgid "{0} marked the password {1} as expired" msgstr "" @@ -3086,13 +3086,13 @@ msgid "The password expiry setting does not exist." msgstr "" msgid "Could not validate the password expiry settings." -msgstr "" +msgstr "Nie udało się zweryfikować ustawień ważności haseł." msgid "The password expiry settings have been updated." -msgstr "" +msgstr "Ustawienia ważności haseł zostały zaktualizowane." msgid "View them in passbolt" -msgstr "" +msgstr "Sprawdź je w passbolt" msgid "{0} resources were affected." msgstr "Naruszone zasoby: {0}" @@ -3101,25 +3101,25 @@ msgid "It would be too much to list them here, but you can go check them on pass msgstr "Nie damy rady ich tu wszystkich zmieścić, ale możesz je sprawdzić bezpośrednio w passbolt." msgid "Change them in passbolt" -msgstr "" +msgstr "Sprawdź je w passbolt" msgid "You have been requested to change them" -msgstr "" +msgstr "Poproszono cię o ich zmianę" msgid "Access for users to your shared passwords have been revoked." msgstr "" msgid "These passwords are now marked as expired." -msgstr "" +msgstr "Te hasła są teraz oznaczone jako nieważne." msgid "Please rotate them to ensure continued security." -msgstr "" +msgstr "Podmień je, aby zapewnić sobie ciągłość zabezpieczeń." msgid "Please rotate it to ensure continued security." -msgstr "" +msgstr "Podmień je, aby zapewnić sobie ciągłość zabezpieczeń." msgid "Change it in passbolt " -msgstr "" +msgstr "Zmień je w passbolt " msgid "Could not retrieve the password policies." msgstr "Nie udało się odzyskać polityk haseł." @@ -3575,7 +3575,7 @@ msgid "SMTP Settings coherent. You may send a test email to validate them." msgstr "" msgid "SMTP Setting errors: {0}" -msgstr "" +msgstr "Błędy ustawień SMTP: {0}" msgid "The {0} plugin endpoints are disabled." msgstr "" @@ -4148,10 +4148,10 @@ msgid "Contact your admin" msgstr "Skontaktuj się z administratorem" msgid "{0} changed your role to user." -msgstr "" +msgstr "Użytkownik {0} zmienił Twoją rolę na rolę użytkownika." msgid "{0} changed the role of {1} to user." -msgstr "" +msgstr "Użytkownik {0} mianował/a użytkownika {1} użytkownikiem." msgid "{0} can no longer perform administration tasks." msgstr "Użytkownik {0} nie może już wykonywać zadań administratorskich." @@ -4247,7 +4247,7 @@ msgid "You just opened an account on passbolt at {0}." msgstr "Twoje konto w passbolt zostało założone w dniu {0}." msgid "There was a change in the user directory." -msgstr "" +msgstr "Nastąpiła zmiana w katalogu użytkowników." msgid "You have been requested to add members to a group." msgstr "" diff --git a/src/Application.php b/src/Application.php index b2f3466c1a..59230e65cd 100644 --- a/src/Application.php +++ b/src/Application.php @@ -64,7 +64,6 @@ use Passbolt\EmailDigest\EmailDigestPlugin; use Passbolt\SelfRegistration\Service\DryRun\SelfRegistrationDefaultDryRunService; use Passbolt\SelfRegistration\Service\DryRun\SelfRegistrationDryRunServiceInterface; -use Passbolt\WebInstaller\Middleware\WebInstallerMiddleware; use Psr\Http\Message\ServerRequestInterface; class Application extends BaseApplication implements AuthenticationServiceProviderInterface @@ -203,12 +202,9 @@ public function getSolutionBootstrapper(): BaseSolutionBootstrapper */ public function initEmails() { - // Gather - if (WebInstallerMiddleware::isConfigured()) { - $this->getEventManager() - ->on(new CoreEmailRedactorPool()) - ->on(new CoreNotificationSettingsDefinition()); - } + $this->getEventManager() + ->on(new CoreEmailRedactorPool()) + ->on(new CoreNotificationSettingsDefinition()); } /** @@ -290,11 +286,11 @@ public function services(ContainerInterface $container): void $container->add(SessionIdentificationServiceInterface::class, SessionIdentificationService::class); $container->add(SelfRegistrationDryRunServiceInterface::class, SelfRegistrationDefaultDryRunService::class); $container->add(AbstractSecureCookieService::class, DefaultSecureCookieService::class); + $container->add(Client::class); $container->addServiceProvider(new TestEmailServiceProvider()); $container->addServiceProvider(new SetupServiceProvider()); $container->addServiceProvider(new ResourceServiceProvider()); $container->addServiceProvider(new UserServiceProvider()); - $container->add(Client::class)->setConcrete(null); if (PHP_SAPI === 'cli') { $container->addServiceProvider(new CommandServiceProvider()); } diff --git a/src/Authenticator/GpgAuthenticator.php b/src/Authenticator/GpgAuthenticator.php index 7b1c1abc68..0dfb282f7e 100644 --- a/src/Authenticator/GpgAuthenticator.php +++ b/src/Authenticator/GpgAuthenticator.php @@ -431,11 +431,15 @@ private function _error(?string $msg): bool /** * Validate the format of the nonce * - * @param string $nonce for example: 'gpgauthv1.3.0|36|de305d54-75b4-431b-adb2-eb6b9e546014|gpgauthv1.3.0' + * @param mixed $nonce Valid nonce example: 'gpgauthv1.3.0|36|de305d54-75b4-431b-adb2-eb6b9e546014|gpgauthv1.3.0' * @return bool true if valid, false otherwise */ - private function _checkNonce(string $nonce): bool + private function _checkNonce($nonce): bool { + if (!is_string($nonce)) { + return $this->_error(__('Invalid verify token type.')); + } + $result = explode('|', $nonce); $errorMsg = __('Invalid verify token format, '); if (count($result) != 4) { diff --git a/src/Command/HealthcheckCommand.php b/src/Command/HealthcheckCommand.php index 64a885da00..7d9dcd8209 100644 --- a/src/Command/HealthcheckCommand.php +++ b/src/Command/HealthcheckCommand.php @@ -225,7 +225,7 @@ private function appendResult(Collection $resultCollection, HealthcheckServiceIn public function render(HealthcheckServiceInterface $healthcheckService): void { switch ($healthcheckService->level()) { - case 'error': + case HealthcheckServiceCollector::LEVEL_ERROR: $this->assert( $healthcheckService->isPassed(), $healthcheckService->getSuccessMessage(), @@ -233,7 +233,7 @@ public function render(HealthcheckServiceInterface $healthcheckService): void $healthcheckService->getHelpMessage() ); break; - case 'warning': + case HealthcheckServiceCollector::LEVEL_WARNING: $this->warning( $healthcheckService->isPassed(), $healthcheckService->getSuccessMessage(), @@ -241,7 +241,7 @@ public function render(HealthcheckServiceInterface $healthcheckService): void $healthcheckService->getHelpMessage() ); break; - case 'notice': + case HealthcheckServiceCollector::LEVEL_NOTICE: $this->notice( $healthcheckService->isPassed(), $healthcheckService->getSuccessMessage(), diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 2ae9d7fe51..a08074abb1 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -31,6 +31,7 @@ use App\Service\Healthcheck\Gpg\PublicKeyReadableAndParsableGpgHealthcheck; use App\Service\Healthcheck\HealthcheckServiceCollector; use App\Service\Healthcheck\HealthcheckWithOptionsInterface; +use App\Service\Subscriptions\SubscriptionCheckInCommandServiceInterface; use App\Utility\Application\FeaturePluginAwareTrait; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; @@ -49,24 +50,26 @@ class InstallCommand extends PassboltCommand private HealthcheckServiceCollector $healthcheckServiceCollector; - /** - * @var \App\Service\Command\ProcessUserService - */ protected ProcessUserService $processUserService; + protected SubscriptionCheckInCommandServiceInterface $subscriptionCheckInCommandService; + /** * The client passed in the constructor might be null when run using the selenium tests * * @param \App\Service\Command\ProcessUserService $processUserService Process user service. + * @param \App\Service\Subscriptions\SubscriptionCheckInCommandServiceInterface $subscriptionCheckInCommandService Service checking the subscription validity. * @param \App\Service\Healthcheck\HealthcheckServiceCollector $healthcheckServiceCollector Health check service collector. */ public function __construct( ProcessUserService $processUserService, + SubscriptionCheckInCommandServiceInterface $subscriptionCheckInCommandService, HealthcheckServiceCollector $healthcheckServiceCollector ) { parent::__construct(); $this->processUserService = $processUserService; + $this->subscriptionCheckInCommandService = $subscriptionCheckInCommandService; $this->healthcheckServiceCollector = $healthcheckServiceCollector; } @@ -149,6 +152,8 @@ public function execute(Arguments $args, ConsoleIo $io): ?int return $this->quickInstall($args, $io); } // Normal mode + $this->subscriptionCheckInCommandService->check($this, $args, $io); + if (!$this->healthchecks($args, $io)) { return $this->errorCode(); } diff --git a/src/Command/MigrateCommand.php b/src/Command/MigrateCommand.php index 6955f9a0ae..09e030e65c 100644 --- a/src/Command/MigrateCommand.php +++ b/src/Command/MigrateCommand.php @@ -17,6 +17,7 @@ namespace App\Command; use App\Service\Command\ProcessUserService; +use App\Service\Subscriptions\SubscriptionCheckInCommandServiceInterface; use Cake\Command\CacheClearallCommand; use Cake\Console\Arguments; use Cake\Console\ConsoleIo; @@ -29,19 +30,22 @@ class MigrateCommand extends PassboltCommand { use DatabaseAwareCommandTrait; - /** - * @var \App\Service\Command\ProcessUserService - */ protected ProcessUserService $processUserService; + protected SubscriptionCheckInCommandServiceInterface $subscriptionCheckInCommandService; + /** * @param \App\Service\Command\ProcessUserService $processUserService Process user service. + * @param \App\Service\Subscriptions\SubscriptionCheckInCommandServiceInterface $subscriptionCheckInCommandService Service checking the subscription validity. */ - public function __construct(ProcessUserService $processUserService) - { + public function __construct( + ProcessUserService $processUserService, + SubscriptionCheckInCommandServiceInterface $subscriptionCheckInCommandService + ) { parent::__construct(); $this->processUserService = $processUserService; + $this->subscriptionCheckInCommandService = $subscriptionCheckInCommandService; } /** @@ -85,6 +89,9 @@ public function execute(Arguments $args, ConsoleIo $io): ?int return $this->errorCode(); } + // Normal mode + $this->subscriptionCheckInCommandService->check($this, $args, $io); + // Migration task $io->out(' ' . __('Running migration scripts.')); $io->hr(); diff --git a/src/Command/PassboltCommand.php b/src/Command/PassboltCommand.php index 0fc89a92ed..9cad4f3802 100644 --- a/src/Command/PassboltCommand.php +++ b/src/Command/PassboltCommand.php @@ -150,6 +150,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar 'help' => __d('cake_console', 'Show application logs.'), ]); + $parser->addArgument('show_queued_emails', [ + 'help' => __d('cake_console', 'Shows records from email_queue table.'), + ]); + $parser->addArgument('version', [ 'help' => __d('cake_console', 'Provide version number'), ]); @@ -194,7 +198,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int * @param array $options Options to append. * @return array */ - protected function formatOptions(Arguments $args, array $options = []): array + public function formatOptions(Arguments $args, array $options = []): array { if ($args->getOption('quiet') && !in_array('-q', $options)) { $options[] = '-q'; @@ -210,7 +214,7 @@ protected function formatOptions(Arguments $args, array $options = []): array * @param \Cake\Console\ConsoleIo $io Console IO. * @return void */ - protected function error(string $msg, ConsoleIo $io): void + public function error(string $msg, ConsoleIo $io): void { $io->out('' . $msg . ''); } diff --git a/src/Command/ShowQueuedEmailsCommand.php b/src/Command/ShowQueuedEmailsCommand.php new file mode 100644 index 0000000000..d23578f712 --- /dev/null +++ b/src/Command/ShowQueuedEmailsCommand.php @@ -0,0 +1,129 @@ +setDescription(__('Shows records from email_queue table.')); + + $parser->addOption('limit', [ + 'short' => 'l', + 'help' => __('Number of records to show.'), + 'default' => 15, + ]); + + $parser->addOption('failed', [ + 'help' => __('Return only failed records.'), + 'boolean' => true, + ]); + + $parser->addOption('oldest', [ + 'help' => __('Returns older records.'), + 'boolean' => true, + ]); + + return $parser; + } + + /** + * @inheritDoc + */ + public function execute(Arguments $args, ConsoleIo $io): ?int + { + parent::execute($args, $io); + + $this->assertOptions($args->getOptions(), $io); + + $limit = (int)$args->getOption('limit'); + $failed = $args->getOption('failed'); + $oldest = $args->getOption('oldest'); + + $io->out('List of queued emails:'); + + $data = []; + + // Header + $data[] = [ + __('Email'), + __('Subject'), + __('Error'), + __('Created'), + __('Sent'), + ]; + + /** @var \EmailQueue\Model\Table\EmailQueueTable $emailQueueTable */ + $emailQueueTable = TableRegistry::getTableLocator()->get('EmailQueue.EmailQueue'); + $order = $oldest ? 'ASC' : 'DESC'; + $queueEmails = $emailQueueTable->find() + ->select(['email', 'subject', 'error', 'created', 'sent']) + ->limit($limit) + ->order([$emailQueueTable->aliasField('created') => $order]); + + if ($failed) { + $queueEmails->where([$emailQueueTable->aliasField('error') . ' IS NOT' => null]); + } + + $queueEmails = $queueEmails->all(); + + if ($queueEmails->isEmpty()) { + $io->out('No records found.'); + + return $this->successCode(); + } + + /** @var \Cake\ORM\Entity $queueEmail */ + foreach ($queueEmails as $queueEmail) { + $data[] = [ + // sequence matters, see headers (first element) + $queueEmail->get('email'), + $queueEmail->get('subject'), + $queueEmail->get('error'), + $queueEmail->get('created')->format('Y-m-d H:i:s'), + $queueEmail->get('sent'), + ]; + } + + $io->helper('Table')->output($data); + + return $this->successCode(); + } + + /** + * @param array $options Options. + * @param \Cake\Console\ConsoleIo $io I/O object. + * @return void + */ + private function assertOptions(array $options, ConsoleIo $io): void + { + if (!Validation::range($options['limit'], 1, 100)) { + $this->error(__('Limit option value should be between 1 and 100.'), $io); + $this->abort(); + } + } +} diff --git a/src/Controller/AppController.php b/src/Controller/AppController.php index ed82e8f3b2..be41398f67 100644 --- a/src/Controller/AppController.php +++ b/src/Controller/AppController.php @@ -99,15 +99,13 @@ protected function success(?string $message = null, $body = null): void * Render an error response * * @param string|null $message optional message - * @param mixed $body optional json reponse body + * @param mixed $body optional json response body * @param int|null $errorCode optional http error code * @return void */ - protected function error(?string $message = null, $body = null, ?int $errorCode = 200): void + protected function error(?string $message = null, $body = null, ?int $errorCode = 400): void { - if ($errorCode !== 200) { - $this->response = $this->response->withStatus($errorCode); - } + $this->response = $this->response->withStatus($errorCode); $header = [ 'id' => UserAction::getInstance()->getUserActionId(), diff --git a/src/Controller/Auth/AuthLoginController.php b/src/Controller/Auth/AuthLoginController.php index 795814489b..e6dc39ede1 100644 --- a/src/Controller/Auth/AuthLoginController.php +++ b/src/Controller/Auth/AuthLoginController.php @@ -86,11 +86,22 @@ public function loginPost() } else { $errors = $result->getErrors(); $message = $errors['X-GPGAuth-Debug'] ?? 'The authentication failed.'; - if ($result->getStatus() === Result::FAILURE_OTHER) { - throw new InternalErrorException($message); - } - $this->error($message); + switch ($result->getStatus()) { + case Result::FAILURE_CREDENTIALS_MISSING: + // We return 200 because it's partial success and BExt relies on this status code + // Changing this would mean breaking compatibility. Be careful! + $this->error($message, null, 200); + break; + case Result::FAILURE_IDENTITY_NOT_FOUND: + $this->error($message); + break; + case Result::FAILURE_CREDENTIALS_INVALID: + $this->error($message); + break; + case Result::FAILURE_OTHER: + throw new InternalErrorException($message); + } } } } diff --git a/src/Notification/Email/AbstractSubscribedEmailRedactorPool.php b/src/Notification/Email/AbstractSubscribedEmailRedactorPool.php index 7b9d7d1db4..d751744e2e 100644 --- a/src/Notification/Email/AbstractSubscribedEmailRedactorPool.php +++ b/src/Notification/Email/AbstractSubscribedEmailRedactorPool.php @@ -17,7 +17,6 @@ namespace App\Notification\Email; use Cake\Event\EventListenerInterface; -use Passbolt\EmailNotificationSettings\Utility\EmailNotificationSettings; abstract class AbstractSubscribedEmailRedactorPool implements EventListenerInterface { @@ -42,17 +41,6 @@ final public function implementedEvents(): array ]; } - /** - * Return true if the redactor is enabled - * - * @param string $notificationSettingPath Notification Settings path with dot notation - * @return mixed - */ - protected function isRedactorEnabled(string $notificationSettingPath) - { - return EmailNotificationSettings::get($notificationSettingPath); - } - /** * @param \App\Notification\Email\CollectSubscribedEmailRedactorEvent $event Event object * @return void diff --git a/src/Notification/Email/EmailCollection.php b/src/Notification/Email/EmailCollection.php index 40e4578d4c..8bce7e98ad 100644 --- a/src/Notification/Email/EmailCollection.php +++ b/src/Notification/Email/EmailCollection.php @@ -28,7 +28,7 @@ class EmailCollection /** * @var \App\Notification\Email\Email[] */ - private $emails = []; + private array $emails = []; /** * @param \App\Notification\Email\Email[] $emails A list of emails @@ -60,7 +60,7 @@ public function addEmail(Email $email) /** * @return \App\Notification\Email\Email[] */ - public function getEmails() + public function getEmails(): array { return $this->emails; } diff --git a/src/Notification/Email/EmailSubscriptionDispatcher.php b/src/Notification/Email/EmailSubscriptionDispatcher.php index e86fa92369..84123ba3a7 100644 --- a/src/Notification/Email/EmailSubscriptionDispatcher.php +++ b/src/Notification/Email/EmailSubscriptionDispatcher.php @@ -20,7 +20,7 @@ use Cake\Event\EventDispatcherTrait; use Cake\Event\EventListenerInterface; use Cake\Event\EventManager; -use Psr\Log\LoggerInterface; +use Passbolt\EmailNotificationSettings\Utility\EmailNotificationSettings; use Psr\Log\NullLogger; use Throwable; @@ -43,12 +43,12 @@ class EmailSubscriptionDispatcher implements EventListenerInterface /** * @var \App\Notification\Email\EmailSubscriptionManager */ - private $emailSubscriptionManager; + private EmailSubscriptionManager $emailSubscriptionManager; /** * @var \App\Notification\Email\EmailSender */ - private $emailSender; + private EmailSender $emailSender; /** * @var \Psr\Log\LoggerInterface @@ -56,21 +56,14 @@ class EmailSubscriptionDispatcher implements EventListenerInterface private $logger; /** - * @param \Cake\Event\EventManager|null $eventManager Event Manager Instance - * @param \App\Notification\Email\EmailSubscriptionManager|null $emailSubscriptionManager instance - * @param \App\Notification\Email\EmailSender|null $emailSender EmailSender Instance - * @param \Psr\Log\LoggerInterface|null $logger Logger Instance + * The constructor */ - public function __construct( - ?EventManager $eventManager = null, - ?EmailSubscriptionManager $emailSubscriptionManager = null, - ?EmailSender $emailSender = null, - ?LoggerInterface $logger = null - ) { - $this->setEventManager($eventManager ?? EventManager::instance()); - $this->emailSubscriptionManager = $emailSubscriptionManager ?? new EmailSubscriptionManager(); - $this->emailSender = $emailSender ?? new EmailSender(); - $this->logger = $logger ?? new NullLogger(); + public function __construct() + { + $this->setEventManager(EventManager::instance()); + $this->emailSubscriptionManager = new EmailSubscriptionManager(); + $this->emailSender = new EmailSender(); + $this->logger = new NullLogger(); } /** @@ -110,6 +103,20 @@ public function __invoke(Event $event) $this->dispatch($event); } + /** + * Check if the email redactor should send emails, based on its settings + * If the path is null, we consider that this redactor cannot be configured, it is always enabled. + * + * @param \App\Notification\Email\SubscribedEmailRedactorInterface $emailRedactor email redactor + * @return bool + */ + private function isRedactorActive(SubscribedEmailRedactorInterface $emailRedactor): bool + { + $settingPath = $emailRedactor->getNotificationSettingPath(); + + return is_null($settingPath) || EmailNotificationSettings::get($settingPath); + } + /** * @param \Cake\Event\Event $event Event object to dispatch * @return void @@ -117,6 +124,9 @@ public function __invoke(Event $event) public function dispatch(Event $event) { foreach ($this->emailSubscriptionManager->getSubscriptionsForEvent($event) as $emailRedactor) { + if (!$this->isRedactorActive($emailRedactor)) { + continue; + } $emailCollection = $emailRedactor->onSubscribedEvent($event); if ($emailCollection->getEmails()) { diff --git a/src/Notification/Email/EmailSubscriptionManager.php b/src/Notification/Email/EmailSubscriptionManager.php index 33cd93607d..dbc95f0e73 100644 --- a/src/Notification/Email/EmailSubscriptionManager.php +++ b/src/Notification/Email/EmailSubscriptionManager.php @@ -33,7 +33,7 @@ class EmailSubscriptionManager /** * @var array */ - private $subscriptions = []; + private array $subscriptions = []; /** * @param \App\Notification\Email\SubscribedEmailRedactorInterface $subscribedEmailRedactor Email Redactor @@ -52,7 +52,7 @@ public function addNewSubscription(SubscribedEmailRedactorInterface $subscribedE * @param \Cake\Event\Event $event Event object * @return \App\Notification\Email\SubscribedEmailRedactorInterface[] */ - public function getSubscriptionsForEvent(Event $event) + public function getSubscriptionsForEvent(Event $event): array { return $this->subscriptions[$event->getName()] ?? []; } diff --git a/src/Notification/Email/Redactor/AdminUserSetupCompleteEmailRedactor.php b/src/Notification/Email/Redactor/AdminUserSetupCompleteEmailRedactor.php index 681d0c7791..61453d2443 100644 --- a/src/Notification/Email/Redactor/AdminUserSetupCompleteEmailRedactor.php +++ b/src/Notification/Email/Redactor/AdminUserSetupCompleteEmailRedactor.php @@ -62,6 +62,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.admin.user.setup.completed'; + } + /** * @param \Cake\Event\Event $event Event Instance * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Comment/CommentAddEmailRedactor.php b/src/Notification/Email/Redactor/Comment/CommentAddEmailRedactor.php index 5c1eb3bf91..9e0bdf66a2 100644 --- a/src/Notification/Email/Redactor/Comment/CommentAddEmailRedactor.php +++ b/src/Notification/Email/Redactor/Comment/CommentAddEmailRedactor.php @@ -137,4 +137,12 @@ function () use ($creator, $resource) { return new Email($recipient, $subject, $data, self::TEMPLATE); } + + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.comment.add'; + } } diff --git a/src/Notification/Email/Redactor/CoreEmailRedactorPool.php b/src/Notification/Email/Redactor/CoreEmailRedactorPool.php index 3727201899..de07946030 100644 --- a/src/Notification/Email/Redactor/CoreEmailRedactorPool.php +++ b/src/Notification/Email/Redactor/CoreEmailRedactorPool.php @@ -47,68 +47,29 @@ class CoreEmailRedactorPool extends AbstractSubscribedEmailRedactorPool /** * @return \App\Notification\Email\SubscribedEmailRedactorInterface[] */ - public function getSubscribedRedactors() + public function getSubscribedRedactors(): array { - $redactors = []; - - if ($this->isRedactorEnabled('send.user.create')) { - $redactors[] = new UserRegisterEmailRedactor(); - $redactors[] = new SelfRegistrationUserEmailRedactor(); - } - if ($this->isRedactorEnabled('send.comment.add')) { - $redactors[] = new CommentAddEmailRedactor(); - } - if ($this->isRedactorEnabled('send.user.recover')) { - $redactors[] = new AccountRecoveryEmailRedactor(); - } - if ($this->isRedactorEnabled('send.user.recoverComplete')) { - $redactors[] = new AccountRecoveryCompleteUserEmailRedactor(); - } - if ($this->isRedactorEnabled('send.admin.user.recover.abort')) { - $redactors[] = new SetupRecoverAbortAdminEmailRedactor(); - } - if ($this->isRedactorEnabled('send.admin.user.recover.complete')) { - $redactors[] = new AccountRecoveryCompleteAdminEmailRedactor(); - } - if ($this->isRedactorEnabled('send.admin.user.disable.user')) { - $redactors[] = new UserDisableEmailRedactor(); - } - if ($this->isRedactorEnabled('send.admin.user.disable.admin')) { - $redactors[] = new AdminDisableEmailRedactor(); - } - if ($this->isRedactorEnabled('send.password.share')) { - $redactors[] = new ShareEmailRedactor(); - } - if ($this->isRedactorEnabled('send.password.create')) { - $redactors[] = new ResourceCreateEmailRedactor(); - } - if ($this->isRedactorEnabled('send.password.update')) { - $redactors[] = new ResourceUpdateEmailRedactor(); - } - if ($this->isRedactorEnabled('send.password.delete')) { - $redactors[] = new ResourceDeleteEmailRedactor(); - } - if ($this->isRedactorEnabled('send.group.user.add')) { - $redactors[] = new GroupUserAddEmailRedactor(); - } - if ($this->isRedactorEnabled('send.group.delete')) { - $redactors[] = new GroupDeleteEmailRedactor(); - } - if ($this->isRedactorEnabled('send.group.user.update')) { - $redactors[] = new GroupUserUpdateEmailRedactor(); - } - if ($this->isRedactorEnabled('send.group.user.delete')) { - $redactors[] = new UserDeleteEmailRedactor(); - $redactors[] = new GroupUserDeleteEmailRedactor(); - } - if ($this->isRedactorEnabled('send.group.manager.update')) { - $redactors[] = new GroupUpdateAdminSummaryEmailRedactor(); - } - if ($this->isRedactorEnabled('send.group.manager.requestAddUser')) { - $redactors[] = new GroupUserAddRequestEmailRedactor(); - } - $logEnabled = Configure::read('passbolt.plugins.log.enabled'); - if ($this->isRedactorEnabled('send.admin.user.setup.completed') && $logEnabled) { + $redactors[] = new UserRegisterEmailRedactor(); + $redactors[] = new SelfRegistrationUserEmailRedactor(); + $redactors[] = new CommentAddEmailRedactor(); + $redactors[] = new AccountRecoveryEmailRedactor(); + $redactors[] = new AccountRecoveryCompleteUserEmailRedactor(); + $redactors[] = new SetupRecoverAbortAdminEmailRedactor(); + $redactors[] = new AccountRecoveryCompleteAdminEmailRedactor(); + $redactors[] = new UserDisableEmailRedactor(); + $redactors[] = new AdminDisableEmailRedactor(); + $redactors[] = new ShareEmailRedactor(); + $redactors[] = new ResourceCreateEmailRedactor(); + $redactors[] = new ResourceUpdateEmailRedactor(); + $redactors[] = new ResourceDeleteEmailRedactor(); + $redactors[] = new GroupUserAddEmailRedactor(); + $redactors[] = new GroupDeleteEmailRedactor(); + $redactors[] = new GroupUserUpdateEmailRedactor(); + $redactors[] = new UserDeleteEmailRedactor(); + $redactors[] = new GroupUserDeleteEmailRedactor(); + $redactors[] = new GroupUpdateAdminSummaryEmailRedactor(); + $redactors[] = new GroupUserAddRequestEmailRedactor(); + if (Configure::read('passbolt.plugins.log.enabled')) { $redactors[] = new AdminUserSetupCompleteEmailRedactor(); } if (Configure::read(UserAdminRoleRevokedEmailRedactor::CONFIG_KEY_EMAIL_ENABLED)) { diff --git a/src/Notification/Email/Redactor/Group/GroupDeleteEmailRedactor.php b/src/Notification/Email/Redactor/Group/GroupDeleteEmailRedactor.php index 8493bb2164..2d06a58603 100644 --- a/src/Notification/Email/Redactor/Group/GroupDeleteEmailRedactor.php +++ b/src/Notification/Email/Redactor/Group/GroupDeleteEmailRedactor.php @@ -61,6 +61,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.group.delete'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Group/GroupUpdateAdminSummaryEmailRedactor.php b/src/Notification/Email/Redactor/Group/GroupUpdateAdminSummaryEmailRedactor.php index 90e2a2abcb..446462ac2c 100644 --- a/src/Notification/Email/Redactor/Group/GroupUpdateAdminSummaryEmailRedactor.php +++ b/src/Notification/Email/Redactor/Group/GroupUpdateAdminSummaryEmailRedactor.php @@ -62,6 +62,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.group.manager.update'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Group/GroupUserAddEmailRedactor.php b/src/Notification/Email/Redactor/Group/GroupUserAddEmailRedactor.php index 5309bfcf8d..da31e33032 100644 --- a/src/Notification/Email/Redactor/Group/GroupUserAddEmailRedactor.php +++ b/src/Notification/Email/Redactor/Group/GroupUserAddEmailRedactor.php @@ -65,6 +65,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.group.user.add'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Group/GroupUserAddRequestEmailRedactor.php b/src/Notification/Email/Redactor/Group/GroupUserAddRequestEmailRedactor.php index 8d49dc718a..0ff14aadc7 100644 --- a/src/Notification/Email/Redactor/Group/GroupUserAddRequestEmailRedactor.php +++ b/src/Notification/Email/Redactor/Group/GroupUserAddRequestEmailRedactor.php @@ -71,6 +71,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.group.manager.requestAddUser'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Group/GroupUserDeleteEmailRedactor.php b/src/Notification/Email/Redactor/Group/GroupUserDeleteEmailRedactor.php index 5fd5b7bbf9..6320a7b6ba 100644 --- a/src/Notification/Email/Redactor/Group/GroupUserDeleteEmailRedactor.php +++ b/src/Notification/Email/Redactor/Group/GroupUserDeleteEmailRedactor.php @@ -67,6 +67,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.group.user.delete'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Group/GroupUserUpdateEmailRedactor.php b/src/Notification/Email/Redactor/Group/GroupUserUpdateEmailRedactor.php index 15ea7b74d2..8df7f52581 100644 --- a/src/Notification/Email/Redactor/Group/GroupUserUpdateEmailRedactor.php +++ b/src/Notification/Email/Redactor/Group/GroupUserUpdateEmailRedactor.php @@ -62,6 +62,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.group.user.update'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Recovery/AccountRecoveryCompleteAdminEmailRedactor.php b/src/Notification/Email/Redactor/Recovery/AccountRecoveryCompleteAdminEmailRedactor.php index ca24fa11bd..ce88a47a7d 100644 --- a/src/Notification/Email/Redactor/Recovery/AccountRecoveryCompleteAdminEmailRedactor.php +++ b/src/Notification/Email/Redactor/Recovery/AccountRecoveryCompleteAdminEmailRedactor.php @@ -46,6 +46,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.admin.user.recover.complete'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Recovery/AccountRecoveryCompleteUserEmailRedactor.php b/src/Notification/Email/Redactor/Recovery/AccountRecoveryCompleteUserEmailRedactor.php index 169576ac7d..c38cc8aa38 100644 --- a/src/Notification/Email/Redactor/Recovery/AccountRecoveryCompleteUserEmailRedactor.php +++ b/src/Notification/Email/Redactor/Recovery/AccountRecoveryCompleteUserEmailRedactor.php @@ -43,6 +43,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.user.recoverComplete'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Recovery/AccountRecoveryEmailRedactor.php b/src/Notification/Email/Redactor/Recovery/AccountRecoveryEmailRedactor.php index e8ebdffa50..5fa66e7f9b 100644 --- a/src/Notification/Email/Redactor/Recovery/AccountRecoveryEmailRedactor.php +++ b/src/Notification/Email/Redactor/Recovery/AccountRecoveryEmailRedactor.php @@ -46,6 +46,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.user.recover'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Resource/ResourceCreateEmailRedactor.php b/src/Notification/Email/Redactor/Resource/ResourceCreateEmailRedactor.php index 4882e19bf7..ca96b8c812 100644 --- a/src/Notification/Email/Redactor/Resource/ResourceCreateEmailRedactor.php +++ b/src/Notification/Email/Redactor/Resource/ResourceCreateEmailRedactor.php @@ -54,6 +54,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.password.create'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Resource/ResourceDeleteEmailRedactor.php b/src/Notification/Email/Redactor/Resource/ResourceDeleteEmailRedactor.php index 0f887b3ca0..aed61492fd 100644 --- a/src/Notification/Email/Redactor/Resource/ResourceDeleteEmailRedactor.php +++ b/src/Notification/Email/Redactor/Resource/ResourceDeleteEmailRedactor.php @@ -51,6 +51,14 @@ public function __construct(?array $config = [], ?UsersTable $usersTable = null) $this->usersTable = $usersTable ?? TableRegistry::getTableLocator()->get('Users'); } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.password.delete'; + } + /** * Return the list of events to which the redactor is subscribed and when it must create emails to be sent. * diff --git a/src/Notification/Email/Redactor/Resource/ResourceUpdateEmailRedactor.php b/src/Notification/Email/Redactor/Resource/ResourceUpdateEmailRedactor.php index d4427f7764..0146bd2618 100644 --- a/src/Notification/Email/Redactor/Resource/ResourceUpdateEmailRedactor.php +++ b/src/Notification/Email/Redactor/Resource/ResourceUpdateEmailRedactor.php @@ -65,6 +65,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.password.update'; + } + /** * @param \Cake\Event\Event $event Resource update event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Setup/SetupRecoverAbortAdminEmailRedactor.php b/src/Notification/Email/Redactor/Setup/SetupRecoverAbortAdminEmailRedactor.php index 210c12a7d4..4c7f09eabb 100644 --- a/src/Notification/Email/Redactor/Setup/SetupRecoverAbortAdminEmailRedactor.php +++ b/src/Notification/Email/Redactor/Setup/SetupRecoverAbortAdminEmailRedactor.php @@ -50,6 +50,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.admin.user.recover.abort'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/Share/ShareEmailRedactor.php b/src/Notification/Email/Redactor/Share/ShareEmailRedactor.php index 44b85626fe..6cf85245f2 100644 --- a/src/Notification/Email/Redactor/Share/ShareEmailRedactor.php +++ b/src/Notification/Email/Redactor/Share/ShareEmailRedactor.php @@ -63,6 +63,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.password.share'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/User/AdminDeleteEmailRedactor.php b/src/Notification/Email/Redactor/User/AdminDeleteEmailRedactor.php index ed46015353..a30c3e7608 100644 --- a/src/Notification/Email/Redactor/User/AdminDeleteEmailRedactor.php +++ b/src/Notification/Email/Redactor/User/AdminDeleteEmailRedactor.php @@ -70,6 +70,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return null; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/User/AdminDisableEmailRedactor.php b/src/Notification/Email/Redactor/User/AdminDisableEmailRedactor.php index 0807b54977..cffac9843a 100644 --- a/src/Notification/Email/Redactor/User/AdminDisableEmailRedactor.php +++ b/src/Notification/Email/Redactor/User/AdminDisableEmailRedactor.php @@ -49,6 +49,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.admin.user.disable.admin'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/User/UserAdminRoleRevokedEmailRedactor.php b/src/Notification/Email/Redactor/User/UserAdminRoleRevokedEmailRedactor.php index 327f5cf8ea..1c270627b1 100644 --- a/src/Notification/Email/Redactor/User/UserAdminRoleRevokedEmailRedactor.php +++ b/src/Notification/Email/Redactor/User/UserAdminRoleRevokedEmailRedactor.php @@ -53,6 +53,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return null; + } + /** * @param \Cake\Event\Event $event User register event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/User/UserDeleteEmailRedactor.php b/src/Notification/Email/Redactor/User/UserDeleteEmailRedactor.php index e1e4e06c92..8a816412c9 100644 --- a/src/Notification/Email/Redactor/User/UserDeleteEmailRedactor.php +++ b/src/Notification/Email/Redactor/User/UserDeleteEmailRedactor.php @@ -42,6 +42,14 @@ class UserDeleteEmailRedactor implements SubscribedEmailRedactorInterface */ protected $Users; + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.group.user.delete'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/User/UserDisableEmailRedactor.php b/src/Notification/Email/Redactor/User/UserDisableEmailRedactor.php index 2844aa741f..2461842890 100644 --- a/src/Notification/Email/Redactor/User/UserDisableEmailRedactor.php +++ b/src/Notification/Email/Redactor/User/UserDisableEmailRedactor.php @@ -55,6 +55,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.admin.user.disable.user'; + } + /** * @param \Cake\Event\Event $event User delete event * @return \App\Notification\Email\EmailCollection diff --git a/src/Notification/Email/Redactor/User/UserRegisterEmailRedactor.php b/src/Notification/Email/Redactor/User/UserRegisterEmailRedactor.php index 737a290e67..4c52f25e75 100644 --- a/src/Notification/Email/Redactor/User/UserRegisterEmailRedactor.php +++ b/src/Notification/Email/Redactor/User/UserRegisterEmailRedactor.php @@ -109,4 +109,12 @@ private function createEmailAdminRegister(User $user, AuthenticationToken $uac, static::TEMPLATE_REGISTER_ADMIN ); } + + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return 'send.user.create'; + } } diff --git a/src/Notification/Email/SubscribedEmailRedactorInterface.php b/src/Notification/Email/SubscribedEmailRedactorInterface.php index 99bec00a3d..5cb1fdfb45 100644 --- a/src/Notification/Email/SubscribedEmailRedactorInterface.php +++ b/src/Notification/Email/SubscribedEmailRedactorInterface.php @@ -54,4 +54,14 @@ public function subscribe(CollectSubscribedEmailRedactorEvent $event); * @return \App\Notification\Email\EmailCollection */ public function onSubscribedEvent(Event $event): EmailCollection; + + /** + * Gets the path of the email notification setting of this redactor + * If enabled in email settings, the redactor is active + * If disabled in the email settings, the redactor is inactive + * If the path is null, the redactor cannot be deactivated and is always active + * + * @return ?string + */ + public function getNotificationSettingPath(): ?string; } diff --git a/src/Service/Healthcheck/Application/LatestVersionApplicationHealthcheck.php b/src/Service/Healthcheck/Application/LatestVersionApplicationHealthcheck.php index 3e18ff4849..931004a209 100644 --- a/src/Service/Healthcheck/Application/LatestVersionApplicationHealthcheck.php +++ b/src/Service/Healthcheck/Application/LatestVersionApplicationHealthcheck.php @@ -20,6 +20,7 @@ use App\Service\Healthcheck\HealthcheckCliInterface; use App\Service\Healthcheck\HealthcheckServiceCollector; use App\Service\Healthcheck\HealthcheckServiceInterface; +use App\Service\Network\SocketService; use Cake\Core\Configure; use Cake\Http\Client; @@ -44,6 +45,20 @@ class LatestVersionApplicationHealthcheck implements HealthcheckServiceInterface */ private bool $exceptionThrown = false; + private Client $client; + + private SocketService $socketService; + + /** + * @param \Cake\Http\Client $client Client. + * @param \App\Service\Network\SocketService $socketService Socket service. + */ + public function __construct(Client $client, SocketService $socketService) + { + $this->client = $client; + $this->socketService = $socketService; + } + /** * @inheritDoc */ @@ -77,21 +92,19 @@ private function isLatestVersion(): bool /** * Return the current master version according to the official passbolt repository * - * @throws \Exception if the github repository is not reachable + * @throws \Cake\Network\Exception\SocketException If the github repository is not reachable * @throws \Exception if the tag information cannot be retrieved * @return string tag name such as 'v1.0.1' */ private function getLatestTagName(): string { + // Make sure github is reachable + $this->socketService->canConnect(['host' => 'github.com', 'port' => 443, 'timeout' => 30]); + $remoteTagName = Configure::read('passbolt.remote.version'); if (is_null($remoteTagName)) { - $url = 'https://api.github.com/repos/passbolt/passbolt_api/releases/latest'; - try { - $HttpSocket = new Client(); - $results = $HttpSocket->get($url); - } catch (\Exception $e) { - throw new \Exception(__('Could not connect to github repository')); - } + $results = $this->client->get('https://api.github.com/repos/passbolt/passbolt_api/releases/latest'); + $tags = json_decode($results->getStringBody(), true); if (!isset($tags['tag_name'])) { throw new \Exception(__('Could not read tag information on github repository')); @@ -151,9 +164,9 @@ public function getFailureMessage(): string $this->remoteVersion ); if ($this->exceptionThrown) { - $msg = __('Could not connect to passbolt repository to check versions'); + $msg = __('Could not connect to passbolt repository to check versions.'); $msg .= ' '; - $msg .= __('It is not possible check if your version is up to date.'); + $msg .= __('It is not possible to check if your version is up-to-date.'); } return $msg; diff --git a/src/Service/Network/SocketService.php b/src/Service/Network/SocketService.php new file mode 100644 index 0000000000..8fbc68f70a --- /dev/null +++ b/src/Service/Network/SocketService.php @@ -0,0 +1,34 @@ +connect(); + } +} diff --git a/src/Service/Subscriptions/DefaultSubscriptionCheckInCommandService.php b/src/Service/Subscriptions/DefaultSubscriptionCheckInCommandService.php new file mode 100644 index 0000000000..2d117040b5 --- /dev/null +++ b/src/Service/Subscriptions/DefaultSubscriptionCheckInCommandService.php @@ -0,0 +1,33 @@ +add(ProcessUserService::class); + $container->add( + SubscriptionCheckInCommandServiceInterface::class, + DefaultSubscriptionCheckInCommandService::class + ); $container->add(HealthcheckCommand::class)->addArguments([ ProcessUserService::class, @@ -55,10 +62,14 @@ public function services(ContainerInterface $container): void ]); $container->add(InstallCommand::class)->addArguments([ ProcessUserService::class, + SubscriptionCheckInCommandServiceInterface::class, HealthcheckServiceCollector::class, ]); $container->add(KeyringInitCommand::class)->addArgument(ProcessUserService::class); - $container->add(MigrateCommand::class)->addArgument(ProcessUserService::class); + $container->add(MigrateCommand::class)->addArguments([ + ProcessUserService::class, + SubscriptionCheckInCommandServiceInterface::class, + ]); $container->add(RecoverUserCommand::class)->addArgument(ProcessUserService::class); $container->add(MigratePostgresCommand::class)->addArgument(ProcessUserService::class); $container->add(RegisterUserCommand::class)->addArgument(ProcessUserService::class); diff --git a/src/ServiceProvider/HealthcheckServiceProvider.php b/src/ServiceProvider/HealthcheckServiceProvider.php index 4f89ff70a3..ed9233b245 100644 --- a/src/ServiceProvider/HealthcheckServiceProvider.php +++ b/src/ServiceProvider/HealthcheckServiceProvider.php @@ -72,6 +72,7 @@ use App\Service\Healthcheck\Ssl\IsRequestHttpsSslHealthcheck; use App\Service\Healthcheck\Ssl\NotSelfSignedSslHealthcheck; use App\Service\Healthcheck\Ssl\PeerValidSslHealthcheck; +use App\Service\Network\SocketService; use Cake\Core\ContainerInterface; use Cake\Core\ServiceProvider; use Cake\Http\Client; @@ -177,7 +178,10 @@ public function services(ContainerInterface $container): void $container->add(GopengpgPrivateKeyFormatGpgHealthcheck::class) ->addArgument(PublicKeyInKeyringGpgHealthcheck::class); // Application health checks - $container->add(LatestVersionApplicationHealthcheck::class); + $container->add(SocketService::class); + $container->add(LatestVersionApplicationHealthcheck::class) + ->addArgument(Client::class) + ->addArgument(SocketService::class); $container->add(SslForceApplicationHealthcheck::class); $container->add(SslFullBaseUrlApplicationHealthcheck::class); $container->add(SeleniumDisabledApplicationHealthcheck::class); diff --git a/tests/Lib/AppIntegrationTestCase.php b/tests/Lib/AppIntegrationTestCase.php index 93624af453..b1b19d6200 100644 --- a/tests/Lib/AppIntegrationTestCase.php +++ b/tests/Lib/AppIntegrationTestCase.php @@ -49,6 +49,7 @@ use Passbolt\EmailDigest\Utility\Digest\DigestTemplateRegistry; use Passbolt\EmailNotificationSettings\Utility\EmailNotificationSettings; use Passbolt\MultiFactorAuthentication\MultiFactorAuthenticationPlugin; +use Passbolt\MultiFactorAuthentication\Utility\MfaSettings; abstract class AppIntegrationTestCase extends TestCase { @@ -106,6 +107,7 @@ public function tearDown(): void DigestTemplateRegistry::clearInstance(); EmailNotificationSettings::flushCache(); $this->clearPlugins(); + MfaSettings::clear(); parent::tearDown(); } diff --git a/tests/Lib/AppTestCase.php b/tests/Lib/AppTestCase.php index de33af9d0b..6c9f37ce7a 100644 --- a/tests/Lib/AppTestCase.php +++ b/tests/Lib/AppTestCase.php @@ -31,6 +31,7 @@ use App\Test\Lib\Utility\ObjectTrait; use App\Test\Lib\Utility\UserAccessControlTrait; use App\Utility\Application\FeaturePluginAwareTrait; +use App\Utility\UserAction; use Cake\Core\Configure; use Cake\TestSuite\TestCase; use CakephpFixtureFactories\ORM\FactoryTableRegistry; @@ -86,6 +87,7 @@ public function tearDown(): void EmailNotificationSettings::flushCache(); $this->clearPlugins(); FactoryTableRegistry::getTableLocator()->clear(); + UserAction::destroy(); parent::tearDown(); } diff --git a/tests/TestCase/Command/InstallCommandTest.php b/tests/TestCase/Command/InstallCommandTest.php index 004976779e..9116e90f8a 100644 --- a/tests/TestCase/Command/InstallCommandTest.php +++ b/tests/TestCase/Command/InstallCommandTest.php @@ -17,6 +17,8 @@ namespace App\Test\TestCase\Command; use App\Model\Entity\Role; +use App\Service\Subscriptions\DefaultSubscriptionCheckInCommandService; +use App\Service\Subscriptions\SubscriptionCheckInCommandServiceInterface; use App\Test\Lib\AppTestCase; use App\Test\Lib\Model\EmailQueueTrait; use App\Test\Lib\Utility\HealthcheckRequestTestTrait; @@ -143,6 +145,9 @@ public function testInstallCommandNormalForceWithDataImport() public function testInstallCommandNormalNoForce_Will_Fail() { + $this->mockService(SubscriptionCheckInCommandServiceInterface::class, function () { + return new DefaultSubscriptionCheckInCommandService(); + }); $this->exec('passbolt install -d test'); $this->assertExitError(); $this->assertOutputContains('Some tables are already present in the database. A new installation would override existing data.'); @@ -151,6 +156,9 @@ public function testInstallCommandNormalNoForce_Will_Fail() public function testInstallCommandForce_Will_Fail_If_BaseUrlIsNotValid() { + $this->mockService(SubscriptionCheckInCommandServiceInterface::class, function () { + return new DefaultSubscriptionCheckInCommandService(); + }); Configure::write('App.fullBaseUrl', 'foo'); $this->exec('passbolt install --force -d test'); $this->assertExitError(); diff --git a/tests/TestCase/Command/ShowQueuedEmailsCommandTest.php b/tests/TestCase/Command/ShowQueuedEmailsCommandTest.php new file mode 100644 index 0000000000..ad3c089a6c --- /dev/null +++ b/tests/TestCase/Command/ShowQueuedEmailsCommandTest.php @@ -0,0 +1,162 @@ +useCommandRunner(); + } + + public function testShowQueuedEmailsCommand_Help() + { + $this->exec('passbolt show_queued_emails -h'); + + $this->assertExitSuccess(); + $this->assertOutputContains('Shows records from email_queue table.'); + $this->assertOutputContains('cake passbolt show_queued_emails'); + // Assert options + $this->assertOutputContains('--limit'); + $this->assertOutputContains('Number of records to show'); + } + + /** + * Data provider for testShowQueuedEmailsCommand_ValidationLimitOption() + * + * @return array + */ + public function invalidLimitOptionProvider(): array + { + return [ + [-100], + [0], + [101], + [2000], + ['abcd'], + ]; + } + + /** + * @dataProvider invalidLimitOptionProvider + */ + public function testShowQueuedEmailsCommand_ValidationLimitOption($limit): void + { + $this->exec('passbolt show_queued_emails --limit=' . $limit); + + $this->assertExitError('Limit option value should be between 1 and 100'); + } + + public function testShowQueuedEmailsCommand_Success(): void + { + EmailQueueFactory::make(['created' => Chronos::now()], 15)->persist(); + $oldEmail = EmailQueueFactory::make(['created' => Chronos::now()->subYears(30)])->persist(); + + $this->exec('passbolt show_queued_emails'); + + $this->assertSuccessOutput(); + // Assert old email is not in the output + $this->assertOutputNotContains($oldEmail['email']); + } + + public function testShowQueuedEmailsCommand_Success_LimitOption(): void + { + $emails = EmailQueueFactory::make(['created' => Chronos::now()], 2)->persist(); + $oldEmails = EmailQueueFactory::make(['created' => Chronos::now()->subYears(35)], 2)->persist(); + + $this->exec('passbolt show_queued_emails --limit=2'); + + $this->assertSuccessOutput(); + foreach ($emails as $email) { + $this->assertOutputContains($email['email']); + $this->assertOutputContains($email['subject']); + } + // Assert old email is not in the output + $this->assertOutputNotContains($oldEmails[1]['email']); + } + + public function testShowQueuedEmailsCommand_Success_FailedOption(): void + { + $email = EmailQueueFactory::make()->persist(); + $errorEmails = EmailQueueFactory::make(['error' => 'something went wrong'], 2)->persist(); + + $this->exec('passbolt show_queued_emails --failed'); + + $this->assertSuccessOutput(); + $this->assertOutputContains($errorEmails[0]['email']); + $this->assertOutputContains($errorEmails[0]['subject']); + $this->assertOutputContains($errorEmails[1]['email']); + $this->assertOutputContains($errorEmails[1]['subject']); + // Assert success email is not in the output + $this->assertOutputNotContains($email['email']); + $this->assertOutputNotContains($email['subject']); + } + + public function testShowQueuedEmailsCommand_Success_OldestOption(): void + { + $latestEmails = EmailQueueFactory::make(['created' => Chronos::now()], 2)->persist(); + $oldEmails = EmailQueueFactory::make(['created' => Chronos::now()->subMonths(5)], 2)->persist(); + + $this->exec('passbolt show_queued_emails --limit=2 --oldest'); + + $this->assertSuccessOutput(); + $this->assertOutputContains($oldEmails[0]['email']); + $this->assertOutputContains($oldEmails[0]['subject']); + $this->assertOutputContains($oldEmails[1]['email']); + $this->assertOutputContains($oldEmails[1]['subject']); + // Assert success email is not in the output + $this->assertOutputNotContains($latestEmails[0]['email']); + $this->assertOutputNotContains($latestEmails[0]['subject']); + } + + // --------------------------- + // Helper methods + // --------------------------- + + public function assertSuccessOutput(): void + { + $this->assertExitSuccess(); + + $this->assertOutputContains('List of queued emails'); + + $fields = [ + __('Email'), + __('Subject'), + __('Error'), + __('Created'), + __('Sent'), + ]; + foreach ($fields as $field) { + $this->assertOutputContains($field); + } + } +} diff --git a/tests/TestCase/Controller/Auth/AuthLoginControllerTest.php b/tests/TestCase/Controller/Auth/AuthLoginControllerTest.php index 3615cac6b6..eb713b43c2 100644 --- a/tests/TestCase/Controller/Auth/AuthLoginControllerTest.php +++ b/tests/TestCase/Controller/Auth/AuthLoginControllerTest.php @@ -16,6 +16,8 @@ */ namespace App\Test\TestCase\Controller\Auth; +use App\Model\Entity\AuthenticationToken; +use App\Test\Factory\AuthenticationTokenFactory; use App\Test\Factory\GpgkeyFactory; use App\Test\Factory\RoleFactory; use App\Test\Factory\UserFactory; @@ -25,6 +27,7 @@ use Cake\Core\Configure; use Cake\ORM\TableRegistry; use Cake\Validation\Validation; +use Passbolt\Log\LogPlugin; use Passbolt\Log\Test\Factory\ActionLogFactory; class AuthLoginControllerTest extends AppIntegrationTestCase @@ -68,6 +71,7 @@ public function testAuthLoginController_Error_NotJson(): void */ public function testRecoverAuthLoginController_Error_UserLoginAsDeletedUser(): void { + $this->enableFeaturePlugin(LogPlugin::class); UserFactory::make() ->with('Gpgkeys', GpgkeyFactory::make()->withAdaKey()) ->user() @@ -81,8 +85,13 @@ public function testRecoverAuthLoginController_Error_UserLoginAsDeletedUser(): v ], ], ]); - $msg = 'There is no user associated with this key. User not found.'; - $this->assertHeader('X-GPGAuth-Debug', $msg); + + $this->assertHeader('X-GPGAuth-Debug', 'There is no user associated with this key. User not found.'); + $this->assertResponseCode(400); + // Check status is correct in action logs + $actions = ActionLogFactory::find()->where(['action_id' => UuidFactory::uuid('AuthLogin.loginPost')])->toArray(); + $this->assertCount(1, $actions); + $this->assertSame(0, $actions[0]['status']); } /** @@ -141,7 +150,7 @@ public function testAuthLoginController_Error_BadServerKeyFingerprint(): void */ public function testAuthLoginController_GetHeaders(): void { - $this->enableFeaturePlugin('Log'); + $this->enableFeaturePlugin(LogPlugin::class); $this->get('/auth/login'); $this->assertResponseOk(); @@ -164,22 +173,25 @@ public function testAuthLoginController_Error_GetJson(): void $this->assertResponseCode(404); } - /** - * Fail without 500 if gpg_auth is not an array - */ public function testAuthLoginController_Validate_Gpg_Auth(): void { + $this->enableFeaturePlugin(LogPlugin::class); + $this->postJson('/auth/login.json', [ 'data' => [ 'gpg_auth' => 'foo', ], ]); - $this->assertResponseSuccess(); + $this->assertResponseCode(400); $msg = 'There is no user associated with this key. No key id set.'; $headers = $this->getHeaders(); $this->assertEquals($msg, $headers['X-GPGAuth-Debug']); $this->assertEquals($headers['X-GPGAuth-Authenticated'], 'false'); + // Check status is correct in action logs + $actions = ActionLogFactory::find()->where(['action_id' => UuidFactory::uuid('AuthLogin.loginPost')])->toArray(); + $this->assertCount(1, $actions); + $this->assertSame(0, $actions[0]['status']); } /** @@ -228,6 +240,7 @@ public function testAuthLoginController_AllStagesFingerprint(): void ], ], ]); + $headers = $this->getHeaders(); $this->assertTrue(isset($headers['X-GPGAuth-Authenticated']), 'Authentication headers should be set for keyid:"' . $keyid . '"'); $this->assertEquals($headers['X-GPGAuth-Authenticated'], 'false', 'The user should not be authenticated at that point'); @@ -428,6 +441,72 @@ public function testAuthLoginController_Stage1UserToken(): void $this->assertFalse($isValid, 'There should not be a valid auth token'); } + public function testAuthLoginController_Stage2_Success(): void + { + $user = UserFactory::make() + ->with('Gpgkeys', GpgkeyFactory::make()->withAdaKey()) + ->user() + ->active() + ->persist(); + $authenticationToken = AuthenticationTokenFactory::make(['user_id' => $user->id]) + ->type(AuthenticationToken::TYPE_LOGIN) + ->active() + ->persist(); + $token = 'gpgauthv1.3.0|36|' . $authenticationToken->get('token') . '|gpgauthv1.3.0'; + + $this->postJson('/auth/login.json', [ + 'data' => [ + 'gpg_auth' => [ + 'keyid' => $this->adaKeyId, + 'user_token_result' => $token, + ], + ], + ]); + + $this->assertSuccess(); + $headers = $this->getHeaders(); + $this->assertSame('true', $headers['X-GPGAuth-Authenticated']); + $this->assertSame('complete', $headers['X-GPGAuth-Progress']); + } + + public function invalidUserTokenProvider(): array + { + return [ + ['gpgauthv1.3.0|36|foo'], + [['foo' => 'bar']], + [[]], + [true], + ]; + } + + /** + * @dataProvider invalidUserTokenProvider + */ + public function testAuthLoginController_Stage2_ErrorInvalidUserToken($token): void + { + UserFactory::make() + ->with('Gpgkeys', GpgkeyFactory::make()->withAdaKey()) + ->user() + ->active() + ->persist(); + + $this->postJson('/auth/login.json', [ + 'data' => [ + 'gpg_auth' => [ + 'keyid' => $this->adaKeyId, + 'user_token_result' => $token, + ], + ], + ]); + + $this->assertSame(400, $this->_response->getStatusCode()); + $headers = $this->getHeaders(); + $this->assertSame('false', $headers['X-GPGAuth-Authenticated']); + $this->assertSame('stage2', $headers['X-GPGAuth-Progress']); + $this->assertTextContains('true', $headers['X-GPGAuth-Error']); + $this->assertTextContains('The user token result should be a valid UUID', $headers['X-GPGAuth-Debug']); + } + // ====== UTILITIES ========================================================= /** diff --git a/tests/TestCase/Controller/Healthcheck/HealthcheckIndexControllerTest.php b/tests/TestCase/Controller/Healthcheck/HealthcheckIndexControllerTest.php index 8e882a7f98..1ed95074b1 100644 --- a/tests/TestCase/Controller/Healthcheck/HealthcheckIndexControllerTest.php +++ b/tests/TestCase/Controller/Healthcheck/HealthcheckIndexControllerTest.php @@ -207,6 +207,7 @@ public function testHealthcheckIndexController_Success_Json(): void 'errorMessage' => false, 'source' => 'env variables', 'isInDb' => false, + 'customSslOptions' => true, ], ]; $this->assertArrayEqualsCanonicalizing($expectedResponse, $result); diff --git a/tests/TestCase/Notification/Email/EmailSubscriptionDispatcherTest.php b/tests/TestCase/Notification/Email/EmailSubscriptionDispatcherTest.php index 7fcba72959..1b139bf397 100644 --- a/tests/TestCase/Notification/Email/EmailSubscriptionDispatcherTest.php +++ b/tests/TestCase/Notification/Email/EmailSubscriptionDispatcherTest.php @@ -17,263 +17,76 @@ namespace App\Test\TestCase\Notification\Email; -use App\Model\Entity\User; -use App\Notification\Email\CollectSubscribedEmailRedactorEvent; use App\Notification\Email\Email; -use App\Notification\Email\EmailCollection; -use App\Notification\Email\EmailSender; use App\Notification\Email\EmailSubscriptionDispatcher; -use App\Notification\Email\EmailSubscriptionManager; -use App\Notification\Email\SubscribedEmailRedactorInterface; +use App\Notification\NotificationSettings\CoreNotificationSettingsDefinition; use App\Test\Factory\UserFactory; +use App\Test\Lib\Model\EmailQueueTrait; use Cake\Event\Event; use Cake\Event\EventManager; use Cake\TestSuite\TestCase; -use Exception; -use Psr\Log\LoggerInterface; +use CakephpTestSuiteLight\Fixture\TruncateDirtyTables; +use Passbolt\EmailNotificationSettings\Test\Lib\EmailNotificationSettingsTestTrait; class EmailSubscriptionDispatcherTest extends TestCase { + use EmailQueueTrait; + use EmailNotificationSettingsTestTrait; use SubscribedEmailRedactorMockTrait; + use TruncateDirtyTables; - /** - * @var \PHPUnit\Framework\MockObject\MockObject|EmailSender - */ - private $emailSenderMock; - - /** - * @var \PHPUnit\Framework\MockObject\MockObject|EventManager - */ - private $eventManagerMock; - - /** - * @var \PHPUnit\Framework\MockObject\MockObject|EmailSubscriptionManager - */ - private $emailSubscriptionManagerMock; - - /** - * @var EmailSubscriptionDispatcher - */ - private $sut; - - /** - * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface - */ - private $loggerMock; + private EmailSubscriptionDispatcher $sut; public function setUp(): void { parent::setUp(); - $this->eventManagerMock = $this->createMock(EventManager::class); - $this->emailSubscriptionManagerMock = $this->createMock(EmailSubscriptionManager::class); - $this->emailSenderMock = $this->createMock(EmailSender::class); - $this->loggerMock = $this->createMock(LoggerInterface::class); - - $this->sut = new EmailSubscriptionDispatcher( - $this->eventManagerMock, - $this->emailSubscriptionManagerMock, - $this->emailSenderMock, - $this->loggerMock - ); - } - - public function testEmailSubscriptionDispatcherCanBeInvokedAsEventListener() - { - $this->emailSubscriptionManagerMock->expects($this->once()) - ->method('getSubscribedEvents') - ->willReturn([]); - - $this->assertTrue(is_array($this->sut->implementedEvents())); - $this->assertTrue(is_callable($this->sut)); - - $this->emailSubscriptionManagerMock->expects($this->once()) - ->method('getSubscriptionsForEvent') - ->willReturn([]); - - call_user_func($this->sut, new Event('event')); - } - - /** - * @dataProvider provideSubscribedRedactors - * @param array $subscribedRedactors - * @param array $subscribedRedactorEmails - */ - public function testEmailSubscriptionDispatcherCreateAndSendEmailForEachRedactorSubscribedOnEvent( - array $subscribedRedactors, - array $subscribedRedactorEmails - ) { - $eventName = 'test_event'; - $event = new Event($eventName); - - $this->emailSubscriptionManagerMock->expects($this->once()) - ->method('getSubscriptionsForEvent') - ->with($event) - ->willReturn($subscribedRedactors); - - $this->emailSenderMock->expects($this->exactly(count($subscribedRedactors))) - ->method('sendEmail') - ->withConsecutive( - [$this->equalTo($subscribedRedactorEmails[0])], - [$this->equalTo($subscribedRedactorEmails[1])], - ); - - $this->sut->dispatch($event); - } - - public function testCollectDispatchCollectEvent() - { - $this->eventManagerMock->expects($this->once()) - ->method('dispatch') - ->with(CollectSubscribedEmailRedactorEvent::create($this->emailSubscriptionManagerMock)); - - $this->eventManagerMock->expects($this->once()) - ->method('on') - ->with($this->sut); - - $this->assertEquals($this->sut, $this->sut->collectSubscribedEmailRedactors()); + $this->sut = new EmailSubscriptionDispatcher(); } - public function testEmailSubscriptionDispatcherDoesNotSendEmailIfCollectionIsEmpty() + public function testEmailSubscriptionDispatcher() { - $eventName = 'test_event'; - $event = new Event($eventName); - $subscribedRedactorMock = $this->createMock(SubscribedEmailRedactorInterface::class); - $subscribedRedactors = [$subscribedRedactorMock]; - - $subscribedRedactorMock->expects($this->once()) - ->method('onSubscribedEvent') - ->willReturn(new EmailCollection()); - - $this->emailSubscriptionManagerMock->expects($this->once()) - ->method('getSubscriptionsForEvent') - ->with($event) - ->willReturn($subscribedRedactors); - - $this->emailSenderMock->expects($this->never()) - ->method('sendEmail'); - - $this->sut->dispatch($event); - } - - public function testEmailSubscriptionDispatcherSendEmailForEachEmailInCollection() - { - $eventName = 'test_event'; - $event = new Event($eventName); - $subscribedRedactorMock = $this->createMock(SubscribedEmailRedactorInterface::class); - $subscribedRedactors = [$subscribedRedactorMock]; - $emails = [ - new Email(UserFactory::make()->willDisable()->getEntity(), 'test', ['test'], 'test'), - new Email(UserFactory::make()->willDisable()->getEntity(), 'test', ['test'], 'test'), - ]; - - $subscribedRedactorMock->expects($this->once()) - ->method('onSubscribedEvent') - ->willReturn(new EmailCollection($emails)); - - $this->emailSubscriptionManagerMock->expects($this->once()) - ->method('getSubscriptionsForEvent') - ->with($event) - ->willReturn($subscribedRedactors); - - $this->emailSenderMock->expects($this->exactly(2)) - ->method('sendEmail') - ->withConsecutive( - [$this->equalTo($emails[0])], - [$this->equalTo($emails[1])], - ); - - $this->sut->dispatch($event); - } - - public function testEmailSubscriptionDispatcherSendEmailSkippedForDisabledRecipients() - { - $event = new Event('test_event'); - $subscribedRedactorMock = $this->createMock(SubscribedEmailRedactorInterface::class); - $subscribedRedactors = [$subscribedRedactorMock]; - $enabledUser = UserFactory::make()->willDisable()->getEntity(); - $userWithDisabledToNull = new User(['username' => 'Foo', 'disabled' => null]); - $disabledUser = UserFactory::make()->disabled()->getEntity(); - $userWithNoDisabledFieldProvided = new User(['username' => 'Foo']); - $emails = [ - new Email($enabledUser, 'test', ['test'], 'test'), - new Email($userWithDisabledToNull, 'test', ['test'], 'test'), - new Email($disabledUser, 'test', ['test'], 'test'), - new Email($userWithNoDisabledFieldProvided, 'test', ['test'], 'test'), - ]; - - $subscribedRedactorMock->expects($this->once()) - ->method('onSubscribedEvent') - ->willReturn(new EmailCollection($emails)); - - $this->emailSubscriptionManagerMock->expects($this->once()) - ->method('getSubscriptionsForEvent') - ->with($event) - ->willReturn($subscribedRedactors); - - $this->emailSenderMock->expects($this->exactly(2)) - ->method('sendEmail') - ->withConsecutive( - [$this->equalTo($emails[0])], - [$this->equalTo($emails[1])], - ); - - $this->sut->dispatch($event); - } - - public function testEmailSubscriptionDispatcherCatchEmailQueueExceptionAndLogError() - { - $eventName = 'test_event'; - $event = new Event($eventName); - $subscribedRedactorMock = $this->createMock(SubscribedEmailRedactorInterface::class); - $subscribedRedactors = [$subscribedRedactorMock]; - $emails = [ - new Email(UserFactory::make()->willDisable()->getEntity(), 'test', ['test'], 'test'), - new Email(UserFactory::make()->willDisable()->getEntity(), 'test', ['test'], 'test'), - ]; - $exception = new Exception(); - $emailCollection = new EmailCollection($emails); - - $subscribedRedactorMock->expects($this->once()) - ->method('onSubscribedEvent') - ->willReturn($emailCollection); - - $this->emailSubscriptionManagerMock->expects($this->once()) - ->method('getSubscriptionsForEvent') - ->with($event) - ->willReturn($subscribedRedactors); - - $this->emailSenderMock->expects($this->exactly(2)) - ->method('sendEmail') - ->willThrowException($exception); - - $this->loggerMock->expects($this->exactly(2)) - ->method('alert') - ->with( - sprintf('EmailSubscriptionDispatcher failed to send email on event `%s`', $eventName), - ['emailCollection' => $emailCollection, 'exception' => $exception] - ); - - $this->sut->dispatch($event); - } - - /** - * @return array - */ - public function provideSubscribedRedactors(): array - { - $emails = [ - new Email(UserFactory::make()->willDisable()->getEntity(), 'test_subject', [], 'test_template'), - new Email(UserFactory::make()->willDisable()->getEntity(), 'test_subject2', ['some_test_data'], 'test_template2'), - ]; + $event = 'foo'; + $settingActivated = 'send.comment.add'; + $this->setEmailNotificationSetting($settingActivated, true); + $settingDeactivated = 'show.comment'; + $userEnabled = UserFactory::make()->getEntity(); + $userEnabled->disabled = null; + $userDisabled = UserFactory::make()->disabled()->getEntity(); + $redactorAlwaysActive = $this->createSubscribedRedactor( + [$event], + new Email($userEnabled, 'redactorAlwaysActive', [], 'test') + ); + $redactorAlwaysRecipientDisabled = $this->createSubscribedRedactor( + [$event], + new Email($userDisabled, 'redactorAlwaysRecipientDisabled', [], 'test') + ); + $redactorOnSettingActivated = $this->createSubscribedRedactor( + [$event], + new Email($userEnabled, 'redactorOnSettingActivated', [], 'test'), + $settingActivated + ); + $redactorOnSettingDeactivated = $this->createSubscribedRedactor( + [$event], + new Email($userEnabled, 'redactorOnSettingDeactivated', [], 'test'), + $settingDeactivated + ); - return [ - [ - [ - $this->createSubscribedRedactor(['event_name'], $emails[0]), - $this->createSubscribedRedactor(['event_name'], $emails[1]), - ], - $emails, - ], - ]; + EventManager::instance() + ->on($redactorAlwaysActive) + ->on($redactorAlwaysRecipientDisabled) + ->on($redactorOnSettingActivated) + ->on($redactorOnSettingDeactivated) + ->on(new CoreNotificationSettingsDefinition()); + + $this->sut->collectSubscribedEmailRedactors(); + $this->sut->dispatch(new Event($event)); + + $this->assertEmailQueueCount(2); + $this->assertEmailIsInQueue([ + 'subject' => 'redactorAlwaysActive', + ]); + $this->assertEmailIsInQueue([ + 'subject' => 'redactorOnSettingActivated', + ]); } } diff --git a/tests/TestCase/Notification/Email/SubscribedEmailRedactorMockTrait.php b/tests/TestCase/Notification/Email/SubscribedEmailRedactorMockTrait.php index 585b945a01..bf5c62088e 100644 --- a/tests/TestCase/Notification/Email/SubscribedEmailRedactorMockTrait.php +++ b/tests/TestCase/Notification/Email/SubscribedEmailRedactorMockTrait.php @@ -17,7 +17,6 @@ namespace App\Test\TestCase\Notification\Email; -use App\Notification\Email\CollectSubscribedEmailRedactorEvent; use App\Notification\Email\Email; use App\Notification\Email\EmailCollection; use App\Notification\Email\SubscribedEmailRedactorInterface; @@ -26,26 +25,26 @@ trait SubscribedEmailRedactorMockTrait { - private function createSubscribedRedactor(array $subscribedEvents, Email $email) - { - return new class ($subscribedEvents, $email) implements SubscribedEmailRedactorInterface + private function createSubscribedRedactor( + array $subscribedEvents, + Email $email, + ?string $notificationSettingPath = null + ): SubscribedEmailRedactorInterface { + return new class ($subscribedEvents, $email, $notificationSettingPath) implements SubscribedEmailRedactorInterface { use SubscribedEmailRedactorTrait; - /** - * @var string - */ - private $subscribedEvents; + private array $subscribedEvents; - /** - * @var Email - */ - private $email; + private Email $email; - public function __construct(array $subscribedEvents, Email $email) + private ?string $notificationSettingPath; + + public function __construct(array $subscribedEvents, Email $email, ?string $notificationSettingPath) { $this->subscribedEvents = $subscribedEvents; $this->email = $email; + $this->notificationSettingPath = $notificationSettingPath; } public function onSubscribedEvent(Event $event): EmailCollection @@ -53,18 +52,17 @@ public function onSubscribedEvent(Event $event): EmailCollection return new EmailCollection([$this->email]); } - public function getSubscribedEvents(): array - { - return $this->subscribedEvents; - } - - public function subscribe(CollectSubscribedEmailRedactorEvent $event) + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string { + return $this->notificationSettingPath; } - public function implementedEvents(): array + public function getSubscribedEvents(): array { - return []; + return $this->subscribedEvents; } }; } diff --git a/tests/TestCase/Notification/Email/SubscribedEmailRedactorTraitTest.php b/tests/TestCase/Notification/Email/SubscribedEmailRedactorTraitTest.php index 7def180215..b1f95cba00 100644 --- a/tests/TestCase/Notification/Email/SubscribedEmailRedactorTraitTest.php +++ b/tests/TestCase/Notification/Email/SubscribedEmailRedactorTraitTest.php @@ -52,6 +52,14 @@ public function getSubscribedEvents(): array ]; } + /** + * @inheritDoc + */ + public function getNotificationSettingPath(): ?string + { + return null; + } + public function onSubscribedEvent(Event $event): EmailCollection { return new EmailCollection(); diff --git a/tests/TestCase/Service/Healthcheck/Application/LatestVersionApplicationHealthcheckTest.php b/tests/TestCase/Service/Healthcheck/Application/LatestVersionApplicationHealthcheckTest.php new file mode 100644 index 0000000000..535a7c888d --- /dev/null +++ b/tests/TestCase/Service/Healthcheck/Application/LatestVersionApplicationHealthcheckTest.php @@ -0,0 +1,65 @@ +check(); + + $this->assertTrue($service->isPassed()); + $this->assertTextNotEquals('undefined', $service->getRemoteVersion()); + } + + public function testLatestVersionApplicationHealthcheck_Error_GithubNotReachable(): void + { + $client = $this->getMockBuilder(Client::class)->disableOriginalConstructor()->getMock(); + $response = new Response([], json_encode(['tag_name' => '4.7.0'])); + $client->method('get')->willReturn($response); + // mock socket service + $socketService = $this->getMockBuilder(SocketService::class)->getMock(); + $socketService + ->method('canConnect') + ->willThrowException(new SocketException('Unable to connect')); + + $service = new LatestVersionApplicationHealthcheck($client, $socketService); + $service->check(); + + $this->assertFalse($service->isPassed()); + $this->assertSame('undefined', $service->getRemoteVersion()); + $expectedFailureMessage = 'Could not connect to passbolt repository to check versions. It is not possible to check if your version is up-to-date.'; + $this->assertSame($expectedFailureMessage, $service->getFailureMessage()); + } +} diff --git a/webroot/css/themes/default/api_authentication.min.css b/webroot/css/themes/default/api_authentication.min.css index b8a7a0f64a..ecb8e9bae8 100644 --- a/webroot/css/themes/default/api_authentication.min.css +++ b/webroot/css/themes/default/api_authentication.min.css @@ -1,7 +1,7 @@ /**! * @name passbolt-styleguide - * @version v4.7.0 - * @date 2024-04-24 + * @version v4.8.0 + * @date 2024-05-16 * @copyright Copyright 2023 Passbolt SA * @source https://github.com/passbolt/passbolt_styleguide * @license AGPL-3.0 diff --git a/webroot/css/themes/default/api_main.min.css b/webroot/css/themes/default/api_main.min.css index 83c9515145..3fdb42a731 100644 --- a/webroot/css/themes/default/api_main.min.css +++ b/webroot/css/themes/default/api_main.min.css @@ -1,7 +1,7 @@ /**! * @name passbolt-styleguide - * @version v4.7.0 - * @date 2024-04-24 + * @version v4.8.0 + * @date 2024-05-16 * @copyright Copyright 2023 Passbolt SA * @source https://github.com/passbolt/passbolt_styleguide * @license AGPL-3.0 diff --git a/webroot/css/themes/default/ext_authentication.min.css b/webroot/css/themes/default/ext_authentication.min.css index b8a7a0f64a..ecb8e9bae8 100644 --- a/webroot/css/themes/default/ext_authentication.min.css +++ b/webroot/css/themes/default/ext_authentication.min.css @@ -1,7 +1,7 @@ /**! * @name passbolt-styleguide - * @version v4.7.0 - * @date 2024-04-24 + * @version v4.8.0 + * @date 2024-05-16 * @copyright Copyright 2023 Passbolt SA * @source https://github.com/passbolt/passbolt_styleguide * @license AGPL-3.0 diff --git a/webroot/css/themes/midgar/api_authentication.min.css b/webroot/css/themes/midgar/api_authentication.min.css index 50b2e4d47f..76ef712294 100644 --- a/webroot/css/themes/midgar/api_authentication.min.css +++ b/webroot/css/themes/midgar/api_authentication.min.css @@ -1,7 +1,7 @@ /**! * @name passbolt-styleguide - * @version v4.7.0 - * @date 2024-04-24 + * @version v4.8.0 + * @date 2024-05-16 * @copyright Copyright 2023 Passbolt SA * @source https://github.com/passbolt/passbolt_styleguide * @license AGPL-3.0 diff --git a/webroot/css/themes/midgar/api_main.min.css b/webroot/css/themes/midgar/api_main.min.css index 3c41ab8073..7a0f348bb5 100644 --- a/webroot/css/themes/midgar/api_main.min.css +++ b/webroot/css/themes/midgar/api_main.min.css @@ -1,7 +1,7 @@ /**! * @name passbolt-styleguide - * @version v4.7.0 - * @date 2024-04-24 + * @version v4.8.0 + * @date 2024-05-16 * @copyright Copyright 2023 Passbolt SA * @source https://github.com/passbolt/passbolt_styleguide * @license AGPL-3.0 diff --git a/webroot/css/themes/midgar/ext_authentication.min.css b/webroot/css/themes/midgar/ext_authentication.min.css index 50b2e4d47f..76ef712294 100644 --- a/webroot/css/themes/midgar/ext_authentication.min.css +++ b/webroot/css/themes/midgar/ext_authentication.min.css @@ -1,7 +1,7 @@ /**! * @name passbolt-styleguide - * @version v4.7.0 - * @date 2024-04-24 + * @version v4.8.0 + * @date 2024-05-16 * @copyright Copyright 2023 Passbolt SA * @source https://github.com/passbolt/passbolt_styleguide * @license AGPL-3.0 diff --git a/webroot/locales/en-UK/common.json b/webroot/locales/en-UK/common.json index b79c6c852e..731db8eda8 100644 --- a/webroot/locales/en-UK/common.json +++ b/webroot/locales/en-UK/common.json @@ -319,7 +319,7 @@ "Directory group's users field to map to Passbolt group's field.": "Directory group's users field to map to Passbolt group's field.", "Directory ID": "Directory ID", "Directory type": "Directory type", - "Directory user's username fallback field to be mapped when user username field cannot be found.": "Directory user's username fallback field to be mapped when user username field cannot be found.", + "Directory user's username fallback field to use when user username field cannot be found.": "Directory user's username fallback field to use when user username field cannot be found.", "Directory user's username field to map to Passbolt user's username field.": "Directory user's username field to map to Passbolt user's username field.", "Disable": "Disable", "Disable (Default)": "Disable (Default)", @@ -731,7 +731,6 @@ "Please review carefully this configuration.": "Please review carefully this configuration.", "Please select a provider": "Please select a provider", "Please try again later or contact your administrator.": "Please try again later or contact your administrator.", - "Please try again.": "Please try again.", "Please wait, while your request is processed.": "Please wait, while your request is processed.", "Please wait...": "Please wait...", "Policy Override": "Policy Override", @@ -1154,7 +1153,6 @@ "This user will not be able to sign in to passbolt and receive email notifications. Other users can share resource with it and add this user to a group.": "This user will not be able to sign in to passbolt and receive email notifications. Other users can share resource with it and add this user to a group.", "This will help protect you from <1>phishing attacks.": "This will help protect you from <1>phishing attacks.", "Time based One Time Password (TOTP)": "Time based One Time Password (TOTP)", - "Time is up": "Time is up", "Time-based One Time Password": "Time-based One Time Password", "Tips for choosing a good passphrase": "Tips for choosing a good passphrase", "TLS must be set to 'Yes' or 'No'": "TLS must be set to 'Yes' or 'No'", @@ -1353,8 +1351,6 @@ "You should keep it offline in a safe place.": "You should keep it offline in a safe place.", "You sign in to passbolt just like you normally do.": "You sign in to passbolt just like you normally do.", "You susccessfully signed in with your {{providerName}} account. You can safely save your configuration.": "You susccessfully signed in with your {{providerName}} account. You can safely save your configuration.", - "You took too long to recover your account.": "You took too long to recover your account.", - "You took too long to set up your account.": "You took too long to set up your account.", "You will be able to save it after submitting": "You will be able to save it after submitting", "You will need this recovery kit later to access your account (for example on a new device).": "You will need this recovery kit later to access your account (for example on a new device).", "you@organization.com": "you@organization.com", diff --git a/webroot/locales/es-ES/common.json b/webroot/locales/es-ES/common.json index 06dc9f7bde..2c0f0f92c6 100644 --- a/webroot/locales/es-ES/common.json +++ b/webroot/locales/es-ES/common.json @@ -291,7 +291,7 @@ "Directory group's users field to map to Passbolt group's field.": "Campo de usuarios del grupo de directorios para asignar al campo del grupo de Passbolt.", "Directory ID": "ID del directorio", "Directory type": "Tipo de directorio", - "Directory user's username fallback field to be mapped when user username field cannot be found.": "Directory user's username fallback field to be mapped when user username field cannot be found.", + "Directory user's username fallback field to be mapped when user username field cannot be found.": "El campo del de nombre de usuario alternativo del directorio del usuario se asignará cuando no se encuentre el campo de nombre de usuario del usuario.", "Directory user's username field to map to Passbolt user's username field.": "Campo de nombre de usuario del usuario del directorio para asignar al campo del nombre de usuario del usuario de Passbolt.", "Disable": "Deshabilitar", "Disable (Default)": "Deshabilitar (predeterminado)", @@ -1182,7 +1182,7 @@ "User self registration enables users with an email from a whitelisted domain to create their passbolt account without prior admin invitation.": "El auto-registro de usuario permite a los usuarios con un correo electrónico de un dominio en la lista blanca crear su cuenta de Passbolt sin previa invitación de un administrador.", "User self registration is disabled.": "El auto-registro de usuarios está desactivado.", "User settings": "Configuración de usuario", - "User username fallback field": "User username fallback field", + "User username fallback field": "Campo del nombre de usuario alternativo del usuario", "User username field mapping": "Asignación del campo de nombre de usuario", "Username": "Nombre de usuario", "Username / Email": "Usuario / Email", diff --git a/webroot/locales/ko-KR/common.json b/webroot/locales/ko-KR/common.json index caec6094fc..6025f8ca6d 100644 --- a/webroot/locales/ko-KR/common.json +++ b/webroot/locales/ko-KR/common.json @@ -107,7 +107,7 @@ "Allow “Remember this device for a month.“ option during MFA.": "MFA를 사용하는 동안 “이 장치를 한 달 동안 기억하기.“ 옵션을 허용", "Allow group manager": "그룹 관리자 허용", "Allow passbolt to access external services to check if a password has been compromised.": "이 옵션을 사용하면 암호를 해독할 때 외부 서비스에 접근할 수 있습니다.", - "Allow passbolt to access external services to check if the user passphrase has been compromised when the user creates it.": "사용자가 암호를 만들 때 사용자 암호문이 손상되었는지 확인하기 위해 패스볼트가 외부 서비스에 액세스하도록 허용합니다.", + "Allow passbolt to access external services to check if the user passphrase has been compromised when the user creates it.": "사용자가 암호를 만들 때 사용자 암호문이 손상되었는지 확인하기 위해 패스볼트가 외부 서비스에 접근하도록 허용합니다.", "Allow users to override the default policy.": "사용자가 기본 정책을 재정의할 수 있도록 허용합니다.", "Allowed domains": "허용 도메인", "Allows Azure and Passbolt API to securely share information.": "Azure 와 Passbolt API가 정보를 안전하게 공유할 수 있습니다.", @@ -291,7 +291,7 @@ "Directory group's users field to map to Passbolt group's field.": "Passbolt 그룹의 필드에 매핑할 디렉터리 그룹의 사용자 필드입니다.", "Directory ID": "디렉터리 ID", "Directory type": "디렉터리 유형", - "Directory user's username fallback field to be mapped when user username field cannot be found.": "Directory user's username fallback field to be mapped when user username field cannot be found.", + "Directory user's username fallback field to be mapped when user username field cannot be found.": "사용자 이름 필드를 찾을 수 없는 경우 매핑할 디렉터리 사용자의 사용자 이름 대체 필드입니다.", "Directory user's username field to map to Passbolt user's username field.": "Passbolt 사용자의 사용자 이름 필드에 매핑할 디렉터리 사용자의 사용자 이름 필드입니다.", "Disable": "사용안함", "Disable (Default)": "사용안함(기본)", @@ -444,9 +444,9 @@ "How do I configure an AD FS SSO?": "AD FS SSO를 구성하려면 어떻게 해야 하나요?", "How do you want to proceed?": "어떻게 진행하길 원하나요?", "How does it work?": "어떻게 작동하나요?", - "I accept the <1>privacy policy": "나는 <1>개인정보취급방침을 동의합니다.", - "I accept the <1>terms": "나는 <1>약관에 동의합니다.", - "I accept the <1>terms and the <3>privacy policy": "나는 <1>약관과 <3>개인정보취급방침을 동의합니다.", + "I accept the <1>privacy policy": "<1>개인정보취급방침을 동의합니다.", + "I accept the <1>terms": "<1>약관에 동의합니다.", + "I accept the <1>terms and the <3>privacy policy": "<1>약관과 <3>개인정보취급방침을 동의합니다.", "I agree to share securely a copy of my private key & passphrase with my organization recovery contacts.": "개인키 및 패스프레이즈의 복사본을 조직 복구 담당자와 안전하게 공유하는 데 동의합니다.", "I agree to share this info with my organization recovery contacts": "조직 복구 담당자와 정보를 공유하는 데 동의합니다.", "I already have an account": "계정이 이미 있습니다.", @@ -1182,7 +1182,7 @@ "User self registration enables users with an email from a whitelisted domain to create their passbolt account without prior admin invitation.": "사용자 자체 등록을 사용하면 화이트리스트에 있는 도메인의 이메일을 사용하는 사용자가 관리자 사전 초대 없이 암호 계정을 만들 수 있습니다.", "User self registration is disabled.": "사용자 자체 등록이 비활성화되었습니다.", "User settings": "사용자 설정", - "User username fallback field": "User username fallback field", + "User username fallback field": "사용자 사용자 이름 대체 필드", "User username field mapping": "사용자의 사용자이름 필드 매핑", "Username": "사용자이름", "Username / Email": "사용자이름 / 이메일", @@ -1316,7 +1316,7 @@ "You took too long to recover your account.": "계정을 복구하는 데 너무 오래 걸렸습니다.", "You took too long to set up your account.": "계정을 설정하는 데 너무 오래 걸렸습니다.", "You will be able to save it after submitting": "제출 후 저장하면 됩니다.", - "You will need this recovery kit later to access your account (for example on a new device).": "나중에 계정(예: 새 기기)에 액세스하려면 이 복구 키트가 필요합니다.", + "You will need this recovery kit later to access your account (for example on a new device).": "나중에 계정(예: 새 기기)에 접근하려면 이 복구 키트가 필요합니다.", "you@organization.com": "you@organization.com", "Your browser is not configured to work with this passbolt instance.": "브라우저가 이 패스볼트 인스턴스와 함께 작동하도록 구성되어 있지 않습니다.", "Your language is missing or you discovered an error in the translation, help us to improve passbolt.": "언어가 누락되었거나 번역에 오류가 있는 경우 패스볼트가 개선되도록 도와주세요.", diff --git a/webroot/locales/pl-PL/common.json b/webroot/locales/pl-PL/common.json index f050eee6d3..8d47e2e5ae 100644 --- a/webroot/locales/pl-PL/common.json +++ b/webroot/locales/pl-PL/common.json @@ -206,9 +206,9 @@ "Configure another phone": "Skonfiguruj kolejny telefon", "Configuring SSO access, please wait...": "Zaczekaj, trwa konfiguracja dostępu SSO...", "Confirm Organization Recovery Key download": "Potwierdź pobieranie klucza odzyskiwania organizacji", - "Confirm password creation": "Confirm password creation", - "Confirm resource creation": "Confirm resource creation", - "Confirm resource edition": "Confirm resource edition", + "Confirm password creation": "Potwierdź stworzenie hasła", + "Confirm resource creation": "Potwierdź stworzenie zasobu", + "Confirm resource edition": "Potwierdź edycję zasobu", "Congratulation! Passbolt extension has been installed.": "Gratulacje! Wtyczka Passbolt została zainstalowana.", "Contact Sales": "Napisz do działu sprzedaży", "Contact your administrator with details about what went wrong.": "Skontaktuj się ze swoim administratorem i szczegółowo opisz problem.", @@ -431,7 +431,7 @@ "Groups I am member of": "Grupy, do których należę", "Groups I manage": "Grupy, którymi zarządzam", "Groups parent group": "Grupa nadrzędna dla grup", - "Hang in there! Depending your installation, you might need to check the documentation in order to run the healthcheck from the CLI": "Hang in there! Depending your installation, you might need to check the documentation in order to run the healthcheck from the CLI", + "Hang in there! Depending your installation, you might need to check the documentation in order to run the healthcheck from the CLI": "Zaczekaj! W zależności od twojej instalacji, sprawdzenie dokumentacji może być konieczne, aby przeprowadzić weryfikację z CLI", "help": "pomoc", "Help site": "Strona pomocy", "Help, I lost my passphrase.": "Pomocy, zgubiłem hasło dostępu.", @@ -441,7 +441,7 @@ "How do I configure a {settings.provider.name} SMTP server?": "Jak skonfigurować serwer SMTP w usłudze {settings.provider.name}?", "How do I configure a AzureAD SSO?": "Jak skonfigurować SSO przez AzureAD?", "How do I configure a Google SSO?": "Jak skonfigurować SSO przez Google?", - "How do I configure an AD FS SSO?": "How do I configure an AD FS SSO?", + "How do I configure an AD FS SSO?": "Jak skonfigurować AD FS SSO?", "How do you want to proceed?": "Co chcesz dalej zrobić?", "How does it work?": "Jak to działa?", "I accept the <1>privacy policy": "Akceptuję <1>politykę prywatności", @@ -454,7 +454,7 @@ "I do not want to share a copy of my private key & passphrase with my organization recovery contacts.": "Nie zgadzam się na udostępnienie kopii mojego prywatnego klucza i hasła dostępu kontaktom awaryjnym z mojej organizacji.", "I lost my passphrase, generate a new private key.": "Zgubiłem hasło dostępu, wygeneruj nowy klucz prywatny.", "I safely stored my recovery kit.": "Udało mi się bezpiecznie przechować pakiet odzyskiwania.", - "I verified with <1>{this.creatorName} that the request is valid.": "I verified with <1>{this.creatorName} that the request is valid.", + "I verified with <1>{this.creatorName} that the request is valid.": "Potwierdziłem/łam z użytkownikiem <1>{this.creatorName}, że ta prośba jest zasadna.", "I want to try again.": "Chcę spróbować ponownie.", "If there was an issue during the transfer, either the operation was cancelled on the mobile side, or the authentication token expired.": "Jeżeli w trakcie transferu wystąpił błąd, to znaczy, że operacja została anulowana po stronie mobilnej lub token uwierzytelniania wygasł.", "If this does not work get in touch with support.": "Jeśli to nie zadziała, skontaktuj się z obsługą klienta.", @@ -717,7 +717,7 @@ "Private": "Prywatny", "Private key": "Klucz prywatny", "Pro tip": "Złota rada", - "Proceed anyway": "Proceed anyway", + "Proceed anyway": "Kontynuuj mimo to", "Profile": "Profil", "Prompt": "Polecenie", "Public": "Publiczny", @@ -830,7 +830,7 @@ "Server response is empty.": "Odpowiedź serwera jest pusta.", "Server url": "Adres URL serwera", "Session Expired": "Sesja wygasła", - "Set expiry date": "Set expiry date", + "Set expiry date": "Ustaw datę ważności", "Set the date automatically:": "Set the date automatically:", "Set the date manually:": "Set the date manually:", "Settings": "Ustawienia", @@ -871,7 +871,7 @@ "Some resources will not be synchronized and will require your attention, see the full report.": "Niektóre zasoby nie zostaną zsynchronizowane i będą wymagać Twojej uwagi. Sprawdź pełny raport.", "Something went wrong, the sign in failed with the following error:": "Coś poszło nie tak. Logowanie nie powiodło się z następującym błędem:", "Something went wrong!": "Coś poszło nie tak!", - "Something wrong?": "Something wrong?", + "Something wrong?": "Coś poszło nie tak?", "Sorry no multi factor authentication is enabled for this organization.": "Przykro nam, w tej organizacji nie włączono uwierzytelniania wieloskładnikowego.", "Sorry the account recovery feature is not enabled for this organization.": "Niestety funkcja odzyskiwania konta nie jest włączona w tej organizacji.", "Sorry the Mobile app setup feature is only available in a secure context (HTTPS).": "Przepraszamy, funkcja konfiguracji aplikacji mobilnej jest dostępna wyłącznie w bezpiecznym kontekście (HTTPS).", @@ -929,7 +929,7 @@ "The account recovery subscription setting has been updated.": "Zaktualizowano ustawienia subskrypcji do odzyskiwania kont.", "The AD FS authentication endpoint.": "The AD FS authentication endpoint.", "The AD FS configuration relative path from the given login url.": "The AD FS configuration relative path from the given login url.", - "The AD FS scope.": "The AD FS scope.", + "The AD FS scope.": "Zakres AD FS.", "The attribute you would like to use for the first part of the email (usually username).": "Atrybut, którego chcesz użyć dla pierwszej części adresu e-mail (zwykle jest to nazwa użytkownika).", "The Azure Active Directory tenant ID, in UUID format.": "ID grupy dostępowej Active Directory z Azure w formacie UUID.", "The Azure AD authentication endpoint. See <1>alternatives.": "Endpoint uwierzytelniania w Azure AD. Sprawdź <1>alternatywne opcje.", @@ -1229,7 +1229,7 @@ "What is multi-factor authentication?": "Co to jest uwierzytelnianie wieloskładnikowe?", "What is password policy?": "Czym jest polityka haseł?", "What is the role of the passphrase?": "Do czego służy hasło dostępu?", - "What is this page?": "What is this page?", + "What is this page?": "Co to za strona?", "What is user passphrase policies?": "Czym są polityki haseł dostępu użytkowników?", "What is user self registration?": "Co to jest samodzielna rejestracja użytkowników?", "When a comment is posted on a password, notify the users who have access to this password.": "Gdy do hasła zostanie dopisany komentarz, powiadom użytkowników z dostępem do tego hasła.", @@ -1264,11 +1264,11 @@ "When using a new browser, you need an additional code from your phone.": "Gdy skorzystasz z nowej przeglądarki, będziesz potrzebować dodatkowego kodu z telefonu.", "Where can I find my account kit ?": "Gdzie znajdę pakiet mojego konta?", "Where to find it?": "Gdzie to znaleźć?", - "Whoops... access is denied": "Whoops... access is denied", - "Whoops... looks like you are lost.": "Whoops... looks like you are lost.", + "Whoops... access is denied": "Ups... odmowa dostępu", + "Whoops... looks like you are lost.": "Ups... ktoś tu chyba się zgubił.", "Why do I need an SMTP server?": "Dlaczego serwer SMTP jest mi potrzebny?", "Why is this token needed?": "Do czego służy ten token?", - "Why shouldn't I use my login password ?": "Why shouldn't I use my login password ?", + "Why shouldn't I use my login password ?": "Dlaczego nie należy używać mojego hasła logowania?", "Will be added": "Zostanie dodane", "Will be updated": "Zostanie zaktualizowane", "Without the private key and the passphrase it is not possible to decrypt!": "Odszyfrowywanie jest niemożliwe bez klucza prywatnego i hasła dostępu!", @@ -1298,7 +1298,7 @@ "You can select the set of characters used for the passwords that are generated randomly by passbolt in the password generator.": "Możesz ustalić zestaw znaków używanych dla haseł generowanych losowo przez passbolt w generatorze haseł.", "You can set the default length for the passphrases that are generated randomly by passbolt in the password generator.": "Możesz ustalić domyślną długość haseł dostępu generowanych losowo przez passbolt w generatorze haseł.", "You can set the default length for the passwords that are generated randomly by passbolt in the password generator.": "Możesz ustalić domyślną długość haseł generowanych losowo przez passbolt w generatorze haseł.", - "You can set the minimal entropy for the users' private key passphrase.": "You can set the minimal entropy for the users' private key passphrase.", + "You can set the minimal entropy for the users' private key passphrase.": "Możesz ustalić minimalną entropię dla haseł dostępu do prywatnych kluczy użytkowników.", "You cannot delete this group!": "Nie możesz usunąć tej grupy!", "You cannot delete this user!": "Nie możesz usunąć tego użytkownika!", "You do not own any passwords yet. It does feel a bit empty here, create your first password.": "Nie posiadasz jeszcze żadnych haseł. Trochę tutaj pusto, więc stwórz swoje pierwsze hasło.", @@ -1386,14 +1386,14 @@ "<0>{{numberResourceSuccess}} out of {{count}} password has been imported._few": "Zaimportowano <0>{{numberResourceSuccess}} spośród {{count}} haseł.", "<0>{{numberResourceSuccess}} out of {{count}} password has been imported._many": "Zaimportowano <0>{{numberResourceSuccess}} spośród {{count}} haseł.", "<0>{{numberResourceSuccess}} out of {{count}} password has been imported._other": "Zaimportowano <0>{{numberResourceSuccess}} spośród {{count}} haseł.", - "Delete resource?_one": "Delete resource?", - "Delete resource?_few": "Delete resources?", - "Delete resource?_many": "Delete resources?", - "Delete resource?_other": "Delete resources?", - "Set an expiry date_one": "Set an expiry date", - "Set an expiry date_few": "Set expiry dates", - "Set an expiry date_many": "Set expiry dates", - "Set an expiry date_other": "Set expiry dates", + "Delete resource?_one": "Usunąć zasób?", + "Delete resource?_few": "Usunąć zasoby?", + "Delete resource?_many": "Usunąć zasoby?", + "Delete resource?_other": "Usunąć zasoby?", + "Set an expiry date_one": "Ustaw datę ważności", + "Set an expiry date_few": "Ustaw daty ważności", + "Set an expiry date_many": "Ustaw daty ważności", + "Set an expiry date_other": "Ustaw daty ważności", "The expiry date of the selected resource has been updated._one": "The expiry date of the selected resource has been updated.", "The expiry date of the selected resource has been updated._few": "The expiry dates of the selected resources have been updated.", "The expiry date of the selected resource has been updated._many": "The expiry dates of the selected resources have been updated.",