Skip to content

Commit

Permalink
Merge branch 'release' into 'master'
Browse files Browse the repository at this point in the history
Merge branch 'release' into 'master'

See merge request passbolt/passbolt-browser-extension!900
  • Loading branch information
cedricalfonsi committed Jun 17, 2024
2 parents de0ce2f + 6582ef0 commit 6796e9d
Show file tree
Hide file tree
Showing 60 changed files with 392 additions and 169 deletions.
15 changes: 12 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]
## [4.8.2] - 2024-06-13
### Improved
- PB-33686 As a user I should be signed out after browser update

### Fixed
- PB-33727 Fix session extension, service worker awaken and user instance storage not set
- PB-33801 Remove active account cache in memory

## [4.8.1] - 2024-05-18
### Fix
### Fixed
- PB-33595 As a user running an instance serving an invalid certificate I should be able to sync the gpgkeyring

## [4.8.0] - 2024-05-16
Expand Down Expand Up @@ -1623,8 +1631,9 @@ self registration settings option in the left-side bar
- AP: User with plugin installed
- LU: Logged in user

[Unreleased]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.8.1...HEAD
[4.8.0]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.8.0...4.8.1
[Unreleased]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.8.2...HEAD
[4.8.2]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.8.1...4.8.2
[4.8.1]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.8.0...4.8.1
[4.8.0]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.7.8...4.8.0
[4.7.8]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.7.7...v4.7.8
[4.7.7]: https://github.com/passbolt/passbolt_browser_extension/compare/v4.7.6...v4.7.7
Expand Down
16 changes: 9 additions & 7 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
Song: https://www.youtube.com/watch?v=hbe3CQamF8k
Song: https://www.youtube.com/watch?v=OypXGyN6OZw

Passbolt v4.8.1 is a maintenance update that addresses issues related to servers serving invalid SSL certificates, which affected the accessibility of the API with certain user journeys.
Passbolt v4.8.2 is a maintenance update that addresses issues related to MV3.

We hope these updates enhance your experience with Passbolt. Your feedback is always valuable to us.

## [4.8.1] - 2024-05-18
### Fix
- PB-33595 As a user running an instance serving an invalid certificate I should be able to sync the gpgkeyring
- PB-33596 As a user running an instance serving an invalid certificate I cannot sync my account settings
- PB-33597 As a user running an instance serving an invalid certificate I cannot install passbolt extension using an API < v3
## [4.8.2] - 2024-06-13
### Improved
- PB-33686 As a user I should be signed out after browser update

### Fixed
- PB-33727 Fix session extension, service worker awaken and user instance storage not set
- PB-33801 Remove active account cache in memory
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "passbolt-browser-extension",
"version": "4.8.1",
"version": "4.8.2",
"license": "AGPL-3.0",
"copyright": "Copyright 2022 Passbolt SA",
"description": "Passbolt web extension for the open source password manager for teams",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import Log from "../../model/log";
import {BrowserExtensionIconService} from "../../service/ui/browserExtensionIcon.service";
import storage from "../../sdk/storage";
import {Config} from "../../model/config";
import AuthModel from "../../model/auth/authModel";
import AppBootstrapPagemod from "../../pagemod/appBootstrapPagemod";

class OnExtensionInstalledController {
/**
Expand All @@ -45,6 +47,11 @@ class OnExtensionInstalledController {
case browser.runtime.OnInstalledReason.UPDATE:
await OnExtensionInstalledController.onUpdate();
break;
case browser.runtime.OnInstalledReason.CHROME_UPDATE:
case browser.runtime.OnInstalledReason.BROWSER_UPDATE:
// Force logout for users to not ask passphrase after a browser update which clear the session storage
await OnExtensionInstalledController.onBrowserUpdate();
break;
default:
console.debug(`The install reason ${details.reason} is not supported`);
break;
Expand All @@ -67,6 +74,45 @@ class OnExtensionInstalledController {
await OnExtensionInstalledController.updateToolbarIcon();
}

/**
* On update of the browser, logout the user. It helps mitigate issue where users update their browser, are still
* signed-in but session memory used to store user passphrase or other information is flushed. These behavior
* deteriorates the user experience, therefore it has been decided to sign the user out.
*/
static async onBrowserUpdate() {
const user = User.getInstance();
// Check if user is valid
if (!user.isValid()) {
return;
}
let authStatus;
try {
const checkAuthStatusService = new CheckAuthStatusService();
// use the cached data as the worker could wake up every 30 secondes.
authStatus = await checkAuthStatusService.checkAuthStatus(false);
} catch (error) {
// Service is unavailable, do nothing...
Log.write({level: 'debug', message: 'Could not check if the user is authenticated, the service is unavailable.'});
return;
}
// Do nothing if user is not authenticated
if (!authStatus.isAuthenticated) {
return;
}
// Logout authenticated user to prevent to ask passphrase for SSO users
const apiClientOptions = await user.getApiClientOptions();
const authModel = new AuthModel(apiClientOptions);
await authModel.logout();
/*
* Reload only tabs that match passbolt app url. Reload is necessary as the application loaded in the tab
* could be unresponsive due to the forced triggered sign-out.
* - When the tab is started the API didn't redirect the user as the user is still signed-in.
* - This script sign-out the user
* - The pagemod of the app starts and throw an error as the user has been signed-out
*/
await browser.tabs.query({}).then(reloadTabsMatchPassboltAppUrl);
}

/**
* Updates the Passbolt icon in the toolbar according to the sign-in status of the current user.
* @returns {Promise<void>}
Expand All @@ -81,11 +127,11 @@ class OnExtensionInstalledController {
let authStatus;
try {
const checkAuthStatusService = new CheckAuthStatusService();
// user the cached data as the worker could wake up every 30 secondes.
// use the cached data as the worker could wake up every 30 secondes.
authStatus = await checkAuthStatusService.checkAuthStatus(false);
} catch (error) {
// Service is unavailable, do nothing...
Log.write({level: 'debug', message: 'The Service is unavailable to check if the user is authenticated'});
Log.write({level: 'debug', message: 'Could not check if the user is authenticated, the service is unavailable.'});
return;
}

Expand Down Expand Up @@ -127,13 +173,25 @@ const closeTabWebStore = tabs => {
};


/**
* Reload the tabs that match passsbolt app url
* @param tabs
*/
const reloadTabsMatchPassboltAppUrl = async tabs => {
for (const tab of tabs) {
if (await AppBootstrapPagemod.assertUrlAttachConstraint(tab)) {
browser.tabs.reload(tab.id);
}
}
};

/**
* Reload the tabs that match pagemod url
* @param tabs
*/
const reloadTabsMatchPagemodUrl = tabs => {
tabs.map(tab => {
if (PagemodManager.hasPagemodMatchUrlToReload(tab.url)) {
const reloadTabsMatchPagemodUrl = async tabs => {
for (const tab of tabs) {
if (await PagemodManager.hasPagemodMatchUrlToReload(tab.url)) {
browser.tabs.reload(tab.id);
} else {
// For other tabs detect and inject the new content script
Expand All @@ -144,7 +202,7 @@ const reloadTabsMatchPagemodUrl = tabs => {
};
WebNavigationService.exec(frameDetails);
}
});
}
};

export default OnExtensionInstalledController;
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
*/

import OnExtensionInstalledController from "./onExtensionInstalledController";
import User from "../../model/user";
import UserSettings from "../../model/userSettings/userSettings";
import WebNavigationService from "../../service/webNavigation/webNavigationService";
import AuthModel from "../../model/auth/authModel";
import CheckAuthStatusService from "../../service/auth/checkAuthStatusService";
import GetActiveAccountService from "../../service/account/getActiveAccountService";
import User from "../../model/user";

// Reset the modules before each test.
beforeEach(() => {
Expand Down Expand Up @@ -73,7 +76,7 @@ describe("OnExtensionInstalledController", () => {
jest.spyOn(OnExtensionInstalledController, "onUpdate");
jest.spyOn(WebNavigationService, "exec");
jest.spyOn(browser.tabs, "query").mockImplementationOnce(() => Promise.resolve(tabs));
jest.spyOn(User.getInstance(), "isValid").mockImplementation(() => true);
jest.spyOn(GetActiveAccountService, "get").mockImplementation(() => {});
jest.spyOn(UserSettings.prototype, "getDomain").mockImplementation(() => "https://passbolt.dev");
// process
await OnExtensionInstalledController.exec(details);
Expand All @@ -89,21 +92,72 @@ describe("OnExtensionInstalledController", () => {
expect(WebNavigationService.exec).toHaveBeenCalledTimes(2);
});

it("Should exec browser update if the reason is chrome update", async() => {
expect.assertions(3);
// data mocked
const details = {
reason: browser.runtime.OnInstalledReason.CHROME_UPDATE
};
// mock function
jest.spyOn(OnExtensionInstalledController, "onBrowserUpdate");
jest.spyOn(User.getInstance(), "isValid").mockImplementation(() => false);
jest.spyOn(AuthModel.prototype, "logout");
// process
await OnExtensionInstalledController.exec(details);
// expectation
expect(OnExtensionInstalledController.onBrowserUpdate).toHaveBeenCalled();
expect(User.getInstance().isValid).toHaveBeenCalledTimes(1);
expect(AuthModel.prototype.logout).toHaveBeenCalledTimes(0);
});

it("Should exec browser update if the reason is browser update", async() => {
expect.assertions(4);
// data mocked
const details = {
reason: browser.runtime.OnInstalledReason.BROWSER_UPDATE
};
const tabs = [
{id: 1, url: "https://passbolt.dev/app/passwords"},
{id: 2, url: "https://passbolt.dev/setup/recover/start/571bec7e-6cce-451d-b53a-f8c93e147228/5ea0fc9c-b180-4873-8e00-9457862e43e0"},
{id: 3, url: "https://passbolt.dev/auth/login"},
{id: 4, url: "https://passbolt.dev"},
{id: 5, url: "https://passbolt.com"},
{id: 6, url: "https://localhost"}
];
// mock function
jest.spyOn(OnExtensionInstalledController, "onBrowserUpdate");
jest.spyOn(User.getInstance(), "isValid").mockImplementation(() => true);
jest.spyOn(UserSettings.prototype, "getDomain").mockImplementation(() => "https://passbolt.dev");
jest.spyOn(AuthModel.prototype, "logout").mockImplementation(() => {});
jest.spyOn(browser.tabs, "query").mockImplementation(() => Promise.resolve(tabs));
jest.spyOn(browser.tabs, "reload");
jest.spyOn(CheckAuthStatusService.prototype, "checkAuthStatus").mockImplementation(() => ({isAuthenticated: true}));
// process
await OnExtensionInstalledController.exec(details);
// expectation
expect(OnExtensionInstalledController.onBrowserUpdate).toHaveBeenCalled();
expect(AuthModel.prototype.logout).toHaveBeenCalledTimes(1);
expect(browser.tabs.query).toHaveBeenCalledTimes(1);
expect(browser.tabs.reload).toHaveBeenCalledWith(tabs[0].id);
});

it("Should not exec update neither install if the reason is unknown", async() => {
expect.assertions(2);
expect.assertions(3);
// data mocked
const details = {
reason: "unknown"
};
// mock function
jest.spyOn(OnExtensionInstalledController, "onUpdate");
jest.spyOn(OnExtensionInstalledController, "onInstall");
jest.spyOn(OnExtensionInstalledController, "onBrowserUpdate");

// process
await OnExtensionInstalledController.exec(details);
// expectation
expect(OnExtensionInstalledController.onUpdate).not.toHaveBeenCalled();
expect(OnExtensionInstalledController.onInstall).not.toHaveBeenCalled();
expect(OnExtensionInstalledController.onBrowserUpdate).not.toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("DeletePasswordExpirySettingsController", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

it("Should delete the entity from the API given an ID", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe("GetOrFindPasswordExpirySettingsController", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

it("Should return the value from the API", async() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe("SavePasswordExpirySettingsController", () => {
fetch.resetMocks();
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));
account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

it("Should save the given dto on the API using PasswordExpirySettingsEntity", async() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe("FindPasswordPoliciesController::exec", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

it("Should return the registered password policies", async() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("SavePasswordPoliciesController::exec", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

it("Should save the given data", async() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe("FindUserPassphrasePoliciesController", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

const account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

it("Should return the value from the API", async() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe("SaveUserPassphrasePoliciesController", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

const account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

it("Should save the given dto on the API", async() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ describe("PasswordExpiry model", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

const account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
model = new PasswordExpirySettingsModel(account, apiClientOptions);
jest.spyOn(model.organisationSettingsModel, "getOrFind").mockImplementation(() => (Promise.resolve({
isPluginEnabled: () => false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe("PasswordPoliciesModel", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

describe("::get", () => {
Expand Down
1 change: 1 addition & 0 deletions src/all/background_page/model/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Validator from "validator";
import {ValidatorRule} from "../utils/validatorRules";

/**
* @deprecated
* The class that deals with users.
*/
const User = (function() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("UserPassphrasePolicies model", () => {
jest.spyOn(browser.cookies, "get").mockImplementationOnce(() => ({value: "csrf-token"}));

const account = new AccountEntity(defaultAccountDto());
apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
});

describe('::findOrDefault', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/all/background_page/pagemod/accountRecoveryPagemod.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class AccountRecovery extends Pagemod {
try {
const tab = port._port.sender.tab;
const account = await GetRequestLocalAccountService.getAccountMatchingContinueUrl(tab.url);
const apiClientOptions = await BuildApiClientOptionsService.buildFromAccount(account);
const apiClientOptions = BuildApiClientOptionsService.buildFromAccount(account);
for (const event of this.events) {
event.listen({port, tab}, apiClientOptions, account);
}
Expand Down
Loading

0 comments on commit 6796e9d

Please sign in to comment.