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:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
@@ -1922,6 +1922,9 @@
|
||||
"typeSshKey": {
|
||||
"message": "SSH key"
|
||||
},
|
||||
"typeNote": {
|
||||
"message": "Note"
|
||||
},
|
||||
"newItemHeader": {
|
||||
"message": "New $TYPE$",
|
||||
"placeholders": {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
"typeIdentity": {
|
||||
"message": "Identity"
|
||||
},
|
||||
"typeNote": {
|
||||
"message": "Note"
|
||||
},
|
||||
"typeSecureNote": {
|
||||
"message": "Secure note"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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> {{ "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> {{ "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> {{ "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> {{
|
||||
"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> {{ "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> {{
|
||||
typeFilter.labelKey | i18n
|
||||
}}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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: [],
|
||||
}),
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
@@ -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";
|
||||
|
||||
53
libs/angular/src/auth/guards/redirect/README.md
Normal file
53
libs/angular/src/auth/guards/redirect/README.md
Normal 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`** → redirect to `/login`
|
||||
- **`AuthenticationStatus.Unlocked`** → redirect to `/vault`
|
||||
- **`AuthenticationStatus.Locked`**
|
||||
- **TDE Locked State** → 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** → 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"})],
|
||||
}
|
||||
```
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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([
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
@@ -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[];
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user