[AC-2420] Hide SM checkbox on member invite when org is on SM Standalone (#8644)

* Refactoring

* Hide SM toggle on member invite and default to true for SM standalone org

* changed from hide sm checkbox to default and disable

* Removed errant addition from conflict resolution
This commit is contained in:
Alex Morask 2024-04-18 11:21:11 -04:00 committed by GitHub
parent a45706cde7
commit adb1ee3d38
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 61 additions and 2 deletions

View File

@ -385,7 +385,12 @@
</h3> </h3>
<p class="tw-text-muted">{{ "secretsManagerAccessDescription" | i18n }}</p> <p class="tw-text-muted">{{ "secretsManagerAccessDescription" | i18n }}</p>
<bit-form-control> <bit-form-control>
<input type="checkbox" bitCheckbox formControlName="accessSecretsManager" /> <input
type="checkbox"
[disabled]="isOnSecretsManagerStandalone"
bitCheckbox
formControlName="accessSecretsManager"
/>
<bit-label> <bit-label>
{{ "userAccessSecretsManagerGA" | i18n }} {{ "userAccessSecretsManagerGA" | i18n }}
</bit-label> </bit-label>

View File

@ -63,6 +63,7 @@ export interface MemberDialogParams {
organizationUserId: string; organizationUserId: string;
allOrganizationUserEmails: string[]; allOrganizationUserEmails: string[];
usesKeyConnector: boolean; usesKeyConnector: boolean;
isOnSecretsManagerStandalone: boolean;
initialTab?: MemberDialogTab; initialTab?: MemberDialogTab;
numConfirmedMembers: number; numConfirmedMembers: number;
} }
@ -88,6 +89,7 @@ export class MemberDialogComponent implements OnDestroy {
organizationUserType = OrganizationUserType; organizationUserType = OrganizationUserType;
PermissionMode = PermissionMode; PermissionMode = PermissionMode;
showNoMasterPasswordWarning = false; showNoMasterPasswordWarning = false;
isOnSecretsManagerStandalone: boolean;
protected organization$: Observable<Organization>; protected organization$: Observable<Organization>;
protected collectionAccessItems: AccessItemView[] = []; protected collectionAccessItems: AccessItemView[] = [];
@ -160,6 +162,13 @@ export class MemberDialogComponent implements OnDestroy {
this.editMode = this.params.organizationUserId != null; this.editMode = this.params.organizationUserId != null;
this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role;
this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember"); this.title = this.i18nService.t(this.editMode ? "editMember" : "inviteMember");
this.isOnSecretsManagerStandalone = this.params.isOnSecretsManagerStandalone;
if (this.isOnSecretsManagerStandalone) {
this.formGroup.patchValue({
accessSecretsManager: true,
});
}
const groups$ = this.organization$.pipe( const groups$ = this.organization$.pipe(
switchMap((organization) => switchMap((organization) =>

View File

@ -37,6 +37,7 @@ import {
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request"; import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
import { OrganizationBillingServiceAbstraction as OrganizationBillingService } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { ProductType } from "@bitwarden/common/enums"; import { ProductType } from "@bitwarden/common/enums";
import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ListResponse } from "@bitwarden/common/models/response/list.response";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -93,6 +94,7 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
organization: Organization; organization: Organization;
status: OrganizationUserStatusType = null; status: OrganizationUserStatusType = null;
orgResetPasswordPolicyEnabled = false; orgResetPasswordPolicyEnabled = false;
orgIsOnSecretsManagerStandalone = false;
protected canUseSecretsManager$: Observable<boolean>; protected canUseSecretsManager$: Observable<boolean>;
@ -119,6 +121,7 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
private groupService: GroupService, private groupService: GroupService,
private collectionService: CollectionService, private collectionService: CollectionService,
organizationManagementPreferencesService: OrganizationManagementPreferencesService, organizationManagementPreferencesService: OrganizationManagementPreferencesService,
private organizationBillingService: OrganizationBillingService,
) { ) {
super( super(
apiService, apiService,
@ -187,6 +190,11 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
.find((p) => p.organizationId === this.organization.id); .find((p) => p.organizationId === this.organization.id);
this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled; this.orgResetPasswordPolicyEnabled = resetPasswordPolicy?.enabled;
this.orgIsOnSecretsManagerStandalone =
await this.organizationBillingService.isOnSecretsManagerStandalone(
this.organization.id,
);
await this.load(); await this.load();
this.searchText = qParams.search; this.searchText = qParams.search;
@ -446,6 +454,7 @@ export class PeopleComponent extends BasePeopleComponent<OrganizationUserView> {
organizationUserId: user != null ? user.id : null, organizationUserId: user != null ? user.id : null,
allOrganizationUserEmails: this.allUsers?.map((user) => user.email) ?? [], allOrganizationUserEmails: this.allUsers?.map((user) => user.email) ?? [],
usesKeyConnector: user?.usesKeyConnector, usesKeyConnector: user?.usesKeyConnector,
isOnSecretsManagerStandalone: this.orgIsOnSecretsManagerStandalone,
initialTab: initialTab, initialTab: initialTab,
numConfirmedMembers: this.confirmedCount, numConfirmedMembers: this.confirmedCount,
}, },

View File

@ -1052,6 +1052,7 @@ const safeProviders: SafeProvider[] = [
useClass: OrganizationBillingService, useClass: OrganizationBillingService,
deps: [ deps: [
ApiServiceAbstraction, ApiServiceAbstraction,
BillingApiServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
EncryptService, EncryptService,
I18nServiceAbstraction, I18nServiceAbstraction,

View File

@ -1,5 +1,6 @@
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response";
import { PlanResponse } from "../../billing/models/response/plan.response"; import { PlanResponse } from "../../billing/models/response/plan.response";
import { ListResponse } from "../../models/response/list.response"; import { ListResponse } from "../../models/response/list.response";
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
@ -11,12 +12,16 @@ export abstract class BillingApiServiceAbstraction {
organizationId: string, organizationId: string,
request: SubscriptionCancellationRequest, request: SubscriptionCancellationRequest,
) => Promise<void>; ) => Promise<void>;
cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>; cancelPremiumUserSubscription: (request: SubscriptionCancellationRequest) => Promise<void>;
createClientOrganization: ( createClientOrganization: (
providerId: string, providerId: string,
request: CreateClientOrganizationRequest, request: CreateClientOrganizationRequest,
) => Promise<void>; ) => Promise<void>;
getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>; getBillingStatus: (id: string) => Promise<OrganizationBillingStatusResponse>;
getOrganizationSubscription: (
organizationId: string,
) => Promise<OrganizationSubscriptionResponse>;
getPlans: () => Promise<ListResponse<PlanResponse>>; getPlans: () => Promise<ListResponse<PlanResponse>>;
getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>; getProviderSubscription: (providerId: string) => Promise<ProviderSubscriptionResponse>;
updateClientOrganization: ( updateClientOrganization: (

View File

@ -41,6 +41,8 @@ export type SubscriptionInformation = {
}; };
export abstract class OrganizationBillingServiceAbstraction { export abstract class OrganizationBillingServiceAbstraction {
isOnSecretsManagerStandalone: (organizationId: string) => Promise<boolean>;
purchaseSubscription: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>; purchaseSubscription: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;
startFree: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>; startFree: (subscription: SubscriptionInformation) => Promise<OrganizationResponse>;

View File

@ -2,6 +2,7 @@ import { ApiService } from "../../abstractions/api.service";
import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction"; import { BillingApiServiceAbstraction } from "../../billing/abstractions/billilng-api.service.abstraction";
import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request"; import { SubscriptionCancellationRequest } from "../../billing/models/request/subscription-cancellation.request";
import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response"; import { OrganizationBillingStatusResponse } from "../../billing/models/response/organization-billing-status.response";
import { OrganizationSubscriptionResponse } from "../../billing/models/response/organization-subscription.response";
import { PlanResponse } from "../../billing/models/response/plan.response"; import { PlanResponse } from "../../billing/models/response/plan.response";
import { ListResponse } from "../../models/response/list.response"; import { ListResponse } from "../../models/response/list.response";
import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request"; import { CreateClientOrganizationRequest } from "../models/request/create-client-organization.request";
@ -49,10 +50,22 @@ export class BillingApiService implements BillingApiServiceAbstraction {
true, true,
true, true,
); );
return new OrganizationBillingStatusResponse(r); return new OrganizationBillingStatusResponse(r);
} }
async getOrganizationSubscription(
organizationId: string,
): Promise<OrganizationSubscriptionResponse> {
const r = await this.apiService.send(
"GET",
"/organizations/" + organizationId + "/subscription",
null,
true,
true,
);
return new OrganizationSubscriptionResponse(r);
}
async getPlans(): Promise<ListResponse<PlanResponse>> { async getPlans(): Promise<ListResponse<PlanResponse>> {
const r = await this.apiService.send("GET", "/plans", null, false, true); const r = await this.apiService.send("GET", "/plans", null, false, true);
return new ListResponse(r, PlanResponse); return new ListResponse(r, PlanResponse);

View File

@ -9,6 +9,7 @@ import { I18nService } from "../../platform/abstractions/i18n.service";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { OrgKey } from "../../types/key"; import { OrgKey } from "../../types/key";
import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "../../vault/abstractions/sync/sync.service.abstraction";
import { BillingApiServiceAbstraction as BillingApiService } from "../abstractions/billilng-api.service.abstraction";
import { import {
OrganizationBillingServiceAbstraction, OrganizationBillingServiceAbstraction,
OrganizationInformation, OrganizationInformation,
@ -28,6 +29,7 @@ interface OrganizationKeys {
export class OrganizationBillingService implements OrganizationBillingServiceAbstraction { export class OrganizationBillingService implements OrganizationBillingServiceAbstraction {
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private billingApiService: BillingApiService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private encryptService: EncryptService, private encryptService: EncryptService,
private i18nService: I18nService, private i18nService: I18nService,
@ -35,6 +37,19 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
private syncService: SyncService, private syncService: SyncService,
) {} ) {}
async isOnSecretsManagerStandalone(organizationId: string): Promise<boolean> {
const response = await this.billingApiService.getOrganizationSubscription(organizationId);
if (response.customerDiscount?.id === "sm-standalone") {
const productIds = response.subscription.items.map((item) => item.productId);
return (
response.customerDiscount?.appliesTo.filter((appliesToProductId) =>
productIds.includes(appliesToProductId),
).length > 0
);
}
return false;
}
async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> { async purchaseSubscription(subscription: SubscriptionInformation): Promise<OrganizationResponse> {
const request = new OrganizationCreateRequest(); const request = new OrganizationCreateRequest();