| | | 1 | | import { |
| | | 2 | | GetParameterCommand, |
| | | 3 | | PutParameterCommand, |
| | | 4 | | type SSM, |
| | | 5 | | } from '@aws-sdk/client-ssm'; |
| | | 6 | | import { GetCallerIdentityCommand, type STS } from '@aws-sdk/client-sts'; |
| | | 7 | | import { injectable } from 'inversify'; |
| | | 8 | | import pc from 'picocolors'; |
| | | 9 | | import { EnvironmentVariable } from '../../domain/EnvironmentVariable.js'; |
| | | 10 | | import { |
| | | 11 | | ExpiredCredentialsError, |
| | | 12 | | SecretOperationError, |
| | | 13 | | SsoSessionExpiredError, |
| | | 14 | | } from '../../domain/errors/DomainErrors.js'; |
| | | 15 | | import type { ILogger } from '../../domain/ports/ILogger.js'; |
| | | 16 | | import type { ISecretProvider } from '../../domain/ports/ISecretProvider.js'; |
| | | 17 | | import { describeError } from '../describeError.js'; |
| | | 18 | | import { isExpiredCredentialsError } from './isExpiredCredentialsError.js'; |
| | | 19 | | import { isSsoSessionExpiredError } from './isSsoSessionExpiredError.js'; |
| | | 20 | | |
| | 9 | 21 | | @injectable() |
| | 9 | 22 | | export class AwsSsmSecretProvider implements ISecretProvider { |
| | | 23 | | private ssm: SSM; |
| | | 24 | | private logger: ILogger; |
| | | 25 | | private sts: STS; |
| | | 26 | | private profile?: string; |
| | 53 | 27 | | private identityLogged = false; |
| | | 28 | | |
| | | 29 | | constructor(ssm: SSM, logger: ILogger, sts: STS, profile?: string) { |
| | 53 | 30 | | this.ssm = ssm; |
| | 53 | 31 | | this.logger = logger; |
| | 53 | 32 | | this.sts = sts; |
| | 53 | 33 | | this.profile = profile; |
| | | 34 | | } |
| | | 35 | | |
| | | 36 | | async getSecret(name: string): Promise<string | undefined> { |
| | 15 | 37 | | await this.logIdentity(); |
| | 15 | 38 | | try { |
| | 15 | 39 | | const command = new GetParameterCommand({ |
| | | 40 | | Name: name, |
| | | 41 | | WithDecryption: true, |
| | | 42 | | }); |
| | 15 | 43 | | const { Parameter } = await this.ssm.send(command); |
| | 8 | 44 | | return Parameter?.Value; |
| | | 45 | | } catch (error) { |
| | 7 | 46 | | if ( |
| | | 47 | | typeof error === 'object' && |
| | | 48 | | error !== null && |
| | | 49 | | 'name' in error && |
| | | 50 | | error.name === 'ParameterNotFound' |
| | | 51 | | ) { |
| | 2 | 52 | | return undefined; |
| | | 53 | | } |
| | 5 | 54 | | if (isSsoSessionExpiredError(error)) { |
| | 1 | 55 | | throw new SsoSessionExpiredError(this.profile, error); |
| | | 56 | | } |
| | 4 | 57 | | if (isExpiredCredentialsError(error)) { |
| | 1 | 58 | | throw new ExpiredCredentialsError(error); |
| | | 59 | | } |
| | 3 | 60 | | throw new SecretOperationError( |
| | | 61 | | `${EnvironmentVariable.maskSecretPath(name)}: ${describeError(error)}`, |
| | | 62 | | ); |
| | | 63 | | } |
| | | 64 | | } |
| | | 65 | | |
| | | 66 | | async setSecret(name: string, value: string): Promise<void> { |
| | 6 | 67 | | await this.logIdentity(); |
| | 6 | 68 | | const command = new PutParameterCommand({ |
| | | 69 | | Name: name, |
| | | 70 | | Value: value, |
| | | 71 | | Type: 'SecureString', |
| | | 72 | | Overwrite: true, |
| | | 73 | | }); |
| | 6 | 74 | | try { |
| | 6 | 75 | | await this.ssm.send(command); |
| | | 76 | | } catch (error) { |
| | 3 | 77 | | if (isSsoSessionExpiredError(error)) { |
| | 1 | 78 | | throw new SsoSessionExpiredError(this.profile, error); |
| | | 79 | | } |
| | 2 | 80 | | if (isExpiredCredentialsError(error)) { |
| | 1 | 81 | | throw new ExpiredCredentialsError(error); |
| | | 82 | | } |
| | 1 | 83 | | throw new SecretOperationError( |
| | | 84 | | `${EnvironmentVariable.maskSecretPath(name)}: ${describeError(error)}`, |
| | | 85 | | ); |
| | | 86 | | } |
| | | 87 | | } |
| | | 88 | | |
| | | 89 | | async logIdentity(): Promise<void> { |
| | 21 | 90 | | if (this.identityLogged) { |
| | 1 | 91 | | return; |
| | | 92 | | } |
| | 20 | 93 | | this.identityLogged = true; |
| | 20 | 94 | | const [region, account] = await Promise.all([ |
| | | 95 | | this.resolveRegion(), |
| | | 96 | | this.resolveAccount(), |
| | | 97 | | ]); |
| | 20 | 98 | | const profile = this.profile ?? 'default'; |
| | 21 | 99 | | this.logger.info(this.formatIdentityBanner(account, region, profile)); |
| | | 100 | | } |
| | | 101 | | |
| | | 102 | | private formatIdentityBanner( |
| | | 103 | | account: string, |
| | | 104 | | region: string, |
| | | 105 | | profile: string, |
| | | 106 | | ): string { |
| | 20 | 107 | | const sep = pc.dim(' · '); |
| | | 108 | | const accountValue = |
| | 20 | 109 | | account === 'unknown' ? pc.red(account) : pc.green(account); |
| | | 110 | | const regionValue = |
| | 20 | 111 | | region === 'unknown' ? pc.red(region) : pc.yellow(region); |
| | 20 | 112 | | return ( |
| | | 113 | | '\n' + |
| | | 114 | | pc.bold(pc.cyan('☁ AWS identity')) + |
| | | 115 | | sep + |
| | | 116 | | pc.dim('account=') + |
| | | 117 | | accountValue + |
| | | 118 | | sep + |
| | | 119 | | pc.dim('region=') + |
| | | 120 | | regionValue + |
| | | 121 | | sep + |
| | | 122 | | pc.dim('profile=') + |
| | | 123 | | pc.magenta(profile) |
| | | 124 | | ); |
| | | 125 | | } |
| | | 126 | | |
| | | 127 | | private async resolveRegion(): Promise<string> { |
| | 20 | 128 | | try { |
| | 20 | 129 | | return await this.ssm.config.region(); |
| | | 130 | | } catch { |
| | 0 | 131 | | return 'unknown'; |
| | | 132 | | } |
| | | 133 | | } |
| | | 134 | | |
| | | 135 | | private async resolveAccount(): Promise<string> { |
| | 20 | 136 | | const fromCredentials = await this.tryCredentialsAccount(); |
| | 20 | 137 | | if (fromCredentials) { |
| | 14 | 138 | | return fromCredentials; |
| | | 139 | | } |
| | 6 | 140 | | return this.tryStsAccount(); |
| | | 141 | | } |
| | | 142 | | |
| | | 143 | | private async tryCredentialsAccount(): Promise<string | undefined> { |
| | 20 | 144 | | try { |
| | 20 | 145 | | const credentials = await this.ssm.config.credentials(); |
| | 19 | 146 | | return credentials.accountId; |
| | | 147 | | } catch { |
| | 1 | 148 | | return undefined; |
| | | 149 | | } |
| | | 150 | | } |
| | | 151 | | |
| | | 152 | | private async tryStsAccount(): Promise<string> { |
| | 6 | 153 | | try { |
| | 6 | 154 | | const identity = await this.sts.send(new GetCallerIdentityCommand({})); |
| | 5 | 155 | | return identity.Account ?? 'unknown'; |
| | | 156 | | } catch { |
| | 1 | 157 | | return 'unknown'; |
| | | 158 | | } |
| | | 159 | | } |
| | | 160 | | } |