mirror of
https://github.com/bitwarden/browser
synced 2026-02-27 01:53:23 +00:00
Merge remote-tracking branch 'origin/PM-14892-Sales-Tax-Estimation-For-Clients' into PM-14892-Sales-Tax-Estimation-For-Clients
This commit is contained in:
@@ -24,6 +24,7 @@ import {
|
||||
} from "../admin-console/models/response/organization-connection.response";
|
||||
import { OrganizationExportResponse } from "../admin-console/models/response/organization-export.response";
|
||||
import { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response";
|
||||
import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response";
|
||||
import {
|
||||
ProviderOrganizationOrganizationDetailsResponse,
|
||||
ProviderOrganizationResponse,
|
||||
@@ -35,7 +36,7 @@ import {
|
||||
ProviderUserUserDetailsResponse,
|
||||
} from "../admin-console/models/response/provider/provider-user.response";
|
||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
||||
import { AuthRequest } from "../auth/models/request/auth.request";
|
||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||
@@ -185,8 +186,8 @@ export abstract class ApiService {
|
||||
putUpdateTdeOffboardingPassword: (request: UpdateTdeOffboardingPasswordRequest) => Promise<any>;
|
||||
postConvertToKeyConnector: () => Promise<void>;
|
||||
//passwordless
|
||||
postAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAdminAuthRequest: (request: CreateAuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||
postAdminAuthRequest: (request: AuthRequest) => Promise<AuthRequestResponse>;
|
||||
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
||||
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
|
||||
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
|
||||
@@ -490,7 +491,9 @@ export abstract class ApiService {
|
||||
) => Promise<OrganizationSponsorshipSyncStatusResponse>;
|
||||
deleteRevokeSponsorship: (sponsoringOrganizationId: string) => Promise<void>;
|
||||
deleteRemoveSponsorship: (sponsoringOrgId: string) => Promise<void>;
|
||||
postPreValidateSponsorshipToken: (sponsorshipToken: string) => Promise<boolean>;
|
||||
postPreValidateSponsorshipToken: (
|
||||
sponsorshipToken: string,
|
||||
) => Promise<PreValidateSponsorshipResponse>;
|
||||
postRedeemSponsorship: (
|
||||
sponsorshipToken: string,
|
||||
request: OrganizationSponsorshipRedeemRequest,
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
|
||||
export function canAccessVaultTab(org: Organization): boolean {
|
||||
return org.canViewAllCollections;
|
||||
}
|
||||
|
||||
export function canAccessSettingsTab(org: Organization): boolean {
|
||||
return (
|
||||
org.isOwner ||
|
||||
org.canManagePolicies ||
|
||||
org.canManageSso ||
|
||||
org.canManageScim ||
|
||||
org.canAccessImportExport ||
|
||||
org.canManageDeviceApprovals
|
||||
);
|
||||
}
|
||||
|
||||
export function canAccessMembersTab(org: Organization): boolean {
|
||||
return org.canManageUsers || org.canManageUsersPassword;
|
||||
}
|
||||
|
||||
export function canAccessGroupsTab(org: Organization): boolean {
|
||||
return org.canManageGroups;
|
||||
}
|
||||
|
||||
export function canAccessReportingTab(org: Organization): boolean {
|
||||
return org.canAccessReports || org.canAccessEventLogs;
|
||||
}
|
||||
|
||||
export function canAccessBillingTab(org: Organization): boolean {
|
||||
return org.isOwner;
|
||||
}
|
||||
|
||||
export function canAccessOrgAdmin(org: Organization): boolean {
|
||||
// Admin console can only be accessed by Owners for disabled organizations
|
||||
if (!org.enabled && !org.isOwner) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
canAccessMembersTab(org) ||
|
||||
canAccessGroupsTab(org) ||
|
||||
canAccessReportingTab(org) ||
|
||||
canAccessBillingTab(org) ||
|
||||
canAccessSettingsTab(org) ||
|
||||
canAccessVaultTab(org)
|
||||
);
|
||||
}
|
||||
|
||||
export function getOrganizationById(id: string) {
|
||||
return map<Organization[], Organization | undefined>((orgs) => orgs.find((o) => o.id === id));
|
||||
}
|
||||
|
||||
export function canAccessAdmin(i18nService: I18nService) {
|
||||
return map<Organization[], Organization[]>((orgs) =>
|
||||
orgs.filter(canAccessOrgAdmin).sort(Utils.getSortFunction(i18nService, "name")),
|
||||
);
|
||||
}
|
||||
|
||||
export function canAccessImport(i18nService: I18nService) {
|
||||
return map<Organization[], Organization[]>((orgs) =>
|
||||
orgs
|
||||
.filter((org) => org.canAccessImportExport || org.canCreateNewCollections)
|
||||
.sort(Utils.getSortFunction(i18nService, "name")),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publishes an observable stream of organizations. This service is meant to
|
||||
* be used widely across Bitwarden as the primary way of fetching organizations.
|
||||
* Risky operations like updates are isolated to the
|
||||
* internal extension `InternalOrganizationServiceAbstraction`.
|
||||
*/
|
||||
export abstract class vNextOrganizationService {
|
||||
/**
|
||||
* Publishes state for all organizations under the specified user.
|
||||
* @returns An observable list of organizations
|
||||
*/
|
||||
organizations$: (userId: UserId) => Observable<Organization[]>;
|
||||
|
||||
// @todo Clean these up. Continuing to expand them is not recommended.
|
||||
// @see https://bitwarden.atlassian.net/browse/AC-2252
|
||||
memberOrganizations$: (userId: UserId) => Observable<Organization[]>;
|
||||
/**
|
||||
* Emits true if the user can create or manage a Free Bitwarden Families sponsorship.
|
||||
*/
|
||||
canManageSponsorships$: (userId: UserId) => Observable<boolean>;
|
||||
/**
|
||||
* Emits true if any of the user's organizations have a Free Bitwarden Families sponsorship available.
|
||||
*/
|
||||
familySponsorshipAvailable$: (userId: UserId) => Observable<boolean>;
|
||||
hasOrganizations: (userId: UserId) => Observable<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Big scary buttons that **update** organization state. These should only be
|
||||
* called from within admin-console scoped code. Extends the base
|
||||
* `OrganizationService` for easy access to `get` calls.
|
||||
* @internal
|
||||
*/
|
||||
export abstract class vNextInternalOrganizationServiceAbstraction extends vNextOrganizationService {
|
||||
/**
|
||||
* Replaces state for the provided organization, or creates it if not found.
|
||||
* @param organization The organization state being saved.
|
||||
* @param userId The userId to replace state for.
|
||||
*/
|
||||
upsert: (OrganizationData: OrganizationData, userId: UserId) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Replaces state for the entire registered organization list for the specified user.
|
||||
* You probably don't want this unless you're calling from a full sync
|
||||
* operation or a logout. See `upsert` for creating & updating a single
|
||||
* organization in the state.
|
||||
* @param organizations A complete list of all organization state for the provided
|
||||
* user.
|
||||
* @param userId The userId to replace state for.
|
||||
*/
|
||||
replace: (organizations: { [id: string]: OrganizationData }, userId: UserId) => Promise<void>;
|
||||
}
|
||||
@@ -12,4 +12,5 @@ export enum PolicyType {
|
||||
DisablePersonalVaultExport = 10, // Disable personal vault export
|
||||
ActivateAutofill = 11, // Activates autofill with page load on the browser extension
|
||||
AutomaticAppLogIn = 12, // Enables automatic log in of apps from configured identity provider
|
||||
FreeFamiliesSponsorshipPolicy = 13, // Disables free families plan for organization
|
||||
}
|
||||
|
||||
@@ -283,9 +283,7 @@ export class Organization {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.hasProvider && this.providerType === ProviderType.Msp
|
||||
? this.isProviderUser
|
||||
: this.isOwner;
|
||||
return this.hasBillableProvider ? this.isProviderUser : this.isOwner;
|
||||
}
|
||||
|
||||
get canEditSubscription() {
|
||||
@@ -304,6 +302,14 @@ export class Organization {
|
||||
return this.providerId != null || this.providerName != null;
|
||||
}
|
||||
|
||||
get hasBillableProvider() {
|
||||
return (
|
||||
this.hasProvider &&
|
||||
(this.providerType === ProviderType.Msp ||
|
||||
this.providerType === ProviderType.MultiOrganizationEnterprise)
|
||||
);
|
||||
}
|
||||
|
||||
get hasReseller() {
|
||||
return this.hasProvider && this.providerType === ProviderType.Reseller;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class OrganizationSponsorshipResponse extends BaseResponse {
|
||||
isPolicyEnabled: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.isPolicyEnabled = this.getResponseProperty("IsPolicyEnabled");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class PreValidateSponsorshipResponse extends BaseResponse {
|
||||
isTokenValid: boolean;
|
||||
isFreeFamilyPolicyEnabled: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.isTokenValid = this.getResponseProperty("IsTokenValid");
|
||||
this.isFreeFamilyPolicyEnabled = this.getResponseProperty("IsFreeFamilyPolicyEnabled");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { OrganizationId, UserId } from "../../../types/guid";
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
|
||||
import { DefaultvNextOrganizationService } from "./default-vnext-organization.service";
|
||||
import { ORGANIZATIONS } from "./vnext-organization.state";
|
||||
|
||||
describe("OrganizationService", () => {
|
||||
let organizationService: DefaultvNextOrganizationService;
|
||||
|
||||
const fakeUserId = Utils.newGuid() as UserId;
|
||||
let fakeStateProvider: FakeStateProvider;
|
||||
|
||||
/**
|
||||
* It is easier to read arrays than records in code, but we store a record
|
||||
* in state. This helper methods lets us build organization arrays in tests
|
||||
* and easily map them to records before storing them in state.
|
||||
*/
|
||||
function arrayToRecord(input: OrganizationData[]): Record<OrganizationId, OrganizationData> {
|
||||
if (input == null) {
|
||||
return undefined;
|
||||
}
|
||||
return Object.fromEntries(input?.map((i) => [i.id, i]));
|
||||
}
|
||||
|
||||
/**
|
||||
* There are a few assertions in this spec that check for array equality
|
||||
* but want to ignore a specific index that _should_ be different. This
|
||||
* function takes two arrays, and an index. It checks for equality of the
|
||||
* arrays, but splices out the specified index from both arrays first.
|
||||
*/
|
||||
function expectIsEqualExceptForIndex(x: any[], y: any[], indexToExclude: number) {
|
||||
// Clone the arrays to avoid modifying the reference values
|
||||
const a = [...x];
|
||||
const b = [...y];
|
||||
delete a[indexToExclude];
|
||||
delete b[indexToExclude];
|
||||
expect(a).toEqual(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a simple mock `OrganizationData[]` array that can be used in tests
|
||||
* to populate state.
|
||||
* @param count The number of organizations to populate the list with. The
|
||||
* function returns undefined if this is less than 1. The default value is 1.
|
||||
* @param suffix A string to append to data fields on each organization.
|
||||
* This defaults to the index of the organization in the list.
|
||||
* @returns an `OrganizationData[]` array that can be used to populate
|
||||
* stateProvider.
|
||||
*/
|
||||
function buildMockOrganizations(count = 1, suffix?: string): OrganizationData[] {
|
||||
if (count < 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function buildMockOrganization(id: OrganizationId, name: string, identifier: string) {
|
||||
const data = new OrganizationData({} as any, {} as any);
|
||||
data.id = id;
|
||||
data.name = name;
|
||||
data.identifier = identifier;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const mockOrganizations = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const s = suffix ? suffix + i.toString() : i.toString();
|
||||
mockOrganizations.push(
|
||||
buildMockOrganization(("org" + s) as OrganizationId, "org" + s, "orgIdentifier" + s),
|
||||
);
|
||||
}
|
||||
|
||||
return mockOrganizations;
|
||||
}
|
||||
|
||||
const setOrganizationsState = (organizationData: OrganizationData[] | null) =>
|
||||
fakeStateProvider.setUserState(
|
||||
ORGANIZATIONS,
|
||||
organizationData == null ? null : arrayToRecord(organizationData),
|
||||
fakeUserId,
|
||||
);
|
||||
|
||||
beforeEach(async () => {
|
||||
fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(fakeUserId));
|
||||
organizationService = new DefaultvNextOrganizationService(fakeStateProvider);
|
||||
});
|
||||
|
||||
describe("canManageSponsorships", () => {
|
||||
it("can because one is available", async () => {
|
||||
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
||||
mockData[0].familySponsorshipAvailable = true;
|
||||
await setOrganizationsState(mockData);
|
||||
const result = await firstValueFrom(organizationService.canManageSponsorships$(fakeUserId));
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("can because one is used", async () => {
|
||||
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
||||
mockData[0].familySponsorshipFriendlyName = "Something";
|
||||
await setOrganizationsState(mockData);
|
||||
const result = await firstValueFrom(organizationService.canManageSponsorships$(fakeUserId));
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("can not because one isn't available or taken", async () => {
|
||||
const mockData: OrganizationData[] = buildMockOrganizations(1);
|
||||
mockData[0].familySponsorshipFriendlyName = null;
|
||||
await setOrganizationsState(mockData);
|
||||
const result = await firstValueFrom(organizationService.canManageSponsorships$(fakeUserId));
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("organizations$", () => {
|
||||
describe("null checking behavior", () => {
|
||||
it("publishes an empty array if organizations in state = undefined", async () => {
|
||||
const mockData: OrganizationData[] = undefined;
|
||||
await setOrganizationsState(mockData);
|
||||
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("publishes an empty array if organizations in state = null", async () => {
|
||||
const mockData: OrganizationData[] = null;
|
||||
await setOrganizationsState(mockData);
|
||||
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("publishes an empty array if organizations in state = []", async () => {
|
||||
const mockData: OrganizationData[] = [];
|
||||
await setOrganizationsState(mockData);
|
||||
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns state for a user", async () => {
|
||||
const mockData = buildMockOrganizations(10);
|
||||
await setOrganizationsState(mockData);
|
||||
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
|
||||
expect(result).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("upsert()", () => {
|
||||
it("can create the organization list if necassary", async () => {
|
||||
// Notice that no default state is provided in this test, so the list in
|
||||
// `stateProvider` will be null when the `upsert` method is called.
|
||||
const mockData = buildMockOrganizations();
|
||||
await organizationService.upsert(mockData[0], fakeUserId);
|
||||
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
|
||||
expect(result).toEqual(mockData.map((x) => new Organization(x)));
|
||||
});
|
||||
|
||||
it("updates an organization that already exists in state", async () => {
|
||||
const mockData = buildMockOrganizations(10);
|
||||
await setOrganizationsState(mockData);
|
||||
const indexToUpdate = 5;
|
||||
const anUpdatedOrganization = {
|
||||
...buildMockOrganizations(1, "UPDATED").pop(),
|
||||
id: mockData[indexToUpdate].id,
|
||||
};
|
||||
await organizationService.upsert(anUpdatedOrganization, fakeUserId);
|
||||
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
|
||||
expect(result[indexToUpdate]).not.toEqual(new Organization(mockData[indexToUpdate]));
|
||||
expect(result[indexToUpdate].id).toEqual(new Organization(mockData[indexToUpdate]).id);
|
||||
expectIsEqualExceptForIndex(
|
||||
result,
|
||||
mockData.map((x) => new Organization(x)),
|
||||
indexToUpdate,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("replace()", () => {
|
||||
it("replaces the entire organization list in state", async () => {
|
||||
const originalData = buildMockOrganizations(10);
|
||||
await setOrganizationsState(originalData);
|
||||
|
||||
const newData = buildMockOrganizations(10, "newData");
|
||||
await organizationService.replace(arrayToRecord(newData), fakeUserId);
|
||||
|
||||
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
|
||||
|
||||
expect(result).toEqual(newData);
|
||||
expect(result).not.toEqual(originalData);
|
||||
});
|
||||
|
||||
// This is more or less a test for logouts
|
||||
it("can replace state with null", async () => {
|
||||
const originalData = buildMockOrganizations(2);
|
||||
await setOrganizationsState(originalData);
|
||||
await organizationService.replace(null, fakeUserId);
|
||||
const result = await firstValueFrom(organizationService.organizations$(fakeUserId));
|
||||
expect(result).toEqual([]);
|
||||
expect(result).not.toEqual(originalData);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { StateProvider } from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { vNextInternalOrganizationServiceAbstraction } from "../../abstractions/organization/vnext.organization.service";
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
import { Organization } from "../../models/domain/organization";
|
||||
|
||||
import { ORGANIZATIONS } from "./vnext-organization.state";
|
||||
|
||||
/**
|
||||
* Filter out organizations from an observable that __do not__ offer a
|
||||
* families-for-enterprise sponsorship to members.
|
||||
* @returns a function that can be used in `Observable<Organization[]>` pipes,
|
||||
* like `organizationService.organizations$`
|
||||
*/
|
||||
function mapToExcludeOrganizationsWithoutFamilySponsorshipSupport() {
|
||||
return map<Organization[], Organization[]>((orgs) => orgs.filter((o) => o.canManageSponsorships));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out organizations from an observable that the organization user
|
||||
* __is not__ a direct member of. This will exclude organizations only
|
||||
* accessible as a provider.
|
||||
* @returns a function that can be used in `Observable<Organization[]>` pipes,
|
||||
* like `organizationService.organizations$`
|
||||
*/
|
||||
function mapToExcludeProviderOrganizations() {
|
||||
return map<Organization[], Organization[]>((orgs) => orgs.filter((o) => o.isMember));
|
||||
}
|
||||
|
||||
/**
|
||||
* Map an observable stream of organizations down to a boolean indicating
|
||||
* if any organizations exist (`orgs.length > 0`).
|
||||
* @returns a function that can be used in `Observable<Organization[]>` pipes,
|
||||
* like `organizationService.organizations$`
|
||||
*/
|
||||
function mapToBooleanHasAnyOrganizations() {
|
||||
return map<Organization[], boolean>((orgs) => orgs.length > 0);
|
||||
}
|
||||
|
||||
export class DefaultvNextOrganizationService
|
||||
implements vNextInternalOrganizationServiceAbstraction
|
||||
{
|
||||
memberOrganizations$(userId: UserId): Observable<Organization[]> {
|
||||
return this.organizations$(userId).pipe(mapToExcludeProviderOrganizations());
|
||||
}
|
||||
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
|
||||
canManageSponsorships$(userId: UserId) {
|
||||
return this.organizations$(userId).pipe(
|
||||
mapToExcludeOrganizationsWithoutFamilySponsorshipSupport(),
|
||||
mapToBooleanHasAnyOrganizations(),
|
||||
);
|
||||
}
|
||||
|
||||
familySponsorshipAvailable$(userId: UserId) {
|
||||
return this.organizations$(userId).pipe(
|
||||
map((orgs) => orgs.some((o) => o.familySponsorshipAvailable)),
|
||||
);
|
||||
}
|
||||
|
||||
hasOrganizations(userId: UserId): Observable<boolean> {
|
||||
return this.organizations$(userId).pipe(mapToBooleanHasAnyOrganizations());
|
||||
}
|
||||
|
||||
async upsert(organization: OrganizationData, userId: UserId): Promise<void> {
|
||||
await this.organizationState(userId).update((existingOrganizations) => {
|
||||
const organizations = existingOrganizations ?? {};
|
||||
organizations[organization.id] = organization;
|
||||
return organizations;
|
||||
});
|
||||
}
|
||||
|
||||
async replace(organizations: { [id: string]: OrganizationData }, userId: UserId): Promise<void> {
|
||||
await this.organizationState(userId).update(() => organizations);
|
||||
}
|
||||
|
||||
organizations$(userId: UserId): Observable<Organization[] | undefined> {
|
||||
return this.organizationState(userId).state$.pipe(this.mapOrganizationRecordToArray());
|
||||
}
|
||||
|
||||
private organizationState(userId: UserId) {
|
||||
return this.stateProvider.getUser(userId, ORGANIZATIONS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a record of `OrganizationData`, which is how we store the
|
||||
* organization list as a JSON object on disk, to an array of
|
||||
* `Organization`, which is how the data is published to callers of the
|
||||
* service.
|
||||
* @returns a function that can be used to pipe organization data from
|
||||
* stored state to an exposed object easily consumable by others.
|
||||
*/
|
||||
private mapOrganizationRecordToArray() {
|
||||
return map<Record<string, OrganizationData>, Organization[]>((orgs) =>
|
||||
Object.values(orgs ?? {})?.map((o) => new Organization(o)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ORGANIZATIONS_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||
|
||||
import { OrganizationData } from "../../models/data/organization.data";
|
||||
|
||||
/**
|
||||
* The `KeyDefinition` for accessing organization lists in application state.
|
||||
* @todo Ideally this wouldn't require a `fromJSON()` call, but `OrganizationData`
|
||||
* has some properties that contain functions. This should probably get
|
||||
* cleaned up.
|
||||
*/
|
||||
export const ORGANIZATIONS = UserKeyDefinition.record<OrganizationData>(
|
||||
ORGANIZATIONS_DISK,
|
||||
"organizations",
|
||||
{
|
||||
deserializer: (obj: Jsonify<OrganizationData>) => OrganizationData.fromJSON(obj),
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AuthRequestType } from "../../enums/auth-request-type";
|
||||
|
||||
export class CreateAuthRequest {
|
||||
export class AuthRequest {
|
||||
constructor(
|
||||
readonly email: string,
|
||||
readonly deviceIdentifier: string,
|
||||
@@ -8,8 +8,8 @@ export class AuthRequestResponse extends BaseResponse {
|
||||
publicKey: string;
|
||||
requestDeviceType: DeviceType;
|
||||
requestIpAddress: string;
|
||||
key: string;
|
||||
masterPasswordHash: string;
|
||||
key: string; // could be either an encrypted MasterKey or an encrypted UserKey
|
||||
masterPasswordHash: string; // if hash is present, the `key` above is an encrypted MasterKey (else `key` is an encrypted UserKey)
|
||||
creationDate: string;
|
||||
requestApproved?: boolean;
|
||||
responseDate?: string;
|
||||
|
||||
@@ -14,14 +14,12 @@ export enum FeatureFlag {
|
||||
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
|
||||
EmailVerification = "email-verification",
|
||||
InlineMenuFieldQualification = "inline-menu-field-qualification",
|
||||
MemberAccessReport = "ac-2059-member-access-report",
|
||||
TwoFactorComponentRefactor = "two-factor-component-refactor",
|
||||
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
|
||||
ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner",
|
||||
VaultBulkManagementAction = "vault-bulk-management-action",
|
||||
IdpAutoSubmitLogin = "idp-auto-submit-login",
|
||||
UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh",
|
||||
EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub",
|
||||
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
|
||||
EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill",
|
||||
DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2",
|
||||
@@ -41,6 +39,7 @@ export enum FeatureFlag {
|
||||
SecurityTasks = "security-tasks",
|
||||
NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss",
|
||||
NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss",
|
||||
DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -65,14 +64,12 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
|
||||
[FeatureFlag.EmailVerification]: FALSE,
|
||||
[FeatureFlag.InlineMenuFieldQualification]: FALSE,
|
||||
[FeatureFlag.MemberAccessReport]: FALSE,
|
||||
[FeatureFlag.TwoFactorComponentRefactor]: FALSE,
|
||||
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
|
||||
[FeatureFlag.ProviderClientVaultPrivacyBanner]: FALSE,
|
||||
[FeatureFlag.VaultBulkManagementAction]: FALSE,
|
||||
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
|
||||
[FeatureFlag.UnauthenticatedExtensionUIRefresh]: FALSE,
|
||||
[FeatureFlag.EnableUpgradePasswordManagerSub]: FALSE,
|
||||
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
|
||||
[FeatureFlag.EnableNewCardCombinedExpiryAutofill]: FALSE,
|
||||
[FeatureFlag.DelayFido2PageScriptInitWithinMv2]: FALSE,
|
||||
@@ -92,6 +89,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.SecurityTasks]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationTemporaryDismiss]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerificationPermanentDismiss]: FALSE,
|
||||
[FeatureFlag.DisableFreeFamiliesSponsorship]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -10,6 +10,11 @@ export abstract class SdkService {
|
||||
*/
|
||||
supported$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Retrieve the version of the SDK.
|
||||
*/
|
||||
version$: Observable<string>;
|
||||
|
||||
/**
|
||||
* Retrieve a client initialized without a user.
|
||||
* This client can only be used for operations that don't require a user context.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// required to avoid linting errors when there are no flags
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export type SharedFlags = {
|
||||
showPasswordless?: boolean;
|
||||
sdk?: boolean;
|
||||
prereleaseBuild?: boolean;
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
distinctUntilChanged,
|
||||
tap,
|
||||
switchMap,
|
||||
catchError,
|
||||
} from "rxjs";
|
||||
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -51,6 +52,11 @@ export class DefaultSdkService implements SdkService {
|
||||
}),
|
||||
);
|
||||
|
||||
version$ = this.client$.pipe(
|
||||
map((client) => client.version()),
|
||||
catchError(() => "Unsupported"),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private sdkClientFactory: SdkClientFactory,
|
||||
private environmentService: EnvironmentService,
|
||||
|
||||
@@ -173,3 +173,7 @@ export const PREMIUM_BANNER_DISK_LOCAL = new StateDefinition("premiumBannerRepro
|
||||
});
|
||||
export const BANNERS_DISMISSED_DISK = new StateDefinition("bannersDismissed", "disk");
|
||||
export const VAULT_BROWSER_UI_ONBOARDING = new StateDefinition("vaultBrowserUiOnboarding", "disk");
|
||||
export const NEW_DEVICE_VERIFICATION_NOTICE = new StateDefinition(
|
||||
"newDeviceVerificationNotice",
|
||||
"disk",
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
} from "../admin-console/models/response/organization-connection.response";
|
||||
import { OrganizationExportResponse } from "../admin-console/models/response/organization-export.response";
|
||||
import { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response";
|
||||
import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response";
|
||||
import {
|
||||
ProviderOrganizationOrganizationDetailsResponse,
|
||||
ProviderOrganizationResponse,
|
||||
@@ -41,7 +42,7 @@ import {
|
||||
} from "../admin-console/models/response/provider/provider-user.response";
|
||||
import { SelectionReadOnlyResponse } from "../admin-console/models/response/selection-read-only.response";
|
||||
import { TokenService } from "../auth/abstractions/token.service";
|
||||
import { CreateAuthRequest } from "../auth/models/request/create-auth.request";
|
||||
import { AuthRequest } from "../auth/models/request/auth.request";
|
||||
import { DeviceVerificationRequest } from "../auth/models/request/device-verification.request";
|
||||
import { DisableTwoFactorAuthenticatorRequest } from "../auth/models/request/disable-two-factor-authenticator.request";
|
||||
import { EmailTokenRequest } from "../auth/models/request/email-token.request";
|
||||
@@ -259,11 +260,12 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
}
|
||||
|
||||
// TODO: PM-3519: Create and move to AuthRequest Api service
|
||||
async postAuthRequest(request: CreateAuthRequest): Promise<AuthRequestResponse> {
|
||||
// TODO: PM-9724: Remove legacy auth request methods when we remove legacy LoginViaAuthRequestV1Components
|
||||
async postAuthRequest(request: AuthRequest): Promise<AuthRequestResponse> {
|
||||
const r = await this.send("POST", "/auth-requests/", request, false, true);
|
||||
return new AuthRequestResponse(r);
|
||||
}
|
||||
async postAdminAuthRequest(request: CreateAuthRequest): Promise<AuthRequestResponse> {
|
||||
async postAdminAuthRequest(request: AuthRequest): Promise<AuthRequestResponse> {
|
||||
const r = await this.send("POST", "/auth-requests/admin-request", request, true, true);
|
||||
return new AuthRequestResponse(r);
|
||||
}
|
||||
@@ -1680,8 +1682,10 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
);
|
||||
}
|
||||
|
||||
async postPreValidateSponsorshipToken(sponsorshipToken: string): Promise<boolean> {
|
||||
const r = await this.send(
|
||||
async postPreValidateSponsorshipToken(
|
||||
sponsorshipToken: string,
|
||||
): Promise<PreValidateSponsorshipResponse> {
|
||||
const response = await this.send(
|
||||
"POST",
|
||||
"/organization/sponsorship/validate-token?sponsorshipToken=" +
|
||||
encodeURIComponent(sponsorshipToken),
|
||||
@@ -1689,7 +1693,8 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
true,
|
||||
true,
|
||||
);
|
||||
return r as boolean;
|
||||
|
||||
return new PreValidateSponsorshipResponse(response);
|
||||
}
|
||||
|
||||
async postRedeemSponsorship(
|
||||
|
||||
@@ -28,6 +28,11 @@ type NumberConstraints = {
|
||||
/** maximum number value. When absent, min value is unbounded. */
|
||||
max?: number;
|
||||
|
||||
/** recommended value. This is the value bitwarden recommends
|
||||
* to the user as an appropriate value.
|
||||
*/
|
||||
recommendation?: number;
|
||||
|
||||
/** requires the number be a multiple of the step value;
|
||||
* this field must be a positive number. +0 and Infinity are
|
||||
* prohibited. When absent, any number is accepted.
|
||||
|
||||
@@ -17,6 +17,10 @@ export class SshKeyView extends ItemView {
|
||||
}
|
||||
|
||||
get maskedPrivateKey(): string {
|
||||
if (!this.privateKey || this.privateKey.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let lines = this.privateKey.split("\n").filter((l) => l.trim() !== "");
|
||||
lines = lines.map((l, i) => {
|
||||
if (i === 0 || i === lines.length - 1) {
|
||||
|
||||
Reference in New Issue
Block a user