1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-24 00:23:17 +00:00

Merge branch 'main' into vault/PM-12049

This commit is contained in:
Matt Bishop
2024-11-11 17:43:44 -05:00
committed by GitHub
254 changed files with 7131 additions and 795 deletions

View File

@@ -8,6 +8,7 @@ import { CustomFieldsComponent } from "./components/custom-fields/custom-fields.
import { IdentitySectionComponent } from "./components/identity/identity.component";
import { ItemDetailsSectionComponent } from "./components/item-details/item-details-section.component";
import { LoginDetailsSectionComponent } from "./components/login-details-section/login-details-section.component";
import { SshKeySectionComponent } from "./components/sshkey-section/sshkey-section.component";
/**
* The complete form for a cipher. Includes all the sub-forms from their respective section components.
@@ -20,6 +21,7 @@ export type CipherForm = {
autoFillOptions?: AutofillOptionsComponent["autofillOptionsForm"];
cardDetails?: CardDetailsSectionComponent["cardDetailsForm"];
identityDetails?: IdentitySectionComponent["identityForm"];
sshKeyDetails?: SshKeySectionComponent["sshKeyForm"];
customFields?: CustomFieldsComponent["customFieldsForm"];
};

View File

@@ -22,6 +22,12 @@
[disabled]="config.mode === 'partial-edit'"
></vault-card-details-section>
<vault-sshkey-section
*ngIf="config.cipherType === CipherType.SshKey"
[disabled]="config.mode === 'partial-edit'"
[originalCipherView]="originalCipherView"
></vault-sshkey-section>
<vault-additional-options-section></vault-additional-options-section>
<!-- Attachments are only available for existing ciphers -->

View File

@@ -42,6 +42,7 @@ import { CardDetailsSectionComponent } from "./card-details-section/card-details
import { IdentitySectionComponent } from "./identity/identity.component";
import { ItemDetailsSectionComponent } from "./item-details/item-details-section.component";
import { LoginDetailsSectionComponent } from "./login-details-section/login-details-section.component";
import { SshKeySectionComponent } from "./sshkey-section/sshkey-section.component";
@Component({
selector: "vault-cipher-form",
@@ -65,6 +66,7 @@ import { LoginDetailsSectionComponent } from "./login-details-section/login-deta
ItemDetailsSectionComponent,
CardDetailsSectionComponent,
IdentitySectionComponent,
SshKeySectionComponent,
NgIf,
AdditionalOptionsSectionComponent,
LoginDetailsSectionComponent,

View File

@@ -0,0 +1,30 @@
<bit-section [formGroup]="sshKeyForm">
<bit-section-header>
<h2 bitTypography="h6">
{{ "typeSshKey" | i18n }}
</h2>
</bit-section-header>
<bit-card>
<bit-form-field>
<bit-label>{{ "sshPrivateKey" | i18n }}</bit-label>
<input id="privateKey" bitInput formControlName="privateKey" type="password" />
<button
type="button"
bitIconButton
bitSuffix
data-testid="toggle-privateKey-visibility"
bitPasswordInputToggle
></button>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "sshPublicKey" | i18n }}</bit-label>
<input id="publicKey" bitInput formControlName="publicKey" />
</bit-form-field>
<bit-form-field>
<bit-label>{{ "sshFingerprint" | i18n }}</bit-label>
<input id="keyFingerprint" bitInput formControlName="keyFingerprint" />
</bit-form-field>
</bit-card>
</bit-section>

View File

@@ -0,0 +1,80 @@
import { CommonModule } from "@angular/common";
import { Component, Input, OnInit } from "@angular/core";
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
CardComponent,
FormFieldModule,
IconButtonModule,
SectionComponent,
SectionHeaderComponent,
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { CipherFormContainer } from "../../cipher-form-container";
@Component({
selector: "vault-sshkey-section",
templateUrl: "./sshkey-section.component.html",
standalone: true,
imports: [
CardComponent,
SectionComponent,
TypographyModule,
FormFieldModule,
ReactiveFormsModule,
SelectModule,
SectionHeaderComponent,
IconButtonModule,
JslibModule,
CommonModule,
],
})
export class SshKeySectionComponent implements OnInit {
/** The original cipher */
@Input() originalCipherView: CipherView;
/** True when all fields should be disabled */
@Input() disabled: boolean;
/**
* All form fields associated with the ssh key
*
* Note: `as` is used to assert the type of the form control,
* leaving as just null gets inferred as `unknown`
*/
sshKeyForm = this.formBuilder.group({
privateKey: null as string | null,
publicKey: null as string | null,
keyFingerprint: null as string | null,
});
constructor(
private cipherFormContainer: CipherFormContainer,
private formBuilder: FormBuilder,
private i18nService: I18nService,
) {}
ngOnInit() {
if (this.originalCipherView?.card) {
this.setInitialValues();
}
this.sshKeyForm.disable();
}
/** Set form initial form values from the current cipher */
private setInitialValues() {
const { privateKey, publicKey, keyFingerprint } = this.originalCipherView.sshKey;
this.sshKeyForm.setValue({
privateKey,
publicKey,
keyFingerprint,
});
}
}

View File

@@ -40,6 +40,9 @@
<app-view-identity-sections *ngIf="cipher.identity" [cipher]="cipher">
</app-view-identity-sections>
<!-- SshKEY SECTIONS -->
<app-sshkey-view *ngIf="hasSshKey" [sshKey]="cipher.sshKey"></app-sshkey-view>
<!-- ADDITIONAL OPTIONS -->
<ng-container *ngIf="cipher.notes">
<app-additional-options [notes]="cipher.notes"> </app-additional-options>

View File

@@ -22,6 +22,7 @@ import { CustomFieldV2Component } from "./custom-fields/custom-fields-v2.compone
import { ItemDetailsV2Component } from "./item-details/item-details-v2.component";
import { ItemHistoryV2Component } from "./item-history/item-history-v2.component";
import { LoginCredentialsViewComponent } from "./login-credentials/login-credentials-view.component";
import { SshKeyViewComponent } from "./sshkey-sections/sshkey-view.component";
import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-identity-sections.component";
@Component({
@@ -39,6 +40,7 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
ItemHistoryV2Component,
CustomFieldV2Component,
CardDetailsComponent,
SshKeyViewComponent,
ViewIdentitySectionsComponent,
LoginCredentialsViewComponent,
AutofillOptionsViewComponent,
@@ -99,6 +101,10 @@ export class CipherViewComponent implements OnChanges, OnDestroy {
return this.cipher.login?.uris.length > 0;
}
get hasSshKey() {
return this.cipher.sshKey?.privateKey;
}
async loadCipherData() {
// Load collections if not provided and the cipher has collectionIds
if (

View File

@@ -0,0 +1,50 @@
<bit-section>
<bit-section-header>
<h2 bitTypography="h6">{{ "typeSshKey" | i18n }}</h2>
</bit-section-header>
<bit-card class="[&_bit-form-field:last-of-type]:tw-mb-0">
<bit-form-field>
<bit-label>{{ "sshPrivateKey" | i18n }}</bit-label>
<input readonly bitInput [value]="sshKey.privateKey" aria-readonly="true" type="password" />
<button
bitSuffix
type="button"
bitIconButton
bitPasswordInputToggle
data-testid="toggle-privateKey"
></button>
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="sshKey.privateKey"
showToast
[appA11yTitle]="'copyValue' | i18n"
></button>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "sshPublicKey" | i18n }}</bit-label>
<input readonly bitInput [value]="sshKey.publicKey" aria-readonly="true" />
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="sshKey.publicKey"
showToast
[appA11yTitle]="'copyValue' | i18n"
></button>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "sshFingerprint" | i18n }}</bit-label>
<input readonly bitInput [value]="sshKey.keyFingerprint" aria-readonly="true" />
<button
bitIconButton="bwi-clone"
bitSuffix
type="button"
[appCopyClick]="sshKey.keyFingerprint"
showToast
[appA11yTitle]="'copyValue' | i18n"
></button>
</bit-form-field>
</bit-card>
</bit-section>

View File

@@ -0,0 +1,35 @@
import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SshKeyView } from "@bitwarden/common/vault/models/view/ssh-key.view";
import {
CardComponent,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
FormFieldModule,
IconButtonModule,
} from "@bitwarden/components";
import { OrgIconDirective } from "../../components/org-icon.directive";
@Component({
selector: "app-sshkey-view",
templateUrl: "sshkey-view.component.html",
standalone: true,
imports: [
CommonModule,
JslibModule,
CardComponent,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
OrgIconDirective,
FormFieldModule,
IconButtonModule,
],
})
export class SshKeyViewComponent {
@Input() sshKey: SshKeyView;
}

View File

@@ -91,6 +91,12 @@ export class CopyCipherFieldDirective implements OnChanges {
return this.cipher.identity?.fullAddressForCopy;
case "secureNote":
return this.cipher.notes;
case "privateKey":
return this.cipher.sshKey?.privateKey;
case "publicKey":
return this.cipher.sshKey?.publicKey;
case "keyFingerprint":
return this.cipher.sshKey?.keyFingerprint;
default:
return null;
}

View File

@@ -15,10 +15,10 @@
bitIconButton="bwi-clone"
[appA11yTitle]="'copyPassword' | i18n"
appStopClick
(click)="copy(h.password)"
>
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
</button>
[appCopyClick]="h.password"
[valueLabel]="'password' | i18n"
showToast
></button>
</bit-item-action>
</ng-container>
</bit-item>

View File

@@ -3,14 +3,13 @@ import { By } from "@angular/platform-browser";
import { BehaviorSubject } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { ColorPasswordModule, ItemModule, ToastService } from "@bitwarden/components";
import { ColorPasswordModule, ItemModule } from "@bitwarden/components";
import { ColorPasswordComponent } from "@bitwarden/components/src/color-password/color-password.component";
import { PasswordHistoryViewComponent } from "./password-history-view.component";
@@ -25,8 +24,6 @@ describe("PasswordHistoryViewComponent", () => {
organizationId: "222-444-555",
} as CipherView;
const copyToClipboard = jest.fn();
const showToast = jest.fn();
const activeAccount$ = new BehaviorSubject<{ id: string }>({ id: "666-444-444" });
const mockCipherService = {
get: jest.fn().mockResolvedValue({ decrypt: jest.fn().mockResolvedValue(mockCipher) }),
@@ -36,17 +33,13 @@ describe("PasswordHistoryViewComponent", () => {
beforeEach(async () => {
mockCipherService.get.mockClear();
mockCipherService.getKeyForCipherKeyDecryption.mockClear();
copyToClipboard.mockClear();
showToast.mockClear();
await TestBed.configureTestingModule({
imports: [ItemModule, ColorPasswordModule, JslibModule],
providers: [
{ provide: WINDOW, useValue: window },
{ provide: CipherService, useValue: mockCipherService },
{ provide: PlatformUtilsService, useValue: { copyToClipboard } },
{ provide: PlatformUtilsService },
{ provide: AccountService, useValue: { activeAccount$ } },
{ provide: ToastService, useValue: { showToast } },
{ provide: I18nService, useValue: { t: (key: string) => key } },
],
}).compileComponents();
@@ -80,18 +73,5 @@ describe("PasswordHistoryViewComponent", () => {
"bad-password-2",
]);
});
it("copies a password", () => {
const copyButton = fixture.debugElement.query(By.css("button"));
copyButton.nativeElement.click();
expect(copyToClipboard).toHaveBeenCalledWith("bad-password-1", { window: window });
expect(showToast).toHaveBeenCalledWith({
message: "passwordCopied",
title: "",
variant: "info",
});
});
});
});

View File

@@ -1,21 +1,14 @@
import { CommonModule } from "@angular/common";
import { OnInit, Inject, Component, Input } from "@angular/core";
import { OnInit, Component, Input } from "@angular/core";
import { firstValueFrom, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PasswordHistoryView } from "@bitwarden/common/vault/models/view/password-history.view";
import {
ToastService,
ItemModule,
ColorPasswordModule,
IconButtonModule,
} from "@bitwarden/components";
import { ItemModule, ColorPasswordModule, IconButtonModule } from "@bitwarden/components";
@Component({
selector: "vault-password-history-view",
@@ -33,29 +26,15 @@ export class PasswordHistoryViewComponent implements OnInit {
history: PasswordHistoryView[] = [];
constructor(
@Inject(WINDOW) private win: Window,
protected cipherService: CipherService,
protected platformUtilsService: PlatformUtilsService,
protected i18nService: I18nService,
protected accountService: AccountService,
protected toastService: ToastService,
) {}
async ngOnInit() {
await this.init();
}
/** Copies a password to the clipboard. */
copy(password: string) {
const copyOptions = this.win != null ? { window: this.win } : undefined;
this.platformUtilsService.copyToClipboard(password, copyOptions);
this.toastService.showToast({
variant: "info",
title: "",
message: this.i18nService.t("passwordCopied"),
});
}
/** Retrieve the password history for the given cipher */
protected async init() {
const cipher = await this.cipherService.get(this.cipherId);

View File

@@ -25,7 +25,10 @@ export type CopyAction =
| "phone"
| "address"
| "secureNote"
| "hiddenField";
| "hiddenField"
| "privateKey"
| "publicKey"
| "keyFingerprint";
type CopyActionInfo = {
/**
@@ -62,6 +65,9 @@ const CopyActions: Record<CopyAction, CopyActionInfo> = {
phone: { typeI18nKey: "phone", protected: true },
address: { typeI18nKey: "address", protected: true },
secureNote: { typeI18nKey: "note", protected: true },
privateKey: { typeI18nKey: "sshPrivateKey", protected: true },
publicKey: { typeI18nKey: "sshPublicKey", protected: true },
keyFingerprint: { typeI18nKey: "sshFingerprint", protected: true },
hiddenField: {
typeI18nKey: "value",
protected: true,