diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0adead2928c..76503845fd6 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -203,10 +203,10 @@ apps/web/src/locales/en/messages.json .github/workflows/release-web.yml @bitwarden/dept-bre ## Docker files have shared ownership ## -**/Dockerfile -**/*.Dockerfile -**/.dockerignore -**/entrypoint.sh +**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre +**/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre +**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre +**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre ## Overrides # For the time being platform owns tsconfig and jest config diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index b09b0b7b67b..103b1ca2a5b 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1934,32 +1934,81 @@ "typeNote": { "message": "Note" }, - "newItemHeader": { - "message": "New $TYPE$", - "placeholders": { - "type": { - "content": "$1", - "example": "Login" - } - } + "newItemHeaderLogin": { + "message": "New Login", + "description": "Header for new login item type" }, - "editItemHeader": { - "message": "Edit $TYPE$", - "placeholders": { - "type": { - "content": "$1", - "example": "Login" - } - } + "newItemHeaderCard": { + "message": "New Card", + "description": "Header for new card item type" }, - "viewItemHeader": { - "message": "View $TYPE$", - "placeholders": { - "type": { - "content": "$1", - "example": "Login" - } - } + "newItemHeaderIdentity": { + "message": "New Identity", + "description": "Header for new identity item type" + }, + "newItemHeaderNote": { + "message": "New Note", + "description": "Header for new note item type" + }, + "newItemHeaderSshKey": { + "message": "New SSH key", + "description": "Header for new SSH key item type" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderLogin": { + "message": "Edit Login", + "description": "Header for edit login item type" + }, + "editItemHeaderCard": { + "message": "Edit Card", + "description": "Header for edit card item type" + }, + "editItemHeaderIdentity": { + "message": "Edit Identity", + "description": "Header for edit identity item type" + }, + "editItemHeaderNote": { + "message": "Edit Note", + "description": "Header for edit note item type" + }, + "editItemHeaderSshKey": { + "message": "Edit SSH key", + "description": "Header for edit SSH key item type" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "viewItemHeaderLogin": { + "message": "View Login", + "description": "Header for view login item type" + }, + "viewItemHeaderCard": { + "message": "View Card", + "description": "Header for view card item type" + }, + "viewItemHeaderIdentity": { + "message": "View Identity", + "description": "Header for view identity item type" + }, + "viewItemHeaderNote": { + "message": "View Note", + "description": "Header for view note item type" + }, + "viewItemHeaderSshKey": { + "message": "View SSH key", + "description": "Header for view SSH key item type" }, "passwordHistory": { "message": "Password history" @@ -5092,15 +5141,9 @@ "itemLocation": { "message": "Item Location" }, - "fileSend": { - "message": "File Send" - }, "fileSends": { "message": "File Sends" }, - "textSend": { - "message": "Text Send" - }, "textSends": { "message": "Text Sends" }, diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 73c91548b54..512690929cc 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -2513,6 +2513,7 @@ export default class AutofillService implements AutofillServiceInterface { "label-tag", "placeholder", "label-left", + "label-right", "label-top", "label-aria", "dataSetValues", @@ -2609,7 +2610,7 @@ export default class AutofillService implements AutofillServiceInterface { } /** - * Updates a fill script to place the `cilck_on_opid`, `focus_on_opid`, and `fill_by_opid` + * Updates a fill script to place the `click_on_opid`, `focus_on_opid`, and `fill_by_opid` * fill script actions associated with the provided field. * @param {AutofillScript} fillScript * @param {AutofillField} field diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts index 6bedb939c30..6298cfaf7c1 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.spec.ts @@ -221,7 +221,7 @@ describe("InlineMenuFieldQualificationService", () => { expect( inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails), - ).toBe(false); + ).toBe(true); }); }); @@ -509,7 +509,7 @@ describe("InlineMenuFieldQualificationService", () => { expect( inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails), - ).toBe(false); + ).toBe(true); }); it("is structured on a page with no password fields but has other types of fields in the form", () => { @@ -568,7 +568,7 @@ describe("InlineMenuFieldQualificationService", () => { ).toBe(false); }); - it("contains a disabled autocomplete type when multiple password fields are on the page", () => { + it("will not exclude a field by autocomplete type when it is the only viewable password field on the page", () => { const field = mock({ type: "text", autoCompleteType: "off", @@ -599,7 +599,7 @@ describe("InlineMenuFieldQualificationService", () => { expect( inlineMenuFieldQualificationService.isFieldForLoginForm(field, pageDetails), - ).toBe(false); + ).toBe(true); }); }); }); diff --git a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts index 497c8ce2b35..ed8e41df8ba 100644 --- a/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts +++ b/apps/browser/src/autofill/services/inline-menu-field-qualification.service.ts @@ -37,7 +37,6 @@ export class InlineMenuFieldQualificationService private newPasswordAutoCompleteValue = "new-password"; private autofillFieldKeywordsMap: AutofillKeywordsMap = new WeakMap(); private submitButtonKeywordsMap: SubmitButtonKeywordsMap = new WeakMap(); - private autocompleteDisabledValues = new Set(["off", "false"]); private accountCreationFieldKeywords = [ "register", "registration", @@ -419,10 +418,8 @@ export class InlineMenuFieldQualificationService } // If a single username field or less is present on the page, then we can assume that the - // provided field is for a login form. This will only be the case if the field does not - // explicitly have its autocomplete attribute set to "off" or "false". - - return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues); + // provided field is for a login form. + return true; } // If the field has a form parent and there are multiple visible password fields @@ -442,9 +439,8 @@ export class InlineMenuFieldQualificationService return true; } - // If the field has a form parent and no username field exists and the field has an - // autocomplete attribute set to "off" or "false", this is not a password field - return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues); + // If the field has a form parent and a username field exists this is a password field + return true; } /** @@ -512,20 +508,12 @@ export class InlineMenuFieldQualificationService } // If the page does not contain any password fields, it might be part of a multistep login form. - // That will only be the case if the field does not explicitly have its autocomplete attribute - // set to "off" or "false". - return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues); + return true; } // If the field is structured within a form, but no password fields are present in the form, // we need to consider whether the field is part of a multistep login form. if (passwordFieldsInPageDetails.length === 0) { - // If the field's autocomplete is set to a disabled value, we should assume that the field is - // not part of a login form. - if (this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues)) { - return false; - } - // If the form that contains a single field, we should assume that it is part // of a multistep login form. const fieldsWithinForm = pageDetails.fields.filter( @@ -561,8 +549,7 @@ export class InlineMenuFieldQualificationService } // If no visible password fields are found, this field might be part of a multipart form. - // Check for an invalid autocompleteType to determine if the field is part of a login form. - return !this.fieldContainsAutocompleteValues(field, this.autocompleteDisabledValues); + return true; } /** diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index b6957248d75..5911b3b6d89 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -188,14 +188,11 @@ export class SendAddEditComponent { * @returns The header text. */ private getHeaderText(mode: SendFormMode, type: SendType) { - const headerKey = - mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader"; - - switch (type) { - case SendType.Text: - return this.i18nService.t(headerKey, this.i18nService.t("textSend")); - case SendType.File: - return this.i18nService.t(headerKey, this.i18nService.t("fileSend")); - } + const isEditMode = mode === "edit" || mode === "partial-edit"; + const translation = { + [SendType.Text]: isEditMode ? "editItemHeaderTextSend" : "newItemHeaderTextSend", + [SendType.File]: isEditMode ? "editItemHeaderFileSend" : "newItemHeaderFileSend", + }; + return this.i18nService.t(translation[type]); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index 3985fc85a54..a1e0a43343f 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -368,20 +368,15 @@ export class AddEditV2Component implements OnInit { } setHeader(mode: CipherFormMode, type: CipherType) { - const partOne = mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader"; - - switch (type) { - case CipherType.Login: - return this.i18nService.t(partOne, this.i18nService.t("typeLogin")); - case CipherType.Card: - return this.i18nService.t(partOne, this.i18nService.t("typeCard")); - case CipherType.Identity: - return this.i18nService.t(partOne, this.i18nService.t("typeIdentity")); - case CipherType.SecureNote: - return this.i18nService.t(partOne, this.i18nService.t("note")); - case CipherType.SshKey: - return this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); - } + const isEditMode = mode === "edit" || mode === "partial-edit"; + const translation = { + [CipherType.Login]: isEditMode ? "editItemHeaderLogin" : "newItemHeaderLogin", + [CipherType.Card]: isEditMode ? "editItemHeaderCard" : "newItemHeaderCard", + [CipherType.Identity]: isEditMode ? "editItemHeaderIdentity" : "newItemHeaderIdentity", + [CipherType.SecureNote]: isEditMode ? "editItemHeaderNote" : "newItemHeaderNote", + [CipherType.SshKey]: isEditMode ? "editItemHeaderSshKey" : "newItemHeaderSshKey", + }; + return this.i18nService.t(translation[type]); } delete = async () => { diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 51116da2865..3d4fdb2e9f9 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -172,28 +172,28 @@ describe("ViewV2Component", () => { params$.next({ cipherId: mockCipher.id }); flush(); // Resolve all promises - expect(component.headerText).toEqual("viewItemHeader typeLogin"); + expect(component.headerText).toEqual("viewItemHeaderLogin"); // Set header text for a card mockCipher.type = CipherType.Card; params$.next({ cipherId: mockCipher.id }); flush(); // Resolve all promises - expect(component.headerText).toEqual("viewItemHeader typeCard"); + expect(component.headerText).toEqual("viewItemHeaderCard"); // Set header text for an identity mockCipher.type = CipherType.Identity; params$.next({ cipherId: mockCipher.id }); flush(); // Resolve all promises - expect(component.headerText).toEqual("viewItemHeader typeIdentity"); + expect(component.headerText).toEqual("viewItemHeaderIdentity"); // Set header text for a secure note mockCipher.type = CipherType.SecureNote; params$.next({ cipherId: mockCipher.id }); flush(); // Resolve all promises - expect(component.headerText).toEqual("viewItemHeader note"); + expect(component.headerText).toEqual("viewItemHeaderNote"); })); it("sends viewed event", fakeAsync(() => { diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 5983b2577a5..915a27e4fd1 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -194,18 +194,14 @@ export class ViewV2Component { } setHeader(type: CipherType) { - switch (type) { - case CipherType.Login: - return this.i18nService.t("viewItemHeader", this.i18nService.t("typeLogin")); - case CipherType.Card: - return this.i18nService.t("viewItemHeader", this.i18nService.t("typeCard")); - case CipherType.Identity: - return this.i18nService.t("viewItemHeader", this.i18nService.t("typeIdentity")); - case CipherType.SecureNote: - return this.i18nService.t("viewItemHeader", this.i18nService.t("note")); - case CipherType.SshKey: - return this.i18nService.t("viewItemHeader", this.i18nService.t("typeSshkey")); - } + const translation = { + [CipherType.Login]: "viewItemHeaderLogin", + [CipherType.Card]: "viewItemHeaderCard", + [CipherType.Identity]: "viewItemHeaderIdentity", + [CipherType.SecureNote]: "viewItemHeaderNote", + [CipherType.SshKey]: "viewItemHeaderSshKey", + }; + return this.i18nService.t(translation[type]); } async getCipherData(id: string, userId: UserId) { diff --git a/apps/cli/package.json b/apps/cli/package.json index 9687d33cb23..659a68d13a5 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -68,7 +68,7 @@ "big-integer": "1.6.52", "browser-hrtime": "1.1.8", "chalk": "4.1.2", - "commander": "11.1.0", + "commander": "14.0.0", "core-js": "3.45.0", "form-data": "4.0.4", "https-proxy-agent": "7.0.6", diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 281bfd5d69f..2212c03f4f8 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -228,8 +228,8 @@ export declare namespace chromium_importer { login?: Login failure?: LoginImportFailure } - export function getInstalledBrowsers(): Promise> - export function getAvailableProfiles(browser: string): Promise> + export function getInstalledBrowsers(): Array + export function getAvailableProfiles(browser: string): Array export function importLogins(browser: string, profileId: string): Promise> } export declare namespace autotype { diff --git a/apps/desktop/src/vault/app/vault/vault-v2.component.ts b/apps/desktop/src/vault/app/vault/vault-v2.component.ts index 13a8fa91bc4..141f05a28aa 100644 --- a/apps/desktop/src/vault/app/vault/vault-v2.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-v2.component.ts @@ -926,17 +926,22 @@ export class VaultV2Component } } else if (this.activeFilter.selectedOrganizationId) { this.addOrganizationId = this.activeFilter.selectedOrganizationId; + } else { + // clear out organizationId when the user switches to a personal vault filter + this.addOrganizationId = null; } if (this.activeFilter.selectedFolderId && this.activeFilter.selectedFolder) { this.folderId = this.activeFilter.selectedFolderId; } - if (this.addOrganizationId && this.config) { - this.config.initialValues = { - ...this.config.initialValues, - organizationId: this.addOrganizationId as OrganizationId, - }; + if (this.config == null) { + return; } + + this.config.initialValues = { + ...this.config.initialValues, + organizationId: this.addOrganizationId as OrganizationId, + }; } private async canNavigateAway(action: string, cipher?: CipherView) { diff --git a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts index c3e573bc3c5..7c5301ffba0 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault.component.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault.component.ts @@ -66,6 +66,7 @@ import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-repromp import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; +import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { CipherViewLike, CipherViewLikeUtils, @@ -288,6 +289,7 @@ export class vNextVaultComponent implements OnInit, OnDestroy { private billingNotificationService: BillingNotificationService, private organizationWarningsService: OrganizationWarningsService, private collectionService: CollectionService, + private restrictedItemTypesService: RestrictedItemTypesService, ) { this.userId$ = this.accountService.activeAccount$.pipe(getUserId); this.filter$ = this.routedVaultFilterService.filter$; @@ -357,9 +359,10 @@ export class vNextVaultComponent implements OnInit, OnDestroy { this.allCiphers$ = combineLatest([ this.organization$, this.userId$, + this.restrictedItemTypesService.restricted$, this.refreshingSubject$, ]).pipe( - switchMap(async ([organization, userId]) => { + switchMap(async ([organization, userId, restricted]) => { // If user swaps organization reset the addAccessToggle if (!this.showAddAccessToggle || organization) { this.addAccessToggle(0); @@ -381,6 +384,11 @@ export class vNextVaultComponent implements OnInit, OnDestroy { ciphers = await this.cipherService.getManyFromApiForOrganization(organization.id); } + // Filter out restricted ciphers before indexing + ciphers = ciphers.filter( + (cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restricted), + ); + await this.searchService.indexCiphers(userId, ciphers, organization.id); return ciphers; }), diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts index f0ecca1686d..60993924ded 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.spec.ts @@ -134,7 +134,7 @@ describe("EmergencyViewDialogComponent", () => { component["updateTitle"](); - expect(component["title"]).toBe("viewItemType typelogin"); + expect(component["title"]).toBe("viewItemHeaderLogin"); }); it("sets card title", () => { @@ -142,7 +142,7 @@ describe("EmergencyViewDialogComponent", () => { component["updateTitle"](); - expect(component["title"]).toBe("viewItemType typecard"); + expect(component["title"]).toBe("viewItemHeaderCard"); }); it("sets identity title", () => { @@ -150,7 +150,7 @@ describe("EmergencyViewDialogComponent", () => { component["updateTitle"](); - expect(component["title"]).toBe("viewItemType typeidentity"); + expect(component["title"]).toBe("viewItemHeaderIdentity"); }); it("sets note title", () => { @@ -158,7 +158,7 @@ describe("EmergencyViewDialogComponent", () => { component["updateTitle"](); - expect(component["title"]).toBe("viewItemType note"); + expect(component["title"]).toBe("viewItemHeaderNote"); }); }); }); diff --git a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts index 67612e5dcd3..656ec894f27 100644 --- a/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/view/emergency-view-dialog.component.ts @@ -73,22 +73,20 @@ export class EmergencyViewDialogComponent { }; private updateTitle() { - const partOne = "viewItemType"; - const type = this.cipher.type; switch (type) { case CipherType.Login: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase()); + this.title = this.i18nService.t("viewItemHeaderLogin"); break; case CipherType.Card: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase()); + this.title = this.i18nService.t("viewItemHeaderCard"); break; case CipherType.Identity: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase()); + this.title = this.i18nService.t("viewItemHeaderIdentity"); break; case CipherType.SecureNote: - this.title = this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase()); + this.title = this.i18nService.t("viewItemHeaderNote"); break; } } diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index 8462e801de0..78e71b392e5 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -521,36 +521,39 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { return await this.cipherService.decrypt(config.originalCipher, activeUserId); } - private updateTitle() { - let partOne: string; + private updateTitle(): void { + const mode = this.formConfig.mode || this.params.mode; + const type = this.cipher?.type ?? this.formConfig.cipherType; + const translation: { [key: string]: { [key: number]: string } } = { + view: { + [CipherType.Login]: "viewItemHeaderLogin", + [CipherType.Card]: "viewItemHeaderCard", + [CipherType.Identity]: "viewItemHeaderIdentity", + [CipherType.SecureNote]: "viewItemHeaderNote", + [CipherType.SshKey]: "viewItemHeaderSshKey", + }, + new: { + [CipherType.Login]: "newItemHeaderLogin", + [CipherType.Card]: "newItemHeaderCard", + [CipherType.Identity]: "newItemHeaderIdentity", + [CipherType.SecureNote]: "newItemHeaderNote", + [CipherType.SshKey]: "newItemHeaderSshKey", + }, + edit: { + [CipherType.Login]: "editItemHeaderLogin", + [CipherType.Card]: "editItemHeaderCard", + [CipherType.Identity]: "editItemHeaderIdentity", + [CipherType.SecureNote]: "editItemHeaderNote", + [CipherType.SshKey]: "editItemHeaderSshKey", + }, + }; - if (this.params.mode === "view") { - partOne = "viewItemType"; - } else if (this.formConfig.mode === "edit" || this.formConfig.mode === "partial-edit") { - partOne = "editItemHeader"; - } else { - partOne = "newItemHeader"; - } + const effectiveMode = + mode === "partial-edit" || mode === "edit" ? "edit" : translation[mode] ? mode : "new"; - const type = this.cipher?.type ?? this.formConfig.cipherType ?? CipherType.Login; + const fullTranslation = translation[effectiveMode][type]; - switch (type) { - case CipherType.Login: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeLogin")); - break; - case CipherType.Card: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeCard")); - break; - case CipherType.Identity: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeIdentity")); - break; - case CipherType.SecureNote: - this.title = this.i18nService.t(partOne, this.i18nService.t("note")); - break; - case CipherType.SshKey: - this.title = this.i18nService.t(partOne, this.i18nService.t("typeSshKey")); - break; - } + this.title = this.i18nService.t(fullTranslation); } /** diff --git a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts index bfad71aca4b..c09238e7953 100644 --- a/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts +++ b/apps/web/src/app/vault/individual-vault/add-edit-v2.component.ts @@ -138,19 +138,15 @@ export class AddEditComponentV2 implements OnInit { * @returns The header text. */ setHeader(mode: CipherFormMode, type: CipherType) { - const partOne = mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader"; - switch (type) { - case CipherType.Login: - return this.i18nService.t(partOne, this.i18nService.t("typeLogin").toLowerCase()); - case CipherType.Card: - return this.i18nService.t(partOne, this.i18nService.t("typeCard").toLowerCase()); - case CipherType.Identity: - return this.i18nService.t(partOne, this.i18nService.t("typeIdentity").toLowerCase()); - case CipherType.SecureNote: - return this.i18nService.t(partOne, this.i18nService.t("note").toLowerCase()); - case CipherType.SshKey: - return this.i18nService.t(partOne, this.i18nService.t("typeSshKey").toLowerCase()); - } + const isEditMode = mode === "edit" || mode === "partial-edit"; + const translation = { + [CipherType.Login]: isEditMode ? "editItemHeaderLogin" : "newItemHeaderLogin", + [CipherType.Card]: isEditMode ? "editItemHeaderCard" : "newItemHeaderCard", + [CipherType.Identity]: isEditMode ? "editItemHeaderIdentity" : "newItemHeaderIdentity", + [CipherType.SecureNote]: isEditMode ? "editItemHeaderNote" : "newItemHeaderNote", + [CipherType.SshKey]: isEditMode ? "editItemHeaderSshKey" : "newItemHeaderSshKey", + }; + return this.i18nService.t(translation[type]); } /** diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index 97f3037407c..ea0b66f12d0 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -194,15 +194,15 @@ export class ViewComponent implements OnInit { switch (this.cipher.type) { case CipherType.Login: - return this.i18nService.t("viewItemType", this.i18nService.t("typeLogin").toLowerCase()); + return this.i18nService.t("viewItemHeaderLogin"); case CipherType.SecureNote: - return this.i18nService.t("viewItemType", this.i18nService.t("note").toLowerCase()); + return this.i18nService.t("viewItemHeaderCard"); case CipherType.Card: - return this.i18nService.t("viewItemType", this.i18nService.t("typeCard").toLowerCase()); + return this.i18nService.t("viewItemHeaderIdentity"); case CipherType.Identity: - return this.i18nService.t("viewItemType", this.i18nService.t("typeIdentity").toLowerCase()); + return this.i18nService.t("viewItemHeaderNote"); case CipherType.SshKey: - return this.i18nService.t("viewItemType", this.i18nService.t("typeSshKey").toLowerCase()); + return this.i18nService.t("viewItemHeaderSshKey"); default: return null; } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index ef6e68ff1a0..dd2e132ba04 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -734,32 +734,81 @@ "viewItem": { "message": "View item" }, - "newItemHeader": { - "message": "New $TYPE$", - "placeholders": { - "type": { - "content": "$1", - "example": "login" - } - } + "newItemHeaderLogin": { + "message": "New Login", + "description": "Header for new login item type" }, - "editItemHeader": { - "message": "Edit $TYPE$", - "placeholders": { - "type": { - "content": "$1", - "example": "login" - } - } + "newItemHeaderCard": { + "message": "New Card", + "description": "Header for new card item type" }, - "viewItemType": { - "message": "View $ITEMTYPE$", - "placeholders": { - "itemtype": { - "content": "$1", - "example": "login" - } - } + "newItemHeaderIdentity": { + "message": "New Identity", + "description": "Header for new identity item type" + }, + "newItemHeaderNote": { + "message": "New Note", + "description": "Header for new note item type" + }, + "newItemHeaderSshKey": { + "message": "New SSH key", + "description": "Header for new SSH key item type" + }, + "newItemHeaderTextSend": { + "message": "New Text Send", + "description": "Header for new text send" + }, + "newItemHeaderFileSend": { + "message": "New File Send", + "description": "Header for new file send" + }, + "editItemHeaderLogin": { + "message": "Edit Login", + "description": "Header for edit login item type" + }, + "editItemHeaderCard": { + "message": "Edit Card", + "description": "Header for edit card item type" + }, + "editItemHeaderIdentity": { + "message": "Edit Identity", + "description": "Header for edit identity item type" + }, + "editItemHeaderNote": { + "message": "Edit Note", + "description": "Header for edit note item type" + }, + "editItemHeaderSshKey": { + "message": "Edit SSH key", + "description": "Header for edit SSH key item type" + }, + "editItemHeaderTextSend": { + "message": "Edit Text Send", + "description": "Header for edit text send" + }, + "editItemHeaderFileSend": { + "message": "Edit File Send", + "description": "Header for edit file send" + }, + "viewItemHeaderLogin": { + "message": "View Login", + "description": "Header for view login item type" + }, + "viewItemHeaderCard": { + "message": "View Card", + "description": "Header for view card item type" + }, + "viewItemHeaderIdentity": { + "message": "View Identity", + "description": "Header for view identity item type" + }, + "viewItemHeaderNote": { + "message": "View Note", + "description": "Header for view note item type" + }, + "viewItemHeaderSshKey": { + "message": "View SSH key", + "description": "Header for view SSH key item type" }, "new": { "message": "New", @@ -9670,6 +9719,9 @@ "failedToSaveIntegration": { "message": "Failed to save integration. Please try again later." }, + "failedToDeleteIntegration": { + "message": "Failed to delete integration. Please try again later." + }, "deviceIdMissing": { "message": "Device ID is missing" }, @@ -10214,15 +10266,9 @@ "learnMoreAboutApi": { "message": "Learn more about Bitwarden's API" }, - "fileSend": { - "message": "File Send" - }, "fileSends": { "message": "File Sends" }, - "textSend": { - "message": "Text Send" - }, "textSends": { "message": "Text Sends" }, diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration.ts index 1bb38915e9a..abd1861caa9 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/models/integration.ts @@ -20,7 +20,6 @@ export type Integration = { */ newBadgeExpiration?: string; description?: string; - isConnected?: boolean; canSetupConnection?: boolean; configuration?: string; template?: string; diff --git a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts index d45d4d21652..ad3d6764713 100644 --- a/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts +++ b/bitwarden_license/bit-common/src/dirt/organization-integrations/services/hec-organization-integration-service.ts @@ -156,6 +156,34 @@ export class HecOrganizationIntegrationService { } } + async deleteHec( + organizationId: OrganizationId, + OrganizationIntegrationId: OrganizationIntegrationId, + OrganizationIntegrationConfigurationId: OrganizationIntegrationConfigurationId, + ) { + if (organizationId != this.organizationId$.getValue()) { + throw new Error("Organization ID mismatch"); + } + // delete the configuration first due to foreign key constraint + await this.integrationConfigurationApiService.deleteOrganizationIntegrationConfiguration( + organizationId, + OrganizationIntegrationId, + OrganizationIntegrationConfigurationId, + ); + + // delete the integration + await this.integrationApiService.deleteOrganizationIntegration( + organizationId, + OrganizationIntegrationId, + ); + + // update the local observable + const updatedIntegrations = this._integrations$ + .getValue() + .filter((i) => i.id !== OrganizationIntegrationId); + this._integrations$.next(updatedIntegrations); + } + /** * Gets a OrganizationIntegration for an OrganizationIntegrationId * @param integrationId id of the integration diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts index c7a7ff45761..0facf282ba3 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.spec.ts @@ -13,12 +13,13 @@ import { DialogService, ToastService } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { openHecConnectDialog } from "../integration-dialog"; +import { HecConnectDialogResultStatus, openHecConnectDialog } from "../integration-dialog"; import { IntegrationCardComponent } from "./integration-card.component"; jest.mock("../integration-dialog", () => ({ openHecConnectDialog: jest.fn(), + HecConnectDialogResultStatus: { Edited: "edit", Delete: "delete" }, })); describe("IntegrationCardComponent", () => { @@ -272,7 +273,7 @@ describe("IntegrationCardComponent", () => { it("should call updateHec if isUpdateAvailable is true", async () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: true, + success: HecConnectDialogResultStatus.Edited, url: "test-url", bearerToken: "token", index: "index", @@ -304,7 +305,7 @@ describe("IntegrationCardComponent", () => { (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: true, + success: HecConnectDialogResultStatus.Edited, url: "test-url", bearerToken: "token", index: "index", @@ -327,10 +328,66 @@ describe("IntegrationCardComponent", () => { expect(mockIntegrationService.updateHec).not.toHaveBeenCalled(); }); - it("should show toast on error", async () => { + it("should call deleteHec when a delete is requested", async () => { + component.organizationId = "org-id" as any; + (openHecConnectDialog as jest.Mock).mockReturnValue({ closed: of({ - success: true, + success: HecConnectDialogResultStatus.Delete, + url: "test-url", + bearerToken: "token", + index: "index", + }), + }); + + mockIntegrationService.deleteHec.mockResolvedValue(undefined); + + await component.setupConnection(); + + expect(mockIntegrationService.deleteHec).toHaveBeenCalledWith( + "org-id", + "integration-id", + "config-id", + ); + expect(mockIntegrationService.saveHec).not.toHaveBeenCalled(); + }); + + it("should not call deleteHec if no existing configuration", async () => { + component.integrationSettings = { + organizationIntegration: null, + name: OrganizationIntegrationServiceType.CrowdStrike, + } as any; + component.organizationId = "org-id" as any; + + (openHecConnectDialog as jest.Mock).mockReturnValue({ + closed: of({ + success: HecConnectDialogResultStatus.Delete, + url: "test-url", + bearerToken: "token", + index: "index", + }), + }); + + mockIntegrationService.deleteHec.mockResolvedValue(undefined); + + await component.setupConnection(); + + expect(mockIntegrationService.deleteHec).not.toHaveBeenCalledWith( + "org-id", + "integration-id", + "config-id", + OrganizationIntegrationServiceType.CrowdStrike, + "test-url", + "token", + "index", + ); + expect(mockIntegrationService.updateHec).not.toHaveBeenCalled(); + }); + + it("should show toast on error while saving", async () => { + (openHecConnectDialog as jest.Mock).mockReturnValue({ + closed: of({ + success: HecConnectDialogResultStatus.Edited, url: "test-url", bearerToken: "token", index: "index", @@ -349,5 +406,28 @@ describe("IntegrationCardComponent", () => { message: mockI18nService.t("failedToSaveIntegration"), }); }); + + it("should show toast on error while deleting", async () => { + (openHecConnectDialog as jest.Mock).mockReturnValue({ + closed: of({ + success: HecConnectDialogResultStatus.Delete, + url: "test-url", + bearerToken: "token", + index: "index", + }), + }); + + jest.spyOn(component, "isUpdateAvailable", "get").mockReturnValue(true); + mockIntegrationService.deleteHec.mockRejectedValue(new Error("fail")); + + await component.setupConnection(); + + expect(mockIntegrationService.deleteHec).toHaveBeenCalled(); + expect(toastService.showToast).toHaveBeenCalledWith({ + variant: "error", + title: "", + message: mockI18nService.t("failedToDeleteIntegration"), + }); + }); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts index f40fb03c5f4..b83b1609bce 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-card/integration-card.component.ts @@ -21,7 +21,11 @@ import { OrganizationId } from "@bitwarden/common/types/guid"; import { DialogService, ToastService } from "@bitwarden/components"; import { SharedModule } from "@bitwarden/web-vault/app/shared"; -import { openHecConnectDialog } from "../integration-dialog/index"; +import { + HecConnectDialogResult, + HecConnectDialogResultStatus, + openHecConnectDialog, +} from "../integration-dialog/index"; @Component({ selector: "app-integration-card", @@ -142,32 +146,20 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { } try { - if (this.isUpdateAvailable) { - const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; - const orgIntegrationConfigurationId = - this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + if (result.success === HecConnectDialogResultStatus.Delete) { + await this.deleteHec(); + } + } catch { + this.toastService.showToast({ + variant: "error", + title: "", + message: this.i18nService.t("failedToDeleteIntegration"), + }); + } - if (!orgIntegrationId || !orgIntegrationConfigurationId) { - throw Error("Organization Integration ID or Configuration ID is missing"); - } - - await this.hecOrganizationIntegrationService.updateHec( - this.organizationId, - orgIntegrationId, - orgIntegrationConfigurationId, - this.integrationSettings.name as OrganizationIntegrationServiceType, - result.url, - result.bearerToken, - result.index, - ); - } else { - await this.hecOrganizationIntegrationService.saveHec( - this.organizationId, - this.integrationSettings.name as OrganizationIntegrationServiceType, - result.url, - result.bearerToken, - result.index, - ); + try { + if (result.success === HecConnectDialogResultStatus.Edited) { + await this.saveHec(result); } } catch { this.toastService.showToast({ @@ -175,7 +167,55 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy { title: "", message: this.i18nService.t("failedToSaveIntegration"), }); - return; } } + + async saveHec(result: HecConnectDialogResult) { + if (this.isUpdateAvailable) { + // retrieve org integration and configuration ids + const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; + const orgIntegrationConfigurationId = + this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + + if (!orgIntegrationId || !orgIntegrationConfigurationId) { + throw Error("Organization Integration ID or Configuration ID is missing"); + } + + // update existing integration and configuration + await this.hecOrganizationIntegrationService.updateHec( + this.organizationId, + orgIntegrationId, + orgIntegrationConfigurationId, + this.integrationSettings.name as OrganizationIntegrationServiceType, + result.url, + result.bearerToken, + result.index, + ); + } else { + // create new integration and configuration + await this.hecOrganizationIntegrationService.saveHec( + this.organizationId, + this.integrationSettings.name as OrganizationIntegrationServiceType, + result.url, + result.bearerToken, + result.index, + ); + } + } + + async deleteHec() { + const orgIntegrationId = this.integrationSettings.organizationIntegration?.id; + const orgIntegrationConfigurationId = + this.integrationSettings.organizationIntegration?.integrationConfiguration[0]?.id; + + if (!orgIntegrationId || !orgIntegrationConfigurationId) { + throw Error("Organization Integration ID or Configuration ID is missing"); + } + + await this.hecOrganizationIntegrationService.deleteHec( + this.organizationId, + orgIntegrationId, + orgIntegrationConfigurationId, + ); + } } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html index 2495feacf60..776ab9f006c 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.html @@ -37,6 +37,19 @@ + + @if (canDelete) { +
+ +
+ } diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts index 1325411ade8..7d367ea92be 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.spec.ts @@ -14,6 +14,7 @@ import { ConnectHecDialogComponent, HecConnectDialogParams, HecConnectDialogResult, + HecConnectDialogResultStatus, openHecConnectDialog, } from "./connect-dialog-hec.component"; @@ -65,7 +66,6 @@ describe("ConnectDialogHecComponent", () => { imageDarkMode: "test-image-dark.png", newBadgeExpiration: "2024-12-31", description: "Test Description", - isConnected: false, canSetupConnection: true, type: IntegrationType.EVENT, } as Integration; @@ -155,8 +155,7 @@ describe("ConnectDialogHecComponent", () => { bearerToken: "token", index: "1", service: "Test Service", - success: true, - error: null, + success: HecConnectDialogResultStatus.Edited, }); }); }); diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts index fbcb67b66ba..16090da1c00 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integration-dialog/connect-dialog/connect-dialog-hec.component.ts @@ -17,10 +17,17 @@ export interface HecConnectDialogResult { bearerToken: string; index: string; service: string; - success: boolean; - error: string | null; + success: HecConnectDialogResultStatusType | null; } +export const HecConnectDialogResultStatus = { + Edited: "edit", + Delete: "delete", +} as const; + +export type HecConnectDialogResultStatusType = + (typeof HecConnectDialogResultStatus)[keyof typeof HecConnectDialogResultStatus]; + @Component({ templateUrl: "./connect-dialog-hec.component.html", imports: [SharedModule], @@ -40,6 +47,7 @@ export class ConnectHecDialogComponent implements OnInit { @Inject(DIALOG_DATA) protected connectInfo: HecConnectDialogParams, protected formBuilder: FormBuilder, private dialogRef: DialogRef, + private dialogService: DialogService, ) {} ngOnInit(): void { @@ -62,23 +70,51 @@ export class ConnectHecDialogComponent implements OnInit { return !!this.hecConfig; } - submit = async (): Promise => { - const formJson = this.formGroup.getRawValue(); + get canDelete(): boolean { + return !!this.hecConfig; + } - const result: HecConnectDialogResult = { - integrationSettings: this.connectInfo.settings, - url: formJson.url || "", - bearerToken: formJson.bearerToken || "", - index: formJson.index || "", - service: formJson.service || "", - success: true, - error: null, - }; + submit = async (): Promise => { + if (this.formGroup.invalid) { + this.formGroup.markAllAsTouched(); + return; + } + const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Edited); this.dialogRef.close(result); return; }; + + delete = async (): Promise => { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "deleteItem" }, + content: { + key: "deleteItemConfirmation", + }, + type: "warning", + }); + + if (confirmed) { + const result = this.getHecConnectDialogResult(HecConnectDialogResultStatus.Delete); + this.dialogRef.close(result); + } + }; + + private getHecConnectDialogResult( + status: HecConnectDialogResultStatusType, + ): HecConnectDialogResult { + const formJson = this.formGroup.getRawValue(); + + return { + integrationSettings: this.connectInfo.settings, + url: formJson.url || "", + bearerToken: formJson.bearerToken || "", + index: formJson.index || "", + service: formJson.service || "", + success: status, + }; + } } export function openHecConnectDialog( diff --git a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts index 6747fd1c2fe..e3af5e273ea 100644 --- a/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts +++ b/bitwarden_license/bit-web/src/app/dirt/organization-integrations/integrations.component.ts @@ -253,7 +253,6 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg", type: IntegrationType.EVENT, description: "crowdstrikeEventIntegrationDesc", - isConnected: false, canSetupConnection: true, }; @@ -265,6 +264,11 @@ export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy { this.hecOrganizationIntegrationService.integrations$ .pipe(takeUntil(this.destroy$)) .subscribe((integrations) => { + // reset all integrations to null first - in case one was deleted + this.integrationsList.forEach((i) => { + i.organizationIntegration = null; + }); + integrations.map((integration) => { const item = this.integrationsList.find((i) => i.name === integration.serviceType); if (item) { diff --git a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts index fefcdcc3daa..2b0682b6602 100644 --- a/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts +++ b/bitwarden_license/bit-web/src/app/secrets-manager/integrations/integrations.module.ts @@ -1,5 +1,11 @@ import { NgModule } from "@angular/core"; +import { HecOrganizationIntegrationService } from "@bitwarden/bit-common/dirt/organization-integrations/services/hec-organization-integration-service"; +import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-api.service"; +import { OrganizationIntegrationConfigurationApiService } from "@bitwarden/bit-common/dirt/organization-integrations/services/organization-integration-configuration-api.service"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { safeProvider } from "@bitwarden/ui-common"; + import { IntegrationCardComponent } from "../../dirt/organization-integrations/integration-card/integration-card.component"; import { IntegrationGridComponent } from "../../dirt/organization-integrations/integration-grid/integration-grid.component"; import { SecretsManagerSharedModule } from "../shared/sm-shared.module"; @@ -14,6 +20,23 @@ import { IntegrationsComponent } from "./integrations.component"; IntegrationCardComponent, IntegrationGridComponent, ], + providers: [ + safeProvider({ + provide: HecOrganizationIntegrationService, + useClass: HecOrganizationIntegrationService, + deps: [OrganizationIntegrationApiService, OrganizationIntegrationConfigurationApiService], + }), + safeProvider({ + provide: OrganizationIntegrationApiService, + useClass: OrganizationIntegrationApiService, + deps: [ApiService], + }), + safeProvider({ + provide: OrganizationIntegrationConfigurationApiService, + useClass: OrganizationIntegrationConfigurationApiService, + deps: [ApiService], + }), + ], declarations: [IntegrationsComponent], }) export class IntegrationsModule {} diff --git a/libs/common/src/enums/event-category.enum.ts b/libs/common/src/enums/event-category.enum.ts new file mode 100644 index 00000000000..a00de477301 --- /dev/null +++ b/libs/common/src/enums/event-category.enum.ts @@ -0,0 +1,110 @@ +import { EventType } from "./event-type.enum"; + +export const EventCategory = { + UserEvents: "userEvents", + ItemEvents: "itemEvents", + CollectionEvents: "collectionEvents", + GroupEvents: "groupEvents", + OrganizationMemberEvents: "organizationMemberEvents", + OrganizationEvents: "organizationEvents", + ProviderEvents: "providerEvents", +} as const; + +export type EventCategory = (typeof EventCategory)[keyof typeof EventCategory]; + +export const EventCategoryEventTypes: Record = { + [EventCategory.UserEvents]: [ + EventType.User_LoggedIn, + EventType.User_ChangedPassword, + EventType.User_Updated2fa, + EventType.User_Disabled2fa, + EventType.User_Recovered2fa, + EventType.User_FailedLogIn, + EventType.User_FailedLogIn2fa, + EventType.User_ClientExportedVault, + EventType.User_UpdatedTempPassword, + EventType.User_MigratedKeyToKeyConnector, + EventType.User_RequestedDeviceApproval, + EventType.User_TdeOffboardingPasswordSet, + ], + [EventCategory.ItemEvents]: [ + EventType.Cipher_Created, + EventType.Cipher_Updated, + EventType.Cipher_Deleted, + EventType.Cipher_AttachmentCreated, + EventType.Cipher_AttachmentDeleted, + EventType.Cipher_Shared, + EventType.Cipher_UpdatedCollections, + EventType.Cipher_ClientViewed, + EventType.Cipher_ClientToggledPasswordVisible, + EventType.Cipher_ClientToggledHiddenFieldVisible, + EventType.Cipher_ClientToggledCardCodeVisible, + EventType.Cipher_ClientCopiedPassword, + EventType.Cipher_ClientCopiedHiddenField, + EventType.Cipher_ClientCopiedCardCode, + EventType.Cipher_ClientAutofilled, + EventType.Cipher_SoftDeleted, + EventType.Cipher_Restored, + EventType.Cipher_ClientToggledCardNumberVisible, + EventType.Cipher_ClientToggledTOTPSeedVisible, + ], + [EventCategory.CollectionEvents]: [ + EventType.Collection_Created, + EventType.Collection_Updated, + EventType.Collection_Deleted, + ], + [EventCategory.GroupEvents]: [ + EventType.Group_Created, + EventType.Group_Updated, + EventType.Group_Deleted, + ], + [EventCategory.OrganizationMemberEvents]: [ + EventType.OrganizationUser_Invited, + EventType.OrganizationUser_Confirmed, + EventType.OrganizationUser_Updated, + EventType.OrganizationUser_Removed, + EventType.OrganizationUser_UpdatedGroups, + EventType.OrganizationUser_UnlinkedSso, + EventType.OrganizationUser_ResetPassword_Enroll, + EventType.OrganizationUser_ResetPassword_Withdraw, + EventType.OrganizationUser_AdminResetPassword, + EventType.OrganizationUser_ResetSsoLink, + EventType.OrganizationUser_FirstSsoLogin, + EventType.OrganizationUser_Revoked, + EventType.OrganizationUser_Restored, + EventType.OrganizationUser_ApprovedAuthRequest, + EventType.OrganizationUser_RejectedAuthRequest, + EventType.OrganizationUser_Deleted, + EventType.OrganizationUser_Left, + ], + [EventCategory.OrganizationEvents]: [ + EventType.Organization_Updated, + EventType.Organization_PurgedVault, + EventType.Organization_ClientExportedVault, + EventType.Organization_VaultAccessed, + EventType.Organization_EnabledSso, + EventType.Organization_DisabledSso, + EventType.Organization_EnabledKeyConnector, + EventType.Organization_DisabledKeyConnector, + EventType.Organization_SponsorshipsSynced, + EventType.Organization_CollectionManagementUpdated, + EventType.Organization_CollectionManagement_LimitCollectionCreationEnabled, + EventType.Organization_CollectionManagement_LimitCollectionCreationDisabled, + EventType.Organization_CollectionManagement_LimitCollectionDeletionEnabled, + EventType.Organization_CollectionManagement_LimitCollectionDeletionDisabled, + EventType.Organization_CollectionManagement_LimitItemDeletionEnabled, + EventType.Organization_CollectionManagement_LimitItemDeletionDisabled, + EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsEnabled, + EventType.Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled, + ], + [EventCategory.ProviderEvents]: [ + EventType.ProviderUser_Invited, + EventType.ProviderUser_Confirmed, + EventType.ProviderUser_Updated, + EventType.ProviderUser_Removed, + EventType.ProviderOrganization_Created, + EventType.ProviderOrganization_Added, + EventType.ProviderOrganization_Removed, + EventType.ProviderOrganization_VaultAccessed, + ], +}; diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index cb28a0ac6ef..e5f2b9bf48a 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -41,6 +41,7 @@ export enum FeatureFlag { /* DIRT */ EventBasedOrganizationIntegrations = "event-based-organization-integrations", + PM22887_RiskInsightsActivityTab = "pm-22887-risk-insights-activity-tab", /* Vault */ PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk", @@ -84,6 +85,7 @@ export const DefaultFeatureFlagValue = { /* DIRT */ [FeatureFlag.EventBasedOrganizationIntegrations]: FALSE, + [FeatureFlag.PM22887_RiskInsightsActivityTab]: FALSE, /* Vault */ [FeatureFlag.CipherKeyEncryption]: FALSE, diff --git a/libs/components/src/anon-layout/anon-layout.component.html b/libs/components/src/anon-layout/anon-layout.component.html index f22d12f5138..c66647c482d 100644 --- a/libs/components/src/anon-layout/anon-layout.component.html +++ b/libs/components/src/anon-layout/anon-layout.component.html @@ -14,7 +14,12 @@
-
+ + +
diff --git a/libs/components/src/stories/introduction.mdx b/libs/components/src/stories/introduction.mdx index 7580262a6ef..4401c47551f 100644 --- a/libs/components/src/stories/introduction.mdx +++ b/libs/components/src/stories/introduction.mdx @@ -4,6 +4,11 @@ import { Meta } from "@storybook/addon-docs";