mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 09:13:33 +00:00
[PM-21024] Use Server for Password Change URLs (#14912)
* migrate change login password service to use bitwarden server rather than fetch directly - avoids CSP entirely * add `HelpUsersUpdatePasswords` policy to policy type * add `HelpUsersUpdatePasswordsPolicy` components * allow list description override for policy description * add `HelpUsersUpdatePasswordsPolicy` when the feature flag is enabled * apply `HelpUsersUpdatePasswords` to everyone in an org * use policy to guard the well known password API * fix tests * refactor to use `policyAppliesToUser$` * remove policy work for change password - this was removed from scope * update copy for show favicon setting - it now handles both favicons and change password urls * remove favicon setting description - no longer needed * only call change password service when the setting is enabled * add popover for permitting cipher details * import permit popover directly into the settings component * replace `nativeFetch` with `fetch` * use string literal to construct URL rather than `URL` class - The `getIconsUrl` can return with an appended path which the new URL constructor will strip when passed as the base parameter * use string literal to construct URL rather than `URL` class instance (#16045) - The `getIconsUrl` can return with an appended path which the new URL constructor will strip when passed as the base parameter * [PM-24716] UI changes for Change URI work (#16043) * use platform service to launch the URI - this allows desktop to open a separate browser instance rather than use electron * fix spacing on web app * add bitLink for focus/hover states * remove spacing around links
This commit is contained in:
@@ -4,10 +4,14 @@
|
||||
*/
|
||||
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import {
|
||||
Environment,
|
||||
EnvironmentService,
|
||||
} from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
|
||||
@@ -18,37 +22,30 @@ import { DefaultChangeLoginPasswordService } from "./default-change-login-passwo
|
||||
describe("DefaultChangeLoginPasswordService", () => {
|
||||
let service: DefaultChangeLoginPasswordService;
|
||||
|
||||
let mockShouldNotExistResponse: Response;
|
||||
let mockWellKnownResponse: Response;
|
||||
|
||||
const getClientType = jest.fn(() => ClientType.Browser);
|
||||
|
||||
const mockApiService = mock<ApiService>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>({
|
||||
getClientType,
|
||||
});
|
||||
const mockDomainSettingsService = mock<DomainSettingsService>();
|
||||
|
||||
const showFavicons$ = new BehaviorSubject<boolean>(true);
|
||||
|
||||
beforeEach(() => {
|
||||
mockApiService.nativeFetch.mockClear();
|
||||
mockApiService.fetch.mockClear();
|
||||
mockApiService.fetch.mockImplementation(() =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve({ uri: null }) } as Response),
|
||||
);
|
||||
|
||||
// Default responses to success state
|
||||
mockShouldNotExistResponse = new Response("Not Found", { status: 404 });
|
||||
mockWellKnownResponse = new Response("OK", { status: 200 });
|
||||
mockDomainSettingsService.showFavicons$ = showFavicons$;
|
||||
|
||||
mockApiService.nativeFetch.mockImplementation((request) => {
|
||||
if (
|
||||
request.url.endsWith("resource-that-should-not-exist-whose-status-code-should-not-be-200")
|
||||
) {
|
||||
return Promise.resolve(mockShouldNotExistResponse);
|
||||
}
|
||||
const mockEnvironmentService = {
|
||||
environment$: of({
|
||||
getIconsUrl: () => "https://icons.bitwarden.com",
|
||||
} as Environment),
|
||||
} as EnvironmentService;
|
||||
|
||||
if (request.url.endsWith(".well-known/change-password")) {
|
||||
return Promise.resolve(mockWellKnownResponse);
|
||||
}
|
||||
|
||||
throw new Error("Unexpected request");
|
||||
});
|
||||
service = new DefaultChangeLoginPasswordService(mockApiService, platformUtilsService);
|
||||
service = new DefaultChangeLoginPasswordService(
|
||||
mockApiService,
|
||||
mockEnvironmentService,
|
||||
mockDomainSettingsService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return null for non-login ciphers", async () => {
|
||||
@@ -85,7 +82,7 @@ describe("DefaultChangeLoginPasswordService", () => {
|
||||
expect(url).toBeNull();
|
||||
});
|
||||
|
||||
it("should check the origin for a reliable status code", async () => {
|
||||
it("should call the icons url endpoint", async () => {
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
@@ -95,35 +92,42 @@ describe("DefaultChangeLoginPasswordService", () => {
|
||||
|
||||
await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(mockApiService.nativeFetch).toHaveBeenCalledWith(
|
||||
expect(mockApiService.fetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://example.com/.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200",
|
||||
url: "https://icons.bitwarden.com/change-password-uri?uri=https%3A%2F%2Fexample.com%2F",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should attempt to fetch the well-known change password URL", async () => {
|
||||
it("should return the original URI when unable to verify the response", async () => {
|
||||
mockApiService.fetch.mockImplementation(() =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve({ uri: null }) } as Response),
|
||||
);
|
||||
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com" }],
|
||||
uris: [{ uri: "https://example.com/" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
await service.getChangePasswordUrl(cipher);
|
||||
const url = await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(mockApiService.nativeFetch).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "https://example.com/.well-known/change-password",
|
||||
}),
|
||||
);
|
||||
expect(url).toBe("https://example.com/");
|
||||
});
|
||||
|
||||
it("should return the well-known change password URL when successful at verifying the response", async () => {
|
||||
it("should return the well known change url from the response", async () => {
|
||||
mockApiService.fetch.mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uri: "https://example.com/.well-known/change-password" }),
|
||||
} as Response);
|
||||
});
|
||||
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com" }],
|
||||
uris: [{ uri: "https://example.com/" }, { uri: "https://working.com/" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
@@ -132,49 +136,20 @@ describe("DefaultChangeLoginPasswordService", () => {
|
||||
expect(url).toBe("https://example.com/.well-known/change-password");
|
||||
});
|
||||
|
||||
it("should return the original URI when unable to verify the response", async () => {
|
||||
mockShouldNotExistResponse = new Response("Ok", { status: 200 });
|
||||
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com/" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
const url = await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(url).toBe("https://example.com/");
|
||||
});
|
||||
|
||||
it("should return the original URI when the well-known URL is not found", async () => {
|
||||
mockWellKnownResponse = new Response("Not Found", { status: 404 });
|
||||
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com/" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
const url = await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(url).toBe("https://example.com/");
|
||||
});
|
||||
|
||||
it("should try the next URI if the first one fails", async () => {
|
||||
mockApiService.nativeFetch.mockImplementation((request) => {
|
||||
if (
|
||||
request.url.endsWith("resource-that-should-not-exist-whose-status-code-should-not-be-200")
|
||||
) {
|
||||
return Promise.resolve(mockShouldNotExistResponse);
|
||||
mockApiService.fetch.mockImplementation((request) => {
|
||||
if (request.url.includes("no-wellknown.com")) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uri: null }),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
if (request.url.endsWith(".well-known/change-password")) {
|
||||
if (request.url.includes("working.com")) {
|
||||
return Promise.resolve(mockWellKnownResponse);
|
||||
}
|
||||
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
||||
if (request.url.includes("working.com")) {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ uri: "https://working.com/.well-known/change-password" }),
|
||||
} as Response);
|
||||
}
|
||||
|
||||
throw new Error("Unexpected request");
|
||||
@@ -192,19 +167,19 @@ describe("DefaultChangeLoginPasswordService", () => {
|
||||
expect(url).toBe("https://working.com/.well-known/change-password");
|
||||
});
|
||||
|
||||
it("should return the first URI when the client type is not browser", async () => {
|
||||
getClientType.mockReturnValue(ClientType.Web);
|
||||
it("returns the first URI when `showFavicons$` setting is disabled", async () => {
|
||||
showFavicons$.next(false);
|
||||
|
||||
const cipher = {
|
||||
type: CipherType.Login,
|
||||
login: Object.assign(new LoginView(), {
|
||||
uris: [{ uri: "https://example.com/" }, { uri: "https://example-2.com/" }],
|
||||
uris: [{ uri: "https://example.com/" }, { uri: "https://another.com/" }],
|
||||
}),
|
||||
} as CipherView;
|
||||
|
||||
const url = await service.getChangePasswordUrl(cipher);
|
||||
|
||||
expect(mockApiService.nativeFetch).not.toHaveBeenCalled();
|
||||
expect(url).toBe("https://example.com/");
|
||||
expect(mockApiService.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user