-
Notifications
You must be signed in to change notification settings - Fork 1
feat: password v2 component #90
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
Open
bornast
wants to merge
14
commits into
master
Choose a base branch
from
task/password-v2-component
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
95ef3fe
Update redis component wrong imports
bornast cf9f385
Move password component to v2 folder
bornast ca3548c
Implement password component tests
bornast 410b3e4
Remove backOff config
bornast 23e3e5d
Add legacy prefix to password v1 component
bornast 0373827
Remove legacy prefix from v1 component
bornast 96b281a
Add password namespace
bornast e19018f
Update imports
bornast 201983d
Make password args optional
bornast e12f460
Fix test assertion
bornast e6cc125
Wrap password value using pulumi secret
bornast 73f3bf1
Implement test to check if password is a pulumi secret
bornast 2cf9afe
Remove console logs
bornast 7297f79
Export password using ES module syntax
bornast 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
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,68 @@ | ||
| import * as aws from '@pulumi/aws'; | ||
| import * as pulumi from '@pulumi/pulumi'; | ||
| import * as random from '@pulumi/random'; | ||
| import { commonTags } from '../../../constants'; | ||
|
|
||
| export namespace Password { | ||
| export type Args = { | ||
| value?: pulumi.Input<string>; | ||
| }; | ||
| } | ||
|
|
||
| export class Password extends pulumi.ComponentResource { | ||
| name: string; | ||
| value: pulumi.Output<string>; | ||
| secret: aws.secretsmanager.Secret; | ||
|
|
||
| constructor( | ||
| name: string, | ||
| args: Password.Args = {}, | ||
| opts: pulumi.ComponentResourceOptions = {}, | ||
| ) { | ||
| super('studion:Password', name, {}, opts); | ||
|
|
||
| this.name = name; | ||
| if (args.value) { | ||
| this.value = pulumi.secret(args.value); | ||
| } else { | ||
| const password = new random.RandomPassword( | ||
| `${this.name}-random-password`, | ||
| { | ||
| length: 16, | ||
| overrideSpecial: '_$', | ||
| special: true, | ||
| }, | ||
| { parent: this }, | ||
| ); | ||
| this.value = password.result; | ||
| } | ||
|
|
||
| this.secret = this.createPasswordSecret(this.value); | ||
| this.registerOutputs(); | ||
| } | ||
|
|
||
| private createPasswordSecret(password: pulumi.Input<string>) { | ||
| const project = pulumi.getProject(); | ||
| const stack = pulumi.getStack(); | ||
|
|
||
| const passwordSecret = new aws.secretsmanager.Secret( | ||
| `${this.name}-password-secret`, | ||
| { | ||
| namePrefix: `${stack}/${project}/${this.name}-`, | ||
| tags: commonTags, | ||
| }, | ||
| { parent: this }, | ||
| ); | ||
|
|
||
| const passwordSecretValue = new aws.secretsmanager.SecretVersion( | ||
| `${this.name}-password-secret-value`, | ||
| { | ||
| secretId: passwordSecret.id, | ||
| secretString: password, | ||
| }, | ||
| { parent: this, dependsOn: [passwordSecret] }, | ||
| ); | ||
|
|
||
| return passwordSecret; | ||
| } | ||
| } |
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
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,121 @@ | ||
| import * as assert from 'node:assert'; | ||
| import { InlineProgramArgs } from '@pulumi/pulumi/automation'; | ||
| import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; | ||
| import { | ||
| DescribeSecretCommand, | ||
| GetSecretValueCommand, | ||
| } from '@aws-sdk/client-secrets-manager'; | ||
| import { PasswordTestContext } from './test-context'; | ||
| import { after, before, describe, it } from 'node:test'; | ||
| import * as automation from '../automation'; | ||
|
|
||
| const programArgs: InlineProgramArgs = { | ||
| stackName: 'dev', | ||
| projectName: 'icb-test-password', | ||
| program: () => import('./infrastructure'), | ||
| }; | ||
|
|
||
| describe('Password component deployment', () => { | ||
| const region = process.env.AWS_REGION; | ||
| if (!region) { | ||
| throw new Error('AWS_REGION environment variable is required'); | ||
| } | ||
|
|
||
| const ctx: PasswordTestContext = { | ||
| outputs: {}, | ||
| config: { | ||
| autoGeneratedPasswordName: 'password-test-auto', | ||
| }, | ||
| clients: { | ||
| secretsManager: new SecretsManagerClient({ region }), | ||
| }, | ||
| }; | ||
|
|
||
| before(async () => { | ||
| ctx.outputs = await automation.deploy(programArgs); | ||
| }); | ||
|
|
||
| after(() => automation.destroy(programArgs)); | ||
|
|
||
| it('should create a password component with the correct configuration', async () => { | ||
| const password = ctx.outputs.autoGeneratedPassword.value; | ||
|
|
||
| assert.ok( | ||
| password.secret, | ||
| 'Password component should have secret property', | ||
| ); | ||
| assert.ok(password.value, 'Password component should have value property'); | ||
| assert.strictEqual( | ||
| password.name, | ||
| ctx.config.autoGeneratedPasswordName, | ||
| 'Password name should match input', | ||
| ); | ||
| }); | ||
|
|
||
| it('should create a secret with auto generated password', async () => { | ||
| const password = ctx.outputs.autoGeneratedPassword.value; | ||
|
|
||
| const secretResult = await ctx.clients.secretsManager.send( | ||
| new DescribeSecretCommand({ | ||
| SecretId: password.secret.arn, | ||
| }), | ||
| ); | ||
|
|
||
| assert.ok(secretResult.ARN, 'Secret should exist'); | ||
| assert.ok(secretResult.Name, 'Secret should have a name'); | ||
| assert.ok(secretResult.CreatedDate, 'Secret should have creation date'); | ||
|
|
||
| const expectedPrefix = `${programArgs.stackName}/${programArgs.projectName}/${ctx.config.autoGeneratedPasswordName}-`; | ||
| assert.ok( | ||
| secretResult.Name?.startsWith(expectedPrefix), | ||
| `Secret name should start with ${expectedPrefix}`, | ||
| ); | ||
| }); | ||
|
|
||
| it('should generate a random password with correct format', async () => { | ||
| const password = ctx.outputs.autoGeneratedPassword.value; | ||
|
|
||
| const secretValue = await ctx.clients.secretsManager.send( | ||
| new GetSecretValueCommand({ | ||
| SecretId: password.secret.arn, | ||
| }), | ||
| ); | ||
|
|
||
| const passwordValue = secretValue.SecretString; | ||
| assert.ok(passwordValue, 'Password value should exist'); | ||
| assert.strictEqual( | ||
| passwordValue.length, | ||
| 16, | ||
| 'Password should be 16 characters long', | ||
| ); | ||
| assert.ok(secretValue.VersionId, 'Secret should have a version ID'); | ||
| }); | ||
|
|
||
| it('should create a secret with custom password value', async () => { | ||
| const password = ctx.outputs.customPassword.value; | ||
|
|
||
| const secretValue = await ctx.clients.secretsManager.send( | ||
| new GetSecretValueCommand({ | ||
| SecretId: password.secret.arn, | ||
| }), | ||
| ); | ||
|
|
||
| const passwordValue = secretValue.SecretString; | ||
| assert.strictEqual( | ||
| passwordValue, | ||
| 'customPass!', | ||
| 'Password should match custom value', | ||
| ); | ||
| }); | ||
|
|
||
| it('should have password value as a secret', async () => { | ||
| const autoGeneratePasswordValue = ctx.outputs.autoGeneratePasswordValue; | ||
| assert.ok( | ||
| autoGeneratePasswordValue.secret, | ||
| 'Auto-generated password should be a secret', | ||
| ); | ||
|
|
||
| const customPasswordValue = ctx.outputs.customPasswordValue; | ||
| assert.ok(customPasswordValue.secret, 'Custom password should be a secret'); | ||
| }); | ||
| }); | ||
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,18 @@ | ||
| import { next as studion } from '@studion/infra-code-blocks'; | ||
|
|
||
| const appName = 'password-test'; | ||
|
|
||
| const autoGeneratedPassword = new studion.Password(`${appName}-auto`); | ||
| const autoGeneratePasswordValue = autoGeneratedPassword.value; | ||
|
|
||
| const customPassword = new studion.Password(`${appName}-custom`, { | ||
| value: 'customPass!', | ||
| }); | ||
| const customPasswordValue = customPassword.value; | ||
|
|
||
| export { | ||
| autoGeneratedPassword, | ||
| customPassword, | ||
| autoGeneratePasswordValue, | ||
| customPasswordValue, | ||
| }; |
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,25 @@ | ||
| import { OutputMap } from '@pulumi/pulumi/automation'; | ||
| import { SecretsManagerClient } from '@aws-sdk/client-secrets-manager'; | ||
|
|
||
| interface PasswordTestConfig { | ||
| autoGeneratedPasswordName: string; | ||
| } | ||
|
|
||
| interface ConfigContext { | ||
| config: PasswordTestConfig; | ||
| } | ||
|
|
||
| interface PulumiProgramContext { | ||
| outputs: OutputMap; | ||
| } | ||
|
|
||
| interface AwsContext { | ||
| clients: { | ||
| secretsManager: SecretsManagerClient; | ||
| }; | ||
| } | ||
|
|
||
| export interface PasswordTestContext | ||
| extends ConfigContext, | ||
| PulumiProgramContext, | ||
| AwsContext {} |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we'll be able to test this, but it is worth to investigate - is the password value masked in the output of the Pulumi program?
Note: it should be due to
additionalSecretOutputsoption sent to thesuper, but having that test would be beneficial to capture potential leaks.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've done some investigation and seems like
additionalSecretOutputsisn't even working for custom components as it states in the docs.I had to wrap the value in
pulumi.secret()which behaves the same as output except the returned output is marked as containing sensitive data.I also implemented a test to verify that the password is treated as a secret. To make that test work, I had to export the password output directly from the infrastructure
indexfile. The reason is that the automation api unwraps values, and for objects it only keeps the secret flag at the top level. Because of that, there’s no reliable way to check whether nested properties are secrets once they’re unwrapped.