-
Notifications
You must be signed in to change notification settings - Fork 59
[DB-21] added variable fetching flow and awsSecretsManagerProvider #266
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 20 commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
618198f
added aws sdk
nafees87n 36a2aa7
added refreshSecrets
nafees87n 74de3a0
Merge branch 'initialization' into aws-secrets-manager
nafees87n 19e82b5
fix: webpack config
nafees87n f19b6d4
fix
nafees87n 2c7faa3
Merge branch 'initialization' into aws-secrets-manager
nafees87n 7804ed1
fix
nafees87n 74f3589
fix: class singleton initialization
nafees87n f01d364
fix: SecretReference type
nafees87n c935804
fix: getSecrets
nafees87n f88792e
Merge branch 'initialization' into aws-secrets-manager
nafees87n c848582
fix: getSecrets
nafees87n 94f66c6
Merge branch 'initialization' into aws-secrets-manager
nafees87n 2cb9f34
added fallback
nafees87n 988dd94
Merge branch 'initialization' into aws-secrets-manager
nafees87n 2aba2f9
Merge branch 'initialization' into aws-secrets-manager
nafees87n 71cfafa
fix: cache cleanup
nafees87n 4a648db
Merge branch 'initialization' into aws-secrets-manager
nafees87n 6452f00
fix: initialization
nafees87n a502c64
fix: infinite loop
nafees87n 4e410c4
fix: types
nafees87n 9beaa0a
Merge branch 'initialization' into aws-secrets-manager
nafees87n 6829224
Merge branch 'initialization' into aws-secrets-manager
nafees87n 07b888a
fix: init
nafees87n 491ebdb
Merge branch 'initialization' into aws-secrets-manager
nafees87n b48422c
fix: eslint rules
nafees87n 9e0478e
revert eslint changes
nafees87n 986438a
remove change
nafees87n 3ae4aab
Merge branch 'initialization' into aws-secrets-manager
nafees87n a0d5cf6
Merge branch 'initialization' into aws-secrets-manager
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
Large diffs are not rendered by default.
Oops, something went wrong.
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
82 changes: 76 additions & 6 deletions
82
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 |
|---|---|---|
| @@ -1,27 +1,97 @@ | ||
| import { CachedSecret, ProviderSpecificConfig, SecretProviderType, SecretReference } from "../types"; | ||
| /* eslint-disable no-unused-vars */ | ||
| import { | ||
| ProviderSpecificConfig, | ||
| SecretProviderType, | ||
| SecretReference, | ||
| SecretValue, | ||
| } from "../types"; | ||
|
|
||
| const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour | ||
| const DEFAULT_MAX_CACHE_SIZE = 100; | ||
|
|
||
| export abstract class AbstractSecretProvider { | ||
| protected cache: Map<string, CachedSecret> = new Map(); | ||
| protected cache: Map<string, SecretValue> = new Map(); | ||
|
|
||
| /** Cache TTL in milliseconds. Subclasses can override. */ | ||
| protected cacheTtlMs: number = DEFAULT_CACHE_TTL_MS; | ||
|
|
||
| /** Maximum cache size (Size of the map). Subclasses can override. */ | ||
| protected maxCacheSize: number = DEFAULT_MAX_CACHE_SIZE; | ||
|
|
||
| abstract readonly type: SecretProviderType; | ||
|
|
||
| abstract readonly id: string; | ||
|
|
||
| protected config: ProviderSpecificConfig; | ||
|
|
||
| protected abstract getSecretIdentfier(ref: SecretReference): string; | ||
| protected abstract getCacheKey(ref: SecretReference): string; | ||
|
|
||
| abstract testConnection(): Promise<boolean>; | ||
|
|
||
| abstract getSecret(ref: SecretReference): Promise<string>; | ||
| abstract getSecret(ref: SecretReference): Promise<SecretValue | null>; | ||
|
|
||
| abstract getSecrets(): Promise<string[]>; | ||
| abstract getSecrets(refs: SecretReference[]): Promise<(SecretValue | null)[]>; | ||
|
|
||
| abstract setSecret(): Promise<void>; | ||
|
|
||
| abstract setSecrets(): Promise<void>; | ||
|
|
||
| abstract removeSecret(): Promise<void>; | ||
|
|
||
| abstract removeSecrets(): Promise<void>; | ||
|
|
||
| protected invalidateCache(): void { | ||
| this.cache.clear(); | ||
| } | ||
|
|
||
| protected getCachedSecret(key: string): SecretValue | null { | ||
| const cached = this.cache.get(key); | ||
| if (cached && cached.fetchedAt + this.cacheTtlMs > Date.now()) { | ||
| return cached; | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| protected setCacheEntry(key: string, value: SecretValue): void { | ||
| if (this.maxCacheSize <= 0) { | ||
| return; | ||
| } | ||
|
|
||
| this.evictExpiredEntries(); | ||
|
|
||
| while (this.cache.size >= this.maxCacheSize) { | ||
| const oldestKey = this.cache.keys().next().value; | ||
| if (!oldestKey) { | ||
| break; | ||
| } | ||
| this.cache.delete(oldestKey); | ||
| } | ||
|
|
||
| this.cache.set(key, value); | ||
| } | ||
|
|
||
| protected evictExpiredEntries(): void { | ||
| const now = Date.now(); | ||
| const keysToDelete: string[] = []; | ||
|
|
||
| this.cache.forEach((value, key) => { | ||
| if (value.fetchedAt + this.cacheTtlMs <= now) { | ||
| keysToDelete.push(key); | ||
| } | ||
| }); | ||
|
|
||
| keysToDelete.forEach((key) => this.cache.delete(key)); | ||
| } | ||
|
|
||
| abstract refreshSecrets(): Promise<(SecretValue | null)[]>; | ||
|
|
||
| static validateConfig(config: any): boolean { | ||
| throw new Error("Not implemented"); | ||
| // Base implementation rejects all configs as a fail-safe. | ||
| // Provider implementations must override with specific validation. | ||
| if (!config) { | ||
| return false; | ||
| } | ||
|
|
||
| return false; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
| } | ||
156 changes: 155 additions & 1 deletion
156
src/lib/secretsManager/providerService/awsSecretManagerProvider.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 |
|---|---|---|
| @@ -1,4 +1,158 @@ | ||
| /* eslint-disable no-continue */ | ||
| /* eslint-disable class-methods-use-this */ | ||
| import { | ||
| AwsSecretReference, | ||
| AWSSecretsManagerConfig, | ||
| AwsSecretValue, | ||
| SecretProviderConfig, | ||
| SecretProviderType, | ||
| } from "../types"; | ||
| import { AbstractSecretProvider } from "./AbstractSecretProvider"; | ||
| import { | ||
| GetSecretValueCommand, | ||
| ListSecretsCommand, | ||
| SecretsManagerClient, | ||
| } from "@aws-sdk/client-secrets-manager"; | ||
|
|
||
| export class AWSSecretsManagerProvider extends AbstractSecretProvider {} | ||
| export class AWSSecretsManagerProvider extends AbstractSecretProvider { | ||
| readonly type = SecretProviderType.AWS_SECRETS_MANAGER; | ||
|
|
||
| readonly id: string; | ||
|
|
||
| protected config: AWSSecretsManagerConfig; | ||
|
|
||
| private client: SecretsManagerClient; | ||
|
|
||
| constructor(providerConfig: SecretProviderConfig) { | ||
| super(); | ||
| this.id = providerConfig.id; | ||
| this.config = providerConfig.config as AWSSecretsManagerConfig; | ||
| this.client = new SecretsManagerClient({ | ||
| region: this.config.region, | ||
| credentials: { | ||
| accessKeyId: this.config.accessKeyId, | ||
| secretAccessKey: this.config.secretAccessKey, | ||
| sessionToken: this.config.sessionToken, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| protected getCacheKey(ref: AwsSecretReference): string { | ||
| return `name:${ref.identifier};version:${ref.version ?? "latest"}`; | ||
| } | ||
|
|
||
| async testConnection(): Promise<boolean> { | ||
| if (!AWSSecretsManagerProvider.validateConfig(this.config)) { | ||
| return false; | ||
| } | ||
|
|
||
| try { | ||
| const listSecretsCommand = new ListSecretsCommand({ MaxResults: 1 }); | ||
| const res = await this.client.send(listSecretsCommand); | ||
| console.log("!!!debug", "aws result", res); | ||
|
|
||
| if (res.$metadata.httpStatusCode !== 200) { | ||
| return false; | ||
| } | ||
|
|
||
| return true; | ||
| } catch (err) { | ||
| console.error( | ||
| "!!!debug", | ||
| "aws secrets manager test connection error", | ||
| err | ||
| ); | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| async getSecret(ref: AwsSecretReference): Promise<AwsSecretValue | null> { | ||
| if (!this.client) { | ||
| throw new Error("AWS Secrets Manager client is not initialized."); | ||
| } | ||
|
|
||
| const cacheKey = this.getCacheKey(ref); | ||
| const cachedSecret = this.getCachedSecret(cacheKey) as AwsSecretValue | null; | ||
|
|
||
| if (cachedSecret) { | ||
| console.log("!!!debug", "returning from cache", cachedSecret); | ||
| return cachedSecret; | ||
| } | ||
|
|
||
| const getSecretCommand = new GetSecretValueCommand({ | ||
| SecretId: ref.identifier, | ||
| VersionId: ref.version, | ||
| }); | ||
|
|
||
| const secretResponse = await this.client.send(getSecretCommand); | ||
|
|
||
| if (secretResponse.$metadata.httpStatusCode !== 200) { | ||
| console.error("!!!debug", "Failed to fetch secret", secretResponse); | ||
| return null; | ||
| } | ||
|
|
||
| if (!secretResponse.SecretString) { | ||
| console.error("!!!debug", "SecretString is empty", secretResponse); | ||
| return null; | ||
| } | ||
|
|
||
| const awsSecret: AwsSecretValue = { | ||
| providerId: this.id, | ||
| secretReference: ref, | ||
| fetchedAt: Date.now(), | ||
| name: secretResponse.Name, | ||
| value: secretResponse.SecretString, | ||
| ARN: secretResponse.ARN, | ||
| versionId: secretResponse.VersionId, | ||
| }; | ||
|
|
||
| console.log("!!!debug", "returning after fetching", awsSecret); | ||
|
|
||
| this.setCacheEntry(cacheKey, awsSecret); | ||
|
|
||
| return awsSecret; | ||
| } | ||
|
nafees87n marked this conversation as resolved.
|
||
|
|
||
| async getSecrets( | ||
| refs: AwsSecretReference[] | ||
| ): Promise<(AwsSecretValue | null)[]> { | ||
| if (!this.client) { | ||
| throw new Error("AWS Secrets Manager client is not initialized."); | ||
| } | ||
|
|
||
| // Not using BatchGetSecretValueCommand as it would require additional permissions | ||
| return Promise.all(refs.map((ref) => this.getSecret(ref))); | ||
| } | ||
|
|
||
| async setSecret(): Promise<void> { | ||
| throw new Error("Method not implemented."); | ||
| } | ||
|
|
||
| async setSecrets(): Promise<void> { | ||
| throw new Error("Method not implemented."); | ||
| } | ||
|
|
||
| async removeSecret(): Promise<void> { | ||
| throw new Error("Method not implemented."); | ||
| } | ||
|
|
||
| async removeSecrets(): Promise<void> { | ||
| throw new Error("Method not implemented."); | ||
| } | ||
|
|
||
| async refreshSecrets(): Promise<(AwsSecretValue | null)[]> { | ||
| const allSecretRefs = Array.from(this.cache.values()).map( | ||
| (secret) => secret.secretReference | ||
| ); | ||
|
|
||
| this.invalidateCache(); | ||
|
|
||
| return this.getSecrets(allSecretRefs); | ||
| } | ||
|
|
||
| static validateConfig(config: AWSSecretsManagerConfig): boolean { | ||
| return Boolean( | ||
| config.accessKeyId && config.secretAccessKey && config.region | ||
| ); | ||
| } | ||
| } | ||
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.