passbolt-browser-extension 4.9.1 → 4.9.2-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/RELEASE_NOTES.md +47 -5
  3. package/doc/resource-types-retrieval-requirements.md +17 -0
  4. package/package.json +4 -8
  5. package/src/all/background_page/controller/InformMenuController/InformMenuController.js +2 -2
  6. package/src/all/background_page/controller/autofill/AutofillController.js +1 -1
  7. package/src/all/background_page/controller/autofill/AutofillController.test.js +42 -0
  8. package/src/all/background_page/controller/import/importResourcesFileController.js +1 -1
  9. package/src/all/background_page/controller/move/moveResourcesController.js +5 -4
  10. package/src/all/background_page/controller/resource/resourceCreateController.js +22 -90
  11. package/src/all/background_page/controller/resource/resourceCreateController.test.js +120 -0
  12. package/src/all/background_page/controller/share/shareFoldersController.js +1 -1
  13. package/src/all/background_page/event/resourceEvents.js +2 -8
  14. package/src/all/background_page/model/entity/export/exportResourcesFileEntity.js +8 -0
  15. package/src/all/background_page/model/entity/export/exportResourcesFileEntity.test.js +4 -0
  16. package/src/all/background_page/model/entity/folder/folderEntity.js +1 -0
  17. package/src/all/background_page/model/entity/folder/folderEntity.test.js +19 -0
  18. package/src/all/background_page/model/entity/permission/permissionsCollection.js +2 -2
  19. package/src/all/background_page/model/entity/permission/permissionsCollection.test.js +24 -2
  20. package/src/all/background_page/model/entity/resource/external/externalResourceEntity.js +52 -4
  21. package/src/all/background_page/model/entity/resource/external/externalResourceEntity.test.data.js +56 -0
  22. package/src/all/background_page/model/entity/resource/external/externalResourceEntity.test.js +74 -0
  23. package/src/all/background_page/model/entity/resource/external/externalResourcesCollection.js +16 -3
  24. package/src/all/background_page/model/entity/resource/external/externalResourcesCollection.test.data.js +21 -0
  25. package/src/all/background_page/model/entity/resource/external/externalResourcesCollection.test.js +24 -0
  26. package/src/all/background_page/model/entity/resource/metadata/resourceMetadataEntity.js +146 -0
  27. package/src/all/background_page/model/entity/resource/metadata/resourceMetadataEntity.test.data.js +47 -0
  28. package/src/all/background_page/model/entity/resource/metadata/resourceMetadataEntity.test.js +90 -0
  29. package/src/all/background_page/model/entity/resource/resourceEntity.js +67 -61
  30. package/src/all/background_page/model/entity/resource/resourceEntity.test.js +63 -132
  31. package/src/all/background_page/model/entity/resource/resourcesCollection.js +17 -3
  32. package/src/all/background_page/model/entity/resource/resourcesCollection.test.data.js +5 -6
  33. package/src/all/background_page/model/entity/resource/resourcesCollection.test.js +33 -37
  34. package/src/all/background_page/model/resource/resourceModel.js +3 -17
  35. package/src/all/background_page/model/share/shareModel.js +3 -3
  36. package/src/all/background_page/service/accountRecovery/decryptPrivateKeyPasswordDataService.js +1 -1
  37. package/src/all/background_page/service/crypto/decryptPrivateKeyService.js +14 -0
  38. package/src/all/background_page/service/crypto/decryptPrivateKeyService.test.js +5 -0
  39. package/src/all/background_page/service/local_storage/resourceLocalStorage.test.js +16 -16
  40. package/src/all/background_page/service/resource/create/resourceCreateService.js +127 -0
  41. package/src/all/background_page/service/resource/create/resourceCreateService.test.data.js +55 -0
  42. package/src/all/background_page/service/resource/create/resourceCreateService.test.js +236 -0
  43. package/src/all/background_page/service/toolbar/toolbarService.js +2 -6
  44. package/src/all/locales/fr-FR/common.json +5 -5
  45. package/src/all/locales/pl-PL/common.json +2 -2
  46. package/src/chrome/manifest.json +1 -1
  47. package/src/chrome-mv3/manifest.json +1 -1
  48. package/src/firefox/manifest.json +1 -1
  49. package/src/safari/manifest.json +1 -1
  50. package/test/matchers/toThrowEntityValidationError.js +7 -4
package/CHANGELOG.md CHANGED
@@ -3,6 +3,51 @@ All notable changes to this project will be documented in this file.
3
3
  This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
+ ## [4.9.2] - 2024-08-26
7
+ ### Fixed
8
+ - PB-33861: Resources with personal field set to null should be considered as personal resources
9
+ - PB-34314: Fix shadow-dom autofill fields
10
+ - PB-34236: Fix Retrieving folder activities displaying no data
11
+
12
+ ### Maintenance
13
+ - PB-34313: Add resources type retrieval requirements documentation
14
+ - PB-34259: E2EE WP1 - Transform dtos from v4 to v5
15
+ - PB-34260: E2EE WP1 - Display resource sidebar information section in v5
16
+ - PB-34261: E2EE WP1 - Display resource sidebar activity section in v5
17
+ - PB-34262: E2EE WP1 - Display resource sidebar description section in v5
18
+ - PB-34263: E2EE WP1 - Display copy username to clipboard from more menu using v5
19
+ - PB-34264: E2EE WP1 - Display resource grid using v5
20
+ - PB-34265: E2EE WP1 - Display resource grid contextual menu using v5
21
+ - PB-34266: E2EE WP1 - Display quickaccess resource view page in v5
22
+ - PB-34267: E2EE WP1 - Display quickaccess home page in v5
23
+ - PB-34268: E2EE WP1 - Display inform menu in v5
24
+ - PB-34269: E2EE WP1 - Autofill resources from Quickaccess in v5 format
25
+ - PB-34270: E2EE WP1 - Make resource entity compatible with v4 and v5
26
+ - PB-34271: E2EE WP1 - Display inform and toolbar suggested resources badge CTA in v5
27
+ - PB-34272: E2EE WP1 - Search resource in webapp using v5
28
+ - PB-34287: E2EE WP1 - Create password resource from webapp in v5 format
29
+ - PB-34288: E2EE WP1 - Create standalone TOTP resource in v5 format
30
+ - PB-34289: E2EE WP1 - Edit password resource in v5 format
31
+ - PB-34290: E2EE WP1 - Edit standalone TOTP resource in v5 format
32
+ - PB-34291: E2EE WP1 - Edit resource description from sidebar in v5 format
33
+ - PB-34292: E2EE WP1 - Delete resource(s) in v5 format
34
+ - PB-34293: E2EE WP1 - Share resource(s) in v5 format
35
+ - PB-34294: E2EE WP1 - Import resource(s) in v5 format
36
+ - PB-34295: E2EE WP1 - Export resource(s) in v5 format
37
+ - PB-34296: E2EE WP1 - Move resource(s) in v5 format
38
+ - PB-34297: E2EE WP1 - Create password resource from quickaccess in v5 format
39
+ - PB-34298: E2EE WP1 - Auto-save password resource from quickaccess in v5 format
40
+ - PB-34299: E2EE WP1 - Make resource entity compatible only with v5
41
+ - PB-34311: E2EE WP1 - Make resource V4 and V5 compatible in both ways
42
+ - PB-34315: E2EE WP1 - Transform DTO to V4 for API and adapt resource validation to v5
43
+ - PB-34391: E2EE WP1 - Enforce resource type id should be required and not null
44
+ - PB-34392: E2EE WP1 - Validate Metadata.uris as array of string, and maxLength
45
+
46
+ ### Security
47
+ - PB-34237: Upgrade vulnerable library i18next-parser
48
+ - PB-34305: Upgrade lockfile-lint library on passbolt_api package-lock.json
49
+ - PB-34422: Remove grunt-browserify dev dependency from browser extension
50
+
6
51
  ## [4.9.1] - 2024-07-23
7
52
  ### Fixed
8
53
  - PB-34134 As a signed-in user I should search resources even if the data integrity is corrupted
@@ -1719,7 +1764,8 @@ self registration settings option in the left-side bar
1719
1764
  - AP: User with plugin installed
1720
1765
  - LU: Logged in user
1721
1766
 
1722
- [Unreleased]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.9.1...HEAD
1767
+ [Unreleased]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.9.2...HEAD
1768
+ [4.9.2]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.9.1...4.9.2
1723
1769
  [4.9.1]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.9.0...4.9.1
1724
1770
  [4.9.0]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.8.2...4.9.0
1725
1771
  [4.8.2]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.8.1...4.8.2
package/RELEASE_NOTES.md CHANGED
@@ -1,9 +1,51 @@
1
- Song: https://www.youtube.com/watch?v=lz2REwKVmnk
1
+ Song: https://www.youtube.com/watch?v=VmtU-bLyReU
2
2
 
3
- Passbolt v4.9.1 is a maintenance update that addresses issues related to the search resources.
3
+ This release candidate addresses several bugs reported by the community. Additionally, it includes numerous maintenance updates as part of our ongoing efforts to ensure a smooth transition and support for the upcoming v5.
4
4
 
5
- We extend our gratitude to the community for their feedback and assistance in testing this release. We hope these updates enhance your experience with Passbolt and we look forward to hearing from you.
5
+ Thank you to the community for reporting these issues.
6
6
 
7
- ## [4.9.1] - 2024-07-23
7
+
8
+ ## [4.9.2] - 2024-08-26
8
9
  ### Fixed
9
- - PB-34134 As a signed-in user I should search resources even if the data integrity is corrupted
10
+ - PB-33861: Resources with personal field set to null should be considered as personal resources
11
+ - PB-34314: Fix shadow-dom autofill fields
12
+ - PB-34236: Fix Retrieving folder activities displaying no data
13
+
14
+ ### Maintenance
15
+ - PB-34313: Add resources type retrieval requirements documentation
16
+ - PB-34259: E2EE WP1 - Transform dtos from v4 to v5
17
+ - PB-34260: E2EE WP1 - Display resource sidebar information section in v5
18
+ - PB-34261: E2EE WP1 - Display resource sidebar activity section in v5
19
+ - PB-34262: E2EE WP1 - Display resource sidebar description section in v5
20
+ - PB-34263: E2EE WP1 - Display copy username to clipboard from more menu using v5
21
+ - PB-34264: E2EE WP1 - Display resource grid using v5
22
+ - PB-34265: E2EE WP1 - Display resource grid contextual menu using v5
23
+ - PB-34266: E2EE WP1 - Display quickaccess resource view page in v5
24
+ - PB-34267: E2EE WP1 - Display quickaccess home page in v5
25
+ - PB-34268: E2EE WP1 - Display inform menu in v5
26
+ - PB-34269: E2EE WP1 - Autofill resources from Quickaccess in v5 format
27
+ - PB-34270: E2EE WP1 - Make resource entity compatible with v4 and v5
28
+ - PB-34271: E2EE WP1 - Display inform and toolbar suggested resources badge CTA in v5
29
+ - PB-34272: E2EE WP1 - Search resource in webapp using v5
30
+ - PB-34287: E2EE WP1 - Create password resource from webapp in v5 format
31
+ - PB-34288: E2EE WP1 - Create standalone TOTP resource in v5 format
32
+ - PB-34289: E2EE WP1 - Edit password resource in v5 format
33
+ - PB-34290: E2EE WP1 - Edit standalone TOTP resource in v5 format
34
+ - PB-34291: E2EE WP1 - Edit resource description from sidebar in v5 format
35
+ - PB-34292: E2EE WP1 - Delete resource(s) in v5 format
36
+ - PB-34293: E2EE WP1 - Share resource(s) in v5 format
37
+ - PB-34294: E2EE WP1 - Import resource(s) in v5 format
38
+ - PB-34295: E2EE WP1 - Export resource(s) in v5 format
39
+ - PB-34296: E2EE WP1 - Move resource(s) in v5 format
40
+ - PB-34297: E2EE WP1 - Create password resource from quickaccess in v5 format
41
+ - PB-34298: E2EE WP1 - Auto-save password resource from quickaccess in v5 format
42
+ - PB-34299: E2EE WP1 - Make resource entity compatible only with v5
43
+ - PB-34311: E2EE WP1 - Make resource V4 and V5 compatible in both ways
44
+ - PB-34315: E2EE WP1 - Transform DTO to V4 for API and adapt resource validation to v5
45
+ - PB-34391: E2EE WP1 - Enforce resource type id should be required and not null
46
+ - PB-34392: E2EE WP1 - Validate Metadata.uris as array of string, and maxLength
47
+
48
+ ### Security
49
+ - PB-34237: Upgrade vulnerable library i18next-parser
50
+ - PB-34305: Upgrade lockfile-lint library on passbolt_api package-lock.json
51
+ - PB-34422: Remove grunt-browserify dev dependency from browser extension
@@ -0,0 +1,17 @@
1
+ ```mermaid
2
+ graph
3
+ A[Request API]
4
+ B[Marshall resource metadata]
5
+ C[Create ResourceEntity]
6
+ D[Failure]
7
+ E[Decrypt metadata]
8
+ C1{ }
9
+ C2{ }
10
+ A --> |is v4 resource type?| C1
11
+ C1 --> |no, is v6 resource type?| C2
12
+ C1 --> |yes| B
13
+ B --> C
14
+ C2 --> |no| D
15
+ C2 --> |yes| E
16
+ E --> C
17
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "passbolt-browser-extension",
3
- "version": "4.9.1",
3
+ "version": "4.9.2-rc.0",
4
4
  "license": "AGPL-3.0",
5
5
  "copyright": "Copyright 2022 Passbolt SA",
6
6
  "description": "Passbolt web extension for the open source password manager for teams",
@@ -21,7 +21,7 @@
21
21
  "locutus": "~2.0.9",
22
22
  "openpgp": "^5.11.1",
23
23
  "papaparse": "^5.2.0",
24
- "passbolt-styleguide": "^4.9.3",
24
+ "passbolt-styleguide": "^4.9.5",
25
25
  "react": "17.0.2",
26
26
  "react-dom": "17.0.2",
27
27
  "secrets-passbolt": "github:passbolt/secrets.js#v2.0.1",
@@ -44,11 +44,10 @@
44
44
  "eslint-plugin-no-unsanitized": "^4.0.1",
45
45
  "eslint-plugin-react": "^7.33.1",
46
46
  "grunt": "^1.0.3",
47
- "grunt-browserify": "^6.0.0",
48
47
  "grunt-contrib-clean": "^2.0.0",
49
48
  "grunt-contrib-copy": "^1.0.0",
50
49
  "grunt-shell": "^4.0.0",
51
- "i18next-parser": "^8.12.0",
50
+ "i18next-parser": "^9.0.1",
52
51
  "jest": "^29.6.2",
53
52
  "jest-each": "^29.6.2",
54
53
  "jest-environment-jsdom": "^29.6.2",
@@ -56,7 +55,7 @@
56
55
  "jest-fetch-mock": "^3.0.3",
57
56
  "jest-junit": "^16.0.0",
58
57
  "jest-webextension-mock": "^3.8.9",
59
- "lockfile-lint": "^4.12.1",
58
+ "lockfile-lint": "^4.14.0",
60
59
  "text-encoding-utf-8": "^1.0.2",
61
60
  "uuid": "^8.3.2",
62
61
  "web-ext": "^8.0.0",
@@ -70,9 +69,6 @@
70
69
  "glob-parent": "5.1.2"
71
70
  },
72
71
  "firefox-profile": "4.3.2",
73
- "crypto-browserify": {
74
- "browserify-sign": "4.2.2"
75
- },
76
72
  "ws": "8.17.1"
77
73
  },
78
74
  "scripts": {
@@ -16,11 +16,11 @@ import ResourceModel from "../../model/resource/resourceModel";
16
16
  import {QuickAccessService} from "../../service/ui/quickAccess.service";
17
17
  import GetPassphraseService from "../../service/passphrase/getPassphraseService";
18
18
  import BrowserTabService from "../../service/ui/browserTab.service";
19
- import ResourceEntity from "../../model/entity/resource/resourceEntity";
20
19
  import ExternalResourceEntity from "../../model/entity/resource/external/externalResourceEntity";
21
20
  import ResourceInProgressCacheService from "../../service/cache/resourceInProgressCache.service";
22
21
  import WorkerService from "../../service/worker/workerService";
23
22
  import ResourceTypeModel from "../../model/resourceType/resourceTypeModel";
23
+ import ResourceMetadataEntity from "../../model/entity/resource/metadata/resourceMetadataEntity";
24
24
 
25
25
  /**
26
26
  * Controller related to the in-form call-to-action
@@ -89,7 +89,7 @@ class InformMenuController {
89
89
  // Retrieve resource name and uri from tab.
90
90
  const tab = await BrowserTabService.getCurrent();
91
91
  const name = tab.title;
92
- const uri = tab.url.substr(0, ResourceEntity.URI_MAX_LENGTH);
92
+ const uri = tab.url.substr(0, ResourceMetadataEntity.URI_MAX_LENGTH);
93
93
 
94
94
  // Store the resource to save in cache.
95
95
  const resourceDto = {name: name, username: username, uri: uri, secret_clear: secret_clear};
@@ -70,7 +70,7 @@ class AutofillController {
70
70
  const secretSchema = await this.resourceTypeModel.getSecretSchemaById(resource.resourceTypeId);
71
71
  const privateKey = await GetDecryptedUserPrivateKeyService.getKey(passphrase);
72
72
  const plaintextSecret = await DecryptAndParseResourceSecretService.decryptAndParse(resource.secret, secretSchema, privateKey);
73
- const {username} = resource;
73
+ const username = resource.metadata?.username || "";
74
74
  const password = plaintextSecret?.password;
75
75
  this.fillCredential(webIntegrationWorker, {username, password});
76
76
  } finally {
@@ -105,6 +105,7 @@ describe("AutofillController", () => {
105
105
  expect(portWrapper.request).toHaveBeenCalledWith('passbolt.quickaccess.fill-form', resource.username, secret.password, tab.url);
106
106
  expect(portWrapper.emit).not.toHaveBeenCalledWith('passbolt.in-form-menu.close');
107
107
  });
108
+
108
109
 
109
110
  it("Should not autofill from a worker that is not inform menu or quickaccess.", async() => {
110
111
  expect.assertions(10);
@@ -144,6 +145,47 @@ describe("AutofillController", () => {
144
145
  expect(portWrapper.request).not.toHaveBeenCalledWith('passbolt.quickaccess.fill-form', resource.username, secret.password, tab.url);
145
146
  expect(portWrapper.emit).not.toHaveBeenCalledWith('passbolt.in-form-menu.close');
146
147
  });
148
+
149
+ it("Should map username with empty string if not exist.", async() => {
150
+ expect.assertions(10);
151
+
152
+ // initialisation
153
+ const requestId = uuidv4();
154
+ const worker = readWorker();
155
+ const controller = new AutofillController(worker, requestId, defaultApiClientOptions(), account);
156
+ const resource = defaultResourceDto({
157
+ username: null
158
+ });
159
+ const secret = {password: "secret"};
160
+ const port = mockPort({name: worker.id, tabId: worker.tabId, frameId: worker.frameId});
161
+ const portWrapper = new Port(port);
162
+ const tab = {url: "https://url.com"};
163
+ // mocked function
164
+ jest.spyOn(WorkerService, "get").mockImplementationOnce(() => ({port: portWrapper, tab: tab}));
165
+ jest.spyOn(controller.getPassphraseService, "requestPassphraseFromQuickAccess");
166
+ jest.spyOn(controller.getPassphraseService, "getPassphrase").mockImplementationOnce(() => pgpKeys.ada.passphrase);
167
+ jest.spyOn(controller.resourceModel, "findForDecrypt").mockImplementationOnce(() => resource);
168
+ jest.spyOn(controller.resourceTypeModel, "getSecretSchemaById").mockImplementationOnce(jest.fn());
169
+ jest.spyOn(GetDecryptedUserPrivateKeyService, "getKey").mockImplementationOnce(() => pgpKeys.ada.private_decrypted);
170
+ jest.spyOn(DecryptAndParseResourceSecretService, "decryptAndParse").mockImplementationOnce(() => secret);
171
+ jest.spyOn(portWrapper, "emit");
172
+ jest.spyOn(portWrapper, "request");
173
+
174
+ // process
175
+ await controller.exec(resource.id, worker.tabId);
176
+
177
+ // expectations
178
+ expect(controller.getPassphraseService.requestPassphraseFromQuickAccess).not.toHaveBeenCalled();
179
+ expect(controller.getPassphraseService.getPassphrase).toHaveBeenCalledTimes(1);
180
+ expect(controller.getPassphraseService.getPassphrase).toHaveBeenCalledWith(worker);
181
+ expect(controller.resourceModel.findForDecrypt).toHaveBeenCalledTimes(1);
182
+ expect(controller.resourceModel.findForDecrypt).toHaveBeenCalledWith(resource.id);
183
+ expect(controller.resourceTypeModel.getSecretSchemaById).toHaveBeenCalledTimes(1);
184
+ expect(controller.resourceTypeModel.getSecretSchemaById).toHaveBeenCalledWith(resource.resourceTypeId);
185
+ expect(portWrapper.emit).not.toHaveBeenCalledWith('passbolt.web-integration.fill-credentials', {username: "", password: secret.password});
186
+ expect(portWrapper.request).not.toHaveBeenCalledWith('passbolt.quickaccess.fill-form', "", secret.password, tab.url);
187
+ expect(portWrapper.emit).not.toHaveBeenCalledWith('passbolt.in-form-menu.close');
188
+ });
147
189
  });
148
190
  });
149
191
 
@@ -248,7 +248,7 @@ class ImportResourcesFileController {
248
248
  */
249
249
  async bulkImportResources(importEntity) {
250
250
  let importedCount = 0;
251
- const resourcesCollection = new ResourcesCollection(importEntity.importResources.toJSON());
251
+ const resourcesCollection = new ResourcesCollection(importEntity.importResources.toResourceCollectionImportDto());
252
252
  const successCallback = (resourceEntity, index) => this.handleImportResourceSuccess(importEntity, ++importedCount, resourceEntity, importEntity.importResources.items[index]);
253
253
  const errorCallback = (error, index) => this.handleImportResourceError(importEntity, ++importedCount, error, importEntity.importResources.items[index]);
254
254
  await this.resourceModel.bulkCreate(resourcesCollection, {successCallback: successCallback, errorCallback: errorCallback});
@@ -76,6 +76,7 @@ class MoveResourcesController {
76
76
  await this.progressService.close();
77
77
  this.cleanup();
78
78
  } catch (error) {
79
+ console.error(error);
79
80
  await this.progressService.close();
80
81
  this.cleanup();
81
82
  throw error;
@@ -144,7 +145,7 @@ class MoveResourcesController {
144
145
  parent = this.resourcesParentFolders.getById(resource.folderParentId);
145
146
  }
146
147
  if (!ResourceEntity.canResourceMove(resource, parent, this.destinationFolder)) {
147
- console.warn(`Resource ${resource.name} can not be moved, skipping.`);
148
+ console.warn(`Resource ${resource.metadata.name} can not be moved, skipping.`);
148
149
  resourceIdsToRemove.push(resource.id);
149
150
  }
150
151
  }
@@ -166,7 +167,7 @@ class MoveResourcesController {
166
167
  async calculateChanges() {
167
168
  this.changes = new PermissionChangesCollection([]);
168
169
  for (const resource of this.resources) {
169
- await this.progressService.finishStep(i18n.t('Calculating changes for {{name}}', {name: resource.name}));
170
+ await this.progressService.finishStep(i18n.t('Calculating changes for {{name}}', {name: resource.metadata.name}));
170
171
  /*
171
172
  * A user who can update a resource can move it
172
173
  * But to change the rights they need to be owner
@@ -201,10 +202,10 @@ class MoveResourcesController {
201
202
  const resourceIdsToRemove = [];
202
203
  for (const resource of this.resources) {
203
204
  if (resource.folderParentId !== this.destinationFolderId) {
204
- await this.progressService.finishStep(i18n.t('Moving {{name}}', {name: resource.name}));
205
+ await this.progressService.finishStep(i18n.t('Moving {{name}}', {name: resource.metadata.name}));
205
206
  await this.resourceModel.move(resource, this.destinationFolderId);
206
207
  } else {
207
- Log.write({level: 'debug', message: `Resource ${resource.name} is already in the folder, skipping.`});
208
+ Log.write({level: 'debug', message: `Resource ${resource.metadata.name} is already in the folder, skipping.`});
208
209
  resourceIdsToRemove.push(resource.id);
209
210
  }
210
211
  }
@@ -11,19 +11,11 @@
11
11
  * @link https://www.passbolt.com Passbolt(tm)
12
12
  * @since 2.9.0
13
13
  */
14
- import {OpenpgpAssertion} from "../../utils/openpgp/openpgpAssertions";
15
- import Keyring from "../../model/keyring";
16
- import EncryptMessageService from "../../service/crypto/encryptMessageService";
17
- import User from "../../model/user";
18
- import ResourceModel from "../../model/resource/resourceModel";
19
- import GetPassphraseService from "../../service/passphrase/getPassphraseService";
20
- import GetDecryptedUserPrivateKeyService from "../../service/account/getDecryptedUserPrivateKeyService";
21
- import FolderModel from "../../model/folder/folderModel";
22
- import ResourceEntity from "../../model/entity/resource/resourceEntity";
14
+
23
15
  import i18n from "../../sdk/i18n";
24
- import ResourceSecretsCollection from "../../model/entity/secret/resource/resourceSecretsCollection";
16
+ import GetPassphraseService from "../../service/passphrase/getPassphraseService";
25
17
  import ProgressService from "../../service/progress/progressService";
26
- import ShareModel from "../../model/share/shareModel";
18
+ import ResourceCreateService from "../../service/resource/create/resourceCreateService";
27
19
 
28
20
  class ResourceCreateController {
29
21
  /**
@@ -37,102 +29,42 @@ class ResourceCreateController {
37
29
  constructor(worker, requestId, apiClientOptions, account) {
38
30
  this.worker = worker;
39
31
  this.requestId = requestId;
40
- this.resourceModel = new ResourceModel(apiClientOptions, account);
41
- this.folderModel = new FolderModel(apiClientOptions);
42
- this.shareModel = new ShareModel(apiClientOptions);
43
- this.keyring = new Keyring();
44
32
  this.progressService = new ProgressService(this.worker, i18n.t('Creating password'));
33
+ this.resourceCreateService = new ResourceCreateService(account, apiClientOptions, this.progressService);
45
34
  this.getPassphraseService = new GetPassphraseService(account);
46
35
  }
47
36
 
48
37
  /**
49
- * Create a resource.
50
- *
38
+ * Controller executor.
51
39
  * @param {object} resourceDto The resource data
52
40
  * @param {string|object} plaintextDto The secret to encrypt
53
- * @return {Promise<ResourceEntity>} resourceEntity
41
+ * @returns {Promise<void>}
54
42
  */
55
- async main(resourceDto, plaintextDto) {
56
- let privateKey;
57
-
58
- /*
59
- * set default goals, we give arbitrarily "more" goals if a parent permission folder is set
60
- * as we don't know how many 'share' operations are needed yet
61
- */
62
- let resource = new ResourceEntity(resourceDto);
63
- const goals = resource.folderParentId ? 10 : 2;
64
-
65
- // Get the passphrase if needed and decrypt secret key
43
+ async _exec(resourceDto, plaintextDto) {
66
44
  try {
67
- const passphrase = await this.getPassphraseService.getPassphrase(this.worker);
68
- privateKey = await GetDecryptedUserPrivateKeyService.getKey(passphrase);
69
- } catch (error) {
70
- console.error(error);
71
- throw error;
72
- }
73
-
74
- try {
75
- this.progressService.start(goals, i18n.t('Initializing'));
76
- const plaintext = await this.resourceModel.serializePlaintextDto(resource.resourceTypeId, plaintextDto);
77
-
78
- // Encrypt and sign
79
- await this.progressService.finishStep(i18n.t('Encrypting secret'), true);
80
- const userId = User.getInstance().get().id;
81
- const userPublicArmoredKey = this.keyring.findPublic(userId).armoredKey;
82
- const userPublicKey = await OpenpgpAssertion.readKeyOrFail(userPublicArmoredKey);
83
- const secret = await EncryptMessageService.encrypt(plaintext, userPublicKey, [privateKey]);
84
- resource.secrets = new ResourceSecretsCollection([{data: secret}]);
85
-
86
- // Save
87
- await this.progressService.finishStep(i18n.t('Creating password'), true);
88
- resource = await this.resourceModel.create(resource);
89
-
90
- // Share if needed
91
- if (resource.folderParentId) {
92
- await this.handleCreateInFolder(resource, privateKey);
93
- }
45
+ const resource = await this.exec(resourceDto, plaintextDto);
46
+ this.worker.port.emit(this.requestId, 'SUCCESS', resource);
94
47
  } catch (error) {
95
48
  console.error(error);
96
- await this.progressService.close();
97
- throw error;
49
+ this.worker.port.emit(this.requestId, 'ERROR', error);
98
50
  }
99
-
100
- await this.progressService.finishStep(i18n.t('Done!'), true);
101
- await this.progressService.close();
102
-
103
- return resource;
104
51
  }
105
52
 
106
53
  /**
107
- * Handle post create operations if resource is created in folder
108
- * This includes sharing the resource to match the parent folder permissions
109
- *
110
- * @param {ResourceEntity} resourceEntity
111
- * @param {openpgp.PrivateKey} privateKey The user decrypted private key
54
+ * @param {object} resourceDto The resource data
55
+ * @param {string|object} plaintextDto The secret to encrypt
112
56
  * @returns {Promise<void>}
113
57
  */
114
- async handleCreateInFolder(resourceEntity, privateKey) {
115
- // Calculate changes if any
116
- await this.progressService.finishStep(i18n.t('Calculate permissions'), true);
117
- const destinationFolder = await this.folderModel.findForShare(resourceEntity.folderParentId);
118
- const changes = await this.resourceModel.calculatePermissionsChangesForCreate(resourceEntity, destinationFolder);
119
-
120
- // Apply changes
121
- if (changes.length) {
122
- const goals = (changes.length * 3) + 2 + this.progressService.progress; // closer to reality...
123
- this.progressService.updateGoals(goals);
124
-
125
- // Sync keyring
126
- await this.progressService.finishStep(i18n.t('Synchronizing keys'), true);
127
- await this.keyring.sync();
128
-
129
- // Share
130
- await this.progressService.finishStep(i18n.t('Start sharing'), true);
131
- const resourcesToShare = [resourceEntity.toDto({secrets: true})];
132
- await this.shareModel.bulkShareResources(resourcesToShare, changes.toDto(), privateKey, async message =>
133
- await this.progressService.finishStep(message)
134
- );
135
- await this.resourceModel.updateLocalStorage();
58
+ async exec(resourceDto, plaintextDto) {
59
+ try {
60
+ const goals = resourceDto.folder_parent_id ? 10 : 2;
61
+ const passphrase = await this.getPassphraseService.getPassphrase(this.worker);
62
+ this.progressService.start(goals, i18n.t('Initializing'));
63
+ const resourceCreated = await this.resourceCreateService.exec(resourceDto, plaintextDto, passphrase);
64
+ await this.progressService.finishStep(i18n.t('Done!'), true);
65
+ return resourceCreated;
66
+ } finally {
67
+ await this.progressService.close();
136
68
  }
137
69
  }
138
70
  }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Passbolt ~ Open source password manager for teams
3
+ * Copyright (c) Passbolt SA (https://www.passbolt.com)
4
+ *
5
+ * Licensed under GNU Affero General Public License version 3 of the or any later version.
6
+ * For full copyright and license information, please see the LICENSE.txt
7
+ * Redistributions of files must retain the above copyright notice.
8
+ *
9
+ * @copyright Copyright (c) Passbolt SA (https://www.passbolt.com)
10
+ * @license https://opensource.org/licenses/AGPL-3.0 AGPL License
11
+ * @link https://www.passbolt.com Passbolt(tm)
12
+ * @since 4.10.0
13
+ */
14
+
15
+ import {defaultApiClientOptions} from "passbolt-styleguide/src/shared/lib/apiClient/apiClientOptions.test.data";
16
+ import ResourceCreateController from "./resourceCreateController";
17
+ import AccountEntity from "../../model/entity/account/accountEntity";
18
+ import {defaultAccountDto} from "../../model/entity/account/accountEntity.test.data";
19
+ import {defaultResourceDto, defaultResourceV4Dto} from "passbolt-styleguide/src/shared/models/entity/resource/resourceEntity.test.data";
20
+ import {pgpKeys} from "passbolt-styleguide/test/fixture/pgpKeys/keys";
21
+ import {enableFetchMocks} from "jest-fetch-mock";
22
+ import {mockApiResponse} from "../../../../../test/mocks/mockApiResponse";
23
+ import {v4 as uuidv4} from "uuid";
24
+ import EncryptMessageService from "../../service/crypto/encryptMessageService";
25
+
26
+ jest.mock("../../service/passphrase/getPassphraseService");
27
+ jest.mock("../../service/progress/progressService");
28
+
29
+ beforeEach(() => {
30
+ enableFetchMocks();
31
+ });
32
+
33
+ describe("ResourceCreateController", () => {
34
+ let controller, worker;
35
+ const secret = "secret";
36
+
37
+ beforeEach(() => {
38
+ worker = {
39
+ port: {
40
+ emit: jest.fn()
41
+ }
42
+ };
43
+ const account = new AccountEntity(defaultAccountDto());
44
+ const apiClientOptions = defaultApiClientOptions();
45
+ controller = new ResourceCreateController(worker, null, apiClientOptions, account);
46
+ controller.getPassphraseService.getPassphrase.mockResolvedValue(pgpKeys.ada.passphrase);
47
+ fetch.doMockOnce(() => mockApiResponse(defaultResourceV4Dto()));
48
+ });
49
+ describe("AccountRecoveryLoginController::_exec", () => {
50
+ it("Should call the resourceCreateService and emit a success message", async() => {
51
+ expect.assertions(3);
52
+
53
+ const resourceDTO = defaultResourceDto();
54
+ jest.spyOn(controller.resourceCreateService, "exec").mockImplementationOnce(() => resourceDTO);
55
+ await controller._exec(resourceDTO, secret);
56
+
57
+ expect(controller.resourceCreateService.exec).toHaveBeenCalledTimes(1);
58
+ expect(controller.resourceCreateService.exec).toHaveBeenCalledWith(resourceDTO, secret, pgpKeys.ada.passphrase);
59
+ expect(controller.worker.port.emit).toHaveBeenCalledWith(null, 'SUCCESS', resourceDTO);
60
+ });
61
+
62
+ it("Should call the resourceCreateService and emit an error message", async() => {
63
+ expect.assertions(1);
64
+
65
+ const error = new Error();
66
+ jest.spyOn(controller.resourceCreateService, "exec").mockImplementationOnce(() => { throw error; });
67
+ await controller._exec(defaultResourceDto(), null);
68
+
69
+ expect(controller.worker.port.emit).toHaveBeenCalledWith(null, 'ERROR', error);
70
+ });
71
+ });
72
+ describe("AccountRecoveryLoginController::exec", () => {
73
+ it("Should call progress service without folder goals", async() => {
74
+ expect.assertions(2);
75
+
76
+ await controller.exec(defaultResourceDto(), secret);
77
+ expect(controller.progressService.start).toHaveBeenCalledTimes(1);
78
+ expect(controller.progressService.start).toHaveBeenCalledWith(2, 'Initializing');
79
+ });
80
+
81
+ it("Should call progress service with folder goals", async() => {
82
+ expect.assertions(2);
83
+ const resource = defaultResourceDto({
84
+ folder_parent_id: uuidv4()
85
+ });
86
+ await controller.exec(resource, secret);
87
+ expect(controller.progressService.start).toHaveBeenCalledTimes(1);
88
+ expect(controller.progressService.start).toHaveBeenCalledWith(10, 'Initializing');
89
+ });
90
+
91
+ it("Should throw an error if the user passphrase cannot be retrieved", async() => {
92
+ expect.assertions(1);
93
+
94
+ controller.getPassphraseService.getPassphrase.mockImplementation(() => { throw new Error("Cannot retrieve key"); });
95
+ const promise = controller.exec(defaultResourceDto(), secret);
96
+ await expect(promise).rejects.toThrowError("Cannot retrieve key");
97
+ });
98
+
99
+ it("Should close progressService when creation succeed", async() => {
100
+ expect.assertions(2);
101
+
102
+ await controller.exec(defaultResourceDto(), secret);
103
+ expect(controller.progressService.close).toHaveBeenCalledTimes(1);
104
+ expect(controller.progressService.close).toHaveBeenCalled();
105
+ });
106
+
107
+ it("Should close progressService when creation failed", async() => {
108
+ expect.assertions(2);
109
+
110
+ jest.spyOn(EncryptMessageService, "encrypt").mockImplementation(() => Promise.reject("Cannot encrypt message"));
111
+
112
+ try {
113
+ await controller.exec(defaultResourceDto(), secret);
114
+ } catch {
115
+ expect(controller.progressService.close).toHaveBeenCalledTimes(1);
116
+ expect(controller.progressService.close).toHaveBeenCalled();
117
+ }
118
+ });
119
+ });
120
+ });
@@ -204,7 +204,7 @@ class ShareFoldersController {
204
204
 
205
205
  // Share resources
206
206
  if (this.resourcesChanges.length) {
207
- const resourcesDto = this.resources.toDto({secrets: true});
207
+ const resourcesDto = this.resources.toDto({secrets: true, metadata: true});
208
208
  const changesDto = this.resourcesChanges.toDto();
209
209
  await this.progressService.finishStep(i18n.t('Synchronizing keys'), true);
210
210
  await this.keyring.sync();
@@ -91,14 +91,8 @@ const listen = function(worker, apiClientOptions, account) {
91
91
  * @param plaintextDto {string|object} The plaintext data to encrypt
92
92
  */
93
93
  worker.port.on('passbolt.resources.create', async(requestId, resourceDto, plaintextDto) => {
94
- try {
95
- const controller = new ResourceCreateController(worker, requestId, apiClientOptions, account);
96
- const savedResource = await controller.main(resourceDto, plaintextDto);
97
- worker.port.emit(requestId, 'SUCCESS', savedResource);
98
- } catch (error) {
99
- console.error(error);
100
- worker.port.emit(requestId, 'ERROR', error);
101
- }
94
+ const controller = new ResourceCreateController(worker, requestId, apiClientOptions, account);
95
+ await controller._exec(resourceDto, plaintextDto);
102
96
  });
103
97
 
104
98
  /*
@@ -87,10 +87,18 @@ class ExportResourcesFileEntity extends Entity {
87
87
  "resources_ids": {
88
88
  "type": "array",
89
89
  "nullable": true,
90
+ "items": {
91
+ "type": "string",
92
+ "format": "uuid"
93
+ }
90
94
  },
91
95
  "folders_ids": {
92
96
  "type": "array",
93
97
  "nullable": true,
98
+ "items": {
99
+ "type": "string",
100
+ "format": "uuid"
101
+ }
94
102
  },
95
103
  "export_resources": ExternalResourcesCollection.getSchema(),
96
104
  "export_folders": ExternalFoldersCollection.getSchema(),