mirror of
https://github.com/bitwarden/browser
synced 2026-02-20 03:13:55 +00:00
Merge branch 'main' into 202505-notifications-refactor
This commit is contained in:
@@ -1,23 +1,22 @@
|
||||
###############################################
|
||||
# Build stage 1 #
|
||||
# Node.js build stage (alpine) #
|
||||
###############################################
|
||||
ARG NODE_VERSION=20
|
||||
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS node-build
|
||||
|
||||
ARG NPM_COMMAND=dist:bit:selfhost
|
||||
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION}-alpine AS node-build
|
||||
|
||||
WORKDIR /source
|
||||
COPY package*.json ./
|
||||
COPY . .
|
||||
|
||||
RUN npm ci
|
||||
|
||||
WORKDIR /source/apps/web
|
||||
ARG NPM_COMMAND=dist:bit:selfhost
|
||||
RUN npm run ${NPM_COMMAND}
|
||||
|
||||
###############################################
|
||||
# Build stage 2 #
|
||||
###############################################
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0-alpine3.21 AS build
|
||||
|
||||
# Docker buildx supplies the value for this arg
|
||||
ARG TARGETPLATFORM
|
||||
@@ -25,11 +24,11 @@ ARG TARGETPLATFORM
|
||||
# Determine proper runtime value for .NET
|
||||
# We put the value in a file to be read by later layers.
|
||||
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
|
||||
RID=linux-x64 ; \
|
||||
RID=linux-musl-x64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
|
||||
RID=linux-arm64 ; \
|
||||
RID=linux-musl-arm64 ; \
|
||||
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
|
||||
RID=linux-arm ; \
|
||||
RID=linux-musl-arm ; \
|
||||
fi \
|
||||
&& echo "RID=$RID" > /tmp/rid.txt
|
||||
|
||||
@@ -57,19 +56,18 @@ WORKDIR /app
|
||||
###############################################
|
||||
# App stage #
|
||||
###############################################
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine3.21
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
LABEL com.bitwarden.product="bitwarden"
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
|
||||
EXPOSE 5000
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN apk add --no-cache curl \
|
||||
icu-libs \
|
||||
&& apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community gosu
|
||||
|
||||
# Copy app from the build stage
|
||||
WORKDIR /bitwarden_server
|
||||
|
||||
@@ -14,5 +14,5 @@ files:
|
||||
zh-TW: zh_TW
|
||||
en-GB: en_GB
|
||||
en-IN: en_IN
|
||||
sr-CY: sr_CY
|
||||
sr: sr_CY
|
||||
sr-CS: sr_CS
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
|
||||
# Setup
|
||||
|
||||
@@ -22,11 +22,10 @@ fi
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
# Create user and group
|
||||
|
||||
groupadd -o -g $LGID $GROUPNAME >/dev/null 2>&1 ||
|
||||
groupmod -o -g $LGID $GROUPNAME >/dev/null 2>&1
|
||||
useradd -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1 ||
|
||||
usermod -o -u $LUID -g $GROUPNAME -s /bin/false $USERNAME >/dev/null 2>&1
|
||||
mkhomedir_helper $USERNAME
|
||||
addgroup -g "$LGID" -S "$GROUPNAME" 2>/dev/null || true
|
||||
adduser -u "$LUID" -G "$GROUPNAME" -S -D -H "$USERNAME" 2>/dev/null || true
|
||||
mkdir -p /home/$USERNAME
|
||||
chown $USERNAME:$GROUPNAME /home/$USERNAME
|
||||
|
||||
# The rest...
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { pathsToModuleNameMapper } = require("ts-jest");
|
||||
|
||||
const { compilerOptions } = require("./tsconfig");
|
||||
const { compilerOptions } = require("../../tsconfig.base");
|
||||
|
||||
const sharedConfig = require("../../libs/shared/jest.config.angular");
|
||||
|
||||
@@ -15,11 +15,11 @@ module.exports = {
|
||||
...pathsToModuleNameMapper(
|
||||
{
|
||||
// lets us use @bitwarden/common/spec in web tests
|
||||
"@bitwarden/common/spec": ["../../libs/common/spec"],
|
||||
"@bitwarden/common/spec": ["libs/common/spec"],
|
||||
...(compilerOptions?.paths ?? {}),
|
||||
},
|
||||
{
|
||||
prefix: "<rootDir>/",
|
||||
prefix: "<rootDir>/../../",
|
||||
},
|
||||
),
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@bitwarden/web-vault",
|
||||
"version": "2025.6.0",
|
||||
"version": "2025.8.0",
|
||||
"scripts": {
|
||||
"build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
|
||||
"build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js",
|
||||
|
||||
@@ -86,7 +86,7 @@ export abstract class BaseMembersComponent<UserView extends UserViewTypes> {
|
||||
protected i18nService: I18nService,
|
||||
protected keyService: KeyService,
|
||||
protected validationService: ValidationService,
|
||||
private logService: LogService,
|
||||
protected logService: LogService,
|
||||
protected userNamePipe: UserNamePipe,
|
||||
protected dialogService: DialogService,
|
||||
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -54,7 +54,6 @@ export enum BulkCollectionsDialogResult {
|
||||
imports: [SharedModule, AccessSelectorModule],
|
||||
selector: "app-bulk-collections-dialog",
|
||||
templateUrl: "bulk-collections-dialog.component.html",
|
||||
standalone: true,
|
||||
})
|
||||
export class BulkCollectionsDialogComponent implements OnDestroy {
|
||||
protected readonly PermissionMode = PermissionMode;
|
||||
|
||||
@@ -12,7 +12,6 @@ const icon = svgIcon`<svg xmlns="http://www.w3.org/2000/svg" width="120" height=
|
||||
|
||||
@Component({
|
||||
selector: "collection-access-restricted",
|
||||
standalone: true,
|
||||
imports: [SharedModule, ButtonModule, NoItemsModule],
|
||||
template: `<bit-no-items [icon]="icon" class="tw-mt-2 tw-block">
|
||||
<span slot="title" class="tw-mt-4 tw-block">{{ "youDoNotHavePermissions" | i18n }}</span>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { GetCollectionNameFromIdPipe } from "../pipes";
|
||||
@Component({
|
||||
selector: "app-collection-badge",
|
||||
templateUrl: "collection-name-badge.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule, GetCollectionNameFromIdPipe],
|
||||
})
|
||||
export class CollectionNameBadgeComponent {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
@Pipe({
|
||||
name: "collectionNameFromId",
|
||||
pure: true,
|
||||
standalone: true,
|
||||
})
|
||||
export class GetCollectionNameFromIdPipe implements PipeTransform {
|
||||
transform(value: string, collections: CollectionView[]) {
|
||||
|
||||
@@ -37,31 +37,6 @@ export function getNestedCollectionTree(
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function getNestedCollectionTree_vNext(
|
||||
collections: (CollectionView | CollectionAdminView)[],
|
||||
): TreeNode<CollectionView | CollectionAdminView>[] {
|
||||
if (!collections) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Collections need to be cloned because ServiceUtils.nestedTraverse actively
|
||||
// modifies the names of collections.
|
||||
// These changes risk affecting collections store in StateService.
|
||||
const clonedCollections = collections
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.map(cloneCollection);
|
||||
|
||||
const nodes: TreeNode<CollectionView | CollectionAdminView>[] = [];
|
||||
clonedCollections.forEach((collection) => {
|
||||
const parts =
|
||||
collection.name != null
|
||||
? collection.name.replace(/^\/+|\/+$/g, "").split(NestingDelimiter)
|
||||
: [];
|
||||
ServiceUtils.nestedTraverse_vNext(nodes, 0, parts, collection, null, NestingDelimiter);
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function getFlatCollectionTree(
|
||||
nodes: TreeNode<CollectionAdminView>[],
|
||||
): CollectionAdminView[];
|
||||
@@ -107,5 +82,7 @@ function cloneCollection(
|
||||
cloned.organizationId = collection.organizationId;
|
||||
cloned.readOnly = collection.readOnly;
|
||||
cloned.manage = collection.manage;
|
||||
cloned.type = collection.type;
|
||||
|
||||
return cloned;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstract
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { 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 { VaultFilterComponent as BaseVaultFilterComponent } from "../../../../vault/individual-vault/vault-filter/components/vault-filter.component";
|
||||
@@ -51,6 +53,8 @@ export class VaultFilterComponent
|
||||
protected dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
protected accountService: AccountService,
|
||||
protected restrictedItemTypesService: RestrictedItemTypesService,
|
||||
protected cipherService: CipherService,
|
||||
) {
|
||||
super(
|
||||
vaultFilterService,
|
||||
@@ -62,6 +66,8 @@ export class VaultFilterComponent
|
||||
dialogService,
|
||||
configService,
|
||||
accountService,
|
||||
restrictedItemTypesService,
|
||||
cipherService,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,7 +134,7 @@ export class VaultFilterComponent
|
||||
|
||||
async buildAllFilters(): Promise<VaultFilterList> {
|
||||
const builderFilter = {} as VaultFilterList;
|
||||
builderFilter.typeFilter = await this.addTypeFilter(["favorites"]);
|
||||
builderFilter.typeFilter = await this.addTypeFilter(["favorites"], this._organization?.id);
|
||||
builderFilter.collectionFilter = await this.addCollectionFilter();
|
||||
builderFilter.trashFilter = await this.addTrashFilter();
|
||||
return builderFilter;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CollectionAdminView, CollectionService } from "@bitwarden/admin-console
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
@@ -34,6 +35,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest
|
||||
stateProvider: StateProvider,
|
||||
collectionService: CollectionService,
|
||||
accountService: AccountService,
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
organizationService,
|
||||
@@ -44,6 +46,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest
|
||||
stateProvider,
|
||||
collectionService,
|
||||
accountService,
|
||||
configService,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,62 +25,67 @@
|
||||
</bit-breadcrumbs>
|
||||
|
||||
<ng-container slot="title-suffix">
|
||||
<ng-container
|
||||
*ngIf="
|
||||
collection != null && (canEditCollection || canDeleteCollection || canViewCollectionInfo)
|
||||
"
|
||||
>
|
||||
<button
|
||||
bitIconButton="bwi-angle-down"
|
||||
[bitMenuTriggerFor]="editCollectionMenu"
|
||||
size="small"
|
||||
type="button"
|
||||
></button>
|
||||
<bit-menu #editCollectionMenu>
|
||||
<ng-container *ngIf="canEditCollection">
|
||||
@if (
|
||||
collection != null && (canEditCollection || canDeleteCollection || canViewCollectionInfo)
|
||||
) {
|
||||
<ng-container>
|
||||
<button
|
||||
bitIconButton="bwi-angle-down"
|
||||
[bitMenuTriggerFor]="editCollectionMenu"
|
||||
size="small"
|
||||
type="button"
|
||||
></button>
|
||||
<bit-menu #editCollectionMenu>
|
||||
<ng-container *ngIf="canEditCollection">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Info, false)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "editInfo" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Access, false)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "access" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!canEditCollection && canViewCollectionInfo">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Info, true)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "viewInfo" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Access, true)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "viewAccess" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<button
|
||||
type="button"
|
||||
*ngIf="canDeleteCollection"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Info, false)"
|
||||
(click)="deleteCollection()"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "editInfo" | i18n }}
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Access, false)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "access" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!canEditCollection && canViewCollectionInfo">
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Info, true)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "viewInfo" | i18n }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editCollection(CollectionDialogTabType.Access, true)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
||||
{{ "viewAccess" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<button type="button" *ngIf="canDeleteCollection" bitMenuItem (click)="deleteCollection()">
|
||||
<span class="tw-text-danger">
|
||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
||||
{{ "delete" | i18n }}
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</ng-container>
|
||||
</bit-menu>
|
||||
</ng-container>
|
||||
}
|
||||
<small *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
@@ -103,48 +108,11 @@
|
||||
*ngIf="filter.type !== 'trash' && filter.collectionId !== Unassigned && organization"
|
||||
class="tw-shrink-0"
|
||||
>
|
||||
<!-- "New" menu is always shown unless the user cannot create a cipher and cannot create a collection-->
|
||||
<ng-container *ngIf="canCreateCipher || canCreateCollection">
|
||||
<div appListDropdown>
|
||||
<button
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
type="button"
|
||||
[bitMenuTriggerFor]="addOptions"
|
||||
id="newItemDropdown"
|
||||
appA11yTitle="{{ 'new' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
|
||||
</button>
|
||||
<bit-menu #addOptions aria-labelledby="newItemDropdown">
|
||||
<ng-container *ngIf="canCreateCipher">
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
|
||||
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeLogin" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
|
||||
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeCard" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
|
||||
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
|
||||
{{ "typeIdentity" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
|
||||
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
|
||||
{{ "note" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="canCreateCollection">
|
||||
<bit-menu-divider *ngIf="canCreateCipher"></bit-menu-divider>
|
||||
<button type="button" bitMenuItem (click)="addCollection()">
|
||||
<i class="bwi bwi-fw bwi-collection-shared" aria-hidden="true"></i>
|
||||
{{ "collection" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-menu>
|
||||
</div>
|
||||
</ng-container>
|
||||
<vault-new-cipher-menu
|
||||
[canCreateCipher]="canCreateCipher"
|
||||
[canCreateCollection]="canCreateCollection"
|
||||
(cipherAdded)="addCipher($event)"
|
||||
(collectionAdded)="addCollection()"
|
||||
/>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionAdminService,
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
@@ -25,6 +27,7 @@ import {
|
||||
SearchModule,
|
||||
SimpleDialogOptions,
|
||||
} from "@bitwarden/components";
|
||||
import { NewCipherMenuComponent } from "@bitwarden/vault";
|
||||
|
||||
import { HeaderModule } from "../../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../../shared";
|
||||
@@ -35,7 +38,6 @@ import {
|
||||
import { CollectionDialogTabType } from "../../shared/components/collection-dialog";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-org-vault-header",
|
||||
templateUrl: "./vault-header.component.html",
|
||||
imports: [
|
||||
@@ -46,6 +48,7 @@ import { CollectionDialogTabType } from "../../shared/components/collection-dial
|
||||
HeaderModule,
|
||||
SearchModule,
|
||||
JslibModule,
|
||||
NewCipherMenuComponent,
|
||||
],
|
||||
})
|
||||
export class VaultHeaderComponent {
|
||||
@@ -98,6 +101,7 @@ export class VaultHeaderComponent {
|
||||
private dialogService: DialogService,
|
||||
private collectionAdminService: CollectionAdminService,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
get title() {
|
||||
@@ -198,7 +202,14 @@ export class VaultHeaderComponent {
|
||||
|
||||
async addCollection() {
|
||||
if (this.organization.productTierType === ProductTierType.Free) {
|
||||
const collections = await this.collectionAdminService.getAll(this.organization.id);
|
||||
const collections = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.collectionAdminService.collectionAdminViews$(this.organization.id, userId),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (collections.length === this.organization.maxCollections) {
|
||||
this.showFreeOrgUpgradeDialog();
|
||||
return;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<app-free-trial-warning
|
||||
<app-organization-free-trial-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
</app-free-trial-warning>
|
||||
<app-reseller-renewal-warning
|
||||
</app-organization-free-trial-warning>
|
||||
<app-organization-reseller-renewal-warning
|
||||
*ngIf="useOrganizationWarningsService$ | async"
|
||||
[organization]="organization"
|
||||
>
|
||||
</app-reseller-renewal-warning>
|
||||
</app-organization-reseller-renewal-warning>
|
||||
<ng-container *ngIf="freeTrialWhenWarningsServiceDisabled$ | async as freeTrial">
|
||||
<bit-banner
|
||||
id="free-trial-banner"
|
||||
@@ -84,6 +84,7 @@
|
||||
{{ trashCleanupWarning }}
|
||||
</bit-callout>
|
||||
<app-vault-items
|
||||
#vaultItems
|
||||
[ciphers]="ciphers"
|
||||
[collections]="collections"
|
||||
[allCollections]="allCollections"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { ActivatedRoute, Params, Router } from "@angular/router";
|
||||
import {
|
||||
BehaviorSubject,
|
||||
@@ -29,13 +29,13 @@ import {
|
||||
import {
|
||||
CollectionAdminService,
|
||||
CollectionAdminView,
|
||||
CollectionService,
|
||||
CollectionView,
|
||||
Unassigned,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
@@ -55,6 +55,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||
@@ -78,8 +79,9 @@ import {
|
||||
DecryptionFailureDialogComponent,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/services/organization-warnings.service";
|
||||
import { ResellerRenewalWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/reseller-renewal-warning.component";
|
||||
import { OrganizationResellerRenewalWarningComponent } from "@bitwarden/web-vault/app/billing/warnings/components/organization-reseller-renewal-warning.component";
|
||||
import { OrganizationWarningsService } from "@bitwarden/web-vault/app/billing/warnings/services/organization-warnings.service";
|
||||
import { VaultItemsComponent } from "@bitwarden/web-vault/app/vault/components/vault-items/vault-items.component";
|
||||
|
||||
import { BillingNotificationService } from "../../../billing/services/billing-notification.service";
|
||||
import {
|
||||
@@ -88,7 +90,7 @@ import {
|
||||
} from "../../../billing/services/reseller-warning.service";
|
||||
import { TrialFlowService } from "../../../billing/services/trial-flow.service";
|
||||
import { FreeTrial } from "../../../billing/types/free-trial";
|
||||
import { FreeTrialWarningComponent } from "../../../billing/warnings/free-trial-warning.component";
|
||||
import { OrganizationFreeTrialWarningComponent } from "../../../billing/warnings/components/organization-free-trial-warning.component";
|
||||
import { SharedModule } from "../../../shared";
|
||||
import { AssignCollectionsWebComponent } from "../../../vault/components/assign-collections";
|
||||
import {
|
||||
@@ -125,11 +127,7 @@ import {
|
||||
BulkCollectionsDialogResult,
|
||||
} from "./bulk-collections-dialog";
|
||||
import { CollectionAccessRestrictedComponent } from "./collection-access-restricted.component";
|
||||
import {
|
||||
getNestedCollectionTree,
|
||||
getFlatCollectionTree,
|
||||
getNestedCollectionTree_vNext,
|
||||
} from "./utils";
|
||||
import { getFlatCollectionTree, getNestedCollectionTree } from "./utils";
|
||||
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
|
||||
import { VaultHeaderComponent } from "./vault-header/vault-header.component";
|
||||
|
||||
@@ -144,7 +142,6 @@ enum AddAccessStatusType {
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-org-vault",
|
||||
templateUrl: "vault.component.html",
|
||||
imports: [
|
||||
@@ -155,8 +152,8 @@ enum AddAccessStatusType {
|
||||
SharedModule,
|
||||
BannerModule,
|
||||
NoItemsModule,
|
||||
FreeTrialWarningComponent,
|
||||
ResellerRenewalWarningComponent,
|
||||
OrganizationFreeTrialWarningComponent,
|
||||
OrganizationResellerRenewalWarningComponent,
|
||||
],
|
||||
providers: [
|
||||
RoutedVaultFilterService,
|
||||
@@ -208,6 +205,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
protected addAccessStatus$ = new BehaviorSubject<AddAccessStatusType>(0);
|
||||
private vaultItemDialogRef?: DialogRef<VaultItemDialogResult> | undefined;
|
||||
|
||||
@ViewChild("vaultItems", { static: false }) vaultItemsComponent: VaultItemsComponent<CipherView>;
|
||||
|
||||
private readonly unpaidSubscriptionDialog$ = this.accountService.activeAccount$.pipe(
|
||||
map((account) => account?.id),
|
||||
switchMap((id) =>
|
||||
@@ -269,6 +268,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
private accountService: AccountService,
|
||||
private billingNotificationService: BillingNotificationService,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
private collectionService: CollectionService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -281,9 +281,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
const filter$ = this.routedVaultFilterService.filter$;
|
||||
|
||||
// FIXME: The RoutedVaultFilterModel uses `organizationId: Unassigned` to represent the individual vault,
|
||||
// but that is never used in Admin Console. This function narrows the type so it doesn't pollute our code here,
|
||||
// but really we should change to using our own vault filter model that only represents valid states in AC.
|
||||
const isOrganizationId = (value: OrganizationId | Unassigned): value is OrganizationId =>
|
||||
value !== Unassigned;
|
||||
const organizationId$ = filter$.pipe(
|
||||
map((filter) => filter.organizationId),
|
||||
filter((filter) => filter !== undefined),
|
||||
filter(isOrganizationId),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
@@ -356,7 +363,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.allCollectionsWithoutUnassigned$ = this.refresh$.pipe(
|
||||
switchMap(() => organizationId$),
|
||||
switchMap((orgId) => this.collectionAdminService.getAll(orgId)),
|
||||
switchMap((orgId) =>
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
@@ -376,9 +388,12 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.allCollectionsWithoutUnassigned$,
|
||||
]).pipe(
|
||||
map(([organizationId, allCollections]) => {
|
||||
// FIXME: We should not assert that the Unassigned type is a CollectionId.
|
||||
// Instead we should consider representing the Unassigned collection as a different object, given that
|
||||
// it is not actually a collection.
|
||||
const noneCollection = new CollectionAdminView();
|
||||
noneCollection.name = this.i18nService.t("unassigned");
|
||||
noneCollection.id = Unassigned;
|
||||
noneCollection.id = Unassigned as CollectionId;
|
||||
noneCollection.organizationId = organizationId;
|
||||
return allCollections.concat(noneCollection);
|
||||
}),
|
||||
@@ -424,16 +439,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
);
|
||||
|
||||
const nestedCollections$ = combineLatest([
|
||||
allCollections$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.OptimizeNestedTraverseTypescript),
|
||||
]).pipe(
|
||||
map(
|
||||
([collections, shouldOptimize]) =>
|
||||
(shouldOptimize
|
||||
? getNestedCollectionTree_vNext(collections)
|
||||
: getNestedCollectionTree(collections)) as TreeNode<CollectionAdminView>[],
|
||||
),
|
||||
const nestedCollections$ = allCollections$.pipe(
|
||||
map((collections) => getNestedCollectionTree(collections)),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
const collections$ = combineLatest([
|
||||
@@ -548,7 +556,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
const filterFunction = createFilterFunction(filter);
|
||||
|
||||
if (await this.searchService.isSearchable(this.userId, searchText)) {
|
||||
return await this.searchService.searchCiphers(
|
||||
return await this.searchService.searchCiphers<CipherView>(
|
||||
this.userId,
|
||||
searchText,
|
||||
[filterFunction],
|
||||
@@ -761,10 +769,13 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async navigateToPaymentMethod() {
|
||||
await this.router.navigate(
|
||||
["organizations", `${this.organization?.id}`, "billing", "payment-method"],
|
||||
{ state: { launchPaymentModalAutomatically: true } },
|
||||
const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
|
||||
);
|
||||
const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method";
|
||||
await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], {
|
||||
state: { launchPaymentModalAutomatically: true },
|
||||
});
|
||||
}
|
||||
|
||||
addAccessToggle(e: AddAccessStatusType) {
|
||||
@@ -781,7 +792,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
async onVaultItemsEvent(event: VaultItemEvent) {
|
||||
async onVaultItemsEvent(event: VaultItemEvent<CipherView>) {
|
||||
this.processingEvent = true;
|
||||
|
||||
try {
|
||||
@@ -1077,6 +1088,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
if (unassignedCiphers.length > 0 || editAccessCiphers.length > 0) {
|
||||
await this.cipherService.restoreManyWithServer(
|
||||
[...unassignedCiphers, ...editAccessCiphers],
|
||||
this.userId,
|
||||
this.organization.id,
|
||||
);
|
||||
}
|
||||
@@ -1141,16 +1153,18 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
try {
|
||||
await this.apiService.deleteCollection(this.organization?.id, collection.id);
|
||||
await this.collectionService.delete([collection.id as CollectionId], this.userId);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("deletedCollectionId", collection.name),
|
||||
});
|
||||
|
||||
// Clear the cipher cache to clear the deleted collection from the cipher state
|
||||
await this.cipherService.clear();
|
||||
|
||||
// Navigate away if we deleted the collection we were viewing
|
||||
if (this.selectedCollection?.node.id === collection.id) {
|
||||
// Clear the cipher cache to clear the deleted collection from the cipher state
|
||||
await this.cipherService.clear();
|
||||
void this.router.navigate([], {
|
||||
queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null },
|
||||
queryParamsHandling: "merge",
|
||||
@@ -1424,6 +1438,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
private refresh() {
|
||||
this.refresh$.next();
|
||||
this.vaultItemsComponent?.clearSelection();
|
||||
}
|
||||
|
||||
private go(queryParams: any = null) {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, switchMap } from "rxjs";
|
||||
import { Observable, Subject, switchMap, takeUntil, scheduled, asyncScheduler } from "rxjs";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
@@ -11,6 +13,8 @@ import {
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { HeaderModule } from "../../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../../shared/shared.module";
|
||||
@@ -22,7 +26,6 @@ import { Integration } from "../shared/components/integrations/models";
|
||||
@Component({
|
||||
selector: "ac-integrations",
|
||||
templateUrl: "./integrations.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
SharedModule,
|
||||
SharedOrganizationModule,
|
||||
@@ -31,12 +34,193 @@ import { Integration } from "../shared/components/integrations/models";
|
||||
FilterIntegrationsPipe,
|
||||
],
|
||||
})
|
||||
export class AdminConsoleIntegrationsComponent implements OnInit {
|
||||
integrationsList: Integration[] = [];
|
||||
export class AdminConsoleIntegrationsComponent implements OnInit, OnDestroy {
|
||||
// integrationsList: Integration[] = [];
|
||||
tabIndex: number;
|
||||
organization$: Observable<Organization>;
|
||||
isEventBasedIntegrationsEnabled: boolean = false;
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
// initialize the integrations list with default integrations
|
||||
integrationsList: Integration[] = [
|
||||
{
|
||||
name: "AD FS",
|
||||
linkURL: "https://bitwarden.com/help/saml-adfs/",
|
||||
image: "../../../../../../../images/integrations/azure-active-directory.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Auth0",
|
||||
linkURL: "https://bitwarden.com/help/saml-auth0/",
|
||||
image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "AWS",
|
||||
linkURL: "https://bitwarden.com/help/saml-aws/",
|
||||
image: "../../../../../../../images/integrations/aws-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Entra ID",
|
||||
linkURL: "https://bitwarden.com/help/saml-azure/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Duo",
|
||||
linkURL: "https://bitwarden.com/help/saml-duo/",
|
||||
image: "../../../../../../../images/integrations/logo-duo-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Google",
|
||||
linkURL: "https://bitwarden.com/help/saml-google/",
|
||||
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "JumpCloud",
|
||||
linkURL: "https://bitwarden.com/help/saml-jumpcloud/",
|
||||
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "KeyCloak",
|
||||
linkURL: "https://bitwarden.com/help/saml-keycloak/",
|
||||
image: "../../../../../../../images/integrations/logo-keycloak-icon.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Okta",
|
||||
linkURL: "https://bitwarden.com/help/saml-okta/",
|
||||
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "OneLogin",
|
||||
linkURL: "https://bitwarden.com/help/saml-onelogin/",
|
||||
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "PingFederate",
|
||||
linkURL: "https://bitwarden.com/help/saml-pingfederate/",
|
||||
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Entra ID",
|
||||
linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "Okta",
|
||||
linkURL: "https://bitwarden.com/help/okta-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "OneLogin",
|
||||
linkURL: "https://bitwarden.com/help/onelogin-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "JumpCloud",
|
||||
linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "Ping Identity",
|
||||
linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "Active Directory",
|
||||
linkURL: "https://bitwarden.com/help/ldap-directory/",
|
||||
image: "../../../../../../../images/integrations/azure-active-directory.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Entra ID",
|
||||
linkURL: "https://bitwarden.com/help/microsoft-entra-id/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "Google Workspace",
|
||||
linkURL: "https://bitwarden.com/help/workspace-directory/",
|
||||
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "Okta",
|
||||
linkURL: "https://bitwarden.com/help/okta-directory/",
|
||||
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "OneLogin",
|
||||
linkURL: "https://bitwarden.com/help/onelogin-directory/",
|
||||
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "Splunk",
|
||||
linkURL: "https://bitwarden.com/help/splunk-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-splunk-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Sentinel",
|
||||
linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Rapid7",
|
||||
linkURL: "https://bitwarden.com/help/rapid7-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-rapid7-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Elastic",
|
||||
linkURL: "https://bitwarden.com/help/elastic-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Panther",
|
||||
linkURL: "https://bitwarden.com/help/panther-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-panther-round-color.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Intune",
|
||||
linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
|
||||
type: IntegrationType.DEVICE,
|
||||
},
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
const orgId = this.route.snapshot.params.organizationId;
|
||||
|
||||
this.organization$ = this.route.params.pipe(
|
||||
switchMap((params) =>
|
||||
this.accountService.activeAccount$.pipe(
|
||||
@@ -48,188 +232,56 @@ export class AdminConsoleIntegrationsComponent implements OnInit {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
scheduled(this.orgIntegrationApiService.getOrganizationIntegrations(orgId), asyncScheduler)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((integrations) => {
|
||||
// Update the integrations list with the fetched integrations
|
||||
if (integrations && integrations.length > 0) {
|
||||
integrations.forEach((integration) => {
|
||||
const configJson = JSON.parse(integration.configuration || "{}");
|
||||
const serviceName = configJson.service ?? "";
|
||||
const existingIntegration = this.integrationsList.find((i) => i.name === serviceName);
|
||||
|
||||
if (existingIntegration) {
|
||||
// if a configuration exists, then it is connected
|
||||
existingIntegration.isConnected = !!integration.configuration;
|
||||
existingIntegration.configuration = integration.configuration || "";
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
private orgIntegrationApiService: OrganizationIntegrationApiService,
|
||||
) {
|
||||
this.integrationsList = [
|
||||
{
|
||||
name: "AD FS",
|
||||
linkURL: "https://bitwarden.com/help/saml-adfs/",
|
||||
image: "../../../../../../../images/integrations/azure-active-directory.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Auth0",
|
||||
linkURL: "https://bitwarden.com/help/saml-auth0/",
|
||||
image: "../../../../../../../images/integrations/logo-auth0-badge-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "AWS",
|
||||
linkURL: "https://bitwarden.com/help/saml-aws/",
|
||||
image: "../../../../../../../images/integrations/aws-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/aws-darkmode.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Entra ID",
|
||||
linkURL: "https://bitwarden.com/help/saml-azure/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Duo",
|
||||
linkURL: "https://bitwarden.com/help/saml-duo/",
|
||||
image: "../../../../../../../images/integrations/logo-duo-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Google",
|
||||
linkURL: "https://bitwarden.com/help/saml-google/",
|
||||
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "JumpCloud",
|
||||
linkURL: "https://bitwarden.com/help/saml-jumpcloud/",
|
||||
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "KeyCloak",
|
||||
linkURL: "https://bitwarden.com/help/saml-keycloak/",
|
||||
image: "../../../../../../../images/integrations/logo-keycloak-icon.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Okta",
|
||||
linkURL: "https://bitwarden.com/help/saml-okta/",
|
||||
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "OneLogin",
|
||||
linkURL: "https://bitwarden.com/help/saml-onelogin/",
|
||||
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "PingFederate",
|
||||
linkURL: "https://bitwarden.com/help/saml-pingfederate/",
|
||||
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Entra ID",
|
||||
linkURL: "https://bitwarden.com/help/microsoft-entra-id-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "Okta",
|
||||
linkURL: "https://bitwarden.com/help/okta-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "OneLogin",
|
||||
linkURL: "https://bitwarden.com/help/onelogin-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "JumpCloud",
|
||||
linkURL: "https://bitwarden.com/help/jumpcloud-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-jumpcloud-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/jumpcloud-darkmode.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "Ping Identity",
|
||||
linkURL: "https://bitwarden.com/help/ping-identity-scim-integration/",
|
||||
image: "../../../../../../../images/integrations/logo-ping-identity-badge-color.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
{
|
||||
name: "Active Directory",
|
||||
linkURL: "https://bitwarden.com/help/ldap-directory/",
|
||||
image: "../../../../../../../images/integrations/azure-active-directory.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Entra ID",
|
||||
linkURL: "https://bitwarden.com/help/microsoft-entra-id/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-entra-id-color.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "Google Workspace",
|
||||
linkURL: "https://bitwarden.com/help/workspace-directory/",
|
||||
image: "../../../../../../../images/integrations/logo-google-badge-color.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "Okta",
|
||||
linkURL: "https://bitwarden.com/help/okta-directory/",
|
||||
image: "../../../../../../../images/integrations/logo-okta-symbol-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/okta-darkmode.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "OneLogin",
|
||||
linkURL: "https://bitwarden.com/help/onelogin-directory/",
|
||||
image: "../../../../../../../images/integrations/logo-onelogin-badge-color.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/onelogin-darkmode.svg",
|
||||
type: IntegrationType.BWDC,
|
||||
},
|
||||
{
|
||||
name: "Splunk",
|
||||
linkURL: "https://bitwarden.com/help/splunk-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-splunk-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/splunk-darkmode.svg",
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.EventBasedOrganizationIntegrations)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((isEnabled) => {
|
||||
this.isEventBasedIntegrationsEnabled = isEnabled;
|
||||
});
|
||||
|
||||
if (this.isEventBasedIntegrationsEnabled) {
|
||||
this.integrationsList.push({
|
||||
name: "Crowdstrike",
|
||||
linkURL: "",
|
||||
image: "../../../../../../../images/integrations/logo-crowdstrike-black.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Sentinel",
|
||||
linkURL: "https://bitwarden.com/help/microsoft-sentinel-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-sentinel-color.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Rapid7",
|
||||
linkURL: "https://bitwarden.com/help/rapid7-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-rapid7-black.svg",
|
||||
imageDarkMode: "../../../../../../../images/integrations/rapid7-darkmode.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Elastic",
|
||||
linkURL: "https://bitwarden.com/help/elastic-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-elastic-badge-color.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Panther",
|
||||
linkURL: "https://bitwarden.com/help/panther-siem/",
|
||||
image: "../../../../../../../images/integrations/logo-panther-round-color.svg",
|
||||
type: IntegrationType.EVENT,
|
||||
},
|
||||
{
|
||||
name: "Microsoft Intune",
|
||||
linkURL: "https://bitwarden.com/help/deploy-browser-extensions-with-intune/",
|
||||
image: "../../../../../../../images/integrations/logo-microsoft-intune-color.svg",
|
||||
type: IntegrationType.DEVICE,
|
||||
},
|
||||
];
|
||||
description: "crowdstrikeEventIntegrationDesc",
|
||||
isConnected: false,
|
||||
canSetupConnection: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
get IntegrationType(): typeof IntegrationType {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<org-switcher [filter]="orgFilter" [hideNewButton]="hideNewOrgButton$ | async"></org-switcher>
|
||||
<bit-nav-group
|
||||
icon="bwi-filter"
|
||||
*ngIf="organization.useRiskInsights"
|
||||
*ngIf="organization.useRiskInsights && organization.canAccessReports"
|
||||
[text]="'accessIntelligence' | i18n"
|
||||
route="access-intelligence"
|
||||
>
|
||||
@@ -74,7 +74,11 @@
|
||||
>
|
||||
<bit-nav-item [text]="'subscription' | i18n" route="billing/subscription"></bit-nav-item>
|
||||
<ng-container *ngIf="(showPaymentAndHistory$ | async) && (organizationIsUnmanaged$ | async)">
|
||||
<bit-nav-item [text]="'paymentMethod' | i18n" route="billing/payment-method"></bit-nav-item>
|
||||
@let paymentDetailsPageData = paymentDetailsPageData$ | async;
|
||||
<bit-nav-item
|
||||
[text]="paymentDetailsPageData.textKey | i18n"
|
||||
[route]="paymentDetailsPageData.route"
|
||||
></bit-nav-item>
|
||||
<bit-nav-item [text]="'billingHistory' | i18n" route="billing/history"></bit-nav-item>
|
||||
</ng-container>
|
||||
</bit-nav-group>
|
||||
|
||||
@@ -27,17 +27,15 @@ import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { BannerModule, IconModule } from "@bitwarden/components";
|
||||
import { BannerModule, IconModule, AdminConsoleLogo } from "@bitwarden/components";
|
||||
|
||||
import { FreeFamiliesPolicyService } from "../../../billing/services/free-families-policy.service";
|
||||
import { OrgSwitcherComponent } from "../../../layouts/org-switcher/org-switcher.component";
|
||||
import { WebLayoutModule } from "../../../layouts/web-layout.module";
|
||||
import { AdminConsoleLogo } from "../../icons/admin-console-logo";
|
||||
|
||||
@Component({
|
||||
selector: "app-organization-layout",
|
||||
templateUrl: "organization-layout.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
@@ -66,6 +64,11 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
|
||||
protected canShowPoliciesTab$: Observable<boolean>;
|
||||
|
||||
protected paymentDetailsPageData$: Observable<{
|
||||
route: string;
|
||||
textKey: string;
|
||||
}>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private organizationService: OrganizationService,
|
||||
@@ -137,6 +140,16 @@ export class OrganizationLayoutComponent implements OnInit {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
this.paymentDetailsPageData$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout)
|
||||
.pipe(
|
||||
map((managePaymentDetailsOutsideCheckout) =>
|
||||
managePaymentDetailsOutsideCheckout
|
||||
? { route: "billing/payment-details", textKey: "paymentDetails" }
|
||||
: { route: "billing/payment-method", textKey: "paymentMethod" },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
canShowVaultTab(organization: Organization): boolean {
|
||||
|
||||
@@ -38,7 +38,6 @@ export interface EntityEventsDialogParams {
|
||||
@Component({
|
||||
imports: [SharedModule],
|
||||
templateUrl: "entity-events.component.html",
|
||||
standalone: true,
|
||||
})
|
||||
export class EntityEventsComponent implements OnInit, OnDestroy {
|
||||
loading = true;
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -156,7 +157,11 @@ export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
private orgCollections$ = from(this.collectionAdminService.getAll(this.organizationId)).pipe(
|
||||
private orgCollections$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.collectionAdminService.collectionAdminViews$(this.organizationId, userId),
|
||||
),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<p *ngIf="!dataSource.filteredData.length">{{ "noGroupsInList" | i18n }}</p>
|
||||
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||
from overflowing the <main> element. -->
|
||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table *ngIf="dataSource.filteredData.length" [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
from,
|
||||
lastValueFrom,
|
||||
map,
|
||||
Observable,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
@@ -25,10 +26,13 @@ import {
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { DialogService, TableDataSource, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { GroupDetailsView, InternalGroupApiService as GroupService } from "../core";
|
||||
|
||||
@@ -85,8 +89,8 @@ export class GroupsComponent {
|
||||
protected searchControl = new FormControl("");
|
||||
|
||||
// Fixed sizes used for cdkVirtualScroll
|
||||
protected rowHeight = 52;
|
||||
protected rowHeightClass = `tw-h-[52px]`;
|
||||
protected rowHeight = 50;
|
||||
protected rowHeightClass = `tw-h-[50px]`;
|
||||
|
||||
protected ModalTabType = GroupAddEditTabType;
|
||||
private refreshGroups$ = new BehaviorSubject<void>(null);
|
||||
@@ -100,6 +104,8 @@ export class GroupsComponent {
|
||||
private logService: LogService,
|
||||
private collectionService: CollectionService,
|
||||
private toastService: ToastService,
|
||||
private keyService: KeyService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.route.params
|
||||
.pipe(
|
||||
@@ -244,16 +250,22 @@ export class GroupsComponent {
|
||||
this.dataSource.data = this.dataSource.data.filter((g) => g !== groupRow);
|
||||
}
|
||||
|
||||
private async toCollectionMap(response: ListResponse<CollectionResponse>) {
|
||||
private toCollectionMap(
|
||||
response: ListResponse<CollectionResponse>,
|
||||
): Observable<Record<string, CollectionView>> {
|
||||
const collections = response.data.map(
|
||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
|
||||
);
|
||||
const decryptedCollections = await this.collectionService.decryptMany(collections);
|
||||
|
||||
// Convert to an object using collection Ids as keys for faster name lookups
|
||||
const collectionMap: Record<string, CollectionView> = {};
|
||||
decryptedCollections.forEach((c) => (collectionMap[c.id] = c));
|
||||
|
||||
return collectionMap;
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
switchMap((orgKeys) => this.collectionService.decryptMany$(collections, orgKeys)),
|
||||
map((collections) => {
|
||||
const collectionMap: Record<string, CollectionView> = {};
|
||||
collections.forEach((c) => (collectionMap[c.id] = c));
|
||||
return collectionMap;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { SharedModule } from "../../../shared/shared.module";
|
||||
|
||||
@Component({
|
||||
templateUrl: "verify-recover-delete-org.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class VerifyRecoverDeleteOrgComponent implements OnInit {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<bit-dialog [title]="'recoverAccount' | i18n" [subtitle]="dialogData.name">
|
||||
<ng-container bitDialogContent>
|
||||
<bit-callout type="warning"
|
||||
>{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }}
|
||||
</bit-callout>
|
||||
|
||||
<auth-input-password
|
||||
[flow]="inputPasswordFlow"
|
||||
[masterPasswordPolicyOptions]="masterPasswordPolicyOptions$ | async"
|
||||
></auth-input-password>
|
||||
</ng-container>
|
||||
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="button" bitButton buttonType="primary" [bitAction]="handlePrimaryButtonClick">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton buttonType="secondary" bitDialogClose>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
@@ -0,0 +1,146 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Inject, ViewChild } from "@angular/core";
|
||||
import { switchMap } from "rxjs";
|
||||
|
||||
import { InputPasswordComponent, InputPasswordFlow } from "@bitwarden/auth/angular";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogModule,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../../services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
|
||||
/**
|
||||
* Encapsulates a few key data inputs needed to initiate an account recovery
|
||||
* process for the organization user in question.
|
||||
*/
|
||||
export type AccountRecoveryDialogData = {
|
||||
/**
|
||||
* The organization user's full name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The organization user's email address
|
||||
*/
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* The `organizationUserId` for the user
|
||||
*/
|
||||
organizationUserId: string;
|
||||
|
||||
/**
|
||||
* The organization's `organizationId`
|
||||
*/
|
||||
organizationId: OrganizationId;
|
||||
};
|
||||
|
||||
export const AccountRecoveryDialogResultType = {
|
||||
Ok: "ok",
|
||||
} as const;
|
||||
|
||||
export type AccountRecoveryDialogResultType =
|
||||
(typeof AccountRecoveryDialogResultType)[keyof typeof AccountRecoveryDialogResultType];
|
||||
|
||||
/**
|
||||
* Used in a dialog for initiating the account recovery process against a
|
||||
* given organization user. An admin will access this form when they want to
|
||||
* reset a user's password and log them out of sessions.
|
||||
*/
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: "app-account-recovery-dialog",
|
||||
templateUrl: "account-recovery-dialog.component.html",
|
||||
imports: [
|
||||
AsyncActionsModule,
|
||||
ButtonModule,
|
||||
CalloutModule,
|
||||
CommonModule,
|
||||
DialogModule,
|
||||
I18nPipe,
|
||||
InputPasswordComponent,
|
||||
],
|
||||
})
|
||||
export class AccountRecoveryDialogComponent {
|
||||
@ViewChild(InputPasswordComponent)
|
||||
inputPasswordComponent: InputPasswordComponent | undefined = undefined;
|
||||
|
||||
masterPasswordPolicyOptions$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
);
|
||||
|
||||
inputPasswordFlow = InputPasswordFlow.ChangePasswordDelegation;
|
||||
|
||||
get loggedOutWarningName() {
|
||||
return this.dialogData.name != null ? this.dialogData.name : this.i18nService.t("thisUser");
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected dialogData: AccountRecoveryDialogData,
|
||||
private accountService: AccountService,
|
||||
private dialogRef: DialogRef<AccountRecoveryDialogResultType>,
|
||||
private i18nService: I18nService,
|
||||
private policyService: PolicyService,
|
||||
private resetPasswordService: OrganizationUserResetPasswordService,
|
||||
private toastService: ToastService,
|
||||
) {}
|
||||
|
||||
handlePrimaryButtonClick = async () => {
|
||||
if (!this.inputPasswordComponent) {
|
||||
throw new Error("InputPasswordComponent is not initialized");
|
||||
}
|
||||
|
||||
const passwordInputResult = await this.inputPasswordComponent.submit();
|
||||
if (!passwordInputResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.resetPasswordService.resetMasterPassword(
|
||||
passwordInputResult.newPassword,
|
||||
this.dialogData.email,
|
||||
this.dialogData.organizationUserId,
|
||||
this.dialogData.organizationId,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: "",
|
||||
message: this.i18nService.t("resetPasswordSuccess"),
|
||||
});
|
||||
|
||||
this.dialogRef.close(AccountRecoveryDialogResultType.Ok);
|
||||
};
|
||||
|
||||
/**
|
||||
* Strongly typed helper to open an `AccountRecoveryDialogComponent`
|
||||
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||
* @param dialogConfig Configuration for the dialog
|
||||
*/
|
||||
static open = (
|
||||
dialogService: DialogService,
|
||||
dialogConfig: DialogConfig<
|
||||
AccountRecoveryDialogData,
|
||||
DialogRef<AccountRecoveryDialogResultType, unknown>
|
||||
>,
|
||||
) => {
|
||||
return dialogService.open<AccountRecoveryDialogResultType, AccountRecoveryDialogData>(
|
||||
AccountRecoveryDialogComponent,
|
||||
dialogConfig,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./account-recovery-dialog.component";
|
||||
@@ -11,10 +11,13 @@ import {
|
||||
OrganizationUserBulkResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response";
|
||||
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
@@ -23,11 +26,13 @@ import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserService } from "../../services/organization-user/organization-user.service";
|
||||
|
||||
import { BaseBulkConfirmComponent } from "./base-bulk-confirm.component";
|
||||
import { BulkUserDetails } from "./bulk-status.component";
|
||||
|
||||
type BulkConfirmDialogParams = {
|
||||
organizationId: string;
|
||||
organization: Organization;
|
||||
users: BulkUserDetails[];
|
||||
};
|
||||
|
||||
@@ -36,7 +41,7 @@ type BulkConfirmDialogParams = {
|
||||
standalone: false,
|
||||
})
|
||||
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
organizationId: string;
|
||||
organization: Organization;
|
||||
organizationKey$: Observable<OrgKey>;
|
||||
users: BulkUserDetails[];
|
||||
|
||||
@@ -47,13 +52,15 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
protected i18nService: I18nService,
|
||||
private stateProvider: StateProvider,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(keyService, encryptService, i18nService);
|
||||
|
||||
this.organizationId = dialogParams.organizationId;
|
||||
this.organization = dialogParams.organization;
|
||||
this.organizationKey$ = this.stateProvider.activeUserId$.pipe(
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((organizationKeysById) => organizationKeysById[this.organizationId as OrganizationId]),
|
||||
map((organizationKeysById) => organizationKeysById[this.organization.id as OrganizationId]),
|
||||
takeUntilDestroyed(),
|
||||
);
|
||||
this.users = dialogParams.users;
|
||||
@@ -66,7 +73,7 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>
|
||||
> =>
|
||||
await this.organizationUserApiService.postOrganizationUsersPublicKey(
|
||||
this.organizationId,
|
||||
this.organization.id,
|
||||
this.filteredUsers.map((user) => user.id),
|
||||
);
|
||||
|
||||
@@ -76,11 +83,19 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
protected postConfirmRequest = async (
|
||||
userIdsWithKeys: { id: string; key: string }[],
|
||||
): Promise<ListResponse<OrganizationUserBulkResponse | ProviderUserBulkResponse>> => {
|
||||
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
|
||||
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
|
||||
this.organizationId,
|
||||
request,
|
||||
);
|
||||
if (
|
||||
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
|
||||
) {
|
||||
return await firstValueFrom(
|
||||
this.organizationUserService.bulkConfirmUsers(this.organization, userIdsWithKeys),
|
||||
);
|
||||
} else {
|
||||
const request = new OrganizationUserBulkConfirmRequest(userIdsWithKeys);
|
||||
return await this.organizationUserApiService.postOrganizationUserBulkConfirm(
|
||||
this.organization.id,
|
||||
request,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
static open(dialogService: DialogService, config: DialogConfig<BulkConfirmDialogParams>) {
|
||||
|
||||
@@ -134,17 +134,11 @@
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="manageUsers"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<bit-label>{{ "manageUsers" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<bit-form-control>
|
||||
<input
|
||||
type="checkbox"
|
||||
bitCheckbox
|
||||
formControlName="manageResetPassword"
|
||||
(change)="handleDependentPermissions()"
|
||||
/>
|
||||
<input type="checkbox" bitCheckbox formControlName="manageResetPassword" />
|
||||
<bit-label>{{ "manageAccountRecovery" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
|
||||
@@ -32,8 +32,8 @@ import {
|
||||
import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
@@ -277,9 +277,16 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
),
|
||||
);
|
||||
|
||||
const collections = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) =>
|
||||
this.collectionAdminService.collectionAdminViews$(this.params.organizationId, userId),
|
||||
),
|
||||
);
|
||||
|
||||
combineLatest({
|
||||
organization: this.organization$,
|
||||
collections: this.collectionAdminService.getAll(this.params.organizationId),
|
||||
collections,
|
||||
userDetails: userDetails$,
|
||||
groups: groups$,
|
||||
})
|
||||
@@ -453,28 +460,6 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
return Object.assign(p, partialPermissions);
|
||||
}
|
||||
|
||||
async handleDependentPermissions() {
|
||||
const separateCustomRolePermissions = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.SeparateCustomRolePermissions,
|
||||
);
|
||||
if (separateCustomRolePermissions) {
|
||||
return;
|
||||
}
|
||||
// Manage Password Reset (Account Recovery) must have Manage Users enabled
|
||||
if (
|
||||
this.permissionsGroup.value.manageResetPassword &&
|
||||
!this.permissionsGroup.value.manageUsers
|
||||
) {
|
||||
this.permissionsGroup.value.manageUsers = true;
|
||||
(document.getElementById("manageUsers") as HTMLInputElement).checked = true;
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("accountRecoveryManageUsers"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
this.formGroup.markAllAsTouched();
|
||||
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog [title]="'recoverAccount' | i18n" [subtitle]="data.name">
|
||||
<ng-container bitDialogContent>
|
||||
<bit-callout type="warning"
|
||||
>{{ "resetPasswordLoggedOutWarning" | i18n: loggedOutWarningName }}
|
||||
</bit-callout>
|
||||
<auth-password-callout
|
||||
[policy]="enforcedPolicyOptions"
|
||||
message="resetPasswordMasterPasswordPolicyInEffect"
|
||||
*ngIf="enforcedPolicyOptions"
|
||||
>
|
||||
</auth-password-callout>
|
||||
<bit-form-field>
|
||||
<bit-label>
|
||||
{{ "newPassword" | i18n }}
|
||||
</bit-label>
|
||||
<input
|
||||
id="newPassword"
|
||||
bitInput
|
||||
[type]="showPassword ? 'text' : 'password'"
|
||||
name="NewPassword"
|
||||
formControlName="newPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-generate"
|
||||
bitSuffix
|
||||
[appA11yTitle]="'generatePassword' | i18n"
|
||||
(click)="generatePassword()"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
[bitIconButton]="showPassword ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||
buttonType="secondary"
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
(click)="togglePassword()"
|
||||
></button>
|
||||
<button
|
||||
type="button"
|
||||
bitSuffix
|
||||
bitIconButton="bwi-clone"
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy()"
|
||||
></button>
|
||||
</bit-form-field>
|
||||
<tools-password-strength
|
||||
[password]="formGroup.value.newPassword"
|
||||
[email]="data.email"
|
||||
[showText]="true"
|
||||
(passwordStrengthScore)="getStrengthScore($event)"
|
||||
>
|
||||
</tools-password-strength>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -1,220 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
import { Subject, switchMap, takeUntil } from "rxjs";
|
||||
|
||||
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
DialogRef,
|
||||
DialogService,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { OrganizationUserResetPasswordService } from "../services/organization-user-reset-password/organization-user-reset-password.service";
|
||||
|
||||
/**
|
||||
* Encapsulates a few key data inputs needed to initiate an account recovery
|
||||
* process for the organization user in question.
|
||||
*/
|
||||
export type ResetPasswordDialogData = {
|
||||
/**
|
||||
* The organization user's full name
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The organization user's email address
|
||||
*/
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* The `organizationUserId` for the user
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The organization's `organizationId`
|
||||
*/
|
||||
organizationId: string;
|
||||
};
|
||||
|
||||
// FIXME: update to use a const object instead of a typescript enum
|
||||
// eslint-disable-next-line @bitwarden/platform/no-enums
|
||||
export enum ResetPasswordDialogResult {
|
||||
Ok = "ok",
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-reset-password",
|
||||
templateUrl: "reset-password.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
/**
|
||||
* Used in a dialog for initiating the account recovery process against a
|
||||
* given organization user. An admin will access this form when they want to
|
||||
* reset a user's password and log them out of sessions.
|
||||
*/
|
||||
export class ResetPasswordComponent implements OnInit, OnDestroy {
|
||||
formGroup = this.formBuilder.group({
|
||||
newPassword: ["", Validators.required],
|
||||
});
|
||||
|
||||
@ViewChild(PasswordStrengthV2Component) passwordStrengthComponent: PasswordStrengthV2Component;
|
||||
|
||||
enforcedPolicyOptions: MasterPasswordPolicyOptions;
|
||||
showPassword = false;
|
||||
passwordStrengthScore: number;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected data: ResetPasswordDialogData,
|
||||
private resetPasswordService: OrganizationUserResetPasswordService,
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private logService: LogService,
|
||||
private dialogService: DialogService,
|
||||
private toastService: ToastService,
|
||||
private formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<ResetPasswordDialogResult>,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.accountService.activeAccount$
|
||||
.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId)),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe(
|
||||
(enforcedPasswordPolicyOptions) =>
|
||||
(this.enforcedPolicyOptions = enforcedPasswordPolicyOptions),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
get loggedOutWarningName() {
|
||||
return this.data.name != null ? this.data.name : this.i18nService.t("thisUser");
|
||||
}
|
||||
|
||||
async generatePassword() {
|
||||
const options = (await this.passwordGenerationService.getOptions())?.[0] ?? {};
|
||||
this.formGroup.patchValue({
|
||||
newPassword: await this.passwordGenerationService.generatePassword(options),
|
||||
});
|
||||
this.passwordStrengthComponent.updatePasswordStrength(this.formGroup.value.newPassword);
|
||||
}
|
||||
|
||||
togglePassword() {
|
||||
this.showPassword = !this.showPassword;
|
||||
document.getElementById("newPassword").focus();
|
||||
}
|
||||
|
||||
copy() {
|
||||
const value = this.formGroup.value.newPassword;
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.platformUtilsService.copyToClipboard(value, { window: window });
|
||||
this.toastService.showToast({
|
||||
variant: "info",
|
||||
title: null,
|
||||
message: this.i18nService.t("valueCopied", this.i18nService.t("password")),
|
||||
});
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
// Validation
|
||||
if (this.formGroup.value.newPassword == null || this.formGroup.value.newPassword === "") {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordRequired"),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.formGroup.value.newPassword.length < Utils.minimumPasswordLength) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordMinlength", Utils.minimumPasswordLength),
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.enforcedPolicyOptions != null &&
|
||||
!this.policyService.evaluateMasterPassword(
|
||||
this.passwordStrengthScore,
|
||||
this.formGroup.value.newPassword,
|
||||
this.enforcedPolicyOptions,
|
||||
)
|
||||
) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("masterPasswordPolicyRequirementsNotMet"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.passwordStrengthScore < 3) {
|
||||
const result = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "weakMasterPassword" },
|
||||
content: { key: "weakMasterPasswordDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.resetPasswordService.resetMasterPassword(
|
||||
this.formGroup.value.newPassword,
|
||||
this.data.email,
|
||||
this.data.id,
|
||||
this.data.organizationId,
|
||||
);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
message: this.i18nService.t("resetPasswordSuccess"),
|
||||
});
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
|
||||
this.dialogRef.close(ResetPasswordDialogResult.Ok);
|
||||
};
|
||||
|
||||
getStrengthScore(result: number) {
|
||||
this.passwordStrengthScore = result;
|
||||
}
|
||||
|
||||
static open = (dialogService: DialogService, input: DialogConfig<ResetPasswordDialogData>) => {
|
||||
return dialogService.open<ResetPasswordDialogResult>(ResetPasswordComponent, input);
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
<app-organization-free-trial-warning
|
||||
[organization]="organization"
|
||||
(clicked)="navigateToPaymentMethod()"
|
||||
>
|
||||
</app-organization-free-trial-warning>
|
||||
<app-header>
|
||||
<bit-search
|
||||
class="tw-grow"
|
||||
@@ -75,7 +80,7 @@
|
||||
</bit-callout>
|
||||
<!-- The padding on the bottom of the cdk-virtual-scroll-viewport element is required to prevent table row content
|
||||
from overflowing the <main> element. -->
|
||||
<cdk-virtual-scroll-viewport scrollWindow [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<cdk-virtual-scroll-viewport bitScrollLayout [itemSize]="rowHeight" class="tw-pb-8">
|
||||
<bit-table [dataSource]="dataSource">
|
||||
<ng-container header>
|
||||
<tr>
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Observable,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
tap,
|
||||
} from "rxjs";
|
||||
|
||||
import {
|
||||
@@ -52,6 +53,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, SimpleDialogOptions, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -60,12 +62,17 @@ import {
|
||||
ChangePlanDialogResultType,
|
||||
openChangePlanDialog,
|
||||
} from "../../../billing/organizations/change-plan-dialog.component";
|
||||
import { OrganizationWarningsService } from "../../../billing/warnings/services";
|
||||
import { BaseMembersComponent } from "../../common/base-members.component";
|
||||
import { PeopleTableDataSource } from "../../common/people-table-data-source";
|
||||
import { GroupApiService } from "../core";
|
||||
import { OrganizationUserView } from "../core/views/organization-user.view";
|
||||
import { openEntityEventsDialog } from "../manage/entity-events.component";
|
||||
|
||||
import {
|
||||
AccountRecoveryDialogComponent,
|
||||
AccountRecoveryDialogResultType,
|
||||
} from "./components/account-recovery/account-recovery-dialog.component";
|
||||
import { BulkConfirmDialogComponent } from "./components/bulk/bulk-confirm-dialog.component";
|
||||
import { BulkDeleteDialogComponent } from "./components/bulk/bulk-delete-dialog.component";
|
||||
import { BulkEnableSecretsManagerDialogComponent } from "./components/bulk/bulk-enable-sm-dialog.component";
|
||||
@@ -78,11 +85,8 @@ import {
|
||||
openUserAddEditDialog,
|
||||
} from "./components/member-dialog";
|
||||
import { isFixedSeatPlan } from "./components/member-dialog/validators/org-seat-limit-reached.validator";
|
||||
import {
|
||||
ResetPasswordComponent,
|
||||
ResetPasswordDialogResult,
|
||||
} from "./components/reset-password.component";
|
||||
import { DeleteManagedMemberWarningService } from "./services/delete-managed-member/delete-managed-member-warning.service";
|
||||
import { OrganizationUserService } from "./services/organization-user/organization-user.service";
|
||||
|
||||
class MembersTableDataSource extends PeopleTableDataSource<OrganizationUserView> {
|
||||
protected statusType = OrganizationUserStatusType;
|
||||
@@ -107,8 +111,8 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
protected showUserManagementControls$: Observable<boolean>;
|
||||
|
||||
// Fixed sizes used for cdkVirtualScroll
|
||||
protected rowHeight = 69;
|
||||
protected rowHeightClass = `tw-h-[69px]`;
|
||||
protected rowHeight = 66;
|
||||
protected rowHeightClass = `tw-h-[66px]`;
|
||||
|
||||
private organizationUsersCount = 0;
|
||||
|
||||
@@ -141,6 +145,8 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
private billingApiService: BillingApiServiceAbstraction,
|
||||
protected deleteManagedMemberWarningService: DeleteManagedMemberWarningService,
|
||||
private configService: ConfigService,
|
||||
private organizationUserService: OrganizationUserService,
|
||||
private organizationWarningsService: OrganizationWarningsService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
@@ -194,7 +200,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
this.organization.canManageUsersPassword &&
|
||||
!this.organization.hasPublicAndPrivateKeys
|
||||
) {
|
||||
const orgShareKey = await this.keyService.getOrgKey(this.organization.id);
|
||||
const orgShareKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[this.organization.id] ?? null),
|
||||
),
|
||||
);
|
||||
|
||||
const orgKeys = await this.keyService.makeKeyPair(orgShareKey);
|
||||
const request = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
const response = await this.organizationApiService.updateKeys(
|
||||
@@ -237,16 +250,16 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
// Setup feature flag-dependent observables
|
||||
const separateCustomRolePermissionsEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.SeparateCustomRolePermissions,
|
||||
);
|
||||
this.showUserManagementControls$ = separateCustomRolePermissionsEnabled$.pipe(
|
||||
map(
|
||||
(separateCustomRolePermissionsEnabled) =>
|
||||
!separateCustomRolePermissionsEnabled || this.organization.canManageUsers,
|
||||
),
|
||||
this.showUserManagementControls$ = organization$.pipe(
|
||||
map((organization) => organization.canManageUsers),
|
||||
);
|
||||
organization$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
tap((org) => (this.organization = org)),
|
||||
switchMap((org) => this.organizationWarningsService.showInactiveSubscriptionDialog$(org)),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
async getUsers(): Promise<OrganizationUserView[]> {
|
||||
@@ -297,17 +310,30 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
* Retrieve a map of all collection IDs <-> names for the organization.
|
||||
*/
|
||||
async getCollectionNameMap() {
|
||||
const collectionMap = new Map<string, string>();
|
||||
const response = await this.apiService.getCollections(this.organization.id);
|
||||
|
||||
const collections = response.data.map(
|
||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse)),
|
||||
const response = from(this.apiService.getCollections(this.organization.id)).pipe(
|
||||
map((res) =>
|
||||
res.data.map((r) => new Collection(new CollectionData(r as CollectionDetailsResponse))),
|
||||
),
|
||||
);
|
||||
const decryptedCollections = await this.collectionService.decryptMany(collections);
|
||||
|
||||
decryptedCollections.forEach((c) => collectionMap.set(c.id, c.name));
|
||||
const decryptedCollections$ = combineLatest([
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
),
|
||||
response,
|
||||
]).pipe(
|
||||
switchMap(([orgKeys, collections]) =>
|
||||
this.collectionService.decryptMany$(collections, orgKeys),
|
||||
),
|
||||
map((collections) => {
|
||||
const collectionMap = new Map<string, string>();
|
||||
collections.forEach((c) => collectionMap.set(c.id, c.name));
|
||||
return collectionMap;
|
||||
}),
|
||||
);
|
||||
|
||||
return collectionMap;
|
||||
return await firstValueFrom(decryptedCollections$);
|
||||
}
|
||||
|
||||
removeUser(id: string): Promise<void> {
|
||||
@@ -327,15 +353,29 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
async confirmUser(user: OrganizationUserView, publicKey: Uint8Array): Promise<void> {
|
||||
const orgKey = await this.keyService.getOrgKey(this.organization.id);
|
||||
const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||
this.organization.id,
|
||||
user.id,
|
||||
request,
|
||||
);
|
||||
if (
|
||||
await firstValueFrom(this.configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation))
|
||||
) {
|
||||
await firstValueFrom(
|
||||
this.organizationUserService.confirmUser(this.organization, user, publicKey),
|
||||
);
|
||||
} else {
|
||||
const orgKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[this.organization.id] ?? null),
|
||||
),
|
||||
);
|
||||
const key = await this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey);
|
||||
const request = new OrganizationUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
await this.organizationUserApiService.postOrganizationUserConfirm(
|
||||
this.organization.id,
|
||||
user.id,
|
||||
request,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async revoke(user: OrganizationUserView) {
|
||||
@@ -694,7 +734,7 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
|
||||
const dialogRef = BulkConfirmDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
organizationId: this.organization.id,
|
||||
organization: this.organization,
|
||||
users: this.dataSource.getCheckedUsers(),
|
||||
},
|
||||
});
|
||||
@@ -738,19 +778,32 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
}
|
||||
|
||||
async resetPassword(user: OrganizationUserView) {
|
||||
const dialogRef = ResetPasswordComponent.open(this.dialogService, {
|
||||
if (!user || !user.email || !user.id) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: this.i18nService.t("errorOccurred"),
|
||||
message: this.i18nService.t("orgUserDetailsNotFound"),
|
||||
});
|
||||
this.logService.error("Org user details not found when attempting account recovery");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const dialogRef = AccountRecoveryDialogComponent.open(this.dialogService, {
|
||||
data: {
|
||||
name: this.userNamePipe.transform(user),
|
||||
email: user != null ? user.email : null,
|
||||
organizationId: this.organization.id,
|
||||
id: user != null ? user.id : null,
|
||||
email: user.email,
|
||||
organizationId: this.organization.id as OrganizationId,
|
||||
organizationUserId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === ResetPasswordDialogResult.Ok) {
|
||||
if (result === AccountRecoveryDialogResultType.Ok) {
|
||||
await this.load();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
protected async removeUserConfirmationDialog(user: OrganizationUserView) {
|
||||
@@ -891,4 +944,14 @@ export class MembersComponent extends BaseMembersComponent<OrganizationUserView>
|
||||
.getCheckedUsers()
|
||||
.every((member) => member.managedByOrganization && validStatuses.includes(member.status));
|
||||
}
|
||||
|
||||
async navigateToPaymentMethod() {
|
||||
const managePaymentDetailsOutsideCheckout = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM21881_ManagePaymentDetailsOutsideCheckout,
|
||||
);
|
||||
const route = managePaymentDetailsOutsideCheckout ? "payment-details" : "payment-method";
|
||||
await this.router.navigate(["organizations", `${this.organization?.id}`, "billing", route], {
|
||||
state: { launchPaymentModalAutomatically: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { PasswordStrengthV2Component } from "@bitwarden/angular/tools/password-strength/password-strength-v2.component";
|
||||
import { PasswordCalloutComponent } from "@bitwarden/auth/angular";
|
||||
import { ScrollLayoutDirective } from "@bitwarden/components";
|
||||
|
||||
import { OrganizationFreeTrialWarningComponent } from "../../../billing/warnings/components";
|
||||
import { LooseComponentsModule } from "../../../shared";
|
||||
import { SharedOrganizationModule } from "../shared";
|
||||
|
||||
@@ -14,7 +16,6 @@ import { BulkRemoveDialogComponent } from "./components/bulk/bulk-remove-dialog.
|
||||
import { BulkRestoreRevokeComponent } from "./components/bulk/bulk-restore-revoke.component";
|
||||
import { BulkStatusComponent } from "./components/bulk/bulk-status.component";
|
||||
import { UserDialogModule } from "./components/member-dialog";
|
||||
import { ResetPasswordComponent } from "./components/reset-password.component";
|
||||
import { MembersRoutingModule } from "./members-routing.module";
|
||||
import { MembersComponent } from "./members.component";
|
||||
|
||||
@@ -27,6 +28,8 @@ import { MembersComponent } from "./members.component";
|
||||
PasswordCalloutComponent,
|
||||
ScrollingModule,
|
||||
PasswordStrengthV2Component,
|
||||
ScrollLayoutDirective,
|
||||
OrganizationFreeTrialWarningComponent,
|
||||
],
|
||||
declarations: [
|
||||
BulkConfirmDialogComponent,
|
||||
@@ -35,7 +38,6 @@ import { MembersComponent } from "./members.component";
|
||||
BulkRestoreRevokeComponent,
|
||||
BulkStatusComponent,
|
||||
MembersComponent,
|
||||
ResetPasswordComponent,
|
||||
BulkDeleteDialogComponent,
|
||||
],
|
||||
})
|
||||
|
||||
@@ -12,13 +12,14 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { OrganizationKeysResponse } from "@bitwarden/common/admin-console/models/response/organization-keys.response";
|
||||
import { OrganizationApiService } from "@bitwarden/common/admin-console/services/organization/organization-api.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { EncryptionType } from "@bitwarden/common/platform/enums";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey, OrgKey, MasterKey } from "@bitwarden/common/types/key";
|
||||
import { KdfType, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -36,6 +37,8 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let organizationApiService: MockProxy<OrganizationApiService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
beforeAll(() => {
|
||||
keyService = mock<KeyService>();
|
||||
@@ -44,6 +47,7 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
organizationApiService = mock<OrganizationApiService>();
|
||||
i18nService = mock<I18nService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
sut = new OrganizationUserResetPasswordService(
|
||||
keyService,
|
||||
@@ -52,6 +56,7 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
organizationUserApiService,
|
||||
organizationApiService,
|
||||
i18nService,
|
||||
accountService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -142,7 +147,10 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||
keyService.getOrgKey.mockResolvedValue(mockOrgKey);
|
||||
keyService.orgKeys$.mockReturnValue(
|
||||
of({ [mockOrgId]: mockOrgKey } as Record<OrganizationId, OrgKey>),
|
||||
);
|
||||
|
||||
encryptService.decryptToBytes.mockResolvedValue(mockRandomBytes);
|
||||
|
||||
encryptService.rsaDecrypt.mockResolvedValue(mockRandomBytes);
|
||||
@@ -170,7 +178,7 @@ describe("OrganizationUserResetPasswordService", () => {
|
||||
});
|
||||
|
||||
it("should throw an error if the org key is null", async () => {
|
||||
keyService.getOrgKey.mockResolvedValue(null);
|
||||
keyService.orgKeys$.mockReturnValue(of(null));
|
||||
await expect(
|
||||
sut.resetMasterPassword(mockNewMP, mockEmail, mockOrgUserId, mockOrgId),
|
||||
).rejects.toThrow();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserApiService,
|
||||
@@ -10,11 +10,16 @@ import {
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import {
|
||||
EncryptedString,
|
||||
EncString,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
import {
|
||||
Argon2KdfConfig,
|
||||
@@ -44,6 +49,7 @@ export class OrganizationUserResetPasswordService
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -96,7 +102,7 @@ export class OrganizationUserResetPasswordService
|
||||
newMasterPassword: string,
|
||||
email: string,
|
||||
orgUserId: string,
|
||||
orgId: string,
|
||||
orgId: OrganizationId,
|
||||
): Promise<void> {
|
||||
const response = await this.organizationUserApiService.getOrganizationUserResetPasswordDetails(
|
||||
orgId,
|
||||
@@ -108,7 +114,14 @@ export class OrganizationUserResetPasswordService
|
||||
}
|
||||
|
||||
// Decrypt Organization's encrypted Private Key with org key
|
||||
const orgSymKey = await this.keyService.getOrgKey(orgId);
|
||||
const orgSymKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[orgId as OrganizationId] ?? null),
|
||||
),
|
||||
);
|
||||
|
||||
if (orgSymKey == null) {
|
||||
throw new Error("No org key found");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserConfirmRequest,
|
||||
OrganizationUserBulkConfirmRequest,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserBulkResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
||||
|
||||
import { OrganizationUserService } from "./organization-user.service";
|
||||
|
||||
describe("OrganizationUserService", () => {
|
||||
let service: OrganizationUserService;
|
||||
let keyService: jest.Mocked<KeyService>;
|
||||
let encryptService: jest.Mocked<EncryptService>;
|
||||
let organizationUserApiService: jest.Mocked<OrganizationUserApiService>;
|
||||
let accountService: jest.Mocked<AccountService>;
|
||||
let i18nService: jest.Mocked<I18nService>;
|
||||
|
||||
const mockOrganization = new Organization();
|
||||
mockOrganization.id = "org-123" as OrganizationId;
|
||||
|
||||
const mockOrganizationUser = new OrganizationUserView();
|
||||
mockOrganizationUser.id = "user-123";
|
||||
|
||||
const mockPublicKey = new Uint8Array(64) as CsprngArray;
|
||||
const mockRandomBytes = new Uint8Array(64) as CsprngArray;
|
||||
const mockOrgKey = new SymmetricCryptoKey(mockRandomBytes) as OrgKey;
|
||||
const mockEncryptedKey = { encryptedString: "encrypted-key" } as EncString;
|
||||
const mockEncryptedCollectionName = { encryptedString: "encrypted-collection-name" } as EncString;
|
||||
const mockDefaultCollectionName = "My Items";
|
||||
|
||||
const setupCommonMocks = () => {
|
||||
keyService.orgKeys$.mockReturnValue(
|
||||
of({ [mockOrganization.id]: mockOrgKey } as Record<OrganizationId, OrgKey>),
|
||||
);
|
||||
encryptService.encryptString.mockResolvedValue(mockEncryptedCollectionName);
|
||||
i18nService.t.mockReturnValue(mockDefaultCollectionName);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = {
|
||||
orgKeys$: jest.fn(),
|
||||
} as any;
|
||||
|
||||
encryptService = {
|
||||
encryptString: jest.fn(),
|
||||
encapsulateKeyUnsigned: jest.fn(),
|
||||
} as any;
|
||||
|
||||
organizationUserApiService = {
|
||||
postOrganizationUserConfirm: jest.fn(),
|
||||
postOrganizationUserBulkConfirm: jest.fn(),
|
||||
} as any;
|
||||
|
||||
accountService = {
|
||||
activeAccount$: of({ id: "user-123" }),
|
||||
} as any;
|
||||
|
||||
i18nService = {
|
||||
t: jest.fn(),
|
||||
} as any;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
OrganizationUserService,
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: EncryptService, useValue: encryptService },
|
||||
{ provide: OrganizationUserApiService, useValue: organizationUserApiService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(OrganizationUserService);
|
||||
});
|
||||
|
||||
describe("confirmUser", () => {
|
||||
beforeEach(() => {
|
||||
setupCommonMocks();
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(mockEncryptedKey);
|
||||
organizationUserApiService.postOrganizationUserConfirm.mockReturnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
it("should confirm a user successfully", (done) => {
|
||||
service.confirmUser(mockOrganization, mockOrganizationUser, mockPublicKey).subscribe({
|
||||
next: () => {
|
||||
expect(i18nService.t).toHaveBeenCalledWith("myItems");
|
||||
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(
|
||||
mockDefaultCollectionName,
|
||||
mockOrgKey,
|
||||
);
|
||||
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
|
||||
mockOrgKey,
|
||||
mockPublicKey,
|
||||
);
|
||||
|
||||
expect(organizationUserApiService.postOrganizationUserConfirm).toHaveBeenCalledWith(
|
||||
mockOrganization.id,
|
||||
mockOrganizationUser.id,
|
||||
{
|
||||
key: mockEncryptedKey.encryptedString,
|
||||
defaultUserCollectionName: mockEncryptedCollectionName.encryptedString,
|
||||
} as OrganizationUserConfirmRequest,
|
||||
);
|
||||
|
||||
done();
|
||||
},
|
||||
error: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("bulkConfirmUsers", () => {
|
||||
const mockUserIdsWithKeys = [
|
||||
{ id: "user-1", key: "key-1" },
|
||||
{ id: "user-2", key: "key-2" },
|
||||
];
|
||||
|
||||
const mockBulkResponse = {
|
||||
data: [
|
||||
{ id: "user-1", error: null } as OrganizationUserBulkResponse,
|
||||
{ id: "user-2", error: null } as OrganizationUserBulkResponse,
|
||||
],
|
||||
} as ListResponse<OrganizationUserBulkResponse>;
|
||||
|
||||
beforeEach(() => {
|
||||
setupCommonMocks();
|
||||
organizationUserApiService.postOrganizationUserBulkConfirm.mockReturnValue(
|
||||
Promise.resolve(mockBulkResponse),
|
||||
);
|
||||
});
|
||||
|
||||
it("should bulk confirm users successfully", (done) => {
|
||||
service.bulkConfirmUsers(mockOrganization, mockUserIdsWithKeys).subscribe({
|
||||
next: (response) => {
|
||||
expect(i18nService.t).toHaveBeenCalledWith("myItems");
|
||||
|
||||
expect(encryptService.encryptString).toHaveBeenCalledWith(
|
||||
mockDefaultCollectionName,
|
||||
mockOrgKey,
|
||||
);
|
||||
|
||||
expect(organizationUserApiService.postOrganizationUserBulkConfirm).toHaveBeenCalledWith(
|
||||
mockOrganization.id,
|
||||
new OrganizationUserBulkConfirmRequest(
|
||||
mockUserIdsWithKeys,
|
||||
mockEncryptedCollectionName.encryptedString,
|
||||
),
|
||||
);
|
||||
|
||||
expect(response).toEqual(mockBulkResponse);
|
||||
|
||||
done();
|
||||
},
|
||||
error: done,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { combineLatest, filter, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserConfirmRequest,
|
||||
OrganizationUserBulkConfirmRequest,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserBulkResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
})
|
||||
export class OrganizationUserService {
|
||||
constructor(
|
||||
protected keyService: KeyService,
|
||||
private encryptService: EncryptService,
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private accountService: AccountService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
private orgKey$(organization: Organization) {
|
||||
return this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
filter((orgKeys) => !!orgKeys),
|
||||
map((organizationKeysById) => organizationKeysById[organization.id as OrganizationId]),
|
||||
);
|
||||
}
|
||||
|
||||
confirmUser(
|
||||
organization: Organization,
|
||||
user: OrganizationUserView,
|
||||
publicKey: Uint8Array,
|
||||
): Observable<void> {
|
||||
const encryptedCollectionName$ = this.getEncryptedDefaultCollectionName$(organization);
|
||||
|
||||
const encryptedKey$ = this.orgKey$(organization).pipe(
|
||||
switchMap((orgKey) => this.encryptService.encapsulateKeyUnsigned(orgKey, publicKey)),
|
||||
);
|
||||
|
||||
return combineLatest([encryptedKey$, encryptedCollectionName$]).pipe(
|
||||
switchMap(([key, collectionName]) => {
|
||||
const request: OrganizationUserConfirmRequest = {
|
||||
key: key.encryptedString,
|
||||
defaultUserCollectionName: collectionName.encryptedString,
|
||||
};
|
||||
|
||||
return this.organizationUserApiService.postOrganizationUserConfirm(
|
||||
organization.id,
|
||||
user.id,
|
||||
request,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
bulkConfirmUsers(
|
||||
organization: Organization,
|
||||
userIdsWithKeys: { id: string; key: string }[],
|
||||
): Observable<ListResponse<OrganizationUserBulkResponse>> {
|
||||
return this.getEncryptedDefaultCollectionName$(organization).pipe(
|
||||
switchMap((collectionName) => {
|
||||
const request = new OrganizationUserBulkConfirmRequest(
|
||||
userIdsWithKeys,
|
||||
collectionName.encryptedString,
|
||||
);
|
||||
|
||||
return this.organizationUserApiService.postOrganizationUserBulkConfirm(
|
||||
organization.id,
|
||||
request,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private getEncryptedDefaultCollectionName$(organization: Organization) {
|
||||
return this.orgKey$(organization).pipe(
|
||||
switchMap((orgKey) =>
|
||||
this.encryptService.encryptString(this.i18nService.t("myItems"), orgKey),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { deepLinkGuard } from "../../auth/guards/deep-link/deep-link.guard";
|
||||
|
||||
import { VaultModule } from "./collections/vault.module";
|
||||
import { isEnterpriseOrgGuard } from "./guards/is-enterprise-org.guard";
|
||||
import { organizationPermissionsGuard } from "./guards/org-permissions.guard";
|
||||
import { organizationRedirectGuard } from "./guards/org-redirect.guard";
|
||||
import { AdminConsoleIntegrationsComponent } from "./integrations/integrations.component";
|
||||
@@ -42,10 +41,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "integrations",
|
||||
canActivate: [
|
||||
isEnterpriseOrgGuard(false),
|
||||
organizationPermissionsGuard(canAccessIntegrations),
|
||||
],
|
||||
canActivate: [organizationPermissionsGuard(canAccessIntegrations)],
|
||||
component: AdminConsoleIntegrationsComponent,
|
||||
data: {
|
||||
titleId: "integrations",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ScrollingModule } from "@angular/cdk/scrolling";
|
||||
import { NgModule } from "@angular/core";
|
||||
|
||||
import { ScrollLayoutDirective } from "@bitwarden/components";
|
||||
|
||||
import { LooseComponentsModule } from "../../shared";
|
||||
|
||||
import { CoreOrganizationModule } from "./core";
|
||||
@@ -18,6 +20,7 @@ import { AccessSelectorModule } from "./shared/components/access-selector";
|
||||
OrganizationsRoutingModule,
|
||||
LooseComponentsModule,
|
||||
ScrollingModule,
|
||||
ScrollLayoutDirective,
|
||||
],
|
||||
declarations: [GroupsComponent, GroupAddEditComponent],
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Directive, Input, OnInit } from "@angular/core";
|
||||
import { UntypedFormControl, UntypedFormGroup } from "@angular/forms";
|
||||
import { Observable, of } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
export abstract class BasePolicy {
|
||||
abstract name: string;
|
||||
@@ -14,38 +14,56 @@ export abstract class BasePolicy {
|
||||
abstract type: PolicyType;
|
||||
abstract component: any;
|
||||
|
||||
display(organization: Organization) {
|
||||
return true;
|
||||
/**
|
||||
* If true, the description will be reused in the policy edit modal. Set this to false if you
|
||||
* have more complex requirements that you will implement in your template instead.
|
||||
**/
|
||||
showDescription: boolean = true;
|
||||
|
||||
display(organization: Organization, configService: ConfigService): Observable<boolean> {
|
||||
return of(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export abstract class BasePolicyComponent implements OnInit {
|
||||
@Input() policyResponse: PolicyResponse;
|
||||
@Input() policy: BasePolicy;
|
||||
@Input() policyResponse: PolicyResponse | undefined;
|
||||
@Input() policy: BasePolicy | undefined;
|
||||
|
||||
enabled = new UntypedFormControl(false);
|
||||
data: UntypedFormGroup = null;
|
||||
data: UntypedFormGroup | undefined;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.enabled.setValue(this.policyResponse.enabled);
|
||||
this.enabled.setValue(this.policyResponse?.enabled);
|
||||
|
||||
if (this.policyResponse.data != null) {
|
||||
if (this.policyResponse?.data != null) {
|
||||
this.loadData();
|
||||
}
|
||||
}
|
||||
|
||||
buildRequest() {
|
||||
const request = new PolicyRequest();
|
||||
request.enabled = this.enabled.value;
|
||||
request.type = this.policy.type;
|
||||
request.data = this.buildRequestData();
|
||||
if (!this.policy) {
|
||||
throw new Error("Policy was not found");
|
||||
}
|
||||
|
||||
const request: PolicyRequest = {
|
||||
type: this.policy.type,
|
||||
enabled: this.enabled.value,
|
||||
data: this.buildRequestData(),
|
||||
};
|
||||
|
||||
return Promise.resolve(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable optional validation before sumitting a respose for policy submission
|
||||
* */
|
||||
confirm(): Promise<boolean> | boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected loadData() {
|
||||
this.data.patchValue(this.policyResponse.data ?? {});
|
||||
this.data?.patchValue(this.policyResponse?.data ?? {});
|
||||
}
|
||||
|
||||
protected buildRequestData() {
|
||||
|
||||
@@ -3,7 +3,8 @@ export { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
export { DisableSendPolicy } from "./disable-send.component";
|
||||
export { MasterPasswordPolicy } from "./master-password.component";
|
||||
export { PasswordGeneratorPolicy } from "./password-generator.component";
|
||||
export { PersonalOwnershipPolicy } from "./personal-ownership.component";
|
||||
export { vNextOrganizationDataOwnershipPolicy } from "./vnext-organization-data-ownership.component";
|
||||
export { OrganizationDataOwnershipPolicy } from "./organization-data-ownership.component";
|
||||
export { RequireSsoPolicy } from "./require-sso.component";
|
||||
export { ResetPasswordPolicy } from "./reset-password.component";
|
||||
export { SendOptionsPolicy } from "./send-options.component";
|
||||
@@ -11,3 +12,4 @@ export { SingleOrgPolicy } from "./single-org.component";
|
||||
export { TwoFactorAuthenticationPolicy } from "./two-factor-authentication.component";
|
||||
export { PoliciesComponent } from "./policies.component";
|
||||
export { RemoveUnlockWithPinPolicy } from "./remove-unlock-with-pin.component";
|
||||
export { RestrictedItemTypesPolicy } from "./restricted-item-types.component";
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
|
||||
export class OrganizationDataOwnershipPolicy extends BasePolicy {
|
||||
name = "organizationDataOwnership";
|
||||
description = "personalOwnershipPolicyDesc";
|
||||
type = PolicyType.OrganizationDataOwnership;
|
||||
component = OrganizationDataOwnershipPolicyComponent;
|
||||
|
||||
display(organization: Organization, configService: ConfigService): Observable<boolean> {
|
||||
return configService
|
||||
.getFeatureFlag$(FeatureFlag.CreateDefaultLocation)
|
||||
.pipe(map((enabled) => !enabled));
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "policy-organization-data-ownership",
|
||||
templateUrl: "organization-data-ownership.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class OrganizationDataOwnershipPolicyComponent extends BasePolicyComponent {}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
|
||||
export class PersonalOwnershipPolicy extends BasePolicy {
|
||||
name = "personalOwnership";
|
||||
description = "personalOwnershipPolicyDesc";
|
||||
type = PolicyType.PersonalOwnership;
|
||||
component = PersonalOwnershipPolicyComponent;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "policy-personal-ownership",
|
||||
templateUrl: "personal-ownership.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class PersonalOwnershipPolicyComponent extends BasePolicyComponent {}
|
||||
@@ -1,38 +1,45 @@
|
||||
<app-header>
|
||||
@let organization = organization$ | async;
|
||||
<button
|
||||
bitBadge
|
||||
class="!tw-align-middle"
|
||||
(click)="changePlan(organization)"
|
||||
*ngIf="isBreadcrumbingEnabled$ | async"
|
||||
slot="title-suffix"
|
||||
type="button"
|
||||
variant="primary"
|
||||
>
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
@if (isBreadcrumbingEnabled$ | async) {
|
||||
<button
|
||||
bitBadge
|
||||
class="!tw-align-middle"
|
||||
(click)="changePlan(organization)"
|
||||
slot="title-suffix"
|
||||
type="button"
|
||||
variant="primary"
|
||||
>
|
||||
{{ "upgrade" | i18n }}
|
||||
</button>
|
||||
}
|
||||
</app-header>
|
||||
|
||||
<bit-container>
|
||||
<ng-container *ngIf="loading">
|
||||
@if (loading) {
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<bit-table *ngIf="!loading">
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let p of policies">
|
||||
<td bitCell *ngIf="p.display(organization)" ngPreserveWhitespaces>
|
||||
<button type="button" bitLink (click)="edit(p)">{{ p.name | i18n }}</button>
|
||||
<span bitBadge variant="success" *ngIf="policiesEnabledMap.get(p.type)">{{
|
||||
"on" | i18n
|
||||
}}</span>
|
||||
<small class="tw-text-muted tw-block">{{ p.description | i18n }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
}
|
||||
@if (!loading) {
|
||||
<bit-table>
|
||||
<ng-template body>
|
||||
@for (p of policies; track p.name) {
|
||||
@if (p.display(organization, configService) | async) {
|
||||
<tr bitRow>
|
||||
<td bitCell ngPreserveWhitespaces>
|
||||
<button type="button" bitLink (click)="edit(p)">{{ p.name | i18n }}</button>
|
||||
@if (policiesEnabledMap.get(p.type)) {
|
||||
<span bitBadge variant="success">{{ "on" | i18n }}</span>
|
||||
}
|
||||
<small class="tw-text-muted tw-block">{{ p.description | i18n }}</small>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
}
|
||||
</bit-container>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import {
|
||||
ChangePlanDialogResultType,
|
||||
@@ -51,6 +52,7 @@ export class PoliciesComponent implements OnInit {
|
||||
private policyListService: PolicyListService,
|
||||
private organizationBillingService: OrganizationBillingServiceAbstraction,
|
||||
private dialogService: DialogService,
|
||||
protected configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -68,25 +70,27 @@ export class PoliciesComponent implements OnInit {
|
||||
await this.load();
|
||||
|
||||
// Handle policies component launch from Event message
|
||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
||||
if (qParams.policyId != null) {
|
||||
const policyIdFromEvents: string = qParams.policyId;
|
||||
for (const orgPolicy of this.orgPolicies) {
|
||||
if (orgPolicy.id === policyIdFromEvents) {
|
||||
for (let i = 0; i < this.policies.length; i++) {
|
||||
if (this.policies[i].type === orgPolicy.type) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.edit(this.policies[i]);
|
||||
break;
|
||||
this.route.queryParams
|
||||
.pipe(first())
|
||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
||||
.subscribe(async (qParams) => {
|
||||
if (qParams.policyId != null) {
|
||||
const policyIdFromEvents: string = qParams.policyId;
|
||||
for (const orgPolicy of this.orgPolicies) {
|
||||
if (orgPolicy.id === policyIdFromEvents) {
|
||||
for (let i = 0; i < this.policies.length; i++) {
|
||||
if (this.policies[i].type === orgPolicy.type) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.edit(this.policies[i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,14 @@ import { LooseComponentsModule, SharedModule } from "../../../shared";
|
||||
|
||||
import { DisableSendPolicyComponent } from "./disable-send.component";
|
||||
import { MasterPasswordPolicyComponent } from "./master-password.component";
|
||||
import { OrganizationDataOwnershipPolicyComponent } from "./organization-data-ownership.component";
|
||||
import { PasswordGeneratorPolicyComponent } from "./password-generator.component";
|
||||
import { PersonalOwnershipPolicyComponent } from "./personal-ownership.component";
|
||||
import { PoliciesComponent } from "./policies.component";
|
||||
import { PolicyEditComponent } from "./policy-edit.component";
|
||||
import { RemoveUnlockWithPinPolicyComponent } from "./remove-unlock-with-pin.component";
|
||||
import { RequireSsoPolicyComponent } from "./require-sso.component";
|
||||
import { ResetPasswordPolicyComponent } from "./reset-password.component";
|
||||
import { RestrictedItemTypesPolicyComponent } from "./restricted-item-types.component";
|
||||
import { SendOptionsPolicyComponent } from "./send-options.component";
|
||||
import { SingleOrgPolicyComponent } from "./single-org.component";
|
||||
import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authentication.component";
|
||||
@@ -21,7 +22,7 @@ import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authenticat
|
||||
DisableSendPolicyComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
PasswordGeneratorPolicyComponent,
|
||||
PersonalOwnershipPolicyComponent,
|
||||
OrganizationDataOwnershipPolicyComponent,
|
||||
RequireSsoPolicyComponent,
|
||||
ResetPasswordPolicyComponent,
|
||||
SendOptionsPolicyComponent,
|
||||
@@ -30,12 +31,13 @@ import { TwoFactorAuthenticationPolicyComponent } from "./two-factor-authenticat
|
||||
PoliciesComponent,
|
||||
PolicyEditComponent,
|
||||
RemoveUnlockWithPinPolicyComponent,
|
||||
RestrictedItemTypesPolicyComponent,
|
||||
],
|
||||
exports: [
|
||||
DisableSendPolicyComponent,
|
||||
MasterPasswordPolicyComponent,
|
||||
PasswordGeneratorPolicyComponent,
|
||||
PersonalOwnershipPolicyComponent,
|
||||
OrganizationDataOwnershipPolicyComponent,
|
||||
RequireSsoPolicyComponent,
|
||||
ResetPasswordPolicyComponent,
|
||||
SendOptionsPolicyComponent,
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</div>
|
||||
<div [hidden]="loading">
|
||||
<p bitTypography="body1">{{ policy.description | i18n }}</p>
|
||||
@if (policy.showDescription) {
|
||||
<p bitTypography="body1">{{ policy.description | i18n }}</p>
|
||||
}
|
||||
<ng-template #policyForm></ng-template>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
@@ -128,13 +128,20 @@ export class PolicyEditComponent implements AfterViewInit {
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
if ((await this.policyComponent.confirm()) == false) {
|
||||
this.dialogRef.close();
|
||||
return;
|
||||
}
|
||||
|
||||
let request: PolicyRequest;
|
||||
|
||||
try {
|
||||
request = await this.policyComponent.buildRequest();
|
||||
} catch (e) {
|
||||
this.toastService.showToast({ variant: "error", title: null, message: e.message });
|
||||
return;
|
||||
}
|
||||
|
||||
await this.policyApiService.putPolicy(this.data.organizationId, this.data.policy.type, request);
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
|
||||
@@ -11,8 +13,8 @@ export class RequireSsoPolicy extends BasePolicy {
|
||||
type = PolicyType.RequireSso;
|
||||
component = RequireSsoPolicyComponent;
|
||||
|
||||
display(organization: Organization) {
|
||||
return organization.useSso;
|
||||
display(organization: Organization, configService: ConfigService) {
|
||||
return of(organization.useSso);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -10,6 +10,7 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
|
||||
@@ -19,8 +20,8 @@ export class ResetPasswordPolicy extends BasePolicy {
|
||||
type = PolicyType.ResetPassword;
|
||||
component = ResetPasswordPolicyComponent;
|
||||
|
||||
display(organization: Organization) {
|
||||
return organization.useResetPassword;
|
||||
display(organization: Organization, configService: ConfigService) {
|
||||
return of(organization.useResetPassword);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +53,10 @@ export class ResetPasswordPolicyComponent extends BasePolicyComponent implements
|
||||
throw new Error("No user found.");
|
||||
}
|
||||
|
||||
if (!this.policyResponse) {
|
||||
throw new Error("Policies not found");
|
||||
}
|
||||
|
||||
const organization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
<!-- To allow for multiple item types we can add a data formGroup, iterate over the
|
||||
cipher types as checkboxes/multi-select and use that as a means to track which types are restricted -->
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
|
||||
export class RestrictedItemTypesPolicy extends BasePolicy {
|
||||
name = "restrictedItemTypePolicy";
|
||||
description = "restrictedItemTypePolicyDesc";
|
||||
type = PolicyType.RestrictedItemTypes;
|
||||
component = RestrictedItemTypesPolicyComponent;
|
||||
|
||||
display(organization: Organization, configService: ConfigService): Observable<boolean> {
|
||||
return configService.getFeatureFlag$(FeatureFlag.RemoveCardItemTypePolicy);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "policy-restricted-item-types",
|
||||
templateUrl: "restricted-item-types.component.html",
|
||||
standalone: false,
|
||||
})
|
||||
export class RestrictedItemTypesPolicyComponent extends BasePolicyComponent {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ export class SingleOrgPolicyComponent extends BasePolicyComponent implements OnI
|
||||
async ngOnInit() {
|
||||
super.ngOnInit();
|
||||
|
||||
if (!this.policyResponse) {
|
||||
throw new Error("Policies not found");
|
||||
}
|
||||
if (!this.policyResponse.canToggleState) {
|
||||
this.enabled.disable();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<p>
|
||||
{{ "organizationDataOwnershipContent" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<bit-form-control>
|
||||
<input type="checkbox" bitCheckbox [formControl]="enabled" id="enabled" />
|
||||
<bit-label>{{ "turnOn" | i18n }}</bit-label>
|
||||
</bit-form-control>
|
||||
|
||||
<ng-template #dialog>
|
||||
<bit-simple-dialog background="alt">
|
||||
<span bitDialogTitle>{{ "organizationDataOwnershipWarningTitle" | i18n }}</span>
|
||||
<ng-container bitDialogContent>
|
||||
<div class="tw-text-left tw-overflow-hidden">
|
||||
{{ "organizationDataOwnershipWarningContentTop" | i18n }}
|
||||
<div class="tw-flex tw-flex-col tw-p-2">
|
||||
<ul class="tw-list-disc tw-pl-5 tw-space-y-2 tw-break-words tw-mb-0">
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning1" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning2" | i18n }}
|
||||
</li>
|
||||
<li>
|
||||
{{ "organizationDataOwnershipWarning3" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ "organizationDataOwnershipWarningContentBottom" | i18n }}
|
||||
<a
|
||||
bitLink
|
||||
href="https://bitwarden.com/resources/credential-lifecycle-management/"
|
||||
target="_blank"
|
||||
>
|
||||
{{ "organizationDataOwnershipContentAnchor" | i18n }}.
|
||||
</a>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container bitDialogFooter>
|
||||
<span class="tw-flex tw-gap-2">
|
||||
<button bitButton buttonType="primary" [bitDialogClose]="true" type="submit">
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" [bitDialogClose]="false" type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</span>
|
||||
</ng-container>
|
||||
</bit-simple-dialog>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Component, OnInit, TemplateRef, ViewChild } from "@angular/core";
|
||||
import { lastValueFrom, Observable } from "rxjs";
|
||||
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../shared";
|
||||
|
||||
import { BasePolicy, BasePolicyComponent } from "./base-policy.component";
|
||||
|
||||
export class vNextOrganizationDataOwnershipPolicy extends BasePolicy {
|
||||
name = "organizationDataOwnership";
|
||||
description = "organizationDataOwnershipDesc";
|
||||
type = PolicyType.OrganizationDataOwnership;
|
||||
component = vNextOrganizationDataOwnershipPolicyComponent;
|
||||
showDescription = false;
|
||||
|
||||
override display(organization: Organization, configService: ConfigService): Observable<boolean> {
|
||||
return configService.getFeatureFlag$(FeatureFlag.CreateDefaultLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "vnext-policy-organization-data-ownership",
|
||||
templateUrl: "vnext-organization-data-ownership.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class vNextOrganizationDataOwnershipPolicyComponent
|
||||
extends BasePolicyComponent
|
||||
implements OnInit
|
||||
{
|
||||
constructor(private dialogService: DialogService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@ViewChild("dialog", { static: true }) warningContent!: TemplateRef<unknown>;
|
||||
|
||||
override async confirm(): Promise<boolean> {
|
||||
if (this.policyResponse?.enabled && !this.enabled.value) {
|
||||
const dialogRef = this.dialogService.open(this.warningContent);
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
return Boolean(result);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,7 @@
|
||||
<bit-label>{{ "limitCollectionDeletionDesc" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="limitCollectionDeletion" />
|
||||
</bit-form-control>
|
||||
<bit-form-control *ngIf="limitItemDeletionFeatureFlagIsEnabled">
|
||||
<bit-form-control>
|
||||
<bit-label>{{ "limitItemDeletionDescription" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="limitItemDeletion" />
|
||||
</bit-form-control>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
firstValueFrom,
|
||||
from,
|
||||
lastValueFrom,
|
||||
map,
|
||||
of,
|
||||
Subject,
|
||||
switchMap,
|
||||
@@ -25,11 +26,10 @@ import { OrganizationUpdateRequest } from "@bitwarden/common/admin-console/model
|
||||
import { OrganizationResponse } from "@bitwarden/common/admin-console/models/response/organization.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -51,8 +51,6 @@ export class AccountComponent implements OnInit, OnDestroy {
|
||||
org: OrganizationResponse;
|
||||
taxFormPromise: Promise<unknown>;
|
||||
|
||||
limitItemDeletionFeatureFlagIsEnabled: boolean;
|
||||
|
||||
// FormGroup validators taken from server Organization domain object
|
||||
protected formGroup = this.formBuilder.group({
|
||||
orgName: this.formBuilder.control(
|
||||
@@ -95,17 +93,11 @@ export class AccountComponent implements OnInit, OnDestroy {
|
||||
private dialogService: DialogService,
|
||||
private formBuilder: FormBuilder,
|
||||
private toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.selfHosted = this.platformUtilsService.isSelfHost();
|
||||
|
||||
this.configService
|
||||
.getFeatureFlag$(FeatureFlag.LimitItemDeletion)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((isAble) => (this.limitItemDeletionFeatureFlagIsEnabled = isAble));
|
||||
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.route.params
|
||||
.pipe(
|
||||
@@ -189,7 +181,13 @@ export class AccountComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Backfill pub/priv key if necessary
|
||||
if (!this.org.hasPublicAndPrivateKeys) {
|
||||
const orgShareKey = await this.keyService.getOrgKey(this.organizationId);
|
||||
const orgShareKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => orgKeys[this.organizationId as OrganizationId] ?? null),
|
||||
),
|
||||
);
|
||||
const orgKeys = await this.keyService.makeKeyPair(orgShareKey);
|
||||
request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherType, toCipherTypeName } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
@@ -80,7 +80,6 @@ export enum DeleteOrganizationDialogResult {
|
||||
|
||||
@Component({
|
||||
selector: "app-delete-organization",
|
||||
standalone: true,
|
||||
imports: [SharedModule, UserVerificationModule],
|
||||
templateUrl: "delete-organization-dialog.component.html",
|
||||
})
|
||||
@@ -163,7 +162,7 @@ export class DeleteOrganizationDialogComponent implements OnInit, OnDestroy {
|
||||
organizationContentSummary.itemCountByType.push(
|
||||
new OrganizationContentSummaryItem(
|
||||
count,
|
||||
this.getOrganizationItemLocalizationKeysByType(CipherType[cipherType]),
|
||||
this.getOrganizationItemLocalizationKeysByType(toCipherTypeName(cipherType)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
<!-- 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 +48,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 +76,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: [],
|
||||
}),
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
CollectionResponse,
|
||||
CollectionView,
|
||||
CollectionService,
|
||||
Collection,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
getOrganizationById,
|
||||
@@ -38,7 +37,9 @@ 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
DialogConfig,
|
||||
@@ -87,8 +88,8 @@ enum ButtonType {
|
||||
}
|
||||
|
||||
export interface CollectionDialogParams {
|
||||
collectionId?: string;
|
||||
organizationId: string;
|
||||
collectionId?: CollectionId;
|
||||
organizationId: OrganizationId;
|
||||
initialTab?: CollectionDialogTabType;
|
||||
parentCollectionId?: string;
|
||||
showOrgSelector?: boolean;
|
||||
@@ -117,7 +118,6 @@ export enum CollectionDialogAction {
|
||||
|
||||
@Component({
|
||||
templateUrl: "collection-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule, AccessSelectorModule, SelectModule],
|
||||
})
|
||||
export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
@@ -137,12 +137,11 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
externalId: { value: "", disabled: true },
|
||||
parent: undefined as string | undefined,
|
||||
access: [[] as AccessItemValue[]],
|
||||
selectedOrg: "",
|
||||
selectedOrg: "" as OrganizationId,
|
||||
});
|
||||
protected PermissionMode = PermissionMode;
|
||||
protected showDeleteButton = false;
|
||||
protected showAddAccessWarning = false;
|
||||
protected collections: Collection[];
|
||||
protected buttonDisplayName: ButtonType = ButtonType.Save;
|
||||
private orgExceedingCollectionLimit!: Organization;
|
||||
|
||||
@@ -167,14 +166,12 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
|
||||
async ngOnInit() {
|
||||
// Opened from the individual vault
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
|
||||
if (this.params.showOrgSelector) {
|
||||
this.showOrgSelector = true;
|
||||
this.formGroup.controls.selectedOrg.valueChanges
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((id) => this.loadOrg(id));
|
||||
const userId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.organizations$ = this.organizationService.organizations$(userId).pipe(
|
||||
first(),
|
||||
map((orgs) =>
|
||||
@@ -196,9 +193,14 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
if (isBreadcrumbEventLogsEnabled) {
|
||||
this.collections = await this.collectionService.getAll();
|
||||
this.organizationSelected.setAsyncValidators(
|
||||
freeOrgCollectionLimitValidator(this.organizations$, this.collections, this.i18nService),
|
||||
freeOrgCollectionLimitValidator(
|
||||
this.organizations$,
|
||||
this.collectionService
|
||||
.encryptedCollections$(userId)
|
||||
.pipe(map((collections) => collections ?? [])),
|
||||
this.i18nService,
|
||||
),
|
||||
);
|
||||
this.formGroup.updateValueAndValidity();
|
||||
}
|
||||
@@ -213,7 +215,7 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}),
|
||||
filter(() => this.organizationSelected.errors?.cannotCreateCollections),
|
||||
switchMap((value) => this.findOrganizationById(value)),
|
||||
switchMap((organizationId) => this.organizations$.pipe(getById(organizationId))),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe((org) => {
|
||||
@@ -223,11 +225,6 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
async findOrganizationById(orgId: string): Promise<Organization | undefined> {
|
||||
const organizations = await firstValueFrom(this.organizations$);
|
||||
return organizations.find((org) => org.id === orgId);
|
||||
}
|
||||
|
||||
async loadOrg(orgId: string) {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const organization$ = this.organizationService
|
||||
@@ -243,9 +240,15 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
return this.groupService.getAll(orgId);
|
||||
}),
|
||||
);
|
||||
|
||||
const collections = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.collectionAdminService.collectionAdminViews$(orgId, userId)),
|
||||
);
|
||||
|
||||
combineLatest({
|
||||
organization: organization$,
|
||||
collections: this.collectionAdminService.getAll(orgId),
|
||||
collections,
|
||||
groups: groups$,
|
||||
users: this.organizationUserApiService.getAllMiniUserDetails(orgId),
|
||||
})
|
||||
@@ -414,7 +417,8 @@ export class CollectionDialogComponent implements OnInit, OnDestroy {
|
||||
collectionView.name = this.formGroup.controls.name.value;
|
||||
}
|
||||
|
||||
const savedCollection = await this.collectionAdminService.save(collectionView);
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
const savedCollection = await this.collectionAdminService.save(collectionView, userId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -592,5 +596,5 @@ export function openCollectionDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<CollectionDialogParams, DialogRef<CollectionDialogResult>>,
|
||||
) {
|
||||
return dialogService.open(CollectionDialogComponent, config);
|
||||
return dialogService.open<CollectionDialogResult>(CollectionDialogComponent, config);
|
||||
}
|
||||
|
||||
@@ -17,16 +17,40 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="tw-p-5">
|
||||
<h3 class="tw-text-main tw-text-lg tw-font-semibold">{{ name }}</h3>
|
||||
<a
|
||||
class="tw-block tw-mb-0 tw-font-bold hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-size-full after:tw-left-0 after:tw-top-0"
|
||||
[href]="linkURL"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
</a>
|
||||
<span *ngIf="showNewBadge()" bitBadge class="tw-mt-3" variant="secondary">
|
||||
{{ "new" | i18n }}
|
||||
</span>
|
||||
<h3 class="tw-text-main tw-text-lg tw-font-semibold">
|
||||
{{ name }}
|
||||
@if (showConnectedBadge()) {
|
||||
<span class="tw-ml-3">
|
||||
@if (isConnected) {
|
||||
<span bitBadge variant="success">{{ "on" | i18n }}</span>
|
||||
}
|
||||
@if (!isConnected) {
|
||||
<span bitBadge>{{ "off" | i18n }}</span>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</h3>
|
||||
<p class="tw-mb-0">{{ description }}</p>
|
||||
|
||||
@if (canSetupConnection) {
|
||||
<button type="button" class="tw-mt-3" bitButton (click)="setupConnection()">
|
||||
<span>{{ "connectIntegrationButtonDesc" | i18n: name }}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (linkURL) {
|
||||
<a
|
||||
class="tw-block tw-mb-0 tw-font-bold hover:tw-no-underline focus:tw-outline-none after:tw-content-[''] after:tw-block after:tw-absolute after:tw-size-full after:tw-left-0 after:tw-top-0"
|
||||
[href]="linkURL"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
</a>
|
||||
}
|
||||
@if (showNewBadge()) {
|
||||
<span bitBadge class="tw-mt-3" variant="secondary">
|
||||
{{ "new" | i18n }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
// FIXME: remove `src` and fix import
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SharedModule } from "@bitwarden/components/src/shared";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -16,6 +19,9 @@ import { IntegrationCardComponent } from "./integration-card.component";
|
||||
describe("IntegrationCardComponent", () => {
|
||||
let component: IntegrationCardComponent;
|
||||
let fixture: ComponentFixture<IntegrationCardComponent>;
|
||||
const mockI18nService = mock<I18nService>();
|
||||
const activatedRoute = mock<ActivatedRoute>();
|
||||
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
||||
|
||||
const systemTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||
const usersPreferenceTheme$ = new BehaviorSubject<ThemeType>(ThemeType.Light);
|
||||
@@ -23,26 +29,22 @@ describe("IntegrationCardComponent", () => {
|
||||
beforeEach(async () => {
|
||||
// reset system theme
|
||||
systemTheme$.next(ThemeType.Light);
|
||||
activatedRoute.snapshot = {
|
||||
paramMap: {
|
||||
get: jest.fn().mockReturnValue("test-organization-id"),
|
||||
},
|
||||
} as any;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IntegrationCardComponent, SharedModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useValue: { selectedTheme$: usersPreferenceTheme$ },
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: systemTheme$,
|
||||
},
|
||||
{
|
||||
provide: I18nPipe,
|
||||
useValue: mock<I18nPipe>(),
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
{ provide: ThemeStateService, useValue: { selectedTheme$: usersPreferenceTheme$ } },
|
||||
{ provide: SYSTEM_THEME_OBSERVABLE, useValue: systemTheme$ },
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||
{ provide: OrganizationIntegrationApiService, useValue: mockOrgIntegrationApiService },
|
||||
{ provide: ToastService, useValue: mock<ToastService>() },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
@@ -55,6 +57,7 @@ describe("IntegrationCardComponent", () => {
|
||||
component.image = "test-image.png";
|
||||
component.linkURL = "https://example.com/";
|
||||
|
||||
mockI18nService.t.mockImplementation((key) => key);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -67,7 +70,7 @@ describe("IntegrationCardComponent", () => {
|
||||
it("renders card body", () => {
|
||||
const name = fixture.nativeElement.querySelector("h3");
|
||||
|
||||
expect(name.textContent).toBe("Integration Name");
|
||||
expect(name.textContent).toContain("Integration Name");
|
||||
});
|
||||
|
||||
it("assigns external rel attribute", () => {
|
||||
@@ -182,4 +185,28 @@ describe("IntegrationCardComponent", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("connected badge", () => {
|
||||
it("shows connected badge when isConnected is true", () => {
|
||||
component.isConnected = true;
|
||||
|
||||
expect(component.showConnectedBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show connected badge when isConnected is false", () => {
|
||||
component.isConnected = false;
|
||||
fixture.detectChanges();
|
||||
const name = fixture.nativeElement.querySelector("h3 > span > span > span");
|
||||
|
||||
expect(name.textContent).toContain("off");
|
||||
// when isConnected is true/false, the badge should be shown as on/off
|
||||
// when isConnected is undefined, the badge should not be shown
|
||||
expect(component.showConnectedBadge()).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show connected badge when isConnected is undefined", () => {
|
||||
component.isConnected = undefined;
|
||||
expect(component.showConnectedBadge()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,18 +9,30 @@ import {
|
||||
OnDestroy,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
import { Observable, Subject, combineLatest, takeUntil } from "rxjs";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { Observable, Subject, combineLatest, lastValueFrom, takeUntil } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import {
|
||||
OrganizationIntegrationType,
|
||||
OrganizationIntegrationRequest,
|
||||
OrganizationIntegrationResponse,
|
||||
OrganizationIntegrationApiService,
|
||||
} from "@bitwarden/bit-common/dirt/integrations/index";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { SharedModule } from "../../../../../../shared/shared.module";
|
||||
import { openHecConnectDialog } from "../integration-dialog/index";
|
||||
import { Integration } from "../models";
|
||||
|
||||
@Component({
|
||||
selector: "app-integration-card",
|
||||
templateUrl: "./integration-card.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
@@ -31,6 +43,7 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
@Input() image: string;
|
||||
@Input() imageDarkMode?: string;
|
||||
@Input() linkURL: string;
|
||||
@Input() integrationSettings: Integration;
|
||||
|
||||
/** Adds relevant `rel` attribute to external links */
|
||||
@Input() externalURL?: boolean;
|
||||
@@ -42,11 +55,19 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
* @example "2024-12-31"
|
||||
*/
|
||||
@Input() newBadgeExpiration?: string;
|
||||
@Input() description?: string;
|
||||
@Input() isConnected?: boolean;
|
||||
@Input() canSetupConnection?: boolean;
|
||||
|
||||
constructor(
|
||||
private themeStateService: ThemeStateService,
|
||||
@Inject(SYSTEM_THEME_OBSERVABLE)
|
||||
private systemTheme$: Observable<ThemeType>,
|
||||
private dialogService: DialogService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
private apiService: OrganizationIntegrationApiService,
|
||||
private toastService: ToastService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@@ -94,4 +115,63 @@ export class IntegrationCardComponent implements AfterViewInit, OnDestroy {
|
||||
|
||||
return expirationDate > new Date();
|
||||
}
|
||||
|
||||
showConnectedBadge(): boolean {
|
||||
return this.isConnected !== undefined;
|
||||
}
|
||||
|
||||
async setupConnection() {
|
||||
// invoke the dialog to connect the integration
|
||||
const dialog = openHecConnectDialog(this.dialogService, {
|
||||
data: {
|
||||
settings: this.integrationSettings,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await lastValueFrom(dialog.closed);
|
||||
|
||||
// the dialog was cancelled
|
||||
if (!result || !result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
// save the integration
|
||||
try {
|
||||
const dbResponse = await this.saveHecIntegration(result.configuration);
|
||||
this.isConnected = !!dbResponse.id;
|
||||
} catch {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
title: null,
|
||||
message: this.i18nService.t("failedToSaveIntegration"),
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async saveHecIntegration(configuration: string): Promise<OrganizationIntegrationResponse> {
|
||||
const organizationId = this.activatedRoute.snapshot.paramMap.get(
|
||||
"organizationId",
|
||||
) as OrganizationId;
|
||||
|
||||
const request = new OrganizationIntegrationRequest(
|
||||
OrganizationIntegrationType.Hec,
|
||||
configuration,
|
||||
);
|
||||
|
||||
const integrations = await this.apiService.getOrganizationIntegrations(organizationId);
|
||||
const existingIntegration = integrations.find(
|
||||
(i) => i.type === OrganizationIntegrationType.Hec,
|
||||
);
|
||||
|
||||
if (existingIntegration) {
|
||||
return await this.apiService.updateOrganizationIntegration(
|
||||
organizationId,
|
||||
existingIntegration.id,
|
||||
request,
|
||||
);
|
||||
} else {
|
||||
return await this.apiService.createOrganizationIntegration(organizationId, request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../../core/tests";
|
||||
|
||||
import { IntegrationCardComponent } from "./integration-card.component";
|
||||
|
||||
class MockThemeService implements Partial<ThemeStateService> {}
|
||||
|
||||
export default {
|
||||
title: "Web/Integration Layout/Integration Card",
|
||||
component: IntegrationCardComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
}),
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useClass: MockThemeService,
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: of(ThemeTypes.Light),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
args: {
|
||||
integrations: [],
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<IntegrationCardComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<app-integration-card
|
||||
[name]="name"
|
||||
[image]="image"
|
||||
[linkURL]="linkURL"
|
||||
></app-integration-card>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
name: "Bitwarden",
|
||||
image: "/integrations/bitwarden-vertical-blue.svg",
|
||||
linkURL: "https://bitwarden.com",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||
<bit-dialog dialogSize="large" [loading]="loading">
|
||||
<span bitDialogTitle>
|
||||
{{ "connectIntegrationButtonDesc" | i18n: connectInfo.settings.name }}
|
||||
</span>
|
||||
<div bitDialogContent class="tw-flex tw-flex-col tw-gap-4">
|
||||
@if (loading) {
|
||||
<ng-container #spinner>
|
||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" aria-hidden="true"></i>
|
||||
</ng-container>
|
||||
}
|
||||
@if (!loading) {
|
||||
<ng-container>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "url" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="url" />
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "bearerToken" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="bearerToken" />
|
||||
</bit-form-field>
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "index" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="index" />
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
<ng-container bitDialogFooter>
|
||||
<button type="submit" bitButton bitFormButton buttonType="primary" [disabled]="loading">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
<button type="button" bitButton bitDialogClose buttonType="secondary" [disabled]="loading">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
</form>
|
||||
@@ -0,0 +1,176 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { FormBuilder, ReactiveFormsModule } from "@angular/forms";
|
||||
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { Integration } from "../../models";
|
||||
|
||||
import {
|
||||
ConnectHecDialogComponent,
|
||||
HecConnectDialogParams,
|
||||
HecConnectDialogResult,
|
||||
openHecConnectDialog,
|
||||
} from "./connect-dialog-hec.component";
|
||||
|
||||
beforeAll(() => {
|
||||
// Mock element.animate for jsdom
|
||||
// the animate function is not available in jsdom, so we provide a mock implementation
|
||||
// This is necessary for tests that rely on animations
|
||||
// This mock does not perform any actual animations, it just provides a structure that allows tests
|
||||
// to run without throwing errors related to missing animate function
|
||||
if (!HTMLElement.prototype.animate) {
|
||||
HTMLElement.prototype.animate = function () {
|
||||
return {
|
||||
play: () => {},
|
||||
pause: () => {},
|
||||
finish: () => {},
|
||||
cancel: () => {},
|
||||
reverse: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
onfinish: null,
|
||||
oncancel: null,
|
||||
startTime: 0,
|
||||
currentTime: 0,
|
||||
playbackRate: 1,
|
||||
playState: "idle",
|
||||
replaceState: "active",
|
||||
effect: null,
|
||||
finished: Promise.resolve(),
|
||||
id: "",
|
||||
remove: () => {},
|
||||
timeline: null,
|
||||
ready: Promise.resolve(),
|
||||
} as unknown as Animation;
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
describe("ConnectDialogHecComponent", () => {
|
||||
let component: ConnectHecDialogComponent;
|
||||
let fixture: ComponentFixture<ConnectHecDialogComponent>;
|
||||
let dialogRefMock = mock<DialogRef<HecConnectDialogResult>>();
|
||||
const mockI18nService = mock<I18nService>();
|
||||
|
||||
const integrationMock: Integration = {
|
||||
name: "Test Integration",
|
||||
image: "test-image.png",
|
||||
linkURL: "https://example.com",
|
||||
imageDarkMode: "test-image-dark.png",
|
||||
newBadgeExpiration: "2024-12-31",
|
||||
description: "Test Description",
|
||||
isConnected: false,
|
||||
canSetupConnection: true,
|
||||
type: IntegrationType.EVENT,
|
||||
} as Integration;
|
||||
const connectInfo: HecConnectDialogParams = { settings: integrationMock };
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRefMock = mock<DialogRef<HecConnectDialogResult>>();
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReactiveFormsModule, SharedModule, BrowserAnimationsModule],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
{ provide: DIALOG_DATA, useValue: connectInfo },
|
||||
{ provide: DialogRef, useValue: dialogRefMock },
|
||||
{ provide: I18nPipe, useValue: mock<I18nPipe>() },
|
||||
{ provide: I18nService, useValue: mockI18nService },
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConnectHecDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
mockI18nService.t.mockImplementation((key) => key);
|
||||
});
|
||||
|
||||
it("should create the component", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should initialize form with empty values", () => {
|
||||
expect(component.formGroup.value).toEqual({
|
||||
url: "",
|
||||
bearerToken: "",
|
||||
index: "",
|
||||
service: "Test Integration",
|
||||
});
|
||||
});
|
||||
|
||||
it("should have required validators for all fields", () => {
|
||||
component.formGroup.setValue({ url: "", bearerToken: "", index: "", service: "" });
|
||||
expect(component.formGroup.valid).toBeFalsy();
|
||||
|
||||
component.formGroup.setValue({
|
||||
url: "https://test.com",
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should invalidate url if not matching pattern", () => {
|
||||
component.formGroup.setValue({
|
||||
url: "ftp://test.com",
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeFalsy();
|
||||
|
||||
component.formGroup.setValue({
|
||||
url: "https://test.com",
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
});
|
||||
expect(component.formGroup.valid).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should call dialogRef.close with correct result on submit", async () => {
|
||||
component.formGroup.setValue({
|
||||
url: "https://test.com",
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
});
|
||||
|
||||
await component.submit();
|
||||
|
||||
expect(dialogRefMock.close).toHaveBeenCalledWith({
|
||||
integrationSettings: integrationMock,
|
||||
configuration: JSON.stringify({
|
||||
url: "https://test.com",
|
||||
bearerToken: "token",
|
||||
index: "1",
|
||||
service: "Test Service",
|
||||
}),
|
||||
success: true,
|
||||
error: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("openCrowdstrikeConnectDialog", () => {
|
||||
it("should call dialogService.open with correct params", () => {
|
||||
const dialogServiceMock = mock<DialogService>();
|
||||
const config: DialogConfig<HecConnectDialogParams, DialogRef<HecConnectDialogResult>> = {
|
||||
data: { settings: { name: "Test" } as Integration },
|
||||
} as any;
|
||||
|
||||
openHecConnectDialog(dialogServiceMock, config);
|
||||
|
||||
expect(dialogServiceMock.open).toHaveBeenCalledWith(ConnectHecDialogComponent, config);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { FormBuilder, Validators } from "@angular/forms";
|
||||
|
||||
import { DIALOG_DATA, DialogConfig, DialogRef, DialogService } from "@bitwarden/components";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { Integration } from "../../models";
|
||||
|
||||
export type HecConnectDialogParams = {
|
||||
settings: Integration;
|
||||
};
|
||||
|
||||
export interface HecConnectDialogResult {
|
||||
integrationSettings: Integration;
|
||||
configuration: string;
|
||||
success: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "./connect-dialog-hec.component.html",
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class ConnectHecDialogComponent implements OnInit {
|
||||
loading = false;
|
||||
formGroup = this.formBuilder.group({
|
||||
url: ["", [Validators.required, Validators.pattern("https?://.+")]],
|
||||
bearerToken: ["", Validators.required],
|
||||
index: ["", Validators.required],
|
||||
service: ["", Validators.required],
|
||||
});
|
||||
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected connectInfo: HecConnectDialogParams,
|
||||
protected formBuilder: FormBuilder,
|
||||
private dialogRef: DialogRef<HecConnectDialogResult>,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
const settings = this.getSettingsAsJson(this.connectInfo.settings.configuration ?? "");
|
||||
|
||||
if (settings) {
|
||||
this.formGroup.patchValue({
|
||||
url: settings?.url || "",
|
||||
bearerToken: settings?.bearerToken || "",
|
||||
index: settings?.index || "",
|
||||
service: this.connectInfo.settings.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSettingsAsJson(configuration: string) {
|
||||
try {
|
||||
return JSON.parse(configuration);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
submit = async (): Promise<void> => {
|
||||
const formJson = this.formGroup.getRawValue();
|
||||
|
||||
const result: HecConnectDialogResult = {
|
||||
integrationSettings: this.connectInfo.settings,
|
||||
configuration: JSON.stringify(formJson),
|
||||
success: true,
|
||||
error: null,
|
||||
};
|
||||
|
||||
this.dialogRef.close(result);
|
||||
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
export function openHecConnectDialog(
|
||||
dialogService: DialogService,
|
||||
config: DialogConfig<HecConnectDialogParams, DialogRef<HecConnectDialogResult>>,
|
||||
) {
|
||||
return dialogService.open<HecConnectDialogResult>(ConnectHecDialogComponent, config);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./connect-dialog/connect-dialog-hec.component";
|
||||
@@ -13,6 +13,10 @@
|
||||
[imageDarkMode]="integration.imageDarkMode"
|
||||
[externalURL]="integration.type === IntegrationType.SDK"
|
||||
[newBadgeExpiration]="integration.newBadgeExpiration"
|
||||
[description]="integration.description | i18n"
|
||||
[isConnected]="integration.isConnected"
|
||||
[canSetupConnection]="integration.canSetupConnection"
|
||||
[integrationSettings]="integration"
|
||||
></app-integration-card>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { OrganizationIntegrationApiService } from "@bitwarden/bit-common/dirt/integrations/services";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
// eslint-disable-next-line import/order
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
// FIXME: remove `src` and fix import
|
||||
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { SharedModule } from "@bitwarden/components/src/shared";
|
||||
import { I18nPipe } from "@bitwarden/ui-common";
|
||||
@@ -21,6 +27,8 @@ import { IntegrationGridComponent } from "./integration-grid.component";
|
||||
describe("IntegrationGridComponent", () => {
|
||||
let component: IntegrationGridComponent;
|
||||
let fixture: ComponentFixture<IntegrationGridComponent>;
|
||||
const mockActivatedRoute = mock<ActivatedRoute>();
|
||||
const mockOrgIntegrationApiService = mock<OrganizationIntegrationApiService>();
|
||||
const integrations: Integration[] = [
|
||||
{
|
||||
name: "Integration 1",
|
||||
@@ -37,6 +45,12 @@ describe("IntegrationGridComponent", () => {
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockActivatedRoute.snapshot = {
|
||||
paramMap: {
|
||||
get: jest.fn().mockReturnValue("test-organization-id"),
|
||||
},
|
||||
} as any;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [IntegrationGridComponent, IntegrationCardComponent, SharedModule],
|
||||
providers: [
|
||||
@@ -56,6 +70,18 @@ describe("IntegrationGridComponent", () => {
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>({ t: (key, p1) => key + " " + p1 }),
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: mockActivatedRoute,
|
||||
},
|
||||
{
|
||||
provide: OrganizationIntegrationApiService,
|
||||
useValue: mockOrgIntegrationApiService,
|
||||
},
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: mock<ToastService>(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import { Integration } from "../models";
|
||||
@Component({
|
||||
selector: "app-integration-grid",
|
||||
templateUrl: "./integration-grid.component.html",
|
||||
standalone: true,
|
||||
imports: [IntegrationCardComponent, SharedModule],
|
||||
})
|
||||
export class IntegrationGridComponent {
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import { importProvidersFrom } from "@angular/core";
|
||||
import { Meta, StoryObj, applicationConfig, moduleMetadata } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { SYSTEM_THEME_OBSERVABLE } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { IntegrationType } from "@bitwarden/common/enums";
|
||||
import { ThemeTypes } from "@bitwarden/common/platform/enums";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
import { PreloadedEnglishI18nModule } from "../../../../../../core/tests";
|
||||
import { IntegrationCardComponent } from "../integration-card/integration-card.component";
|
||||
import { IntegrationGridComponent } from "../integration-grid/integration-grid.component";
|
||||
|
||||
class MockThemeService implements Partial<ThemeStateService> {}
|
||||
|
||||
export default {
|
||||
title: "Web/Integration Layout/Integration Grid",
|
||||
component: IntegrationGridComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [importProvidersFrom(PreloadedEnglishI18nModule)],
|
||||
}),
|
||||
moduleMetadata({
|
||||
imports: [IntegrationCardComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ThemeStateService,
|
||||
useClass: MockThemeService,
|
||||
},
|
||||
{
|
||||
provide: SYSTEM_THEME_OBSERVABLE,
|
||||
useValue: of(ThemeTypes.Dark),
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<IntegrationGridComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: /*html*/ `
|
||||
<app-integration-grid [integrations]="integrations"></app-integration-grid>
|
||||
`,
|
||||
}),
|
||||
args: {
|
||||
integrations: [
|
||||
{
|
||||
name: "Card 1",
|
||||
linkURL: "https://bitwarden.com",
|
||||
image: "/integrations/bitwarden-vertical-blue.svg",
|
||||
type: IntegrationType.SSO,
|
||||
},
|
||||
{
|
||||
name: "Card 2",
|
||||
linkURL: "https://bitwarden.com",
|
||||
image: "/integrations/bitwarden-vertical-blue.svg",
|
||||
type: IntegrationType.SDK,
|
||||
},
|
||||
{
|
||||
name: "Card 3",
|
||||
linkURL: "https://bitwarden.com",
|
||||
image: "/integrations/bitwarden-vertical-blue.svg",
|
||||
type: IntegrationType.SCIM,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import { Integration } from "../../../shared/components/integrations/models";
|
||||
|
||||
@Pipe({
|
||||
name: "filterIntegrations",
|
||||
standalone: true,
|
||||
})
|
||||
export class FilterIntegrationsPipe implements PipeTransform {
|
||||
transform(integrations: Integration[], type: IntegrationType): Integration[] {
|
||||
|
||||
@@ -17,4 +17,8 @@ export type Integration = {
|
||||
* @example "2024-12-31"
|
||||
*/
|
||||
newBadgeExpiration?: string;
|
||||
description?: string;
|
||||
isConnected?: boolean;
|
||||
canSetupConnection?: boolean;
|
||||
configuration?: string;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
|
||||
it("returns null if organization is not found", async () => {
|
||||
const orgs: Organization[] = [];
|
||||
const validator = freeOrgCollectionLimitValidator(of(orgs), [], i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of(orgs), of([]), i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result: Observable<ValidationErrors> = validator(control) as Observable<ValidationErrors>;
|
||||
@@ -28,7 +28,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
});
|
||||
|
||||
it("returns null if control is not an instance of FormControl", async () => {
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), of([]), i18nService);
|
||||
const control = {} as AbstractControl;
|
||||
|
||||
const result: Observable<ValidationErrors | null> = validator(
|
||||
@@ -40,7 +40,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
});
|
||||
|
||||
it("returns null if control is not provided", async () => {
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), [], i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of([]), of([]), i18nService);
|
||||
|
||||
const result: Observable<ValidationErrors | null> = validator(
|
||||
undefined as any,
|
||||
@@ -53,7 +53,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
it("returns null if organization has not reached collection limit (Observable)", async () => {
|
||||
const org = { id: "org-id", maxCollections: 2 } as Organization;
|
||||
const collections = [{ organizationId: "org-id" } as Collection];
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), of(collections), i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result$ = validator(control) as Observable<ValidationErrors | null>;
|
||||
@@ -65,7 +65,7 @@ describe("freeOrgCollectionLimitValidator", () => {
|
||||
it("returns error if organization has reached collection limit (Observable)", async () => {
|
||||
const org = { id: "org-id", maxCollections: 1 } as Organization;
|
||||
const collections = [{ organizationId: "org-id" } as Collection];
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), collections, i18nService);
|
||||
const validator = freeOrgCollectionLimitValidator(of([org]), of(collections), i18nService);
|
||||
const control = new FormControl("org-id");
|
||||
|
||||
const result$ = validator(control) as Observable<ValidationErrors | null>;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { AbstractControl, AsyncValidatorFn, FormControl, ValidationErrors } from "@angular/forms";
|
||||
import { map, Observable, of } from "rxjs";
|
||||
import { combineLatest, map, Observable, of } from "rxjs";
|
||||
|
||||
import { Collection } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { getById } from "@bitwarden/common/platform/misc";
|
||||
|
||||
export function freeOrgCollectionLimitValidator(
|
||||
orgs: Observable<Organization[]>,
|
||||
collections: Collection[],
|
||||
organizations$: Observable<Organization[]>,
|
||||
collections$: Observable<Collection[]>,
|
||||
i18nService: I18nService,
|
||||
): AsyncValidatorFn {
|
||||
return (control: AbstractControl): Observable<ValidationErrors | null> => {
|
||||
@@ -21,15 +22,16 @@ export function freeOrgCollectionLimitValidator(
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return orgs.pipe(
|
||||
map((organizations) => organizations.find((org) => org.id === orgId)),
|
||||
map((org) => {
|
||||
if (!org) {
|
||||
return combineLatest([organizations$.pipe(getById(orgId)), collections$]).pipe(
|
||||
map(([organization, collections]) => {
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const orgCollections = collections.filter((c) => c.organizationId === org.id);
|
||||
const hasReachedLimit = org.maxCollections === orgCollections.length;
|
||||
const orgCollections = collections.filter(
|
||||
(collection: Collection) => collection.organizationId === organization.id,
|
||||
);
|
||||
const hasReachedLimit = organization.maxCollections === orgCollections.length;
|
||||
|
||||
if (hasReachedLimit) {
|
||||
return {
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
import { Component, inject } from "@angular/core";
|
||||
import { Params } from "@angular/router";
|
||||
|
||||
import { BitwardenLogo } from "@bitwarden/auth/angular";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { OrganizationSponsorshipResponse } from "@bitwarden/common/admin-console/models/response/organization-sponsorship.response";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { Icons, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||
|
||||
@@ -22,7 +21,7 @@ import { BaseAcceptComponent } from "../../../common/base.accept.component";
|
||||
standalone: false,
|
||||
})
|
||||
export class AcceptFamilySponsorshipComponent extends BaseAcceptComponent {
|
||||
protected logo = BitwardenLogo;
|
||||
protected logo = Icons.BitwardenLogo;
|
||||
failedShortMessage = "inviteAcceptFailedShort";
|
||||
failedMessage = "inviteAcceptFailed";
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
|
||||
@Component({
|
||||
templateUrl: "families-for-enterprise-setup.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule, OrganizationPlansComponent],
|
||||
})
|
||||
export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy {
|
||||
|
||||
@@ -13,7 +13,6 @@ import { SharedModule } from "../../shared";
|
||||
|
||||
@Component({
|
||||
templateUrl: "create-organization.component.html",
|
||||
standalone: true,
|
||||
imports: [SharedModule, OrganizationPlansComponent, HeaderModule],
|
||||
})
|
||||
export class CreateOrganizationComponent {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { DOCUMENT } from "@angular/common";
|
||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component, DestroyRef, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import { Subject, filter, firstValueFrom, map, takeUntil, timeout } from "rxjs";
|
||||
import { Subject, filter, firstValueFrom, map, timeout } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { DocumentLangSetter } from "@bitwarden/angular/platform/i18n";
|
||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { InternalOrganizationServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
@@ -26,6 +25,7 @@ import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SearchService } from "@bitwarden/common/vault/abstractions/search.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -34,13 +34,15 @@ import {
|
||||
DisableSendPolicy,
|
||||
MasterPasswordPolicy,
|
||||
PasswordGeneratorPolicy,
|
||||
PersonalOwnershipPolicy,
|
||||
OrganizationDataOwnershipPolicy,
|
||||
vNextOrganizationDataOwnershipPolicy,
|
||||
RequireSsoPolicy,
|
||||
ResetPasswordPolicy,
|
||||
SendOptionsPolicy,
|
||||
SingleOrgPolicy,
|
||||
TwoFactorAuthenticationPolicy,
|
||||
RemoveUnlockWithPinPolicy,
|
||||
RestrictedItemTypesPolicy,
|
||||
} from "./admin-console/organizations/policies";
|
||||
|
||||
const BroadcasterSubscriptionId = "AppComponent";
|
||||
@@ -60,7 +62,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
loading = false;
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
private broadcasterService: BroadcasterService,
|
||||
private folderService: InternalFolderService,
|
||||
private cipherService: CipherService,
|
||||
@@ -86,15 +87,16 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
private accountService: AccountService,
|
||||
private processReloadService: ProcessReloadServiceAbstraction,
|
||||
private deviceTrustToastService: DeviceTrustToastService,
|
||||
private readonly destoryRef: DestroyRef,
|
||||
private readonly documentLangSetter: DocumentLangSetter,
|
||||
) {
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
|
||||
const langSubscription = this.documentLangSetter.start();
|
||||
this.destoryRef.onDestroy(() => langSubscription.unsubscribe());
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.i18nService.locale$.pipe(takeUntil(this.destroy$)).subscribe((locale) => {
|
||||
this.document.documentElement.lang = locale;
|
||||
});
|
||||
|
||||
this.ngZone.runOutsideAngular(() => {
|
||||
window.onmousemove = () => this.recordActivity();
|
||||
window.onmousedown = () => this.recordActivity();
|
||||
@@ -243,9 +245,11 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
new PasswordGeneratorPolicy(),
|
||||
new SingleOrgPolicy(),
|
||||
new RequireSsoPolicy(),
|
||||
new PersonalOwnershipPolicy(),
|
||||
new OrganizationDataOwnershipPolicy(),
|
||||
new vNextOrganizationDataOwnershipPolicy(),
|
||||
new DisableSendPolicy(),
|
||||
new SendOptionsPolicy(),
|
||||
new RestrictedItemTypesPolicy(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -285,7 +289,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
this.keyService.clearKeys(userId),
|
||||
this.cipherService.clear(userId),
|
||||
this.folderService.clear(userId),
|
||||
this.collectionService.clear(userId),
|
||||
this.biometricStateService.logout(userId),
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { ChangePasswordService, DefaultChangePasswordService } from "@bitwarden/auth/angular";
|
||||
import {
|
||||
ChangePasswordService,
|
||||
DefaultChangePasswordService,
|
||||
} from "@bitwarden/angular/auth/password-management/change-password";
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
import { UserKeyRotationService } from "@bitwarden/web-vault/app/key-management/key-rotation/user-key-rotation.service";
|
||||
|
||||
export class WebChangePasswordService
|
||||
@@ -14,6 +18,7 @@ export class WebChangePasswordService
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
private userKeyRotationService: UserKeyRotationService,
|
||||
private routerService: RouterService,
|
||||
) {
|
||||
super(keyService, masterPasswordApiService, masterPasswordService);
|
||||
}
|
||||
@@ -31,4 +36,8 @@ export class WebChangePasswordService
|
||||
newPasswordHint,
|
||||
);
|
||||
}
|
||||
|
||||
async clearDeeplinkState() {
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export * from "./change-password";
|
||||
export * from "./login";
|
||||
export * from "./login-decryption-options";
|
||||
export * from "./webauthn-login";
|
||||
export * from "./set-password-jit";
|
||||
export * from "./password-management";
|
||||
export * from "./registration";
|
||||
export * from "./two-factor-auth";
|
||||
export * from "./link-sso.service";
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
LoginDecryptionOptionsService,
|
||||
DefaultLoginDecryptionOptionsService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
|
||||
import { RouterService } from "../../../../core/router.service";
|
||||
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||
|
||||
export class WebLoginDecryptionOptionsService
|
||||
extends DefaultLoginDecryptionOptionsService
|
||||
@@ -16,7 +16,7 @@ export class WebLoginDecryptionOptionsService
|
||||
constructor(
|
||||
protected messagingService: MessagingService,
|
||||
private routerService: RouterService,
|
||||
private acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
private organizationInviteService: OrganizationInviteService,
|
||||
) {
|
||||
super(messagingService);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export class WebLoginDecryptionOptionsService
|
||||
// accepted while being enrolled in admin recovery. So we need to clear
|
||||
// the redirect and stored org invite.
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.acceptOrganizationInviteService.clearOrganizationInvitation();
|
||||
await this.organizationInviteService.clearOrganizationInvitation();
|
||||
} catch (error) {
|
||||
throw new Error(error);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
@@ -10,7 +9,10 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { ResetPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/reset-password-policy-options";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
@@ -22,7 +24,6 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legac
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { RouterService } from "../../../../../../../../apps/web/src/app/core";
|
||||
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||
|
||||
import { WebLoginComponentService } from "./web-login-component.service";
|
||||
|
||||
@@ -32,7 +33,7 @@ jest.mock("../../../../../utils/flags", () => ({
|
||||
|
||||
describe("WebLoginComponentService", () => {
|
||||
let service: WebLoginComponentService;
|
||||
let acceptOrganizationInviteService: MockProxy<AcceptOrganizationInviteService>;
|
||||
let organizationInviteService: MockProxy<OrganizationInviteService>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let internalPolicyService: MockProxy<InternalPolicyService>;
|
||||
@@ -44,9 +45,10 @@ describe("WebLoginComponentService", () => {
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
|
||||
beforeEach(() => {
|
||||
acceptOrganizationInviteService = mock<AcceptOrganizationInviteService>();
|
||||
organizationInviteService = mock<OrganizationInviteService>();
|
||||
logService = mock<LogService>();
|
||||
policyApiService = mock<PolicyApiServiceAbstraction>();
|
||||
internalPolicyService = mock<InternalPolicyService>();
|
||||
@@ -57,12 +59,13 @@ describe("WebLoginComponentService", () => {
|
||||
platformUtilsService = mock<PlatformUtilsService>();
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
configService = mock<ConfigService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
WebLoginComponentService,
|
||||
{ provide: DefaultLoginComponentService, useClass: WebLoginComponentService },
|
||||
{ provide: AcceptOrganizationInviteService, useValue: acceptOrganizationInviteService },
|
||||
{ provide: OrganizationInviteService, useValue: organizationInviteService },
|
||||
{ provide: LogService, useValue: logService },
|
||||
{ provide: PolicyApiServiceAbstraction, useValue: policyApiService },
|
||||
{ provide: InternalPolicyService, useValue: internalPolicyService },
|
||||
@@ -73,6 +76,7 @@ describe("WebLoginComponentService", () => {
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: ConfigService, useValue: configService },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(WebLoginComponentService);
|
||||
@@ -83,26 +87,29 @@ describe("WebLoginComponentService", () => {
|
||||
});
|
||||
|
||||
describe("getOrgPoliciesFromOrgInvite", () => {
|
||||
const mockEmail = "test@example.com";
|
||||
const orgInvite: OrganizationInvite = {
|
||||
organizationId: "org-id",
|
||||
token: "token",
|
||||
email: mockEmail,
|
||||
organizationUserId: "org-user-id",
|
||||
initOrganization: false,
|
||||
orgSsoIdentifier: "sso-id",
|
||||
orgUserHasExistingUser: false,
|
||||
organizationName: "org-name",
|
||||
};
|
||||
|
||||
it("returns undefined if organization invite is null", async () => {
|
||||
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
const result = await service.getOrgPoliciesFromOrgInvite();
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
const result = await service.getOrgPoliciesFromOrgInvite(mockEmail);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it("logs an error if getPoliciesByToken throws an error", async () => {
|
||||
const error = new Error("Test error");
|
||||
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue({
|
||||
organizationId: "org-id",
|
||||
token: "token",
|
||||
email: "email",
|
||||
organizationUserId: "org-user-id",
|
||||
initOrganization: false,
|
||||
orgSsoIdentifier: "sso-id",
|
||||
orgUserHasExistingUser: false,
|
||||
organizationName: "org-name",
|
||||
});
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
policyApiService.getPoliciesByToken.mockRejectedValue(error);
|
||||
await service.getOrgPoliciesFromOrgInvite();
|
||||
await service.getOrgPoliciesFromOrgInvite(mockEmail);
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
@@ -117,16 +124,7 @@ describe("WebLoginComponentService", () => {
|
||||
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
|
||||
resetPasswordPolicyOptions.autoEnrollEnabled = autoEnrollEnabled;
|
||||
|
||||
acceptOrganizationInviteService.getOrganizationInvite.mockResolvedValue({
|
||||
organizationId: "org-id",
|
||||
token: "token",
|
||||
email: "email",
|
||||
organizationUserId: "org-user-id",
|
||||
initOrganization: false,
|
||||
orgSsoIdentifier: "sso-id",
|
||||
orgUserHasExistingUser: false,
|
||||
organizationName: "org-name",
|
||||
});
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue(policies);
|
||||
|
||||
internalPolicyService.getResetPasswordPolicyOptions.mockReturnValue([
|
||||
@@ -134,11 +132,11 @@ describe("WebLoginComponentService", () => {
|
||||
resetPasswordPolicyEnabled,
|
||||
]);
|
||||
|
||||
internalPolicyService.masterPasswordPolicyOptions$.mockReturnValue(
|
||||
of(masterPasswordPolicyOptions),
|
||||
internalPolicyService.combinePoliciesIntoMasterPasswordPolicyOptions.mockReturnValue(
|
||||
masterPasswordPolicyOptions,
|
||||
);
|
||||
|
||||
const result = await service.getOrgPoliciesFromOrgInvite();
|
||||
const result = await service.getOrgPoliciesFromOrgInvite(mockEmail);
|
||||
|
||||
expect(result).toEqual({
|
||||
policies: policies,
|
||||
@@ -148,5 +146,40 @@ describe("WebLoginComponentService", () => {
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("given the orgInvite email does not match the provided email", () => {
|
||||
const mockMismatchedEmail = "mismatched@example.com";
|
||||
it("should clear the login redirect URL and organization invite", async () => {
|
||||
// Arrange
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue({
|
||||
...orgInvite,
|
||||
email: mockMismatchedEmail,
|
||||
});
|
||||
|
||||
// Act
|
||||
await service.getOrgPoliciesFromOrgInvite(mockEmail);
|
||||
|
||||
// Assert
|
||||
expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1);
|
||||
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should log an error and return undefined", async () => {
|
||||
// Arrange
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue({
|
||||
...orgInvite,
|
||||
email: mockMismatchedEmail,
|
||||
});
|
||||
|
||||
// Act
|
||||
const result = await service.getOrgPoliciesFromOrgInvite(mockEmail);
|
||||
|
||||
// Assert
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
`WebLoginComponentService.getOrgPoliciesFromOrgInvite: Email mismatch. Expected: ${mockMismatchedEmail}, Received: ${mockEmail}`,
|
||||
);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
DefaultLoginComponentService,
|
||||
@@ -14,15 +13,15 @@ import { InternalPolicyService } from "@bitwarden/common/admin-console/abstracti
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
import { RouterService } from "../../../../core/router.service";
|
||||
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||
|
||||
@Injectable()
|
||||
export class WebLoginComponentService
|
||||
@@ -30,7 +29,7 @@ export class WebLoginComponentService
|
||||
implements LoginComponentService
|
||||
{
|
||||
constructor(
|
||||
protected acceptOrganizationInviteService: AcceptOrganizationInviteService,
|
||||
protected organizationInviteService: OrganizationInviteService,
|
||||
protected logService: LogService,
|
||||
protected policyApiService: PolicyApiServiceAbstraction,
|
||||
protected policyService: InternalPolicyService,
|
||||
@@ -42,6 +41,7 @@ export class WebLoginComponentService
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
private router: Router,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
super(
|
||||
cryptoFunctionService,
|
||||
@@ -66,10 +66,27 @@ export class WebLoginComponentService
|
||||
return;
|
||||
}
|
||||
|
||||
async getOrgPoliciesFromOrgInvite(): Promise<PasswordPolicies | null> {
|
||||
const orgInvite = await this.acceptOrganizationInviteService.getOrganizationInvite();
|
||||
async getOrgPoliciesFromOrgInvite(email: string): Promise<PasswordPolicies | undefined> {
|
||||
const orgInvite = await this.organizationInviteService.getOrganizationInvite();
|
||||
|
||||
if (orgInvite != null) {
|
||||
/**
|
||||
* Check if the email on the org invite matches the email submitted in the login form. This is
|
||||
* important because say userA at "userA@mail.com" clicks an emailed org invite link, but then
|
||||
* on the login page form they change the email to "userB@mail.com". We don't want to apply the org
|
||||
* invite in state to userB. Therefore we clear the login redirect url as well as the org invite,
|
||||
* allowing userB to login as normal.
|
||||
*/
|
||||
if (orgInvite.email !== email.toLowerCase()) {
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.organizationInviteService.clearOrganizationInvitation();
|
||||
|
||||
this.logService.error(
|
||||
`WebLoginComponentService.getOrgPoliciesFromOrgInvite: Email mismatch. Expected: ${orgInvite.email}, Received: ${email}`,
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let policies: Policy[];
|
||||
|
||||
try {
|
||||
@@ -84,7 +101,7 @@ export class WebLoginComponentService
|
||||
}
|
||||
|
||||
if (policies == null) {
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const resetPasswordPolicy = this.policyService.getResetPasswordPolicyOptions(
|
||||
@@ -95,12 +112,8 @@ export class WebLoginComponentService
|
||||
const isPolicyAndAutoEnrollEnabled =
|
||||
resetPasswordPolicy[1] && resetPasswordPolicy[0].autoEnrollEnabled;
|
||||
|
||||
const enforcedPasswordPolicyOptions = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.policyService.masterPasswordPolicyOptions$(userId, policies)),
|
||||
),
|
||||
);
|
||||
const enforcedPasswordPolicyOptions =
|
||||
this.policyService.combinePoliciesIntoMasterPasswordPolicyOptions(policies);
|
||||
|
||||
return {
|
||||
policies,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
|
||||
import { ORGANIZATION_INVITE } from "@bitwarden/common/auth/services/organization-invite/organization-invite-state";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
|
||||
export class WebOrganizationInviteService implements OrganizationInviteService {
|
||||
private organizationInvitationState: GlobalState<OrganizationInvite | null>;
|
||||
|
||||
constructor(private readonly globalStateProvider: GlobalStateProvider) {
|
||||
this.organizationInvitationState = this.globalStateProvider.get(ORGANIZATION_INVITE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently stored organization invite
|
||||
*/
|
||||
async getOrganizationInvite(): Promise<OrganizationInvite | null> {
|
||||
return await firstValueFrom(this.organizationInvitationState.state$);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores a new organization invite
|
||||
* @param invite an organization invite
|
||||
* @throws if the invite is nullish
|
||||
*/
|
||||
async setOrganizationInvitation(invite: OrganizationInvite): Promise<void> {
|
||||
if (invite == null) {
|
||||
throw new Error("Invite cannot be null. Use clearOrganizationInvitation instead.");
|
||||
}
|
||||
await this.organizationInvitationState.update(() => invite);
|
||||
}
|
||||
|
||||
/** Clears the currently stored organization invite */
|
||||
async clearOrganizationInvitation(): Promise<void> {
|
||||
await this.organizationInvitationState.update(() => null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./set-initial-password/web-set-initial-password.service";
|
||||
@@ -0,0 +1,206 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
} from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import {
|
||||
FakeUserDecryptionOptions as UserDecryptionOptions,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { SetPasswordRequest } from "@bitwarden/common/auth/models/request/set-password.request";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { KeysRequest } from "@bitwarden/common/models/request/keys.request";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
|
||||
import { WebSetInitialPasswordService } from "./web-set-initial-password.service";
|
||||
|
||||
describe("WebSetInitialPasswordService", () => {
|
||||
let sut: SetInitialPasswordService;
|
||||
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let kdfConfigService: MockProxy<KdfConfigService>;
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let masterPasswordApiService: MockProxy<MasterPasswordApiService>;
|
||||
let masterPasswordService: MockProxy<InternalMasterPasswordServiceAbstraction>;
|
||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||
let organizationUserApiService: MockProxy<OrganizationUserApiService>;
|
||||
let userDecryptionOptionsService: MockProxy<InternalUserDecryptionOptionsServiceAbstraction>;
|
||||
let organizationInviteService: MockProxy<OrganizationInviteService>;
|
||||
let routerService: MockProxy<RouterService>;
|
||||
|
||||
beforeEach(() => {
|
||||
apiService = mock<ApiService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
i18nService = mock<I18nService>();
|
||||
kdfConfigService = mock<KdfConfigService>();
|
||||
keyService = mock<KeyService>();
|
||||
masterPasswordApiService = mock<MasterPasswordApiService>();
|
||||
masterPasswordService = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||
organizationUserApiService = mock<OrganizationUserApiService>();
|
||||
userDecryptionOptionsService = mock<InternalUserDecryptionOptionsServiceAbstraction>();
|
||||
organizationInviteService = mock<OrganizationInviteService>();
|
||||
routerService = mock<RouterService>();
|
||||
|
||||
sut = new WebSetInitialPasswordService(
|
||||
apiService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
organizationApiService,
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
organizationInviteService,
|
||||
routerService,
|
||||
);
|
||||
});
|
||||
|
||||
it("should instantiate", () => {
|
||||
expect(sut).not.toBeFalsy();
|
||||
});
|
||||
|
||||
describe("setInitialPassword(...)", () => {
|
||||
// Mock function parameters
|
||||
let credentials: SetInitialPasswordCredentials;
|
||||
let userType: SetInitialPasswordUserType;
|
||||
let userId: UserId;
|
||||
|
||||
// Mock other function data
|
||||
let userKey: UserKey;
|
||||
let userKeyEncString: EncString;
|
||||
let masterKeyEncryptedUserKey: [UserKey, EncString];
|
||||
|
||||
let keyPair: [string, EncString];
|
||||
let keysRequest: KeysRequest;
|
||||
|
||||
let userDecryptionOptions: UserDecryptionOptions;
|
||||
let userDecryptionOptionsSubject: BehaviorSubject<UserDecryptionOptions>;
|
||||
let setPasswordRequest: SetPasswordRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock function parameters
|
||||
credentials = {
|
||||
newMasterKey: new SymmetricCryptoKey(new Uint8Array(32).buffer as CsprngArray) as MasterKey,
|
||||
newServerMasterKeyHash: "newServerMasterKeyHash",
|
||||
newLocalMasterKeyHash: "newLocalMasterKeyHash",
|
||||
newPasswordHint: "newPasswordHint",
|
||||
kdfConfig: DEFAULT_KDF_CONFIG,
|
||||
orgSsoIdentifier: "orgSsoIdentifier",
|
||||
orgId: "orgId",
|
||||
resetPasswordAutoEnroll: false,
|
||||
};
|
||||
userId = "userId" as UserId;
|
||||
userType = SetInitialPasswordUserType.JIT_PROVISIONED_MP_ORG_USER;
|
||||
|
||||
// Mock other function data
|
||||
userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
userKeyEncString = new EncString("masterKeyEncryptedUserKey");
|
||||
masterKeyEncryptedUserKey = [userKey, userKeyEncString];
|
||||
|
||||
keyPair = ["publicKey", new EncString("privateKey")];
|
||||
keysRequest = new KeysRequest(keyPair[0], keyPair[1].encryptedString);
|
||||
|
||||
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
|
||||
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
|
||||
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
|
||||
|
||||
setPasswordRequest = new SetPasswordRequest(
|
||||
credentials.newServerMasterKeyHash,
|
||||
masterKeyEncryptedUserKey[1].encryptedString,
|
||||
credentials.newPasswordHint,
|
||||
credentials.orgSsoIdentifier,
|
||||
keysRequest,
|
||||
credentials.kdfConfig.kdfType,
|
||||
credentials.kdfConfig.iterations,
|
||||
);
|
||||
});
|
||||
|
||||
function setupMocks() {
|
||||
// Mock makeMasterKeyEncryptedUserKey() values
|
||||
keyService.userKey$.mockReturnValue(of(userKey));
|
||||
keyService.encryptUserKeyWithMasterKey.mockResolvedValue(masterKeyEncryptedUserKey);
|
||||
|
||||
// Mock keyPair values
|
||||
keyService.userPrivateKey$.mockReturnValue(of(null));
|
||||
keyService.userPublicKey$.mockReturnValue(of(null));
|
||||
keyService.makeKeyPair.mockResolvedValue(keyPair);
|
||||
}
|
||||
|
||||
describe("given the initial password was successfully set", () => {
|
||||
it("should call routerService.getAndClearLoginRedirectUrl()", async () => {
|
||||
// Arrange
|
||||
setupMocks();
|
||||
|
||||
// Act
|
||||
await sut.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(routerService.getAndClearLoginRedirectUrl).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call acceptOrganizationInviteService.clearOrganizationInvitation()", async () => {
|
||||
// Arrange
|
||||
setupMocks();
|
||||
|
||||
// Act
|
||||
await sut.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
// Assert
|
||||
expect(masterPasswordApiService.setPassword).toHaveBeenCalledWith(setPasswordRequest);
|
||||
expect(organizationInviteService.clearOrganizationInvitation).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given the initial password was NOT successfully set (due to some error in setInitialPassword())", () => {
|
||||
it("should NOT call routerService.getAndClearLoginRedirectUrl()", async () => {
|
||||
// Arrange
|
||||
credentials.newMasterKey = null; // will trigger an error in setInitialPassword()
|
||||
setupMocks();
|
||||
|
||||
// Act
|
||||
const promise = sut.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
// Assert
|
||||
await expect(promise).rejects.toThrow();
|
||||
expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled();
|
||||
expect(routerService.getAndClearLoginRedirectUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should NOT call acceptOrganizationInviteService.clearOrganizationInvitation()", async () => {
|
||||
// Arrange
|
||||
credentials.newMasterKey = null; // will trigger an error in setInitialPassword()
|
||||
setupMocks();
|
||||
|
||||
// Act
|
||||
const promise = sut.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
// Assert
|
||||
await expect(promise).rejects.toThrow();
|
||||
expect(masterPasswordApiService.setPassword).not.toHaveBeenCalled();
|
||||
expect(organizationInviteService.clearOrganizationInvitation).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import { DefaultSetInitialPasswordService } from "@bitwarden/angular/auth/password-management/set-initial-password/default-set-initial-password.service.implementation";
|
||||
import {
|
||||
SetInitialPasswordCredentials,
|
||||
SetInitialPasswordService,
|
||||
SetInitialPasswordUserType,
|
||||
} from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.service.abstraction";
|
||||
import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KdfConfigService, KeyService } from "@bitwarden/key-management";
|
||||
import { RouterService } from "@bitwarden/web-vault/app/core";
|
||||
|
||||
export class WebSetInitialPasswordService
|
||||
extends DefaultSetInitialPasswordService
|
||||
implements SetInitialPasswordService
|
||||
{
|
||||
constructor(
|
||||
protected apiService: ApiService,
|
||||
protected encryptService: EncryptService,
|
||||
protected i18nService: I18nService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected keyService: KeyService,
|
||||
protected masterPasswordApiService: MasterPasswordApiService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||
protected organizationUserApiService: OrganizationUserApiService,
|
||||
protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction,
|
||||
private organizationInviteService: OrganizationInviteService,
|
||||
private routerService: RouterService,
|
||||
) {
|
||||
super(
|
||||
apiService,
|
||||
encryptService,
|
||||
i18nService,
|
||||
kdfConfigService,
|
||||
keyService,
|
||||
masterPasswordApiService,
|
||||
masterPasswordService,
|
||||
organizationApiService,
|
||||
organizationUserApiService,
|
||||
userDecryptionOptionsService,
|
||||
);
|
||||
}
|
||||
|
||||
override async setInitialPassword(
|
||||
credentials: SetInitialPasswordCredentials,
|
||||
userType: SetInitialPasswordUserType,
|
||||
userId: UserId,
|
||||
) {
|
||||
await super.setInitialPassword(credentials, userType, userId);
|
||||
|
||||
/**
|
||||
* TODO: Investigate refactoring the following logic in https://bitwarden.atlassian.net/browse/PM-22615
|
||||
* ---
|
||||
* When a user has been invited to an org, they can be accepted into the org in two different ways:
|
||||
*
|
||||
* 1) By clicking the email invite link, which triggers the normal AcceptOrganizationComponent flow
|
||||
* a. This flow sets an org invite in state
|
||||
* b. However, if the user does not already have an account AND the org has SSO enabled AND the require
|
||||
* SSO policy enabled, the AcceptOrganizationComponent will send the user to /sso to accelerate
|
||||
* the user through the SSO JIT provisioning process (see #2 below)
|
||||
*
|
||||
* 2) By logging in via SSO, which triggers the JIT provisioning process
|
||||
* a. This flow does NOT (itself) set an org invite in state
|
||||
* b. The set initial password process on the server accepts the user into the org after successfully
|
||||
* setting the password (see server - SetInitialMasterPasswordCommand.cs)
|
||||
*
|
||||
* If a user clicks the email link but gets accelerated through the SSO JIT process (see 1b),
|
||||
* the SSO JIT process will accept the user into the org upon setting their initial password (see 2b),
|
||||
* at which point we must remember to clear the deep linked URL used for accepting the org invite, as well
|
||||
* as clear the org invite itself that was originally set in state by the AcceptOrganizationComponent.
|
||||
*/
|
||||
await this.routerService.getAndClearLoginRedirectUrl();
|
||||
await this.organizationInviteService.clearOrganizationInvitation();
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,15 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { OrganizationInvite } from "@bitwarden/common/auth/services/organization-invite/organization-invite";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { DEFAULT_KDF_CONFIG, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||
import { OrganizationInvite } from "../../../organization-invite/organization-invite";
|
||||
|
||||
import { WebRegistrationFinishService } from "./web-registration-finish.service";
|
||||
|
||||
describe("WebRegistrationFinishService", () => {
|
||||
@@ -29,30 +25,26 @@ describe("WebRegistrationFinishService", () => {
|
||||
|
||||
let keyService: MockProxy<KeyService>;
|
||||
let accountApiService: MockProxy<AccountApiService>;
|
||||
let acceptOrgInviteService: MockProxy<AcceptOrganizationInviteService>;
|
||||
let organizationInviteService: MockProxy<OrganizationInviteService>;
|
||||
let policyApiService: MockProxy<PolicyApiServiceAbstraction>;
|
||||
let logService: MockProxy<LogService>;
|
||||
let policyService: MockProxy<PolicyService>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
let accountService: FakeAccountService;
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock<KeyService>();
|
||||
accountApiService = mock<AccountApiService>();
|
||||
acceptOrgInviteService = mock<AcceptOrganizationInviteService>();
|
||||
organizationInviteService = mock<OrganizationInviteService>();
|
||||
policyApiService = mock<PolicyApiServiceAbstraction>();
|
||||
logService = mock<LogService>();
|
||||
policyService = mock<PolicyService>();
|
||||
accountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
service = new WebRegistrationFinishService(
|
||||
keyService,
|
||||
accountApiService,
|
||||
acceptOrgInviteService,
|
||||
organizationInviteService,
|
||||
policyApiService,
|
||||
logService,
|
||||
policyService,
|
||||
accountService,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -72,21 +64,21 @@ describe("WebRegistrationFinishService", () => {
|
||||
});
|
||||
|
||||
it("returns null when the org invite is null", async () => {
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getOrgNameFromOrgInvite();
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns the organization name from the organization invite when it exists", async () => {
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
|
||||
const result = await service.getOrgNameFromOrgInvite();
|
||||
|
||||
expect(result).toEqual(orgInvite.organizationName);
|
||||
expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,22 +94,22 @@ describe("WebRegistrationFinishService", () => {
|
||||
});
|
||||
|
||||
it("returns null when the org invite is null", async () => {
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getMasterPasswordPolicyOptsFromOrgInvite();
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null when the policies are null", async () => {
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue(null);
|
||||
|
||||
const result = await service.getMasterPasswordPolicyOptsFromOrgInvite();
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith(
|
||||
orgInvite.organizationId,
|
||||
orgInvite.token,
|
||||
@@ -127,13 +119,13 @@ describe("WebRegistrationFinishService", () => {
|
||||
});
|
||||
|
||||
it("logs an error and returns null when policies cannot be fetched", async () => {
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
policyApiService.getPoliciesByToken.mockRejectedValue(new Error("error"));
|
||||
|
||||
const result = await service.getMasterPasswordPolicyOptsFromOrgInvite();
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith(
|
||||
orgInvite.organizationId,
|
||||
orgInvite.token,
|
||||
@@ -147,14 +139,14 @@ describe("WebRegistrationFinishService", () => {
|
||||
const masterPasswordPolicies = [new Policy()];
|
||||
const masterPasswordPolicyOptions = new MasterPasswordPolicyOptions();
|
||||
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
policyApiService.getPoliciesByToken.mockResolvedValue(masterPasswordPolicies);
|
||||
policyService.masterPasswordPolicyOptions$.mockReturnValue(of(masterPasswordPolicyOptions));
|
||||
|
||||
const result = await service.getMasterPasswordPolicyOptsFromOrgInvite();
|
||||
|
||||
expect(result).toEqual(masterPasswordPolicyOptions);
|
||||
expect(acceptOrgInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(organizationInviteService.getOrganizationInvite).toHaveBeenCalled();
|
||||
expect(policyApiService.getPoliciesByToken).toHaveBeenCalledWith(
|
||||
orgInvite.organizationId,
|
||||
orgInvite.token,
|
||||
@@ -221,7 +213,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
await service.finishRegistration(email, passwordInputResult, emailVerificationToken);
|
||||
|
||||
@@ -257,7 +249,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(orgInvite);
|
||||
|
||||
await service.finishRegistration(email, passwordInputResult);
|
||||
|
||||
@@ -293,7 +285,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
await service.finishRegistration(
|
||||
email,
|
||||
@@ -334,7 +326,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
await service.finishRegistration(
|
||||
email,
|
||||
@@ -377,7 +369,7 @@ describe("WebRegistrationFinishService", () => {
|
||||
keyService.makeUserKey.mockResolvedValue([userKey, userKeyEncString]);
|
||||
keyService.makeKeyPair.mockResolvedValue(userKeyPair);
|
||||
accountApiService.registerFinish.mockResolvedValue();
|
||||
acceptOrgInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
organizationInviteService.getOrganizationInvite.mockResolvedValue(null);
|
||||
|
||||
await service.finishRegistration(
|
||||
email,
|
||||
|
||||
@@ -12,14 +12,15 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
|
||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { RegisterFinishRequest } from "@bitwarden/common/auth/models/request/registration/register-finish.request";
|
||||
import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service";
|
||||
import {
|
||||
EncryptedString,
|
||||
EncString,
|
||||
} from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { EncryptedString, EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { AcceptOrganizationInviteService } from "../../../organization-invite/accept-organization.service";
|
||||
|
||||
export class WebRegistrationFinishService
|
||||
extends DefaultRegistrationFinishService
|
||||
implements RegistrationFinishService
|
||||
@@ -27,17 +28,16 @@ export class WebRegistrationFinishService
|
||||
constructor(
|
||||
protected keyService: KeyService,
|
||||
protected accountApiService: AccountApiService,
|
||||
private acceptOrgInviteService: AcceptOrganizationInviteService,
|
||||
private organizationInviteService: OrganizationInviteService,
|
||||
private policyApiService: PolicyApiServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private policyService: PolicyService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(keyService, accountApiService);
|
||||
}
|
||||
|
||||
override async getOrgNameFromOrgInvite(): Promise<string | null> {
|
||||
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
|
||||
const orgInvite = await this.organizationInviteService.getOrganizationInvite();
|
||||
if (orgInvite == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ export class WebRegistrationFinishService
|
||||
|
||||
override async getMasterPasswordPolicyOptsFromOrgInvite(): Promise<MasterPasswordPolicyOptions | null> {
|
||||
// If there's a deep linked org invite, use it to get the password policies
|
||||
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
|
||||
const orgInvite = await this.organizationInviteService.getOrganizationInvite();
|
||||
|
||||
if (orgInvite == null) {
|
||||
return null;
|
||||
@@ -100,7 +100,7 @@ export class WebRegistrationFinishService
|
||||
// web specific logic
|
||||
// Org invites are deep linked. Non-existent accounts are redirected to the register page.
|
||||
// Org user id and token are included here only for validation and two factor purposes.
|
||||
const orgInvite = await this.acceptOrgInviteService.getOrganizationInvite();
|
||||
const orgInvite = await this.organizationInviteService.getOrganizationInvite();
|
||||
if (orgInvite != null) {
|
||||
registerRequest.organizationUserId = orgInvite.organizationUserId;
|
||||
registerRequest.orgInviteToken = orgInvite.token;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user