mirror of
https://github.com/bitwarden/browser
synced 2025-12-21 10:43:35 +00:00
Merge branch 'master' into EC-598-beeep-properly-store-passkeys-in-bitwarden
This commit is contained in:
@@ -79,8 +79,8 @@ describe("Dashlane CSV Importer", () => {
|
||||
expect(cipher2.card.cardholderName).toBe("John Doe");
|
||||
expect(cipher2.card.number).toBe("41111111111111111");
|
||||
expect(cipher2.card.code).toBe("123");
|
||||
expect(cipher2.card.expMonth).toBe("01");
|
||||
expect(cipher2.card.expYear).toBe("23");
|
||||
expect(cipher2.card.expMonth).toBe("1");
|
||||
expect(cipher2.card.expYear).toBe("2023");
|
||||
|
||||
expect(cipher2.fields.length).toBe(2);
|
||||
|
||||
|
||||
133
libs/common/spec/importers/enpass/enpass-json-importer.spec.ts
Normal file
133
libs/common/spec/importers/enpass/enpass-json-importer.spec.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { EnpassJsonImporter as Importer } from "@bitwarden/common/importers/enpass/enpass-json-importer";
|
||||
import { FieldView } from "@bitwarden/common/models/view/field.view";
|
||||
|
||||
import { creditCard } from "./test-data/json/credit-card";
|
||||
import { folders } from "./test-data/json/folders";
|
||||
import { login } from "./test-data/json/login";
|
||||
import { loginAndroidUrl } from "./test-data/json/login-android-url";
|
||||
import { note } from "./test-data/json/note";
|
||||
|
||||
function validateCustomField(fields: FieldView[], fieldName: string, expectedValue: any) {
|
||||
expect(fields).toBeDefined();
|
||||
const customField = fields.find((f) => f.name === fieldName);
|
||||
expect(customField).toBeDefined();
|
||||
|
||||
expect(customField.value).toEqual(expectedValue);
|
||||
}
|
||||
|
||||
describe("Enpass JSON Importer", () => {
|
||||
it("should create folders/ nested folder and assignment", async () => {
|
||||
const importer = new Importer();
|
||||
const testDataString = JSON.stringify(folders);
|
||||
const result = await importer.parse(testDataString);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
expect(result.folders.length).toEqual(2);
|
||||
const folder1 = result.folders.shift();
|
||||
expect(folder1.name).toEqual("Social");
|
||||
|
||||
// Created 2 folders and Twitter is child of Social
|
||||
const folder2 = result.folders.shift();
|
||||
expect(folder2.name).toEqual("Social/Twitter");
|
||||
|
||||
// Expect that the single cipher item is assigned to sub-folder "Social/Twitter"
|
||||
const folderRelationship = result.folderRelationships.shift();
|
||||
expect(folderRelationship).toEqual([0, 1]);
|
||||
});
|
||||
|
||||
it("should parse login items", async () => {
|
||||
const importer = new Importer();
|
||||
const testDataString = JSON.stringify(login);
|
||||
const result = await importer.parse(testDataString);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.type).toEqual(CipherType.Login);
|
||||
expect(cipher.name).toEqual("Amazon");
|
||||
expect(cipher.subTitle).toEqual("emily@enpass.io");
|
||||
expect(cipher.favorite).toBe(true);
|
||||
expect(cipher.notes).toEqual("some notes on the login item");
|
||||
|
||||
expect(cipher.login.username).toEqual("emily@enpass.io");
|
||||
expect(cipher.login.password).toEqual("$&W:v@}4\\iRpUXVbjPdPKDGbD<xK>");
|
||||
expect(cipher.login.totp).toEqual("TOTP_SEED_VALUE");
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
const uriView = cipher.login.uris.shift();
|
||||
expect(uriView.uri).toEqual("https://www.amazon.com");
|
||||
|
||||
// remaining fields as custom fields
|
||||
expect(cipher.fields.length).toEqual(3);
|
||||
validateCustomField(cipher.fields, "Phone number", "12345678");
|
||||
validateCustomField(cipher.fields, "Security question", "SECURITY_QUESTION");
|
||||
validateCustomField(cipher.fields, "Security answer", "SECURITY_ANSWER");
|
||||
});
|
||||
|
||||
it("should parse login items with Android Autofill information", async () => {
|
||||
const importer = new Importer();
|
||||
const testDataString = JSON.stringify(loginAndroidUrl);
|
||||
const result = await importer.parse(testDataString);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.type).toEqual(CipherType.Login);
|
||||
expect(cipher.name).toEqual("Amazon");
|
||||
|
||||
expect(cipher.login.uris.length).toEqual(5);
|
||||
expect(cipher.login.uris[0].uri).toEqual("https://www.amazon.com");
|
||||
expect(cipher.login.uris[1].uri).toEqual("androidapp://com.amazon.0");
|
||||
expect(cipher.login.uris[2].uri).toEqual("androidapp://com.amazon.1");
|
||||
expect(cipher.login.uris[3].uri).toEqual("androidapp://com.amazon.2");
|
||||
expect(cipher.login.uris[4].uri).toEqual("androidapp://com.amazon.3");
|
||||
});
|
||||
|
||||
it("should parse credit card items", async () => {
|
||||
const importer = new Importer();
|
||||
const testDataString = JSON.stringify(creditCard);
|
||||
const result = await importer.parse(testDataString);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.type).toEqual(CipherType.Card);
|
||||
expect(cipher.name).toEqual("Emily Sample Credit Card");
|
||||
expect(cipher.subTitle).toEqual("Amex, *10005");
|
||||
expect(cipher.favorite).toBe(true);
|
||||
expect(cipher.notes).toEqual("some notes on the credit card");
|
||||
|
||||
expect(cipher.card.cardholderName).toEqual("Emily Sample");
|
||||
expect(cipher.card.number).toEqual("3782 822463 10005");
|
||||
expect(cipher.card.brand).toEqual("Amex");
|
||||
expect(cipher.card.code).toEqual("1234");
|
||||
expect(cipher.card.expMonth).toEqual("3");
|
||||
expect(cipher.card.expYear).toEqual("23");
|
||||
|
||||
// remaining fields as custom fields
|
||||
expect(cipher.fields.length).toEqual(9);
|
||||
validateCustomField(cipher.fields, "PIN", "9874");
|
||||
validateCustomField(cipher.fields, "Username", "Emily_ENP");
|
||||
validateCustomField(
|
||||
cipher.fields,
|
||||
"Login password",
|
||||
"nnn tug shoot selfish bon liars convent dusty minnow uncheck"
|
||||
);
|
||||
validateCustomField(cipher.fields, "Website", "http://global.americanexpress.com/");
|
||||
validateCustomField(cipher.fields, "Issuing bank", "American Express");
|
||||
validateCustomField(cipher.fields, "Credit limit", "100000");
|
||||
validateCustomField(cipher.fields, "Withdrawal limit", "50000");
|
||||
validateCustomField(cipher.fields, "Interest rate", "1.5");
|
||||
validateCustomField(cipher.fields, "If lost, call", "12345678");
|
||||
});
|
||||
|
||||
it("should parse notes", async () => {
|
||||
const importer = new Importer();
|
||||
const testDataString = JSON.stringify(note);
|
||||
const result = await importer.parse(testDataString);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.type).toEqual(CipherType.SecureNote);
|
||||
expect(cipher.name).toEqual("some secure note title");
|
||||
expect(cipher.favorite).toBe(false);
|
||||
expect(cipher.notes).toEqual("some secure note content");
|
||||
});
|
||||
});
|
||||
274
libs/common/spec/importers/enpass/test-data/json/credit-card.ts
Normal file
274
libs/common/spec/importers/enpass/test-data/json/credit-card.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
|
||||
|
||||
export const creditCard: EnpassJsonFile = {
|
||||
folders: [],
|
||||
items: [
|
||||
{
|
||||
archived: 0,
|
||||
auto_submit: 1,
|
||||
category: "creditcard",
|
||||
createdAt: 1666449561,
|
||||
favorite: 1,
|
||||
fields: [
|
||||
{
|
||||
deleted: 0,
|
||||
history: [
|
||||
{
|
||||
updated_at: 1534490234,
|
||||
value: "Wendy Apple Seed",
|
||||
},
|
||||
{
|
||||
updated_at: 1535521811,
|
||||
value: "Emma",
|
||||
},
|
||||
{
|
||||
updated_at: 1535522090,
|
||||
value: "Emily",
|
||||
},
|
||||
],
|
||||
label: "Cardholder",
|
||||
order: 1,
|
||||
sensitive: 0,
|
||||
type: "ccName",
|
||||
uid: 0,
|
||||
updated_at: 1666449561,
|
||||
value: "Emily Sample",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Type",
|
||||
order: 2,
|
||||
sensitive: 0,
|
||||
type: "ccType",
|
||||
uid: 17,
|
||||
updated_at: 1666449561,
|
||||
value: "American Express",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
history: [
|
||||
{
|
||||
updated_at: 1534490234,
|
||||
value: "1234 1234 5678 0000",
|
||||
},
|
||||
],
|
||||
label: "Number",
|
||||
order: 3,
|
||||
sensitive: 0,
|
||||
type: "ccNumber",
|
||||
uid: 1,
|
||||
updated_at: 1666449561,
|
||||
value: "3782 822463 10005",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "CVC",
|
||||
order: 4,
|
||||
sensitive: 1,
|
||||
type: "ccCvc",
|
||||
uid: 2,
|
||||
updated_at: 1666449561,
|
||||
value: "1234",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "PIN",
|
||||
order: 5,
|
||||
sensitive: 1,
|
||||
type: "ccPin",
|
||||
uid: 3,
|
||||
updated_at: 1666449561,
|
||||
value: "9874",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Expiry date",
|
||||
order: 6,
|
||||
sensitive: 0,
|
||||
type: "ccExpiry",
|
||||
uid: 4,
|
||||
updated_at: 1666449561,
|
||||
value: "03/23",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "INTERNET BANKING",
|
||||
order: 7,
|
||||
sensitive: 0,
|
||||
type: "section",
|
||||
uid: 103,
|
||||
updated_at: 1666449561,
|
||||
value: "",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
history: [
|
||||
{
|
||||
updated_at: 1534490234,
|
||||
value: "WendySeed",
|
||||
},
|
||||
{
|
||||
updated_at: 1535521811,
|
||||
value: "Emma1",
|
||||
},
|
||||
{
|
||||
updated_at: 1535522182,
|
||||
value: "Emily1",
|
||||
},
|
||||
],
|
||||
label: "Username",
|
||||
order: 8,
|
||||
sensitive: 0,
|
||||
type: "username",
|
||||
uid: 15,
|
||||
updated_at: 1666449561,
|
||||
value: "Emily_ENP",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Login password",
|
||||
order: 9,
|
||||
sensitive: 1,
|
||||
type: "password",
|
||||
uid: 16,
|
||||
updated_at: 1666449561,
|
||||
value: "nnn tug shoot selfish bon liars convent dusty minnow uncheck",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Transaction password",
|
||||
order: 10,
|
||||
sensitive: 1,
|
||||
type: "ccTxnpassword",
|
||||
uid: 9,
|
||||
updated_at: 1666449561,
|
||||
value: "",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Website",
|
||||
order: 11,
|
||||
sensitive: 0,
|
||||
type: "url",
|
||||
uid: 14,
|
||||
updated_at: 1666449561,
|
||||
value: "http://global.americanexpress.com/",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "ADDITIONAL DETAILS",
|
||||
order: 12,
|
||||
sensitive: 0,
|
||||
type: "section",
|
||||
uid: 104,
|
||||
updated_at: 1666449561,
|
||||
value: "",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Issuing bank",
|
||||
order: 13,
|
||||
sensitive: 0,
|
||||
type: "ccBankname",
|
||||
uid: 6,
|
||||
updated_at: 1666449561,
|
||||
value: "American Express",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Issued on",
|
||||
order: 14,
|
||||
sensitive: 0,
|
||||
type: "date",
|
||||
uid: 7,
|
||||
updated_at: 1666449561,
|
||||
value: "",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Valid from",
|
||||
order: 15,
|
||||
sensitive: 0,
|
||||
type: "ccValidfrom",
|
||||
uid: 18,
|
||||
updated_at: 1666449561,
|
||||
value: "",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Credit limit",
|
||||
order: 16,
|
||||
sensitive: 0,
|
||||
type: "numeric",
|
||||
uid: 10,
|
||||
updated_at: 1666449561,
|
||||
value: "100000",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Withdrawal limit",
|
||||
order: 17,
|
||||
sensitive: 0,
|
||||
type: "numeric",
|
||||
uid: 11,
|
||||
updated_at: 1666449561,
|
||||
value: "50000",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Interest rate",
|
||||
order: 18,
|
||||
sensitive: 0,
|
||||
type: "numeric",
|
||||
uid: 12,
|
||||
updated_at: 1666449561,
|
||||
value: "1.5",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "If lost, call",
|
||||
order: 19,
|
||||
sensitive: 0,
|
||||
type: "phone",
|
||||
uid: 8,
|
||||
updated_at: 1666449561,
|
||||
value: "12345678",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
],
|
||||
icon: {
|
||||
fav: "global.americanexpress.com",
|
||||
image: {
|
||||
file: "cc/american_express",
|
||||
},
|
||||
type: 2,
|
||||
uuid: "",
|
||||
},
|
||||
note: "some notes on the credit card",
|
||||
subtitle: "***** 0000",
|
||||
template_type: "creditcard.default",
|
||||
title: "Emily Sample Credit Card",
|
||||
trashed: 0,
|
||||
updated_at: 1666554351,
|
||||
uuid: "dbbc741b-81d6-491a-9660-92995fd8958c",
|
||||
},
|
||||
],
|
||||
};
|
||||
45
libs/common/spec/importers/enpass/test-data/json/folders.ts
Normal file
45
libs/common/spec/importers/enpass/test-data/json/folders.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
|
||||
|
||||
export const folders: EnpassJsonFile = {
|
||||
folders: [
|
||||
{
|
||||
icon: "1008",
|
||||
parent_uuid: "",
|
||||
title: "Social",
|
||||
updated_at: 1666449561,
|
||||
uuid: "7b2ed0da-8cd9-445f-9a1a-490ca2b9ffbc",
|
||||
},
|
||||
{
|
||||
icon: "1008",
|
||||
parent_uuid: "7b2ed0da-8cd9-445f-9a1a-490ca2b9ffbc",
|
||||
title: "Twitter",
|
||||
updated_at: 1666450857,
|
||||
uuid: "7fe8a8bc-b848-4f9f-9870-c2936317e74d",
|
||||
},
|
||||
],
|
||||
items: [
|
||||
{
|
||||
archived: 0,
|
||||
auto_submit: 1,
|
||||
category: "note",
|
||||
createdAt: 1666554621,
|
||||
favorite: 0,
|
||||
folders: ["7fe8a8bc-b848-4f9f-9870-c2936317e74d"],
|
||||
icon: {
|
||||
fav: "",
|
||||
image: {
|
||||
file: "misc/secure_note",
|
||||
},
|
||||
type: 1,
|
||||
uuid: "",
|
||||
},
|
||||
note: "some secure note content",
|
||||
subtitle: "",
|
||||
template_type: "note.default",
|
||||
title: "some secure note title",
|
||||
trashed: 0,
|
||||
updated_at: 1666554621,
|
||||
uuid: "8b5ea2f6-f62b-4fec-a235-4a40946026b6",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
|
||||
|
||||
import { login } from "./login";
|
||||
|
||||
export const loginAndroidUrl: EnpassJsonFile = {
|
||||
folders: [],
|
||||
items: [
|
||||
{
|
||||
archived: 0,
|
||||
auto_submit: 1,
|
||||
category: "login",
|
||||
createdAt: 1666449561,
|
||||
favorite: 1,
|
||||
fields: [
|
||||
...login.items[0].fields,
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Autofill Info",
|
||||
order: 9,
|
||||
sensitive: 0,
|
||||
type: ".Android#",
|
||||
uid: 7696,
|
||||
updated_at: 1666551057,
|
||||
value: "com.amazon.0",
|
||||
value_updated_at: 1666551057,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Autofill Info 1",
|
||||
order: 9,
|
||||
sensitive: 0,
|
||||
type: ".Android#",
|
||||
uid: 7696,
|
||||
updated_at: 1666551057,
|
||||
value:
|
||||
"android://pMUhLBalOhcc3yK-84sMiGc2U856FVVUhm8PZveoRfNFT3ocT1KWZlciAkF2ED--B5i_fMuNlC6JfPxcHk1AQg==@com.amazon.1",
|
||||
value_updated_at: 1666551057,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Autofill Info2 ",
|
||||
order: 9,
|
||||
sensitive: 0,
|
||||
type: ".Android#",
|
||||
uid: 7696,
|
||||
updated_at: 1666551057,
|
||||
value: "android://com.amazon.2",
|
||||
value_updated_at: 1666551057,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Autofill Info 3",
|
||||
order: 9,
|
||||
sensitive: 0,
|
||||
type: ".Android#",
|
||||
uid: 7696,
|
||||
updated_at: 1666551057,
|
||||
value: "androidapp://com.amazon.3",
|
||||
value_updated_at: 1666551057,
|
||||
},
|
||||
],
|
||||
icon: {
|
||||
fav: "www.amazon.com",
|
||||
image: {
|
||||
file: "web/amazon.com",
|
||||
},
|
||||
type: 1,
|
||||
uuid: "",
|
||||
},
|
||||
note: "some notes on the login item",
|
||||
subtitle: "emily@enpass.io",
|
||||
template_type: "login.default",
|
||||
title: "Amazon",
|
||||
trashed: 0,
|
||||
updated_at: 1666449561,
|
||||
uuid: "f717cb7c-6cce-4b24-b023-ec8a429cc992",
|
||||
},
|
||||
],
|
||||
};
|
||||
130
libs/common/spec/importers/enpass/test-data/json/login.ts
Normal file
130
libs/common/spec/importers/enpass/test-data/json/login.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
|
||||
|
||||
export const login: EnpassJsonFile = {
|
||||
folders: [],
|
||||
items: [
|
||||
{
|
||||
archived: 0,
|
||||
auto_submit: 1,
|
||||
category: "login",
|
||||
createdAt: 1666449561,
|
||||
favorite: 1,
|
||||
fields: [
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Username",
|
||||
order: 1,
|
||||
sensitive: 0,
|
||||
type: "username",
|
||||
uid: 10,
|
||||
updated_at: 1666449561,
|
||||
value: "",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "E-mail",
|
||||
order: 2,
|
||||
sensitive: 0,
|
||||
type: "email",
|
||||
uid: 12,
|
||||
updated_at: 1666449561,
|
||||
value: "emily@enpass.io",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Password",
|
||||
order: 3,
|
||||
sensitive: 1,
|
||||
type: "password",
|
||||
uid: 11,
|
||||
updated_at: 1666449561,
|
||||
value: "$&W:v@}4\\iRpUXVbjPdPKDGbD<xK>",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Website",
|
||||
order: 4,
|
||||
sensitive: 0,
|
||||
type: "url",
|
||||
uid: 13,
|
||||
updated_at: 1666449561,
|
||||
value: "https://www.amazon.com",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "ADDITIONAL DETAILS",
|
||||
order: 5,
|
||||
sensitive: 0,
|
||||
type: "section",
|
||||
uid: 101,
|
||||
updated_at: 1666449561,
|
||||
value: "",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Phone number",
|
||||
order: 6,
|
||||
sensitive: 0,
|
||||
type: "phone",
|
||||
uid: 14,
|
||||
updated_at: 1666449561,
|
||||
value: "12345678",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "TOTP",
|
||||
order: 7,
|
||||
sensitive: 0,
|
||||
type: "totp",
|
||||
uid: 102,
|
||||
updated_at: 1666449561,
|
||||
value: "TOTP_SEED_VALUE",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Security question",
|
||||
order: 8,
|
||||
sensitive: 0,
|
||||
type: "text",
|
||||
uid: 15,
|
||||
updated_at: 1666449561,
|
||||
value: "SECURITY_QUESTION",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
{
|
||||
deleted: 0,
|
||||
label: "Security answer",
|
||||
order: 9,
|
||||
sensitive: 1,
|
||||
type: "text",
|
||||
uid: 16,
|
||||
updated_at: 1666449561,
|
||||
value: "SECURITY_ANSWER",
|
||||
value_updated_at: 1666449561,
|
||||
},
|
||||
],
|
||||
icon: {
|
||||
fav: "www.amazon.com",
|
||||
image: {
|
||||
file: "web/amazon.com",
|
||||
},
|
||||
type: 1,
|
||||
uuid: "",
|
||||
},
|
||||
note: "some notes on the login item",
|
||||
subtitle: "emily@enpass.io",
|
||||
template_type: "login.default",
|
||||
title: "Amazon",
|
||||
trashed: 0,
|
||||
updated_at: 1666449561,
|
||||
uuid: "f717cb7c-6cce-4b24-b023-ec8a429cc992",
|
||||
},
|
||||
],
|
||||
};
|
||||
29
libs/common/spec/importers/enpass/test-data/json/note.ts
Normal file
29
libs/common/spec/importers/enpass/test-data/json/note.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { EnpassJsonFile } from "@bitwarden/common/importers/enpass/types/enpass-json-type";
|
||||
|
||||
export const note: EnpassJsonFile = {
|
||||
folders: [],
|
||||
items: [
|
||||
{
|
||||
archived: 0,
|
||||
auto_submit: 1,
|
||||
category: "note",
|
||||
createdAt: 1666554621,
|
||||
favorite: 0,
|
||||
icon: {
|
||||
fav: "",
|
||||
image: {
|
||||
file: "misc/secure_note",
|
||||
},
|
||||
type: 1,
|
||||
uuid: "",
|
||||
},
|
||||
note: "some secure note content",
|
||||
subtitle: "",
|
||||
template_type: "note.default",
|
||||
title: "some secure note title",
|
||||
trashed: 0,
|
||||
updated_at: 1666554621,
|
||||
uuid: "8b5ea2f6-f62b-4fec-a235-4a40946026b6",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { FSecureFskImporter as Importer } from "@bitwarden/common/importers/fsecure-fsk-importer";
|
||||
|
||||
const TestDataWithStyleSetToWebsite: string = JSON.stringify({
|
||||
data: {
|
||||
"8d58b5cf252dd06fbd98f5289e918ab1": {
|
||||
color: "#00baff",
|
||||
reatedDate: 1609302913,
|
||||
creditCvv: "",
|
||||
creditExpiry: "",
|
||||
creditNumber: "",
|
||||
favorite: 0,
|
||||
modifiedDate: 1609302913,
|
||||
notes: "note",
|
||||
password: "word",
|
||||
passwordList: [],
|
||||
passwordModifiedDate: 1609302913,
|
||||
rev: 1,
|
||||
service: "My first pass",
|
||||
style: "website",
|
||||
type: 1,
|
||||
url: "https://bitwarden.com",
|
||||
username: "pass",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const TestDataWithStyleSetToGlobe: string = JSON.stringify({
|
||||
data: {
|
||||
"8d58b5cf252dd06fbd98f5289e918ab1": {
|
||||
color: "#00baff",
|
||||
reatedDate: 1609302913,
|
||||
creditCvv: "",
|
||||
creditExpiry: "",
|
||||
creditNumber: "",
|
||||
favorite: 0,
|
||||
modifiedDate: 1609302913,
|
||||
notes: "note",
|
||||
password: "word",
|
||||
passwordList: [],
|
||||
passwordModifiedDate: 1609302913,
|
||||
rev: 1,
|
||||
service: "My first pass",
|
||||
style: "globe",
|
||||
type: 1,
|
||||
url: "https://bitwarden.com",
|
||||
username: "pass",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe("FSecure FSK Importer", () => {
|
||||
it("should parse data with style set to website", async () => {
|
||||
const importer = new Importer();
|
||||
const result = await importer.parse(TestDataWithStyleSetToWebsite);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.login.username).toEqual("pass");
|
||||
expect(cipher.login.password).toEqual("word");
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
const uriView = cipher.login.uris.shift();
|
||||
expect(uriView.uri).toEqual("https://bitwarden.com");
|
||||
});
|
||||
|
||||
it("should parse data with style set to globe", async () => {
|
||||
const importer = new Importer();
|
||||
const result = await importer.parse(TestDataWithStyleSetToGlobe);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.login.username).toEqual("pass");
|
||||
expect(cipher.login.password).toEqual("word");
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
const uriView = cipher.login.uris.shift();
|
||||
expect(uriView.uri).toEqual("https://bitwarden.com");
|
||||
});
|
||||
});
|
||||
533
libs/common/spec/importers/keepass2-xml-importer-testdata.ts
Normal file
533
libs/common/spec/importers/keepass2-xml-importer-testdata.ts
Normal file
@@ -0,0 +1,533 @@
|
||||
export const TestData = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<KeePassFile>
|
||||
<Meta>
|
||||
<Generator>KeePass</Generator>
|
||||
<DatabaseName />
|
||||
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
|
||||
<DatabaseDescription />
|
||||
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
|
||||
<DefaultUserName />
|
||||
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
|
||||
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
|
||||
<Color />
|
||||
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
|
||||
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
|
||||
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
|
||||
<MemoryProtection>
|
||||
<ProtectTitle>False</ProtectTitle>
|
||||
<ProtectUserName>False</ProtectUserName>
|
||||
<ProtectPassword>True</ProtectPassword>
|
||||
<ProtectURL>False</ProtectURL>
|
||||
<ProtectNotes>False</ProtectNotes>
|
||||
</MemoryProtection>
|
||||
<RecycleBinEnabled>True</RecycleBinEnabled>
|
||||
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
|
||||
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
|
||||
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
|
||||
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
|
||||
<HistoryMaxItems>10</HistoryMaxItems>
|
||||
<HistoryMaxSize>6291456</HistoryMaxSize>
|
||||
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
|
||||
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
|
||||
<Binaries />
|
||||
<CustomData />
|
||||
</Meta>
|
||||
<Root>
|
||||
<Group>
|
||||
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
|
||||
<Name>Root</Name>
|
||||
<Notes />
|
||||
<IconID>48</IconID>
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
|
||||
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>1</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
|
||||
</Times>
|
||||
<IsExpanded>True</IsExpanded>
|
||||
<DefaultAutoTypeSequence />
|
||||
<EnableAutoType>null</EnableAutoType>
|
||||
<EnableSearching>null</EnableSearching>
|
||||
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
|
||||
<Group>
|
||||
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
|
||||
<Name>Folder2</Name>
|
||||
<Notes>a note about the folder</Notes>
|
||||
<IconID>48</IconID>
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
|
||||
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>1</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
|
||||
</Times>
|
||||
<IsExpanded>True</IsExpanded>
|
||||
<DefaultAutoTypeSequence />
|
||||
<EnableAutoType>null</EnableAutoType>
|
||||
<EnableSearching>null</EnableSearching>
|
||||
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<IconID>1</IconID>
|
||||
<ForegroundColor />
|
||||
<BackgroundColor />
|
||||
<OverrideURL />
|
||||
<Tags />
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
|
||||
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>0</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
|
||||
</Times>
|
||||
<String>
|
||||
<Key>att2</Key>
|
||||
<Value>att2value</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>attr1</Key>
|
||||
<Value>att1value
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>This is a note!!!
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">googpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Google</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>google.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>googleuser</Value>
|
||||
</String>
|
||||
<AutoType>
|
||||
<Enabled>True</Enabled>
|
||||
<DataTransferObfuscation>0</DataTransferObfuscation>
|
||||
</AutoType>
|
||||
<History>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<IconID>0</IconID>
|
||||
<ForegroundColor />
|
||||
<BackgroundColor />
|
||||
<OverrideURL />
|
||||
<Tags />
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
|
||||
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>0</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
|
||||
</Times>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>This is a note!!!
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">googpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Google</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>google.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>googleuser</Value>
|
||||
</String>
|
||||
<AutoType>
|
||||
<Enabled>True</Enabled>
|
||||
<DataTransferObfuscation>0</DataTransferObfuscation>
|
||||
</AutoType>
|
||||
</Entry>
|
||||
</History>
|
||||
</Entry>
|
||||
</Group>
|
||||
</Group>
|
||||
<DeletedObjects />
|
||||
</Root>
|
||||
</KeePassFile>`;
|
||||
export const TestData1 = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<KeePassFile>
|
||||
<Meta>
|
||||
<Generator>KeePass</Generator>
|
||||
<DatabaseName />
|
||||
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
|
||||
<DatabaseDescription />
|
||||
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
|
||||
<DefaultUserName />
|
||||
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
|
||||
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
|
||||
<Color />
|
||||
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
|
||||
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
|
||||
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
|
||||
<MemoryProtection>
|
||||
<ProtectTitle>False</ProtectTitle>
|
||||
<ProtectUserName>False</ProtectUserName>
|
||||
<ProtectPassword>True</ProtectPassword>
|
||||
<ProtectURL>False</ProtectURL>
|
||||
<ProtectNotes>False</ProtectNotes>
|
||||
</MemoryProtection>
|
||||
<RecycleBinEnabled>True</RecycleBinEnabled>
|
||||
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
|
||||
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
|
||||
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
|
||||
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
|
||||
<HistoryMaxItems>10</HistoryMaxItems>
|
||||
<HistoryMaxSize>6291456</HistoryMaxSize>
|
||||
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
|
||||
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
|
||||
<Binaries />
|
||||
<CustomData />
|
||||
</Meta>
|
||||
<Group>
|
||||
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
|
||||
<Name>Root</Name>
|
||||
<Notes />
|
||||
<IconID>48</IconID>
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
|
||||
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>1</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
|
||||
</Times>
|
||||
<IsExpanded>True</IsExpanded>
|
||||
<DefaultAutoTypeSequence />
|
||||
<EnableAutoType>null</EnableAutoType>
|
||||
<EnableSearching>null</EnableSearching>
|
||||
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
|
||||
<Group>
|
||||
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
|
||||
<Name>Folder2</Name>
|
||||
<Notes>a note about the folder</Notes>
|
||||
<IconID>48</IconID>
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
|
||||
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>1</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
|
||||
</Times>
|
||||
<IsExpanded>True</IsExpanded>
|
||||
<DefaultAutoTypeSequence />
|
||||
<EnableAutoType>null</EnableAutoType>
|
||||
<EnableSearching>null</EnableSearching>
|
||||
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<IconID>1</IconID>
|
||||
<ForegroundColor />
|
||||
<BackgroundColor />
|
||||
<OverrideURL />
|
||||
<Tags />
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
|
||||
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>0</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
|
||||
</Times>
|
||||
<String>
|
||||
<Key>att2</Key>
|
||||
<Value>att2value</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>attr1</Key>
|
||||
<Value>att1value
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>This is a note!!!
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">googpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Google</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>google.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>googleuser</Value>
|
||||
</String>
|
||||
<AutoType>
|
||||
<Enabled>True</Enabled>
|
||||
<DataTransferObfuscation>0</DataTransferObfuscation>
|
||||
</AutoType>
|
||||
<History>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<IconID>0</IconID>
|
||||
<ForegroundColor />
|
||||
<BackgroundColor />
|
||||
<OverrideURL />
|
||||
<Tags />
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
|
||||
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>0</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
|
||||
</Times>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>This is a note!!!
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">googpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Google</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>google.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>googleuser</Value>
|
||||
</String>
|
||||
<AutoType>
|
||||
<Enabled>True</Enabled>
|
||||
<DataTransferObfuscation>0</DataTransferObfuscation>
|
||||
</AutoType>
|
||||
</Entry>
|
||||
</History>
|
||||
</Entry>
|
||||
</Group>
|
||||
</Group>
|
||||
<DeletedObjects />
|
||||
</KeePassFile>`;
|
||||
export const TestData2 = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<Meta>
|
||||
<Generator>KeePass</Generator>
|
||||
<DatabaseName />
|
||||
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
|
||||
<DatabaseDescription />
|
||||
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
|
||||
<DefaultUserName />
|
||||
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
|
||||
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
|
||||
<Color />
|
||||
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
|
||||
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
|
||||
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
|
||||
<MemoryProtection>
|
||||
<ProtectTitle>False</ProtectTitle>
|
||||
<ProtectUserName>False</ProtectUserName>
|
||||
<ProtectPassword>True</ProtectPassword>
|
||||
<ProtectURL>False</ProtectURL>
|
||||
<ProtectNotes>False</ProtectNotes>
|
||||
</MemoryProtection>
|
||||
<RecycleBinEnabled>True</RecycleBinEnabled>
|
||||
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
|
||||
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
|
||||
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
|
||||
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
|
||||
<HistoryMaxItems>10</HistoryMaxItems>
|
||||
<HistoryMaxSize>6291456</HistoryMaxSize>
|
||||
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
|
||||
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
|
||||
<Binaries />
|
||||
<CustomData />
|
||||
</Meta>
|
||||
<Root>
|
||||
<Group>
|
||||
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
|
||||
<Name>Root</Name>
|
||||
<Notes />
|
||||
<IconID>48</IconID>
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
|
||||
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>1</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
|
||||
</Times>
|
||||
<IsExpanded>True</IsExpanded>
|
||||
<DefaultAutoTypeSequence />
|
||||
<EnableAutoType>null</EnableAutoType>
|
||||
<EnableSearching>null</EnableSearching>
|
||||
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
|
||||
<Group>
|
||||
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
|
||||
<Name>Folder2</Name>
|
||||
<Notes>a note about the folder</Notes>
|
||||
<IconID>48</IconID>
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
|
||||
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>1</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
|
||||
</Times>
|
||||
<IsExpanded>True</IsExpanded>
|
||||
<DefaultAutoTypeSequence />
|
||||
<EnableAutoType>null</EnableAutoType>
|
||||
<EnableSearching>null</EnableSearching>
|
||||
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<IconID>1</IconID>
|
||||
<ForegroundColor />
|
||||
<BackgroundColor />
|
||||
<OverrideURL />
|
||||
<Tags />
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
|
||||
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>0</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
|
||||
</Times>
|
||||
<String>
|
||||
<Key>att2</Key>
|
||||
<Value>att2value</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>attr1</Key>
|
||||
<Value>att1value
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>This is a note!!!
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">googpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Google</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>google.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>googleuser</Value>
|
||||
</String>
|
||||
<AutoType>
|
||||
<Enabled>True</Enabled>
|
||||
<DataTransferObfuscation>0</DataTransferObfuscation>
|
||||
</AutoType>
|
||||
<History>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<IconID>0</IconID>
|
||||
<ForegroundColor />
|
||||
<BackgroundColor />
|
||||
<OverrideURL />
|
||||
<Tags />
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
|
||||
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>0</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
|
||||
</Times>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>This is a note!!!
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">googpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Google</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>google.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>googleuser</Value>
|
||||
</String>
|
||||
<AutoType>
|
||||
<Enabled>True</Enabled>
|
||||
<DataTransferObfuscation>0</DataTransferObfuscation>
|
||||
</AutoType>
|
||||
</Entry>
|
||||
</History>
|
||||
</Entry>
|
||||
</Group>
|
||||
</Group>
|
||||
<DeletedObjects />
|
||||
</Root>`;
|
||||
@@ -1,184 +1,7 @@
|
||||
import { KeePass2XmlImporter as Importer } from "@bitwarden/common/importers/keepass2-xml-importer";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folder.view";
|
||||
|
||||
const TestData = `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<KeePassFile>
|
||||
<Meta>
|
||||
<Generator>KeePass</Generator>
|
||||
<DatabaseName />
|
||||
<DatabaseNameChanged>2016-12-31T21:33:52Z</DatabaseNameChanged>
|
||||
<DatabaseDescription />
|
||||
<DatabaseDescriptionChanged>2016-12-31T21:33:52Z</DatabaseDescriptionChanged>
|
||||
<DefaultUserName />
|
||||
<DefaultUserNameChanged>2016-12-31T21:33:52Z</DefaultUserNameChanged>
|
||||
<MaintenanceHistoryDays>365</MaintenanceHistoryDays>
|
||||
<Color />
|
||||
<MasterKeyChanged>2016-12-31T21:33:59Z</MasterKeyChanged>
|
||||
<MasterKeyChangeRec>-1</MasterKeyChangeRec>
|
||||
<MasterKeyChangeForce>-1</MasterKeyChangeForce>
|
||||
<MemoryProtection>
|
||||
<ProtectTitle>False</ProtectTitle>
|
||||
<ProtectUserName>False</ProtectUserName>
|
||||
<ProtectPassword>True</ProtectPassword>
|
||||
<ProtectURL>False</ProtectURL>
|
||||
<ProtectNotes>False</ProtectNotes>
|
||||
</MemoryProtection>
|
||||
<RecycleBinEnabled>True</RecycleBinEnabled>
|
||||
<RecycleBinUUID>AAAAAAAAAAAAAAAAAAAAAA==</RecycleBinUUID>
|
||||
<RecycleBinChanged>2016-12-31T21:33:52Z</RecycleBinChanged>
|
||||
<EntryTemplatesGroup>AAAAAAAAAAAAAAAAAAAAAA==</EntryTemplatesGroup>
|
||||
<EntryTemplatesGroupChanged>2016-12-31T21:33:52Z</EntryTemplatesGroupChanged>
|
||||
<HistoryMaxItems>10</HistoryMaxItems>
|
||||
<HistoryMaxSize>6291456</HistoryMaxSize>
|
||||
<LastSelectedGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastSelectedGroup>
|
||||
<LastTopVisibleGroup>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleGroup>
|
||||
<Binaries />
|
||||
<CustomData />
|
||||
</Meta>
|
||||
<Root>
|
||||
<Group>
|
||||
<UUID>KvS57lVwl13AfGFLwkvq4Q==</UUID>
|
||||
<Name>Root</Name>
|
||||
<Notes />
|
||||
<IconID>48</IconID>
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:33:52Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:33:52Z</LastModificationTime>
|
||||
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:33:52Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>1</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:33:52Z</LocationChanged>
|
||||
</Times>
|
||||
<IsExpanded>True</IsExpanded>
|
||||
<DefaultAutoTypeSequence />
|
||||
<EnableAutoType>null</EnableAutoType>
|
||||
<EnableSearching>null</EnableSearching>
|
||||
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
|
||||
<Group>
|
||||
<UUID>P0ParXgGMBW6caOL2YrhqQ==</UUID>
|
||||
<Name>Folder2</Name>
|
||||
<Notes>a note about the folder</Notes>
|
||||
<IconID>48</IconID>
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:43:30Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:43:43Z</LastModificationTime>
|
||||
<LastAccessTime>2017-01-01T22:58:00Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:43:30Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>1</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:43:43Z</LocationChanged>
|
||||
</Times>
|
||||
<IsExpanded>True</IsExpanded>
|
||||
<DefaultAutoTypeSequence />
|
||||
<EnableAutoType>null</EnableAutoType>
|
||||
<EnableSearching>null</EnableSearching>
|
||||
<LastTopVisibleEntry>AAAAAAAAAAAAAAAAAAAAAA==</LastTopVisibleEntry>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<IconID>1</IconID>
|
||||
<ForegroundColor />
|
||||
<BackgroundColor />
|
||||
<OverrideURL />
|
||||
<Tags />
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:40:23Z</LastModificationTime>
|
||||
<LastAccessTime>2016-12-31T21:40:23Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>0</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:43:48Z</LocationChanged>
|
||||
</Times>
|
||||
<String>
|
||||
<Key>att2</Key>
|
||||
<Value>att2value</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>attr1</Key>
|
||||
<Value>att1value
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>This is a note!!!
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">googpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Google</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>google.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>googleuser</Value>
|
||||
</String>
|
||||
<AutoType>
|
||||
<Enabled>True</Enabled>
|
||||
<DataTransferObfuscation>0</DataTransferObfuscation>
|
||||
</AutoType>
|
||||
<History>
|
||||
<Entry>
|
||||
<UUID>fAa543oYlgnJKkhKag5HLw==</UUID>
|
||||
<IconID>0</IconID>
|
||||
<ForegroundColor />
|
||||
<BackgroundColor />
|
||||
<OverrideURL />
|
||||
<Tags />
|
||||
<Times>
|
||||
<CreationTime>2016-12-31T21:34:13Z</CreationTime>
|
||||
<LastModificationTime>2016-12-31T21:34:40Z</LastModificationTime>
|
||||
<LastAccessTime>2016-12-31T21:34:40Z</LastAccessTime>
|
||||
<ExpiryTime>2016-12-31T21:34:13Z</ExpiryTime>
|
||||
<Expires>False</Expires>
|
||||
<UsageCount>0</UsageCount>
|
||||
<LocationChanged>2016-12-31T21:34:40Z</LocationChanged>
|
||||
</Times>
|
||||
<String>
|
||||
<Key>Notes</Key>
|
||||
<Value>This is a note!!!
|
||||
|
||||
line1
|
||||
line2</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Password</Key>
|
||||
<Value ProtectInMemory="True">googpass</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>Title</Key>
|
||||
<Value>Google</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>URL</Key>
|
||||
<Value>google.com</Value>
|
||||
</String>
|
||||
<String>
|
||||
<Key>UserName</Key>
|
||||
<Value>googleuser</Value>
|
||||
</String>
|
||||
<AutoType>
|
||||
<Enabled>True</Enabled>
|
||||
<DataTransferObfuscation>0</DataTransferObfuscation>
|
||||
</AutoType>
|
||||
</Entry>
|
||||
</History>
|
||||
</Entry>
|
||||
</Group>
|
||||
</Group>
|
||||
<DeletedObjects />
|
||||
</Root>
|
||||
</KeePassFile>`;
|
||||
import { TestData, TestData1, TestData2 } from "./keepass2-xml-importer-testdata";
|
||||
|
||||
describe("KeePass2 Xml Importer", () => {
|
||||
it("should parse XML data", async () => {
|
||||
@@ -186,4 +9,34 @@ describe("KeePass2 Xml Importer", () => {
|
||||
const result = await importer.parse(TestData);
|
||||
expect(result != null).toBe(true);
|
||||
});
|
||||
|
||||
it("parse XML should contains folders", async () => {
|
||||
const importer = new Importer();
|
||||
const folder = new FolderView();
|
||||
folder.name = "Folder2";
|
||||
const actual = [folder];
|
||||
|
||||
const result = await importer.parse(TestData);
|
||||
expect(result.folders).toEqual(actual);
|
||||
});
|
||||
|
||||
it("parse XML should contains login details", async () => {
|
||||
const importer = new Importer();
|
||||
const result = await importer.parse(TestData);
|
||||
expect(result.ciphers[0].login.uri != null).toBe(true);
|
||||
expect(result.ciphers[0].login.username != null).toBe(true);
|
||||
expect(result.ciphers[0].login.password != null).toBe(true);
|
||||
});
|
||||
|
||||
it("should return error with missing root tag", async () => {
|
||||
const importer = new Importer();
|
||||
const result = await importer.parse(TestData1);
|
||||
expect(result.errorMessage).toBe("Missing `KeePassFile > Root` node.");
|
||||
});
|
||||
|
||||
it("should return error with missing KeePassFile tag", async () => {
|
||||
const importer = new Importer();
|
||||
const result = await importer.parse(TestData2);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
77
libs/common/spec/importers/keeper-csv-importer.spec.ts
Normal file
77
libs/common/spec/importers/keeper-csv-importer.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { KeeperCsvImporter as Importer } from "@bitwarden/common/importers/keeper/keeper-csv-importer";
|
||||
|
||||
import { testData as TestData } from "./test-data/keeper-csv/testdata.csv";
|
||||
|
||||
describe("Keeper CSV Importer", () => {
|
||||
let importer: Importer;
|
||||
beforeEach(() => {
|
||||
importer = new Importer();
|
||||
});
|
||||
|
||||
it("should parse login data", async () => {
|
||||
const result = await importer.parse(TestData);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.name).toEqual("Bar");
|
||||
expect(cipher.login.username).toEqual("john.doe@example.com");
|
||||
expect(cipher.login.password).toEqual("1234567890abcdef");
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
const uriView = cipher.login.uris.shift();
|
||||
expect(uriView.uri).toEqual("https://example.com/");
|
||||
expect(cipher.notes).toEqual("These are some notes.");
|
||||
|
||||
const cipher2 = result.ciphers.shift();
|
||||
expect(cipher2.name).toEqual("Bar 1");
|
||||
expect(cipher2.login.username).toEqual("john.doe1@example.com");
|
||||
expect(cipher2.login.password).toEqual("234567890abcdef1");
|
||||
expect(cipher2.login.uris.length).toEqual(1);
|
||||
const uriView2 = cipher2.login.uris.shift();
|
||||
expect(uriView2.uri).toEqual("https://an.example.com/");
|
||||
expect(cipher2.notes).toBeNull();
|
||||
|
||||
const cipher3 = result.ciphers.shift();
|
||||
expect(cipher3.name).toEqual("Bar 2");
|
||||
expect(cipher3.login.username).toEqual("john.doe2@example.com");
|
||||
expect(cipher3.login.password).toEqual("34567890abcdef12");
|
||||
expect(cipher3.notes).toBeNull();
|
||||
expect(cipher3.login.uris.length).toEqual(1);
|
||||
const uriView3 = cipher3.login.uris.shift();
|
||||
expect(uriView3.uri).toEqual("https://another.example.com/");
|
||||
});
|
||||
|
||||
it("should import TOTP when present", async () => {
|
||||
const result = await importer.parse(TestData);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.login.totp).toBeNull();
|
||||
|
||||
const cipher2 = result.ciphers.shift();
|
||||
expect(cipher2.login.totp).toBeNull();
|
||||
|
||||
const cipher3 = result.ciphers.shift();
|
||||
expect(cipher3.login.totp).toEqual(
|
||||
"otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30"
|
||||
);
|
||||
});
|
||||
|
||||
it("should parse custom fields", async () => {
|
||||
const result = await importer.parse(TestData);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.fields).toBeNull();
|
||||
|
||||
const cipher2 = result.ciphers.shift();
|
||||
expect(cipher2.fields.length).toBe(2);
|
||||
expect(cipher2.fields[0].name).toEqual("Account ID");
|
||||
expect(cipher2.fields[0].value).toEqual("12345");
|
||||
expect(cipher2.fields[1].name).toEqual("Org ID");
|
||||
expect(cipher2.fields[1].value).toEqual("54321");
|
||||
|
||||
const cipher3 = result.ciphers.shift();
|
||||
expect(cipher3.fields[0].name).toEqual("Account ID");
|
||||
expect(cipher3.fields[0].value).toEqual("23456");
|
||||
});
|
||||
});
|
||||
34
libs/common/spec/importers/passky-json-importer.spec.ts
Normal file
34
libs/common/spec/importers/passky-json-importer.spec.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PasskyJsonImporter as Importer } from "@bitwarden/common/importers/passky/passky-json-importer";
|
||||
|
||||
import { testData as EncryptedData } from "./test-data/passky-json/passky-encrypted.json";
|
||||
import { testData as UnencryptedData } from "./test-data/passky-json/passky-unencrypted.json";
|
||||
|
||||
describe("Passky Json Importer", () => {
|
||||
let importer: Importer;
|
||||
beforeEach(() => {
|
||||
importer = new Importer();
|
||||
});
|
||||
|
||||
it("should not import encrypted backups", async () => {
|
||||
const testDataJson = JSON.stringify(EncryptedData);
|
||||
const result = await importer.parse(testDataJson);
|
||||
expect(result != null).toBe(true);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.errorMessage).toBe("Unable to import an encrypted passky backup.");
|
||||
});
|
||||
|
||||
it("should parse login data", async () => {
|
||||
const testDataJson = JSON.stringify(UnencryptedData);
|
||||
const result = await importer.parse(testDataJson);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
expect(cipher.name).toEqual("https://bitwarden.com/");
|
||||
expect(cipher.login.username).toEqual("testUser");
|
||||
expect(cipher.login.password).toEqual("testPassword");
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
const uriView = cipher.login.uris.shift();
|
||||
expect(uriView.uri).toEqual("https://bitwarden.com/");
|
||||
expect(cipher.notes).toEqual("my notes");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export const testData = `"Foo","Bar","john.doe@example.com","1234567890abcdef","https://example.com/","These are some notes.",""
|
||||
"Foo","Bar 1","john.doe1@example.com","234567890abcdef1","https://an.example.com/","","","Account ID","12345","Org ID","54321"
|
||||
"Foo\\Baz","Bar 2","john.doe2@example.com","34567890abcdef12","https://another.example.com/","","","Account ID","23456","TFC:Keeper","otpauth://totp/Amazon:me@company.com?secret=JBSWY3DPEHPK3PXP&issuer=Amazon&algorithm=SHA1&digits=6&period=30"
|
||||
`;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
|
||||
|
||||
export const testData: PasskyJsonExport = {
|
||||
encrypted: true,
|
||||
passwords: [
|
||||
{
|
||||
website:
|
||||
"w68uw6nCjUI3w7MNYsK7w6xqwqHDlXLCpsOEw4/Dq8KbIMK3w6fCvQJFFcOECsOlwprCqUAawqnDvsKbwrLCsCXCtcOlw4dp",
|
||||
username: "bMKyUC0VPTx5woHCr8K9wpvDgGrClFAKw6VfJTgob8KVwqNoN8KIEA==",
|
||||
password: "XcKxO2FjwqIJPkoHwqrDvcKtXcORw6TDlMOlw7TDvMORfmlNdMKOwq7DocO+",
|
||||
message:
|
||||
"w5jCrWTCgAV1RcO+DsOzw5zCvD5CwqLCtcKtw6sPwpbCmcOxwrfDlcOQw4h1wqomEhNtUkRgwrzCkxrClFBSHsO5wrfCrg==",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PasskyJsonExport } from "@bitwarden/common/importers/passky/passky-json-types";
|
||||
|
||||
export const testData: PasskyJsonExport = {
|
||||
encrypted: false,
|
||||
passwords: [
|
||||
{
|
||||
website: "https://bitwarden.com/",
|
||||
username: "testUser",
|
||||
password: "testPassword",
|
||||
message: "my notes",
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,5 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
@@ -67,33 +66,34 @@ export function identityTokenResponseFactory() {
|
||||
}
|
||||
|
||||
describe("LogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
apiService = mock<ApiService>();
|
||||
tokenService = mock<TokenService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
authService = mock<AuthService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.calledWith(accessToken).mockResolvedValue(decodedToken);
|
||||
|
||||
// The base class is abstract so we test it via PasswordLogInStrategy
|
||||
passwordLogInStrategy = new PasswordLogInStrategy(
|
||||
@@ -113,12 +113,11 @@ describe("LogInStrategy", () => {
|
||||
|
||||
describe("base class", () => {
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
tokenService.decodeToken(accessToken).resolves(decodedToken);
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.received(1).addAccount(
|
||||
expect(stateService.addAccount).toHaveBeenCalledWith(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
@@ -140,10 +139,9 @@ describe("LogInStrategy", () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
cryptoService.received(1).setEncKey(encKey);
|
||||
cryptoService.received(1).setEncPrivateKey(privateKey);
|
||||
|
||||
messagingService.received(1).send("loggedIn");
|
||||
expect(cryptoService.setEncKey).toHaveBeenCalledWith(encKey);
|
||||
expect(cryptoService.setEncPrivateKey).toHaveBeenCalledWith(privateKey);
|
||||
expect(messagingService.send).toHaveBeenCalledWith("loggedIn");
|
||||
});
|
||||
|
||||
it("builds AuthResult", async () => {
|
||||
@@ -151,16 +149,16 @@ describe("LogInStrategy", () => {
|
||||
tokenResponse.forcePasswordReset = true;
|
||||
tokenResponse.resetMasterPassword = true;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.forcePasswordReset = true;
|
||||
expected.resetMasterPassword = true;
|
||||
expected.twoFactorProviders = null;
|
||||
expected.captchaSiteKey = "";
|
||||
expect(result).toEqual(expected);
|
||||
expect(result).toEqual({
|
||||
forcePasswordReset: true,
|
||||
resetMasterPassword: true,
|
||||
twoFactorProviders: null,
|
||||
captchaSiteKey: "",
|
||||
} as AuthResult);
|
||||
});
|
||||
|
||||
it("rejects login if CAPTCHA is required", async () => {
|
||||
@@ -171,12 +169,12 @@ describe("LogInStrategy", () => {
|
||||
HCaptcha_SiteKey: captchaSiteKey,
|
||||
});
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.didNotReceive().addAccount(Arg.any());
|
||||
messagingService.didNotReceive().send(Arg.any());
|
||||
expect(stateService.addAccount).not.toHaveBeenCalled();
|
||||
expect(messagingService.send).not.toHaveBeenCalled();
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.captchaSiteKey = captchaSiteKey;
|
||||
@@ -186,13 +184,20 @@ describe("LogInStrategy", () => {
|
||||
it("makes a new public and private key for an old account", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.privateKey = null;
|
||||
cryptoService.makeKeyPair(Arg.any()).resolves(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
cryptoService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postAccountKeys(Arg.any());
|
||||
// User key must be set before the new RSA keypair is generated, otherwise we can't decrypt the EncKey
|
||||
expect(cryptoService.setKey).toHaveBeenCalled();
|
||||
expect(cryptoService.makeKeyPair).toHaveBeenCalled();
|
||||
expect(cryptoService.setKey.mock.invocationCallOrder[0]).toBeLessThan(
|
||||
cryptoService.makeKeyPair.mock.invocationCallOrder[0]
|
||||
);
|
||||
|
||||
expect(apiService.postAccountKeys).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -206,12 +211,12 @@ describe("LogInStrategy", () => {
|
||||
error_description: "Two factor required.",
|
||||
});
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.didNotReceive().addAccount(Arg.any());
|
||||
messagingService.didNotReceive().send(Arg.any());
|
||||
expect(stateService.addAccount).not.toHaveBeenCalled();
|
||||
expect(messagingService.send).not.toHaveBeenCalled();
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
|
||||
@@ -220,26 +225,25 @@ describe("LogInStrategy", () => {
|
||||
});
|
||||
|
||||
it("sends stored 2FA token to server", async () => {
|
||||
tokenService.getTwoFactorToken().resolves(twoFactorToken);
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(twoFactorToken);
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === false
|
||||
);
|
||||
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
twoFactor: {
|
||||
provider: TwoFactorProviderType.Remember,
|
||||
token: twoFactorToken,
|
||||
remember: false,
|
||||
} as TokenTwoFactorRequest,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sends 2FA token provided by user to server (single step)", async () => {
|
||||
// This occurs if the user enters the 2FA code as an argument in the CLI
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
credentials.twoFactor = new TokenTwoFactorRequest(
|
||||
twoFactorProviderType,
|
||||
twoFactorToken,
|
||||
@@ -248,14 +252,13 @@ describe("LogInStrategy", () => {
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === twoFactorRemember
|
||||
);
|
||||
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
twoFactor: {
|
||||
provider: twoFactorProviderType,
|
||||
token: twoFactorToken,
|
||||
remember: twoFactorRemember,
|
||||
} as TokenTwoFactorRequest,
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -269,21 +272,20 @@ describe("LogInStrategy", () => {
|
||||
null
|
||||
);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
|
||||
await passwordLogInStrategy.logInTwoFactor(
|
||||
new TokenTwoFactorRequest(twoFactorProviderType, twoFactorToken, twoFactorRemember),
|
||||
null
|
||||
);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === twoFactorRemember
|
||||
);
|
||||
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
twoFactor: {
|
||||
provider: twoFactorProviderType,
|
||||
token: twoFactorToken,
|
||||
remember: twoFactorRemember,
|
||||
} as TokenTwoFactorRequest,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
@@ -31,41 +30,43 @@ const preloginKey = new SymmetricCryptoKey(
|
||||
const deviceId = Utils.newGuid();
|
||||
|
||||
describe("PasswordLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let authService: MockProxy<AuthService>;
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
apiService = mock<ApiService>();
|
||||
tokenService = mock<TokenService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
authService = mock<AuthService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey);
|
||||
authService.makePreloginKey.mockResolvedValue(preloginKey);
|
||||
|
||||
cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword);
|
||||
cryptoService
|
||||
.hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization)
|
||||
.resolves(localHashedPassword);
|
||||
cryptoService.hashPassword
|
||||
.calledWith(masterPassword, expect.anything(), undefined)
|
||||
.mockResolvedValue(hashedPassword);
|
||||
cryptoService.hashPassword
|
||||
.calledWith(masterPassword, expect.anything(), HashPurpose.LocalAuthorization)
|
||||
.mockResolvedValue(localHashedPassword);
|
||||
|
||||
passwordLogInStrategy = new PasswordLogInStrategy(
|
||||
cryptoService,
|
||||
@@ -81,23 +82,24 @@ describe("PasswordLogInStrategy", () => {
|
||||
);
|
||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
});
|
||||
|
||||
it("sends master password credentials to the server", async () => {
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any; // Need to access private fields
|
||||
return (
|
||||
passwordTokenRequest.email === email &&
|
||||
passwordTokenRequest.masterPasswordHash === hashedPassword &&
|
||||
passwordTokenRequest.device.identifier === deviceId &&
|
||||
passwordTokenRequest.twoFactor.provider == null &&
|
||||
passwordTokenRequest.twoFactor.token == null &&
|
||||
passwordTokenRequest.captchaResponse == null
|
||||
);
|
||||
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
email: email,
|
||||
masterPasswordHash: hashedPassword,
|
||||
device: expect.objectContaining({
|
||||
identifier: deviceId,
|
||||
}),
|
||||
twoFactor: expect.objectContaining({
|
||||
provider: null,
|
||||
token: null,
|
||||
}),
|
||||
captchaResponse: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -105,7 +107,7 @@ describe("PasswordLogInStrategy", () => {
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
cryptoService.received(1).setKey(preloginKey);
|
||||
cryptoService.received(1).setKeyHash(localHashedPassword);
|
||||
expect(cryptoService.setKey).toHaveBeenCalledWith(preloginKey);
|
||||
expect(cryptoService.setKeyHash).toHaveBeenCalledWith(localHashedPassword);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
@@ -18,23 +17,21 @@ import { SsoLogInCredentials } from "@bitwarden/common/models/domain/log-in-cred
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
describe("SsoLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||
|
||||
let ssoLogInStrategy: SsoLogInStrategy;
|
||||
let credentials: SsoLogInCredentials;
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
const encKey = "ENC_KEY";
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const keyConnectorUrl = "KEY_CONNECTOR_URL";
|
||||
|
||||
const ssoCode = "SSO_CODE";
|
||||
@@ -43,19 +40,20 @@ describe("SsoLogInStrategy", () => {
|
||||
const ssoOrgId = "SSO_ORG_ID";
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
apiService = mock<ApiService>();
|
||||
tokenService = mock<TokenService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
keyConnectorService = mock<KeyConnectorService>();
|
||||
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
ssoLogInStrategy = new SsoLogInStrategy(
|
||||
cryptoService,
|
||||
@@ -73,21 +71,22 @@ describe("SsoLogInStrategy", () => {
|
||||
});
|
||||
|
||||
it("sends SSO information to server", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const ssoTokenRequest = actual as any;
|
||||
return (
|
||||
ssoTokenRequest.code === ssoCode &&
|
||||
ssoTokenRequest.codeVerifier === ssoCodeVerifier &&
|
||||
ssoTokenRequest.redirectUri === ssoRedirectUrl &&
|
||||
ssoTokenRequest.device.identifier === deviceId &&
|
||||
ssoTokenRequest.twoFactor.provider == null &&
|
||||
ssoTokenRequest.twoFactor.token == null
|
||||
);
|
||||
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
code: ssoCode,
|
||||
codeVerifier: ssoCodeVerifier,
|
||||
redirectUri: ssoRedirectUrl,
|
||||
device: expect.objectContaining({
|
||||
identifier: deviceId,
|
||||
}),
|
||||
twoFactor: expect.objectContaining({
|
||||
provider: null,
|
||||
token: null,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
@@ -95,23 +94,23 @@ describe("SsoLogInStrategy", () => {
|
||||
it("does not set keys for new SSO user flow", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.key = null;
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
cryptoService.didNotReceive().setEncPrivateKey(privateKey);
|
||||
cryptoService.didNotReceive().setEncKey(encKey);
|
||||
expect(cryptoService.setEncPrivateKey).not.toHaveBeenCalled();
|
||||
expect(cryptoService.setEncKey).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gets and sets KeyConnector key for enrolled user", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
|
||||
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
});
|
||||
|
||||
it("converts new SSO user to Key Connector on first login", async () => {
|
||||
@@ -119,10 +118,13 @@ describe("SsoLogInStrategy", () => {
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
tokenResponse.key = null;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).convertNewSsoUserToKeyConnector(tokenResponse, ssoOrgId);
|
||||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||
tokenResponse,
|
||||
ssoOrgId
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||
@@ -19,17 +18,17 @@ import { UserApiLogInCredentials } from "@bitwarden/common/models/domain/log-in-
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
describe("UserApiLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let environmentService: SubstituteOf<EnvironmentService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let tokenService: MockProxy<TokenService>;
|
||||
let appIdService: MockProxy<AppIdService>;
|
||||
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||
let messagingService: MockProxy<MessagingService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let twoFactorService: MockProxy<TwoFactorService>;
|
||||
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||
let environmentService: MockProxy<EnvironmentService>;
|
||||
|
||||
let apiLogInStrategy: UserApiLogInStrategy;
|
||||
let credentials: UserApiLogInCredentials;
|
||||
@@ -40,20 +39,21 @@ describe("UserApiLogInStrategy", () => {
|
||||
const apiClientSecret = "API_CLIENT_SECRET";
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
environmentService = Substitute.for<EnvironmentService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
cryptoService = mock<CryptoService>();
|
||||
apiService = mock<ApiService>();
|
||||
tokenService = mock<TokenService>();
|
||||
appIdService = mock<AppIdService>();
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
messagingService = mock<MessagingService>();
|
||||
logService = mock<LogService>();
|
||||
stateService = mock<StateService>();
|
||||
twoFactorService = mock<TwoFactorService>();
|
||||
keyConnectorService = mock<KeyConnectorService>();
|
||||
environmentService = mock<EnvironmentService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
appIdService.getAppId.mockResolvedValue(deviceId);
|
||||
tokenService.getTwoFactorToken.mockResolvedValue(null);
|
||||
tokenService.decodeToken.mockResolvedValue({});
|
||||
|
||||
apiLogInStrategy = new UserApiLogInStrategy(
|
||||
cryptoService,
|
||||
@@ -73,43 +73,43 @@ describe("UserApiLogInStrategy", () => {
|
||||
});
|
||||
|
||||
it("sends api key credentials to the server", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const apiTokenRequest = actual as any;
|
||||
return (
|
||||
apiTokenRequest.clientId === apiClientId &&
|
||||
apiTokenRequest.clientSecret === apiClientSecret &&
|
||||
apiTokenRequest.device.identifier === deviceId &&
|
||||
apiTokenRequest.twoFactor.provider == null &&
|
||||
apiTokenRequest.twoFactor.token == null &&
|
||||
apiTokenRequest.captchaResponse == null
|
||||
);
|
||||
expect(apiService.postIdentityToken).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
clientId: apiClientId,
|
||||
clientSecret: apiClientSecret,
|
||||
device: expect.objectContaining({
|
||||
identifier: deviceId,
|
||||
}),
|
||||
twoFactor: expect.objectContaining({
|
||||
provider: null,
|
||||
token: null,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.received(1).setApiKeyClientId(apiClientId);
|
||||
stateService.received(1).setApiKeyClientSecret(apiClientSecret);
|
||||
stateService.received(1).addAccount(Arg.any());
|
||||
expect(stateService.setApiKeyClientId).toHaveBeenCalledWith(apiClientId);
|
||||
expect(stateService.setApiKeyClientSecret).toHaveBeenCalledWith(apiClientSecret);
|
||||
expect(stateService.addAccount).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gets and sets the Key Connector key from environmentUrl", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
environmentService.getKeyConnectorUrl.mockReturnValue(keyConnectorUrl);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
|
||||
expect(keyConnectorService.getAndSetKey).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -241,4 +241,89 @@ describe("Utils Service", () => {
|
||||
expect(Utils.fromByteStringToArray(null)).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mapToRecord", () => {
|
||||
it("should handle null", () => {
|
||||
expect(Utils.mapToRecord(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should handle empty map", () => {
|
||||
expect(Utils.mapToRecord(new Map())).toEqual({});
|
||||
});
|
||||
|
||||
it("should handle convert a Map to a Record", () => {
|
||||
const map = new Map([
|
||||
["key1", "value1"],
|
||||
["key2", "value2"],
|
||||
]);
|
||||
expect(Utils.mapToRecord(map)).toEqual({ key1: "value1", key2: "value2" });
|
||||
});
|
||||
|
||||
it("should handle convert a Map to a Record with non-string keys", () => {
|
||||
const map = new Map([
|
||||
[1, "value1"],
|
||||
[2, "value2"],
|
||||
]);
|
||||
const result = Utils.mapToRecord(map);
|
||||
expect(result).toEqual({ 1: "value1", 2: "value2" });
|
||||
expect(Utils.recordToMap(result)).toEqual(map);
|
||||
});
|
||||
|
||||
it("should not convert an object if it's not a map", () => {
|
||||
const obj = { key1: "value1", key2: "value2" };
|
||||
expect(Utils.mapToRecord(obj as any)).toEqual(obj);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recordToMap", () => {
|
||||
it("should handle null", () => {
|
||||
expect(Utils.recordToMap(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should handle empty record", () => {
|
||||
expect(Utils.recordToMap({})).toEqual(new Map());
|
||||
});
|
||||
|
||||
it("should handle convert a Record to a Map", () => {
|
||||
const record = { key1: "value1", key2: "value2" };
|
||||
expect(Utils.recordToMap(record)).toEqual(new Map(Object.entries(record)));
|
||||
});
|
||||
|
||||
it("should handle convert a Record to a Map with non-string keys", () => {
|
||||
const record = { 1: "value1", 2: "value2" };
|
||||
const result = Utils.recordToMap(record);
|
||||
expect(result).toEqual(
|
||||
new Map([
|
||||
[1, "value1"],
|
||||
[2, "value2"],
|
||||
])
|
||||
);
|
||||
expect(Utils.mapToRecord(result)).toEqual(record);
|
||||
});
|
||||
|
||||
it("should not convert an object if already a map", () => {
|
||||
const map = new Map([
|
||||
["key1", "value1"],
|
||||
["key2", "value2"],
|
||||
]);
|
||||
expect(Utils.recordToMap(map as any)).toEqual(map);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeRFC3986URIComponent", () => {
|
||||
it("returns input string with expected encoded chars", () => {
|
||||
expect(Utils.encodeRFC3986URIComponent("test'user@example.com")).toBe(
|
||||
"test%27user%40example.com"
|
||||
);
|
||||
expect(Utils.encodeRFC3986URIComponent("(test)user@example.com")).toBe(
|
||||
"%28test%29user%40example.com"
|
||||
);
|
||||
expect(Utils.encodeRFC3986URIComponent("testuser!@example.com")).toBe(
|
||||
"testuser%21%40example.com"
|
||||
);
|
||||
expect(Utils.encodeRFC3986URIComponent("Test*User@example.com")).toBe(
|
||||
"Test%2AUser%40example.com"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { CipherService } from "@bitwarden/common/abstractions/cipher.service";
|
||||
@@ -8,13 +7,15 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||
import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/enums/cipherType";
|
||||
import { KdfType } from "@bitwarden/common/enums/kdfType";
|
||||
import { KdfType, DEFAULT_KDF_ITERATIONS } from "@bitwarden/common/enums/kdfType";
|
||||
import { Utils } from "@bitwarden/common/misc/utils";
|
||||
import { Cipher } from "@bitwarden/common/models/domain/cipher";
|
||||
import { EncString } from "@bitwarden/common/models/domain/enc-string";
|
||||
import { Folder } from "@bitwarden/common/models/domain/folder";
|
||||
import { Login } from "@bitwarden/common/models/domain/login";
|
||||
import { CipherWithIdExport as CipherExport } from "@bitwarden/common/models/export/cipher-with-ids.export";
|
||||
import { CipherView } from "@bitwarden/common/models/view/cipher.view";
|
||||
import { FolderView } from "@bitwarden/common/models/view/folder.view";
|
||||
import { LoginView } from "@bitwarden/common/models/view/login.view";
|
||||
import { ExportService } from "@bitwarden/common/services/export.service";
|
||||
|
||||
@@ -32,6 +33,10 @@ const UserCipherDomains = [
|
||||
generateCipherDomain(true),
|
||||
];
|
||||
|
||||
const UserFolderViews = [generateFolderView(), generateFolderView()];
|
||||
|
||||
const UserFolders = [generateFolder(), generateFolder()];
|
||||
|
||||
function generateCipherView(deleted: boolean) {
|
||||
return BuildTestObject(
|
||||
{
|
||||
@@ -72,6 +77,26 @@ function generateCipherDomain(deleted: boolean) {
|
||||
);
|
||||
}
|
||||
|
||||
function generateFolderView() {
|
||||
return BuildTestObject(
|
||||
{
|
||||
id: GetUniqueString("id"),
|
||||
name: GetUniqueString("name"),
|
||||
revisionDate: new Date(),
|
||||
},
|
||||
FolderView
|
||||
);
|
||||
}
|
||||
|
||||
function generateFolder() {
|
||||
const actual = Folder.fromJSON({
|
||||
revisionDate: new Date("2022-08-04T01:06:40.441Z").toISOString(),
|
||||
name: "name",
|
||||
id: "id",
|
||||
});
|
||||
return actual;
|
||||
}
|
||||
|
||||
function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string) {
|
||||
const actual = JSON.stringify(JSON.parse(jsonResult).items);
|
||||
const items: CipherExport[] = [];
|
||||
@@ -84,6 +109,34 @@ function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string
|
||||
expect(actual).toEqual(JSON.stringify(items));
|
||||
}
|
||||
|
||||
function expectEqualFolderViews(folderviews: FolderView[] | Folder[], jsonResult: string) {
|
||||
const actual = JSON.stringify(JSON.parse(jsonResult).folders);
|
||||
const folders: FolderResponse[] = [];
|
||||
folderviews.forEach((c) => {
|
||||
const folder = new FolderResponse();
|
||||
folder.id = c.id;
|
||||
folder.name = c.name.toString();
|
||||
folders.push(folder);
|
||||
});
|
||||
|
||||
expect(actual.length).toBeGreaterThan(0);
|
||||
expect(actual).toEqual(JSON.stringify(folders));
|
||||
}
|
||||
|
||||
function expectEqualFolders(folders: Folder[], jsonResult: string) {
|
||||
const actual = JSON.stringify(JSON.parse(jsonResult).folders);
|
||||
const items: Folder[] = [];
|
||||
folders.forEach((c) => {
|
||||
const item = new Folder();
|
||||
item.id = c.id;
|
||||
item.name = c.name;
|
||||
items.push(item);
|
||||
});
|
||||
|
||||
expect(actual.length).toBeGreaterThan(0);
|
||||
expect(actual).toEqual(JSON.stringify(items));
|
||||
}
|
||||
|
||||
describe("ExportService", () => {
|
||||
let exportService: ExportService;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
@@ -99,8 +152,8 @@ describe("ExportService", () => {
|
||||
folderService = Substitute.for<FolderService>();
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
|
||||
folderService.folderViews$.returns(new BehaviorSubject([]));
|
||||
folderService.folders$.returns(new BehaviorSubject([]));
|
||||
folderService.getAllDecryptedFromState().resolves(UserFolderViews);
|
||||
folderService.getAllFromState().resolves(UserFolders);
|
||||
|
||||
exportService = new ExportService(
|
||||
folderService,
|
||||
@@ -179,7 +232,7 @@ describe("ExportService", () => {
|
||||
});
|
||||
|
||||
it("specifies kdfIterations", () => {
|
||||
expect(exportObject.kdfIterations).toEqual(100000);
|
||||
expect(exportObject.kdfIterations).toEqual(DEFAULT_KDF_ITERATIONS);
|
||||
});
|
||||
|
||||
it("has kdfType", () => {
|
||||
@@ -208,4 +261,25 @@ describe("ExportService", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("exported unencrypted object contains folders", async () => {
|
||||
cipherService.getAllDecrypted().resolves(UserCipherViews.slice(0, 1));
|
||||
await folderService.getAllDecryptedFromState();
|
||||
const actual = await exportService.getExport("json");
|
||||
|
||||
expectEqualFolderViews(UserFolderViews, actual);
|
||||
});
|
||||
|
||||
it("exported encrypted json contains folders", async () => {
|
||||
cipherService.getAll().resolves(UserCipherDomains.slice(0, 1));
|
||||
await folderService.getAllFromState();
|
||||
const actual = await exportService.getExport("encrypted_json");
|
||||
|
||||
expectEqualFolders(UserFolders, actual);
|
||||
});
|
||||
});
|
||||
|
||||
export class FolderResponse {
|
||||
id: string = null;
|
||||
name: string = null;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { MockProxy, mock, any, mockClear, matches } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom, Subject } from "rxjs";
|
||||
import { MockProxy, mock, any, mockClear } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||
import { SyncNotifierService } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction";
|
||||
import { OrganizationData } from "@bitwarden/common/models/data/organization.data";
|
||||
import { SyncResponse } from "@bitwarden/common/models/response/sync.response";
|
||||
import { OrganizationService } from "@bitwarden/common/services/organization/organization.service";
|
||||
import { SyncEventArgs } from "@bitwarden/common/types/syncEventArgs";
|
||||
|
||||
describe("Organization Service", () => {
|
||||
let organizationService: OrganizationService;
|
||||
@@ -14,8 +11,6 @@ describe("Organization Service", () => {
|
||||
let stateService: MockProxy<StateService>;
|
||||
let activeAccount: BehaviorSubject<string>;
|
||||
let activeAccountUnlocked: BehaviorSubject<boolean>;
|
||||
let syncNotifierService: MockProxy<SyncNotifierService>;
|
||||
let sync: Subject<SyncEventArgs>;
|
||||
|
||||
const resetStateService = async (
|
||||
customizeStateService: (stateService: MockProxy<StateService>) => void
|
||||
@@ -25,7 +20,7 @@ describe("Organization Service", () => {
|
||||
stateService.activeAccount$ = activeAccount;
|
||||
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
|
||||
customizeStateService(stateService);
|
||||
organizationService = new OrganizationService(stateService, syncNotifierService);
|
||||
organizationService = new OrganizationService(stateService);
|
||||
await new Promise((r) => setTimeout(r, 50));
|
||||
};
|
||||
|
||||
@@ -41,12 +36,7 @@ describe("Organization Service", () => {
|
||||
"1": organizationData("1", "Test Org"),
|
||||
});
|
||||
|
||||
sync = new Subject<SyncEventArgs>();
|
||||
|
||||
syncNotifierService = mock<SyncNotifierService>();
|
||||
syncNotifierService.sync$ = sync;
|
||||
|
||||
organizationService = new OrganizationService(stateService, syncNotifierService);
|
||||
organizationService = new OrganizationService(stateService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -169,36 +159,6 @@ describe("Organization Service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("syncEvent works", () => {
|
||||
it("Complete event updates data", async () => {
|
||||
sync.next({
|
||||
status: "Completed",
|
||||
successfully: true,
|
||||
data: new SyncResponse({
|
||||
profile: {
|
||||
organizations: [
|
||||
{
|
||||
id: "1",
|
||||
name: "Updated Name",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
|
||||
expect(stateService.setOrganizations).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(stateService.setOrganizations).toHaveBeenLastCalledWith(
|
||||
matches((organizationData: { [id: string]: OrganizationData }) => {
|
||||
const organization = organizationData["1"];
|
||||
return organization?.name === "Updated Name";
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function organizationData(id: string, name: string) {
|
||||
const data = new OrganizationData({} as any);
|
||||
data.id = id;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ProfileResponse } from "../../models/response/profile.response";
|
||||
export abstract class AvatarUpdateService {
|
||||
avatarUpdate$ = new Observable<string | null>();
|
||||
abstract pushUpdate(color: string): Promise<ProfileResponse | void>;
|
||||
abstract loadColorFromState(): Promise<string | null>;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { CipherCreateRequest } from "../models/request/cipher-create.request";
|
||||
import { CipherPartialRequest } from "../models/request/cipher-partial.request";
|
||||
import { CipherShareRequest } from "../models/request/cipher-share.request";
|
||||
import { CipherRequest } from "../models/request/cipher.request";
|
||||
import { CollectionBulkDeleteRequest } from "../models/request/collection-bulk-delete.request";
|
||||
import { CollectionRequest } from "../models/request/collection.request";
|
||||
import { DeleteRecoverRequest } from "../models/request/delete-recover.request";
|
||||
import { DeviceVerificationRequest } from "../models/request/device-verification.request";
|
||||
@@ -22,7 +23,6 @@ import { EmergencyAccessInviteRequest } from "../models/request/emergency-access
|
||||
import { EmergencyAccessPasswordRequest } from "../models/request/emergency-access-password.request";
|
||||
import { EmergencyAccessUpdateRequest } from "../models/request/emergency-access-update.request";
|
||||
import { EventRequest } from "../models/request/event.request";
|
||||
import { GroupRequest } from "../models/request/group.request";
|
||||
import { IapCheckRequest } from "../models/request/iap-check.request";
|
||||
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
||||
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
|
||||
@@ -34,15 +34,6 @@ import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user
|
||||
import { KeysRequest } from "../models/request/keys.request";
|
||||
import { OrganizationConnectionRequest } from "../models/request/organization-connection.request";
|
||||
import { OrganizationImportRequest } from "../models/request/organization-import.request";
|
||||
import { OrganizationUserAcceptRequest } from "../models/request/organization-user-accept.request";
|
||||
import { OrganizationUserBulkConfirmRequest } from "../models/request/organization-user-bulk-confirm.request";
|
||||
import { OrganizationUserBulkRequest } from "../models/request/organization-user-bulk.request";
|
||||
import { OrganizationUserConfirmRequest } from "../models/request/organization-user-confirm.request";
|
||||
import { OrganizationUserInviteRequest } from "../models/request/organization-user-invite.request";
|
||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "../models/request/organization-user-reset-password-enrollment.request";
|
||||
import { OrganizationUserResetPasswordRequest } from "../models/request/organization-user-reset-password.request";
|
||||
import { OrganizationUserUpdateGroupsRequest } from "../models/request/organization-user-update-groups.request";
|
||||
import { OrganizationUserUpdateRequest } from "../models/request/organization-user-update.request";
|
||||
import { OrganizationSponsorshipCreateRequest } from "../models/request/organization/organization-sponsorship-create.request";
|
||||
import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organization-sponsorship-redeem.request";
|
||||
import { PasswordHintRequest } from "../models/request/password-hint.request";
|
||||
@@ -71,6 +62,7 @@ import { TaxInfoUpdateRequest } from "../models/request/tax-info-update.request"
|
||||
import { TwoFactorEmailRequest } from "../models/request/two-factor-email.request";
|
||||
import { TwoFactorProviderRequest } from "../models/request/two-factor-provider.request";
|
||||
import { TwoFactorRecoveryRequest } from "../models/request/two-factor-recovery.request";
|
||||
import { UpdateAvatarRequest } from "../models/request/update-avatar.request";
|
||||
import { UpdateDomainsRequest } from "../models/request/update-domains.request";
|
||||
import { UpdateKeyRequest } from "../models/request/update-key.request";
|
||||
import { UpdateProfileRequest } from "../models/request/update-profile.request";
|
||||
@@ -93,7 +85,7 @@ import { BillingPaymentResponse } from "../models/response/billing-payment.respo
|
||||
import { BreachAccountResponse } from "../models/response/breach-account.response";
|
||||
import { CipherResponse } from "../models/response/cipher.response";
|
||||
import {
|
||||
CollectionGroupDetailsResponse,
|
||||
CollectionAccessDetailsResponse,
|
||||
CollectionResponse,
|
||||
} from "../models/response/collection.response";
|
||||
import { DeviceVerificationResponse } from "../models/response/device-verification.response";
|
||||
@@ -105,7 +97,6 @@ import {
|
||||
EmergencyAccessViewResponse,
|
||||
} from "../models/response/emergency-access.response";
|
||||
import { EventResponse } from "../models/response/event.response";
|
||||
import { GroupDetailsResponse, GroupResponse } from "../models/response/group.response";
|
||||
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
||||
@@ -117,13 +108,6 @@ import {
|
||||
} from "../models/response/organization-connection.response";
|
||||
import { OrganizationExportResponse } from "../models/response/organization-export.response";
|
||||
import { OrganizationSponsorshipSyncStatusResponse } from "../models/response/organization-sponsorship-sync-status.response";
|
||||
import { OrganizationUserBulkPublicKeyResponse } from "../models/response/organization-user-bulk-public-key.response";
|
||||
import { OrganizationUserBulkResponse } from "../models/response/organization-user-bulk.response";
|
||||
import {
|
||||
OrganizationUserDetailsResponse,
|
||||
OrganizationUserUserDetailsResponse,
|
||||
OrganizationUserResetPasswordDetailsReponse,
|
||||
} from "../models/response/organization-user.response";
|
||||
import { PaymentResponse } from "../models/response/payment.response";
|
||||
import { PlanResponse } from "../models/response/plan.response";
|
||||
import { PolicyResponse } from "../models/response/policy.response";
|
||||
@@ -136,8 +120,8 @@ import {
|
||||
import { ProviderUserBulkPublicKeyResponse } from "../models/response/provider/provider-user-bulk-public-key.response";
|
||||
import { ProviderUserBulkResponse } from "../models/response/provider/provider-user-bulk.response";
|
||||
import {
|
||||
ProviderUserUserDetailsResponse,
|
||||
ProviderUserResponse,
|
||||
ProviderUserUserDetailsResponse,
|
||||
} from "../models/response/provider/provider-user.response";
|
||||
import { ProviderResponse } from "../models/response/provider/provider.response";
|
||||
import { SelectionReadOnlyResponse } from "../models/response/selection-read-only.response";
|
||||
@@ -156,13 +140,18 @@ import { TwoFactorEmailResponse } from "../models/response/two-factor-email.resp
|
||||
import { TwoFactorProviderResponse } from "../models/response/two-factor-provider.response";
|
||||
import { TwoFactorRecoverResponse } from "../models/response/two-factor-recover.response";
|
||||
import {
|
||||
TwoFactorWebAuthnResponse,
|
||||
ChallengeResponse,
|
||||
TwoFactorWebAuthnResponse,
|
||||
} from "../models/response/two-factor-web-authn.response";
|
||||
import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key.response";
|
||||
import { UserKeyResponse } from "../models/response/user-key.response";
|
||||
import { SendAccessView } from "../models/view/send-access.view";
|
||||
|
||||
/**
|
||||
* @deprecated The `ApiService` class is deprecated and calls should be extracted into individual
|
||||
* api services. The `send` method is still allowed to be used within api services. For background
|
||||
* of this decision please read https://contributing.bitwarden.com/architecture/adr/refactor-api-service.
|
||||
*/
|
||||
export abstract class ApiService {
|
||||
send: (
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
@@ -183,6 +172,7 @@ export abstract class ApiService {
|
||||
getUserSubscription: () => Promise<SubscriptionResponse>;
|
||||
getTaxInfo: () => Promise<TaxInfoResponse>;
|
||||
putProfile: (request: UpdateProfileRequest) => Promise<ProfileResponse>;
|
||||
putAvatar: (request: UpdateAvatarRequest) => Promise<ProfileResponse>;
|
||||
putTaxInfo: (request: TaxInfoUpdateRequest) => Promise<any>;
|
||||
postPrelogin: (request: PreloginRequest) => Promise<PreloginResponse>;
|
||||
postEmailToken: (request: EmailTokenRequest) => Promise<any>;
|
||||
@@ -313,13 +303,16 @@ export abstract class ApiService {
|
||||
) => Promise<AttachmentUploadDataResponse>;
|
||||
postAttachmentFile: (id: string, attachmentId: string, data: FormData) => Promise<any>;
|
||||
|
||||
getCollectionDetails: (
|
||||
organizationId: string,
|
||||
id: string
|
||||
) => Promise<CollectionGroupDetailsResponse>;
|
||||
getUserCollections: () => Promise<ListResponse<CollectionResponse>>;
|
||||
getCollections: (organizationId: string) => Promise<ListResponse<CollectionResponse>>;
|
||||
getCollectionUsers: (organizationId: string, id: string) => Promise<SelectionReadOnlyResponse[]>;
|
||||
getCollectionAccessDetails: (
|
||||
organizationId: string,
|
||||
id: string
|
||||
) => Promise<CollectionAccessDetailsResponse>;
|
||||
getManyCollectionsWithAccessDetails: (
|
||||
orgId: string
|
||||
) => Promise<ListResponse<CollectionAccessDetailsResponse>>;
|
||||
postCollection: (
|
||||
organizationId: string,
|
||||
request: CollectionRequest
|
||||
@@ -335,97 +328,17 @@ export abstract class ApiService {
|
||||
request: CollectionRequest
|
||||
) => Promise<CollectionResponse>;
|
||||
deleteCollection: (organizationId: string, id: string) => Promise<any>;
|
||||
deleteManyCollections: (request: CollectionBulkDeleteRequest) => Promise<any>;
|
||||
deleteCollectionUser: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
organizationUserId: string
|
||||
) => Promise<any>;
|
||||
|
||||
getGroupDetails: (organizationId: string, id: string) => Promise<GroupDetailsResponse>;
|
||||
getGroups: (organizationId: string) => Promise<ListResponse<GroupResponse>>;
|
||||
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
|
||||
postGroup: (organizationId: string, request: GroupRequest) => Promise<GroupResponse>;
|
||||
putGroup: (organizationId: string, id: string, request: GroupRequest) => Promise<GroupResponse>;
|
||||
putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>;
|
||||
deleteGroup: (organizationId: string, id: string) => Promise<any>;
|
||||
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
|
||||
|
||||
getOrganizationUser: (
|
||||
organizationId: string,
|
||||
id: string
|
||||
) => Promise<OrganizationUserDetailsResponse>;
|
||||
getOrganizationUserGroups: (organizationId: string, id: string) => Promise<string[]>;
|
||||
getOrganizationUsers: (
|
||||
organizationId: string
|
||||
) => Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
|
||||
getOrganizationUserResetPasswordDetails: (
|
||||
organizationId: string,
|
||||
id: string
|
||||
) => Promise<OrganizationUserResetPasswordDetailsReponse>;
|
||||
postOrganizationUserInvite: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserInviteRequest
|
||||
) => Promise<any>;
|
||||
postOrganizationUserReinvite: (organizationId: string, id: string) => Promise<any>;
|
||||
postManyOrganizationUserReinvite: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
postOrganizationUserAccept: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserAcceptRequest
|
||||
) => Promise<any>;
|
||||
postOrganizationUserConfirm: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserConfirmRequest
|
||||
) => Promise<any>;
|
||||
postOrganizationUsersPublicKey: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkPublicKeyResponse>>;
|
||||
postOrganizationUserBulkConfirm: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkConfirmRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
|
||||
putOrganizationUser: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserUpdateRequest
|
||||
) => Promise<any>;
|
||||
putOrganizationUserGroups: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserUpdateGroupsRequest
|
||||
) => Promise<any>;
|
||||
putOrganizationUserResetPasswordEnrollment: (
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
request: OrganizationUserResetPasswordEnrollmentRequest
|
||||
) => Promise<void>;
|
||||
putOrganizationUserResetPassword: (
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserResetPasswordRequest
|
||||
) => Promise<any>;
|
||||
deleteOrganizationUser: (organizationId: string, id: string) => Promise<any>;
|
||||
deleteManyOrganizationUsers: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
revokeOrganizationUser: (organizationId: string, id: string) => Promise<any>;
|
||||
revokeManyOrganizationUsers: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
restoreOrganizationUser: (organizationId: string, id: string) => Promise<any>;
|
||||
restoreManyOrganizationUsers: (
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkRequest
|
||||
) => Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
|
||||
getSync: () => Promise<SyncResponse>;
|
||||
postPublicImportDirectory: (request: OrganizationImportRequest) => Promise<any>;
|
||||
|
||||
|
||||
@@ -66,8 +66,8 @@ export abstract class CipherService {
|
||||
deleteManyWithServer: (ids: string[]) => Promise<any>;
|
||||
deleteAttachment: (id: string, attachmentId: string) => Promise<void>;
|
||||
deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise<void>;
|
||||
sortCiphersByLastUsed: (a: any, b: any) => number;
|
||||
sortCiphersByLastUsedThenName: (a: any, b: any) => number;
|
||||
sortCiphersByLastUsed: (a: CipherView, b: CipherView) => number;
|
||||
sortCiphersByLastUsedThenName: (a: CipherView, b: CipherView) => number;
|
||||
getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number;
|
||||
softDelete: (id: string | string[]) => Promise<any>;
|
||||
softDeleteWithServer: (id: string) => Promise<any>;
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { EventType } from "../enums/eventType";
|
||||
|
||||
export abstract class EventService {
|
||||
collect: (
|
||||
eventType: EventType,
|
||||
cipherId?: string,
|
||||
uploadImmediately?: boolean,
|
||||
organizationId?: string
|
||||
) => Promise<any>;
|
||||
uploadEvents: (userId?: string) => Promise<any>;
|
||||
clearEvents: (userId?: string) => Promise<any>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { EventType } from "../../enums/eventType";
|
||||
|
||||
export abstract class EventCollectionService {
|
||||
collect: (
|
||||
eventType: EventType,
|
||||
cipherId?: string,
|
||||
uploadImmediately?: boolean,
|
||||
organizationId?: string
|
||||
) => Promise<any>;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export abstract class EventUploadService {
|
||||
uploadEvents: (userId?: string) => Promise<void>;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export abstract class FolderService {
|
||||
clearCache: () => Promise<void>;
|
||||
encrypt: (model: FolderView, key?: SymmetricCryptoKey) => Promise<Folder>;
|
||||
get: (id: string) => Promise<Folder>;
|
||||
getAllFromState: () => Promise<Folder[]>;
|
||||
/**
|
||||
* @deprecated Only use in CLI!
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,6 @@ export abstract class I18nService {
|
||||
translationLocale: string;
|
||||
collator: Intl.Collator;
|
||||
localeNames: Map<string, string>;
|
||||
t: (id: string, p1?: string, p2?: string, p3?: string) => string;
|
||||
t: (id: string, p1?: string | number, p2?: string | number, p3?: string | number) => string;
|
||||
translate: (id: string, p1?: string, p2?: string, p3?: string) => string;
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ export abstract class LoginService {
|
||||
setEmail: (value: string) => void;
|
||||
setRememberEmail: (value: boolean) => void;
|
||||
clearValues: () => void;
|
||||
saveEmailSettings: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import { ListResponse } from "../../models/response/list.response";
|
||||
|
||||
import {
|
||||
OrganizationUserAcceptRequest,
|
||||
OrganizationUserBulkConfirmRequest,
|
||||
OrganizationUserConfirmRequest,
|
||||
OrganizationUserInviteRequest,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
OrganizationUserResetPasswordRequest,
|
||||
OrganizationUserUpdateGroupsRequest,
|
||||
OrganizationUserUpdateRequest,
|
||||
} from "./requests";
|
||||
import {
|
||||
OrganizationUserBulkPublicKeyResponse,
|
||||
OrganizationUserBulkResponse,
|
||||
OrganizationUserDetailsResponse,
|
||||
OrganizationUserResetPasswordDetailsReponse,
|
||||
OrganizationUserUserDetailsResponse,
|
||||
} from "./responses";
|
||||
|
||||
/**
|
||||
* Service for interacting with Organization Users via the API
|
||||
*/
|
||||
export abstract class OrganizationUserService {
|
||||
/**
|
||||
* Retrieve a single organization user by Id
|
||||
* @param organizationId - Identifier for the user's organization
|
||||
* @param id - Organization user identifier
|
||||
* @param options - Options for the request
|
||||
*/
|
||||
abstract getOrganizationUser(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
options?: {
|
||||
includeGroups?: boolean;
|
||||
}
|
||||
): Promise<OrganizationUserDetailsResponse>;
|
||||
|
||||
/**
|
||||
* Retrieve a list of groups Ids the specified organization user belongs to
|
||||
* @param organizationId - Identifier for the user's organization
|
||||
* @param id - Organization user identifier
|
||||
*/
|
||||
abstract getOrganizationUserGroups(organizationId: string, id: string): Promise<string[]>;
|
||||
|
||||
/**
|
||||
* Retrieve a list of all users that belong to the specified organization
|
||||
* @param organizationId - Identifier for the organization
|
||||
* @param options - Options for the request
|
||||
*/
|
||||
abstract getAllUsers(
|
||||
organizationId: string,
|
||||
options?: {
|
||||
includeCollections?: boolean;
|
||||
includeGroups?: boolean;
|
||||
}
|
||||
): Promise<ListResponse<OrganizationUserUserDetailsResponse>>;
|
||||
|
||||
/**
|
||||
* Retrieve reset password details for the specified organization user
|
||||
* @param organizationId - Identifier for the user's organization
|
||||
* @param id - Organization user identifier
|
||||
*/
|
||||
abstract getOrganizationUserResetPasswordDetails(
|
||||
organizationId: string,
|
||||
id: string
|
||||
): Promise<OrganizationUserResetPasswordDetailsReponse>;
|
||||
|
||||
/**
|
||||
* Create new organization user invite(s) for the specified organization
|
||||
* @param organizationId - Identifier for the organization
|
||||
* @param request - New user invitation request details
|
||||
*/
|
||||
abstract postOrganizationUserInvite(
|
||||
organizationId: string,
|
||||
request: OrganizationUserInviteRequest
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Re-invite the specified organization user
|
||||
* @param organizationId - Identifier for the user's organization
|
||||
* @param id - Organization user identifier
|
||||
*/
|
||||
abstract postOrganizationUserReinvite(organizationId: string, id: string): Promise<any>;
|
||||
|
||||
/**
|
||||
* Re-invite many organization users for the specified organization
|
||||
* @param organizationId - Identifier for the organization
|
||||
* @param ids - A list of organization user identifiers
|
||||
* @return List of user ids, including both those that were successfully re-invited and those that had an error
|
||||
*/
|
||||
abstract postManyOrganizationUserReinvite(
|
||||
organizationId: string,
|
||||
ids: string[]
|
||||
): Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
|
||||
/**
|
||||
* Accept an organization user invitation
|
||||
* @param organizationId - Identifier for the organization to accept
|
||||
* @param id - Organization user identifier
|
||||
* @param request - Request details for accepting the invitation
|
||||
*/
|
||||
abstract postOrganizationUserAccept(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserAcceptRequest
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Confirm an organization user that has accepted their invitation
|
||||
* @param organizationId - Identifier for the organization to confirm
|
||||
* @param id - Organization user identifier
|
||||
* @param request - Request details for confirming the user
|
||||
*/
|
||||
abstract postOrganizationUserConfirm(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserConfirmRequest
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Retrieve a list of the specified users' public keys
|
||||
* @param organizationId - Identifier for the organization to accept
|
||||
* @param ids - A list of organization user identifiers to retrieve public keys for
|
||||
*/
|
||||
abstract postOrganizationUsersPublicKey(
|
||||
organizationId: string,
|
||||
ids: string[]
|
||||
): Promise<ListResponse<OrganizationUserBulkPublicKeyResponse>>;
|
||||
|
||||
/**
|
||||
* Confirm many organization users that have accepted their invitations
|
||||
* @param organizationId - Identifier for the organization to confirm users
|
||||
* @param request - Bulk request details for confirming the user
|
||||
*/
|
||||
abstract postOrganizationUserBulkConfirm(
|
||||
organizationId: string,
|
||||
request: OrganizationUserBulkConfirmRequest
|
||||
): Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
|
||||
/**
|
||||
* Update an organization users
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
* @param id - Organization user identifier
|
||||
* @param request - Request details for updating the user
|
||||
*/
|
||||
abstract putOrganizationUser(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserUpdateRequest
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update an organization user's groups
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
* @param id - Organization user identifier
|
||||
* @param groupIds - List of group ids to associate the user with
|
||||
*/
|
||||
abstract putOrganizationUserGroups(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
groupIds: OrganizationUserUpdateGroupsRequest
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Update an organization user's reset password enrollment
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
* @param userId - Organization user identifier
|
||||
* @param request - Reset password enrollment details
|
||||
*/
|
||||
abstract putOrganizationUserResetPasswordEnrollment(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
request: OrganizationUserResetPasswordEnrollmentRequest
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Reset an organization user's password
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
* @param id - Organization user identifier
|
||||
* @param request - Reset password details
|
||||
*/
|
||||
abstract putOrganizationUserResetPassword(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
request: OrganizationUserResetPasswordRequest
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete an organization user
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
* @param id - Organization user identifier
|
||||
*/
|
||||
abstract deleteOrganizationUser(organizationId: string, id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Delete many organization users
|
||||
* @param organizationId - Identifier for the organization the users belongs to
|
||||
* @param ids - List of organization user identifiers to delete
|
||||
* @return List of user ids, including both those that were successfully deleted and those that had an error
|
||||
*/
|
||||
abstract deleteManyOrganizationUsers(
|
||||
organizationId: string,
|
||||
ids: string[]
|
||||
): Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
|
||||
/**
|
||||
* Revoke an organization user's access to the organization
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
* @param id - Organization user identifier
|
||||
*/
|
||||
abstract revokeOrganizationUser(organizationId: string, id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Revoke many organization users' access to the organization
|
||||
* @param organizationId - Identifier for the organization the users belongs to
|
||||
* @param ids - List of organization user identifiers to revoke
|
||||
* @return List of user ids, including both those that were successfully revoked and those that had an error
|
||||
*/
|
||||
abstract revokeManyOrganizationUsers(
|
||||
organizationId: string,
|
||||
ids: string[]
|
||||
): Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
|
||||
/**
|
||||
* Restore an organization user's access to the organization
|
||||
* @param organizationId - Identifier for the organization the user belongs to
|
||||
* @param id - Organization user identifier
|
||||
*/
|
||||
abstract restoreOrganizationUser(organizationId: string, id: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* Restore many organization users' access to the organization
|
||||
* @param organizationId - Identifier for the organization the users belongs to
|
||||
* @param ids - List of organization user identifiers to restore
|
||||
* @return List of user ids, including both those that were successfully restored and those that had an error
|
||||
*/
|
||||
abstract restoreManyOrganizationUsers(
|
||||
organizationId: string,
|
||||
ids: string[]
|
||||
): Promise<ListResponse<OrganizationUserBulkResponse>>;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from "./organization-user-accept.request";
|
||||
export * from "./organization-user-bulk-confirm.request";
|
||||
export * from "./organization-user-confirm.request";
|
||||
export * from "./organization-user-invite.request";
|
||||
export * from "./organization-user-reset-password.request";
|
||||
export * from "./organization-user-reset-password-enrollment.request";
|
||||
export * from "./organization-user-update.request";
|
||||
export * from "./organization-user-update-groups.request";
|
||||
@@ -0,0 +1,12 @@
|
||||
import { OrganizationUserType } from "../../../enums/organizationUserType";
|
||||
import { PermissionsApi } from "../../../models/api/permissions.api";
|
||||
import { SelectionReadOnlyRequest } from "../../../models/request/selection-read-only.request";
|
||||
|
||||
export class OrganizationUserInviteRequest {
|
||||
emails: string[] = [];
|
||||
type: OrganizationUserType;
|
||||
accessAll: boolean;
|
||||
collections: SelectionReadOnlyRequest[] = [];
|
||||
groups: string[];
|
||||
permissions: PermissionsApi;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SecretVerificationRequest } from "./secret-verification.request";
|
||||
import { SecretVerificationRequest } from "../../../models/request/secret-verification.request";
|
||||
|
||||
export class OrganizationUserResetPasswordEnrollmentRequest extends SecretVerificationRequest {
|
||||
resetPasswordKey: string;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { OrganizationUserType } from "../../../enums/organizationUserType";
|
||||
import { PermissionsApi } from "../../../models/api/permissions.api";
|
||||
import { SelectionReadOnlyRequest } from "../../../models/request/selection-read-only.request";
|
||||
|
||||
export class OrganizationUserUpdateRequest {
|
||||
type: OrganizationUserType;
|
||||
accessAll: boolean;
|
||||
collections: SelectionReadOnlyRequest[] = [];
|
||||
groups: string[] = [];
|
||||
permissions: PermissionsApi;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./organization-user.response";
|
||||
export * from "./organization-user-bulk.response";
|
||||
export * from "./organization-user-bulk-public-key.response";
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseResponse } from "./base.response";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class OrganizationUserBulkPublicKeyResponse extends BaseResponse {
|
||||
id: string;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BaseResponse } from "./base.response";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class OrganizationUserBulkResponse extends BaseResponse {
|
||||
id: string;
|
||||
@@ -1,10 +1,9 @@
|
||||
import { KdfType } from "../../enums/kdfType";
|
||||
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "../../enums/organizationUserType";
|
||||
import { PermissionsApi } from "../api/permissions.api";
|
||||
|
||||
import { BaseResponse } from "./base.response";
|
||||
import { SelectionReadOnlyResponse } from "./selection-read-only.response";
|
||||
import { KdfType } from "../../../enums/kdfType";
|
||||
import { OrganizationUserStatusType } from "../../../enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "../../../enums/organizationUserType";
|
||||
import { PermissionsApi } from "../../../models/api/permissions.api";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { SelectionReadOnlyResponse } from "../../../models/response/selection-read-only.response";
|
||||
|
||||
export class OrganizationUserResponse extends BaseResponse {
|
||||
id: string;
|
||||
@@ -14,6 +13,8 @@ export class OrganizationUserResponse extends BaseResponse {
|
||||
accessAll: boolean;
|
||||
permissions: PermissionsApi;
|
||||
resetPasswordEnrolled: boolean;
|
||||
collections: SelectionReadOnlyResponse[] = [];
|
||||
groups: string[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -24,6 +25,15 @@ export class OrganizationUserResponse extends BaseResponse {
|
||||
this.permissions = new PermissionsApi(this.getResponseProperty("Permissions"));
|
||||
this.accessAll = this.getResponseProperty("AccessAll");
|
||||
this.resetPasswordEnrolled = this.getResponseProperty("ResetPasswordEnrolled");
|
||||
|
||||
const collections = this.getResponseProperty("Collections");
|
||||
if (collections != null) {
|
||||
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
|
||||
}
|
||||
const groups = this.getResponseProperty("Groups");
|
||||
if (groups != null) {
|
||||
this.groups = groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,14 +53,8 @@ export class OrganizationUserUserDetailsResponse extends OrganizationUserRespons
|
||||
}
|
||||
|
||||
export class OrganizationUserDetailsResponse extends OrganizationUserResponse {
|
||||
collections: SelectionReadOnlyResponse[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
const collections = this.getResponseProperty("Collections");
|
||||
if (collections != null) {
|
||||
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { Utils } from "../../misc/utils";
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
import { I18nService } from "../i18n.service";
|
||||
|
||||
export function canAccessVaultTab(org: Organization): boolean {
|
||||
return org.isManager;
|
||||
return org.canViewAssignedCollections || org.canViewAllCollections || org.canManageGroups;
|
||||
}
|
||||
|
||||
export function canAccessSettingsTab(org: Organization): boolean {
|
||||
@@ -34,19 +35,6 @@ export function canAccessBillingTab(org: Organization): boolean {
|
||||
return org.canManageBilling;
|
||||
}
|
||||
|
||||
export function canManageCollections(org: Organization): boolean {
|
||||
return (
|
||||
org.canCreateNewCollections ||
|
||||
org.canEditAnyCollection ||
|
||||
org.canDeleteAnyCollection ||
|
||||
org.canViewAssignedCollections
|
||||
);
|
||||
}
|
||||
|
||||
export function canAccessManageTab(org: Organization): boolean {
|
||||
return canAccessMembersTab(org) || canAccessGroupsTab(org) || canManageCollections(org);
|
||||
}
|
||||
|
||||
export function canAccessOrgAdmin(org: Organization): boolean {
|
||||
return (
|
||||
canAccessMembersTab(org) ||
|
||||
@@ -54,8 +42,7 @@ export function canAccessOrgAdmin(org: Organization): boolean {
|
||||
canAccessReportingTab(org) ||
|
||||
canAccessBillingTab(org) ||
|
||||
canAccessSettingsTab(org) ||
|
||||
canAccessVaultTab(org) ||
|
||||
canAccessManageTab(org)
|
||||
canAccessVaultTab(org)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,6 +56,10 @@ export function canAccessAdmin(i18nService: I18nService) {
|
||||
);
|
||||
}
|
||||
|
||||
export function isNotProviderUser(org: Organization): boolean {
|
||||
return !org.isProviderUser;
|
||||
}
|
||||
|
||||
export abstract class OrganizationService {
|
||||
organizations$: Observable<Organization[]>;
|
||||
|
||||
@@ -83,3 +74,7 @@ export abstract class OrganizationService {
|
||||
canManageSponsorships: () => Promise<boolean>;
|
||||
hasOrganizations: () => boolean;
|
||||
}
|
||||
|
||||
export abstract class InternalOrganizationService extends OrganizationService {
|
||||
replace: (organizations: { [id: string]: OrganizationData }) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ export abstract class PlatformUtilsService {
|
||||
isViewOpen: () => Promise<boolean>;
|
||||
launchUri: (uri: string, options?: any) => void;
|
||||
getApplicationVersion: () => Promise<string>;
|
||||
getApplicationVersionNumber: () => Promise<string>;
|
||||
supportsWebAuthn: (win: Window) => boolean;
|
||||
supportsDuo: () => boolean;
|
||||
showToast: (
|
||||
@@ -33,7 +34,8 @@ export abstract class PlatformUtilsService {
|
||||
confirmText?: string,
|
||||
cancelText?: string,
|
||||
type?: string,
|
||||
bodyIsHtml?: boolean
|
||||
bodyIsHtml?: boolean,
|
||||
target?: string
|
||||
) => Promise<boolean>;
|
||||
isDev: () => boolean;
|
||||
isSelfHost: () => boolean;
|
||||
|
||||
@@ -349,4 +349,7 @@ export abstract class StateService<T extends Account = Account> {
|
||||
* @deprecated Do not call this directly, use ConfigService
|
||||
*/
|
||||
setServerConfig: (value: ServerConfigData, options?: StorageOptions) => Promise<void>;
|
||||
|
||||
getAvatarColor: (options?: StorageOptions) => Promise<string | null | undefined>;
|
||||
setAvatarColor: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ export abstract class AbstractStorageService {
|
||||
abstract remove(key: string, options?: StorageOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export abstract class AbstractCachedStorageService extends AbstractStorageService {
|
||||
export abstract class AbstractMemoryStorageService extends AbstractStorageService {
|
||||
// Used to identify the service in the session sync decorator framework
|
||||
static readonly TYPE = "MemoryStorageService";
|
||||
readonly type = AbstractMemoryStorageService.TYPE;
|
||||
|
||||
abstract get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
|
||||
abstract getBypassCache<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
|
||||
}
|
||||
|
||||
export interface MemoryStorageServiceInterface {
|
||||
get<T>(key: string, options?: MemoryStorageOptions<T>): Promise<T>;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarderOptions";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class AnonAddyForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarderOptions";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class DuckDuckGoForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarderOptions";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class FastmailForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarderOptions";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class FirefoxRelayForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
|
||||
import { ForwarderOptions } from "./forwarderOptions";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export interface Forwarder {
|
||||
generate(apiService: ApiService, options: ForwarderOptions): Promise<string>;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
|
||||
import { Forwarder } from "./forwarder";
|
||||
import { ForwarderOptions } from "./forwarderOptions";
|
||||
import { ForwarderOptions } from "./forwarder-options";
|
||||
|
||||
export class SimpleLoginForwarder implements Forwarder {
|
||||
async generate(apiService: ApiService, options: ForwarderOptions): Promise<string> {
|
||||
@@ -67,6 +67,7 @@ export const regularImportOptions = [
|
||||
{ id: "encryptrcsv", name: "Encryptr (csv)" },
|
||||
{ id: "yoticsv", name: "Yoti (csv)" },
|
||||
{ id: "nordpasscsv", name: "Nordpass (csv)" },
|
||||
{ id: "passkyjson", name: "Passky (json)" },
|
||||
] as const;
|
||||
|
||||
export type ImportType =
|
||||
|
||||
@@ -3,5 +3,5 @@ export enum KdfType {
|
||||
}
|
||||
|
||||
export const DEFAULT_KDF_TYPE = KdfType.PBKDF2_SHA256;
|
||||
export const DEFAULT_KDF_ITERATIONS = 100000;
|
||||
export const DEFAULT_KDF_ITERATIONS = 600000;
|
||||
export const SEND_KDF_ITERATIONS = 100000;
|
||||
|
||||
@@ -137,8 +137,7 @@ export class DashlaneCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.card.number = row.cc_number;
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.code = row.code;
|
||||
cipher.card.expMonth = row.expiration_month;
|
||||
cipher.card.expYear = row.expiration_year.substring(2, 4);
|
||||
this.setCardExpiration(cipher, `${row.expiration_month}/${row.expiration_year}`);
|
||||
|
||||
// If you add more mapped fields please extend this
|
||||
mappedValues = [
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { CipherType } from "../enums/cipherType";
|
||||
import { SecureNoteType } from "../enums/secureNoteType";
|
||||
import { ImportResult } from "../models/domain/import-result";
|
||||
import { CardView } from "../models/view/card.view";
|
||||
import { SecureNoteView } from "../models/view/secure-note.view";
|
||||
|
||||
import { BaseImporter } from "./base-importer";
|
||||
import { Importer } from "./importer";
|
||||
import { CipherType } from "../../enums/cipherType";
|
||||
import { SecureNoteType } from "../../enums/secureNoteType";
|
||||
import { ImportResult } from "../../models/domain/import-result";
|
||||
import { CardView } from "../../models/view/card.view";
|
||||
import { SecureNoteView } from "../../models/view/secure-note.view";
|
||||
import { BaseImporter } from "../base-importer";
|
||||
import { Importer } from "../importer";
|
||||
|
||||
export class EnpassCsvImporter extends BaseImporter implements Importer {
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
@@ -1,17 +1,21 @@
|
||||
import { CipherType } from "../enums/cipherType";
|
||||
import { FieldType } from "../enums/fieldType";
|
||||
import { ImportResult } from "../models/domain/import-result";
|
||||
import { CardView } from "../models/view/card.view";
|
||||
import { CipherView } from "../models/view/cipher.view";
|
||||
import { FolderView } from "../models/view/folder.view";
|
||||
import { CipherType } from "../../enums/cipherType";
|
||||
import { FieldType } from "../../enums/fieldType";
|
||||
import { ImportResult } from "../../models/domain/import-result";
|
||||
import { CardView } from "../../models/view/card.view";
|
||||
import { CipherView } from "../../models/view/cipher.view";
|
||||
import { FolderView } from "../../models/view/folder.view";
|
||||
import { BaseImporter } from "../base-importer";
|
||||
import { Importer } from "../importer";
|
||||
|
||||
import { BaseImporter } from "./base-importer";
|
||||
import { Importer } from "./importer";
|
||||
import { EnpassJsonFile, EnpassFolder, EnpassField } from "./types/enpass-json-type";
|
||||
|
||||
type EnpassFolderTreeItem = EnpassFolder & { children: EnpassFolderTreeItem[] };
|
||||
const androidUrlRegex = new RegExp("androidapp://.*==@", "g");
|
||||
|
||||
export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
const result = new ImportResult();
|
||||
const results = JSON.parse(data);
|
||||
const results: EnpassJsonFile = JSON.parse(data);
|
||||
if (results == null || results.items == null || results.items.length === 0) {
|
||||
result.success = false;
|
||||
return Promise.resolve(result);
|
||||
@@ -28,7 +32,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
result.folders.push(f);
|
||||
});
|
||||
|
||||
results.items.forEach((item: any) => {
|
||||
results.items.forEach((item) => {
|
||||
if (item.folders != null && item.folders.length > 0 && foldersIndexMap.has(item.folders[0])) {
|
||||
result.folderRelationships.push([
|
||||
result.ciphers.length,
|
||||
@@ -50,7 +54,7 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
this.processCard(cipher, item.fields);
|
||||
} else if (
|
||||
item.template_type.indexOf("identity.") < 0 &&
|
||||
item.fields.some((f: any) => f.type === "password" && !this.isNullOrWhitespace(f.value))
|
||||
item.fields.some((f) => f.type === "password" && !this.isNullOrWhitespace(f.value))
|
||||
) {
|
||||
this.processLogin(cipher, item.fields);
|
||||
} else {
|
||||
@@ -68,9 +72,9 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
private processLogin(cipher: CipherView, fields: any[]) {
|
||||
private processLogin(cipher: CipherView, fields: EnpassField[]) {
|
||||
const urls: string[] = [];
|
||||
fields.forEach((field: any) => {
|
||||
fields.forEach((field) => {
|
||||
if (this.isNullOrWhitespace(field.value) || field.type === "section") {
|
||||
return;
|
||||
}
|
||||
@@ -86,6 +90,13 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
cipher.login.totp = field.value;
|
||||
} else if (field.type === "url") {
|
||||
urls.push(field.value);
|
||||
} else if (field.type === ".Android#") {
|
||||
let cleanedValue = field.value.startsWith("androidapp://")
|
||||
? field.value
|
||||
: "androidapp://" + field.value;
|
||||
cleanedValue = cleanedValue.replace("android://", "");
|
||||
cleanedValue = cleanedValue.replace(androidUrlRegex, "androidapp://");
|
||||
urls.push(cleanedValue);
|
||||
} else {
|
||||
this.processKvp(
|
||||
cipher,
|
||||
@@ -98,10 +109,10 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
cipher.login.uris = this.makeUriArray(urls);
|
||||
}
|
||||
|
||||
private processCard(cipher: CipherView, fields: any[]) {
|
||||
private processCard(cipher: CipherView, fields: EnpassField[]) {
|
||||
cipher.card = new CardView();
|
||||
cipher.type = CipherType.Card;
|
||||
fields.forEach((field: any) => {
|
||||
fields.forEach((field) => {
|
||||
if (
|
||||
this.isNullOrWhitespace(field.value) ||
|
||||
field.type === "section" ||
|
||||
@@ -137,8 +148,8 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
});
|
||||
}
|
||||
|
||||
private processNote(cipher: CipherView, fields: any[]) {
|
||||
fields.forEach((field: any) => {
|
||||
private processNote(cipher: CipherView, fields: EnpassField[]) {
|
||||
fields.forEach((field) => {
|
||||
if (this.isNullOrWhitespace(field.value) || field.type === "section") {
|
||||
return;
|
||||
}
|
||||
@@ -151,17 +162,17 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
});
|
||||
}
|
||||
|
||||
private buildFolderTree(folders: any[]): any[] {
|
||||
private buildFolderTree(folders: EnpassFolder[]): EnpassFolderTreeItem[] {
|
||||
if (folders == null) {
|
||||
return [];
|
||||
}
|
||||
const folderTree: any[] = [];
|
||||
const map = new Map<string, any>([]);
|
||||
folders.forEach((obj: any) => {
|
||||
const folderTree: EnpassFolderTreeItem[] = [];
|
||||
const map = new Map<string, EnpassFolderTreeItem>([]);
|
||||
folders.forEach((obj: EnpassFolderTreeItem) => {
|
||||
map.set(obj.uuid, obj);
|
||||
obj.children = [];
|
||||
});
|
||||
folders.forEach((obj: any) => {
|
||||
folders.forEach((obj: EnpassFolderTreeItem) => {
|
||||
if (obj.parent_uuid != null && obj.parent_uuid !== "" && map.has(obj.parent_uuid)) {
|
||||
map.get(obj.parent_uuid).children.push(obj);
|
||||
} else {
|
||||
@@ -171,11 +182,15 @@ export class EnpassJsonImporter extends BaseImporter implements Importer {
|
||||
return folderTree;
|
||||
}
|
||||
|
||||
private flattenFolderTree(titlePrefix: string, tree: any[], map: Map<string, string>) {
|
||||
private flattenFolderTree(
|
||||
titlePrefix: string,
|
||||
tree: EnpassFolderTreeItem[],
|
||||
map: Map<string, string>
|
||||
) {
|
||||
if (tree == null) {
|
||||
return;
|
||||
}
|
||||
tree.forEach((f: any) => {
|
||||
tree.forEach((f) => {
|
||||
if (f.title != null && f.title.trim() !== "") {
|
||||
let title = f.title.trim();
|
||||
if (titlePrefix != null && titlePrefix.trim() !== "") {
|
||||
@@ -0,0 +1,79 @@
|
||||
type Login = "login.default";
|
||||
|
||||
type CreditCard = "creditcard.default";
|
||||
|
||||
type Identity = "identity.default";
|
||||
|
||||
type Note = "note.default";
|
||||
|
||||
type Password = "password.default";
|
||||
|
||||
type Finance =
|
||||
| "finance.stock"
|
||||
| "finance.bankaccount"
|
||||
| "finance.loan"
|
||||
| "finance.mutualfund"
|
||||
| "finance.insurance"
|
||||
| "finance.other";
|
||||
|
||||
type License = "license.driving" | "license.hunting" | "license.software" | "license.other";
|
||||
|
||||
type Travel =
|
||||
| "travel.passport"
|
||||
| "travel.flightdetails"
|
||||
| "travel.hotelreservation"
|
||||
| "travel.visa"
|
||||
| "travel.freqflyer"
|
||||
| "travel.other";
|
||||
|
||||
type Computer =
|
||||
| "computer.database"
|
||||
| "computer.emailaccount"
|
||||
| "computer.ftp"
|
||||
| "computer.messaging"
|
||||
| "computer.internetprovider"
|
||||
| "computer.server"
|
||||
| "computer.wifi"
|
||||
| "computer.hosting"
|
||||
| "computer.other";
|
||||
|
||||
type Misc =
|
||||
| "misc.Aadhar"
|
||||
| "misc.address"
|
||||
| "misc.library"
|
||||
| "misc.rewardprogram"
|
||||
| "misc.lens"
|
||||
| "misc.service"
|
||||
| "misc.vehicleinfo"
|
||||
| "misc.itic"
|
||||
| "misc.itz"
|
||||
| "misc.propertyinfo"
|
||||
| "misc.clothsize"
|
||||
| "misc.contact"
|
||||
| "misc.membership"
|
||||
| "misc.cellphone"
|
||||
| "misc.emergencyno"
|
||||
| "misc.pan"
|
||||
| "misc.identity"
|
||||
| "misc.regcode"
|
||||
| "misc.prescription"
|
||||
| "misc.serial"
|
||||
| "misc.socialsecurityno"
|
||||
| "misc.isic"
|
||||
| "misc.calling"
|
||||
| "misc.voicemail"
|
||||
| "misc.voter"
|
||||
| "misc.combilock"
|
||||
| "misc.other";
|
||||
|
||||
export type EnpassItemTemplate =
|
||||
| Login
|
||||
| CreditCard
|
||||
| Identity
|
||||
| Note
|
||||
| Password
|
||||
| Finance
|
||||
| License
|
||||
| Travel
|
||||
| Computer
|
||||
| Misc;
|
||||
85
libs/common/src/importers/enpass/types/enpass-json-type.ts
Normal file
85
libs/common/src/importers/enpass/types/enpass-json-type.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { EnpassItemTemplate } from "./enpass-item-templates";
|
||||
|
||||
export type EnpassJsonFile = {
|
||||
folders: EnpassFolder[];
|
||||
items: EnpassItem[];
|
||||
};
|
||||
|
||||
export type EnpassFolder = {
|
||||
icon: string;
|
||||
parent_uuid: string;
|
||||
title: string;
|
||||
updated_at: number;
|
||||
uuid: string;
|
||||
};
|
||||
|
||||
export type EnpassItem = {
|
||||
archived: number;
|
||||
auto_submit: number;
|
||||
category: string;
|
||||
createdAt: number;
|
||||
favorite: number;
|
||||
fields?: EnpassField[];
|
||||
icon: Icon;
|
||||
note: string;
|
||||
subtitle: string;
|
||||
template_type: EnpassItemTemplate;
|
||||
title: string;
|
||||
trashed: number;
|
||||
updated_at: number;
|
||||
uuid: string;
|
||||
folders?: string[];
|
||||
};
|
||||
|
||||
export type EnpassFieldType =
|
||||
| "text"
|
||||
| "password"
|
||||
| "pin"
|
||||
| "numeric"
|
||||
| "date"
|
||||
| "email"
|
||||
| "url"
|
||||
| "phone"
|
||||
| "username"
|
||||
| "totp"
|
||||
| "multiline"
|
||||
| "ccName"
|
||||
| "ccNumber"
|
||||
| "ccCvc"
|
||||
| "ccPin"
|
||||
| "ccExpiry"
|
||||
| "ccBankname"
|
||||
| "ccTxnpassword"
|
||||
| "ccType"
|
||||
| "ccValidfrom"
|
||||
| "section"
|
||||
| ".Android#";
|
||||
|
||||
export type EnpassField = {
|
||||
deleted: number;
|
||||
history?: History[];
|
||||
label: string;
|
||||
order: number;
|
||||
sensitive: number;
|
||||
type: EnpassFieldType;
|
||||
uid: number;
|
||||
updated_at: number;
|
||||
value: string;
|
||||
value_updated_at: number;
|
||||
};
|
||||
|
||||
export type History = {
|
||||
updated_at: number;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type Icon = {
|
||||
fav: string;
|
||||
image: Image;
|
||||
type: number;
|
||||
uuid: string;
|
||||
};
|
||||
|
||||
export type Image = {
|
||||
file: string;
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { CipherType } from "../enums/cipherType";
|
||||
import { ImportResult } from "../models/domain/import-result";
|
||||
import { CardView } from "../models/view/card.view";
|
||||
|
||||
import { BaseImporter } from "./base-importer";
|
||||
import { Importer } from "./importer";
|
||||
|
||||
export class FSecureFskImporter extends BaseImporter implements Importer {
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
const result = new ImportResult();
|
||||
const results = JSON.parse(data);
|
||||
if (results == null || results.data == null) {
|
||||
result.success = false;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
for (const key in results.data) {
|
||||
// eslint-disable-next-line
|
||||
if (!results.data.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = results.data[key];
|
||||
const cipher = this.initLoginCipher();
|
||||
cipher.name = this.getValueOrDefault(value.service);
|
||||
cipher.notes = this.getValueOrDefault(value.notes);
|
||||
|
||||
if (value.style === "website" || value.style === "globe") {
|
||||
cipher.login.username = this.getValueOrDefault(value.username);
|
||||
cipher.login.password = this.getValueOrDefault(value.password);
|
||||
cipher.login.uris = this.makeUriArray(value.url);
|
||||
} else if (value.style === "creditcard") {
|
||||
cipher.type = CipherType.Card;
|
||||
cipher.card = new CardView();
|
||||
cipher.card.cardholderName = this.getValueOrDefault(value.username);
|
||||
cipher.card.number = this.getValueOrDefault(value.creditNumber);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.code = this.getValueOrDefault(value.creditCvv);
|
||||
if (!this.isNullOrWhitespace(value.creditExpiry)) {
|
||||
if (!this.setCardExpiration(cipher, value.creditExpiry)) {
|
||||
this.processKvp(cipher, "Expiration", value.creditExpiry);
|
||||
}
|
||||
}
|
||||
if (!this.isNullOrWhitespace(value.password)) {
|
||||
this.processKvp(cipher, "PIN", value.password);
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.convertToNoteIfNeeded(cipher);
|
||||
this.cleanupCipher(cipher);
|
||||
result.ciphers.push(cipher);
|
||||
}
|
||||
|
||||
result.success = true;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { CipherType } from "../../enums/cipherType";
|
||||
|
||||
import { FSecureFskImporter as Importer } from "./fsecure-fsk-importer";
|
||||
import { CreditCardTestEntry, LoginTestEntry } from "./fsk-test-data";
|
||||
|
||||
describe("FSecure FSK Importer", () => {
|
||||
it("should import data of type login", async () => {
|
||||
const importer = new Importer();
|
||||
const LoginTestEntryStringified = JSON.stringify(LoginTestEntry);
|
||||
const result = await importer.parse(LoginTestEntryStringified);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
|
||||
expect(cipher.name).toEqual("example.com");
|
||||
expect(cipher.favorite).toBe(true);
|
||||
expect(cipher.notes).toEqual("some note for example.com");
|
||||
|
||||
expect(cipher.type).toBe(CipherType.Login);
|
||||
expect(cipher.login.username).toEqual("jdoe");
|
||||
expect(cipher.login.password).toEqual("somePassword");
|
||||
|
||||
expect(cipher.login.uris.length).toEqual(1);
|
||||
const uriView = cipher.login.uris.shift();
|
||||
expect(uriView.uri).toEqual("https://www.example.com");
|
||||
});
|
||||
|
||||
it("should import data of type creditCard", async () => {
|
||||
const importer = new Importer();
|
||||
const CreditCardTestEntryStringified = JSON.stringify(CreditCardTestEntry);
|
||||
const result = await importer.parse(CreditCardTestEntryStringified);
|
||||
expect(result != null).toBe(true);
|
||||
|
||||
const cipher = result.ciphers.shift();
|
||||
|
||||
expect(cipher.name).toEqual("My credit card");
|
||||
expect(cipher.favorite).toBe(false);
|
||||
expect(cipher.notes).toEqual("some notes to my card");
|
||||
|
||||
expect(cipher.type).toBe(CipherType.Card);
|
||||
expect(cipher.card.cardholderName).toEqual("John Doe");
|
||||
expect(cipher.card.number).toEqual("4242424242424242");
|
||||
expect(cipher.card.code).toEqual("123");
|
||||
|
||||
expect(cipher.fields.length).toBe(2);
|
||||
expect(cipher.fields[0].name).toEqual("Expiration");
|
||||
expect(cipher.fields[0].value).toEqual("22.10.2026");
|
||||
|
||||
expect(cipher.fields[1].name).toEqual("PIN");
|
||||
expect(cipher.fields[1].value).toEqual("1234");
|
||||
});
|
||||
});
|
||||
79
libs/common/src/importers/fsecure/fsecure-fsk-importer.ts
Normal file
79
libs/common/src/importers/fsecure/fsecure-fsk-importer.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { CipherType } from "../../enums/cipherType";
|
||||
import { ImportResult } from "../../models/domain/import-result";
|
||||
import { CardView } from "../../models/view/card.view";
|
||||
import { CipherView } from "../../models/view/cipher.view";
|
||||
import { BaseImporter } from "../base-importer";
|
||||
import { Importer } from "../importer";
|
||||
|
||||
import { FskEntry, FskEntryTypesEnum, FskFile } from "./fsecure-fsk-types";
|
||||
|
||||
export class FSecureFskImporter extends BaseImporter implements Importer {
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
const result = new ImportResult();
|
||||
const results: FskFile = JSON.parse(data);
|
||||
if (results == null || results.data == null) {
|
||||
result.success = false;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
for (const key in results.data) {
|
||||
// eslint-disable-next-line
|
||||
if (!results.data.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = results.data[key];
|
||||
const cipher = this.parseEntry(value);
|
||||
result.ciphers.push(cipher);
|
||||
}
|
||||
|
||||
result.success = true;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
private parseEntry(entry: FskEntry): CipherView {
|
||||
const cipher = this.initLoginCipher();
|
||||
cipher.name = this.getValueOrDefault(entry.service);
|
||||
cipher.notes = this.getValueOrDefault(entry.notes);
|
||||
cipher.favorite = entry.favorite > 0;
|
||||
|
||||
switch (entry.type) {
|
||||
case FskEntryTypesEnum.Login:
|
||||
this.handleLoginEntry(entry, cipher);
|
||||
break;
|
||||
case FskEntryTypesEnum.CreditCard:
|
||||
this.handleCreditCardEntry(entry, cipher);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
break;
|
||||
}
|
||||
|
||||
this.convertToNoteIfNeeded(cipher);
|
||||
this.cleanupCipher(cipher);
|
||||
return cipher;
|
||||
}
|
||||
|
||||
private handleLoginEntry(entry: FskEntry, cipher: CipherView) {
|
||||
cipher.login.username = this.getValueOrDefault(entry.username);
|
||||
cipher.login.password = this.getValueOrDefault(entry.password);
|
||||
cipher.login.uris = this.makeUriArray(entry.url);
|
||||
}
|
||||
|
||||
private handleCreditCardEntry(entry: FskEntry, cipher: CipherView) {
|
||||
cipher.type = CipherType.Card;
|
||||
cipher.card = new CardView();
|
||||
cipher.card.cardholderName = this.getValueOrDefault(entry.username);
|
||||
cipher.card.number = this.getValueOrDefault(entry.creditNumber);
|
||||
cipher.card.brand = this.getCardBrand(cipher.card.number);
|
||||
cipher.card.code = this.getValueOrDefault(entry.creditCvv);
|
||||
if (!this.isNullOrWhitespace(entry.creditExpiry)) {
|
||||
if (!this.setCardExpiration(cipher, entry.creditExpiry)) {
|
||||
this.processKvp(cipher, "Expiration", entry.creditExpiry);
|
||||
}
|
||||
}
|
||||
if (!this.isNullOrWhitespace(entry.password)) {
|
||||
this.processKvp(cipher, "PIN", entry.password);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
libs/common/src/importers/fsecure/fsecure-fsk-types.ts
Normal file
37
libs/common/src/importers/fsecure/fsecure-fsk-types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface FskFile {
|
||||
data: Data;
|
||||
}
|
||||
|
||||
export interface Data {
|
||||
[key: string]: FskEntry;
|
||||
}
|
||||
|
||||
export enum FskEntryTypesEnum {
|
||||
Login = 1,
|
||||
CreditCard = 2,
|
||||
}
|
||||
|
||||
export interface FskEntry {
|
||||
color: string;
|
||||
creditCvv: string;
|
||||
creditExpiry: string;
|
||||
creditNumber: string;
|
||||
favorite: number; // UNIX timestamp
|
||||
notes: string;
|
||||
password: string;
|
||||
passwordList: PasswordList[];
|
||||
passwordModifiedDate: number; // UNIX timestamp
|
||||
rev: string | number;
|
||||
service: string;
|
||||
style: string;
|
||||
type: FskEntryTypesEnum;
|
||||
url: string;
|
||||
username: string;
|
||||
createdDate: number; // UNIX timestamp
|
||||
modifiedDate: number; // UNIX timestamp
|
||||
}
|
||||
|
||||
export interface PasswordList {
|
||||
changedate: string;
|
||||
password: string;
|
||||
}
|
||||
49
libs/common/src/importers/fsecure/fsk-test-data.ts
Normal file
49
libs/common/src/importers/fsecure/fsk-test-data.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { FskFile } from "./fsecure-fsk-types";
|
||||
|
||||
export const LoginTestEntry: FskFile = {
|
||||
data: {
|
||||
"1c3a2e31dcaa8459edd70a9d895ce298": {
|
||||
color: "#00A34D",
|
||||
createdDate: 0,
|
||||
creditCvv: "",
|
||||
creditExpiry: "",
|
||||
creditNumber: "",
|
||||
favorite: 1666440874,
|
||||
modifiedDate: 0,
|
||||
notes: "some note for example.com",
|
||||
password: "somePassword",
|
||||
passwordList: [],
|
||||
passwordModifiedDate: 0,
|
||||
rev: 1,
|
||||
service: "example.com",
|
||||
style: "website",
|
||||
type: 1,
|
||||
url: "https://www.example.com",
|
||||
username: "jdoe",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CreditCardTestEntry: FskFile = {
|
||||
data: {
|
||||
"156498a46a3254f16035cbbbd09c2b8f": {
|
||||
color: "#00baff",
|
||||
createdDate: 1666438977,
|
||||
creditCvv: "123",
|
||||
creditExpiry: "22.10.2026",
|
||||
creditNumber: "4242424242424242",
|
||||
favorite: 0,
|
||||
modifiedDate: 1666438977,
|
||||
notes: "some notes to my card",
|
||||
password: "1234",
|
||||
passwordList: [],
|
||||
passwordModifiedDate: 1666438977,
|
||||
rev: 1,
|
||||
service: "My credit card",
|
||||
style: "creditcard",
|
||||
type: 2,
|
||||
url: "mybank",
|
||||
username: "John Doe",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -15,7 +15,23 @@ export class KeePass2XmlImporter extends BaseImporter implements Importer {
|
||||
return Promise.resolve(this.result);
|
||||
}
|
||||
|
||||
const rootGroup = doc.querySelector("KeePassFile > Root > Group");
|
||||
//Note: The doc.querySelector("KeePassFile > Root > Group") no longers works on node and we have to breakdown the query by nodes
|
||||
const KeePassFileNode = doc.querySelector("KeePassFile");
|
||||
|
||||
if (KeePassFileNode == null) {
|
||||
this.result.errorMessage = "Missing `KeePassFile` node.";
|
||||
this.result.success = false;
|
||||
return Promise.resolve(this.result);
|
||||
}
|
||||
|
||||
const RootNode = KeePassFileNode.querySelector("Root");
|
||||
if (RootNode == null) {
|
||||
this.result.errorMessage = "Missing `KeePassFile > Root` node.";
|
||||
this.result.success = false;
|
||||
return Promise.resolve(this.result);
|
||||
}
|
||||
|
||||
const rootGroup = RootNode.querySelector("Group");
|
||||
if (rootGroup == null) {
|
||||
this.result.errorMessage = "Missing `KeePassFile > Root > Group` node.";
|
||||
this.result.success = false;
|
||||
|
||||
@@ -18,7 +18,12 @@ export class KeeperCsvImporter extends BaseImporter implements Importer {
|
||||
|
||||
this.processFolder(result, value[0]);
|
||||
const cipher = this.initLoginCipher();
|
||||
cipher.notes = this.getValueOrDefault(value[5]) + "\n";
|
||||
|
||||
const notes = this.getValueOrDefault(value[5]);
|
||||
if (notes) {
|
||||
cipher.notes = `${notes}\n`;
|
||||
}
|
||||
|
||||
cipher.name = this.getValueOrDefault(value[1], "--");
|
||||
cipher.login.username = this.getValueOrDefault(value[2]);
|
||||
cipher.login.password = this.getValueOrDefault(value[3]);
|
||||
@@ -27,7 +32,11 @@ export class KeeperCsvImporter extends BaseImporter implements Importer {
|
||||
if (value.length > 7) {
|
||||
// we have some custom fields.
|
||||
for (let i = 7; i < value.length; i = i + 2) {
|
||||
this.processKvp(cipher, value[i], value[i + 1]);
|
||||
if (value[i] == "TFC:Keeper") {
|
||||
cipher.login.totp = value[i + 1];
|
||||
} else {
|
||||
this.processKvp(cipher, value[i], value[i + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
43
libs/common/src/importers/passky/passky-json-importer.ts
Normal file
43
libs/common/src/importers/passky/passky-json-importer.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ImportResult } from "../../models/domain/import-result";
|
||||
import { BaseImporter } from "../base-importer";
|
||||
import { Importer } from "../importer";
|
||||
|
||||
import { PasskyJsonExport } from "./passky-json-types";
|
||||
|
||||
export class PasskyJsonImporter extends BaseImporter implements Importer {
|
||||
parse(data: string): Promise<ImportResult> {
|
||||
const result = new ImportResult();
|
||||
const passkyExport: PasskyJsonExport = JSON.parse(data);
|
||||
if (
|
||||
passkyExport == null ||
|
||||
passkyExport.passwords == null ||
|
||||
passkyExport.passwords.length === 0
|
||||
) {
|
||||
result.success = false;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
if (passkyExport.encrypted == true) {
|
||||
result.success = false;
|
||||
result.errorMessage = "Unable to import an encrypted passky backup.";
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
|
||||
passkyExport.passwords.forEach((record) => {
|
||||
const cipher = this.initLoginCipher();
|
||||
cipher.name = record.website;
|
||||
cipher.login.username = record.username;
|
||||
cipher.login.password = record.password;
|
||||
|
||||
cipher.login.uris = this.makeUriArray(record.website);
|
||||
cipher.notes = record.message;
|
||||
|
||||
this.convertToNoteIfNeeded(cipher);
|
||||
this.cleanupCipher(cipher);
|
||||
result.ciphers.push(cipher);
|
||||
});
|
||||
|
||||
result.success = true;
|
||||
return Promise.resolve(result);
|
||||
}
|
||||
}
|
||||
11
libs/common/src/importers/passky/passky-json-types.ts
Normal file
11
libs/common/src/importers/passky/passky-json-types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface PasskyJsonExport {
|
||||
encrypted: boolean;
|
||||
passwords: LoginEntry[];
|
||||
}
|
||||
|
||||
export interface LoginEntry {
|
||||
website: string;
|
||||
username: string;
|
||||
password: string;
|
||||
message: string;
|
||||
}
|
||||
@@ -50,6 +50,9 @@ export abstract class LogInStrategy {
|
||||
| PasswordlessLogInCredentials
|
||||
): Promise<AuthResult>;
|
||||
|
||||
// The user key comes from different sources depending on the login strategy
|
||||
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactor: TokenTwoFactorRequest,
|
||||
captchaResponse: string = null
|
||||
@@ -74,11 +77,6 @@ export abstract class LogInStrategy {
|
||||
throw new Error("Invalid response object.");
|
||||
}
|
||||
|
||||
protected onSuccessfulLogin(response: IdentityTokenResponse): Promise<void> {
|
||||
// Implemented in subclass if required
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async buildDeviceRequest() {
|
||||
const appId = await this.appIdService.getAppId();
|
||||
return new DeviceRequest(appId, this.platformUtilsService);
|
||||
@@ -134,6 +132,9 @@ export abstract class LogInStrategy {
|
||||
await this.tokenService.setTwoFactorToken(response);
|
||||
}
|
||||
|
||||
await this.setUserKey(response);
|
||||
|
||||
// Must come after the user Key is set, otherwise createKeyPairForOldAccount will fail
|
||||
const newSsoUser = response.key == null;
|
||||
if (!newSsoUser) {
|
||||
await this.cryptoService.setEncKey(response.key);
|
||||
@@ -142,8 +143,6 @@ export abstract class LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
await this.onSuccessfulLogin(response);
|
||||
|
||||
this.messagingService.send("loggedIn");
|
||||
|
||||
return result;
|
||||
|
||||
@@ -56,7 +56,7 @@ export class PasswordLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async onSuccessfulLogin() {
|
||||
async setUserKey() {
|
||||
await this.cryptoService.setKey(this.key);
|
||||
await this.cryptoService.setKeyHash(this.localHashedPassword);
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class PasswordlessLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async onSuccessfulLogin() {
|
||||
async setUserKey() {
|
||||
await this.cryptoService.setKey(this.passwordlessCredentials.decKey);
|
||||
await this.cryptoService.setKeyHash(this.passwordlessCredentials.localPasswordHash);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ export class SsoLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async onSuccessfulLogin(tokenResponse: IdentityTokenResponse) {
|
||||
async setUserKey(tokenResponse: IdentityTokenResponse) {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class UserApiLogInStrategy extends LogInStrategy {
|
||||
);
|
||||
}
|
||||
|
||||
async onSuccessfulLogin(tokenResponse: IdentityTokenResponse) {
|
||||
async setUserKey(tokenResponse: IdentityTokenResponse) {
|
||||
if (tokenResponse.apiUseKeyConnector) {
|
||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
|
||||
|
||||
72
libs/common/src/misc/serviceUtils.spec.ts
Normal file
72
libs/common/src/misc/serviceUtils.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { ITreeNodeObject, TreeNode } from "../models/domain/tree-node";
|
||||
|
||||
import { ServiceUtils } from "./serviceUtils";
|
||||
|
||||
type FakeObject = { id: string; name: string };
|
||||
|
||||
describe("serviceUtils", () => {
|
||||
let nodeTree: TreeNode<FakeObject>[];
|
||||
beforeEach(() => {
|
||||
nodeTree = [
|
||||
createTreeNode({ id: "1", name: "1" }, [
|
||||
createTreeNode({ id: "1.1", name: "1.1" }, [
|
||||
createTreeNode({ id: "1.1.1", name: "1.1.1" }),
|
||||
]),
|
||||
createTreeNode({ id: "1.2", name: "1.2" }),
|
||||
])(null),
|
||||
createTreeNode({ id: "2", name: "2" }, [createTreeNode({ id: "2.1", name: "2.1" })])(null),
|
||||
createTreeNode({ id: "3", name: "3" }, [])(null),
|
||||
];
|
||||
});
|
||||
|
||||
describe("nestedTraverse", () => {
|
||||
it("should traverse a tree and add a node at the correct position given a valid path", () => {
|
||||
const nodeToBeAdded: FakeObject = { id: "1.2.1", name: "1.2.1" };
|
||||
const path = ["1", "1.2", "1.2.1"];
|
||||
|
||||
ServiceUtils.nestedTraverse(nodeTree, 0, path, nodeToBeAdded, null, "/");
|
||||
expect(nodeTree[0].children[1].children[0].node).toEqual(nodeToBeAdded);
|
||||
});
|
||||
|
||||
it("should combine the path for missing nodes and use as the added node name given an invalid path", () => {
|
||||
const nodeToBeAdded: FakeObject = { id: "blank", name: "blank" };
|
||||
const path = ["3", "3.1", "3.1.1"];
|
||||
|
||||
ServiceUtils.nestedTraverse(nodeTree, 0, path, nodeToBeAdded, null, "/");
|
||||
expect(nodeTree[2].children[0].node.name).toEqual("3.1/3.1.1");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTreeNodeObject", () => {
|
||||
it("should return a matching node given a single tree branch and a valid id", () => {
|
||||
const id = "1.1.1";
|
||||
const given = ServiceUtils.getTreeNodeObject(nodeTree[0], id);
|
||||
expect(given.node.id).toEqual(id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTreeNodeObjectFromList", () => {
|
||||
it("should return a matching node given a list of branches and a valid id", () => {
|
||||
const id = "1.1.1";
|
||||
const given = ServiceUtils.getTreeNodeObjectFromList(nodeTree, id);
|
||||
expect(given.node.id).toEqual(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type TreeNodeFactory<T extends ITreeNodeObject> = (
|
||||
obj: T,
|
||||
children?: TreeNodeFactoryWithoutParent<T>[]
|
||||
) => TreeNodeFactoryWithoutParent<T>;
|
||||
|
||||
type TreeNodeFactoryWithoutParent<T extends ITreeNodeObject> = (
|
||||
parent?: TreeNode<T>
|
||||
) => TreeNode<T>;
|
||||
|
||||
const createTreeNode: TreeNodeFactory<FakeObject> =
|
||||
(obj, children = []) =>
|
||||
(parent) => {
|
||||
const node = new TreeNode<FakeObject>(obj, parent, obj.name, obj.id);
|
||||
node.children = children.map((childFunc) => childFunc(node));
|
||||
return node;
|
||||
};
|
||||
@@ -1,47 +1,62 @@
|
||||
import { ITreeNodeObject, TreeNode } from "../models/domain/tree-node";
|
||||
|
||||
export class ServiceUtils {
|
||||
/**
|
||||
* Recursively adds a node to nodeTree
|
||||
* @param {TreeNode<ITreeNodeObject>[]} nodeTree - An array of TreeNodes that the node will be added to
|
||||
* @param {number} partIndex - Index of the `parts` array that is being processed
|
||||
* @param {string[]} parts - Array of strings that represent the path to the `obj` node
|
||||
* @param {ITreeNodeObject} obj - The node to be added to the tree
|
||||
* @param {ITreeNodeObject} parent - The parent node of the `obj` node
|
||||
* @param {string} delimiter - The delimiter used to split the path string, will be used to combine the path for missing nodes
|
||||
*/
|
||||
static nestedTraverse(
|
||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||
partIndex: number,
|
||||
parts: string[],
|
||||
obj: ITreeNodeObject,
|
||||
parent: ITreeNodeObject,
|
||||
parent: TreeNode<ITreeNodeObject> | undefined,
|
||||
delimiter: string
|
||||
) {
|
||||
if (parts.length <= partIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const end = partIndex === parts.length - 1;
|
||||
const partName = parts[partIndex];
|
||||
const end: boolean = partIndex === parts.length - 1;
|
||||
const partName: string = parts[partIndex];
|
||||
|
||||
for (let i = 0; i < nodeTree.length; i++) {
|
||||
if (nodeTree[i].node.name !== parts[partIndex]) {
|
||||
if (nodeTree[i].node.name !== partName) {
|
||||
continue;
|
||||
}
|
||||
if (end && nodeTree[i].node.id !== obj.id) {
|
||||
// Another node with the same name.
|
||||
nodeTree.push(new TreeNode(obj, partName, parent));
|
||||
// Another node exists with the same name as the node being added
|
||||
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||
return;
|
||||
}
|
||||
// Move down the tree to the next level
|
||||
ServiceUtils.nestedTraverse(
|
||||
nodeTree[i].children,
|
||||
partIndex + 1,
|
||||
parts,
|
||||
obj,
|
||||
nodeTree[i].node,
|
||||
nodeTree[i],
|
||||
delimiter
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's no node here with the same name...
|
||||
if (nodeTree.filter((n) => n.node.name === partName).length === 0) {
|
||||
// And we're at the end of the path given, add the node
|
||||
if (end) {
|
||||
nodeTree.push(new TreeNode(obj, partName, parent));
|
||||
nodeTree.push(new TreeNode(obj, parent, partName));
|
||||
return;
|
||||
}
|
||||
const newPartName = parts[partIndex] + delimiter + parts[partIndex + 1];
|
||||
// And we're not at the end of the path, combine the current name with the next name
|
||||
// 1, *1.2, 1.2.1 becomes
|
||||
// 1, *1.2/1.2.1
|
||||
const newPartName = partName + delimiter + parts[partIndex + 1];
|
||||
ServiceUtils.nestedTraverse(
|
||||
nodeTree,
|
||||
0,
|
||||
@@ -53,7 +68,37 @@ export class ServiceUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a tree for a node with a matching `id`
|
||||
* @param {TreeNode<ITreeNodeObject>} nodeTree - A single TreeNode branch that will be searched
|
||||
* @param {string} id - The id of the node to be found
|
||||
* @returns {TreeNode<ITreeNodeObject>} The node with a matching `id`
|
||||
*/
|
||||
static getTreeNodeObject(
|
||||
nodeTree: TreeNode<ITreeNodeObject>,
|
||||
id: string
|
||||
): TreeNode<ITreeNodeObject> {
|
||||
if (nodeTree.node.id === id) {
|
||||
return nodeTree;
|
||||
}
|
||||
for (let i = 0; i < nodeTree.children.length; i++) {
|
||||
if (nodeTree.children[i].children != null) {
|
||||
const node = ServiceUtils.getTreeNodeObject(nodeTree.children[i], id);
|
||||
if (node !== null) {
|
||||
return node;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches an array of tree nodes for a node with a matching `id`
|
||||
* @param {TreeNode<ITreeNodeObject>} nodeTree - An array of TreeNode branches that will be searched
|
||||
* @param {string} id - The id of the node to be found
|
||||
* @returns {TreeNode<ITreeNodeObject>} The node with a matching `id`
|
||||
*/
|
||||
static getTreeNodeObjectFromList(
|
||||
nodeTree: TreeNode<ITreeNodeObject>[],
|
||||
id: string
|
||||
): TreeNode<ITreeNodeObject> {
|
||||
@@ -61,7 +106,7 @@ export class ServiceUtils {
|
||||
if (nodeTree[i].node.id === id) {
|
||||
return nodeTree[i];
|
||||
} else if (nodeTree[i].children != null) {
|
||||
const node = ServiceUtils.getTreeNodeObject(nodeTree[i].children, id);
|
||||
const node = ServiceUtils.getTreeNodeObjectFromList(nodeTree[i].children, id);
|
||||
if (node !== null) {
|
||||
return node;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
import { getHostname, parse } from "tldts";
|
||||
import { Merge } from "type-fest";
|
||||
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { EncryptService } from "../abstractions/encrypt.service";
|
||||
@@ -55,6 +56,10 @@ export class Utils {
|
||||
}
|
||||
|
||||
static fromB64ToArray(str: string): Uint8Array {
|
||||
if (str == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Utils.isNode) {
|
||||
return new Uint8Array(Buffer.from(str, "base64"));
|
||||
} else {
|
||||
@@ -108,6 +113,9 @@ export class Utils {
|
||||
}
|
||||
|
||||
static fromBufferToB64(buffer: ArrayBuffer): string {
|
||||
if (buffer == null) {
|
||||
return null;
|
||||
}
|
||||
if (Utils.isNode) {
|
||||
return Buffer.from(buffer).toString("base64");
|
||||
} else {
|
||||
@@ -423,6 +431,73 @@ export class Utils {
|
||||
return this.global.bitwardenContainerService;
|
||||
}
|
||||
|
||||
static validateHexColor(color: string) {
|
||||
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts map to a Record<string, V> with the same data. Inverse of recordToMap
|
||||
* Useful in toJSON methods, since Maps are not serializable
|
||||
* @param map
|
||||
* @returns
|
||||
*/
|
||||
static mapToRecord<K extends string | number, V>(map: Map<K, V>): Record<string, V> {
|
||||
if (map == null) {
|
||||
return null;
|
||||
}
|
||||
if (!(map instanceof Map)) {
|
||||
return map;
|
||||
}
|
||||
return Object.fromEntries(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts record to a Map<string, V> with the same data. Inverse of mapToRecord
|
||||
* Useful in fromJSON methods, since Maps are not serializable
|
||||
*
|
||||
* Warning: If the record has string keys that are numbers, they will be converted to numbers in the map
|
||||
* @param record
|
||||
* @returns
|
||||
*/
|
||||
static recordToMap<K extends string | number, V>(record: Record<K, V>): Map<K, V> {
|
||||
if (record == null) {
|
||||
return null;
|
||||
} else if (record instanceof Map) {
|
||||
return record;
|
||||
}
|
||||
|
||||
const entries = Object.entries(record);
|
||||
if (entries.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
if (isNaN(Number(entries[0][0]))) {
|
||||
return new Map(entries) as Map<K, V>;
|
||||
} else {
|
||||
return new Map(entries.map((e) => [Number(e[0]), e[1]])) as Map<K, V>;
|
||||
}
|
||||
}
|
||||
|
||||
/** Applies Object.assign, but converts the type nicely using Type-Fest Merge<Destination, Source> */
|
||||
static merge<Destination, Source>(
|
||||
destination: Destination,
|
||||
source: Source
|
||||
): Merge<Destination, Source> {
|
||||
return Object.assign(destination, source) as unknown as Merge<Destination, Source>;
|
||||
}
|
||||
|
||||
/**
|
||||
* encodeURIComponent escapes all characters except the following:
|
||||
* alphabetic, decimal digits, - _ . ! ~ * ' ( )
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
|
||||
*/
|
||||
static encodeRFC3986URIComponent(str: string): string {
|
||||
return encodeURIComponent(str).replace(
|
||||
/[!'()*]/g,
|
||||
(c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
||||
);
|
||||
}
|
||||
|
||||
private static isMobile(win: Window) {
|
||||
let mobile = false;
|
||||
((a) => {
|
||||
@@ -440,6 +515,10 @@ export class Utils {
|
||||
return mobile || win.navigator.userAgent.match(/iPad/i) != null;
|
||||
}
|
||||
|
||||
static delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
private static isAppleMobile(win: Window) {
|
||||
return (
|
||||
win.navigator.userAgent.match(/iPhone/i) != null ||
|
||||
|
||||
@@ -4,19 +4,9 @@ export class PermissionsApi extends BaseResponse {
|
||||
accessEventLogs: boolean;
|
||||
accessImportExport: boolean;
|
||||
accessReports: boolean;
|
||||
/**
|
||||
* @deprecated Sep 29 2021: This permission has been split out to `createNewCollections`, `editAnyCollection`, and
|
||||
* `deleteAnyCollection`. It exists here for backwards compatibility with Server versions <= 1.43.0
|
||||
*/
|
||||
manageAllCollections: boolean;
|
||||
createNewCollections: boolean;
|
||||
editAnyCollection: boolean;
|
||||
deleteAnyCollection: boolean;
|
||||
/**
|
||||
* @deprecated Sep 29 2021: This permission has been split out to `editAssignedCollections` and
|
||||
* `deleteAssignedCollections`. It exists here for backwards compatibility with Server versions <= 1.43.0
|
||||
*/
|
||||
manageAssignedCollections: boolean;
|
||||
editAssignedCollections: boolean;
|
||||
deleteAssignedCollections: boolean;
|
||||
manageCiphers: boolean;
|
||||
@@ -36,10 +26,6 @@ export class PermissionsApi extends BaseResponse {
|
||||
this.accessImportExport = this.getResponseProperty("AccessImportExport");
|
||||
this.accessReports = this.getResponseProperty("AccessReports");
|
||||
|
||||
// For backwards compatibility with Server <= 1.43.0
|
||||
this.manageAllCollections = this.getResponseProperty("ManageAllCollections");
|
||||
this.manageAssignedCollections = this.getResponseProperty("ManageAssignedCollections");
|
||||
|
||||
this.createNewCollections = this.getResponseProperty("CreateNewCollections");
|
||||
this.editAnyCollection = this.getResponseProperty("EditAnyCollection");
|
||||
this.deleteAnyCollection = this.getResponseProperty("DeleteAnyCollection");
|
||||
|
||||
@@ -20,7 +20,9 @@ export class OrganizationData {
|
||||
useSso: boolean;
|
||||
useKeyConnector: boolean;
|
||||
useScim: boolean;
|
||||
useCustomPermissions: boolean;
|
||||
useResetPassword: boolean;
|
||||
useSecretsManager: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
seats: number;
|
||||
@@ -60,7 +62,9 @@ export class OrganizationData {
|
||||
this.useSso = response.useSso;
|
||||
this.useKeyConnector = response.useKeyConnector;
|
||||
this.useScim = response.useScim;
|
||||
this.useCustomPermissions = response.useCustomPermissions;
|
||||
this.useResetPassword = response.useResetPassword;
|
||||
this.useSecretsManager = response.useSecretsManager;
|
||||
this.selfHost = response.selfHost;
|
||||
this.usersGetPremium = response.usersGetPremium;
|
||||
this.seats = response.seats;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Except, Jsonify } from "type-fest";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { AuthenticationStatus } from "../../enums/authenticationStatus";
|
||||
import { KdfType } from "../../enums/kdfType";
|
||||
@@ -40,7 +40,7 @@ export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
}
|
||||
|
||||
static fromJSON<TEncrypted, TDecrypted>(
|
||||
obj: Jsonify<EncryptionPair<Jsonify<TEncrypted>, Jsonify<TDecrypted>>>,
|
||||
obj: { encrypted?: Jsonify<TEncrypted>; decrypted?: string | Jsonify<TDecrypted> },
|
||||
decryptedFromJson?: (decObj: Jsonify<TDecrypted> | string) => TDecrypted,
|
||||
encryptedFromJson?: (encObj: Jsonify<TEncrypted>) => TEncrypted
|
||||
) {
|
||||
@@ -123,7 +123,7 @@ export class AccountKeys {
|
||||
apiKeyClientSecret?: string;
|
||||
|
||||
toJSON() {
|
||||
return Object.assign(this as Except<AccountKeys, "publicKey">, {
|
||||
return Utils.merge(this, {
|
||||
publicKey: Utils.fromBufferToByteString(this.publicKey),
|
||||
});
|
||||
}
|
||||
@@ -233,6 +233,7 @@ export class AccountSettings {
|
||||
vaultTimeout?: number;
|
||||
vaultTimeoutAction?: string = "lock";
|
||||
serverConfig?: ServerConfigData;
|
||||
avatarColor?: string;
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||
if (obj == null) {
|
||||
@@ -251,7 +252,7 @@ export class AccountSettings {
|
||||
}
|
||||
|
||||
export type AccountSettingsSettings = {
|
||||
equivalentDomains?: { [id: string]: any };
|
||||
equivalentDomains?: string[][];
|
||||
};
|
||||
|
||||
export class AccountTokens {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType";
|
||||
import { OrganizationUserType } from "../../enums/organizationUserType";
|
||||
import { ProductType } from "../../enums/productType";
|
||||
@@ -20,7 +22,9 @@ export class Organization {
|
||||
useSso: boolean;
|
||||
useKeyConnector: boolean;
|
||||
useScim: boolean;
|
||||
useCustomPermissions: boolean;
|
||||
useResetPassword: boolean;
|
||||
useSecretsManager: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
seats: number;
|
||||
@@ -64,7 +68,9 @@ export class Organization {
|
||||
this.useSso = obj.useSso;
|
||||
this.useKeyConnector = obj.useKeyConnector;
|
||||
this.useScim = obj.useScim;
|
||||
this.useCustomPermissions = obj.useCustomPermissions;
|
||||
this.useResetPassword = obj.useResetPassword;
|
||||
this.useSecretsManager = obj.useSecretsManager;
|
||||
this.selfHost = obj.selfHost;
|
||||
this.usersGetPremium = obj.usersGetPremium;
|
||||
this.seats = obj.seats;
|
||||
@@ -125,23 +131,19 @@ export class Organization {
|
||||
}
|
||||
|
||||
get canCreateNewCollections() {
|
||||
return (
|
||||
this.isManager ||
|
||||
(this.permissions.createNewCollections ?? this.permissions.manageAllCollections)
|
||||
);
|
||||
return this.isManager || this.permissions.createNewCollections;
|
||||
}
|
||||
|
||||
get canEditAnyCollection() {
|
||||
return (
|
||||
this.isAdmin || (this.permissions.editAnyCollection ?? this.permissions.manageAllCollections)
|
||||
);
|
||||
return this.isAdmin || this.permissions.editAnyCollection;
|
||||
}
|
||||
|
||||
get canUseAdminCollections() {
|
||||
return this.canEditAnyCollection;
|
||||
}
|
||||
|
||||
get canDeleteAnyCollection() {
|
||||
return (
|
||||
this.isAdmin ||
|
||||
(this.permissions.deleteAnyCollection ?? this.permissions.manageAllCollections)
|
||||
);
|
||||
return this.isAdmin || this.permissions.deleteAnyCollection;
|
||||
}
|
||||
|
||||
get canViewAllCollections() {
|
||||
@@ -149,17 +151,11 @@ export class Organization {
|
||||
}
|
||||
|
||||
get canEditAssignedCollections() {
|
||||
return (
|
||||
this.isManager ||
|
||||
(this.permissions.editAssignedCollections ?? this.permissions.manageAssignedCollections)
|
||||
);
|
||||
return this.isManager || this.permissions.editAssignedCollections;
|
||||
}
|
||||
|
||||
get canDeleteAssignedCollections() {
|
||||
return (
|
||||
this.isManager ||
|
||||
(this.permissions.deleteAssignedCollections ?? this.permissions.manageAssignedCollections)
|
||||
);
|
||||
return this.isManager || this.permissions.deleteAssignedCollections;
|
||||
}
|
||||
|
||||
get canViewAssignedCollections() {
|
||||
@@ -201,4 +197,19 @@ export class Organization {
|
||||
get hasProvider() {
|
||||
return this.providerId != null || this.providerName != null;
|
||||
}
|
||||
|
||||
get canAccessSecretsManager() {
|
||||
return this.useSecretsManager;
|
||||
}
|
||||
|
||||
static fromJSON(json: Jsonify<Organization>) {
|
||||
if (json == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.assign(new Organization(), json, {
|
||||
familySponsorshipLastSyncDate: new Date(json.familySponsorshipLastSyncDate),
|
||||
familySponsorshipValidUntil: new Date(json.familySponsorshipValidUntil),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,22 +4,25 @@ import { State } from "./state";
|
||||
describe("state", () => {
|
||||
describe("fromJSON", () => {
|
||||
it("should deserialize to an instance of itself", () => {
|
||||
expect(State.fromJSON({})).toBeInstanceOf(State);
|
||||
expect(State.fromJSON({}, () => new Account({}))).toBeInstanceOf(State);
|
||||
});
|
||||
|
||||
it("should always assign an object to accounts", () => {
|
||||
const state = State.fromJSON({});
|
||||
const state = State.fromJSON({}, () => new Account({}));
|
||||
expect(state.accounts).not.toBeNull();
|
||||
expect(state.accounts).toEqual({});
|
||||
});
|
||||
|
||||
it("should build an account map", () => {
|
||||
const accountsSpy = jest.spyOn(Account, "fromJSON");
|
||||
const state = State.fromJSON({
|
||||
accounts: {
|
||||
userId: {},
|
||||
const state = State.fromJSON(
|
||||
{
|
||||
accounts: {
|
||||
userId: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
Account.fromJSON
|
||||
);
|
||||
|
||||
expect(state.accounts["userId"]).toBeInstanceOf(Account);
|
||||
expect(accountsSpy).toHaveBeenCalled();
|
||||
|
||||
@@ -19,26 +19,28 @@ export class State<
|
||||
|
||||
// TODO, make Jsonify<State,TGlobalState,TAccount> work. It currently doesn't because Globals doesn't implement Jsonify.
|
||||
static fromJSON<TGlobalState extends GlobalState, TAccount extends Account>(
|
||||
obj: any
|
||||
obj: any,
|
||||
accountDeserializer: (json: Jsonify<TAccount>) => TAccount
|
||||
): State<TGlobalState, TAccount> {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.assign(new State(null), obj, {
|
||||
accounts: State.buildAccountMapFromJSON(obj?.accounts),
|
||||
accounts: State.buildAccountMapFromJSON(obj?.accounts, accountDeserializer),
|
||||
});
|
||||
}
|
||||
|
||||
private static buildAccountMapFromJSON(
|
||||
jsonAccounts: Jsonify<{ [userId: string]: Jsonify<Account> }>
|
||||
private static buildAccountMapFromJSON<TAccount extends Account>(
|
||||
jsonAccounts: { [userId: string]: Jsonify<TAccount> },
|
||||
accountDeserializer: (json: Jsonify<TAccount>) => TAccount
|
||||
) {
|
||||
if (!jsonAccounts) {
|
||||
return {};
|
||||
}
|
||||
const accounts: { [userId: string]: Account } = {};
|
||||
const accounts: { [userId: string]: TAccount } = {};
|
||||
for (const userId in jsonAccounts) {
|
||||
accounts[userId] = Account.fromJSON(jsonAccounts[userId]);
|
||||
accounts[userId] = accountDeserializer(jsonAccounts[userId]);
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
export class TreeNode<T extends ITreeNodeObject> {
|
||||
parent: T;
|
||||
node: T;
|
||||
parent: TreeNode<T>;
|
||||
children: TreeNode<T>[] = [];
|
||||
|
||||
constructor(node: T, name: string, parent: T) {
|
||||
constructor(node: T, parent: TreeNode<T>, name?: string, id?: string) {
|
||||
this.parent = parent;
|
||||
this.node = node;
|
||||
this.node.name = name;
|
||||
if (name) {
|
||||
this.node.name = name;
|
||||
}
|
||||
if (id) {
|
||||
this.node.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,4 +7,5 @@ export class WindowState {
|
||||
displayBounds: any;
|
||||
x?: number;
|
||||
y?: number;
|
||||
zoomFactor?: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export class CollectionBulkDeleteRequest {
|
||||
ids: string[];
|
||||
organizationId: string;
|
||||
|
||||
constructor(ids: string[], organizationId?: string) {
|
||||
this.ids = ids == null ? [] : ids;
|
||||
this.organizationId = organizationId;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export class CollectionRequest {
|
||||
name: string;
|
||||
externalId: string;
|
||||
groups: SelectionReadOnlyRequest[] = [];
|
||||
users: SelectionReadOnlyRequest[] = [];
|
||||
|
||||
constructor(collection?: Collection) {
|
||||
if (collection == null) {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { SelectionReadOnlyRequest } from "./selection-read-only.request";
|
||||
|
||||
export class GroupRequest {
|
||||
name: string;
|
||||
accessAll: boolean;
|
||||
externalId: string;
|
||||
collections: SelectionReadOnlyRequest[] = [];
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { OrganizationUserType } from "../../enums/organizationUserType";
|
||||
import { PermissionsApi } from "../api/permissions.api";
|
||||
|
||||
import { SelectionReadOnlyRequest } from "./selection-read-only.request";
|
||||
|
||||
export class OrganizationUserInviteRequest {
|
||||
emails: string[] = [];
|
||||
type: OrganizationUserType;
|
||||
accessAll: boolean;
|
||||
collections: SelectionReadOnlyRequest[] = [];
|
||||
permissions: PermissionsApi;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { OrganizationUserType } from "../../enums/organizationUserType";
|
||||
import { PermissionsApi } from "../api/permissions.api";
|
||||
|
||||
import { SelectionReadOnlyRequest } from "./selection-read-only.request";
|
||||
|
||||
export class OrganizationUserUpdateRequest {
|
||||
type: OrganizationUserType;
|
||||
accessAll: boolean;
|
||||
collections: SelectionReadOnlyRequest[] = [];
|
||||
permissions: PermissionsApi;
|
||||
}
|
||||
7
libs/common/src/models/request/update-avatar.request.ts
Normal file
7
libs/common/src/models/request/update-avatar.request.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class UpdateAvatarRequest {
|
||||
avatarColor: string;
|
||||
|
||||
constructor(avatarColor: string) {
|
||||
this.avatarColor = avatarColor;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OrganizationUserResetPasswordRequest } from "./organization-user-reset-password.request";
|
||||
import { OrganizationUserResetPasswordRequest } from "../../abstractions/organization-user/requests";
|
||||
|
||||
export class UpdateTempPasswordRequest extends OrganizationUserResetPasswordRequest {
|
||||
masterPasswordHint: string;
|
||||
|
||||
@@ -25,14 +25,27 @@ export class CollectionDetailsResponse extends CollectionResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export class CollectionGroupDetailsResponse extends CollectionResponse {
|
||||
export class CollectionAccessDetailsResponse extends CollectionResponse {
|
||||
groups: SelectionReadOnlyResponse[] = [];
|
||||
users: SelectionReadOnlyResponse[] = [];
|
||||
|
||||
/**
|
||||
* Flag indicating the user has been explicitly assigned to this Collection
|
||||
*/
|
||||
assigned: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.assigned = this.getResponseProperty("Assigned") || false;
|
||||
|
||||
const groups = this.getResponseProperty("Groups");
|
||||
if (groups != null) {
|
||||
this.groups = groups.map((g: any) => new SelectionReadOnlyResponse(g));
|
||||
}
|
||||
|
||||
const users = this.getResponseProperty("Users");
|
||||
if (users != null) {
|
||||
this.users = users.map((g: any) => new SelectionReadOnlyResponse(g));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ export class EmergencyAccessGranteeDetailsResponse extends BaseResponse {
|
||||
status: EmergencyAccessStatusType;
|
||||
waitTimeDays: number;
|
||||
creationDate: string;
|
||||
avatarColor: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -25,6 +26,7 @@ export class EmergencyAccessGranteeDetailsResponse extends BaseResponse {
|
||||
this.status = this.getResponseProperty("Status");
|
||||
this.waitTimeDays = this.getResponseProperty("WaitTimeDays");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.avatarColor = this.getResponseProperty("AvatarColor");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +39,7 @@ export class EmergencyAccessGrantorDetailsResponse extends BaseResponse {
|
||||
status: EmergencyAccessStatusType;
|
||||
waitTimeDays: number;
|
||||
creationDate: string;
|
||||
avatarColor: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -48,6 +51,7 @@ export class EmergencyAccessGrantorDetailsResponse extends BaseResponse {
|
||||
this.status = this.getResponseProperty("Status");
|
||||
this.waitTimeDays = this.getResponseProperty("WaitTimeDays");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.avatarColor = this.getResponseProperty("AvatarColor");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { BaseResponse } from "./base.response";
|
||||
import { SelectionReadOnlyResponse } from "./selection-read-only.response";
|
||||
|
||||
export class GroupResponse extends BaseResponse {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
name: string;
|
||||
accessAll: boolean;
|
||||
externalId: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.organizationId = this.getResponseProperty("OrganizationId");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.accessAll = this.getResponseProperty("AccessAll");
|
||||
this.externalId = this.getResponseProperty("ExternalId");
|
||||
}
|
||||
}
|
||||
|
||||
export class GroupDetailsResponse extends GroupResponse {
|
||||
collections: SelectionReadOnlyResponse[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
const collections = this.getResponseProperty("Collections");
|
||||
if (collections != null) {
|
||||
this.collections = collections.map((c: any) => new SelectionReadOnlyResponse(c));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,9 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
useSso: boolean;
|
||||
useKeyConnector: boolean;
|
||||
useScim: boolean;
|
||||
useCustomPermissions: boolean;
|
||||
useResetPassword: boolean;
|
||||
useSecretsManager: boolean;
|
||||
selfHost: boolean;
|
||||
usersGetPremium: boolean;
|
||||
seats: number;
|
||||
@@ -59,7 +61,9 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
this.useSso = this.getResponseProperty("UseSso");
|
||||
this.useKeyConnector = this.getResponseProperty("UseKeyConnector") ?? false;
|
||||
this.useScim = this.getResponseProperty("UseScim") ?? false;
|
||||
this.useCustomPermissions = this.getResponseProperty("UseCustomPermissions") ?? false;
|
||||
this.useResetPassword = this.getResponseProperty("UseResetPassword");
|
||||
this.useSecretsManager = this.getResponseProperty("UseSecretsManager");
|
||||
this.selfHost = this.getResponseProperty("SelfHost");
|
||||
this.usersGetPremium = this.getResponseProperty("UsersGetPremium");
|
||||
this.seats = this.getResponseProperty("Seats");
|
||||
|
||||
@@ -14,6 +14,7 @@ export class ProfileResponse extends BaseResponse {
|
||||
culture: string;
|
||||
twoFactorEnabled: boolean;
|
||||
key: string;
|
||||
avatarColor: string;
|
||||
privateKey: string;
|
||||
securityStamp: string;
|
||||
forcePasswordReset: boolean;
|
||||
@@ -34,6 +35,7 @@ export class ProfileResponse extends BaseResponse {
|
||||
this.culture = this.getResponseProperty("Culture");
|
||||
this.twoFactorEnabled = this.getResponseProperty("TwoFactorEnabled");
|
||||
this.key = this.getResponseProperty("Key");
|
||||
this.avatarColor = this.getResponseProperty("AvatarColor");
|
||||
this.privateKey = this.getResponseProperty("PrivateKey");
|
||||
this.securityStamp = this.getResponseProperty("SecurityStamp");
|
||||
this.forcePasswordReset = this.getResponseProperty("ForcePasswordReset") ?? false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user