diff --git a/src/lib/secretsManager/USAGE_EXAMPLES.ts b/src/lib/secretsManager/USAGE_EXAMPLES.txt similarity index 100% rename from src/lib/secretsManager/USAGE_EXAMPLES.ts rename to src/lib/secretsManager/USAGE_EXAMPLES.txt diff --git a/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts b/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts index d76e1db2..314c36fc 100644 --- a/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts +++ b/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts @@ -22,7 +22,8 @@ export class SecretsManagerEncryptedStorage extends AbstractSecretsManagerStorag } async getAll(): Promise { - const allData = this.encryptedStore.getAll(); + const allData = + this.encryptedStore.getAll>(); return Object.values(allData); } diff --git a/src/lib/secretsManager/errors.ts b/src/lib/secretsManager/errors.ts new file mode 100644 index 00000000..62dec904 --- /dev/null +++ b/src/lib/secretsManager/errors.ts @@ -0,0 +1,54 @@ +import { SecretReference } from "./types"; + +export enum SecretsErrorCode { + SAFE_STORAGE_ENCRYPTION_NOT_AVAILABLE = "safe_storage_encryption_not_available", + + PROVIDER_NOT_FOUND = "provider_not_found", + + AUTH_FAILED = "auth_failed", + PERMISSION_DENIED = "permission_denied", + + SECRET_NOT_FOUND = "secret_not_found", + SECRET_FETCH_FAILED = "secret_fetch_failed", + + STORAGE_READ_FAILED = "storage_read_failed", + STORAGE_WRITE_FAILED = "storage_write_failed", + + UNKNOWN = "unknown", +} + +export interface SecretsError { + code: SecretsErrorCode; + message: string; + providerId?: string; + secretRef?: SecretReference; + cause?: Error; // Original error +} + +export type SecretsManagerError = { + type: "error"; + error: SecretsError; +}; + +export type SecretsSuccess = T extends void + ? { type: "success" } + : { type: "success"; data: T }; + +export type SecretsResult = SecretsSuccess | SecretsManagerError; + +export type SecretsResultPromise = Promise>; + +export function createSecretsError( + code: SecretsErrorCode, + message: string, + context?: Omit +): SecretsManagerError { + return { + type: "error", + error: { + code, + message, + ...context, + }, + }; +} diff --git a/src/lib/secretsManager/index.ts b/src/lib/secretsManager/index.ts new file mode 100644 index 00000000..fc17a466 --- /dev/null +++ b/src/lib/secretsManager/index.ts @@ -0,0 +1,112 @@ +import { SecretsManagerEncryptedStorage } from "./encryptedStorage/SecretsManagerEncryptedStorage"; +import { FileBasedProviderRegistry } from "./providerRegistry/FileBasedProviderRegistry"; +import { ProviderChangeCallback } from "./providerRegistry/AbstractProviderRegistry"; +import { SecretsManager } from "./secretsManager"; +import { + SecretProviderConfig, + SecretProviderMetadata, + SecretReference, + SecretValue, +} from "./types"; +import { + createSecretsError, + SecretsErrorCode, + SecretsResultPromise, +} from "./errors"; + +const getSecretsManager = (): SecretsManager => { + if (!SecretsManager.isInitialized()) { + return null as any; + } + return SecretsManager.getInstance(); +}; + +const PROVIDERS_DIRECTORY = "providers"; + +export const initSecretsManager = async (): SecretsResultPromise => { + try { + const secretsStorage = new SecretsManagerEncryptedStorage( + PROVIDERS_DIRECTORY + ); + const registry = new FileBasedProviderRegistry(secretsStorage); + + await SecretsManager.initialize(registry); + + return { + type: "success", + }; + } catch (error) { + if ((error as Error).name === "SafeStorageEncryptionNotAvailable") { + return createSecretsError( + SecretsErrorCode.SAFE_STORAGE_ENCRYPTION_NOT_AVAILABLE, + "Safe storage encryption is not available.", // UI to show OS specific message here + { + cause: error as Error, + } + ); + } + + return createSecretsError( + SecretsErrorCode.UNKNOWN, + "Failed to initialize SecretsManager.", + { + cause: error as Error, + } + ); + } +}; + +export const subscribeToProvidersChange = ( + callback: ProviderChangeCallback +): (() => void) => { + return getSecretsManager().onProvidersChange(callback); +}; + +export const setSecretProviderConfig = async ( + config: SecretProviderConfig +): SecretsResultPromise => { + return getSecretsManager().setProviderConfig(config); +}; + +export const removeSecretProviderConfig = async ( + providerId: string +): SecretsResultPromise => { + return getSecretsManager().removeProviderConfig(providerId); +}; + +export const getSecretProviderConfig = async ( + providerId: string +): SecretsResultPromise => { + return getSecretsManager().getProviderConfig(providerId); +}; + +export const testSecretProviderConnection = async ( + providerId: string +): SecretsResultPromise => { + return getSecretsManager().testProviderConnection(providerId); +}; + +export const getSecretValue = async ( + providerId: string, + ref: SecretReference +): SecretsResultPromise => { + return getSecretsManager().getSecret(providerId, ref); +}; + +export const getSecretValues = async ( + secrets: Array<{ providerId: string; ref: SecretReference }> +): SecretsResultPromise => { + return getSecretsManager().getSecrets(secrets); +}; + +export const refreshSecrets = async ( + providerId: string +): SecretsResultPromise<(SecretValue | null)[]> => { + return getSecretsManager().refreshSecrets(providerId); +}; + +export const listSecretProviders = async (): SecretsResultPromise< + SecretProviderMetadata[] +> => { + return getSecretsManager().listProviders(); +}; diff --git a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts index 0d8bb10f..8525d7b5 100644 --- a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts @@ -1,9 +1,13 @@ -import { SecretProviderConfig, SecretProviderType } from "../types"; +import { + SecretProviderConfig, + SecretProviderMetadata, + SecretProviderType, +} from "../types"; import { AbstractSecretsManagerStorage } from "../encryptedStorage/AbstractSecretsManagerStorage"; import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; export type ProviderChangeCallback = ( - configs: Omit[] + configs: SecretProviderMetadata[] ) => void; export abstract class AbstractProviderRegistry { diff --git a/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts index 703582e3..d330bff5 100644 --- a/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts @@ -17,16 +17,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { private async initProvidersFromStorage(): Promise { const configs = await this.getAllProviderConfigs(); configs.forEach((config) => { - try { - this.providers.set(config.id, createProviderInstance(config)); - } catch (error) { - // TODO error to be propagated - console.log( - "!!!debug", - `Failed to initialize provider for config id: ${config.id}`, - error - ); - } + this.providers.set(config.id, createProviderInstance(config)); // TODO: check if this needs error handling }); } @@ -36,12 +27,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { } async getProviderConfig(id: string): Promise { - try { - return await this.store.get(id); - } catch (error) { - console.error(`Failed to load provider config for id: ${id}`, error); - return null; - } + return this.store.get(id); } async setProviderConfig(config: SecretProviderConfig): Promise { @@ -55,7 +41,9 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { this.providers.delete(id); } - getProvider(providerId: string): AbstractSecretProvider | null { + getProvider( + providerId: string + ): AbstractSecretProvider | null { return this.providers.get(providerId) ?? null; } @@ -88,16 +76,8 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { } for (const [id, config] of Object.entries(data)) { - try { - // recreate provider instance - this.providers.set(id, createProviderInstance(config)); - } catch (error) { - console.log( - "!!!debug", - `Failed to sync provider for config id: ${id}`, - error - ); - } + // recreate provider instance + this.providers.set(id, createProviderInstance(config)); } } @@ -106,7 +86,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { ): void { this.changeCallbacks.forEach((callback) => { const configsMetadata = Object.values(data).map((config) => { - const { config: _, ...metadata } = config; + const { credentials: _, ...metadata } = config; return metadata; }); callback(configsMetadata); diff --git a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts index 0087af99..55e3d651 100644 --- a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts +++ b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts @@ -67,24 +67,16 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { @@ -93,7 +85,9 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { + try { + await this.registry.setProviderConfig(config); + return { type: "success" }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.STORAGE_WRITE_FAILED, + error instanceof Error ? error.message : String(error), + { + providerId: config.id, + cause: error as Error, + } + ); + } } - async removeProviderConfig(id: string) { - await this.registry.deleteProviderConfig(id); + async removeProviderConfig(id: string): SecretsResultPromise { + try { + await this.registry.deleteProviderConfig(id); + return { type: "success" }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.STORAGE_WRITE_FAILED, + error instanceof Error ? error.message : String(error), + { + providerId: id, + cause: error as Error, + } + ); + } } - async getProviderConfig(id: string): Promise { - return this.registry.getProviderConfig(id); + async getProviderConfig( + id: string + ): SecretsResultPromise { + try { + const config = await this.registry.getProviderConfig(id); + return { type: "success", data: config }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.STORAGE_READ_FAILED, + error instanceof Error ? error.message : String(error), + { + providerId: id, + cause: error as Error, + } + ); + } } - async testProviderConnection(id: string): Promise { - const provider = this.registry.getProvider(id); + async testProviderConnection(id: string): SecretsResultPromise { + try { + const provider = this.registry.getProvider(id); - if (!provider) { - throw new Error(`Provider with id ${id} not found`); - } - - const isConnected = await provider.testConnection(); + if (!provider) { + return createSecretsError( + SecretsErrorCode.PROVIDER_NOT_FOUND, + `Provider with id ${id} not found`, + { providerId: id } + ); + } - return isConnected ?? false; + const isConnected = await provider.testConnection(); + return { type: "success", data: isConnected ?? false }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.AUTH_FAILED, + error instanceof Error + ? error.message + : `Failed to test connection for provider ${id}`, + { providerId: id, cause: error as Error } + ); + } } async getSecret( providerId: string, ref: SecretReference - ): Promise { - const provider = this.registry.getProvider(providerId); - if (!provider) { - throw new Error(`Provider with id ${providerId} not found`); + ): SecretsResultPromise { + try { + const provider = this.registry.getProvider(providerId); + if (!provider) { + return createSecretsError( + SecretsErrorCode.PROVIDER_NOT_FOUND, + `Provider with id ${providerId} not found`, + { providerId } + ); + } + const secretValue = await provider.getSecret(ref); + return { type: "success", data: secretValue }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.SECRET_FETCH_FAILED, + error instanceof Error + ? error.message + : `Failed to fetch secret from provider`, + { providerId, secretRef: ref, cause: error as Error } + ); } - - const secretValue = await provider.getSecret(ref); - - return secretValue; } async getSecrets( secrets: Array<{ providerId: string; ref: SecretReference }> - ): Promise { + ): SecretsResultPromise { const providerMap: Map = new Map(); for (const s of secrets) { @@ -112,57 +184,82 @@ export class SecretsManager { const results: SecretValue[] = []; + // Handle partial failures appropriately for (const [providerId, refs] of providerMap.entries()) { + try { + const provider = this.registry.getProvider(providerId); + if (!provider) { + return createSecretsError( + SecretsErrorCode.PROVIDER_NOT_FOUND, + `Provider with id ${providerId} not found`, + { providerId } + ); + } + + const secretValues = await provider.getSecrets(refs); + results.push( + ...secretValues.filter((sv): sv is SecretValue => sv !== null) + ); + } catch (error) { + return createSecretsError( + SecretsErrorCode.SECRET_FETCH_FAILED, + error instanceof Error + ? error.message + : `Failed to fetch secrets from provider ${providerId}`, + { providerId, cause: error as Error } + ); + } + } + + return { type: "success", data: results }; + } + + async refreshSecrets( + providerId: string + ): SecretsResultPromise<(SecretValue | null)[]> { + try { const provider = this.registry.getProvider(providerId); if (!provider) { - // TODO: Error to be handled properly - continue; + return createSecretsError( + SecretsErrorCode.PROVIDER_NOT_FOUND, + `Provider with id ${providerId} not found`, + { providerId } + ); } - const secretValues = await provider.getSecrets(refs); + const secrets = await provider.refreshSecrets(); - results.push( - ...secretValues.filter((sv): sv is SecretValue => sv !== null) + return { type: "success", data: secrets }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.SECRET_FETCH_FAILED, + error instanceof Error + ? error.message + : `Failed to refresh secrets for provider ${providerId}`, + { providerId, cause: error as Error } ); } - - return results; } - async refreshSecrets(providerId: string): Promise<(SecretValue | null)[]> { - const provider = this.registry.getProvider(providerId); - if (!provider) { - throw new Error(`Provider with id ${providerId} not found`); + async listProviders(): SecretsResultPromise< + SecretProviderMetadata[] + > { + try { + const configs = await this.registry.getAllProviderConfigs(); + const configMetadata: SecretProviderMetadata[] = + configs.map(({ credentials: _, ...rest }) => rest); + return { type: "success", data: configMetadata }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.STORAGE_READ_FAILED, + error instanceof Error ? error.message : "Failed to list providers", + { cause: error as Error } + ); } - - return provider.refreshSecrets(); - } - - async listProviders(): Promise[]> { - const configs = await this.registry.getAllProviderConfigs(); - - const configMetadata: Omit[] = configs.map( - ({ config: _, ...rest }) => rest - ); - - return configMetadata; } onProvidersChange(callback: ProviderChangeCallback): () => void { return this.registry.onProvidersChange(callback); } } - -/** - * // At app startup (once): - * await SecretsManager.initialize(registry); - * - * // Everywhere else: - * import { getSecretsManager } from "./secretsManager"; - * const secretsManager = getSecretsManager(); - * await secretsManager.getSecret(...); - */ -export function getSecretsManager(): SecretsManager { - return SecretsManager.getInstance(); -} diff --git a/src/lib/secretsManager/types.ts b/src/lib/secretsManager/types.ts index 8664ab17..1fd1a5bf 100644 --- a/src/lib/secretsManager/types.ts +++ b/src/lib/secretsManager/types.ts @@ -27,6 +27,8 @@ export type SecretProviderConfig = | AWSSecretProviderConfig | HashicorpVaultProviderConfig; +export type SecretProviderMetadata = Omit; + export type SecretReference = AwsSecretReference | VaultSecretReference; export type SecretValue = AwsSecretValue | VaultSecretValue; diff --git a/src/lib/storage/EncryptedElectronStore.ts b/src/lib/storage/EncryptedElectronStore.ts index 5922c0be..2c6aff87 100644 --- a/src/lib/storage/EncryptedElectronStore.ts +++ b/src/lib/storage/EncryptedElectronStore.ts @@ -20,9 +20,11 @@ export class EncryptedElectronStore { constructor(storeName: string) { if (!safeStorage.isEncryptionAvailable()) { - throw new Error( + const error = new Error( "Encryption is not available on this system. Please ensure your operating system's secure storage is properly configured." ); + error.name = "SafeStorageEncryptionNotAvailable"; + throw error; } const storeOptions: Store.Options = { diff --git a/src/main/events.js b/src/main/events.js index 4b7ea513..d8c6395e 100644 --- a/src/main/events.js +++ b/src/main/events.js @@ -23,12 +23,18 @@ import { createOrUpdateAxiosInstance } from "./actions/getProxiedAxios"; // and then build these utilites elsewhere // eslint-disable-next-line import/no-cycle import createTrayMenu from "./main"; -import { SecretsManagerEncryptedStorage } from "../lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage"; -import { FileBasedProviderRegistry } from "../lib/secretsManager/providerRegistry/FileBasedProviderRegistry"; import { - getSecretsManager, - SecretsManager, -} from "../lib/secretsManager/secretsManager"; + getSecretProviderConfig, + getSecretValue, + getSecretValues, + initSecretsManager, + listSecretProviders, + refreshSecrets, + removeSecretProviderConfig, + setSecretProviderConfig, + subscribeToProvidersChange, + testSecretProviderConnection, +} from "../lib/secretsManager"; const getFileCategory = (fileExtension) => { switch (fileExtension) { @@ -276,60 +282,65 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { webAppWindow?.send("helper-server-hit"); }); - let secretsManager = null; - - ipcMain.handle("init-secretsManager", async () => { - const secretsStorage = new SecretsManagerEncryptedStorage("providers"); - const registry = new FileBasedProviderRegistry(secretsStorage); + ipcMain.handle("secretsManager:init", () => { + return initSecretsManager(); + }); - await SecretsManager.initialize(registry); - secretsManager = getSecretsManager(); - return true; + ipcMain.handle("secretsManager:subscribeToProvidersChange", () => { + subscribeToProvidersChange((providers) => { + webAppWindow?.webContents.send( + "secretsManager:providersChanged", + providers + ); + }); }); ipcMain.handle( - "secretsManager:addProviderConfig", - async (event, { config }) => { - await secretsManager.setProviderConfig(config); + "secretsManager:setSecretProviderConfig", + (event, { config }) => { + return setSecretProviderConfig(config); } ); - ipcMain.handle("secretsManager:getProviderConfig", async (event, { id }) => { - const providerConfig = await secretsManager.getProviderConfig(id); - console.log("!!!debug", "getConfig", providerConfig); - return providerConfig; - }); - ipcMain.handle( - "secretsManager:removeProviderConfig", - async (event, { id }) => { - await secretsManager.removeProviderConfig(id); + "secretsManager:getSecretProviderConfig", + (event, { providerId }) => { + return getSecretProviderConfig(providerId); } ); ipcMain.handle( - "secretsManager:testConnection", - async (event, { providerId }) => { - const providerConnection = await secretsManager.testProviderConnection( - providerId - ); + "secretsManager:removeSecretProviderConfig", + (event, { providerId }) => { + return removeSecretProviderConfig(providerId); + } + ); - return providerConnection; + ipcMain.handle( + "secretsManager:testProviderConnection", + (event, { providerId }) => { + return testSecretProviderConnection(providerId); } ); ipcMain.handle( - "secretsManager:resolveSecret", - async (event, { providerId, ref }) => { - console.log("!!!debug", "resolve", { - providerId, - ref, - }); - const secretValue = await secretsManager.getSecret(providerId, ref); - console.log("!!!debug", "resolveSecret value", secretValue); - return secretValue; + "secretsManager:getSecretValue", + (event, { providerId, secretReference }) => { + return getSecretValue(providerId, secretReference); } ); + + ipcMain.handle("secretsManager:getSecretValues", (event, { secrets }) => { + return getSecretValues(secrets); + }); + + ipcMain.handle("secretsManager:refreshSecrets", (event, { providerId }) => { + return refreshSecrets(providerId); + }); + + ipcMain.handle("secretsManager:listSecretProviders", () => { + return listSecretProviders(); + }); }; export const registerMainProcessCommonEvents = () => {