diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index fef0181352c6..f69693a1dbc5 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -171,13 +171,11 @@ import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider"; -import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory"; import { PasswordStrengthService, PasswordStrengthServiceAbstraction, } from "@bitwarden/common/tools/password-strength"; -import { createSystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { DefaultEnvService } from "@bitwarden/common/tools/providers"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; @@ -233,6 +231,7 @@ import { KdfConfigService, KeyService as KeyServiceAbstraction, } from "@bitwarden/key-management"; +import { enableLogForTypes } from "@bitwarden/logging"; import { BackgroundSyncService } from "@bitwarden/platform/background-sync"; import { ActiveUserStateProvider, @@ -1079,6 +1078,9 @@ export default class MainBackground { this.importApiService = new ImportApiService(this.apiService); + const envService = new DefaultEnvService(this.configService, this.platformUtilsService); + const logProvider = enableLogForTypes(this.logService, []); + this.importService = new ImportService( this.cipherService, this.folderService, @@ -1090,15 +1092,8 @@ export default class MainBackground { this.pinService, this.accountService, this.restrictedItemTypesService, - createSystemServiceProvider( - new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService), - this.stateProvider, - this.policyService, - buildExtensionRegistry(), - this.logService, - this.platformUtilsService, - this.configService, - ), + envService, + logProvider, ); this.individualVaultExportService = new IndividualVaultExportService( diff --git a/apps/browser/src/tools/popup/generator/credential-generator-history.component.ts b/apps/browser/src/tools/popup/generator/credential-generator-history.component.ts index 441e5d6e4c6f..f0a0a861ac57 100644 --- a/apps/browser/src/tools/popup/generator/credential-generator-history.component.ts +++ b/apps/browser/src/tools/popup/generator/credential-generator-history.component.ts @@ -7,11 +7,6 @@ import { ReplaySubject, Subject, firstValueFrom, map, switchMap, takeUntil } fro import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - SemanticLogger, - disabledSemanticLoggerProvider, - ifEnabledSemanticLoggerProvider, -} from "@bitwarden/common/tools/log"; import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, DialogService } from "@bitwarden/components"; import { @@ -19,6 +14,11 @@ import { EmptyCredentialHistoryComponent, } from "@bitwarden/generator-components"; import { GeneratorHistoryService } from "@bitwarden/generator-history"; +import { + disabledSemanticLoggerProvider, + ifEnabledSemanticLoggerProvider, + SemanticLogger, +} from "@bitwarden/logging"; import { PopOutComponent } from "../../../platform/popup/components/pop-out.component"; import { PopupFooterComponent } from "../../../platform/popup/layout/popup-footer.component"; diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 8fb48fbc1ee2..9369fcc15cbc 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -114,13 +114,11 @@ import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; -import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider"; -import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory"; import { PasswordStrengthService, PasswordStrengthServiceAbstraction, } from "@bitwarden/common/tools/password-strength"; -import { createSystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { DefaultEnvService } from "@bitwarden/common/tools/providers"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendStateProvider } from "@bitwarden/common/tools/send/services/send-state.provider"; import { SendService } from "@bitwarden/common/tools/send/services/send.service"; @@ -158,6 +156,7 @@ import { BiometricStateService, DefaultBiometricStateService, } from "@bitwarden/key-management"; +import { enableLogForTypes } from "@bitwarden/logging"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; import { ActiveUserStateProvider, @@ -823,6 +822,9 @@ export class ServiceContainer { this.importApiService = new ImportApiService(this.apiService); + const envService = new DefaultEnvService(this.configService, this.platformUtilsService); + const logProvider = enableLogForTypes(this.logService, []); + this.importService = new ImportService( this.cipherService, this.folderService, @@ -834,15 +836,8 @@ export class ServiceContainer { this.pinService, this.accountService, this.restrictedItemTypesService, - createSystemServiceProvider( - new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService), - this.stateProvider, - this.policyService, - buildExtensionRegistry(), - this.logService, - this.platformUtilsService, - this.configService, - ), + envService, + logProvider, ); this.individualExportService = new IndividualVaultExportService( diff --git a/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts b/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts index cd07d3684fb5..365d47616cde 100644 --- a/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts +++ b/apps/web/src/app/tools/send/send-access/default-send-access-service.spec.ts @@ -3,14 +3,13 @@ import { Router, UrlTree } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; import { firstValueFrom, NEVER } from "rxjs"; +import { LOG_PROVIDER } from "@bitwarden/angular/services/injection-tokens"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { StateProvider } from "@bitwarden/common/platform/state"; import { mockAccountServiceWith, FakeStateProvider } from "@bitwarden/common/spec"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { UserId } from "@bitwarden/common/types/guid"; -import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; +import { LogProvider, SemanticLogger } from "@bitwarden/logging"; import { DefaultSendAccessService } from "./default-send-access-service"; import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory"; @@ -21,7 +20,7 @@ describe("DefaultSendAccessService", () => { let sendApiService: MockProxy; let router: MockProxy; let logger: MockProxy; - let systemServiceProvider: MockProxy; + let logProvider: LogProvider; beforeEach(() => { const accountService = mockAccountServiceWith("user-id" as UserId); @@ -29,9 +28,7 @@ describe("DefaultSendAccessService", () => { sendApiService = mock(); router = mock(); logger = mock(); - systemServiceProvider = mock(); - - systemServiceProvider.log.mockReturnValue(logger); + logProvider = jest.fn().mockReturnValue(logger) as any; TestBed.configureTestingModule({ providers: [ @@ -39,7 +36,7 @@ describe("DefaultSendAccessService", () => { { provide: StateProvider, useValue: stateProvider }, { provide: SendApiService, useValue: sendApiService }, { provide: Router, useValue: router }, - { provide: SYSTEM_SERVICE_PROVIDER, useValue: systemServiceProvider }, + { provide: LOG_PROVIDER, useValue: logProvider }, ], }); @@ -48,7 +45,7 @@ describe("DefaultSendAccessService", () => { describe("constructor", () => { it("creates logger with type 'SendAccessAuthenticationService' when initialized", () => { - expect(systemServiceProvider.log).toHaveBeenCalledWith({ + expect(logProvider).toHaveBeenCalledWith({ type: "SendAccessAuthenticationService", }); }); diff --git a/apps/web/src/app/tools/send/send-access/default-send-access-service.ts b/apps/web/src/app/tools/send/send-access/default-send-access-service.ts index 732303ce25ae..0c5a8722e24a 100644 --- a/apps/web/src/app/tools/send/send-access/default-send-access-service.ts +++ b/apps/web/src/app/tools/send/send-access/default-send-access-service.ts @@ -2,13 +2,12 @@ import { Injectable, Inject } from "@angular/core"; import { Router, UrlTree } from "@angular/router"; import { map, of, from, catchError, timeout } from "rxjs"; +import { LOG_PROVIDER } from "@bitwarden/angular/services/injection-tokens"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { StateProvider } from "@bitwarden/common/platform/state"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; -import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; +import { LogProvider, SemanticLogger } from "@bitwarden/logging"; import { SEND_RESPONSE_KEY, SEND_CONTEXT_KEY } from "./send-access-memory"; import { SendAccessService } from "./send-access-service.abstraction"; @@ -24,9 +23,9 @@ export class DefaultSendAccessService implements SendAccessService { private readonly state: StateProvider, private readonly api: SendApiService, private readonly router: Router, - @Inject(SYSTEM_SERVICE_PROVIDER) system: SystemServiceProvider, + @Inject(LOG_PROVIDER) log: LogProvider, ) { - this.logger = system.log({ type: "SendAccessAuthenticationService" }); + this.logger = log({ type: "SendAccessAuthenticationService" }); } redirect$(sendId: string) { diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts index 267de83db9fa..824898ce6d5b 100644 --- a/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts +++ b/apps/web/src/app/tools/send/send-access/try-send-access.guard.spec.ts @@ -2,9 +2,8 @@ import { TestBed } from "@angular/core/testing"; import { ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from "@angular/router"; import { firstValueFrom, Observable, of } from "rxjs"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; +import { LOG_PROVIDER } from "@bitwarden/angular/services/injection-tokens"; +import { LogProvider, SemanticLogger } from "@bitwarden/logging"; import { SendAccessService } from "./send-access-service.abstraction"; import { trySendAccess } from "./try-send-access.guard"; @@ -22,10 +21,8 @@ function createMockLogger(): SemanticLogger { } as any as SemanticLogger; } -function createMockSystemServiceProvider(): SystemServiceProvider { - return { - log: jest.fn().mockReturnValue(createMockLogger()), - } as any as SystemServiceProvider; +function createMockLogProvider(): LogProvider { + return jest.fn().mockReturnValue(createMockLogger()) as any; } function createMockSendAccessService() { @@ -38,18 +35,18 @@ function createMockSendAccessService() { describe("trySendAccess", () => { let mockSendAccessService: ReturnType; - let mockSystemServiceProvider: SystemServiceProvider; + let mockLogProvider: LogProvider; let mockRouterState: RouterStateSnapshot; beforeEach(() => { mockSendAccessService = createMockSendAccessService(); - mockSystemServiceProvider = createMockSystemServiceProvider(); + mockLogProvider = createMockLogProvider(); mockRouterState = {} as RouterStateSnapshot; TestBed.configureTestingModule({ providers: [ { provide: SendAccessService, useValue: mockSendAccessService }, - { provide: SYSTEM_SERVICE_PROVIDER, useValue: mockSystemServiceProvider }, + { provide: LOG_PROVIDER, useValue: mockLogProvider }, ], }); }); @@ -97,7 +94,7 @@ describe("trySendAccess", () => { await expect(firstValueFrom(guardResult!)).resolves.toEqual(expectedUrlTree); // Logger methods should not be called for warnings or panics - const mockLogger = (mockSystemServiceProvider.log as jest.Mock).mock.results[0].value; + const mockLogger = (mockLogProvider as jest.Mock).mock.results[0].value; expect(mockLogger.warn).not.toHaveBeenCalled(); expect(mockLogger.panic).not.toHaveBeenCalled(); }); @@ -116,7 +113,7 @@ describe("trySendAccess", () => { sendIdValue === undefined ? { key } : { sendId: sendIdValue, key }, ); const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + (mockLogProvider as jest.Mock).mockReturnValue(mockLogger); await expect(async () => { const result$ = TestBed.runInInjectionContext(() => @@ -125,7 +122,7 @@ describe("trySendAccess", () => { await firstValueFrom(result$); }).rejects.toThrow("Logger panic called"); - expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ + expect(mockLogProvider).toHaveBeenCalledWith({ function: "trySendAccess", }); expect(mockLogger.warn).toHaveBeenCalledWith( @@ -142,7 +139,7 @@ describe("trySendAccess", () => { const key = "valid-key"; const mockRoute = createMockRoute({ sendId: value, key }); const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + (mockLogProvider as jest.Mock).mockReturnValue(mockLogger); await expect(async () => { const result$ = TestBed.runInInjectionContext(() => @@ -151,7 +148,7 @@ describe("trySendAccess", () => { await firstValueFrom(result$); }).rejects.toThrow("Logger panic called"); - expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); + expect(mockLogProvider).toHaveBeenCalledWith({ function: "trySendAccess" }); expect(mockLogger.panic).toHaveBeenCalledWith( { expected: "string", actual: type }, "sendId has invalid type", @@ -167,7 +164,7 @@ describe("trySendAccess", () => { invalidSendId === undefined ? { key } : { sendId: invalidSendId, key }, ); const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + (mockLogProvider as jest.Mock).mockReturnValue(mockLogger); await expect(async () => { const result$ = TestBed.runInInjectionContext(() => @@ -189,7 +186,7 @@ describe("trySendAccess", () => { keyValue === undefined ? { sendId } : { sendId, key: keyValue }, ); const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + (mockLogProvider as jest.Mock).mockReturnValue(mockLogger); await expect(async () => { const result$ = TestBed.runInInjectionContext(() => @@ -198,7 +195,7 @@ describe("trySendAccess", () => { await firstValueFrom(result$); }).rejects.toThrow("Logger panic called"); - expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); + expect(mockLogProvider).toHaveBeenCalledWith({ function: "trySendAccess" }); expect(mockLogger.panic).toHaveBeenCalledWith("key missing from the route parameters"); }); @@ -210,7 +207,7 @@ describe("trySendAccess", () => { const sendId = "valid-send-id"; const mockRoute = createMockRoute({ sendId, key: value }); const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + (mockLogProvider as jest.Mock).mockReturnValue(mockLogger); await expect(async () => { const result$ = TestBed.runInInjectionContext(() => @@ -219,7 +216,7 @@ describe("trySendAccess", () => { await firstValueFrom(result$); }).rejects.toThrow("Logger panic called"); - expect(mockSystemServiceProvider.log).toHaveBeenCalledWith({ function: "trySendAccess" }); + expect(mockLogProvider).toHaveBeenCalledWith({ function: "trySendAccess" }); expect(mockLogger.panic).toHaveBeenCalledWith( { expected: "string", actual: type }, "key has invalid type", @@ -235,7 +232,7 @@ describe("trySendAccess", () => { invalidKey === undefined ? { sendId } : { sendId, key: invalidKey }, ); const mockLogger = createMockLogger(); - (mockSystemServiceProvider.log as jest.Mock).mockReturnValue(mockLogger); + (mockLogProvider as jest.Mock).mockReturnValue(mockLogger); await expect(async () => { const result$ = TestBed.runInInjectionContext(() => diff --git a/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts index 51941bf8e744..b75f55e5e554 100644 --- a/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts +++ b/apps/web/src/app/tools/send/send-access/try-send-access.guard.ts @@ -2,8 +2,8 @@ import { inject } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from "@angular/router"; import { from, ignoreElements, concat } from "rxjs"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; -import { SYSTEM_SERVICE_PROVIDER } from "@bitwarden/generator-components"; +import { LOG_PROVIDER } from "@bitwarden/angular/services/injection-tokens"; +import { LogProvider } from "@bitwarden/logging"; import { SendAccessService } from "./send-access-service.abstraction"; @@ -12,8 +12,8 @@ export const trySendAccess: CanActivateFn = ( _state: RouterStateSnapshot, ) => { const sendAccess = inject(SendAccessService); - const system = inject(SYSTEM_SERVICE_PROVIDER); - const logger = system.log({ function: "trySendAccess" }); + const log = inject(LOG_PROVIDER); + const logger = log({ function: "trySendAccess" }); const { sendId, key } = route.params; if (!sendId) { diff --git a/libs/angular/src/services/injection-tokens.ts b/libs/angular/src/services/injection-tokens.ts index 6bf3ab772522..0c259bd67836 100644 --- a/libs/angular/src/services/injection-tokens.ts +++ b/libs/angular/src/services/injection-tokens.ts @@ -15,10 +15,12 @@ import { import { Theme } from "@bitwarden/common/platform/enums"; import { Message } from "@bitwarden/common/platform/messaging"; import { HttpOperations } from "@bitwarden/common/services/api.service"; +import { LogProvider } from "@bitwarden/logging"; import { SafeInjectionToken } from "@bitwarden/ui-common"; // Re-export the SafeInjectionToken from ui-common export { SafeInjectionToken } from "@bitwarden/ui-common"; +export const LOG_PROVIDER = new SafeInjectionToken("LogProvider"); export const WINDOW = new SafeInjectionToken("WINDOW"); export const DOCUMENT = new SafeInjectionToken("DOCUMENT"); export const OBSERVABLE_MEMORY_STORAGE = new SafeInjectionToken< diff --git a/libs/common/src/tools/abstractions/env.service.ts b/libs/common/src/tools/abstractions/env.service.ts new file mode 100644 index 000000000000..8281cfc3c623 --- /dev/null +++ b/libs/common/src/tools/abstractions/env.service.ts @@ -0,0 +1,69 @@ +import { Observable } from "rxjs"; +import { SemVer } from "semver"; + +import { DeviceType } from "../../enums"; +import { ClientType } from "../../enums/client-type.enum"; +import { FeatureFlag } from "../../enums/feature-flag.enum"; +import { ServerConfig } from "../../platform/abstractions/config/server-config"; +import { Region } from "../../platform/abstractions/environment.service"; +import { ServerSettings } from "../../platform/models/domain/server-settings"; +import { UserId } from "../../types/guid"; + +/** + * Facade for environmental awareness and configuration services. + * Provides a unified interface for checking runtime environment capabilities, + * feature flags, and server configuration. + */ +export abstract class EnvService { + /* ConfigService methods */ + abstract readonly serverConfig$: Observable; + abstract readonly serverSettings$: Observable; + abstract readonly cloudRegion$: Observable; + + abstract getFeatureFlag$(key: Flag): Observable; + abstract userCachedFeatureFlag$( + key: Flag, + userId: UserId, + ): Observable; + abstract getFeatureFlag(key: Flag): Promise; + abstract checkServerMeetsVersionRequirement$( + minimumRequiredServerVersion: SemVer, + ): Observable; + abstract ensureConfigFetched(): Promise; + + /* PlatformUtilsService methods */ + abstract getDevice(): DeviceType; + abstract getDeviceString(): string; + abstract getClientType(): ClientType; + abstract isFirefox(): boolean; + abstract isChrome(): boolean; + abstract isEdge(): boolean; + abstract isOpera(): boolean; + abstract isVivaldi(): boolean; + abstract isSafari(): boolean; + abstract isChromium(): boolean; + abstract isMacAppStore(): boolean; + abstract isPopupOpen(): Promise; + abstract launchUri(uri: string, options?: any): void; + abstract getApplicationVersion(): Promise; + abstract getApplicationVersionNumber(): Promise; + abstract supportsWebAuthn(win: Window): boolean; + abstract supportsDuo(): boolean; + abstract supportsAutofill(): boolean; + abstract supportsFileDownloads(): boolean; + abstract isDev(): boolean; + abstract isSelfHost(): boolean; + abstract copyToClipboard(text: string, options?: any): void | boolean; + abstract readFromClipboard(): Promise; + abstract supportsSecureStorage(): boolean; + abstract getAutofillKeyboardShortcut(): Promise; + /** + * @deprecated use `@bitwarden/components/ToastService.showToast` instead + */ + abstract showToast( + type: "error" | "success" | "warning" | "info", + title: string, + text: string | string[], + options?: any, + ): void; +} diff --git a/libs/common/src/tools/extension/extension.service.spec.ts b/libs/common/src/tools/extension/extension.service.spec.ts index 9959488fecab..e17e4148cadc 100644 --- a/libs/common/src/tools/extension/extension.service.spec.ts +++ b/libs/common/src/tools/extension/extension.service.spec.ts @@ -1,13 +1,14 @@ import { mock } from "jest-mock-extended"; import { BehaviorSubject, firstValueFrom } from "rxjs"; +import { disabledSemanticLoggerProvider } from "@bitwarden/logging"; + import { FakeAccountService, FakeStateProvider, awaitAsync } from "../../../spec"; import { Account } from "../../auth/abstractions/account.service"; import { EXTENSION_DISK, UserKeyDefinition } from "../../platform/state"; import { UserId } from "../../types/guid"; import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; -import { disabledSemanticLoggerProvider } from "../log"; import { UserStateSubjectDependencyProvider } from "../state/user-state-subject-dependency-provider"; import { Site } from "./data"; diff --git a/libs/common/src/tools/extension/extension.service.ts b/libs/common/src/tools/extension/extension.service.ts index c4e6887b8213..4532fe9943ac 100644 --- a/libs/common/src/tools/extension/extension.service.ts +++ b/libs/common/src/tools/extension/extension.service.ts @@ -1,8 +1,9 @@ import { shareReplay } from "rxjs"; +import { SemanticLogger } from "@bitwarden/logging"; + import { Account } from "../../auth/abstractions/account.service"; import { BoundDependency } from "../dependencies"; -import { SemanticLogger } from "../log"; import { UserStateSubject } from "../state/user-state-subject"; import { UserStateSubjectDependencyProvider } from "../state/user-state-subject-dependency-provider"; diff --git a/libs/common/src/tools/log/index.ts b/libs/common/src/tools/log/index.ts deleted file mode 100644 index 1c93a6ce63fa..000000000000 --- a/libs/common/src/tools/log/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./factory"; -export * from "./disabled-logger"; -export { LogProvider } from "./types"; -export { SemanticLogger } from "./semantic-logger.abstraction"; diff --git a/libs/common/src/tools/log/types.ts b/libs/common/src/tools/log/types.ts deleted file mode 100644 index ca887af4225b..000000000000 --- a/libs/common/src/tools/log/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Jsonify } from "type-fest"; - -import { SemanticLogger } from "./semantic-logger.abstraction"; - -/** Creates a semantic logger. - * @param context all logs emitted by the logger are extended with - * these fields. - * @remarks The `message`, `level`, `provider`, and `content` fields - * are reserved for use by the semantic logging system. - */ -export type LogProvider = (context: Jsonify) => SemanticLogger; diff --git a/libs/common/src/tools/log/util.ts b/libs/common/src/tools/log/util.ts deleted file mode 100644 index cf1c39230e14..000000000000 --- a/libs/common/src/tools/log/util.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { LogService } from "../../platform/abstractions/log.service"; - -// show our GRIT - these functions implement generalized logging -// controls and should return DISABLED_LOGGER in production. -export function warnLoggingEnabled(logService: LogService, method: string, context?: any) { - logService.warning({ - method, - context, - provider: "tools/log", - message: "Semantic logging enabled. 🦟 Please report this bug if you see it 🦟", - }); -} diff --git a/libs/common/src/tools/providers.spec.ts b/libs/common/src/tools/providers.spec.ts index 5953e5ebab2a..3991b174c354 100644 --- a/libs/common/src/tools/providers.spec.ts +++ b/libs/common/src/tools/providers.spec.ts @@ -1,5 +1,7 @@ import { mock, MockProxy } from "jest-mock-extended"; +import { disabledSemanticLoggerProvider } from "@bitwarden/logging"; + import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction"; import { ConfigService } from "../platform/abstractions/config/config.service"; import { LogService } from "../platform/abstractions/log.service"; @@ -9,7 +11,6 @@ import { StateProvider } from "../platform/state"; import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider"; import { ExtensionRegistry } from "./extension/extension-registry.abstraction"; import { ExtensionService } from "./extension/extension.service"; -import { disabledSemanticLoggerProvider } from "./log"; import { createSystemServiceProvider } from "./providers"; describe("SystemServiceProvider", () => { diff --git a/libs/common/src/tools/providers.ts b/libs/common/src/tools/providers.ts index ac42c556042b..fc34854276d3 100644 --- a/libs/common/src/tools/providers.ts +++ b/libs/common/src/tools/providers.ts @@ -1,17 +1,33 @@ -import { LogService } from "@bitwarden/logging"; +import { SemVer } from "semver"; + +import { + disabledSemanticLoggerProvider, + enableLogForTypes, + LogProvider, + LogService, +} from "@bitwarden/logging"; import { BitwardenClient } from "@bitwarden/sdk-internal"; import { StateProvider } from "@bitwarden/state"; import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction"; +import { FeatureFlag } from "../enums/feature-flag.enum"; import { ConfigService } from "../platform/abstractions/config/config.service"; import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service"; +import { UserId } from "../types/guid"; +import { EnvService } from "./abstractions/env.service"; import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider"; import { ExtensionRegistry } from "./extension/extension-registry.abstraction"; import { ExtensionService } from "./extension/extension.service"; -import { disabledSemanticLoggerProvider, enableLogForTypes, LogProvider } from "./log"; -/** Provides access to commonly-used cross-cutting services. */ +export { EnvService } from "./abstractions/env.service"; + +/** + * @deprecated Use individual service injection instead: PolicyService, ExtensionService, + * LogProvider, and EnvService (which consolidates ConfigService and PlatformUtilsService). + * + * Provides access to commonly-used cross-cutting services. + */ export type SystemServiceProvider = { /** Policy configured by the administrative console */ readonly policy: PolicyService; @@ -32,7 +48,11 @@ export type SystemServiceProvider = { readonly sdk?: BitwardenClient; }; -/** Constructs a system service provider. */ +/** + * @deprecated Use individual service injection instead. + * + * Constructs a system service provider. + */ export function createSystemServiceProvider( encryptor: LegacyEncryptorProvider, state: StateProvider, @@ -64,3 +84,161 @@ export function createSystemServiceProvider( environment, }; } + +/** + * Facade exposing methods from ConfigService and PlatformUtilsService that are related to environmental awareness + */ +export class DefaultEnvService extends EnvService { + constructor( + private configService: ConfigService, + private platformUtilsService: PlatformUtilsService, + ) { + super(); + } + + /* ConfigService methods */ + get serverConfig$() { + return this.configService.serverConfig$; + } + + get serverSettings$() { + return this.configService.serverSettings$; + } + + get cloudRegion$() { + return this.configService.cloudRegion$; + } + + getFeatureFlag$(key: Flag) { + return this.configService.getFeatureFlag$(key); + } + + userCachedFeatureFlag$(key: Flag, userId: UserId) { + return this.configService.userCachedFeatureFlag$(key, userId); + } + + getFeatureFlag(key: Flag) { + return this.configService.getFeatureFlag(key); + } + + checkServerMeetsVersionRequirement$(minimumRequiredServerVersion: SemVer) { + return this.configService.checkServerMeetsVersionRequirement$(minimumRequiredServerVersion); + } + + ensureConfigFetched() { + return this.configService.ensureConfigFetched(); + } + + /* PlatformUtilsService methods */ + getDevice() { + return this.platformUtilsService.getDevice(); + } + + getDeviceString() { + return this.platformUtilsService.getDeviceString(); + } + + getClientType() { + return this.platformUtilsService.getClientType(); + } + + isFirefox() { + return this.platformUtilsService.isFirefox(); + } + + isChrome() { + return this.platformUtilsService.isChrome(); + } + + isEdge() { + return this.platformUtilsService.isEdge(); + } + + isOpera() { + return this.platformUtilsService.isOpera(); + } + + isVivaldi() { + return this.platformUtilsService.isVivaldi(); + } + + isSafari() { + return this.platformUtilsService.isSafari(); + } + + isChromium() { + return this.platformUtilsService.isChromium(); + } + + isMacAppStore() { + return this.platformUtilsService.isMacAppStore(); + } + + isPopupOpen() { + return this.platformUtilsService.isPopupOpen(); + } + + launchUri(uri: string, options?: any) { + return this.platformUtilsService.launchUri(uri, options); + } + + getApplicationVersion() { + return this.platformUtilsService.getApplicationVersion(); + } + + getApplicationVersionNumber() { + return this.platformUtilsService.getApplicationVersionNumber(); + } + + supportsWebAuthn(win: Window) { + return this.platformUtilsService.supportsWebAuthn(win); + } + + supportsDuo() { + return this.platformUtilsService.supportsDuo(); + } + + supportsAutofill() { + return this.platformUtilsService.supportsAutofill(); + } + + supportsFileDownloads() { + return this.platformUtilsService.supportsFileDownloads(); + } + + isDev() { + return this.platformUtilsService.isDev(); + } + + isSelfHost() { + return this.platformUtilsService.isSelfHost(); + } + + copyToClipboard(text: string, options?: any) { + return this.platformUtilsService.copyToClipboard(text, options); + } + + readFromClipboard() { + return this.platformUtilsService.readFromClipboard(); + } + + supportsSecureStorage() { + return this.platformUtilsService.supportsSecureStorage(); + } + + getAutofillKeyboardShortcut() { + return this.platformUtilsService.getAutofillKeyboardShortcut(); + } + + /** + * @deprecated use `@bitwarden/components/ToastService.showToast` instead + */ + showToast( + type: "error" | "success" | "warning" | "info", + title: string, + text: string | string[], + options?: any, + ) { + return this.platformUtilsService.showToast(type, title, text, options); + } +} diff --git a/libs/common/src/tools/state/user-state-subject-dependency-provider.ts b/libs/common/src/tools/state/user-state-subject-dependency-provider.ts index 89b6d7e72700..7a85ab2ba55d 100644 --- a/libs/common/src/tools/state/user-state-subject-dependency-provider.ts +++ b/libs/common/src/tools/state/user-state-subject-dependency-provider.ts @@ -1,8 +1,9 @@ import { Jsonify } from "type-fest"; +import { SemanticLogger } from "@bitwarden/logging"; + import { StateProvider } from "../../platform/state"; import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider"; -import { SemanticLogger } from "../log"; /** Aggregates user state subject dependencies */ export abstract class UserStateSubjectDependencyProvider { diff --git a/libs/common/src/tools/state/user-state-subject.spec.ts b/libs/common/src/tools/state/user-state-subject.spec.ts index a6d452d37fdb..bdf2c1f91eca 100644 --- a/libs/common/src/tools/state/user-state-subject.spec.ts +++ b/libs/common/src/tools/state/user-state-subject.spec.ts @@ -2,6 +2,8 @@ // @ts-strict-ignore import { BehaviorSubject, of, Subject } from "rxjs"; +import { disabledSemanticLoggerProvider } from "@bitwarden/logging"; + import { awaitAsync, FakeAccountService, @@ -13,7 +15,6 @@ import { GENERATOR_DISK, UserKeyDefinition } from "../../platform/state"; import { UserId } from "../../types/guid"; import { LegacyEncryptorProvider } from "../cryptography/legacy-encryptor-provider"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; -import { disabledSemanticLoggerProvider } from "../log"; import { PrivateClassifier } from "../private-classifier"; import { StateConstraints } from "../types"; diff --git a/libs/common/src/tools/state/user-state-subject.ts b/libs/common/src/tools/state/user-state-subject.ts index e6b66d8f6996..0bf813b4a629 100644 --- a/libs/common/src/tools/state/user-state-subject.ts +++ b/libs/common/src/tools/state/user-state-subject.ts @@ -29,11 +29,12 @@ import { switchMap, } from "rxjs"; +import { SemanticLogger } from "@bitwarden/logging"; + import { Account } from "../../auth/abstractions/account.service"; import { EncString } from "../../key-management/crypto/models/enc-string"; import { SingleUserState, UserKeyDefinition } from "../../platform/state"; import { UserEncryptor } from "../cryptography/user-encryptor.abstraction"; -import { SemanticLogger } from "../log"; import { anyComplete, pin, ready, withLatestReady } from "../rx"; import { Constraints, SubjectConstraints, WithConstraints } from "../types"; diff --git a/libs/importer/src/components/importer-providers.ts b/libs/importer/src/components/importer-providers.ts index b00bd65211e6..ed9457ae8916 100644 --- a/libs/importer/src/components/importer-providers.ts +++ b/libs/importer/src/components/importer-providers.ts @@ -4,8 +4,8 @@ // eslint-disable-next-line no-restricted-imports import { CollectionService } from "@bitwarden/admin-console/common"; import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; +import { LOG_PROVIDER } from "@bitwarden/angular/services/injection-tokens"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; @@ -17,16 +17,12 @@ import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/crypt import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider"; import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction"; import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory"; -import { - createSystemServiceProvider, - SystemServiceProvider, -} from "@bitwarden/common/tools/providers"; +import { DefaultEnvService, EnvService } from "@bitwarden/common/tools/providers"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KeyService } from "@bitwarden/key-management"; -import { StateProvider } from "@bitwarden/state"; -import { SafeInjectionToken } from "@bitwarden/ui-common"; +import { disabledSemanticLoggerProvider, enableLogForTypes } from "@bitwarden/logging"; import { ImportApiService, @@ -35,10 +31,6 @@ import { ImportServiceAbstraction, } from "../services"; -// FIXME: unify with `SYSTEM_SERVICE_PROVIDER` when migrating it from the generator component module -// to a general module. -const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken("SystemServices"); - /** Import service factories */ export const ImporterProviders: SafeProvider[] = [ safeProvider({ @@ -59,17 +51,20 @@ export const ImporterProviders: SafeProvider[] = [ deps: [], }), safeProvider({ - provide: SYSTEM_SERVICE_PROVIDER, - useFactory: createSystemServiceProvider, - deps: [ - LegacyEncryptorProvider, - StateProvider, - PolicyService, - ExtensionRegistry, - LogService, - PlatformUtilsService, - ConfigService, - ], + provide: EnvService, + useClass: DefaultEnvService, + deps: [ConfigService, PlatformUtilsService], + }), + safeProvider({ + provide: LOG_PROVIDER, + useFactory: (logger: LogService, env: EnvService) => { + if (env.isDev()) { + return enableLogForTypes(logger, []); + } else { + return disabledSemanticLoggerProvider; + } + }, + deps: [LogService, EnvService], }), safeProvider({ provide: ImportServiceAbstraction, @@ -85,7 +80,8 @@ export const ImporterProviders: SafeProvider[] = [ PinServiceAbstraction, AccountService, RestrictedItemTypesService, - SYSTEM_SERVICE_PROVIDER, + EnvService, + LOG_PROVIDER, ], }), ]; diff --git a/libs/importer/src/services/import.service.spec.ts b/libs/importer/src/services/import.service.spec.ts index c3d555af9361..b465a677b226 100644 --- a/libs/importer/src/services/import.service.spec.ts +++ b/libs/importer/src/services/import.service.spec.ts @@ -10,11 +10,9 @@ import { ClientType } from "@bitwarden/client-type"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { EnvService } from "@bitwarden/common/tools/providers"; import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -22,6 +20,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KeyService } from "@bitwarden/key-management"; +import { LogProvider, SemanticLogger } from "@bitwarden/logging"; import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer"; import { Importer } from "../importers/importer"; @@ -44,7 +43,9 @@ describe("ImportService", () => { let pinService: MockProxy; let accountService: MockProxy; let restrictedItemTypesService: MockProxy; - let systemServiceProvider: MockProxy; + let envService: MockProxy; + let logProvider: LogProvider; + let mockLogger: MockProxy; beforeEach(() => { cipherService = mock(); @@ -57,17 +58,12 @@ describe("ImportService", () => { pinService = mock(); restrictedItemTypesService = mock(); - const configService = mock(); - configService.getFeatureFlag$.mockReturnValue(new BehaviorSubject(false)); + envService = mock(); + envService.getFeatureFlag$.mockReturnValue(new BehaviorSubject(false)); + envService.getClientType.mockReturnValue(ClientType.Desktop); - const environment = mock(); - environment.getClientType.mockReturnValue(ClientType.Desktop); - - systemServiceProvider = mock({ - configService, - environment, - log: jest.fn().mockReturnValue({ debug: jest.fn() }), - }); + mockLogger = mock(); + logProvider = () => mockLogger; importService = new ImportService( cipherService, @@ -80,7 +76,8 @@ describe("ImportService", () => { pinService, accountService, restrictedItemTypesService, - systemServiceProvider, + envService, + logProvider, ); }); @@ -279,17 +276,11 @@ describe("ImportService", () => { typeSubject = new Subject(); mockLogger = { debug: jest.fn() }; - const configService = mock(); - configService.getFeatureFlag$.mockReturnValue(featureFlagSubject); + envService = mock(); + envService.getFeatureFlag$.mockReturnValue(featureFlagSubject); + envService.getClientType.mockReturnValue(ClientType.Desktop); - const environment = mock(); - environment.getClientType.mockReturnValue(ClientType.Desktop); - - systemServiceProvider = mock({ - configService, - environment, - log: jest.fn().mockReturnValue(mockLogger), - }); + logProvider = jest.fn(() => mockLogger) as any; // Recreate the service with the updated mocks for logging tests importService = new ImportService( @@ -303,7 +294,8 @@ describe("ImportService", () => { pinService, accountService, restrictedItemTypesService, - systemServiceProvider, + envService, + logProvider, ); }); diff --git a/libs/importer/src/services/import.service.ts b/libs/importer/src/services/import.service.ts index 351d89be3fae..f416fbaf7df6 100644 --- a/libs/importer/src/services/import.service.ts +++ b/libs/importer/src/services/import.service.ts @@ -20,8 +20,7 @@ import { KvpRequest } from "@bitwarden/common/models/request/kvp.request"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { EnvService } from "@bitwarden/common/tools/providers"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -32,6 +31,7 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service"; import { KeyService } from "@bitwarden/key-management"; +import { LogProvider, SemanticLogger } from "@bitwarden/logging"; import { AscendoCsvImporter, @@ -128,9 +128,10 @@ export class ImportService implements ImportServiceAbstraction { private pinService: PinServiceAbstraction, private accountService: AccountService, private restrictedItemTypesService: RestrictedItemTypesService, - private system: SystemServiceProvider, + private env: EnvService, + log: LogProvider, ) { - this.logger = system.log({ type: "ImportService" }); + this.logger = log({ type: "ImportService" }); } getImportOptions(): ImportOption[] { @@ -138,20 +139,18 @@ export class ImportService implements ImportServiceAbstraction { } metadata$(type$: Observable): Observable { - const browserEnabled$ = this.system.configService.getFeatureFlag$( - FeatureFlag.UseChromiumImporter, - ); - const client = this.system.environment.getClientType(); + const browserEnabled$ = this.env.getFeatureFlag$(FeatureFlag.UseChromiumImporter); + const client = this.env.getClientType(); const capabilities$ = combineLatest([type$, browserEnabled$]).pipe( map(([type, enabled]) => { let loaders = availableLoaders(type, client); // Mac App Store is currently disabled due to sandboxing. - let isUnsupported = this.system.environment.isMacAppStore(); + let isUnsupported = this.env.isMacAppStore(); if (enabled && type === "bravecsv") { try { - const device = this.system.environment.getDevice(); + const device = this.env.getDevice(); const isWindowsDesktop = device === DeviceType.WindowsDesktop; if (isWindowsDesktop) { isUnsupported = true; diff --git a/libs/common/src/tools/log/default-semantic-logger.spec.ts b/libs/logging/src/default-semantic-logger.spec.ts similarity index 73% rename from libs/common/src/tools/log/default-semantic-logger.spec.ts rename to libs/logging/src/default-semantic-logger.spec.ts index 7f608fb40efd..1e042de5ec80 100644 --- a/libs/common/src/tools/log/default-semantic-logger.spec.ts +++ b/libs/logging/src/default-semantic-logger.spec.ts @@ -1,9 +1,8 @@ import { mock } from "jest-mock-extended"; -import { LogService } from "../../platform/abstractions/log.service"; -import { LogLevelType } from "../../platform/enums"; - import { DefaultSemanticLogger } from "./default-semantic-logger"; +import { LogLevel } from "./log-level"; +import { LogService } from "./log.service"; const logger = mock(); @@ -18,7 +17,7 @@ describe("DefaultSemanticLogger", () => { log.debug("this is a debug message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Debug, { "@timestamp": 0, message: "this is a debug message", level: "debug", @@ -30,19 +29,19 @@ describe("DefaultSemanticLogger", () => { log.debug({ example: "this is content" }); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Debug, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Debug, { "@timestamp": 0, content: { example: "this is content" }, level: "debug", }); }); - it("writes structural content to console.log with a message", () => { + it("writes structural content and message to console.log", () => { const log = new DefaultSemanticLogger(logger, {}, () => 0); log.info({ example: "this is content" }, "this is a message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Info, { "@timestamp": 0, content: { example: "this is content" }, message: "this is a message", @@ -57,7 +56,7 @@ describe("DefaultSemanticLogger", () => { log.info("this is an info message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Info, { "@timestamp": 0, message: "this is an info message", level: "information", @@ -69,19 +68,19 @@ describe("DefaultSemanticLogger", () => { log.info({ example: "this is content" }); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Info, { "@timestamp": 0, content: { example: "this is content" }, level: "information", }); }); - it("writes structural content to console.log with a message", () => { + it("writes structural content and message to console.log", () => { const log = new DefaultSemanticLogger(logger, {}, () => 0); log.info({ example: "this is content" }, "this is a message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Info, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Info, { "@timestamp": 0, content: { example: "this is content" }, message: "this is a message", @@ -96,7 +95,7 @@ describe("DefaultSemanticLogger", () => { log.warn("this is a warning message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Warning, { "@timestamp": 0, message: "this is a warning message", level: "warning", @@ -108,19 +107,19 @@ describe("DefaultSemanticLogger", () => { log.warn({ example: "this is content" }); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Warning, { "@timestamp": 0, content: { example: "this is content" }, level: "warning", }); }); - it("writes structural content to console.warn with a message", () => { + it("writes structural content and message to console.warn", () => { const log = new DefaultSemanticLogger(logger, {}, () => 0); log.warn({ example: "this is content" }, "this is a message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Warning, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Warning, { "@timestamp": 0, content: { example: "this is content" }, message: "this is a message", @@ -135,7 +134,7 @@ describe("DefaultSemanticLogger", () => { log.error("this is an error message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Error, { "@timestamp": 0, message: "this is an error message", level: "error", @@ -147,19 +146,19 @@ describe("DefaultSemanticLogger", () => { log.error({ example: "this is content" }); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Error, { "@timestamp": 0, content: { example: "this is content" }, level: "error", }); }); - it("writes structural content to console.error with a message", () => { + it("writes structural content and message to console.error", () => { const log = new DefaultSemanticLogger(logger, {}, () => 0); log.error({ example: "this is content" }, "this is a message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Error, { "@timestamp": 0, content: { example: "this is content" }, message: "this is a message", @@ -169,26 +168,26 @@ describe("DefaultSemanticLogger", () => { }); describe("panic", () => { - it("writes structural log messages to console.error before throwing the message", () => { + it("writes structural log messages without content to console.error before throwing", () => { const log = new DefaultSemanticLogger(logger, {}, () => 0); expect(() => log.panic("this is an error message")).toThrow("this is an error message"); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Error, { "@timestamp": 0, message: "this is an error message", level: "error", }); }); - it("writes structural log messages to console.error with a message before throwing the message", () => { + it("writes structural error message with structured content to console.error before throwing", () => { const log = new DefaultSemanticLogger(logger, {}, () => 0); expect(() => log.panic({ example: "this is content" }, "this is an error message")).toThrow( "this is an error message", ); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Error, { "@timestamp": 0, content: { example: "this is content" }, message: "this is an error message", @@ -196,14 +195,14 @@ describe("DefaultSemanticLogger", () => { }); }); - it("writes structural log messages to console.error with a content before throwing the message", () => { + it("writes structural error message with unstructured content to console.error before throwing", () => { const log = new DefaultSemanticLogger(logger, {}, () => 0); expect(() => log.panic("this is content", "this is an error message")).toThrow( "this is an error message", ); - expect(logger.write).toHaveBeenCalledWith(LogLevelType.Error, { + expect(logger.write).toHaveBeenCalledWith(LogLevel.Error, { "@timestamp": 0, content: "this is content", message: "this is an error message", diff --git a/libs/common/src/tools/log/default-semantic-logger.ts b/libs/logging/src/default-semantic-logger.ts similarity index 73% rename from libs/common/src/tools/log/default-semantic-logger.ts rename to libs/logging/src/default-semantic-logger.ts index eb1ecbe36c61..dacda8e4ed6f 100644 --- a/libs/common/src/tools/log/default-semantic-logger.ts +++ b/libs/logging/src/default-semantic-logger.ts @@ -1,8 +1,7 @@ import { Jsonify } from "type-fest"; -import { LogService } from "../../platform/abstractions/log.service"; -import { LogLevelType } from "../../platform/enums"; - +import { LogLevel } from "./log-level"; +import { LogService } from "./log.service"; import { SemanticLogger } from "./semantic-logger.abstraction"; /** Sends semantic logs to the console. @@ -26,29 +25,29 @@ export class DefaultSemanticLogger implements SemanticLo readonly context: object; debug(content: Jsonify, message?: string): void { - this.log(content, LogLevelType.Debug, message); + this.log(content, LogLevel.Debug, message); } info(content: Jsonify, message?: string): void { - this.log(content, LogLevelType.Info, message); + this.log(content, LogLevel.Info, message); } warn(content: Jsonify, message?: string): void { - this.log(content, LogLevelType.Warning, message); + this.log(content, LogLevel.Warning, message); } error(content: Jsonify, message?: string): void { - this.log(content, LogLevelType.Error, message); + this.log(content, LogLevel.Error, message); } panic(content: Jsonify, message?: string): never { - this.log(content, LogLevelType.Error, message); + this.log(content, LogLevel.Error, message); const panicMessage = message ?? (typeof content === "string" ? content : "a fatal error occurred"); throw new Error(panicMessage); } - private log(content: Jsonify, level: LogLevelType, message?: string) { + private log(content: Jsonify, level: LogLevel, message?: string) { const log = { ...this.context, message, @@ -66,15 +65,15 @@ export class DefaultSemanticLogger implements SemanticLo } } -function stringifyLevel(level: LogLevelType) { +function stringifyLevel(level: LogLevel) { switch (level) { - case LogLevelType.Debug: + case LogLevel.Debug: return "debug"; - case LogLevelType.Info: + case LogLevel.Info: return "information"; - case LogLevelType.Warning: + case LogLevel.Warning: return "warning"; - case LogLevelType.Error: + case LogLevel.Error: return "error"; default: return `${level}`; diff --git a/libs/common/src/tools/log/disabled-logger.ts b/libs/logging/src/disabled-logger.ts similarity index 92% rename from libs/common/src/tools/log/disabled-logger.ts rename to libs/logging/src/disabled-logger.ts index 53feb0c8b397..e0cded0fcbf8 100644 --- a/libs/common/src/tools/log/disabled-logger.ts +++ b/libs/logging/src/disabled-logger.ts @@ -1,6 +1,6 @@ import { Jsonify } from "type-fest"; -import { deepFreeze } from "../util"; +import { deepFreeze } from "@bitwarden/common/tools/util"; import { SemanticLogger } from "./semantic-logger.abstraction"; diff --git a/libs/common/src/tools/log/factory.ts b/libs/logging/src/factory.ts similarity index 85% rename from libs/common/src/tools/log/factory.ts rename to libs/logging/src/factory.ts index 5f0f2d74e91a..5f2ab8a75304 100644 --- a/libs/common/src/tools/log/factory.ts +++ b/libs/logging/src/factory.ts @@ -1,12 +1,10 @@ import { Jsonify } from "type-fest"; -import { LogService } from "../../platform/abstractions/log.service"; - import { DefaultSemanticLogger } from "./default-semantic-logger"; import { DISABLED_LOGGER } from "./disabled-logger"; -import { SemanticLogger } from "./semantic-logger.abstraction"; -import { LogProvider } from "./types"; -import { warnLoggingEnabled } from "./util"; +import { LogService } from "./log.service"; + +import { LogProvider, SemanticLogger } from "./index"; /** Instantiates a semantic logger that emits nothing when a message * is logged. @@ -86,3 +84,14 @@ export function ifEnabledSemanticLoggerProvider( return DISABLED_LOGGER; } } + +// show our GRIT - these functions implement generalized logging +// controls and should return DISABLED_LOGGER in production. +function warnLoggingEnabled(logService: LogService, method: string, context?: any) { + logService.warning({ + method, + context, + provider: "tools/log", + message: "Semantic logging enabled. 🦟 Please report this bug if you see it 🦟", + }); +} diff --git a/libs/logging/src/index.ts b/libs/logging/src/index.ts index 8ce4b62cd3fa..65e8c03605ff 100644 --- a/libs/logging/src/index.ts +++ b/libs/logging/src/index.ts @@ -1,3 +1,50 @@ +import { Jsonify } from "type-fest"; + +import { SemanticLogger } from "./semantic-logger.abstraction"; + export { LogService } from "./log.service"; export { LogLevel } from "./log-level"; export { ConsoleLogService } from "./console-log.service"; +export { SemanticLogger } from "./semantic-logger.abstraction"; +export { DISABLED_LOGGER } from "./disabled-logger"; +export { + disabledSemanticLoggerProvider, + consoleSemanticLoggerProvider, + enableLogForTypes, + ifEnabledSemanticLoggerProvider, +} from "./factory"; + +/** + * Creates a semantic logger with a fixed context that is included in all log messages. + * + * @param context - Contextual metadata that will be included in every log entry + * emitted by the returned logger. This is used to identify the source or scope + * of log messages (e.g., `{ type: "ImportService" }` or `{ accountId: "123" }`). + * + * @returns A SemanticLogger instance that includes the provided context in all log output. + * + * @remarks + * By convention, avoid using the following field names in the context object, as they + * may conflict with fields added by the semantic logging implementation: + * - `message` - The log message text + * - `level` - The log level (debug, info, warn, error, panic) + * - `provider` - The logging provider identifier + * - `content` - Additional data passed to individual log calls + * + * Note: These field names are not enforced at compile-time or runtime, but using them + * may result in unexpected behavior or field name collisions in log output. + * + * @example + * ```typescript + * // Create a logger for a service + * const log = logProvider({ type: "ImportService" }); + * + * // All logs from this logger will include { type: "ImportService" } + * log.debug("Starting import"); + * // Output: { type: "ImportService", level: "debug", message: "Starting import" } + * + * log.info({ itemCount: 42 }, "Import complete"); + * // Output: { type: "ImportService", level: "info", content: { itemCount: 42 }, message: "Import complete" } + * ``` + */ +export type LogProvider = (context: Jsonify) => SemanticLogger; diff --git a/libs/logging/src/logging.spec.ts b/libs/logging/src/logging.spec.ts index 04a057a156fb..168d13a2369b 100644 --- a/libs/logging/src/logging.spec.ts +++ b/libs/logging/src/logging.spec.ts @@ -1,8 +1,93 @@ +import { mock } from "jest-mock-extended"; + import * as lib from "./index"; +import { SemanticLogger } from "./index"; + +describe("logging module", () => { + describe("public API", () => { + it("should export LogService", () => { + expect(lib.LogService).toBeDefined(); + }); + + it("should export LogLevel", () => { + expect(lib.LogLevel).toBeDefined(); + }); + + it("should export ConsoleLogService", () => { + expect(lib.ConsoleLogService).toBeDefined(); + }); + + it("should export DISABLED_LOGGER", () => { + expect(lib.DISABLED_LOGGER).toBeDefined(); + }); + + it("should export disabledSemanticLoggerProvider", () => { + expect(lib.disabledSemanticLoggerProvider).toBeDefined(); + }); + + it("should export consoleSemanticLoggerProvider", () => { + expect(lib.consoleSemanticLoggerProvider).toBeDefined(); + }); + + it("should export enableLogForTypes", () => { + expect(lib.enableLogForTypes).toBeDefined(); + }); + + it("should export ifEnabledSemanticLoggerProvider", () => { + expect(lib.ifEnabledSemanticLoggerProvider).toBeDefined(); + }); + }); + + describe("SemanticLogger", () => { + let logger: SemanticLogger; + + beforeEach(() => { + logger = mock(); + }); + + describe("logging methods", () => { + it("should accept a message string", () => { + logger.debug("debug message"); + logger.info("info message"); + logger.warn("warn message"); + logger.error("error message"); + + expect(logger.debug).toHaveBeenCalledWith("debug message"); + expect(logger.info).toHaveBeenCalledWith("info message"); + expect(logger.warn).toHaveBeenCalledWith("warn message"); + expect(logger.error).toHaveBeenCalledWith("error message"); + }); + + it("should accept content object and optional message", () => { + logger.debug({ step: 1 }, "processing step"); + logger.info({ count: 42 }, "items processed"); + logger.warn({ threshold: 100 }, "approaching limit"); + logger.error({ code: 500 }, "server error"); + + expect(logger.debug).toHaveBeenCalledWith({ step: 1 }, "processing step"); + expect(logger.info).toHaveBeenCalledWith({ count: 42 }, "items processed"); + expect(logger.warn).toHaveBeenCalledWith({ threshold: 100 }, "approaching limit"); + expect(logger.error).toHaveBeenCalledWith({ code: 500 }, "server error"); + }); + }); + + describe("panic", () => { + beforeEach(() => { + logger.panic = jest.fn((content: any, msg?: string) => { + const errorMsg = msg || (typeof content === "string" ? content : "panic"); + throw new Error(errorMsg); + }) as any; + }); + + it("should throw when called with a message", () => { + expect(() => logger.panic("critical error")).toThrow("critical error"); + }); -describe("logging", () => { - // This test will fail until something is exported from index.ts - it("should work", () => { - expect(lib).toBeDefined(); + it("should throw when called with content and message", () => { + expect(() => logger.panic({ reason: "invalid state" }, "system panic")).toThrow( + "system panic", + ); + }); + }); }); }); diff --git a/libs/common/src/tools/log/semantic-logger.abstraction.ts b/libs/logging/src/semantic-logger.abstraction.ts similarity index 100% rename from libs/common/src/tools/log/semantic-logger.abstraction.ts rename to libs/logging/src/semantic-logger.abstraction.ts diff --git a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts index 9ec0e636f9ad..36cdab22d3e6 100644 --- a/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history-dialog.component.ts @@ -15,14 +15,14 @@ import { import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - SemanticLogger, - disabledSemanticLoggerProvider, - ifEnabledSemanticLoggerProvider, -} from "@bitwarden/common/tools/log"; import { UserId } from "@bitwarden/common/types/guid"; import { ButtonModule, DialogModule, DialogService } from "@bitwarden/components"; import { GeneratorHistoryService } from "@bitwarden/generator-history"; +import { + disabledSemanticLoggerProvider, + ifEnabledSemanticLoggerProvider, + SemanticLogger, +} from "@bitwarden/logging"; import { CredentialGeneratorHistoryComponent as CredentialGeneratorHistoryToolsComponent } from "./credential-generator-history.component"; import { EmptyCredentialHistoryComponent } from "./empty-credential-history.component"; diff --git a/libs/tools/generator/components/src/credential-generator-history.component.ts b/libs/tools/generator/components/src/credential-generator-history.component.ts index 3965b2be83e6..82fcdc1cf8c9 100644 --- a/libs/tools/generator/components/src/credential-generator-history.component.ts +++ b/libs/tools/generator/components/src/credential-generator-history.component.ts @@ -8,11 +8,6 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { - SemanticLogger, - disabledSemanticLoggerProvider, - ifEnabledSemanticLoggerProvider, -} from "@bitwarden/common/tools/log"; import { UserId } from "@bitwarden/common/types/guid"; import { ColorPasswordModule, @@ -22,6 +17,11 @@ import { } from "@bitwarden/components"; import { AlgorithmsByType, CredentialGeneratorService } from "@bitwarden/generator-core"; import { GeneratedCredential, GeneratorHistoryService } from "@bitwarden/generator-history"; +import { + disabledSemanticLoggerProvider, + ifEnabledSemanticLoggerProvider, + SemanticLogger, +} from "@bitwarden/logging"; import { GeneratorModule } from "./generator.module"; import { translate } from "./util"; diff --git a/libs/tools/generator/components/src/credential-generator.component.ts b/libs/tools/generator/components/src/credential-generator.component.ts index 78b803392dfa..4b180472a53f 100644 --- a/libs/tools/generator/components/src/credential-generator.component.ts +++ b/libs/tools/generator/components/src/credential-generator.component.ts @@ -31,11 +31,6 @@ import { Account, AccountService } from "@bitwarden/common/auth/abstractions/acc import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { VendorId } from "@bitwarden/common/tools/extension"; -import { - SemanticLogger, - disabledSemanticLoggerProvider, - ifEnabledSemanticLoggerProvider, -} from "@bitwarden/common/tools/log"; import { UserId } from "@bitwarden/common/types/guid"; import { ToastService, Option } from "@bitwarden/components"; import { @@ -55,6 +50,11 @@ import { Type, } from "@bitwarden/generator-core"; import { GeneratorHistoryService } from "@bitwarden/generator-history"; +import { + disabledSemanticLoggerProvider, + ifEnabledSemanticLoggerProvider, + SemanticLogger, +} from "@bitwarden/logging"; import { translate } from "./util"; diff --git a/libs/tools/generator/components/src/generator-services.module.ts b/libs/tools/generator/components/src/generator-services.module.ts index 935f7dc2d60c..c8f846cd533e 100644 --- a/libs/tools/generator/components/src/generator-services.module.ts +++ b/libs/tools/generator/components/src/generator-services.module.ts @@ -1,9 +1,10 @@ import { NgModule } from "@angular/core"; -import { from, take } from "rxjs"; +import { from } from "rxjs"; +import { take } from "rxjs/operators"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; -import { SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens"; +import { LOG_PROVIDER, SafeInjectionToken } from "@bitwarden/angular/services/injection-tokens"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -22,12 +23,7 @@ import { DefaultFields, DefaultSites, Extension } from "@bitwarden/common/tools/ import { RuntimeExtensionRegistry } from "@bitwarden/common/tools/extension/runtime-extension-registry"; import { VendorExtensions, Vendors } from "@bitwarden/common/tools/extension/vendor"; import { RestClient } from "@bitwarden/common/tools/integration/rpc"; -import { - LogProvider, - disabledSemanticLoggerProvider, - enableLogForTypes, -} from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { DefaultEnvService, EnvService } from "@bitwarden/common/tools/providers"; import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; import { BuiltIn, @@ -38,22 +34,33 @@ import { DefaultCredentialGeneratorService, } from "@bitwarden/generator-core"; import { KeyService } from "@bitwarden/key-management"; +import { disabledSemanticLoggerProvider, enableLogForTypes, LogProvider } from "@bitwarden/logging"; export const RANDOMIZER = new SafeInjectionToken("Randomizer"); const GENERATOR_SERVICE_PROVIDER = new SafeInjectionToken( "CredentialGeneratorProviders", ); -// FIXME: relocate the system service provider to a more general module once -// NX migration is complete. -export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken( - "SystemServices", -); - /** Shared module containing generator component dependencies */ @NgModule({ imports: [JslibModule], providers: [ + safeProvider({ + provide: EnvService, + useClass: DefaultEnvService, + deps: [ConfigService, PlatformUtilsService], + }), + safeProvider({ + provide: LOG_PROVIDER, + useFactory: (logger: LogService, env: EnvService) => { + if (env.isDev()) { + return enableLogForTypes(logger, []); + } else { + return disabledSemanticLoggerProvider; + } + }, + deps: [LogService, EnvService], + }), safeProvider({ provide: RANDOMIZER, useFactory: createRandomizer, @@ -83,51 +90,29 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken { - let log: LogProvider; - if (environment.isDev()) { - log = enableLogForTypes(logger, []); - } else { - log = disabledSemanticLoggerProvider; - } - - const extension = new ExtensionService(registry, { + return new ExtensionService(registry, { encryptor, state, log, now: Date.now, }); - - return { - policy, - extension, - log, - configService, - }; }, - deps: [ - LegacyEncryptorProvider, - StateProvider, - PolicyService, - ExtensionRegistry, - LogService, - PlatformUtilsService, - ConfigService, - ], + deps: [ExtensionRegistry, LegacyEncryptorProvider, StateProvider, LOG_PROVIDER], }), safeProvider({ provide: GENERATOR_SERVICE_PROVIDER, useFactory: ( - system: SystemServiceProvider, + policy: PolicyService, + extension: ExtensionService, + log: LogProvider, + configService: ConfigService, random: Randomizer, encryptor: LegacyEncryptorProvider, state: StateProvider, @@ -137,23 +122,27 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken (featureFlag = ff)); + const metadata = new providers.GeneratorMetadataProvider( userStateDeps, - system, + policy, + extension, Object.values(BuiltIn), ); - const sdkService = featureFlag ? system.sdk : undefined; - const profile = new providers.GeneratorProfileProvider(userStateDeps, system.policy); + const sdkService: undefined = featureFlag ? undefined : undefined; + const profile = new providers.GeneratorProfileProvider(userStateDeps, policy); const generator: providers.GeneratorDependencyProvider = { randomizer: random, @@ -166,7 +155,7 @@ export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken(); const SomeExtensionService = mock(); -const SomeConfigService = mock; - -const SomeSdkService = mock; - -const ApplicationProvider = { - /** Policy configured by the administrative console */ - policy: SomePolicyService, - - /** Client extension metadata and profile access */ - extension: SomeExtensionService, - - /** Event monitoring and diagnostic interfaces */ - log: disabledSemanticLoggerProvider, - - /** Feature flag retrieval */ - configService: SomeConfigService, - - /** SDK access for password generation */ - sdk: SomeSdkService, -} as unknown as SystemServiceProvider; - describe("GeneratorMetadataProvider", () => { beforeEach(() => { jest.resetAllMocks(); @@ -121,17 +97,26 @@ describe("GeneratorMetadataProvider", () => { describe("constructor", () => { it("throws when the forwarder site isn't defined by the extension service", () => { SomeExtensionService.site.mockReturnValue(undefined); - expect(() => new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [])).toThrow( - "forwarder extension site not found", - ); + expect( + () => + new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ), + ).toThrow("forwarder extension site not found"); }); }); describe("metadata", () => { it("returns algorithm metadata", async () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [ - password, - ]); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [password], + ); const metadata = provider.metadata(password.id); @@ -145,13 +130,15 @@ describe("GeneratorMetadataProvider", () => { host: { authentication: true, selfHost: "maybe", baseUrl: "https://www.example.com" }, requestedFields: [], }; - const application = { - ...ApplicationProvider, - extension: mock({ - site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])), - }), - }; - const provider = new GeneratorMetadataProvider(SystemProvider, application, []); + const extension = mock({ + site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])), + }); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + extension, + [], + ); const metadata = provider.metadata({ forwarder: Bitwarden.id }); @@ -159,13 +146,23 @@ describe("GeneratorMetadataProvider", () => { }); it("panics when metadata not found", async () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); expect(() => provider.metadata("not found" as any)).toThrow("metadata not found"); }); it("panics when an extension not found", async () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); expect(() => provider.metadata({ forwarder: "not found" as any })).toThrow( "extension not found", @@ -175,7 +172,12 @@ describe("GeneratorMetadataProvider", () => { describe("types", () => { it("returns the credential types", async () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); const result = provider.types(); @@ -185,7 +187,12 @@ describe("GeneratorMetadataProvider", () => { describe("algorithms", () => { it("returns the password category's algorithms", () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); const result = provider.algorithms({ type: Type.password }); @@ -193,7 +200,12 @@ describe("GeneratorMetadataProvider", () => { }); it("returns the username category's algorithms", () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); const result = provider.algorithms({ type: Type.username }); @@ -201,7 +213,12 @@ describe("GeneratorMetadataProvider", () => { }); it("returns the email category's algorithms", () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); const result = provider.algorithms({ type: Type.email }); @@ -215,13 +232,15 @@ describe("GeneratorMetadataProvider", () => { host: { authentication: true, selfHost: "maybe", baseUrl: "https://www.example.com" }, requestedFields: [], }; - const application = { - ...ApplicationProvider, - extension: mock({ - site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])), - }), - }; - const provider = new GeneratorMetadataProvider(SystemProvider, application, []); + const extension = mock({ + site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])), + }); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + extension, + [], + ); const result = provider.algorithms({ type: Type.email }); @@ -235,7 +254,12 @@ describe("GeneratorMetadataProvider", () => { [Algorithm.plusAddress], [Algorithm.username], ])("returns explicit algorithms (=%p)", (algorithm) => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); const result = provider.algorithms({ algorithm }); @@ -249,13 +273,15 @@ describe("GeneratorMetadataProvider", () => { host: { authentication: true, selfHost: "maybe", baseUrl: "https://www.example.com" }, requestedFields: [], }; - const application = { - ...ApplicationProvider, - extension: mock({ - site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])), - }), - }; - const provider = new GeneratorMetadataProvider(SystemProvider, application, []); + const extension = mock({ + site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])), + }); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + extension, + [], + ); const result = provider.algorithms({ algorithm: { forwarder: Bitwarden.id } }); @@ -263,7 +289,12 @@ describe("GeneratorMetadataProvider", () => { }); it("returns an empty array when the algorithm is invalid", () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); // `any` cast required because this test subverts the type system const result = provider.algorithms({ algorithm: "an invalid algorithm" as any }); @@ -278,13 +309,15 @@ describe("GeneratorMetadataProvider", () => { host: { authentication: true, selfHost: "maybe", baseUrl: "https://www.example.com" }, requestedFields: [], }; - const application = { - ...ApplicationProvider, - extension: mock({ - site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])), - }), - }; - const provider = new GeneratorMetadataProvider(SystemProvider, application, []); + const extension = mock({ + site: () => new ExtensionSite(SomeSite, new Map([[Bitwarden.id, extensionMetadata]])), + }); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + extension, + [], + ); // `any` cast required because this test subverts the type system const result = provider.algorithms({ @@ -295,7 +328,12 @@ describe("GeneratorMetadataProvider", () => { }); it("panics when neither an algorithm nor a category is specified", () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); // `any` cast required because this test subverts the type system expect(() => provider.algorithms({} as any)).toThrow("algorithm or type required"); @@ -309,9 +347,12 @@ describe("GeneratorMetadataProvider", () => { [Algorithm.password, password], ])("gets a specific algorithm", async (algorithm, metadata) => { SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([])); - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [ - metadata, - ]); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [metadata], + ); const result = new ReplaySubject(1); provider.algorithms$({ algorithm }, { account$: SomeAccount$ }).subscribe(result); @@ -325,7 +366,12 @@ describe("GeneratorMetadataProvider", () => { [Type.password, [password, passphrase]], ])("gets a category of algorithms", async (category, metadata) => { SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([])); - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, metadata); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + metadata, + ); const result = new ReplaySubject(1); provider.algorithms$({ type: category }, { account$: SomeAccount$ }).subscribe(result); @@ -344,7 +390,12 @@ describe("GeneratorMetadataProvider", () => { } as any); SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([policy])); const metadata = [password, passphrase]; - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, metadata); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + metadata, + ); const algorithmResult = new ReplaySubject(1); const categoryResult = new ReplaySubject(1); @@ -361,9 +412,12 @@ describe("GeneratorMetadataProvider", () => { it("omits algorithms whose metadata is unavailable", async () => { SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([])); - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [ - password, - ]); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [password], + ); const algorithmResult = new ReplaySubject(1); const categoryResult = new ReplaySubject(1); @@ -379,7 +433,12 @@ describe("GeneratorMetadataProvider", () => { }); it("panics when neither algorithm nor category are specified", () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); expect(() => provider.algorithms$({} as any, { account$: SomeAccount$ })).toThrow( "algorithm or type required", @@ -403,9 +462,12 @@ describe("GeneratorMetadataProvider", () => { [Type.password, password], ])("emits the user's %s preference", async (type, metadata) => { SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([])); - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [ - metadata, - ]); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [metadata], + ); const result = new ReplaySubject(1); provider.preference$(type, { account$: SomeAccount$ }).subscribe(result); @@ -415,9 +477,12 @@ describe("GeneratorMetadataProvider", () => { it("emits a default when the user's preference is unavailable", async () => { SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([])); - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, [ - plusAddress, - ]); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [plusAddress], + ); const result = new ReplaySubject(1); // precondition: the preferred email is excluded from the provided metadata @@ -430,7 +495,12 @@ describe("GeneratorMetadataProvider", () => { it("emits the original preference when the user's preference is unavailable and there is no metadata", async () => { SomePolicyService.policiesByType$.mockReturnValue(new BehaviorSubject([])); - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); const result = new ReplaySubject(1); provider.preference$(Type.email, { account$: SomeAccount$ }).subscribe(result); @@ -441,7 +511,12 @@ describe("GeneratorMetadataProvider", () => { describe("preferences", () => { it("returns a user state subject", () => { - const provider = new GeneratorMetadataProvider(SystemProvider, ApplicationProvider, []); + const provider = new GeneratorMetadataProvider( + SystemProvider, + SomePolicyService, + SomeExtensionService, + [], + ); const subject = provider.preferences({ account$: SomeAccount$ }); diff --git a/libs/tools/generator/core/src/providers/generator-metadata-provider.ts b/libs/tools/generator/core/src/providers/generator-metadata-provider.ts index 52901545023e..60b29ab4598e 100644 --- a/libs/tools/generator/core/src/providers/generator-metadata-provider.ts +++ b/libs/tools/generator/core/src/providers/generator-metadata-provider.ts @@ -1,14 +1,15 @@ import { Observable, distinctUntilChanged, map, shareReplay, switchMap, takeUntil } from "rxjs"; +import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { BoundDependency } from "@bitwarden/common/tools/dependencies"; import { ExtensionSite } from "@bitwarden/common/tools/extension"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { ExtensionService } from "@bitwarden/common/tools/extension/extension.service"; import { anyComplete, memoizedMap, pin } from "@bitwarden/common/tools/rx"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; +import { SemanticLogger } from "@bitwarden/logging"; import { GeneratorMetadata, @@ -37,16 +38,18 @@ import { PREFERENCES } from "./credential-preferences"; export class GeneratorMetadataProvider { /** Instantiates the context provider * @param system dependency providers for user state subjects - * @param application dependency providers for system services + * @param policy dependency for policy service + * @param extension dependency for extension service */ constructor( private readonly system: UserStateSubjectDependencyProvider, - private readonly application: SystemServiceProvider, + private readonly policy: PolicyService, + private readonly extension: ExtensionService, algorithms: ReadonlyArray>, ) { this.log = system.log({ type: "GeneratorMetadataProvider" }); - const site = application.extension.site("forwarder"); + const site = extension.site("forwarder"); if (!site) { this.log.panic("forwarder extension site not found"); } @@ -137,21 +140,19 @@ export class GeneratorMetadataProvider { const available$ = id$.pipe( switchMap((id) => { - const policies$ = this.application.policy - .policiesByType$(PolicyType.PasswordGenerator, id) - .pipe( - map((p) => - availableAlgorithms(p) - .filter((a) => this._metadata.has(a)) - .sort(), - ), - // interning the set transformation lets `distinctUntilChanged()` eliminate - // repeating policy emissions using reference equality - memoizedMap((a) => new Set(a), { key: (a) => a.join(":") }), - distinctUntilChanged(), - // complete policy emissions otherwise `switchMap` holds `available$` open indefinitely - takeUntil(anyComplete(id$)), - ); + const policies$ = this.policy.policiesByType$(PolicyType.PasswordGenerator, id).pipe( + map((p) => + availableAlgorithms(p) + .filter((a) => this._metadata.has(a)) + .sort(), + ), + // interning the set transformation lets `distinctUntilChanged()` eliminate + // repeating policy emissions using reference equality + memoizedMap((a) => new Set(a), { key: (a) => a.join(":") }), + distinctUntilChanged(), + // complete policy emissions otherwise `switchMap` holds `available$` open indefinitely + takeUntil(anyComplete(id$)), + ); return policies$; }), map( diff --git a/libs/tools/generator/core/src/providers/generator-profile-provider.spec.ts b/libs/tools/generator/core/src/providers/generator-profile-provider.spec.ts index 32d99aa8a1f6..360f42e9fb53 100644 --- a/libs/tools/generator/core/src/providers/generator-profile-provider.spec.ts +++ b/libs/tools/generator/core/src/providers/generator-profile-provider.spec.ts @@ -8,12 +8,12 @@ import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { GENERATOR_DISK, UserKeyDefinition } from "@bitwarden/common/platform/state"; import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider"; import { UserEncryptor } from "@bitwarden/common/tools/cryptography/user-encryptor.abstraction"; -import { disabledSemanticLoggerProvider } from "@bitwarden/common/tools/log"; import { PrivateClassifier } from "@bitwarden/common/tools/private-classifier"; import { IdentityConstraint } from "@bitwarden/common/tools/state/identity-state-constraint"; import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; import { StateConstraints } from "@bitwarden/common/tools/types"; import { OrganizationId, PolicyId, UserId } from "@bitwarden/common/types/guid"; +import { disabledSemanticLoggerProvider } from "@bitwarden/logging"; import { FakeStateProvider, FakeAccountService, awaitAsync } from "../../../../../common/spec"; import { CoreProfileMetadata, ProfileContext } from "../metadata/profile-metadata"; diff --git a/libs/tools/generator/core/src/providers/generator-profile-provider.ts b/libs/tools/generator/core/src/providers/generator-profile-provider.ts index 4117d1f2a784..c1a3bc571893 100644 --- a/libs/tools/generator/core/src/providers/generator-profile-provider.ts +++ b/libs/tools/generator/core/src/providers/generator-profile-provider.ts @@ -12,10 +12,10 @@ import { import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { BoundDependency } from "@bitwarden/common/tools/dependencies"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; import { anyComplete } from "@bitwarden/common/tools/rx"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; import { UserStateSubjectDependencyProvider } from "@bitwarden/common/tools/state/user-state-subject-dependency-provider"; +import { SemanticLogger } from "@bitwarden/logging"; import { ProfileContext, CoreProfileMetadata, ProfileMetadata } from "../metadata"; import { GeneratorConstraints } from "../types/generator-constraints"; diff --git a/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts b/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts index 81e7ae6ac636..04ca226b110a 100644 --- a/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts +++ b/libs/tools/generator/core/src/services/default-credential-generator.service.spec.ts @@ -1,12 +1,12 @@ +import { mock } from "jest-mock-extended"; import { BehaviorSubject, Subject, firstValueFrom, of } from "rxjs"; import { Account } from "@bitwarden/common/auth/abstractions/account.service"; -import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service"; import { Site, VendorId } from "@bitwarden/common/tools/extension"; import { Bitwarden } from "@bitwarden/common/tools/extension/vendor/bitwarden"; import { Vendor } from "@bitwarden/common/tools/extension/vendor/data"; -import { SemanticLogger, ifEnabledSemanticLoggerProvider } from "@bitwarden/common/tools/log"; import { UserId } from "@bitwarden/common/types/guid"; +import { LogProvider, SemanticLogger } from "@bitwarden/logging"; import { awaitAsync } from "../../../../../common/spec"; import { @@ -40,16 +40,20 @@ type MockTwoLevelPartial = { describe("DefaultCredentialGeneratorService", () => { let service: DefaultCredentialGeneratorService; let providers: MockTwoLevelPartial; - let system: any; - let log: SemanticLogger; let mockExtension: { settings: jest.Mock }; + let mockLogger: SemanticLogger; + let logProvider: LogProvider; let account: Account; let createService: (overrides?: any) => DefaultCredentialGeneratorService; beforeEach(() => { - log = ifEnabledSemanticLoggerProvider(false, new ConsoleLogService(true), { - from: "DefaultCredentialGeneratorService tests", - }); + mockLogger = mock(); + // Override panic to throw errors as expected by tests + mockLogger.panic = jest.fn((context: any, msg?: string) => { + const errorMsg = msg || (typeof context === "string" ? context : context?.message || "panic"); + throw new Error(errorMsg); + }) as any; + logProvider = () => mockLogger; mockExtension = { settings: jest.fn() }; @@ -61,11 +65,6 @@ describe("DefaultCredentialGeneratorService", () => { name: "Test User", }; - system = { - log: jest.fn().mockReturnValue(log), - extension: mockExtension, - }; - providers = { metadata: { metadata: jest.fn(), @@ -87,7 +86,11 @@ describe("DefaultCredentialGeneratorService", () => { // similar to how the overrides are applied const providersCast = providers as unknown as CredentialGeneratorProviders; - const instance = new DefaultCredentialGeneratorService(providersCast, system); + const instance = new DefaultCredentialGeneratorService( + providersCast, + mockExtension as any, + logProvider, + ); Object.assign(instance, overrides); return instance; }; diff --git a/libs/tools/generator/core/src/services/default-credential-generator.service.ts b/libs/tools/generator/core/src/services/default-credential-generator.service.ts index 453139d284c2..4a1c98c35e67 100644 --- a/libs/tools/generator/core/src/services/default-credential-generator.service.ts +++ b/libs/tools/generator/core/src/services/default-credential-generator.service.ts @@ -18,10 +18,10 @@ import { import { Account } from "@bitwarden/common/auth/abstractions/account.service"; import { BoundDependency, OnDependency } from "@bitwarden/common/tools/dependencies"; import { VendorId } from "@bitwarden/common/tools/extension"; -import { SemanticLogger } from "@bitwarden/common/tools/log"; -import { SystemServiceProvider } from "@bitwarden/common/tools/providers"; +import { ExtensionService } from "@bitwarden/common/tools/extension/extension.service"; import { anyComplete, memoizedMap } from "@bitwarden/common/tools/rx"; import { UserStateSubject } from "@bitwarden/common/tools/state/user-state-subject"; +import { LogProvider, SemanticLogger } from "@bitwarden/logging"; import { CredentialGeneratorService } from "../abstractions"; import { @@ -43,13 +43,15 @@ const THREE_MINUTES = 3 * 60 * 1000; export class DefaultCredentialGeneratorService implements CredentialGeneratorService { /** Instantiate the `DefaultCredentialGeneratorService`. * @param provide application services required by the credential generator. - * @param system low-level services required by the credential generator. + * @param extension service for managing forwarder extensions. + * @param log factory for creating semantic loggers. */ constructor( private readonly provide: CredentialGeneratorProviders, - private readonly system: SystemServiceProvider, + private readonly extension: ExtensionService, + log: LogProvider, ) { - this.log = system.log({ type: "DefaultCredentialGeneratorService" }); + this.log = log({ type: "DefaultCredentialGeneratorService" }); } private readonly log: SemanticLogger; @@ -192,7 +194,7 @@ export class DefaultCredentialGeneratorService implements CredentialGeneratorSer } this.log.info({ profile, vendor, site: activeProfile.site }, "loading extension profile"); - settings = this.system.extension.settings(activeProfile, vendor, dependencies); + settings = this.extension.settings(activeProfile, vendor, dependencies); } else { this.log.info({ profile, algorithm: metadata.id }, "loading generator profile"); settings = this.provide.profile.settings(activeProfile, dependencies);