Creating a landing page for SM, where user can request access from ad… (#9504)

* Creating a landing page for SM, where user can request access from admins

* moving files to better folder, also fixing UI

* updating file paths

* cleaning up the code

* Updating API request to be the new one, and fixing HTML

* Adding coowners

* Updating OrganizaitonId in the request model to be a Guid

* Update apps/web/src/app/secrets-manager/secrets-manager-landing/request-sm-access.component.ts

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* Update apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Update apps/web/src/app/layouts/product-switcher/navigation-switcher/navigation-switcher.component.ts

Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>

* Suggested changes from Maceij and Thomas

* fixing merge issues

* fixing issues

* Fixing logic to match top bar

* updating file name to not start with a capital letter

* renaming folder

* updating names

* Getting around the lint issue

* fixing lint issues

* Changes requested by Vicky

* Maciej suggested changes

* Fixing comments

* Update apps/web/src/app/secrets-manager/secrets-manager-landing/sm-landing-api.service.ts

Thomas's suggested improvement

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>

* ui fixes

* New awesome changes, to include the scenario where a Provider user is logged in, and to handle if an admin needs instructions to enable SM for themselves

* renaming fuctions and variables

---------

Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
Co-authored-by: Maciej Zieniuk <167752252+mzieniukbw@users.noreply.github.com>
This commit is contained in:
cd-bitwarden 2024-07-25 11:03:57 -04:00 committed by GitHub
parent f4023762a8
commit 5180ec44e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 469 additions and 37 deletions

1
.github/CODEOWNERS vendored
View File

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

View File

@ -15,21 +15,45 @@
class="tw-mt-2 tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-0"
>
<span class="tw-text-xs !tw-text-alt2 tw-p-2 tw-pb-0">{{ "moreFromBitwarden" | i18n }}</span>
<a
*ngFor="let more of moreProducts"
[href]="more.marketingRoute"
target="_blank"
rel="noreferrer"
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
<div>
{{ more.otherProductOverrides?.name ?? more.name }}
<div *ngIf="more.otherProductOverrides?.supportingText" class="tw-text-xs tw-font-normal">
{{ more.otherProductOverrides.supportingText }}
<ng-container *ngFor="let more of moreProducts">
<!-- <a> for when the marketing route is external -->
<a
*ngIf="more.marketingRoute.external"
[href]="more.marketingRoute.route"
target="_blank"
rel="noreferrer"
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
<div>
{{ more.otherProductOverrides?.name ?? more.name }}
<div
*ngIf="more.otherProductOverrides?.supportingText"
class="tw-text-xs tw-font-normal"
>
{{ more.otherProductOverrides.supportingText }}
</div>
</div>
</div>
</a>
</a>
<!-- <a> for when the marketing route is internal, it needs to use [routerLink] instead of [href] like the external <a> uses. -->
<a
*ngIf="!more.marketingRoute.external"
[routerLink]="more.marketingRoute.route"
rel="noreferrer"
class="tw-flex tw-py-2 tw-px-4 tw-font-semibold !tw-text-alt2 !tw-no-underline hover:tw-bg-primary-300/60 [&>:not(.bwi)]:hover:tw-underline"
>
<i class="bwi bwi-fw {{ more.icon }} tw-mt-1 tw-mx-1"></i>
<div>
{{ more.otherProductOverrides?.name ?? more.name }}
<div
*ngIf="more.otherProductOverrides?.supportingText"
class="tw-text-xs tw-font-normal"
>
{{ more.otherProductOverrides.supportingText }}
</div>
</div>
</a>
</ng-container>
</section>
</ng-container>
</div>

View File

@ -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,
},
],

View File

@ -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"
>
<span class="tw-mb-1 tw-text-xs tw-text-muted">{{ "moreFromBitwarden" | i18n }}</span>
<a
*ngFor="let product of products.other"
bitLink
[href]="product.marketingRoute"
target="_blank"
rel="noreferrer"
>
<span class="tw-flex tw-items-center tw-font-normal">
<i class="bwi bwi-fw {{ product.icon }} tw-m-0 !tw-mr-3"></i>{{ product.name }}
</span>
</a>
<span *ngFor="let product of products.other">
<!-- <a> for when the marketing route is internal, it needs to use [routerLink] instead of [href] like the external <a> uses. -->
<a
*ngIf="!product.marketingRoute.external"
bitLink
[routerLink]="product.marketingRoute.route"
>
<span class="tw-flex tw-items-center tw-font-normal">
<i class="bwi bwi-fw {{ product.icon }} tw-m-0 !tw-mr-3"></i>{{ product.name }}
</span>
</a>
<!-- <a> for when the marketing route is external -->
<a
*ngIf="product.marketingRoute.external"
bitLink
[href]="product.marketingRoute.route"
target="_blank"
rel="noreferrer"
>
<span class="tw-flex tw-items-center tw-font-normal">
<i class="bwi bwi-fw {{ product.icon }} tw-m-0 !tw-mr-3"></i>{{ product.name }}
</span>
</a>
</span>
</section>
</div>
</bit-menu>

View File

@ -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"),

View File

@ -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,

View File

@ -0,0 +1,6 @@
import { Guid } from "@bitwarden/common/src/types/guid";
export class RequestSMAccessRequest {
OrganizationId: Guid;
EmailContent: string;
}

View File

@ -0,0 +1,39 @@
<app-header></app-header>
<bit-container>
<form [formGroup]="requestAccessForm" [bitSubmit]="submit">
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
<div class="tw-col-span-9">
<p bitTypography="body1">{{ "youNeedApprovalFromYourAdminToTrySecretsManager" | i18n }}</p>
<bit-form-field>
<bit-label>{{ "addANote" | i18n }}</bit-label>
<textarea
rows="20"
id="request_access_textarea"
bitInput
formControlName="requestAccessEmailContents"
></textarea>
</bit-form-field>
<bit-form-field>
<bit-label>{{ "organization" | i18n }}</bit-label>
<bit-select formControlName="selectedOrganization">
<bit-option
*ngFor="let org of organizations"
[value]="org"
[label]="org.name"
required
></bit-option>
</bit-select>
</bit-form-field>
<div class="tw-flex tw-gap-x-4 tw-mt-4">
<button bitButton bitFormButton type="submit" buttonType="primary">
{{ "sendRequest" | i18n }}
</button>
<button type="button" bitButton buttonType="secondary" [routerLink]="'/sm-landing'">
{{ "cancel" | i18n }}
</button>
</div>
</div>
</div>
</form>
</bit-container>

View File

@ -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<Organization>(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"]);
}
}

View File

@ -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<void> {
await this.apiService.send("POST", "/request-access/request-sm-access", request, true, false);
}
}

View File

@ -0,0 +1,53 @@
<app-header></app-header>
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
<div class="tw-col-span-6">
<img [src]="imageSrc" class="tw-max-w-full" alt="Bitwarden" aria-hidden="true" />
</div>
<div class="tw-col-span-6 tw-mx-4">
<h1 bitTypography="h1">{{ "bitwardenSecretsManager" | i18n }}</h1>
<bit-container *ngIf="this.showSecretsManagerInformation">
<p bitTypography="body1">
{{ "developmentDevOpsAndITTeamsChooseBWSecret" | i18n }}
</p>
<ul class="tw-list-outside">
<li bitTypography="body1" class="tw-mb-2">
<b>{{ "centralizeSecretsManagement" | i18n }}</b>
{{ "centralizeSecretsManagementDescription" | i18n }}
</li>
<li bitTypography="body1" class="tw-mb-2">
<b>{{ "preventSecretLeaks" | i18n }}</b> {{ "preventSecretLeaksDescription" | i18n }}
</li>
<li bitTypography="body1" class="tw-mb-2">
<b>{{ "enhanceDeveloperProductivity" | i18n }}</b>
{{ "enhanceDeveloperProductivityDescription" | i18n }}
</li>
<li bitTypography="body1" class="tw-mb-2">
<b>{{ "strengthenBusinessSecurity" | i18n }}</b>
{{ "strengthenBusinessSecurityDescription" | i18n }}
</li>
</ul>
</bit-container>
<bit-container *ngIf="this.showGiveMembersAccessInstructions">
<p bitTypography="body1">
{{ "giveMembersAccess" | i18n }}
</p>
<ul class="tw-list-outside">
<li bitTypography="body1" class="tw-mb-2">
{{ "openYourOrganizations" | i18n }} <b>{{ "members" | i18n }}</b>
{{ "viewAndSelectTheMembers" | i18n }}
</li>
<li bitTypography="body1" class="tw-mb-2">
{{ "usingTheMenuSelect" | i18n }} <b>{{ "activateSecretsManager" | i18n }}</b>
{{ "toGrantAccessToSelectedMembers" | i18n }}
</li>
</ul>
</bit-container>
<button type="button" bitButton buttonType="primary" [routerLink]="tryItNowUrl">
{{ "tryItNow" | i18n }}
</button>
<a bitLink linkType="primary" [href]="learnMoreUrl" target="_blank" class="tw-m-5">
{{ "learnMore" | i18n }}
</a>
</div>
</div>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

BIN
apps/web/src/images/sm.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -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**.'"