Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/lib/secretsManager/baseTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ProviderConfig<T extends SecretProviderType, C> {
* @template T - The provider type
*/
export interface SecretReference<T extends SecretProviderType> {
id: string;
type: T;
alias: string;
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import { SecretProviderConfig } from "../types";
import { SecretProviderConfig, SecretValue } from "../types";

export type StorageChangeCallback = (
data: Record<string, SecretProviderConfig>
export type ProviderStorageChangeCallback = (
providers: Record<string, SecretProviderConfig>
) => void;
export type SecretStorageChangeCallback = (
secrets: Record<string, SecretValue>
) => void;

export abstract class AbstractSecretsManagerStorage {
abstract set(_key: string, _data: SecretProviderConfig): Promise<void>;

abstract get(_key: string): Promise<SecretProviderConfig | null>;
abstract setProviderConfig(
_providerId: string,
_data: SecretProviderConfig
): Promise<void>;
abstract setSecretValue(_secretId: string, _data: SecretValue): Promise<void>;
abstract setSecretValues(
_entries: Record<string, SecretValue>
): Promise<void>;

abstract getAll(): Promise<SecretProviderConfig[]>;
abstract getProviderConfig(
_providerId: string
): Promise<SecretProviderConfig | null>;
abstract getSecretValue(_secretId: string): Promise<SecretValue | null>;

abstract delete(_key: string): Promise<void>;
abstract getAllProviderConfigs(): Promise<SecretProviderConfig[]>;
abstract getAllSecretValues(): Promise<SecretValue[]>;
abstract deleteProviderConfig(_providerId: string): Promise<void>;
abstract deleteSecretValue(_secretId: string): Promise<void>;
abstract deleteSecretValues(_keys: string[]): Promise<void>;

abstract onStorageChange(callback: StorageChangeCallback): () => void;
abstract onProvidersChange(
callback: ProviderStorageChangeCallback
): () => void;
abstract onSecretsChange(callback: SecretStorageChangeCallback): () => void;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {
AbstractSecretsManagerStorage,
StorageChangeCallback,
ProviderStorageChangeCallback,
SecretStorageChangeCallback,
} from "./AbstractSecretsManagerStorage";
import { EncryptedElectronStore } from "../../storage/EncryptedElectronStore";
import { SecretProviderConfig } from "../types";
import { SecretProviderConfig, SecretReference, SecretValue } from "../types";

export class SecretsManagerEncryptedStorage extends AbstractSecretsManagerStorage {
private encryptedStore: EncryptedElectronStore;
Expand All @@ -13,25 +14,82 @@ export class SecretsManagerEncryptedStorage extends AbstractSecretsManagerStorag
this.encryptedStore = new EncryptedElectronStore(storeName);
}

async set(key: string, data: SecretProviderConfig): Promise<void> {
return this.encryptedStore.set<SecretProviderConfig>(key, data);
async setProviderConfig(
providerId: string,
data: SecretProviderConfig
): Promise<void> {
return this.encryptedStore.set<SecretProviderConfig>(
`providers.${providerId}`,
data
);
}

async get(key: string): Promise<SecretProviderConfig | null> {
return this.encryptedStore.get<SecretProviderConfig>(key);
async setSecretValue(secretId: string, data: SecretValue): Promise<void> {
return this.encryptedStore.set<SecretValue>(`secrets.${secretId}`, data);
}

async getAll(): Promise<SecretProviderConfig[]> {
const allData =
this.encryptedStore.getAll<Record<string, SecretProviderConfig>>();
return Object.values(allData);
async setSecretValues(entries: Record<string, SecretValue>): Promise<void> {
const current =
this.encryptedStore.get<Record<string, SecretValue>>("secrets") ?? {};
this.encryptedStore.set<Record<string, SecretValue>>("secrets", {
...current,
...entries,
});
}

async delete(key: string): Promise<void> {
return this.encryptedStore.delete(key);
async getProviderConfig(
providerId: string
): Promise<SecretProviderConfig | null> {
return this.encryptedStore.get<SecretProviderConfig>(
`providers.${providerId}`
);
}

onStorageChange(callback: StorageChangeCallback): () => void {
return this.encryptedStore.onChange<SecretProviderConfig>(callback);
async getSecretValue(secretId: string): Promise<SecretValue | null> {
return this.encryptedStore.get<SecretValue>(`secrets.${secretId}`);
}

async getAllProviderConfigs(): Promise<SecretProviderConfig[]> {
const allProviders =
this.encryptedStore.get<Record<string, SecretProviderConfig>>(
`providers`
);
return Object.values(allProviders ?? {});
}

async getAllSecretValues(): Promise<SecretValue[]> {
const allSecrets =
this.encryptedStore.get<Record<string, SecretValue>>(`secrets`);
return Object.values(allSecrets ?? {});
}

async deleteProviderConfig(providerId: string): Promise<void> {
return this.encryptedStore.delete(`providers.${providerId}`);
}

async deleteSecretValue(secretId: string): Promise<void> {
return this.encryptedStore.delete(`secrets.${secretId}`);
}

async deleteSecretValues(keys: string[]): Promise<void> {
const current =
this.encryptedStore.get<Record<string, SecretValue>>("secrets") ?? {};
for (const key of keys) {
delete current[key];
}
this.encryptedStore.set<Record<string, SecretValue>>("secrets", current);
}

onProvidersChange(callback: ProviderStorageChangeCallback): () => void {
return this.encryptedStore.onKeyChange<
Record<string, SecretProviderConfig>
>("providers", callback);
}

onSecretsChange(callback: SecretStorageChangeCallback): () => void {
return this.encryptedStore.onKeyChange<Record<string, SecretValue>>(
"secrets",
callback
);
}
}
12 changes: 11 additions & 1 deletion src/lib/secretsManager/errors.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SecretReference } from "./types";
import { SecretReference, SecretValue } from "./types";

export enum SecretsErrorCode {
SAFE_STORAGE_ENCRYPTION_NOT_AVAILABLE = "safe_storage_encryption_not_available",
Expand Down Expand Up @@ -38,6 +38,16 @@ export type SecretsResult<T> = SecretsSuccess<T> | SecretsManagerError;

export type SecretsResultPromise<T> = Promise<SecretsResult<T>>;

export interface SecretFetchError {
secretRefId: string;
message: string;
}

export interface FetchSecretsResultData {
secrets: SecretValue[];
errors: SecretFetchError[];
}

export function createSecretsError(
code: SecretsErrorCode,
message: string,
Expand Down
78 changes: 67 additions & 11 deletions src/lib/secretsManager/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { SecretsManagerEncryptedStorage } from "./encryptedStorage/SecretsManagerEncryptedStorage";
import {
AbstractSecretsManagerStorage,
ProviderStorageChangeCallback,
SecretStorageChangeCallback,
} from "./encryptedStorage/AbstractSecretsManagerStorage";
import { FileBasedProviderRegistry } from "./providerRegistry/FileBasedProviderRegistry";
import { ProviderChangeCallback } from "./providerRegistry/AbstractProviderRegistry";
import { SecretsManager } from "./secretsManager";
Expand All @@ -12,23 +17,50 @@ import {
createSecretsError,
SecretsErrorCode,
SecretsResultPromise,
FetchSecretsResultData,
} from "./errors";
import { createProviderInstance } from "./providerService/providerFactory";

export class NoopSecretsManagerStorage extends AbstractSecretsManagerStorage {
async setProviderConfig(): Promise<void> { }
async setSecretValue(): Promise<void> { }
async setSecretValues(): Promise<void> { }
async deleteSecretValues(): Promise<void> { }
async getProviderConfig(): Promise<null> {
return null;
}
async getSecretValue(): Promise<null> {
return null;
}
async getAllProviderConfigs(): Promise<[]> {
return [];
}
async getAllSecretValues(): Promise<[]> {
return [];
}
async deleteProviderConfig(): Promise<void> { }
async deleteSecretValue(): Promise<void> { }
onProvidersChange(_callback: ProviderStorageChangeCallback): () => void {
return () => { };
}
onSecretsChange(_callback: SecretStorageChangeCallback): () => void {
return () => { };
}
}

const getSecretsManager = (): SecretsManager => {
if (!SecretsManager.isInitialized()) {
return null as any;
}
return SecretsManager.getInstance();
};

const PROVIDERS_DIRECTORY = "providers";

export const initSecretsManager = async (): SecretsResultPromise<void> => {
export const initSecretsManager = async (
userId: string
): SecretsResultPromise<void> => {
try {
const secretsStorage = new SecretsManagerEncryptedStorage(
PROVIDERS_DIRECTORY
);
const storeName = `sm-${userId}`;
const secretsStorage = new SecretsManagerEncryptedStorage(storeName);
const registry = new FileBasedProviderRegistry(secretsStorage);

await SecretsManager.initialize(registry);
Expand Down Expand Up @@ -100,10 +132,11 @@ export const getSecretValues = async (
return getSecretsManager().getSecrets(secrets);
};

export const refreshSecrets = async (
providerId: string
): SecretsResultPromise<(SecretValue | null)[]> => {
return getSecretsManager().refreshSecrets(providerId);
export const fetchAndSaveSecrets = async (
providerId: string,
secretRefs: SecretReference[]
): SecretsResultPromise<FetchSecretsResultData> => {
return getSecretsManager().fetchAndSaveSecrets(providerId, secretRefs);
};

export const listSecretProviders = async (): SecretsResultPromise<
Expand All @@ -112,11 +145,27 @@ export const listSecretProviders = async (): SecretsResultPromise<
return getSecretsManager().listProviders();
};

export const removeSecretValue = async (
providerId: string,
ref: SecretReference
): SecretsResultPromise<void> => {
return getSecretsManager().removeSecret(providerId, ref);
};

export const removeSecretValues = async (
secrets: Array<{ providerId: string; ref: SecretReference }>
): SecretsResultPromise<void> => {
return getSecretsManager().removeSecrets(secrets);
};

export const testSecretProviderConnectionWithConfig = async (
config: SecretProviderConfig
): SecretsResultPromise<boolean> => {
try {
const provider = createProviderInstance(config);
const provider = createProviderInstance(
config,
new NoopSecretsManagerStorage()
);
const isConnected = await provider.testConnection();
return { type: "success", data: isConnected ?? false };
} catch (error) {
Expand All @@ -127,3 +176,10 @@ export const testSecretProviderConnectionWithConfig = async (
);
}
};


export const listSecrets = async (
providerId: string
): SecretsResultPromise<SecretValue[]> => {
return getSecretsManager().listSecrets(providerId);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,26 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry {
private async initProvidersFromStorage(): Promise<void> {
const configs = await this.getAllProviderConfigs();
configs.forEach((config) => {
this.providers.set(config.id, createProviderInstance(config)); // TODO: check if this needs error handling
this.providers.set(config.id, createProviderInstance(config, this.store));
});
}

async getAllProviderConfigs(): Promise<SecretProviderConfig[]> {
const allConfigs = this.store.getAll();
return allConfigs;
return this.store.getAllProviderConfigs();
}

async getProviderConfig(id: string): Promise<SecretProviderConfig | null> {
return this.store.get(id);
return this.store.getProviderConfig(id);
}

async setProviderConfig(config: SecretProviderConfig): Promise<void> {
const provider = createProviderInstance(config);
await this.store.set(config.id, config);
const provider = createProviderInstance(config, this.store);
await this.store.setProviderConfig(config.id, config);
this.providers.set(config.id, provider);
}

async deleteProviderConfig(id: string): Promise<void> {
await this.store.delete(id);
await this.store.deleteProviderConfig(id);
this.providers.delete(id);
}

Expand All @@ -56,9 +55,9 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry {
}

private setupStorageListener(): void {
this.store.onStorageChange((data) => {
this.syncProvidersFromStorageData(data);
this.notifyChangeCallbacks(data);
this.store.onProvidersChange((providers) => {
this.syncProvidersFromStorageData(providers);
this.notifyChangeCallbacks(providers);
});
}

Expand All @@ -76,8 +75,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry {
}

for (const [id, config] of Object.entries(data)) {
// recreate provider instance
this.providers.set(id, createProviderInstance(config));
this.providers.set(id, createProviderInstance(config, this.store));
}
}

Expand Down
Loading