mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 05:13:29 +00:00
[PM-22839] Update Device Approvals visibility based on SSO configuration (#16144)
* Add ssoEnabled and ssoMemberDecryptionType properties to ProfileOrganizationResponse * Add SSO support to Organization model with ssoEnabled and ssoMemberDecryptionType properties, and implement related tests * Upsert organization SSO settings in memory after save Updates organization data in memory with new SSO configuration values to ensure immediate UI updates for Device Approvals page visibility. * Refactor SSO component to simplify upsertOrganizationWithSsoChanges method - Updated the method signature to accept a single OrganizationSsoRequest object instead of separate parameters. - Adjusted the internal logic to directly use properties from the OrganizationSsoRequest for updating the organization state. * Specify OrganizationData type for updatedOrganization in SSO component
This commit is contained in:
@@ -16,8 +16,9 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
InternalOrganizationServiceAbstraction,
|
||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationData } from "@bitwarden/common/admin-console/models/data/organization.data";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import {
|
||||
@@ -196,7 +197,7 @@ export class SsoComponent implements OnInit, OnDestroy {
|
||||
private apiService: ApiService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private i18nService: I18nService,
|
||||
private organizationService: OrganizationService,
|
||||
private organizationService: InternalOrganizationServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private toastService: ToastService,
|
||||
@@ -298,6 +299,8 @@ export class SsoComponent implements OnInit, OnDestroy {
|
||||
const response = await this.organizationApiService.updateSso(this.organizationId, request);
|
||||
this.populateForm(response);
|
||||
|
||||
await this.upsertOrganizationWithSsoChanges(request);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -399,4 +402,25 @@ export class SsoComponent implements OnInit, OnDestroy {
|
||||
|
||||
document.body.append(div);
|
||||
}
|
||||
|
||||
private async upsertOrganizationWithSsoChanges(
|
||||
organizationSsoRequest: OrganizationSsoRequest,
|
||||
): Promise<void> {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
const currentOrganization = await firstValueFrom(
|
||||
this.organizationService
|
||||
.organizations$(userId)
|
||||
.pipe(getOrganizationById(this.organizationId)),
|
||||
);
|
||||
|
||||
if (currentOrganization) {
|
||||
const updatedOrganization: OrganizationData = {
|
||||
...currentOrganization,
|
||||
ssoEnabled: organizationSsoRequest.enabled,
|
||||
ssoMemberDecryptionType: organizationSsoRequest.data.memberDecryptionType,
|
||||
};
|
||||
|
||||
await this.organizationService.upsert(updatedOrganization, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ describe("ORGANIZATIONS state", () => {
|
||||
useOrganizationDomains: false,
|
||||
useAdminSponsoredFamilies: false,
|
||||
isAdminInitiated: false,
|
||||
ssoEnabled: false,
|
||||
ssoMemberDecryptionType: undefined,
|
||||
},
|
||||
};
|
||||
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { MemberDecryptionType } from "../../../auth/enums/sso";
|
||||
import { ProductTierType } from "../../../billing/enums";
|
||||
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
||||
import { PermissionsApi } from "../api/permissions.api";
|
||||
@@ -63,6 +64,8 @@ export class OrganizationData {
|
||||
useRiskInsights: boolean;
|
||||
useAdminSponsoredFamilies: boolean;
|
||||
isAdminInitiated: boolean;
|
||||
ssoEnabled: boolean;
|
||||
ssoMemberDecryptionType?: MemberDecryptionType;
|
||||
|
||||
constructor(
|
||||
response?: ProfileOrganizationResponse,
|
||||
@@ -128,6 +131,8 @@ export class OrganizationData {
|
||||
this.useRiskInsights = response.useRiskInsights;
|
||||
this.useAdminSponsoredFamilies = response.useAdminSponsoredFamilies;
|
||||
this.isAdminInitiated = response.isAdminInitiated;
|
||||
this.ssoEnabled = response.ssoEnabled;
|
||||
this.ssoMemberDecryptionType = response.ssoMemberDecryptionType;
|
||||
|
||||
this.isMember = options.isMember;
|
||||
this.isProviderUser = options.isProviderUser;
|
||||
|
||||
182
libs/common/src/admin-console/models/domain/organization.spec.ts
Normal file
182
libs/common/src/admin-console/models/domain/organization.spec.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { MemberDecryptionType } from "../../../auth/enums/sso";
|
||||
import { ProductTierType } from "../../../billing/enums";
|
||||
import { OrganizationUserStatusType, OrganizationUserType } from "../../enums";
|
||||
import { PermissionsApi } from "../api/permissions.api";
|
||||
import { OrganizationData } from "../data/organization.data";
|
||||
|
||||
import { Organization } from "./organization";
|
||||
|
||||
describe("Organization", () => {
|
||||
let data: OrganizationData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "test-org-id",
|
||||
name: "Test Organization",
|
||||
status: OrganizationUserStatusType.Confirmed,
|
||||
type: OrganizationUserType.Admin,
|
||||
enabled: true,
|
||||
usePolicies: true,
|
||||
useGroups: true,
|
||||
useDirectory: true,
|
||||
useEvents: true,
|
||||
useTotp: true,
|
||||
use2fa: true,
|
||||
useApi: true,
|
||||
useSso: true,
|
||||
useOrganizationDomains: true,
|
||||
useKeyConnector: false,
|
||||
useScim: true,
|
||||
useCustomPermissions: false,
|
||||
useResetPassword: true,
|
||||
useSecretsManager: true,
|
||||
usePasswordManager: true,
|
||||
useActivateAutofillPolicy: false,
|
||||
selfHost: false,
|
||||
usersGetPremium: false,
|
||||
seats: 10,
|
||||
maxCollections: 100,
|
||||
maxStorageGb: 1,
|
||||
ssoBound: false,
|
||||
identifier: "test-identifier",
|
||||
permissions: new PermissionsApi({
|
||||
accessEventLogs: false,
|
||||
accessImportExport: false,
|
||||
accessReports: false,
|
||||
createNewCollections: false,
|
||||
editAnyCollection: false,
|
||||
deleteAnyCollection: false,
|
||||
editAssignedCollections: false,
|
||||
deleteAssignedCollections: false,
|
||||
manageCiphers: false,
|
||||
manageGroups: false,
|
||||
managePolicies: false,
|
||||
manageSso: false,
|
||||
manageUsers: false,
|
||||
manageResetPassword: false,
|
||||
manageScim: false,
|
||||
}),
|
||||
resetPasswordEnrolled: false,
|
||||
userId: "user-id",
|
||||
organizationUserId: "org-user-id",
|
||||
hasPublicAndPrivateKeys: false,
|
||||
providerId: null,
|
||||
providerName: null,
|
||||
providerType: null,
|
||||
isProviderUser: false,
|
||||
isMember: true,
|
||||
familySponsorshipFriendlyName: null,
|
||||
familySponsorshipAvailable: false,
|
||||
productTierType: ProductTierType.Enterprise,
|
||||
keyConnectorEnabled: false,
|
||||
keyConnectorUrl: null,
|
||||
familySponsorshipLastSyncDate: null,
|
||||
familySponsorshipValidUntil: null,
|
||||
familySponsorshipToDelete: null,
|
||||
accessSecretsManager: false,
|
||||
limitCollectionCreation: false,
|
||||
limitCollectionDeletion: false,
|
||||
limitItemDeletion: false,
|
||||
allowAdminAccessToAllCollectionItems: true,
|
||||
userIsManagedByOrganization: false,
|
||||
useRiskInsights: false,
|
||||
useAdminSponsoredFamilies: false,
|
||||
isAdminInitiated: false,
|
||||
ssoEnabled: false,
|
||||
ssoMemberDecryptionType: MemberDecryptionType.MasterPassword,
|
||||
} as OrganizationData;
|
||||
});
|
||||
|
||||
describe("canManageDeviceApprovals", () => {
|
||||
it("should return false when user is not admin and has no manageResetPassword permission", () => {
|
||||
data.type = OrganizationUserType.User;
|
||||
data.useSso = true;
|
||||
data.ssoEnabled = true;
|
||||
data.ssoMemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
data.permissions.manageResetPassword = false;
|
||||
|
||||
const organization = new Organization(data);
|
||||
|
||||
expect(organization.canManageDeviceApprovals).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when useSso is false", () => {
|
||||
data.type = OrganizationUserType.Admin;
|
||||
data.useSso = false;
|
||||
data.ssoEnabled = true;
|
||||
data.ssoMemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
|
||||
const organization = new Organization(data);
|
||||
|
||||
expect(organization.canManageDeviceApprovals).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when ssoEnabled is false", () => {
|
||||
data.type = OrganizationUserType.Admin;
|
||||
data.useSso = true;
|
||||
data.ssoEnabled = false;
|
||||
data.ssoMemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
|
||||
const organization = new Organization(data);
|
||||
|
||||
expect(organization.canManageDeviceApprovals).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when ssoMemberDecryptionType is not TrustedDeviceEncryption", () => {
|
||||
data.type = OrganizationUserType.Admin;
|
||||
data.useSso = true;
|
||||
data.ssoEnabled = true;
|
||||
data.ssoMemberDecryptionType = MemberDecryptionType.MasterPassword;
|
||||
|
||||
const organization = new Organization(data);
|
||||
|
||||
expect(organization.canManageDeviceApprovals).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true when admin has all required SSO settings enabled", () => {
|
||||
data.type = OrganizationUserType.Admin;
|
||||
data.useSso = true;
|
||||
data.ssoEnabled = true;
|
||||
data.ssoMemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
|
||||
const organization = new Organization(data);
|
||||
|
||||
expect(organization.canManageDeviceApprovals).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when owner has all required SSO settings enabled", () => {
|
||||
data.type = OrganizationUserType.Owner;
|
||||
data.useSso = true;
|
||||
data.ssoEnabled = true;
|
||||
data.ssoMemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
|
||||
const organization = new Organization(data);
|
||||
|
||||
expect(organization.canManageDeviceApprovals).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when user has manageResetPassword permission and all SSO settings enabled", () => {
|
||||
data.type = OrganizationUserType.User;
|
||||
data.useSso = true;
|
||||
data.ssoEnabled = true;
|
||||
data.ssoMemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
data.permissions.manageResetPassword = true;
|
||||
|
||||
const organization = new Organization(data);
|
||||
|
||||
expect(organization.canManageDeviceApprovals).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true when provider user has all required SSO settings enabled", () => {
|
||||
data.type = OrganizationUserType.User;
|
||||
data.isProviderUser = true;
|
||||
data.useSso = true;
|
||||
data.ssoEnabled = true;
|
||||
data.ssoMemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption;
|
||||
|
||||
const organization = new Organization(data);
|
||||
|
||||
expect(organization.canManageDeviceApprovals).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { MemberDecryptionType } from "../../../auth/enums/sso";
|
||||
import { ProductTierType } from "../../../billing/enums";
|
||||
import { OrganizationId } from "../../../types/guid";
|
||||
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
||||
@@ -94,6 +95,8 @@ export class Organization {
|
||||
useRiskInsights: boolean;
|
||||
useAdminSponsoredFamilies: boolean;
|
||||
isAdminInitiated: boolean;
|
||||
ssoEnabled: boolean;
|
||||
ssoMemberDecryptionType?: MemberDecryptionType;
|
||||
|
||||
constructor(obj?: OrganizationData) {
|
||||
if (obj == null) {
|
||||
@@ -155,6 +158,8 @@ export class Organization {
|
||||
this.useRiskInsights = obj.useRiskInsights;
|
||||
this.useAdminSponsoredFamilies = obj.useAdminSponsoredFamilies;
|
||||
this.isAdminInitiated = obj.isAdminInitiated;
|
||||
this.ssoEnabled = obj.ssoEnabled;
|
||||
this.ssoMemberDecryptionType = obj.ssoMemberDecryptionType;
|
||||
}
|
||||
|
||||
get canAccess() {
|
||||
@@ -304,7 +309,12 @@ export class Organization {
|
||||
}
|
||||
|
||||
get canManageDeviceApprovals() {
|
||||
return (this.isAdmin || this.permissions.manageResetPassword) && this.useSso;
|
||||
return (
|
||||
(this.isAdmin || this.permissions.manageResetPassword) &&
|
||||
this.useSso &&
|
||||
this.ssoEnabled &&
|
||||
this.ssoMemberDecryptionType === MemberDecryptionType.TrustedDeviceEncryption
|
||||
);
|
||||
}
|
||||
|
||||
get isExemptFromPolicies() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MemberDecryptionType } from "../../../auth/enums/sso";
|
||||
import { ProductTierType } from "../../../billing/enums";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
||||
@@ -58,6 +59,8 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
useRiskInsights: boolean;
|
||||
useAdminSponsoredFamilies: boolean;
|
||||
isAdminInitiated: boolean;
|
||||
ssoEnabled: boolean;
|
||||
ssoMemberDecryptionType?: MemberDecryptionType;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@@ -127,5 +130,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
||||
this.useRiskInsights = this.getResponseProperty("UseRiskInsights");
|
||||
this.useAdminSponsoredFamilies = this.getResponseProperty("UseAdminSponsoredFamilies");
|
||||
this.isAdminInitiated = this.getResponseProperty("IsAdminInitiated");
|
||||
this.ssoEnabled = this.getResponseProperty("SsoEnabled") ?? false;
|
||||
this.ssoMemberDecryptionType = this.getResponseProperty("SsoMemberDecryptionType");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user