Skip to content

Commit

Permalink
[PM-5718] Fix free organization generating TOTP (#11918)
Browse files Browse the repository at this point in the history
* [PM-5718] Fix totp generation for free orgs in old add-edit component

* [PM-5718] Fix totp generation for free orgs in view cipher view component

* [PM-5718] Cleanup merge conflicts

* Don't generate totp code for premium users or free orgs

* Added redirect to organization helper page

* Changed text to learn more

* Only show upgrade message to premium users

* Show upgrade message to free users with free orgs as well

---------

Co-authored-by: Matt Bishop <[email protected]>
Co-authored-by: gbubemismith <[email protected]>
  • Loading branch information
3 people authored Jan 13, 2025
1 parent 3bed613 commit 459fb1b
Show file tree
Hide file tree
Showing 9 changed files with 89 additions and 19 deletions.
14 changes: 14 additions & 0 deletions apps/desktop/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,20 @@ export class AppComponent implements OnInit, OnDestroy {
}
break;
}
case "upgradeOrganization": {
const upgradeConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "upgradeOrganization" },
content: { key: "upgradeOrganizationDesc" },
acceptButtonText: { key: "learnMore" },
type: "info",
});
if (upgradeConfirmed) {
this.platformUtilsService.launchUri(
"https://bitwarden.com/help/upgrade-from-individual-to-org/",
);
}
break;
}
case "emailVerificationRequired": {
const emailVerificationConfirmed = await this.dialogService.openSimpleDialog({
title: { key: "emailVerificationRequired" },
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -3474,5 +3474,14 @@
},
"changeAcctEmail": {
"message": "Change account email"
},
"organizationUpgradeRequired": {
"message": "Organization upgrade required"
},
"upgradeOrganization": {
"message": "Upgrade organization"
},
"upgradeOrganizationDesc": {
"message": "This feature is not available for free organizations. Switch to a paid plan to unlock more features."
}
}
10 changes: 10 additions & 0 deletions apps/desktop/src/vault/app/vault/view.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,16 @@ <h2 class="box-header">
</span>
</div>
</div>
<div class="box-content-row box-content-row-flex totp" *ngIf="showUpgradeRequiredTotp">
<div class="row-main">
<span class="row-label">{{ "verificationCodeTotp" | i18n }}</span>
<span class="row-label">
<a [routerLink]="" (click)="upgradeOrganization()"
>{{ "organizationUpgradeRequired" | i18n }}
</a>
</span>
</div>
</div>
</div>
<!-- Card -->
<div *ngIf="cipher.card">
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/vault/app/vault/view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,10 @@ export class ViewComponent extends BaseViewComponent implements OnInit, OnDestro
this.messagingService.send("premiumRequired");
}
}

upgradeOrganization() {
this.messagingService.send("upgradeOrganization", {
organizationId: this.cipher.organizationId,
});
}
}
5 changes: 3 additions & 2 deletions apps/web/src/app/vault/individual-vault/add-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
protected messagingService: MessagingService,
eventCollectionService: EventCollectionService,
protected policyService: PolicyService,
organizationService: OrganizationService,
protected organizationService: OrganizationService,
logService: LogService,
passwordRepromptService: PasswordRepromptService,
dialogService: DialogService,
Expand Down Expand Up @@ -307,7 +307,8 @@ export class AddEditComponent extends BaseAddEditComponent implements OnInit, On
this.cipher.type === CipherType.Login &&
this.cipher.login.totp &&
this.organization?.productTierType != ProductTierType.Free &&
(this.cipher.organizationUseTotp || this.canAccessPremium)
((this.canAccessPremium && this.cipher.organizationId == null) ||
this.cipher.organizationUseTotp)
);
}

Expand Down
2 changes: 1 addition & 1 deletion libs/angular/src/vault/components/add-edit.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
protected policyService: PolicyService,
protected logService: LogService,
protected passwordRepromptService: PasswordRepromptService,
private organizationService: OrganizationService,
protected organizationService: OrganizationService,
protected dialogService: DialogService,
protected win: Window,
protected datePipe: DatePipe,
Expand Down
16 changes: 10 additions & 6 deletions libs/angular/src/vault/components/view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class ViewComponent implements OnDestroy, OnInit {
showPrivateKey: boolean;
canAccessPremium: boolean;
showPremiumRequiredTotp: boolean;
showUpgradeRequiredTotp: boolean;
totpCode: string;
totpCodeFormatted: string;
totpDash: number;
Expand Down Expand Up @@ -151,22 +152,25 @@ export class ViewComponent implements OnDestroy, OnInit {
this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
);
this.showPremiumRequiredTotp =
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationUseTotp;
this.cipher.login.totp && !this.canAccessPremium && !this.cipher.organizationId;
this.canDeleteCipher$ = this.cipherAuthorizationService.canDeleteCipher$(this.cipher, [
this.collectionId as CollectionId,
]);

this.showUpgradeRequiredTotp =
this.cipher.login.totp && this.cipher.organizationId && !this.cipher.organizationUseTotp;

if (this.cipher.folderId) {
this.folder = await (
await firstValueFrom(this.folderService.folderViews$(activeUserId))
).find((f) => f.id == this.cipher.folderId);
}

if (
this.cipher.type === CipherType.Login &&
this.cipher.login.totp &&
(cipher.organizationUseTotp || this.canAccessPremium)
) {
const canGenerateTotp = this.cipher.organizationId
? this.cipher.organizationUseTotp
: this.canAccessPremium;

if (this.cipher.type === CipherType.Login && this.cipher.login.totp && canGenerateTotp) {
await this.totpUpdateCode();
const interval = this.totpService.getTimeInterval(this.cipher.login.totp);
await this.totpTick(interval);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ <h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
<bit-label [appTextDrag]="totpCodeCopyObj?.totpCode"
>{{ "verificationCodeTotp" | i18n }}
<span
*ngIf="!(isPremium$ | async)"
*ngIf="!(allowTotpGeneration$ | async)"
bitBadge
variant="success"
class="tw-ml-2 tw-cursor-pointer"
Expand All @@ -130,14 +130,14 @@ <h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
id="totp"
readonly
bitInput
[type]="!(isPremium$ | async) ? 'password' : 'text'"
[type]="!(allowTotpGeneration$ | async) ? 'password' : 'text'"
[value]="totpCodeCopyObj?.totpCodeFormatted || '*** ***'"
aria-readonly="true"
data-testid="login-totp"
class="tw-font-mono"
/>
<div
*ngIf="isPremium$ | async"
*ngIf="allowTotpGeneration$ | async"
bitTotpCountdown
[cipher]="cipher"
bitSuffix
Expand All @@ -152,7 +152,7 @@ <h2 bitTypography="h6">{{ "loginCredentials" | i18n }}</h2>
showToast
[appA11yTitle]="'copyVerificationCode' | i18n"
data-testid="copy-totp"
[disabled]="!(isPremium$ | async)"
[disabled]="!(allowTotpGeneration$ | async)"
class="disabled:tw-cursor-default"
></button>
</bit-form-field>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@
// @ts-strict-ignore
import { CommonModule, DatePipe } from "@angular/common";
import { Component, inject, Input } from "@angular/core";
import { Observable, switchMap } from "rxjs";
import {
BehaviorSubject,
combineLatest,
filter,
map,
Observable,
shareReplay,
switchMap,
} from "rxjs";

import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
Expand All @@ -12,13 +20,13 @@ import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
BadgeModule,
ColorPasswordModule,
FormFieldModule,
IconButtonModule,
SectionComponent,
SectionHeaderComponent,
TypographyModule,
IconButtonModule,
BadgeModule,
ColorPasswordModule,
} from "@bitwarden/components";

// FIXME: remove `src` and fix import
Expand Down Expand Up @@ -51,13 +59,31 @@ type TotpCodeValues = {
],
})
export class LoginCredentialsViewComponent {
@Input() cipher: CipherView;
@Input()
get cipher(): CipherView {
return this._cipher$.value;
}
set cipher(value: CipherView) {
this._cipher$.next(value);
}
private _cipher$ = new BehaviorSubject<CipherView>(null);

isPremium$: Observable<boolean> = this.accountService.activeAccount$.pipe(
private _userHasPremium$: Observable<boolean> = this.accountService.activeAccount$.pipe(
switchMap((account) =>
this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id),
),
);

allowTotpGeneration$: Observable<boolean> = combineLatest([
this._userHasPremium$,
this._cipher$.pipe(filter((c) => c != null)),
]).pipe(
map(([userHasPremium, cipher]) => {
// User premium status only applies to personal ciphers, organizationUseTotp applies to organization ciphers
return (userHasPremium && cipher.organizationId == null) || cipher.organizationUseTotp;
}),
shareReplay({ refCount: true, bufferSize: 1 }),
);
showPasswordCount: boolean = false;
passwordRevealed: boolean = false;
totpCodeCopyObj: TotpCodeValues;
Expand Down

0 comments on commit 459fb1b

Please sign in to comment.