mirror of
https://github.com/bitwarden/browser
synced 2026-03-01 02:51:24 +00:00
Merge branch 'main' into auth/pm-26209/bugfix-desktop-error-on-auth-request-approval
This commit is contained in:
@@ -794,6 +794,9 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
case "viewEvents":
|
||||
await this.viewEvents(event.item);
|
||||
break;
|
||||
case "editCipher":
|
||||
await this.editCipher(event.item);
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
this.processingEvent$.next(false);
|
||||
@@ -856,7 +859,7 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
* @param cipherView - When set, the cipher to be edited
|
||||
* @param cloneCipher - `true` when the cipher should be cloned.
|
||||
*/
|
||||
async editCipher(cipher: CipherView | undefined, cloneCipher: boolean) {
|
||||
async editCipher(cipher: CipherView | undefined, cloneCipher?: boolean) {
|
||||
if (
|
||||
cipher &&
|
||||
cipher.reprompt !== 0 &&
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import {
|
||||
CollectionService,
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserUserDetailsResponse,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { GroupApiService } from "../../../core";
|
||||
|
||||
@@ -18,6 +22,9 @@ describe("OrganizationMembersService", () => {
|
||||
let organizationUserApiService: jest.Mocked<OrganizationUserApiService>;
|
||||
let groupService: jest.Mocked<GroupApiService>;
|
||||
let apiService: jest.Mocked<ApiService>;
|
||||
let keyService: jest.Mocked<KeyService>;
|
||||
let accountService: jest.Mocked<AccountService>;
|
||||
let collectionService: jest.Mocked<CollectionService>;
|
||||
|
||||
const mockOrganizationId = "org-123" as OrganizationId;
|
||||
|
||||
@@ -51,6 +58,7 @@ describe("OrganizationMembersService", () => {
|
||||
const createMockCollection = (id: string, name: string) => ({
|
||||
id,
|
||||
name,
|
||||
organizationId: mockOrganizationId,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -66,12 +74,27 @@ describe("OrganizationMembersService", () => {
|
||||
getCollections: jest.fn(),
|
||||
} as any;
|
||||
|
||||
keyService = {
|
||||
orgKeys$: jest.fn(),
|
||||
} as any;
|
||||
|
||||
accountService = {
|
||||
activeAccount$: of({ id: "user-123" } as any),
|
||||
} as any;
|
||||
|
||||
collectionService = {
|
||||
decryptMany$: jest.fn(),
|
||||
} as any;
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
OrganizationMembersService,
|
||||
{ provide: OrganizationUserApiService, useValue: organizationUserApiService },
|
||||
{ provide: GroupApiService, useValue: groupService },
|
||||
{ provide: ApiService, useValue: apiService },
|
||||
{ provide: KeyService, useValue: keyService },
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: CollectionService, useValue: collectionService },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -88,11 +111,15 @@ describe("OrganizationMembersService", () => {
|
||||
data: [mockUser],
|
||||
} as any;
|
||||
const mockCollections = [createMockCollection("col-1", "Collection 1")];
|
||||
const mockOrgKey = { [mockOrganizationId]: {} as any };
|
||||
const mockDecryptedCollections = [{ id: "col-1", name: "Collection 1" }];
|
||||
|
||||
organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse);
|
||||
apiService.getCollections.mockResolvedValue({
|
||||
data: mockCollections,
|
||||
} as any);
|
||||
keyService.orgKeys$.mockReturnValue(of(mockOrgKey));
|
||||
collectionService.decryptMany$.mockReturnValue(of(mockDecryptedCollections as any));
|
||||
|
||||
const result = await service.loadUsers(organization);
|
||||
|
||||
@@ -171,11 +198,19 @@ describe("OrganizationMembersService", () => {
|
||||
createMockCollection("col-2", "Alpha Collection"),
|
||||
createMockCollection("col-3", "Beta Collection"),
|
||||
];
|
||||
const mockOrgKey = { [mockOrganizationId]: {} as any };
|
||||
const mockDecryptedCollections = [
|
||||
{ id: "col-1", name: "Zebra Collection" },
|
||||
{ id: "col-2", name: "Alpha Collection" },
|
||||
{ id: "col-3", name: "Beta Collection" },
|
||||
];
|
||||
|
||||
organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse);
|
||||
apiService.getCollections.mockResolvedValue({
|
||||
data: mockCollections,
|
||||
} as any);
|
||||
keyService.orgKeys$.mockReturnValue(of(mockOrgKey));
|
||||
collectionService.decryptMany$.mockReturnValue(of(mockDecryptedCollections as any));
|
||||
|
||||
const result = await service.loadUsers(organization);
|
||||
|
||||
@@ -223,11 +258,19 @@ describe("OrganizationMembersService", () => {
|
||||
// col-2 is missing - should be filtered out
|
||||
createMockCollection("col-3", "Collection 3"),
|
||||
];
|
||||
const mockOrgKey = { [mockOrganizationId]: {} as any };
|
||||
const mockDecryptedCollections = [
|
||||
{ id: "col-1", name: "Collection 1" },
|
||||
// col-2 is missing - should be filtered out
|
||||
{ id: "col-3", name: "Collection 3" },
|
||||
];
|
||||
|
||||
organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse);
|
||||
apiService.getCollections.mockResolvedValue({
|
||||
data: mockCollections,
|
||||
} as any);
|
||||
keyService.orgKeys$.mockReturnValue(of(mockOrgKey));
|
||||
collectionService.decryptMany$.mockReturnValue(of(mockDecryptedCollections as any));
|
||||
|
||||
const result = await service.loadUsers(organization);
|
||||
|
||||
@@ -269,11 +312,14 @@ describe("OrganizationMembersService", () => {
|
||||
const mockUsersResponse: ListResponse<OrganizationUserUserDetailsResponse> = {
|
||||
data: null as any,
|
||||
} as any;
|
||||
const mockOrgKey = { [mockOrganizationId]: {} as any };
|
||||
|
||||
organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse);
|
||||
apiService.getCollections.mockResolvedValue({
|
||||
data: [],
|
||||
} as any);
|
||||
keyService.orgKeys$.mockReturnValue(of(mockOrgKey));
|
||||
collectionService.decryptMany$.mockReturnValue(of([]));
|
||||
|
||||
const result = await service.loadUsers(organization);
|
||||
|
||||
@@ -285,11 +331,14 @@ describe("OrganizationMembersService", () => {
|
||||
const mockUsersResponse: ListResponse<OrganizationUserUserDetailsResponse> = {
|
||||
data: undefined as any,
|
||||
} as any;
|
||||
const mockOrgKey = { [mockOrganizationId]: {} as any };
|
||||
|
||||
organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse);
|
||||
apiService.getCollections.mockResolvedValue({
|
||||
data: [],
|
||||
} as any);
|
||||
keyService.orgKeys$.mockReturnValue(of(mockOrgKey));
|
||||
collectionService.decryptMany$.mockReturnValue(of([]));
|
||||
|
||||
const result = await service.loadUsers(organization);
|
||||
|
||||
@@ -322,11 +371,14 @@ describe("OrganizationMembersService", () => {
|
||||
const mockUsersResponse: ListResponse<OrganizationUserUserDetailsResponse> = {
|
||||
data: [mockUser],
|
||||
} as any;
|
||||
const mockOrgKey = { [mockOrganizationId]: {} as any };
|
||||
|
||||
organizationUserApiService.getAllUsers.mockResolvedValue(mockUsersResponse);
|
||||
apiService.getCollections.mockResolvedValue({
|
||||
data: [],
|
||||
} as any);
|
||||
keyService.orgKeys$.mockReturnValue(of(mockOrgKey));
|
||||
collectionService.decryptMany$.mockReturnValue(of([]));
|
||||
|
||||
const result = await service.loadUsers(organization);
|
||||
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { combineLatest, firstValueFrom, from, map, switchMap } from "rxjs";
|
||||
|
||||
import { OrganizationUserApiService } from "@bitwarden/admin-console/common";
|
||||
import {
|
||||
Collection,
|
||||
CollectionData,
|
||||
CollectionDetailsResponse,
|
||||
CollectionService,
|
||||
OrganizationUserApiService,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { GroupApiService } from "../../../core";
|
||||
import { OrganizationUserView } from "../../../core/views/organization-user.view";
|
||||
@@ -13,6 +23,9 @@ export class OrganizationMembersService {
|
||||
private organizationUserApiService: OrganizationUserApiService,
|
||||
private groupService: GroupApiService,
|
||||
private apiService: ApiService,
|
||||
private keyService: KeyService,
|
||||
private accountService: AccountService,
|
||||
private collectionService: CollectionService,
|
||||
) {}
|
||||
|
||||
async loadUsers(organization: Organization): Promise<OrganizationUserView[]> {
|
||||
@@ -62,15 +75,38 @@ export class OrganizationMembersService {
|
||||
}
|
||||
|
||||
private async getCollectionNameMap(organization: Organization): Promise<Map<string, string>> {
|
||||
const response = this.apiService
|
||||
.getCollections(organization.id)
|
||||
.then((res) =>
|
||||
res.data.map((r: { id: string; name: string }) => ({ id: r.id, name: r.name })),
|
||||
);
|
||||
const collections$ = from(this.apiService.getCollections(organization.id)).pipe(
|
||||
map((response) => {
|
||||
return response.data.map((r) =>
|
||||
Collection.fromCollectionData(new CollectionData(r as CollectionDetailsResponse)),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const collections = await response;
|
||||
const collectionMap = new Map<string, string>();
|
||||
collections.forEach((c: { id: string; name: string }) => collectionMap.set(c.id, c.name));
|
||||
return collectionMap;
|
||||
const orgKey$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((orgKeys) => {
|
||||
if (orgKeys == null) {
|
||||
throw new Error("Organization keys not found for provided User.");
|
||||
}
|
||||
return orgKeys;
|
||||
}),
|
||||
);
|
||||
|
||||
return await firstValueFrom(
|
||||
combineLatest([orgKey$, collections$]).pipe(
|
||||
switchMap(([orgKey, collections]) =>
|
||||
this.collectionService.decryptMany$(collections, orgKey),
|
||||
),
|
||||
map((decryptedCollections) => {
|
||||
const collectionMap: Map<string, string> = new Map<string, string>();
|
||||
decryptedCollections.forEach((c) => {
|
||||
collectionMap.set(c.id, c.name);
|
||||
});
|
||||
return collectionMap;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
||||
const BroadcasterSubscriptionId = "AppComponent";
|
||||
const IdleTimeout = 60000 * 10; // 10 minutes
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-root",
|
||||
templateUrl: "app.component.html",
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<billing-pricing-card
|
||||
[tagline]="'planDescFamiliesV2' | i18n"
|
||||
[price]="{ amount: familiesData.price, cadence: 'monthly' }"
|
||||
[button]="{ type: 'secondary', text: ('upgradeToFamilies' | i18n) }"
|
||||
[button]="{ type: 'secondary', text: ('startFreeFamiliesTrial' | i18n) }"
|
||||
[features]="familiesData.features"
|
||||
(buttonClick)="openUpgradeDialog('Families')"
|
||||
>
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("UpgradeAccountComponent", () => {
|
||||
expect(sut["familiesCardDetails"].price.amount).toBe(40 / 12);
|
||||
expect(sut["familiesCardDetails"].price.cadence).toBe("monthly");
|
||||
expect(sut["familiesCardDetails"].button.type).toBe("secondary");
|
||||
expect(sut["familiesCardDetails"].button.text).toBe("upgradeToFamilies");
|
||||
expect(sut["familiesCardDetails"].button.text).toBe("startFreeFamiliesTrial");
|
||||
expect(sut["familiesCardDetails"].features).toEqual(["Feature A", "Feature B", "Feature C"]);
|
||||
});
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ export class UpgradeAccountComponent implements OnInit {
|
||||
},
|
||||
button: {
|
||||
text: this.i18nService.t(
|
||||
this.isFamiliesPlan(tier.id) ? "upgradeToFamilies" : "upgradeToPremium",
|
||||
this.isFamiliesPlan(tier.id) ? "startFreeFamiliesTrial" : "upgradeToPremium",
|
||||
),
|
||||
type: buttonType,
|
||||
},
|
||||
|
||||
@@ -161,7 +161,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
|
||||
};
|
||||
|
||||
this.upgradeToMessage = this.i18nService.t(
|
||||
this.isFamiliesPlan ? "upgradeToFamilies" : "upgradeToPremium",
|
||||
this.isFamiliesPlan ? "startFreeFamiliesTrial" : "upgradeToPremium",
|
||||
);
|
||||
} else {
|
||||
this.complete.emit({ status: UpgradePaymentStatus.Closed, organizationId: null });
|
||||
|
||||
@@ -38,7 +38,6 @@ describe("FreeFamiliesPolicyService", () => {
|
||||
describe("showSponsoredFamiliesDropdown$", () => {
|
||||
it("should return true when all conditions are met", async () => {
|
||||
// Configure mocks
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
// Create a test organization that meets all criteria
|
||||
@@ -58,7 +57,6 @@ describe("FreeFamiliesPolicyService", () => {
|
||||
|
||||
it("should return false when organization is not Enterprise", async () => {
|
||||
// Configure mocks
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
// Create a test organization that is not Enterprise tier
|
||||
@@ -74,27 +72,8 @@ describe("FreeFamiliesPolicyService", () => {
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when feature flag is disabled", async () => {
|
||||
// Configure mocks to disable feature flag
|
||||
configService.getFeatureFlag$.mockReturnValue(of(false));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
// Create a test organization that meets other criteria
|
||||
const organization = {
|
||||
id: "org-id",
|
||||
productTierType: ProductTierType.Enterprise,
|
||||
useAdminSponsoredFamilies: true,
|
||||
isAdmin: true,
|
||||
} as Organization;
|
||||
|
||||
// Test the method
|
||||
const result = await firstValueFrom(service.showSponsoredFamiliesDropdown$(of(organization)));
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false when families feature is disabled by policy", async () => {
|
||||
// Configure mocks with a policy that disables the feature
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policiesByType$.mockReturnValue(
|
||||
of([{ organizationId: "org-id", enabled: true } as Policy]),
|
||||
);
|
||||
@@ -114,7 +93,6 @@ describe("FreeFamiliesPolicyService", () => {
|
||||
|
||||
it("should return false when useAdminSponsoredFamilies is false", async () => {
|
||||
// Configure mocks
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
// Create a test organization with useAdminSponsoredFamilies set to false
|
||||
@@ -132,7 +110,6 @@ describe("FreeFamiliesPolicyService", () => {
|
||||
|
||||
it("should return true when user is an owner but not admin", async () => {
|
||||
// Configure mocks
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
// Create a test organization where user is owner but not admin
|
||||
@@ -152,7 +129,6 @@ describe("FreeFamiliesPolicyService", () => {
|
||||
|
||||
it("should return true when user can manage users but is not admin or owner", async () => {
|
||||
// Configure mocks
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
// Create a test organization where user can manage users but is not admin or owner
|
||||
@@ -172,7 +148,6 @@ describe("FreeFamiliesPolicyService", () => {
|
||||
|
||||
it("should return false when user has no admin permissions", async () => {
|
||||
// Configure mocks
|
||||
configService.getFeatureFlag$.mockReturnValue(of(true));
|
||||
policyService.policiesByType$.mockReturnValue(of([]));
|
||||
|
||||
// Create a test organization where user has no admin permissions
|
||||
|
||||
@@ -8,8 +8,6 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
interface EnterpriseOrgStatus {
|
||||
isFreeFamilyPolicyEnabled: boolean;
|
||||
@@ -23,7 +21,6 @@ export class FreeFamiliesPolicyService {
|
||||
private policyService: PolicyService,
|
||||
private organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
organizations$ = this.accountService.activeAccount$.pipe(
|
||||
@@ -58,20 +55,14 @@ export class FreeFamiliesPolicyService {
|
||||
userId,
|
||||
);
|
||||
|
||||
return combineLatest([
|
||||
enterpriseOrganization$,
|
||||
this.configService.getFeatureFlag$(FeatureFlag.PM17772_AdminInitiatedSponsorships),
|
||||
organization,
|
||||
policies$,
|
||||
]).pipe(
|
||||
map(([isEnterprise, featureFlagEnabled, org, policies]) => {
|
||||
return combineLatest([enterpriseOrganization$, organization, policies$]).pipe(
|
||||
map(([isEnterprise, org, policies]) => {
|
||||
const familiesFeatureDisabled = policies.some(
|
||||
(policy) => policy.organizationId === org.id && policy.enabled,
|
||||
);
|
||||
|
||||
return (
|
||||
isEnterprise &&
|
||||
featureFlagEnabled &&
|
||||
!familiesFeatureDisabled &&
|
||||
org.useAdminSponsoredFamilies &&
|
||||
(org.isAdmin || org.isOwner || org.canManageUsers)
|
||||
|
||||
@@ -8,6 +8,8 @@ import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.servic
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall";
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "dynamic-avatar",
|
||||
imports: [SharedModule],
|
||||
@@ -25,10 +27,20 @@ type SizeTypes = "xlarge" | "large" | "default" | "small" | "xsmall";
|
||||
</span>`,
|
||||
})
|
||||
export class DynamicAvatarComponent implements OnDestroy {
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() border = false;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() id: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() text: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() title: string;
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() size: SizeTypes = "default";
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { SharedModule } from "../../shared";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "environment-selector",
|
||||
templateUrl: "environment-selector.component.html",
|
||||
|
||||
@@ -121,4 +121,153 @@ describe("InactiveTwoFactorReportComponent", () => {
|
||||
it("should call fullSync method of syncService", () => {
|
||||
expect(syncServiceMock.fullSync).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
describe("isInactive2faCipher", () => {
|
||||
beforeEach(() => {
|
||||
// Add both domain and host to services map
|
||||
component.services.set("example.com", "https://example.com/2fa-doc");
|
||||
component.services.set("sub.example.com", "https://sub.example.com/2fa-doc");
|
||||
fixture.detectChanges();
|
||||
});
|
||||
it("should return true and documentation for cipher with matching domain", () => {
|
||||
const cipher = createCipherView({
|
||||
login: {
|
||||
uris: [{ uri: "https://example.com/login" }],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(true);
|
||||
expect(doc).toBe("https://example.com/2fa-doc");
|
||||
});
|
||||
|
||||
it("should return true and documentation for cipher with matching host", () => {
|
||||
const cipher = createCipherView({
|
||||
login: {
|
||||
uris: [{ uri: "https://sub.example.com/login" }],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(true);
|
||||
expect(doc).toBe("https://sub.example.com/2fa-doc");
|
||||
});
|
||||
|
||||
it("should return false for cipher with non-matching domain or host", () => {
|
||||
const cipher = createCipherView({
|
||||
login: {
|
||||
uris: [{ uri: "https://otherdomain.com/login" }],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(false);
|
||||
expect(doc).toBe("");
|
||||
});
|
||||
|
||||
it("should return false if cipher type is not Login", () => {
|
||||
const cipher = createCipherView({
|
||||
type: 2,
|
||||
login: {
|
||||
uris: [{ uri: "https://example.com/login" }],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(false);
|
||||
expect(doc).toBe("");
|
||||
});
|
||||
|
||||
it("should return false if cipher has TOTP", () => {
|
||||
const cipher = createCipherView({
|
||||
login: {
|
||||
totp: "some-totp",
|
||||
uris: [{ uri: "https://example.com/login" }],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(false);
|
||||
expect(doc).toBe("");
|
||||
});
|
||||
|
||||
it("should return false if cipher is deleted", () => {
|
||||
const cipher = createCipherView({
|
||||
isDeleted: true,
|
||||
login: {
|
||||
uris: [{ uri: "https://example.com/login" }],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(false);
|
||||
expect(doc).toBe("");
|
||||
});
|
||||
|
||||
it("should return false if cipher does not have edit access and no organization", () => {
|
||||
component.organization = null;
|
||||
const cipher = createCipherView({
|
||||
edit: false,
|
||||
login: {
|
||||
uris: [{ uri: "https://example.com/login" }],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(false);
|
||||
expect(doc).toBe("");
|
||||
});
|
||||
|
||||
it("should return false if cipher does not have viewPassword", () => {
|
||||
const cipher = createCipherView({
|
||||
viewPassword: false,
|
||||
login: {
|
||||
uris: [{ uri: "https://example.com/login" }],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(false);
|
||||
expect(doc).toBe("");
|
||||
});
|
||||
|
||||
it("should check all uris and return true if any matches domain or host", () => {
|
||||
const cipher = createCipherView({
|
||||
login: {
|
||||
uris: [
|
||||
{ uri: "https://otherdomain.com/login" },
|
||||
{ uri: "https://sub.example.com/dashboard" },
|
||||
],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(true);
|
||||
expect(doc).toBe("https://sub.example.com/2fa-doc");
|
||||
});
|
||||
|
||||
it("should return false if uris array is empty", () => {
|
||||
const cipher = createCipherView({
|
||||
login: {
|
||||
uris: [],
|
||||
},
|
||||
});
|
||||
const [doc, isInactive] = (component as any).isInactive2faCipher(cipher);
|
||||
expect(isInactive).toBe(false);
|
||||
expect(doc).toBe("");
|
||||
});
|
||||
|
||||
function createCipherView({
|
||||
type = 1,
|
||||
login = {},
|
||||
isDeleted = false,
|
||||
edit = true,
|
||||
viewPassword = true,
|
||||
}: any): any {
|
||||
return {
|
||||
id: "test-id",
|
||||
type,
|
||||
login: {
|
||||
totp: null,
|
||||
hasUris: true,
|
||||
uris: [],
|
||||
...login,
|
||||
},
|
||||
isDeleted,
|
||||
edit,
|
||||
viewPassword,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,7 +109,18 @@ export class InactiveTwoFactorReportComponent extends CipherReportComponent impl
|
||||
const u = login.uris[i];
|
||||
if (u.uri != null && u.uri !== "") {
|
||||
const uri = u.uri.replace("www.", "");
|
||||
const host = Utils.getHost(uri);
|
||||
const domain = Utils.getDomain(uri);
|
||||
// check host first
|
||||
if (host != null && this.services.has(host)) {
|
||||
if (this.services.get(host) != null) {
|
||||
docFor2fa = this.services.get(host) || "";
|
||||
}
|
||||
isInactive2faCipher = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// then check domain
|
||||
if (domain != null && this.services.has(domain)) {
|
||||
if (this.services.get(domain) != null) {
|
||||
docFor2fa = this.services.get(domain) || "";
|
||||
|
||||
@@ -19,6 +19,8 @@ import { RequestSMAccessRequest } from "../models/requests/request-sm-access.req
|
||||
|
||||
import { SmLandingApiService } from "./sm-landing-api.service";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-request-sm-access",
|
||||
templateUrl: "request-sm-access.component.html",
|
||||
|
||||
@@ -12,6 +12,8 @@ import { NoItemsModule, SearchModule } from "@bitwarden/components";
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared/shared.module";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-sm-landing",
|
||||
imports: [SharedModule, SearchModule, NoItemsModule, HeaderModule],
|
||||
|
||||
@@ -12,6 +12,8 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||
import { HeaderModule } from "../layouts/header/header.module";
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-domain-rules",
|
||||
templateUrl: "domain-rules.component.html",
|
||||
|
||||
@@ -39,6 +39,8 @@ import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
|
||||
import { HeaderModule } from "../layouts/header/header.module";
|
||||
import { SharedModule } from "../shared";
|
||||
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush
|
||||
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection
|
||||
@Component({
|
||||
selector: "app-preferences",
|
||||
templateUrl: "preferences.component.html",
|
||||
|
||||
@@ -169,10 +169,12 @@
|
||||
|
||||
<bit-menu-divider *ngIf="showMenuDivider"></bit-menu-divider>
|
||||
|
||||
<button bitMenuItem type="button" (click)="toggleFavorite()">
|
||||
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i>
|
||||
{{ (cipher.favorite ? "unfavorite" : "favorite") | i18n }}
|
||||
</button>
|
||||
@if (!viewingOrgVault) {
|
||||
<button bitMenuItem type="button" (click)="toggleFavorite()">
|
||||
<i class="bwi bwi-fw bwi-star" aria-hidden="true"></i>
|
||||
{{ (cipher.favorite ? "unfavorite" : "favorite") | i18n }}
|
||||
</button>
|
||||
}
|
||||
<button bitMenuItem type="button" (click)="editCipher()" *ngIf="canEditCipher">
|
||||
<i class="bwi bwi-fw bwi-pencil-square" aria-hidden="true"></i>
|
||||
{{ "edit" | i18n }}
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
[showPremiumFeatures]="showPremiumFeatures"
|
||||
[useEvents]="useEvents"
|
||||
[viewingOrgVault]="viewingOrgVault"
|
||||
[cloneable]="canClone(item)"
|
||||
[cloneable]="canClone$(item) | async"
|
||||
[organizations]="allOrganizations"
|
||||
[collections]="allCollections"
|
||||
[checked]="selection.isSelected(item)"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// @ts-strict-ignore
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { toSignal, takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Observable, combineLatest, map, of, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
@@ -111,8 +111,6 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@Input() enforceOrgDataOwnershipPolicy: boolean;
|
||||
|
||||
private readonly restrictedPolicies = toSignal(this.restrictedItemTypesService.restricted$);
|
||||
|
||||
private _ciphers?: C[] = [];
|
||||
// FIXME(https://bitwarden.atlassian.net/browse/CL-903): Migrate to Signals
|
||||
// eslint-disable-next-line @angular-eslint/prefer-signals
|
||||
@@ -390,37 +388,22 @@ export class VaultItemsComponent<C extends CipherViewLike> {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: PM-13944 Refactor to use cipherAuthorizationService.canClone$ instead
|
||||
protected canClone(vaultItem: VaultItem<C>) {
|
||||
// This will check for restrictions from org policies before allowing cloning.
|
||||
const isItemRestricted = this.restrictedPolicies().some(
|
||||
(rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher),
|
||||
protected canClone$(vaultItem: VaultItem<C>): Observable<boolean> {
|
||||
return this.restrictedItemTypesService.restricted$.pipe(
|
||||
switchMap((restrictedTypes) => {
|
||||
// This will check for restrictions from org policies before allowing cloning.
|
||||
const isItemRestricted = restrictedTypes.some(
|
||||
(rt) => rt.cipherType === CipherViewLikeUtils.getType(vaultItem.cipher),
|
||||
);
|
||||
if (isItemRestricted) {
|
||||
return of(false);
|
||||
}
|
||||
return this.cipherAuthorizationService.canCloneCipher$(
|
||||
vaultItem.cipher,
|
||||
this.showAdminActions,
|
||||
);
|
||||
}),
|
||||
);
|
||||
if (isItemRestricted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vaultItem.cipher.organizationId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const org = this.allOrganizations.find((o) => o.id === vaultItem.cipher.organizationId);
|
||||
|
||||
// Admins and custom users can always clone in the Org Vault
|
||||
if (this.viewingOrgVault && (org.isAdmin || org.permissions.editAnyCollection)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the cipher belongs to a collection with canManage permission
|
||||
const orgCollections = this.allCollections.filter((c) => c.organizationId === org.id);
|
||||
|
||||
for (const collection of orgCollections) {
|
||||
if (vaultItem.cipher.collectionIds.includes(collection.id as any) && collection.manage) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected canEditCipher(cipher: C) {
|
||||
|
||||
@@ -6495,17 +6495,32 @@
|
||||
"tdeDisabledMasterPasswordRequired": {
|
||||
"message": "Your organization has updated your decryption options. Please set a master password to access your vault."
|
||||
},
|
||||
"maximumVaultTimeout": {
|
||||
"message": "Vault timeout"
|
||||
"sessionTimeoutPolicyTitle": {
|
||||
"message": "Session timeout"
|
||||
},
|
||||
"maximumVaultTimeoutDesc": {
|
||||
"message": "Set a maximum vault timeout for members."
|
||||
"sessionTimeoutPolicyDescription": {
|
||||
"message": "Set a maximum session timeout for all members except owners."
|
||||
},
|
||||
"maximumVaultTimeoutLabel": {
|
||||
"message": "Maximum vault timeout"
|
||||
"maximumAllowedTimeout": {
|
||||
"message": "Maximum allowed timeout"
|
||||
},
|
||||
"invalidMaximumVaultTimeout": {
|
||||
"message": "Invalid maximum vault timeout."
|
||||
"maximumAllowedTimeoutRequired": {
|
||||
"message": "Maximum allowed timeout is required."
|
||||
},
|
||||
"sessionTimeoutPolicyInvalidTime": {
|
||||
"message": "Time is invalid. Change at least one value."
|
||||
},
|
||||
"sessionTimeoutAction": {
|
||||
"message": "Session timeout action"
|
||||
},
|
||||
"immediately": {
|
||||
"message": "Immediately"
|
||||
},
|
||||
"onSystemLock": {
|
||||
"message": "On system lock"
|
||||
},
|
||||
"onAppRestart": {
|
||||
"message": "On app restart"
|
||||
},
|
||||
"hours": {
|
||||
"message": "Hours"
|
||||
@@ -6513,6 +6528,21 @@
|
||||
"minutes": {
|
||||
"message": "Minutes"
|
||||
},
|
||||
"sessionTimeoutConfirmationNeverTitle": {
|
||||
"message": "Are you certain you want to allow a maximum timeout of \"Never\" for all members?"
|
||||
},
|
||||
"sessionTimeoutConfirmationNeverDescription": {
|
||||
"message": "This option will save your members' encryption keys on their devices. If you choose this option, ensure that their devices are adequately protected."
|
||||
},
|
||||
"learnMoreAboutDeviceProtection": {
|
||||
"message": "Learn more about device protection"
|
||||
},
|
||||
"sessionTimeoutConfirmationOnSystemLockTitle": {
|
||||
"message": "\"System lock\" will only apply to the browser and desktop app"
|
||||
},
|
||||
"sessionTimeoutConfirmationOnSystemLockDescription": {
|
||||
"message": "The mobile and web app will use \"on app restart\" as their maximum allowed timeout, since the option is not supported."
|
||||
},
|
||||
"vaultTimeoutPolicyInEffect": {
|
||||
"message": "Your organization policies have set your maximum allowed vault timeout to $HOURS$ hour(s) and $MINUTES$ minute(s).",
|
||||
"placeholders": {
|
||||
@@ -11945,5 +11975,8 @@
|
||||
},
|
||||
"cardNumberLabel": {
|
||||
"message": "Card number"
|
||||
},
|
||||
"startFreeFamiliesTrial": {
|
||||
"message": "Start free Families trial"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user