mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +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 { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import {
|
import {
|
||||||
getOrganizationById,
|
getOrganizationById,
|
||||||
OrganizationService,
|
InternalOrganizationServiceAbstraction,
|
||||||
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
} 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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import {
|
import {
|
||||||
@@ -196,7 +197,7 @@ export class SsoComponent implements OnInit, OnDestroy {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: InternalOrganizationServiceAbstraction,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
private toastService: ToastService,
|
private toastService: ToastService,
|
||||||
@@ -298,6 +299,8 @@ export class SsoComponent implements OnInit, OnDestroy {
|
|||||||
const response = await this.organizationApiService.updateSso(this.organizationId, request);
|
const response = await this.organizationApiService.updateSso(this.organizationId, request);
|
||||||
this.populateForm(response);
|
this.populateForm(response);
|
||||||
|
|
||||||
|
await this.upsertOrganizationWithSsoChanges(request);
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
title: null,
|
title: null,
|
||||||
@@ -399,4 +402,25 @@ export class SsoComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
document.body.append(div);
|
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,
|
useOrganizationDomains: false,
|
||||||
useAdminSponsoredFamilies: false,
|
useAdminSponsoredFamilies: false,
|
||||||
isAdminInitiated: false,
|
isAdminInitiated: false,
|
||||||
|
ssoEnabled: false,
|
||||||
|
ssoMemberDecryptionType: undefined,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
|
const result = sut.deserializer(JSON.parse(JSON.stringify(expectedResult)));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { MemberDecryptionType } from "../../../auth/enums/sso";
|
||||||
import { ProductTierType } from "../../../billing/enums";
|
import { ProductTierType } from "../../../billing/enums";
|
||||||
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
||||||
import { PermissionsApi } from "../api/permissions.api";
|
import { PermissionsApi } from "../api/permissions.api";
|
||||||
@@ -63,6 +64,8 @@ export class OrganizationData {
|
|||||||
useRiskInsights: boolean;
|
useRiskInsights: boolean;
|
||||||
useAdminSponsoredFamilies: boolean;
|
useAdminSponsoredFamilies: boolean;
|
||||||
isAdminInitiated: boolean;
|
isAdminInitiated: boolean;
|
||||||
|
ssoEnabled: boolean;
|
||||||
|
ssoMemberDecryptionType?: MemberDecryptionType;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
response?: ProfileOrganizationResponse,
|
response?: ProfileOrganizationResponse,
|
||||||
@@ -128,6 +131,8 @@ export class OrganizationData {
|
|||||||
this.useRiskInsights = response.useRiskInsights;
|
this.useRiskInsights = response.useRiskInsights;
|
||||||
this.useAdminSponsoredFamilies = response.useAdminSponsoredFamilies;
|
this.useAdminSponsoredFamilies = response.useAdminSponsoredFamilies;
|
||||||
this.isAdminInitiated = response.isAdminInitiated;
|
this.isAdminInitiated = response.isAdminInitiated;
|
||||||
|
this.ssoEnabled = response.ssoEnabled;
|
||||||
|
this.ssoMemberDecryptionType = response.ssoMemberDecryptionType;
|
||||||
|
|
||||||
this.isMember = options.isMember;
|
this.isMember = options.isMember;
|
||||||
this.isProviderUser = options.isProviderUser;
|
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
|
// @ts-strict-ignore
|
||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { MemberDecryptionType } from "../../../auth/enums/sso";
|
||||||
import { ProductTierType } from "../../../billing/enums";
|
import { ProductTierType } from "../../../billing/enums";
|
||||||
import { OrganizationId } from "../../../types/guid";
|
import { OrganizationId } from "../../../types/guid";
|
||||||
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
||||||
@@ -94,6 +95,8 @@ export class Organization {
|
|||||||
useRiskInsights: boolean;
|
useRiskInsights: boolean;
|
||||||
useAdminSponsoredFamilies: boolean;
|
useAdminSponsoredFamilies: boolean;
|
||||||
isAdminInitiated: boolean;
|
isAdminInitiated: boolean;
|
||||||
|
ssoEnabled: boolean;
|
||||||
|
ssoMemberDecryptionType?: MemberDecryptionType;
|
||||||
|
|
||||||
constructor(obj?: OrganizationData) {
|
constructor(obj?: OrganizationData) {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
@@ -155,6 +158,8 @@ export class Organization {
|
|||||||
this.useRiskInsights = obj.useRiskInsights;
|
this.useRiskInsights = obj.useRiskInsights;
|
||||||
this.useAdminSponsoredFamilies = obj.useAdminSponsoredFamilies;
|
this.useAdminSponsoredFamilies = obj.useAdminSponsoredFamilies;
|
||||||
this.isAdminInitiated = obj.isAdminInitiated;
|
this.isAdminInitiated = obj.isAdminInitiated;
|
||||||
|
this.ssoEnabled = obj.ssoEnabled;
|
||||||
|
this.ssoMemberDecryptionType = obj.ssoMemberDecryptionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
get canAccess() {
|
get canAccess() {
|
||||||
@@ -304,7 +309,12 @@ export class Organization {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get canManageDeviceApprovals() {
|
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() {
|
get isExemptFromPolicies() {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MemberDecryptionType } from "../../../auth/enums/sso";
|
||||||
import { ProductTierType } from "../../../billing/enums";
|
import { ProductTierType } from "../../../billing/enums";
|
||||||
import { BaseResponse } from "../../../models/response/base.response";
|
import { BaseResponse } from "../../../models/response/base.response";
|
||||||
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
import { OrganizationUserStatusType, OrganizationUserType, ProviderType } from "../../enums";
|
||||||
@@ -58,6 +59,8 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
|||||||
useRiskInsights: boolean;
|
useRiskInsights: boolean;
|
||||||
useAdminSponsoredFamilies: boolean;
|
useAdminSponsoredFamilies: boolean;
|
||||||
isAdminInitiated: boolean;
|
isAdminInitiated: boolean;
|
||||||
|
ssoEnabled: boolean;
|
||||||
|
ssoMemberDecryptionType?: MemberDecryptionType;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@@ -127,5 +130,7 @@ export class ProfileOrganizationResponse extends BaseResponse {
|
|||||||
this.useRiskInsights = this.getResponseProperty("UseRiskInsights");
|
this.useRiskInsights = this.getResponseProperty("UseRiskInsights");
|
||||||
this.useAdminSponsoredFamilies = this.getResponseProperty("UseAdminSponsoredFamilies");
|
this.useAdminSponsoredFamilies = this.getResponseProperty("UseAdminSponsoredFamilies");
|
||||||
this.isAdminInitiated = this.getResponseProperty("IsAdminInitiated");
|
this.isAdminInitiated = this.getResponseProperty("IsAdminInitiated");
|
||||||
|
this.ssoEnabled = this.getResponseProperty("SsoEnabled") ?? false;
|
||||||
|
this.ssoMemberDecryptionType = this.getResponseProperty("SsoMemberDecryptionType");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user