1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 21:50:15 +00:00

Merge branch 'main' into pm-18701-optional-payment-modal-after-signup

This commit is contained in:
cyprain-okeke
2025-06-20 12:59:40 +01:00
committed by GitHub
60 changed files with 426 additions and 350 deletions

2
.github/CODEOWNERS vendored
View File

@@ -81,7 +81,9 @@ bitwarden_license/bit-web/src/app/billing @bitwarden/team-billing-dev
apps/browser/src/platform @bitwarden/team-platform-dev
apps/cli/src/platform @bitwarden/team-platform-dev
apps/desktop/macos @bitwarden/team-platform-dev
apps/desktop/scripts @bitwarden/team-platform-dev
apps/desktop/src/platform @bitwarden/team-platform-dev
apps/desktop/resources @bitwarden/team-platform-dev
apps/web/src/app/platform @bitwarden/team-platform-dev
libs/angular/src/platform @bitwarden/team-platform-dev
libs/common/src/platform @bitwarden/team-platform-dev

View File

@@ -1922,6 +1922,9 @@
"typeSshKey": {
"message": "SSH key"
},
"typeNote": {
"message": "Note"
},
"newItemHeader": {
"message": "New $TYPE$",
"placeholders": {

View File

@@ -45,6 +45,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import {
CardComponent,
CheckboxModule,
@@ -58,7 +59,6 @@ import {
SelectModule,
TypographyModule,
} from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
import { AutofillBrowserSettingsService } from "../../../autofill/services/autofill-browser-settings.service";
import { BrowserApi } from "../../../platform/browser/browser-api";

View File

@@ -1,7 +1,3 @@
// FIXME (PM-22628): angular imports are forbidden in background
// eslint-disable-next-line no-restricted-imports
import { Injectable } from "@angular/core";
import { VaultTimeoutSettingsService } from "@bitwarden/common/key-management/vault-timeout";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
@@ -20,7 +16,6 @@ import {
import { NativeMessagingBackground } from "../../background/nativeMessaging.background";
import { BrowserApi } from "../../platform/browser/browser-api";
@Injectable()
export class BackgroundBrowserBiometricsService extends BiometricsService {
constructor(
private nativeMessagingBackground: () => NativeMessagingBackground,

View File

@@ -12,8 +12,11 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums";
import {
RestrictedCipherType,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";

View File

@@ -8,9 +8,10 @@ import { map, Observable } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherType } from "@bitwarden/common/vault/enums";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CipherMenuItem, CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
import { ButtonModule, DialogService, MenuModule, NoItemsModule } from "@bitwarden/components";
import { AddEditFolderDialogComponent, RestrictedItemTypesService } from "@bitwarden/vault";
import { AddEditFolderDialogComponent } from "@bitwarden/vault";
import { BrowserApi } from "../../../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";

View File

@@ -20,7 +20,10 @@ import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folde
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { RestrictedCipherType, RestrictedItemTypesService } from "@bitwarden/vault";
import {
RestrictedCipherType,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import {
CachedFilterState,

View File

@@ -39,9 +39,12 @@ import { ITreeNodeObject, TreeNode } from "@bitwarden/common/vault/models/domain
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { ServiceUtils } from "@bitwarden/common/vault/service-utils";
import {
isCipherViewRestricted,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
import { ChipSelectOption } from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
const FILTER_VISIBILITY_KEY = new KeyDefinition<boolean>(VAULT_SETTINGS_DISK, "filterVisibility", {
deserializer: (obj) => obj,
@@ -227,18 +230,8 @@ export class VaultPopupListFiltersService {
}
// Check if cipher type is restricted (with organization exemptions)
if (restrictions && restrictions.length > 0) {
const isRestricted = restrictions.some(
(restrictedType) =>
restrictedType.cipherType === cipher.type &&
(cipher.organizationId
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
: restrictedType.allowViewOrgIds.length === 0),
);
if (isRestricted) {
return false;
}
if (isCipherViewRestricted(cipher, restrictions)) {
return false;
}
if (filters.cipherType !== null && cipher.type !== filters.cipherType) {

View File

@@ -89,7 +89,7 @@ async function run(context) {
} else {
// For non-Appstore builds, we don't need the inherit binary as they are not sandboxed,
// but we sign and include it anyway for consistency. It should be removed once DDG supports the proxy directly.
const entitlementsName = "entitlements.mac.plist";
const entitlementsName = "entitlements.mac.inherit.plist";
const entitlementsPath = path.join(__dirname, "..", "resources", entitlementsName);
child_process.execSync(
`codesign -s '${id}' -i ${packageId} -f --timestamp --options runtime --entitlements ${entitlementsPath} ${proxyPath}`,

View File

@@ -23,6 +23,9 @@
"typeIdentity": {
"message": "Identity"
},
"typeNote": {
"message": "Note"
},
"typeSecureNote": {
"message": "Secure note"
},

View File

@@ -12,7 +12,9 @@
<div class="box-content-row" *ngIf="!editMode" appBoxRow>
<label for="type">{{ "type" | i18n }}</label>
<select id="type" name="Type" [(ngModel)]="cipher.type" (change)="typeChange()">
<option *ngFor="let o of typeOptions" [ngValue]="o.value">{{ o.name }}</option>
<option *ngFor="let item of menuItems$ | async" [ngValue]="item.type">
{{ item.labelKey | i18n }}
</option>
</select>
</div>
<div class="box-content-row" appBoxRow>

View File

@@ -3,6 +3,7 @@
import { DatePipe } from "@angular/common";
import { Component, NgZone, OnChanges, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { NgForm } from "@angular/forms";
import { map, shareReplay } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common";
import { AddEditComponent as BaseAddEditComponent } from "@bitwarden/angular/vault/components/add-edit.component";
@@ -22,6 +23,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
import { DialogService, ToastService } from "@bitwarden/components";
import { PasswordRepromptService, SshImportPromptService } from "@bitwarden/vault";
@@ -35,6 +38,18 @@ const BroadcasterSubscriptionId = "AddEditComponent";
export class AddEditComponent extends BaseAddEditComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild("form")
private form: NgForm;
menuItems$ = this.restrictedItemTypesService.restricted$.pipe(
map((restrictedItemTypes) =>
// Filter out restricted item types from the default CIPHER_MENU_ITEMS array
CIPHER_MENU_ITEMS.filter(
(typeOption) =>
!restrictedItemTypes.some(
(restrictedType) => restrictedType.cipherType === typeOption.type,
),
),
),
shareReplay({ bufferSize: 1, refCount: true }),
);
constructor(
cipherService: CipherService,
@@ -59,6 +74,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
cipherAuthorizationService: CipherAuthorizationService,
sdkService: SdkService,
sshImportPromptService: SshImportPromptService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) {
super(
cipherService,

View File

@@ -36,7 +36,7 @@
class="primary"
(click)="restore()"
appA11yTitle="{{ 'restore' | i18n }}"
*ngIf="cipher.isDeleted"
*ngIf="cipher.isDeleted && cipher.permissions.restore"
>
<i class="bwi bwi-undo bwi-fw bwi-lg" aria-hidden="true"></i>
</button>
@@ -50,7 +50,7 @@
<i class="bwi bwi-files bwi-fw bwi-lg" aria-hidden="true"></i>
</button>
</ng-container>
<div class="right" *ngIf="((canDeleteCipher$ | async) && action === 'edit') || action === 'view'">
<div class="right" *ngIf="cipher.permissions.delete && (action === 'edit' || action === 'view')">
<button
type="button"
(click)="delete()"

View File

@@ -1,6 +1,6 @@
import { CommonModule } from "@angular/common";
import { Input, Output, EventEmitter, Component, OnInit, ViewChild } from "@angular/core";
import { Observable, firstValueFrom } from "rxjs";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
@@ -25,6 +25,7 @@ export class ItemFooterComponent implements OnInit {
@Input() collectionId: string | null = null;
@Input({ required: true }) action: string = "view";
@Input() isSubmitting: boolean = false;
@Input() masterPasswordAlreadyPrompted: boolean = false;
@Output() onEdit = new EventEmitter<CipherView>();
@Output() onClone = new EventEmitter<CipherView>();
@Output() onDelete = new EventEmitter<CipherView>();
@@ -32,10 +33,8 @@ export class ItemFooterComponent implements OnInit {
@Output() onCancel = new EventEmitter<CipherView>();
@ViewChild("submitBtn", { static: false }) submitBtn: ButtonComponent | null = null;
canDeleteCipher$: Observable<boolean> = new Observable();
activeUserId: UserId | null = null;
private passwordReprompted = false;
passwordReprompted: boolean = false;
constructor(
protected cipherService: CipherService,
@@ -49,8 +48,8 @@ export class ItemFooterComponent implements OnInit {
) {}
async ngOnInit() {
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher);
this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.passwordReprompted = this.masterPasswordAlreadyPrompted;
}
async clone() {

View File

@@ -20,78 +20,20 @@
</h2>
</div>
<ul id="type-filters" *ngIf="!isCollapsed" class="filter-options">
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Login }"
>
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.Login)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Login"
>
<i class="bwi bwi-fw bwi-globe" aria-hidden="true"></i>&nbsp;{{ "typeLogin" | i18n }}
</button>
</span>
</li>
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Card }">
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.Card)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Card"
>
<i class="bwi bwi-fw bwi-credit-card" aria-hidden="true"></i>&nbsp;{{ "typeCard" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.Identity }"
>
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.Identity)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.Identity"
>
<i class="bwi bwi-fw bwi-id-card" aria-hidden="true"></i>&nbsp;{{ "typeIdentity" | i18n }}
</button>
</span>
</li>
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SecureNote }"
>
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.SecureNote)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.SecureNote"
>
<i class="bwi bwi-fw bwi-sticky-note" aria-hidden="true"></i>&nbsp;{{
"typeSecureNote" | i18n
}}
</button>
</span>
</li>
<li
class="filter-option"
[ngClass]="{ active: activeFilter.cipherType === cipherTypeEnum.SshKey }"
>
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(cipherTypeEnum.SshKey)"
[attr.aria-pressed]="activeFilter.cipherType === cipherTypeEnum.SshKey"
>
<i class="bwi bwi-fw bwi-key" aria-hidden="true"></i>&nbsp;{{ "typeSshKey" | i18n }}
</button>
</span>
</li>
@for (typeFilter of typeFilters$ | async; track typeFilter) {
<li class="filter-option" [ngClass]="{ active: activeFilter.cipherType === typeFilter.type }">
<span class="filter-buttons">
<button
type="button"
class="filter-button"
(click)="applyFilter(typeFilter.type)"
[attr.aria-pressed]="activeFilter.cipherType === typeFilter.type"
>
<i class="bwi bwi-fw {{ typeFilter.icon }}" aria-hidden="true"></i>&nbsp;{{
typeFilter.labelKey | i18n
}}
</button>
</span>
</li>
}
</ul>

View File

@@ -1,6 +1,9 @@
import { Component } from "@angular/core";
import { map, shareReplay } from "rxjs";
import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angular/vault/vault-filter/components/type-filter.component";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
@Component({
selector: "app-type-filter",
@@ -8,7 +11,22 @@ import { TypeFilterComponent as BaseTypeFilterComponent } from "@bitwarden/angul
standalone: false,
})
export class TypeFilterComponent extends BaseTypeFilterComponent {
constructor() {
protected typeFilters$ = this.restrictedItemTypesService.restricted$.pipe(
map((restrictedItemTypes) =>
// Filter out restricted item types from the typeFilters array
CIPHER_MENU_ITEMS.filter(
(typeFilter) =>
!restrictedItemTypes.some(
(restrictedType) =>
restrictedType.allowViewOrgIds.length === 0 &&
restrictedType.cipherType === typeFilter.type,
),
),
),
shareReplay({ bufferSize: 1, refCount: true }),
);
constructor(private restrictedItemTypesService: RestrictedItemTypesService) {
super();
}
}

View File

@@ -72,25 +72,11 @@
<i class="bwi bwi-plus bwi-lg" aria-hidden="true"></i>
</button>
<bit-menu #addCipherMenu>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe tw-mr-1" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card tw-mr-1" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card tw-mr-1" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note tw-mr-1" aria-hidden="true"></i>
{{ "typeSecureNote" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SshKey)">
<i class="bwi bwi-key tw-mr-1" aria-hidden="true"></i>
{{ "typeSshKey" | i18n }}
</button>
@for (itemTypes of itemTypes$ | async; track itemTypes.type) {
<button type="button" bitMenuItem (click)="addCipher(itemTypes.type)">
<i class="bwi {{ itemTypes.icon }} tw-mr-1" aria-hidden="true"></i>
{{ itemTypes.labelKey | i18n }}
</button>
}
</bit-menu>
</ng-template>

View File

@@ -10,6 +10,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { MenuModule } from "@bitwarden/components";
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
@@ -25,8 +26,9 @@ export class VaultItemsV2Component extends BaseVaultItemsComponent {
private readonly searchBarService: SearchBarService,
cipherService: CipherService,
accountService: AccountService,
restrictedItemTypesService: RestrictedItemTypesService,
) {
super(searchService, cipherService, accountService);
super(searchService, cipherService, accountService, restrictedItemTypesService);
this.searchBarService.searchText$
.pipe(distinctUntilChanged(), takeUntilDestroyed())

View File

@@ -8,6 +8,7 @@ import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { SearchBarService } from "../../../app/layout/search/search-bar.service";
@@ -22,8 +23,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent {
searchBarService: SearchBarService,
cipherService: CipherService,
accountService: AccountService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) {
super(searchService, cipherService, accountService);
super(searchService, cipherService, accountService, restrictedItemTypesService);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => {

View File

@@ -20,6 +20,7 @@
(onDelete)="deleteCipher()"
(onCancel)="cancelCipher($event)"
[isSubmitting]="isSubmitting"
[masterPasswordAlreadyPrompted]="cipherRepromptId === cipherId"
></app-vault-item-footer>
<div class="content">
<div class="inner-content">

View File

@@ -13,8 +13,6 @@ import { firstValueFrom, Subject, takeUntil, switchMap, lastValueFrom, Observabl
import { filter, map, take } from "rxjs/operators";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { VaultViewPasswordHistoryService } from "@bitwarden/angular/services/view-password-history.service";
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
@@ -45,6 +43,8 @@ import {
DialogService,
ItemModule,
ToastService,
CopyClickListener,
COPY_CLICK_LISTENER,
} from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import {
@@ -64,6 +64,7 @@ import {
DefaultChangeLoginPasswordService,
DefaultCipherFormConfigService,
PasswordRepromptService,
CipherFormComponent,
} from "@bitwarden/vault";
import { NavComponent } from "../../../app/layout/nav.component";
@@ -114,15 +115,21 @@ const BroadcasterSubscriptionId = "VaultComponent";
useClass: DesktopPremiumUpgradePromptService,
},
{ provide: CipherFormGenerationService, useClass: DesktopCredentialGenerationService },
{
provide: COPY_CLICK_LISTENER,
useExisting: VaultV2Component,
},
],
})
export class VaultV2Component implements OnInit, OnDestroy {
export class VaultV2Component implements OnInit, OnDestroy, CopyClickListener {
@ViewChild(VaultItemsV2Component, { static: true })
vaultItemsComponent: VaultItemsV2Component | null = null;
@ViewChild(VaultFilterComponent, { static: true })
vaultFilterComponent: VaultFilterComponent | null = null;
@ViewChild("folderAddEdit", { read: ViewContainerRef, static: true })
folderAddEditModalRef: ViewContainerRef | null = null;
@ViewChild(CipherFormComponent)
cipherFormComponent: CipherFormComponent | null = null;
action: CipherFormMode | "view" | null = null;
cipherId: string | null = null;
@@ -158,7 +165,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
),
);
private modal: ModalRef | null = null;
private componentIsDestroyed$ = new Subject<boolean>();
private allOrganizations: Organization[] = [];
private allCollections: CollectionView[] = [];
@@ -167,7 +173,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
private route: ActivatedRoute,
private router: Router,
private i18nService: I18nService,
private modalService: ModalService,
private broadcasterService: BroadcasterService,
private changeDetectorRef: ChangeDetectorRef,
private ngZone: NgZone,
@@ -375,6 +380,13 @@ export class VaultV2Component implements OnInit, OnDestroy {
}
}
/**
* Handler for Vault level CopyClickDirectives to send the minimizeOnCopy message
*/
onCopy() {
this.messagingService.send("minimizeOnCopy");
}
async viewCipher(cipher: CipherView) {
if (await this.shouldReprompt(cipher, "view")) {
return;
@@ -410,6 +422,26 @@ export class VaultV2Component implements OnInit, OnDestroy {
result?.action === AttachmentDialogResult.Uploaded
) {
await this.vaultItemsComponent?.refresh().catch(() => {});
if (this.cipherFormComponent == null) {
return;
}
const updatedCipher = await this.cipherService.get(
this.cipherId as CipherId,
this.activeUserId as UserId,
);
const updatedCipherView = await this.cipherService.decrypt(
updatedCipher,
this.activeUserId as UserId,
);
this.cipherFormComponent.patchCipher((currentCipher) => {
currentCipher.attachments = updatedCipherView.attachments;
currentCipher.revisionDate = updatedCipherView.revisionDate;
return currentCipher;
});
}
}
@@ -712,10 +744,17 @@ export class VaultV2Component implements OnInit, OnDestroy {
}
async editFolder(folderId: string) {
if (!this.activeUserId) {
return;
}
const folderView = await firstValueFrom(
this.folderService.getDecrypted$(folderId, this.activeUserId),
);
if (!folderView) {
return;
}
const dialogRef = AddEditFolderDialogComponent.open(this.dialogService, {
editFolderConfig: {
folder: {
@@ -730,7 +769,7 @@ export class VaultV2Component implements OnInit, OnDestroy {
result === AddEditFolderDialogResult.Deleted ||
result === AddEditFolderDialogResult.Created
) {
await this.vaultFilterComponent.reloadCollectionsAndFolders(this.activeFilter);
await this.vaultFilterComponent?.reloadCollectionsAndFolders(this.activeFilter);
}
}
@@ -775,10 +814,6 @@ export class VaultV2Component implements OnInit, OnDestroy {
.catch(() => {});
}
private addCipherWithChangeDetection(type: CipherType) {
this.functionWithChangeDetection(() => this.addCipher(type).catch(() => {}));
}
private copyValue(cipher: CipherView, value: string, labelI18nKey: string, aType: string) {
this.functionWithChangeDetection(() => {
(async () => {

View File

@@ -11,8 +11,8 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
import { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
import { VaultFilterService } from "../../../../vault/individual-vault/vault-filter/services/abstractions/vault-filter.service";

View File

@@ -1,22 +1,21 @@
<!-- Please remove this disable statement when editing this file! -->
<!-- eslint-disable tailwindcss/no-custom-classname -->
<div class="tw-flex" *ngIf="!hideMultiSelect">
<bit-form-field *ngIf="permissionMode == 'edit'" class="tw-mr-3 tw-shrink-0">
<bit-form-field *ngIf="permissionMode == 'edit'" class="tw-mr-3 tw-shrink-0 tw-basis-2/5">
<bit-label>{{ "permission" | i18n }}</bit-label>
<select
<bit-select
bitInput
[disabled]="disabled"
[(ngModel)]="initialPermission"
[ngModelOptions]="{ standalone: true }"
(blur)="handleBlur()"
(closed)="handleBlur()"
>
<option *ngFor="let p of permissionList" [value]="p.perm">
{{ p.labelId | i18n }}
</option>
</select>
<bit-option *ngFor="let p of permissionList" [value]="p.perm" [label]="p.labelId | i18n">
</bit-option>
</bit-select>
</bit-form-field>
<bit-form-field class="tw-grow" *ngIf="!disabled">
<bit-form-field class="tw-grow tw-p-3" *ngIf="!disabled">
<bit-label>{{ selectorLabelText }}</bit-label>
<bit-multi-select
class="tw-w-full"
@@ -51,7 +50,7 @@
[formGroupName]="i"
[ngClass]="{ 'tw-text-muted': item.readonly }"
>
<td bitCell [ngSwitch]="item.type">
<td bitCell [ngSwitch]="item.type" class="tw-w-5/12">
<div class="tw-flex tw-items-center" *ngSwitchCase="itemType.Member">
<bit-avatar size="small" class="tw-mr-3" text="{{ item.labelName }}"></bit-avatar>
<div class="tw-flex tw-flex-col">
@@ -79,28 +78,22 @@
<td bitCell *ngIf="permissionMode != 'hidden'">
<ng-container *ngIf="canEditItemPermission(item); else readOnlyPerm">
<label class="tw-sr-only" [for]="'permission' + i"
>{{ item.labelName }} {{ "permission" | i18n }}</label
>
<div class="tw-relative tw-inline-block">
<select
<bit-form-field>
<bit-label>{{ item.labelName }} {{ "permission" | i18n }}</bit-label>
<bit-select
bitInput
class="tw-apperance-none -tw-ml-3 tw-max-w-40 tw-appearance-none tw-overflow-ellipsis !tw-rounded tw-border-transparent !tw-bg-transparent tw-pr-6 tw-font-bold hover:tw-border-primary-700"
formControlName="permission"
[id]="'permission' + i"
(blur)="handleBlur()"
(closed)="handleBlur()"
>
<option *ngFor="let p of permissionList" [value]="p.perm">
{{ p.labelId | i18n }}
</option>
</select>
<label
[for]="'permission' + i"
class="tw-absolute tw-inset-y-0 tw-right-4 tw-mb-0 tw-flex tw-items-center"
>
<i class="bwi bwi-sm bwi-angle-down tw-leading-[0]"></i>
</label>
</div>
<bit-option
*ngFor="let p of permissionList"
[value]="p.perm"
[label]="p.labelId | i18n"
>
</bit-option>
</bit-select>
</bit-form-field>
</ng-container>
<ng-template #readOnlyPerm>

View File

@@ -14,6 +14,7 @@ import {
ButtonModule,
FormFieldModule,
IconButtonModule,
SelectModule,
TableModule,
TabsModule,
} from "@bitwarden/components";
@@ -71,6 +72,7 @@ describe("AccessSelectorComponent", () => {
PreloadedEnglishI18nModule,
JslibModule,
IconButtonModule,
SelectModule,
],
declarations: [TestableAccessSelectorComponent, UserTypePipe],
providers: [],

View File

@@ -10,6 +10,7 @@ import {
DialogModule,
FormFieldModule,
IconButtonModule,
SelectModule,
TableModule,
TabsModule,
} from "@bitwarden/components";
@@ -47,6 +48,7 @@ export default {
TableModule,
JslibModule,
IconButtonModule,
SelectModule,
],
providers: [],
}),

View File

@@ -35,8 +35,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
import { LoginView } from "@bitwarden/common/vault/models/view/login.view";
import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { LayoutComponent } from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
import { GroupView } from "../../../admin-console/organizations/core";
import { PreloadedEnglishI18nModule } from "../../../core/tests";

View File

@@ -22,8 +22,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { DialogService, ToastService } from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
import { TrialFlowService } from "../../../../billing/services/trial-flow.service";
import { VaultFilterService } from "../services/abstractions/vault-filter.service";

View File

@@ -3,7 +3,7 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { RestrictedCipherType } from "@bitwarden/vault";
import { RestrictedCipherType } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { createFilterFunction } from "./filter-function";
import { All } from "./routed-vault-filter.model";

View File

@@ -1,7 +1,10 @@
import { Unassigned } from "@bitwarden/admin-console/common";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { RestrictedCipherType } from "@bitwarden/vault";
import {
isCipherViewRestricted,
RestrictedCipherType,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model";
@@ -83,24 +86,9 @@ export function createFilterFunction(
) {
return false;
}
// Restricted types
if (restrictedTypes && restrictedTypes.length > 0) {
// Filter the cipher if that type is restricted unless
// - The cipher belongs to an organization and that organization allows viewing the cipher type
// OR
// - The cipher belongs to the user's personal vault and at least one other organization does not restrict that type
if (
restrictedTypes.some(
(restrictedType) =>
restrictedType.cipherType === cipher.type &&
(cipher.organizationId
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
: restrictedType.allowViewOrgIds.length === 0),
)
) {
return false;
}
if (restrictedTypes && isCipherViewRestricted(cipher, restrictedTypes)) {
return false;
}
return true;
};

View File

@@ -18,13 +18,13 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import {
BreadcrumbsModule,
DialogService,
MenuModule,
SimpleDialogOptions,
} from "@bitwarden/components";
import { RestrictedItemTypesService } from "@bitwarden/vault";
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared/components/collection-dialog";
import { HeaderModule } from "../../../layouts/header/header.module";

View File

@@ -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 { filterOutNullish } from "@bitwarden/common/vault/utils/observable-utilities";
import { DialogRef, DialogService, Icons, ToastService } from "@bitwarden/components";
import {
@@ -79,7 +80,6 @@ import {
DecryptionFailureDialogComponent,
DefaultCipherFormConfigService,
PasswordRepromptService,
RestrictedItemTypesService,
} from "@bitwarden/vault";
import {

View File

@@ -7621,9 +7621,9 @@
"message": "Service account updated",
"description": "Notifies that a service account has been updated"
},
"newSaSelectAccess": {
"message": "Type or select projects or secrets",
"description": "Instructions for selecting projects or secrets for a new service account"
"typeOrSelectProjects": {
"message": "Type or select projects",
"description": "Instructions for selecting projects for a service account"
},
"newSaTypeToFilter": {
"message": "Type to filter",

View File

@@ -1,5 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
<div class="tw-w-2/5">
<div class="tw-w-full tw-max-w-[1200px]">
<p class="tw-mt-8" *ngIf="!loading">
{{ "projectPeopleDescription" | i18n }}
</p>

View File

@@ -1,5 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
<div class="tw-w-2/5">
<div class="tw-w-full tw-max-w-[1200px]">
<p class="tw-mt-8" *ngIf="!loading">
{{ "projectMachineAccountsDescription" | i18n }}
</p>

View File

@@ -2,9 +2,9 @@
// @ts-strict-ignore
import { Component, Inject, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DialogRef, DIALOG_DATA, BitValidators, ToastService } from "@bitwarden/components";
import { ServiceAccountView } from "../../models/view/service-account.view";
@@ -46,8 +46,8 @@ export class ServiceAccountDialogComponent implements OnInit {
@Inject(DIALOG_DATA) private data: ServiceAccountOperation,
private serviceAccountService: ServiceAccountService,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private router: Router,
) {}
async ngOnInit() {
@@ -87,8 +87,17 @@ export class ServiceAccountDialogComponent implements OnInit {
let serviceAccountMessage: string;
if (this.data.operation == OperationType.Add) {
await this.serviceAccountService.create(this.data.organizationId, serviceAccountView);
const newServiceAccount = await this.serviceAccountService.create(
this.data.organizationId,
serviceAccountView,
);
serviceAccountMessage = this.i18nService.t("machineAccountCreated");
await this.router.navigate([
"sm",
this.data.organizationId,
"machine-accounts",
newServiceAccount.id,
]);
} else {
await this.serviceAccountService.update(
this.data.serviceAccountId,

View File

@@ -1,5 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
<div class="tw-w-2/5">
<div class="tw-w-full tw-max-w-[1200px]">
<p class="tw-mt-8" *ngIf="!loading">
{{ "machineAccountPeopleDescription" | i18n }}
</p>

View File

@@ -1,5 +1,5 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" *ngIf="!loading; else spinner">
<div class="tw-w-2/5">
<div class="tw-w-full tw-max-w-[1200px]">
<p class="tw-mt-8" *ngIf="!loading">
{{ "machineAccountProjectsDescription" | i18n }}
</p>
@@ -9,7 +9,7 @@
[addButtonMode]="true"
[items]="potentialGrantees"
[label]="'projects' | i18n"
[hint]="'newSaSelectAccess' | i18n"
[hint]="'typeOrSelectProjects' | i18n"
[columnTitle]="'projects' | i18n"
[emptyMessage]="'serviceAccountEmptyProjectAccesspolicies' | i18n"
>

View File

@@ -91,7 +91,10 @@ export class ServiceAccountService {
);
}
async create(organizationId: string, serviceAccountView: ServiceAccountView) {
async create(
organizationId: string,
serviceAccountView: ServiceAccountView,
): Promise<ServiceAccountView> {
const orgKey = await this.getOrganizationKey(organizationId);
const request = await this.getServiceAccountRequest(orgKey, serviceAccountView);
const r = await this.apiService.send(
@@ -101,9 +104,14 @@ export class ServiceAccountService {
true,
true,
);
this._serviceAccount.next(
await this.createServiceAccountView(orgKey, new ServiceAccountResponse(r)),
const serviceAccount = await this.createServiceAccountView(
orgKey,
new ServiceAccountResponse(r),
);
this._serviceAccount.next(serviceAccount);
return serviceAccount;
}
async delete(serviceAccounts: ServiceAccountView[]): Promise<BulkOperationStatus[]> {

View File

@@ -1,6 +1,6 @@
export * from "./auth.guard";
export * from "./active-auth.guard";
export * from "./lock.guard";
export * from "./redirect.guard";
export * from "./redirect/redirect.guard";
export * from "./tde-decryption-required.guard";
export * from "./unauth.guard";

View File

@@ -0,0 +1,53 @@
# Redirect Guard
The `redirectGuard` redirects the user based on their `AuthenticationStatus`. It is applied to the root route (`/`).
<br>
### Order of Operations
The `redirectGuard` will redirect the user based on the following checks, _in order_:
- **`AuthenticationStatus.LoggedOut`** &rarr; redirect to `/login`
- **`AuthenticationStatus.Unlocked`** &rarr; redirect to `/vault`
- **`AuthenticationStatus.Locked`**
- **TDE Locked State** &rarr; redirect to `/login-initiated`
- A user is in a TDE Locked State if they meet all 3 of the following conditions
1. Auth status is `Locked`
2. TDE is enabled
3. User has never had a user key (that is, user has not unlocked/decrypted yet)
- **Standard Locked State** &rarr; redirect to `/lock`
<br>
| Order | AuthenticationStatus | Redirect To |
| ----- | ------------------------------------------------------------------------------- | ------------------ |
| 1 | `LoggedOut` | `/login` |
| 2 | `Unlocked` | `/vault` |
| 3 | **TDE Locked State** <br> `Locked` + <br> `tdeEnabled` + <br> `!everHadUserKey` | `/login-initiated` |
| 4 | **Standard Locked State** <br> `Locked` | `/lock` |
<br>
### Default Routes and Route Overrides
The default redirect routes are mapped to object properties:
```typescript
const defaultRoutes: RedirectRoutes = {
loggedIn: "/vault",
loggedOut: "/login",
locked: "/lock",
notDecrypted: "/login-initiated",
};
```
But when applying the guard to the root route, the developer can override specific redirect routes by passing in a custom object. This is useful for subtle differences in client-specific routing:
```typescript
// app-routing.module.ts (Browser Extension)
{
path: "",
canActivate: [redirectGuard({ loggedIn: "/tabs/current"})],
}
```

View File

@@ -25,12 +25,14 @@ const defaultRoutes: RedirectRoutes = {
};
/**
* Guard that consolidates all redirection logic, should be applied to root route.
* Redirects the user to the appropriate route based on their `AuthenticationStatus`.
* This guard should be applied to the root route.
*
* TODO: This should return Observable<boolean | UrlTree> once we can get rid of all the promises
*/
export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActivateFn {
const routes = { ...defaultRoutes, ...overrides };
return async (route) => {
const authService = inject(AuthService);
const keyService = inject(KeyService);
@@ -41,16 +43,21 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
const authStatus = await authService.getAuthStatus();
// Logged Out
if (authStatus === AuthenticationStatus.LoggedOut) {
return router.createUrlTree([routes.loggedOut], { queryParams: route.queryParams });
}
// Unlocked
if (authStatus === AuthenticationStatus.Unlocked) {
return router.createUrlTree([routes.loggedIn], { queryParams: route.queryParams });
}
// If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the
// login decryption options component.
// Locked: TDE Locked State
// - If user meets all 3 of the following conditions:
// 1. Auth status is Locked
// 2. TDE is enabled
// 3. User has never had a user key (has not decrypted yet)
const tdeEnabled = await firstValueFrom(deviceTrustService.supportsDeviceTrust$);
const userId = await firstValueFrom(accountService.activeAccount$.pipe(getUserId));
const everHadUserKey = await firstValueFrom(keyService.everHadUserKey$(userId));
@@ -64,6 +71,7 @@ export function redirectGuard(overrides: Partial<RedirectRoutes> = {}): CanActiv
return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams });
}
// Locked: Standard Locked State
if (authStatus === AuthenticationStatus.Locked) {
return router.createUrlTree([routes.locked], { queryParams: route.queryParams });
}

View File

@@ -294,6 +294,7 @@ import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service";
import { FolderService } from "@bitwarden/common/vault/services/folder/folder.service";
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
import { TotpService } from "@bitwarden/common/vault/services/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/services/vault-settings/vault-settings.service";
import { DefaultTaskService, TaskService } from "@bitwarden/common/vault/tasks";
@@ -680,6 +681,11 @@ const safeProviders: SafeProvider[] = [
KdfConfigService,
],
}),
safeProvider({
provide: RestrictedItemTypesService,
useClass: RestrictedItemTypesService,
deps: [ConfigService, AccountService, OrganizationServiceAbstraction, PolicyServiceAbstraction],
}),
safeProvider({
provide: PasswordStrengthServiceAbstraction,
useClass: PasswordStrengthService,

View File

@@ -84,7 +84,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
showCardNumber = false;
showCardCode = false;
cipherType = CipherType;
typeOptions: any[];
cardBrandOptions: any[];
cardExpMonthOptions: any[];
identityTitleOptions: any[];
@@ -139,13 +138,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected sdkService: SdkService,
private sshImportPromptService: SshImportPromptService,
) {
this.typeOptions = [
{ name: i18nService.t("typeLogin"), value: CipherType.Login },
{ name: i18nService.t("typeCard"), value: CipherType.Card },
{ name: i18nService.t("typeIdentity"), value: CipherType.Identity },
{ name: i18nService.t("typeSecureNote"), value: CipherType.SecureNote },
];
this.cardBrandOptions = [
{ name: "-- " + i18nService.t("select") + " --", value: null },
{ name: "Visa", value: "Visa" },
@@ -215,8 +207,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.writeableCollections = await this.loadCollections();
this.canUseReprompt = await this.passwordRepromptService.enabled();
this.typeOptions.push({ name: this.i18nService.t("typeSshKey"), value: CipherType.SshKey });
}
ngOnDestroy() {

View File

@@ -8,7 +8,9 @@ import {
combineLatest,
filter,
from,
map,
of,
shareReplay,
switchMap,
takeUntil,
} from "rxjs";
@@ -20,6 +22,11 @@ import { getUserId } from "@bitwarden/common/auth/services/account.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 {
isCipherViewRestricted,
RestrictedItemTypesService,
} from "@bitwarden/common/vault/services/restricted-item-types.service";
import { CIPHER_MENU_ITEMS } from "@bitwarden/common/vault/types/cipher-menu-items";
@Directive()
export class VaultItemsComponent implements OnInit, OnDestroy {
@@ -35,6 +42,19 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
organization: Organization;
CipherType = CipherType;
protected itemTypes$ = this.restrictedItemTypesService.restricted$.pipe(
map((restrictedItemTypes) =>
// Filter out restricted item types
CIPHER_MENU_ITEMS.filter(
(itemType) =>
!restrictedItemTypes.some(
(restrictedType) => restrictedType.cipherType === itemType.type,
),
),
),
shareReplay({ bufferSize: 1, refCount: true }),
);
protected searchPending = false;
/** Construct filters as an observable so it can be appended to the cipher stream. */
@@ -62,6 +82,7 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
protected searchService: SearchService,
protected cipherService: CipherService,
protected accountService: AccountService,
protected restrictedItemTypesService: RestrictedItemTypesService,
) {
this.subscribeToCiphers();
}
@@ -143,18 +164,22 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
this._searchText$,
this._filter$,
of(userId),
this.restrictedItemTypesService.restricted$,
]),
),
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId]) => {
switchMap(([indexedCiphers, failedCiphers, searchText, filter, userId, restricted]) => {
let allCiphers = indexedCiphers ?? [];
const _failedCiphers = failedCiphers ?? [];
allCiphers = [..._failedCiphers, ...allCiphers];
const restrictedTypeFilter = (cipher: CipherView) =>
!isCipherViewRestricted(cipher, restricted);
return this.searchService.searchCiphers(
userId,
searchText,
[filter, this.deletedFilter],
[filter, this.deletedFilter, restrictedTypeFilter],
allCiphers,
);
}),

View File

@@ -228,16 +228,6 @@ export abstract class ApiService {
request: CipherBulkRestoreRequest,
) => Promise<ListResponse<CipherResponse>>;
/**
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
* This method still exists for backward compatibility with old server versions.
*/
postCipherAttachmentLegacy: (id: string, data: FormData) => Promise<CipherResponse>;
/**
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
* This method still exists for backward compatibility with old server versions.
*/
postCipherAttachmentAdminLegacy: (id: string, data: FormData) => Promise<CipherResponse>;
postCipherAttachment: (
id: string,
request: AttachmentRequest,

View File

@@ -639,24 +639,6 @@ export class ApiService implements ApiServiceAbstraction {
return new AttachmentUploadDataResponse(r);
}
/**
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
* This method still exists for backward compatibility with old server versions.
*/
async postCipherAttachmentLegacy(id: string, data: FormData): Promise<CipherResponse> {
const r = await this.send("POST", "/ciphers/" + id + "/attachment", data, true, true);
return new CipherResponse(r);
}
/**
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
* This method still exists for backward compatibility with old server versions.
*/
async postCipherAttachmentAdminLegacy(id: string, data: FormData): Promise<CipherResponse> {
const r = await this.send("POST", "/ciphers/" + id + "/attachment-admin", data, true, true);
return new CipherResponse(r);
}
deleteCipherAttachment(id: string, attachmentId: string): Promise<any> {
return this.send("DELETE", "/ciphers/" + id + "/attachment/" + attachmentId, null, true, true);
}

View File

@@ -157,6 +157,15 @@ export class CardView extends ItemView {
return undefined;
}
return Object.assign(new CardView(), obj);
const cardView = new CardView();
cardView.cardholderName = obj.cardholderName ?? null;
cardView.brand = obj.brand ?? null;
cardView.number = obj.number ?? null;
cardView.expMonth = obj.expMonth ?? null;
cardView.expYear = obj.expYear ?? null;
cardView.code = obj.code ?? null;
return cardView;
}
}

View File

@@ -169,6 +169,27 @@ export class IdentityView extends ItemView {
return undefined;
}
return Object.assign(new IdentityView(), obj);
const identityView = new IdentityView();
identityView.title = obj.title ?? null;
identityView.firstName = obj.firstName ?? null;
identityView.middleName = obj.middleName ?? null;
identityView.lastName = obj.lastName ?? null;
identityView.address1 = obj.address1 ?? null;
identityView.address2 = obj.address2 ?? null;
identityView.address3 = obj.address3 ?? null;
identityView.city = obj.city ?? null;
identityView.state = obj.state ?? null;
identityView.postalCode = obj.postalCode ?? null;
identityView.country = obj.country ?? null;
identityView.company = obj.company ?? null;
identityView.email = obj.email ?? null;
identityView.phone = obj.phone ?? null;
identityView.ssn = obj.ssn ?? null;
identityView.username = obj.username ?? null;
identityView.passportNumber = obj.passportNumber ?? null;
identityView.licenseNumber = obj.licenseNumber ?? null;
return identityView;
}
}

View File

@@ -116,13 +116,18 @@ export class LoginView extends ItemView {
return undefined;
}
const passwordRevisionDate =
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
const uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || [];
const loginView = new LoginView();
return Object.assign(new LoginView(), obj, {
passwordRevisionDate,
uris,
});
loginView.username = obj.username ?? null;
loginView.password = obj.password ?? null;
loginView.passwordRevisionDate =
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
loginView.totp = obj.totp ?? null;
loginView.autofillOnPageLoad = obj.autofillOnPageLoad ?? null;
loginView.uris = obj.uris?.map((uri) => LoginUriView.fromSdkLoginUriView(uri)) || [];
// FIDO2 credentials are not decrypted here, they remain encrypted
loginView.fido2Credentials = null;
return loginView;
}
}

View File

@@ -37,6 +37,9 @@ export class SecureNoteView extends ItemView {
return undefined;
}
return Object.assign(new SecureNoteView(), obj);
const secureNoteView = new SecureNoteView();
secureNoteView.type = obj.type ?? null;
return secureNoteView;
}
}

View File

@@ -55,10 +55,12 @@ export class SshKeyView extends ItemView {
return undefined;
}
const keyFingerprint = obj.fingerprint;
const sshKeyView = new SshKeyView();
return Object.assign(new SshKeyView(), obj, {
keyFingerprint,
});
sshKeyView.privateKey = obj.privateKey ?? null;
sshKeyView.publicKey = obj.publicKey ?? null;
sshKeyView.keyFingerprint = obj.fingerprint ?? null;
return sshKeyView;
}
}

View File

@@ -1,5 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { ITreeNodeObject, TreeNode } from "./models/domain/tree-node";
export class ServiceUtils {

View File

@@ -6,7 +6,6 @@ import {
FileUploadApiMethods,
FileUploadService,
} from "../../../platform/abstractions/file-upload/file-upload.service";
import { Utils } from "../../../platform/misc/utils";
import { EncArrayBuffer } from "../../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
@@ -47,18 +46,7 @@ export class CipherFileUploadService implements CipherFileUploadServiceAbstracti
this.generateMethods(uploadDataResponse, response, request.adminRequest),
);
} catch (e) {
if (
(e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) ||
(e as ErrorResponse).statusCode === 405
) {
response = await this.legacyServerAttachmentFileUpload(
request.adminRequest,
cipher.id,
encFileName,
encData,
dataEncKey[1],
);
} else if (e instanceof ErrorResponse) {
if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
throw e;
@@ -113,50 +101,4 @@ export class CipherFileUploadService implements CipherFileUploadServiceAbstracti
}
};
}
/**
* @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads.
* This method still exists for backward compatibility with old server versions.
*/
async legacyServerAttachmentFileUpload(
admin: boolean,
cipherId: string,
encFileName: EncString,
encData: EncArrayBuffer,
key: EncString,
) {
const fd = new FormData();
try {
const blob = new Blob([encData.buffer], { type: "application/octet-stream" });
fd.append("key", key.encryptedString);
fd.append("data", blob, encFileName.encryptedString);
} catch (e) {
if (Utils.isNode && !Utils.isBrowser) {
fd.append("key", key.encryptedString);
fd.append(
"data",
Buffer.from(encData.buffer) as any,
{
filepath: encFileName.encryptedString,
contentType: "application/octet-stream",
} as any,
);
} else {
throw e;
}
}
let response: CipherResponse;
try {
if (admin) {
response = await this.apiService.postCipherAttachmentAdminLegacy(cipherId, fd);
} else {
response = await this.apiService.postCipherAttachmentLegacy(cipherId, fd);
}
} catch (e) {
throw new Error((e as ErrorResponse).getSingleMessage());
}
return response;
}
}

View File

@@ -1,4 +1,3 @@
import { TestBed } from "@angular/core/testing";
import { mock, MockProxy } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
@@ -49,19 +48,16 @@ describe("RestrictedItemTypesService", () => {
fakeAccount = { id: Utils.newGuid() as UserId } as Account;
accountService.activeAccount$ = of(fakeAccount);
TestBed.configureTestingModule({
providers: [
{ provide: PolicyService, useValue: policyService },
{ provide: OrganizationService, useValue: organizationService },
{ provide: AccountService, useValue: accountService },
{ provide: ConfigService, useValue: configService },
],
});
configService.getFeatureFlag$.mockReturnValue(of(true));
organizationService.organizations$.mockReturnValue(of([org1, org2]));
policyService.policiesByType$.mockReturnValue(of([]));
service = TestBed.inject(RestrictedItemTypesService);
service = new RestrictedItemTypesService(
configService,
accountService,
organizationService,
policyService,
);
});
it("emits empty array when feature flag is disabled", async () => {
@@ -106,7 +102,6 @@ describe("RestrictedItemTypesService", () => {
});
it("returns empty allowViewOrgIds when all orgs restrict the same type", async () => {
configService.getFeatureFlag$.mockReturnValue(of(true));
organizationService.organizations$.mockReturnValue(of([org1, org2]));
policyService.policiesByType$.mockReturnValue(of([policyOrg1, policyOrg2]));
@@ -117,7 +112,6 @@ describe("RestrictedItemTypesService", () => {
});
it("aggregates multiple types and computes allowViewOrgIds correctly", async () => {
configService.getFeatureFlag$.mockReturnValue(of(true));
organizationService.organizations$.mockReturnValue(of([org1, org2]));
policyService.policiesByType$.mockReturnValue(
of([

View File

@@ -1,4 +1,3 @@
import { Injectable } from "@angular/core";
import { combineLatest, map, of, Observable } from "rxjs";
import { switchMap, distinctUntilChanged, shareReplay } from "rxjs/operators";
@@ -10,13 +9,13 @@ import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
export type RestrictedCipherType = {
cipherType: CipherType;
allowViewOrgIds: string[];
};
@Injectable({ providedIn: "root" })
export class RestrictedItemTypesService {
/**
* Emits an array of RestrictedCipherType objects:
@@ -78,3 +77,25 @@ export class RestrictedItemTypesService {
private policyService: PolicyService,
) {}
}
/**
* Filter that returns whether a cipher is restricted from being viewed by the user
* Criteria:
* - the cipher's type is restricted by at least one org
* UNLESS
* - the cipher belongs to an organization and that organization does not restrict that type
* OR
* - the cipher belongs to the user's personal vault and at least one other organization does not restrict that type
*/
export function isCipherViewRestricted(
cipher: CipherView,
restrictedTypes: RestrictedCipherType[],
) {
return restrictedTypes.some(
(restrictedType) =>
restrictedType.cipherType === cipher.type &&
(cipher.organizationId
? !restrictedType.allowViewOrgIds.includes(cipher.organizationId)
: restrictedType.allowViewOrgIds.length === 0),
);
}

View File

@@ -19,6 +19,6 @@ export const CIPHER_MENU_ITEMS = Object.freeze([
{ type: CipherType.Login, icon: "bwi-globe", labelKey: "typeLogin" },
{ type: CipherType.Card, icon: "bwi-credit-card", labelKey: "typeCard" },
{ type: CipherType.Identity, icon: "bwi-id-card", labelKey: "typeIdentity" },
{ type: CipherType.SecureNote, icon: "bwi-sticky-note", labelKey: "note" },
{ type: CipherType.SecureNote, icon: "bwi-sticky-note", labelKey: "typeNote" },
{ type: CipherType.SshKey, icon: "bwi-key", labelKey: "typeSshKey" },
] as const) satisfies readonly CipherMenuItem[];

View File

@@ -1,10 +1,11 @@
import { Component, ElementRef, ViewChild } from "@angular/core";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { mock } from "jest-mock-extended";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService } from "../";
import { ToastService, CopyClickListener, COPY_CLICK_LISTENER } from "../";
import { CopyClickDirective } from "./copy-click.directive";
@@ -34,10 +35,12 @@ describe("CopyClickDirective", () => {
let fixture: ComponentFixture<TestCopyClickComponent>;
const copyToClipboard = jest.fn();
const showToast = jest.fn();
const copyClickListener = mock<CopyClickListener>();
beforeEach(async () => {
copyToClipboard.mockClear();
showToast.mockClear();
copyClickListener.onCopy.mockClear();
await TestBed.configureTestingModule({
imports: [TestCopyClickComponent],
@@ -55,6 +58,7 @@ describe("CopyClickDirective", () => {
},
{ provide: PlatformUtilsService, useValue: { copyToClipboard } },
{ provide: ToastService, useValue: { showToast } },
{ provide: COPY_CLICK_LISTENER, useValue: copyClickListener },
],
}).compileComponents();
@@ -92,7 +96,6 @@ describe("CopyClickDirective", () => {
successToastButton.click();
expect(showToast).toHaveBeenCalledWith({
message: "copySuccessful",
title: null,
variant: "success",
});
});
@@ -103,7 +106,6 @@ describe("CopyClickDirective", () => {
infoToastButton.click();
expect(showToast).toHaveBeenCalledWith({
message: "copySuccessful",
title: null,
variant: "info",
});
});
@@ -115,8 +117,15 @@ describe("CopyClickDirective", () => {
expect(showToast).toHaveBeenCalledWith({
message: "valueCopied Content",
title: null,
variant: "success",
});
});
it("should call copyClickListener.onCopy when value is copied", () => {
const successToastButton = fixture.componentInstance.successToastButton.nativeElement;
successToastButton.click();
expect(copyClickListener.onCopy).toHaveBeenCalledWith("success toast shown");
});
});

View File

@@ -1,12 +1,19 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Directive, HostListener, Input } from "@angular/core";
import { Directive, HostListener, Input, InjectionToken, Inject, Optional } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { ToastService, ToastVariant } from "../";
/**
* Listener that can be provided to receive copy events to allow for customized behavior.
*/
export interface CopyClickListener {
onCopy(value: string): void;
}
export const COPY_CLICK_LISTENER = new InjectionToken<CopyClickListener>("CopyClickListener");
@Directive({
selector: "[appCopyClick]",
})
@@ -18,6 +25,7 @@ export class CopyClickDirective {
private platformUtilsService: PlatformUtilsService,
private toastService: ToastService,
private i18nService: I18nService,
@Optional() @Inject(COPY_CLICK_LISTENER) private copyListener?: CopyClickListener,
) {}
@Input("appCopyClick") valueToCopy = "";
@@ -26,7 +34,7 @@ export class CopyClickDirective {
* When set, the toast displayed will show `<valueLabel> copied`
* instead of the default messaging.
*/
@Input() valueLabel: string;
@Input() valueLabel?: string;
/**
* When set without a value, a success toast will be shown when the value is copied
@@ -54,6 +62,10 @@ export class CopyClickDirective {
@HostListener("click") onClick() {
this.platformUtilsService.copyToClipboard(this.valueToCopy);
if (this.copyListener) {
this.copyListener.onCopy(this.valueToCopy);
}
if (this._showToast) {
const message = this.valueLabel
? this.i18nService.t("valueCopied", this.valueLabel)
@@ -61,7 +73,6 @@ export class CopyClickDirective {
this.toastService.showToast({
variant: this.toastVariant,
title: null,
message,
});
}

View File

@@ -1,7 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "<%= offsetFromRoot %>/dist/out-tsc",
"outDir": "<%= offsetFromRoot %>dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]

View File

@@ -24,10 +24,6 @@ export * as VaultIcons from "./icons";
export { DefaultSshImportPromptService } from "./services/default-ssh-import-prompt.service";
export { SshImportPromptService } from "./services/ssh-import-prompt.service";
export {
RestrictedItemTypesService,
RestrictedCipherType,
} from "./services/restricted-item-types.service";
export * from "./abstractions/change-login-password.service";
export * from "./services/default-change-login-password.service";