mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-20 02:01:47 +01:00
[AC-2504] Add new members access report card (#9335)
* Added new report card and FeatureFlag for MemberAccessReport * Add new "isEnterpriseOrgGuard" * Add member access icon * Show upgrade organization dialog for enterprise on member access report click * verify member access featureflag on enterprise org guard * add comment with TODO information for follow up task * Improved readability, removed path to wrong component and refactored buildReports to use the productType * added TODO to remove the feature flag on cleanup * changing ProductType to ProductTierType on isEnterpriseOrgGuard
This commit is contained in:
parent
08cdecf514
commit
1a37d02556
@ -0,0 +1,67 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
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 { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: "root",
|
||||||
|
})
|
||||||
|
export class IsEnterpriseOrgGuard implements CanActivate {
|
||||||
|
constructor(
|
||||||
|
private router: Router,
|
||||||
|
private organizationService: OrganizationService,
|
||||||
|
private dialogService: DialogService,
|
||||||
|
private configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
|
const isMemberAccessReportEnabled = await firstValueFrom(
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.MemberAccessReport),
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: Remove on "MemberAccessReport" feature flag cleanup
|
||||||
|
if (!isMemberAccessReportEnabled) {
|
||||||
|
return this.router.createUrlTree(["/"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const org = await this.organizationService.get(route.params.organizationId);
|
||||||
|
|
||||||
|
if (org == null) {
|
||||||
|
return this.router.createUrlTree(["/"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (org.productTierType != ProductTierType.Enterprise) {
|
||||||
|
// Users without billing permission can't access billing
|
||||||
|
if (!org.canEditSubscription) {
|
||||||
|
await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "upgradeOrganizationEnterprise" },
|
||||||
|
content: { key: "onlyAvailableForEnterpriseOrganization" },
|
||||||
|
acceptButtonText: { key: "ok" },
|
||||||
|
cancelButtonText: null,
|
||||||
|
type: "info",
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
const upgradeConfirmed = await this.dialogService.openSimpleDialog({
|
||||||
|
title: { key: "upgradeOrganizationEnterprise" },
|
||||||
|
content: { key: "onlyAvailableForEnterpriseOrganization" },
|
||||||
|
acceptButtonText: { key: "upgradeOrganization" },
|
||||||
|
type: "info",
|
||||||
|
icon: "bwi-arrow-circle-up",
|
||||||
|
});
|
||||||
|
if (upgradeConfirmed) {
|
||||||
|
await this.router.navigate(["organizations", org.id, "billing", "subscription"], {
|
||||||
|
queryParams: { upgrade: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return org.productTierType == ProductTierType.Enterprise;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
|
import { ActivatedRoute, NavigationEnd, Router } from "@angular/router";
|
||||||
import { filter, map, Observable, startWith, concatMap } from "rxjs";
|
import { filter, map, Observable, startWith, concatMap, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
|
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 { ReportVariant, reports, ReportType, ReportEntry } from "../../../tools/reports";
|
import { ReportVariant, reports, ReportType, ReportEntry } from "../../../tools/reports";
|
||||||
|
|
||||||
@ -14,13 +17,21 @@ export class ReportsHomeComponent implements OnInit {
|
|||||||
reports$: Observable<ReportEntry[]>;
|
reports$: Observable<ReportEntry[]>;
|
||||||
homepage$: Observable<boolean>;
|
homepage$: Observable<boolean>;
|
||||||
|
|
||||||
|
private isMemberAccessReportEnabled: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
|
private configService: ConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
async ngOnInit() {
|
||||||
|
// TODO: Remove on "MemberAccessReport" feature flag cleanup
|
||||||
|
this.isMemberAccessReportEnabled = await firstValueFrom(
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.MemberAccessReport),
|
||||||
|
);
|
||||||
|
|
||||||
this.homepage$ = this.router.events.pipe(
|
this.homepage$ = this.router.events.pipe(
|
||||||
filter((event) => event instanceof NavigationEnd),
|
filter((event) => event instanceof NavigationEnd),
|
||||||
map((event) => this.isReportsHomepageRouteUrl((event as NavigationEnd).urlAfterRedirects)),
|
map((event) => this.isReportsHomepageRouteUrl((event as NavigationEnd).urlAfterRedirects)),
|
||||||
@ -29,16 +40,15 @@ export class ReportsHomeComponent implements OnInit {
|
|||||||
|
|
||||||
this.reports$ = this.route.params.pipe(
|
this.reports$ = this.route.params.pipe(
|
||||||
concatMap((params) => this.organizationService.get$(params.organizationId)),
|
concatMap((params) => this.organizationService.get$(params.organizationId)),
|
||||||
map((org) => this.buildReports(org.isFreeOrg)),
|
map((org) => this.buildReports(org.productTierType)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildReports(upgradeRequired: boolean): ReportEntry[] {
|
private buildReports(productType: ProductTierType): ReportEntry[] {
|
||||||
const reportRequiresUpgrade = upgradeRequired
|
const reportRequiresUpgrade =
|
||||||
? ReportVariant.RequiresUpgrade
|
productType == ProductTierType.Free ? ReportVariant.RequiresUpgrade : ReportVariant.Enabled;
|
||||||
: ReportVariant.Enabled;
|
|
||||||
|
|
||||||
return [
|
const reportsArray = [
|
||||||
{
|
{
|
||||||
...reports[ReportType.ExposedPasswords],
|
...reports[ReportType.ExposedPasswords],
|
||||||
variant: reportRequiresUpgrade,
|
variant: reportRequiresUpgrade,
|
||||||
@ -60,6 +70,18 @@ export class ReportsHomeComponent implements OnInit {
|
|||||||
variant: reportRequiresUpgrade,
|
variant: reportRequiresUpgrade,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (this.isMemberAccessReportEnabled) {
|
||||||
|
reportsArray.push({
|
||||||
|
...reports[ReportType.MemberAccessReport],
|
||||||
|
variant:
|
||||||
|
productType == ProductTierType.Enterprise
|
||||||
|
? ReportVariant.Enabled
|
||||||
|
: ReportVariant.RequiresEnterprise,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return reportsArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
private isReportsHomepageRouteUrl(url: string): boolean {
|
private isReportsHomepageRouteUrl(url: string): boolean {
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import { svgIcon } from "@bitwarden/components";
|
||||||
|
|
||||||
|
export const MemberAccess = svgIcon`
|
||||||
|
<svg width="94" height="63" viewBox="0 0 94 63" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M70 10H2V46H12.3227H12.6806H46.2498H70V18.1286V10Z" fill="#518FFF"/>
|
||||||
|
<path d="M65.7419 1H6C3.23858 1 1 3.23858 1 6V42.2581C1 45.0195 3.23857 47.2581 6 47.2581H12.2835H12.6401H46.0818H65.7419C68.5034 47.2581 70.7419 45.0195 70.7419 42.2581V11.9933V6C70.7419 3.23858 68.5034 1 65.7419 1Z" stroke="white" stroke-width="2"/>
|
||||||
|
<circle cx="70.129" cy="27.0968" r="13.0968" fill="#175DDC" stroke="white" stroke-width="2"/>
|
||||||
|
<path d="M88.9315 61.8708C73.6363 61.8708 66.0989 61.8708 50.4248 61.8708C48.57 61.8708 47.8031 59.945 48.0426 58.4704C49.725 48.1136 58.9691 40.1934 70.1207 40.1934C81.2722 40.1934 90.5164 48.1136 92.1988 58.4704C92.5526 60.6485 91.3052 61.8708 88.9315 61.8708Z" fill="#175DDC" stroke="white" stroke-width="2"/>
|
||||||
|
<path d="M55.7419 5.61292V5.1613" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M59.8064 5.61292V5.1613" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<path d="M63.871 5.61292V5.1613" stroke="white" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
<line x1="2" y1="9" x2="69.7419" y2="9" stroke="white" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
`;
|
@ -1,6 +1,7 @@
|
|||||||
import { ReportBreach } from "./icons/report-breach.icon";
|
import { ReportBreach } from "./icons/report-breach.icon";
|
||||||
import { ReportExposedPasswords } from "./icons/report-exposed-passwords.icon";
|
import { ReportExposedPasswords } from "./icons/report-exposed-passwords.icon";
|
||||||
import { ReportInactiveTwoFactor } from "./icons/report-inactive-two-factor.icon";
|
import { ReportInactiveTwoFactor } from "./icons/report-inactive-two-factor.icon";
|
||||||
|
import { MemberAccess } from "./icons/report-member-access.icon";
|
||||||
import { ReportReusedPasswords } from "./icons/report-reused-passwords.icon";
|
import { ReportReusedPasswords } from "./icons/report-reused-passwords.icon";
|
||||||
import { ReportUnsecuredWebsites } from "./icons/report-unsecured-websites.icon";
|
import { ReportUnsecuredWebsites } from "./icons/report-unsecured-websites.icon";
|
||||||
import { ReportWeakPasswords } from "./icons/report-weak-passwords.icon";
|
import { ReportWeakPasswords } from "./icons/report-weak-passwords.icon";
|
||||||
@ -13,6 +14,7 @@ export enum ReportType {
|
|||||||
UnsecuredWebsites = "unsecuredWebsites",
|
UnsecuredWebsites = "unsecuredWebsites",
|
||||||
Inactive2fa = "inactive2fa",
|
Inactive2fa = "inactive2fa",
|
||||||
DataBreach = "dataBreach",
|
DataBreach = "dataBreach",
|
||||||
|
MemberAccessReport = "memberAccessReport",
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReportWithoutVariant = Omit<ReportEntry, "variant">;
|
type ReportWithoutVariant = Omit<ReportEntry, "variant">;
|
||||||
@ -54,4 +56,10 @@ export const reports: Record<ReportType, ReportWithoutVariant> = {
|
|||||||
route: "breach-report",
|
route: "breach-report",
|
||||||
icon: ReportBreach,
|
icon: ReportBreach,
|
||||||
},
|
},
|
||||||
|
[ReportType.MemberAccessReport]: {
|
||||||
|
title: "memberAccessReport",
|
||||||
|
description: "memberAccessReportDesc",
|
||||||
|
route: "member-access-report",
|
||||||
|
icon: MemberAccess,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -2,4 +2,5 @@ export enum ReportVariant {
|
|||||||
Enabled = "Enabled",
|
Enabled = "Enabled",
|
||||||
RequiresPremium = "RequiresPremium",
|
RequiresPremium = "RequiresPremium",
|
||||||
RequiresUpgrade = "RequiresUpgrade",
|
RequiresUpgrade = "RequiresUpgrade",
|
||||||
|
RequiresEnterprise = "RequiresEnterprise",
|
||||||
}
|
}
|
||||||
|
@ -8384,6 +8384,12 @@
|
|||||||
"message": "This email address will receive all invoices pertaining to this provider",
|
"message": "This email address will receive all invoices pertaining to this provider",
|
||||||
"description": "A hint that shows up on the Provider setup page to inform the admin the billing email will receive the provider's invoices."
|
"description": "A hint that shows up on the Provider setup page to inform the admin the billing email will receive the provider's invoices."
|
||||||
},
|
},
|
||||||
|
"upgradeOrganizationEnterprise": {
|
||||||
|
"message": "Identify security risks by auditing member access"
|
||||||
|
},
|
||||||
|
"onlyAvailableForEnterpriseOrganization": {
|
||||||
|
"message": "Quickly view member access across the organization by upgrading to an Enterprise plan."
|
||||||
|
},
|
||||||
"date": {
|
"date": {
|
||||||
"message": "Date"
|
"message": "Date"
|
||||||
},
|
},
|
||||||
@ -8393,5 +8399,11 @@
|
|||||||
"invoiceNumberHeader": {
|
"invoiceNumberHeader": {
|
||||||
"message": "Invoice number",
|
"message": "Invoice number",
|
||||||
"description": "A table header for an invoice's number"
|
"description": "A table header for an invoice's number"
|
||||||
|
},
|
||||||
|
"memberAccessReport": {
|
||||||
|
"message": "Member access"
|
||||||
|
},
|
||||||
|
"memberAccessReportDesc": {
|
||||||
|
"message": "Ensure members have access to the right credentials and their accounts are secure. Use this report to obtain a CSV of member access and account configurations."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ export enum FeatureFlag {
|
|||||||
BulkDeviceApproval = "bulk-device-approval",
|
BulkDeviceApproval = "bulk-device-approval",
|
||||||
EmailVerification = "email-verification",
|
EmailVerification = "email-verification",
|
||||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||||
|
MemberAccessReport = "ac-2059-member-access-report",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||||
@ -48,6 +49,7 @@ export const DefaultFeatureFlagValue = {
|
|||||||
[FeatureFlag.BulkDeviceApproval]: FALSE,
|
[FeatureFlag.BulkDeviceApproval]: FALSE,
|
||||||
[FeatureFlag.EmailVerification]: FALSE,
|
[FeatureFlag.EmailVerification]: FALSE,
|
||||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||||
|
[FeatureFlag.MemberAccessReport]: FALSE,
|
||||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||||
|
|
||||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||||
|
Loading…
Reference in New Issue
Block a user