1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-18 02:41:15 +02: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:
aj-rosado 2024-06-18 22:13:55 +01:00 committed by GitHub
parent 08cdecf514
commit 1a37d02556
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 134 additions and 8 deletions

View File

@ -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;
}
}

View File

@ -1,8 +1,11 @@
import { Component, OnInit } from "@angular/core";
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 { 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";
@ -14,13 +17,21 @@ export class ReportsHomeComponent implements OnInit {
reports$: Observable<ReportEntry[]>;
homepage$: Observable<boolean>;
private isMemberAccessReportEnabled: boolean;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
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(
filter((event) => event instanceof NavigationEnd),
map((event) => this.isReportsHomepageRouteUrl((event as NavigationEnd).urlAfterRedirects)),
@ -29,16 +40,15 @@ export class ReportsHomeComponent implements OnInit {
this.reports$ = this.route.params.pipe(
concatMap((params) => this.organizationService.get$(params.organizationId)),
map((org) => this.buildReports(org.isFreeOrg)),
map((org) => this.buildReports(org.productTierType)),
);
}
private buildReports(upgradeRequired: boolean): ReportEntry[] {
const reportRequiresUpgrade = upgradeRequired
? ReportVariant.RequiresUpgrade
: ReportVariant.Enabled;
private buildReports(productType: ProductTierType): ReportEntry[] {
const reportRequiresUpgrade =
productType == ProductTierType.Free ? ReportVariant.RequiresUpgrade : ReportVariant.Enabled;
return [
const reportsArray = [
{
...reports[ReportType.ExposedPasswords],
variant: reportRequiresUpgrade,
@ -60,6 +70,18 @@ export class ReportsHomeComponent implements OnInit {
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 {

View File

@ -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>
`;

View File

@ -1,6 +1,7 @@
import { ReportBreach } from "./icons/report-breach.icon";
import { ReportExposedPasswords } from "./icons/report-exposed-passwords.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 { ReportUnsecuredWebsites } from "./icons/report-unsecured-websites.icon";
import { ReportWeakPasswords } from "./icons/report-weak-passwords.icon";
@ -13,6 +14,7 @@ export enum ReportType {
UnsecuredWebsites = "unsecuredWebsites",
Inactive2fa = "inactive2fa",
DataBreach = "dataBreach",
MemberAccessReport = "memberAccessReport",
}
type ReportWithoutVariant = Omit<ReportEntry, "variant">;
@ -54,4 +56,10 @@ export const reports: Record<ReportType, ReportWithoutVariant> = {
route: "breach-report",
icon: ReportBreach,
},
[ReportType.MemberAccessReport]: {
title: "memberAccessReport",
description: "memberAccessReportDesc",
route: "member-access-report",
icon: MemberAccess,
},
};

View File

@ -2,4 +2,5 @@ export enum ReportVariant {
Enabled = "Enabled",
RequiresPremium = "RequiresPremium",
RequiresUpgrade = "RequiresUpgrade",
RequiresEnterprise = "RequiresEnterprise",
}

View File

@ -8384,6 +8384,12 @@
"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."
},
"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": {
"message": "Date"
},
@ -8393,5 +8399,11 @@
"invoiceNumberHeader": {
"message": "Invoice 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."
}
}

View File

@ -19,6 +19,7 @@ export enum FeatureFlag {
BulkDeviceApproval = "bulk-device-approval",
EmailVerification = "email-verification",
InlineMenuFieldQualification = "inline-menu-field-qualification",
MemberAccessReport = "ac-2059-member-access-report",
}
export type AllowedFeatureFlagTypes = boolean | number | string;
@ -48,6 +49,7 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.BulkDeviceApproval]: FALSE,
[FeatureFlag.EmailVerification]: FALSE,
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
[FeatureFlag.MemberAccessReport]: FALSE,
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;