-
Notifications
You must be signed in to change notification settings - Fork 57
[DB-19] feat: secretsManager #265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 18 commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
757ca49
init files
nafees87n 0f8cb7a
added external secrets manager interfaces
nafees87n 7b4d869
fix: config type
nafees87n fa080d0
added method
nafees87n 0caccc0
refactor
nafees87n a965650
refactor folder name
nafees87n e68e69f
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n 2f38597
fix: interface reusability
nafees87n bbefd3f
fix
nafees87n a808ba9
added caching service
nafees87n aa26c49
refactored providerRegistry
nafees87n 13f5131
fix: types
nafees87n 2bb53a6
fix
nafees87n 36e1b45
poc code
nafees87n 3b34f7d
added initilization flow
nafees87n bdb67fb
remove unused code
nafees87n b597f38
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n 6968919
fix
nafees87n 1756a21
fix: encrypted storage init
nafees87n e603899
fix: error
nafees87n 4375589
fix: type
nafees87n 0468284
fix: schemas
nafees87n c0b5a93
fix: config read/write
nafees87n 3faf236
fix: provider reading
nafees87n ae99164
added key sanitization
nafees87n d8b7462
fix: deletion result
nafees87n 9ae079b
fix: orphaned indexes in manifest
nafees87n 8de1756
use electron-store
nafees87n 56c2b4b
fix: initialization
nafees87n 0f93b01
fix: file name
nafees87n 11f1ede
fix: encryption code
nafees87n 8ff48a7
cleanup unncessary changes
nafees87n 2a4ca6d
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n 04f496b
wrap in trycatch
nafees87n cbec03c
removed linting changes
nafees87n 555f2f0
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n 63a10fc
fix: new line
nafees87n 88fb8fb
[DB-21] added variable fetching flow and awsSecretsManagerProvider (#…
nafees87n e95012d
convert into generic types
wrongsahil caaf624
fix: types
nafees87n 7bce339
readded vault types
nafees87n 20c9454
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n 75cd56c
adds support for listener in secretsManager (#273)
nafees87n acf3121
[DB-29] added IPC methods for secretsManager (#277)
nafees87n 312b1f6
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
12 changes: 12 additions & 0 deletions
12
src/lib/secretsManager/encryptedStorage/AbstractEncryptedStorage.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| export abstract class AbstractEncryptedStorage { | ||
| abstract initialize(): Promise<void>; | ||
|
|
||
| abstract save<T extends Record<string, any>>( | ||
| key: string, | ||
| data: T | ||
| ): Promise<void>; | ||
|
|
||
| abstract load<T extends Record<string, any>>(key: string): Promise<T>; | ||
|
|
||
| abstract delete(key: string): Promise<void>; | ||
| } |
96 changes: 96 additions & 0 deletions
96
src/lib/secretsManager/encryptedStorage/encryptedFsStorage.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| import { safeStorage } from "electron"; | ||
| import { AbstractEncryptedStorage } from "./AbstractEncryptedStorage"; | ||
| import { | ||
| appendPath, | ||
| createFsResource, | ||
| } from "../../../renderer/actions/local-sync/common-utils"; | ||
| import { | ||
| createFolder, | ||
| deleteFsResource, | ||
| getIfFolderExists, | ||
| parseFileRaw, | ||
| writeContentRaw, | ||
| } from "../../../renderer/actions/local-sync/fs-utils"; | ||
|
|
||
| export class EncryptedFsStorage extends AbstractEncryptedStorage { | ||
| private readonly baseFolderPath: string; | ||
|
|
||
| constructor(baseFolderPath: string) { | ||
| super(); | ||
| this.baseFolderPath = baseFolderPath; | ||
| } | ||
|
|
||
| async initialize(): Promise<void> { | ||
| if (!safeStorage.isEncryptionAvailable()) { | ||
| // Show trouble shooting steps to user | ||
| throw new Error("Encryption is not available on this system. "); | ||
| } | ||
|
|
||
| if (!this.baseFolderPath) { | ||
| throw new Error("Base folder path is not set for EncryptedFsStorage."); | ||
| } | ||
| } | ||
|
|
||
| async save<T extends Record<string, any>>( | ||
| key: string, | ||
| data: T | ||
| ): Promise<void> { | ||
| const stringifiedData = JSON.stringify(data); | ||
| const encryptedData = safeStorage.encryptString(stringifiedData); | ||
|
|
||
| const fsFolderResource = createFsResource({ | ||
| rootPath: this.baseFolderPath, | ||
| path: this.baseFolderPath, | ||
| type: "folder", | ||
| }); | ||
|
|
||
| const providerFolderExists = await getIfFolderExists(fsFolderResource); | ||
|
|
||
| if (!providerFolderExists) { | ||
| await createFolder(fsFolderResource); | ||
| } | ||
|
|
||
| const fsResource = createFsResource({ | ||
| rootPath: this.baseFolderPath, | ||
| path: appendPath(this.baseFolderPath, key), | ||
| type: "file", | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| try { | ||
| await writeContentRaw(fsResource, encryptedData.toString("base64")); | ||
| } catch (err) { | ||
| console.error("!!!debug", "Error writing encrypted data", err); | ||
| } | ||
nafees87n marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| async load<T extends Record<string, any>>(key: string): Promise<T> { | ||
| const fsResource = createFsResource({ | ||
| rootPath: this.baseFolderPath, | ||
| path: appendPath(this.baseFolderPath, key), | ||
| type: "file", | ||
| }); | ||
| const fileContent = await parseFileRaw({ | ||
| resource: fsResource, | ||
| }); | ||
|
|
||
| if (fileContent.type === "error") { | ||
| throw new Error( | ||
| `Failed to load encrypted data for key: ${key}, error: ${fileContent.error.message}` | ||
| ); | ||
| } | ||
|
|
||
| const encryptedBuffer = Buffer.from(fileContent.content, "base64"); | ||
| const decryptedString = safeStorage.decryptString(encryptedBuffer); | ||
| return JSON.parse(decryptedString) as T; | ||
| } | ||
|
|
||
| async delete(key: string): Promise<void> { | ||
| const fsResource = createFsResource({ | ||
| rootPath: this.baseFolderPath, | ||
| path: appendPath(this.baseFolderPath, key), | ||
| type: "file", | ||
| }); | ||
|
|
||
| await deleteFsResource(fsResource); | ||
| } | ||
| } | ||
36 changes: 36 additions & 0 deletions
36
src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| import { SecretProviderConfig, SecretProviderType } from "../types"; | ||
| import { AbstractEncryptedStorage } from "../encryptedStorage/AbstractEncryptedStorage"; | ||
| import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; | ||
|
|
||
| type ProviderManifestItem = { | ||
| id: string; | ||
| type: SecretProviderType; | ||
| }; | ||
|
|
||
| export type ProviderManifest = ProviderManifestItem[]; | ||
|
|
||
| export abstract class AbstractProviderRegistry { | ||
| protected encryptedStorage: AbstractEncryptedStorage; | ||
|
|
||
| protected providers: Map<string, AbstractSecretProvider> = new Map(); | ||
|
|
||
| constructor(encryptedStorage: AbstractEncryptedStorage) { | ||
| this.encryptedStorage = encryptedStorage; | ||
| } | ||
|
|
||
| abstract initialize(): Promise<void>; | ||
|
|
||
| protected abstract loadManifest(): Promise<ProviderManifest>; | ||
|
|
||
| protected abstract saveManifest(manifest: ProviderManifest): Promise<void>; | ||
|
|
||
| abstract getAllProviderConfigs(): Promise<SecretProviderConfig[]>; | ||
|
|
||
| abstract getProviderConfig(id: string): Promise<SecretProviderConfig | null>; | ||
|
|
||
| abstract setProviderConfig(config: SecretProviderConfig): Promise<void>; | ||
|
|
||
| abstract deleteProviderConfig(id: string): Promise<void>; | ||
|
|
||
| abstract getProvider(providerId: string): AbstractSecretProvider | null; | ||
| } |
223 changes: 223 additions & 0 deletions
223
src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| import * as path from "path"; | ||
| import { SecretProviderConfig } from "../types"; | ||
| import { createProviderInstance } from "../providerService/providerFactory"; | ||
| import { | ||
| AbstractProviderRegistry, | ||
| ProviderManifest, | ||
| } from "./AbstractProviderRegistry"; | ||
| import { AbstractEncryptedStorage } from "../encryptedStorage/AbstractEncryptedStorage"; | ||
| import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; | ||
| import { createFsResource } from "../../../renderer/actions/local-sync/common-utils"; | ||
| import { | ||
| createGlobalConfigFolder, | ||
| getIfFileExists, | ||
| getIfFolderExists, | ||
| parseFile, | ||
| writeToGlobalConfig, | ||
| } from "../../../renderer/actions/local-sync/fs-utils"; | ||
| import { | ||
| CORE_CONFIG_FILE_VERSION, | ||
| GLOBAL_CONFIG_FILE_NAME, | ||
| } from "../../../renderer/actions/local-sync/constants"; | ||
| import { GlobalConfigRecordFileType } from "../../../renderer/actions/local-sync/file-types/file-types"; | ||
| import { Static } from "@sinclair/typebox"; | ||
| import { GlobalConfig } from "../../../renderer/actions/local-sync/schemas"; | ||
|
|
||
| // TODO:@nafees check version of config.json | ||
| const MANIFEST_FILENAME = GLOBAL_CONFIG_FILE_NAME; | ||
| export class FileBasedProviderRegistry extends AbstractProviderRegistry { | ||
| private manifestPath: string; | ||
|
|
||
| private configDir: string; | ||
|
|
||
| protected providers: Map<string, AbstractSecretProvider> = new Map(); | ||
|
|
||
| constructor(encryptedStorage: AbstractEncryptedStorage, configDir: string) { | ||
| super(encryptedStorage); | ||
| this.configDir = configDir; | ||
| this.manifestPath = path.join(configDir, MANIFEST_FILENAME); | ||
| } | ||
|
|
||
| getProvider(providerId: string): AbstractSecretProvider | null { | ||
| return this.providers.get(providerId) || null; | ||
| } | ||
|
|
||
| async initialize(): Promise<void> { | ||
| await this.ensureConfigDir(); | ||
| await this.ensureConfigFile(); | ||
| await this.initProvidersFromManifest(); | ||
| } | ||
nafees87n marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private async initProvidersFromManifest() { | ||
| const configs = await this.getAllProviderConfigs(); | ||
| configs.forEach((config) => { | ||
| this.providers.set(config.id, createProviderInstance(config)); | ||
| }); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| async getAllProviderConfigs(): Promise<SecretProviderConfig[]> { | ||
| const providerManifest = await this.loadManifest(); | ||
|
|
||
| console.log("!!!debug", "manifest loaded", providerManifest); | ||
| const configs: SecretProviderConfig[] = []; | ||
|
|
||
| for (const entry of providerManifest) { | ||
| const config = await this.encryptedStorage.load<SecretProviderConfig>( | ||
| entry.id | ||
| ); | ||
|
|
||
| if (config) { | ||
| configs.push(config); | ||
| } else { | ||
| // Should we throw error for this case? | ||
| console.log("!!!debug", "Config not found for entry", entry); | ||
| } | ||
| } | ||
|
|
||
| console.log("!!!debug", "all configs", configs); | ||
| return configs; | ||
| } | ||
|
|
||
| async setProviderConfig(config: SecretProviderConfig): Promise<void> { | ||
| const storageKey = config.id; | ||
|
|
||
| await this.encryptedStorage.save(storageKey, config); | ||
|
|
||
| const manifest = await this.loadManifest(); | ||
| const existingEntryIndex = manifest.findIndex((p) => p.id === config.id); | ||
| if (existingEntryIndex !== -1) { | ||
| manifest[existingEntryIndex] = { id: config.id, type: config.type }; | ||
| } else { | ||
| manifest.push({ id: config.id, type: config.type }); | ||
| } | ||
|
|
||
| await this.saveManifest(manifest); | ||
| this.providers.set(config.id, createProviderInstance(config)); | ||
| } | ||
|
|
||
| async deleteProviderConfig(id: string): Promise<void> { | ||
| const providerManifest = await this.loadManifest(); | ||
| const entry = providerManifest.find((p) => p.id === id); | ||
| if (!entry) return; | ||
|
|
||
| await this.encryptedStorage.delete(id); | ||
|
|
||
| providerManifest.splice(providerManifest.indexOf(entry), 1); | ||
|
|
||
| await this.saveManifest(providerManifest); | ||
| this.providers.delete(id); | ||
| } | ||
|
|
||
| async getProviderConfig(id: string): Promise<SecretProviderConfig | null> { | ||
| const providerManifest = await this.loadManifest(); | ||
| const entry = providerManifest.find((p) => p.id === id); | ||
| if (!entry) return null; | ||
|
|
||
| return this.encryptedStorage.load<SecretProviderConfig>(id); | ||
| } | ||
|
|
||
| private async ensureConfigDir(): Promise<void> { | ||
| try { | ||
| const globalConfigFolderResource = createFsResource({ | ||
| rootPath: this.configDir, | ||
| path: this.configDir, | ||
| type: "folder", | ||
| }); | ||
| const globalConfigFolderExists = await getIfFolderExists( | ||
| globalConfigFolderResource | ||
| ); | ||
|
|
||
| if (!globalConfigFolderExists) { | ||
| await createGlobalConfigFolder(); | ||
| } | ||
| } catch (error) { | ||
| console.error("Failed to create config directory:", error); | ||
| throw new Error("Failed to create config directory."); | ||
| } | ||
| } | ||
|
|
||
| private async ensureConfigFile(): Promise<void> { | ||
| const globalConfigFileResource = createFsResource({ | ||
| rootPath: this.configDir, | ||
| path: this.manifestPath, | ||
| type: "file", | ||
| }); | ||
|
|
||
| const globalConfigFileExists = await getIfFileExists( | ||
| globalConfigFileResource | ||
| ); | ||
|
|
||
| if (!globalConfigFileExists) { | ||
| const config: Static<typeof GlobalConfig> = { | ||
| version: CORE_CONFIG_FILE_VERSION, | ||
| workspaces: [], | ||
| providers: [], | ||
| }; | ||
| const writeResult = await writeToGlobalConfig(config); | ||
|
|
||
| if (writeResult.type === "error") { | ||
| throw new Error("Failed to create manifest file."); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| protected async loadManifest(): Promise<ProviderManifest> { | ||
| const globalConfigFileResource = createFsResource({ | ||
| rootPath: this.configDir, | ||
| path: this.manifestPath, | ||
| type: "file", | ||
| }); | ||
|
|
||
| const globalConfigFileExists = await getIfFileExists( | ||
| globalConfigFileResource | ||
| ); | ||
|
|
||
| if (!globalConfigFileExists) { | ||
| return []; | ||
| } | ||
|
|
||
| const readResult = await parseFile({ | ||
| resource: globalConfigFileResource, | ||
| fileType: new GlobalConfigRecordFileType(), | ||
| }); | ||
|
|
||
| if (readResult.type === "error") { | ||
| throw new Error("Failed to parse manifest file."); | ||
| } | ||
|
|
||
| console.log("!!!debug", "readResult", readResult); | ||
| // TODO:@nafees handle versioning and schema in schema.ts | ||
| return (readResult.content.providers ?? []) as ProviderManifest; | ||
| } | ||
|
|
||
| protected async saveManifest( | ||
| providerManifest: ProviderManifest | ||
| ): Promise<void> { | ||
| const globalConfigFileResource = createFsResource({ | ||
| rootPath: this.configDir, | ||
| path: this.manifestPath, | ||
| type: "file", | ||
| }); | ||
|
|
||
| const readResult = await parseFile({ | ||
| resource: globalConfigFileResource, | ||
| fileType: new GlobalConfigRecordFileType(), | ||
| }); | ||
|
|
||
| if (readResult.type === "error") { | ||
| throw new Error("Failed to parse manifest file."); | ||
| } | ||
|
|
||
| const updatedConfig: Static<typeof GlobalConfig> = { | ||
| version: readResult.content.version || CORE_CONFIG_FILE_VERSION, | ||
| workspaces: readResult.content.workspaces || [], | ||
| providers: providerManifest, | ||
| }; | ||
|
|
||
| const writeResult = await writeToGlobalConfig(updatedConfig); | ||
|
|
||
| if (writeResult.type === "error") { | ||
| throw new Error("Failed to write manifest file."); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
27 changes: 27 additions & 0 deletions
27
src/lib/secretsManager/providerService/AbstractSecretProvider.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| import { CachedSecret, SecretProviderType, SecretReference } from "../types"; | ||
|
|
||
| export abstract class AbstractSecretProvider { | ||
| protected cache: Map<string, CachedSecret> = new Map(); | ||
|
|
||
| abstract readonly type: SecretProviderType; | ||
|
|
||
| abstract readonly id: string; | ||
|
|
||
| protected config: any; | ||
|
|
||
| protected abstract getSecretIdentfier(ref: SecretReference): string; | ||
nafees87n marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| abstract testConnection(): Promise<boolean>; | ||
|
|
||
| abstract getSecret(ref: SecretReference): Promise<string>; | ||
|
|
||
| abstract getSecrets(): Promise<string[]>; | ||
|
|
||
| abstract setSecret(): Promise<void>; | ||
|
|
||
| abstract setSecrets(): Promise<void>; | ||
nafees87n marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| static validateConfig(config: any): boolean { | ||
| throw new Error("Not implemented"); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.