diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 78003b00ff..9eace676f4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -6,6 +6,7 @@ ## Secrets Manager team files ## bitwarden_license/bit-web/src/app/secrets-manager @bitwarden/team-secrets-manager-dev +apps/web/src/app/secrets-manager/ @bitwarden/team-secrets-manager-dev ## Auth team files ## apps/browser/src/auth @bitwarden/team-auth-dev diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html index c91e14fa52..08195d95bf 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.html @@ -15,21 +15,45 @@ class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0" > {{ "moreFromBitwarden" | i18n }} - - -
- {{ more.otherProductOverrides?.name ?? more.name }} -
- {{ more.otherProductOverrides.supportingText }} + + + + +
+ {{ more.otherProductOverrides?.name ?? more.name }} +
+ {{ more.otherProductOverrides.supportingText }} +
-
- + + + + +
+ {{ more.otherProductOverrides?.name ?? more.name }} +
+ {{ more.otherProductOverrides.supportingText }} +
+
+
+
diff --git a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts index d7400e478c..a07f56db2d 100644 --- a/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts +++ b/apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.spec.ts @@ -80,7 +80,10 @@ describe("NavigationProductSwitcherComponent", () => { isActive: false, name: "Other Product", icon: "bwi-lock", - marketingRoute: "https://www.example.com/", + marketingRoute: { + route: "https://www.example.com/", + external: true, + }, }, ], }); @@ -100,7 +103,10 @@ describe("NavigationProductSwitcherComponent", () => { isActive: false, name: "Other Product", icon: "bwi-lock", - marketingRoute: "https://www.example.com/", + marketingRoute: { + route: "https://www.example.com/", + external: true, + }, otherProductOverrides: { name: "Alternate name" }, }, ], @@ -117,7 +123,10 @@ describe("NavigationProductSwitcherComponent", () => { isActive: false, name: "Other Product", icon: "bwi-lock", - marketingRoute: "https://www.example.com/", + marketingRoute: { + route: "https://www.example.com/", + external: true, + }, otherProductOverrides: { name: "Alternate name", supportingText: "Supporting Text" }, }, ], @@ -134,9 +143,27 @@ describe("NavigationProductSwitcherComponent", () => { mockProducts$.next({ bento: [], other: [ - { name: "AA Product", icon: "bwi-lock", marketingRoute: "https://www.example.com/" }, - { name: "Test Product", icon: "bwi-lock", marketingRoute: "https://www.example.com/" }, - { name: "Organizations", icon: "bwi-lock", marketingRoute: "https://www.example.com/" }, + { + name: "AA Product", + icon: "bwi-lock", + marketingRoute: { + route: "https://www.example.com/", + external: true, + }, + }, + { + name: "Test Product", + icon: "bwi-lock", + marketingRoute: { + route: "https://www.example.com/", + external: true, + }, + }, + { + name: "Organizations", + icon: "bwi-lock", + marketingRoute: { route: "https://www.example.com/", external: true }, + }, ], }); @@ -157,7 +184,10 @@ describe("NavigationProductSwitcherComponent", () => { { name: "Organizations", icon: "bwi-lock", - marketingRoute: "https://www.example.com/", + marketingRoute: { + route: "https://www.example.com/", + external: true, + }, isActive: true, }, ], diff --git a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html index 62d8b6a075..55f7240194 100644 --- a/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html +++ b/apps/web/src/app/layouts/product-switcher/product-switcher-content.component.html @@ -34,17 +34,30 @@ class="tw-mt-4 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0 tw-border-t tw-border-solid tw-border-t-text-muted tw-p-2 tw-pb-0" > {{ "moreFromBitwarden" | i18n }} - - - {{ product.name }} - - + + + + + {{ product.name }} + + + + + + {{ product.name }} + + + diff --git a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts index 434391cd50..28474d792a 100644 --- a/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts +++ b/apps/web/src/app/layouts/product-switcher/shared/product-switcher.service.ts @@ -30,7 +30,13 @@ export type ProductSwitcherItem = { /** * Route for items in the `otherProducts$` section */ - marketingRoute?: string | any[]; + marketingRoute?: { + route: string | any[]; + external: boolean; + }; + /** + * Route definition for external/internal routes for items in the `otherProducts$` section + */ /** * Used to apply css styles to show when a button is selected @@ -136,7 +142,10 @@ export class ProductSwitcherService { name: "Password Manager", icon: "bwi-lock", appRoute: "/vault", - marketingRoute: "https://bitwarden.com/products/personal/", + marketingRoute: { + route: "https://bitwarden.com/products/personal/", + external: true, + }, isActive: !this.router.url.includes("/sm/") && !this.router.url.includes("/organizations/") && @@ -146,7 +155,10 @@ export class ProductSwitcherService { name: "Secrets Manager", icon: "bwi-cli", appRoute: ["/sm", smOrg?.id], - marketingRoute: "https://bitwarden.com/products/secrets-manager/", + marketingRoute: { + route: "/sm-landing", + external: false, + }, isActive: this.router.url.includes("/sm/"), otherProductOverrides: { supportingText: this.i18n.transform("secureYourInfrastructure"), @@ -156,7 +168,10 @@ export class ProductSwitcherService { name: "Admin Console", icon: "bwi-business", appRoute: ["/organizations", acOrg?.id], - marketingRoute: "https://bitwarden.com/products/business/", + marketingRoute: { + route: "https://bitwarden.com/products/business/", + external: true, + }, isActive: this.router.url.includes("/organizations/"), }, provider: { @@ -168,7 +183,10 @@ export class ProductSwitcherService { orgs: { name: "Organizations", icon: "bwi-business", - marketingRoute: "https://bitwarden.com/products/business/", + marketingRoute: { + route: "https://bitwarden.com/products/business/", + external: true, + }, otherProductOverrides: { name: "Share your passwords", supportingText: this.i18n.transform("protectYourFamilyOrBusiness"), diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 6b382756f9..034f65366a 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -60,6 +60,8 @@ import { EnvironmentSelectorComponent } from "./components/environment-selector/ import { DataProperties } from "./core"; import { FrontendLayoutComponent } from "./layouts/frontend-layout.component"; import { UserLayoutComponent } from "./layouts/user-layout.component"; +import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component"; +import { SMLandingComponent } from "./secrets-manager/secrets-manager-landing/sm-landing.component"; import { DomainRulesComponent } from "./settings/domain-rules.component"; import { PreferencesComponent } from "./settings/preferences.component"; import { GeneratorComponent } from "./tools/generator.component"; @@ -415,6 +417,16 @@ const routes: Routes = [ component: SendComponent, data: { titleId: "send" } satisfies DataProperties, }, + { + path: "sm-landing", + component: SMLandingComponent, + data: { titleId: "moreProductsFromBitwarden" }, + }, + { + path: "request-sm-access", + component: RequestSMAccessComponent, + data: { titleId: "requestAccessToSecretsManager" }, + }, { path: "create-organization", component: CreateOrganizationComponent, diff --git a/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts new file mode 100644 index 0000000000..fb580b93ee --- /dev/null +++ b/apps/web/src/app/secrets-manager/models/requests/request-sm-access.request.ts @@ -0,0 +1,6 @@ +import { Guid } from "@bitwarden/common/src/types/guid"; + +export class RequestSMAccessRequest { + OrganizationId: Guid; + EmailContent: string; +} diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.html b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.html new file mode 100644 index 0000000000..901ce0d178 --- /dev/null +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.html @@ -0,0 +1,39 @@ + + + +
+
+
+

{{ "youNeedApprovalFromYourAdminToTrySecretsManager" | i18n }}

+ + {{ "addANote" | i18n }} + + + + {{ "organization" | i18n }} + + + + +
+ + +
+
+
+
+
diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts new file mode 100644 index 0000000000..890cbe8ca1 --- /dev/null +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts @@ -0,0 +1,75 @@ +import { Component, OnInit } from "@angular/core"; +import { FormControl, FormGroup, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { Guid } from "@bitwarden/common/types/guid"; +import { NoItemsModule, SearchModule, ToastService } from "@bitwarden/components"; + +import { HeaderModule } from "../../layouts/header/header.module"; +import { OssModule } from "../../oss.module"; +import { SharedModule } from "../../shared/shared.module"; +import { RequestSMAccessRequest } from "../models/requests/request-sm-access.request"; + +import { SmLandingApiService } from "./sm-landing-api.service"; + +@Component({ + selector: "app-request-sm-access", + standalone: true, + templateUrl: "request-sm-access.component.html", + imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule, OssModule], +}) +export class RequestSMAccessComponent implements OnInit { + requestAccessForm = new FormGroup({ + requestAccessEmailContents: new FormControl( + this.i18nService.t("requestAccessSMDefaultEmailContent"), + [Validators.required], + ), + selectedOrganization: new FormControl(null, [Validators.required]), + }); + organizations: Organization[] = []; + + constructor( + private router: Router, + private i18nService: I18nService, + private organizationService: OrganizationService, + private smLandingApiService: SmLandingApiService, + private toastService: ToastService, + ) {} + + async ngOnInit() { + this.organizations = (await this.organizationService.getAll()) + .filter((e) => e.enabled) + .sort((a, b) => a.name.localeCompare(b.name)); + + if (this.organizations === null || this.organizations.length < 1) { + await this.navigateToCreateOrganizationPage(); + } + } + + submit = async () => { + this.requestAccessForm.markAllAsTouched(); + if (this.requestAccessForm.invalid) { + return; + } + + const formValue = this.requestAccessForm.value; + const request = new RequestSMAccessRequest(); + request.OrganizationId = formValue.selectedOrganization.id as Guid; + request.EmailContent = formValue.requestAccessEmailContents; + + await this.smLandingApiService.requestSMAccessFromAdmins(request); + this.toastService.showToast({ + variant: "success", + title: null, + message: this.i18nService.t("smAccessRequestEmailSent"), + }); + await this.router.navigate(["/"]); + }; + + async navigateToCreateOrganizationPage() { + await this.router.navigate(["/create-organization"]); + } +} diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing-api.service.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing-api.service.ts new file mode 100644 index 0000000000..db2b2ee69a --- /dev/null +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing-api.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from "@angular/core"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; + +import { RequestSMAccessRequest } from "../models/requests/request-sm-access.request"; + +@Injectable({ + providedIn: "root", +}) +export class SmLandingApiService { + constructor(private apiService: ApiService) {} + + async requestSMAccessFromAdmins(request: RequestSMAccessRequest): Promise { + await this.apiService.send("POST", "/request-access/request-sm-access", request, true, false); + } +} diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.html b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.html new file mode 100644 index 0000000000..659baa7fde --- /dev/null +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.html @@ -0,0 +1,53 @@ + + +
+
+ +
+
+

{{ "bitwardenSecretsManager" | i18n }}

+ +

+ {{ "developmentDevOpsAndITTeamsChooseBWSecret" | i18n }} +

+
    +
  • + {{ "centralizeSecretsManagement" | i18n }} + {{ "centralizeSecretsManagementDescription" | i18n }} +
  • +
  • + {{ "preventSecretLeaks" | i18n }} {{ "preventSecretLeaksDescription" | i18n }} +
  • +
  • + {{ "enhanceDeveloperProductivity" | i18n }} + {{ "enhanceDeveloperProductivityDescription" | i18n }} +
  • +
  • + {{ "strengthenBusinessSecurity" | i18n }} + {{ "strengthenBusinessSecurityDescription" | i18n }} +
  • +
+
+ +

+ {{ "giveMembersAccess" | i18n }} +

+
    +
  • + {{ "openYourOrganizations" | i18n }} {{ "members" | i18n }} + {{ "viewAndSelectTheMembers" | i18n }} +
  • +
  • + {{ "usingTheMenuSelect" | i18n }} {{ "activateSecretsManager" | i18n }} + {{ "toGrantAccessToSelectedMembers" | i18n }} +
  • +
+
+ + + {{ "learnMore" | i18n }} + +
+
diff --git a/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts new file mode 100644 index 0000000000..392f8403bd --- /dev/null +++ b/apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing.component.ts @@ -0,0 +1,76 @@ +import { Component } from "@angular/core"; + +import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { NoItemsModule, SearchModule } from "@bitwarden/components"; + +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared/shared.module"; + +@Component({ + selector: "app-sm-landing", + standalone: true, + imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule], + templateUrl: "sm-landing.component.html", +}) +export class SMLandingComponent { + tryItNowUrl: string; + learnMoreUrl: string = "https://bitwarden.com/help/secrets-manager-overview/"; + imageSrc: string = "../images/sm.webp"; + showSecretsManagerInformation: boolean = true; + showGiveMembersAccessInstructions: boolean = false; + + constructor(private organizationService: OrganizationService) {} + + async ngOnInit() { + const enabledOrganizations = (await this.organizationService.getAll()).filter((e) => e.enabled); + + if (enabledOrganizations.length > 0) { + this.handleEnabledOrganizations(enabledOrganizations); + } else { + // Person is not part of any orgs they need to be in an organization in order to use SM + this.tryItNowUrl = "/create-organization"; + } + } + + private handleEnabledOrganizations(enabledOrganizations: Organization[]) { + // People get to this page because SM (Secrets Manager) isn't enabled for them (or the Organization they are a part of) + // 1 - SM is enabled for the Organization but not that user + //1a - person is Admin+ (Admin or higher) and just needs instructions on how to enable it for themselves + //1b - person is beneath admin status and needs to request SM access from Administrators/Owners + // 2 - SM is not enabled for the organization yet + //2a - person is Owner/Provider - Direct them to the subscription/billing page + //2b - person is Admin - Direct them to request access page where an email is sent to owner/admins + //2c - person is user - Direct them to request access page where an email is sent to owner/admins + + // We use useSecretsManager because we want to get the first org the person is a part of where SM is enabled but they don't have access enabled yet + const adminPlusNeedsInstructionsToEnableSM = enabledOrganizations.find( + (o) => o.isAdmin && o.useSecretsManager, + ); + const ownerNeedsToEnableSM = enabledOrganizations.find( + (o) => o.isOwner && !o.useSecretsManager, + ); + + // 1a If Organization has SM Enabled, but this logged in person does not have it enabled, but they are admin+ then give them instructions to enable. + if (adminPlusNeedsInstructionsToEnableSM != undefined) { + this.showHowToEnableSMForMembers(adminPlusNeedsInstructionsToEnableSM.id); + } + // 2a Owners can enable SM in the subscription area of Admin Console. + else if (ownerNeedsToEnableSM != undefined) { + this.tryItNowUrl = `/organizations/${ownerNeedsToEnableSM.id}/billing/subscription`; + } + // 1b and 2b 2c, they must be lower than an Owner, and they need access, or want their org to have access to SM. + else { + this.tryItNowUrl = "/request-sm-access"; + } + } + + private showHowToEnableSMForMembers(orgId: string) { + this.showGiveMembersAccessInstructions = true; + this.showSecretsManagerInformation = false; + this.learnMoreUrl = + "https://bitwarden.com/help/secrets-manager-quick-start/#give-members-access"; + this.imageSrc = "../images/sm-give-access.png"; + this.tryItNowUrl = `/organizations/${orgId}/members`; + } +} diff --git a/apps/web/src/images/sm-give-access.png b/apps/web/src/images/sm-give-access.png new file mode 100644 index 0000000000..0bd8a33d10 Binary files /dev/null and b/apps/web/src/images/sm-give-access.png differ diff --git a/apps/web/src/images/sm.webp b/apps/web/src/images/sm.webp new file mode 100644 index 0000000000..c7fb0ebe3f Binary files /dev/null and b/apps/web/src/images/sm.webp differ diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 04121c7894..5fd5065b5b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -4813,6 +4813,75 @@ "message": "or", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more, see how it works, **or** try it now.'" }, + "developmentDevOpsAndITTeamsChooseBWSecret": { + "message": "Development, DevOps, and IT teams choose Bitwarden Secrets Manager to securely manage and deploy their infrastructure and machine secrets." + }, + "centralizeSecretsManagement": { + "message": "Centralize secrets management." + }, + "centralizeSecretsManagementDescription": { + "message": "Securely store and manage secrets in one location to prevent secret sprawl across your organization." + }, + "preventSecretLeaks": { + "message": "Prevent secret leaks." + }, + "preventSecretLeaksDescription": { + "message": "Protect secrets with end-to-end encryption. No more hard coding secrets or sharing through .env files." + }, + "enhanceDeveloperProductivity": { + "message": "Enhance developer productivity." + }, + "enhanceDeveloperProductivityDescription": { + "message": "Programmatically retrieve and deploy secrets at runtime so developers can focus on what matters most, like improving code quality." + }, + "strengthenBusinessSecurity": { + "message": "Strengthen business security." + }, + "strengthenBusinessSecurityDescription": { + "message": "Maintain tight control over machine and human access to secrets with SSO integrations, event logs, and access rotation." + }, + "tryItNow": { + "message": "Try it now" + }, + "sendRequest": { + "message": "Send request" + }, + "addANote": { + "message": "Add a note" + }, + "bitwardenSecretsManager": { + "message": "Bitwarden Secrets Manager" + }, + "moreProductsFromBitwarden": { + "message": "More products from Bitwarden" + }, + "requestAccessToSecretsManager": { + "message": "Request access to Secrets Manager" + }, + "youNeedApprovalFromYourAdminToTrySecretsManager": { + "message": "You need approval from your administrator to try Secrets Manager." + }, + "smAccessRequestEmailSent" : { + "message": "Access request for secrets manager email sent to admins." + }, + "requestAccessSMDefaultEmailContent": { + "message": "Hi,\n\nI am requesting a subscription to Bitwarden Secrets Manager for our team. Your support would mean a great deal!\n\nBitwarden Secrets Manager is an end-to-end encrypted secrets management solution for securely storing, sharing, and deploying machine credentials like API keys, database passwords, and authentication certificates.\n\nSecrets Manager will help us to:\n\n- Improve security\n- Streamline operations\n- Prevent costly secret leaks\n\nTo request a free trial for our team, please reach out to Bitwarden.\n\nThank you for your help!" + }, + "giveMembersAccess": { + "message": "Give members access:" + }, + "viewAndSelectTheMembers" : { + "message" :"view and select the members you want to give access to Secrets Manager." + }, + "openYourOrganizations": { + "message": "Open your organization's" + }, + "usingTheMenuSelect": { + "message": "Using the menu, select" + }, + "toGrantAccessToSelectedMembers": { + "message": "to grant access to selected members." + }, "sendVaultCardTryItNow": { "message": "try it now", "description": "This will be used as part of a larger sentence, broken up to include links. The full sentence will read 'Learn more, see how it works, or **try it now**.'"