Skip to content

Commit

Permalink
Merge #11084
Browse files Browse the repository at this point in the history
11084: Add 'secret' to config r=Frassle a=Frassle

<!--- 
Thanks so much for your contribution! If this is your first time contributing, please ensure that you have read the [CONTRIBUTING](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) documentation.
-->

# Description

<!--- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. -->

Fixes project level config correctly type checking secret values. 
Also adds a "secret: true" flag to the config schema to require that the config value is secret (this doesn't currently enforce that the default must be secret because we have no way of setting a secure value in Pulumi.yaml).
This removes the support for `pulumi config list/get` to show project level defaults, they now purely look at stack config. Ideally we'll add this back in but it needs to be done in such a way that we can correctly validate secure config values, but only initializing the decrypter (and thus asking for passphrases etc) if it's needed for the values that list/get is showing.
By default list doesn't do any decryption just transforming any secure value to the string "[secure]". That clearly won't typecheck in the current system if the key is tagged as a number, but what ever system we devise _should_ allow it to typecheck.

## Checklist

<!--- Please provide details if the checkbox below is to be left unchecked. -->
- [x] I have added tests that prove my fix is effective or that my feature works
<!--- 
User-facing changes require a CHANGELOG entry.
-->
- [x] I have run `make changelog` and committed the `changelog/pending/<file>` documenting my change
<!--
If the change(s) in this PR is a modification of an existing call to the Pulumi Service,
then the service should honor older versions of the CLI where this change would not exist.
You must then bump the API version in /pkg/backend/httpstate/client/api.go, as well as add
it to the service.
-->
- [ ] Yes, there are changes in this PR that warrants bumping the Pulumi Service API version
  <!-- `@Pulumi` employees: If yes, you must submit corresponding changes in the service repo. -->


Co-authored-by: Fraser Waters <fraser@pulumi.com>
  • Loading branch information
bors[bot] and Frassle committed Oct 24, 2022
2 parents 7711564 + ea609d5 commit 8a77eb3
Show file tree
Hide file tree
Showing 16 changed files with 283 additions and 90 deletions.
@@ -0,0 +1,4 @@
changes:
- type: fix
scope: engine
description: Fix type validation of stack config with secure values.
23 changes: 12 additions & 11 deletions pkg/cmd/pulumi/config.go
Expand Up @@ -794,21 +794,21 @@ func listConfig(ctx context.Context,
stackName := stack.Ref().Name().String()
// when listing configuration values
// also show values coming from the project
configError := workspace.ValidateStackConfigAndApplyProjectConfig(stackName, project, ps.Config)
if configError != nil {
return configError
err = workspace.ApplyProjectConfig(stackName, project, ps.Config)
if err != nil {
return err
}

cfg := ps.Config

// By default, we will use a blinding decrypter to show "[secret]". If requested, display secrets in plaintext.
decrypter := config.NewBlindingDecrypter()
if cfg.HasSecureValue() && showSecrets {
dec, decerr := getStackDecrypter(stack)
if decerr != nil {
return decerr
stackDecrypter, err := getStackDecrypter(stack)
if err != nil {
return err
}
decrypter = dec
decrypter = stackDecrypter
}

var keys config.KeyArray
Expand Down Expand Up @@ -887,12 +887,13 @@ func getConfig(ctx context.Context, stack backend.Stack, key config.Key, path, j
if err != nil {
return err
}

stackName := stack.Ref().Name().String()
configError := workspace.ValidateStackConfigAndApplyProjectConfig(stackName, project, ps.Config)
if configError != nil {
return configError
// when asking for a configuration value, include values from the project config
err = workspace.ApplyProjectConfig(stackName, project, ps.Config)
if err != nil {
return err
}

cfg := ps.Config

v, ok, err := cfg.Get(key, path)
Expand Down
7 changes: 6 additions & 1 deletion pkg/cmd/pulumi/destroy.go
Expand Up @@ -165,8 +165,13 @@ func newDestroyCmd() *cobra.Command {
return result.FromError(fmt.Errorf("getting stack configuration: %w", err))
}

decrypter, err := sm.Decrypter()
if err != nil {
return result.FromError(fmt.Errorf("getting stack decrypter: %w", err))
}

stackName := s.Ref().Name().String()
configError := workspace.ValidateStackConfigAndApplyProjectConfig(stackName, proj, cfg.Config)
configError := workspace.ValidateStackConfigAndApplyProjectConfig(stackName, proj, cfg.Config, decrypter)
if configError != nil {
return result.FromError(fmt.Errorf("validating stack config: %w", configError))
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/cmd/pulumi/import.go
Expand Up @@ -498,7 +498,12 @@ func newImportCmd() *cobra.Command {
return result.FromError(fmt.Errorf("getting stack configuration: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config)
decrypter, err := sm.Decrypter()
if err != nil {
return result.FromError(fmt.Errorf("getting stack decrypter: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter)
if configErr != nil {
return result.FromError(fmt.Errorf("validating stack config: %w", configErr))
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/cmd/pulumi/logs.go
Expand Up @@ -79,7 +79,12 @@ func newLogsCmd() *cobra.Command {
return fmt.Errorf("getting stack configuration: %w", err)
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config)
decrypter, err := sm.Decrypter()
if err != nil {
return fmt.Errorf("getting stack decrypter: %w", err)
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter)
if configErr != nil {
return fmt.Errorf("validating stack config: %w", configErr)
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/cmd/pulumi/preview.go
Expand Up @@ -154,7 +154,12 @@ func newPreviewCmd() *cobra.Command {
return result.FromError(fmt.Errorf("getting stack configuration: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config)
decrypter, err := sm.Decrypter()
if err != nil {
return result.FromError(fmt.Errorf("getting stack decrypter: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter)
if configErr != nil {
return result.FromError(fmt.Errorf("validating stack config: %w", configErr))
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/cmd/pulumi/refresh.go
Expand Up @@ -152,7 +152,12 @@ func newRefreshCmd() *cobra.Command {
return result.FromError(fmt.Errorf("getting stack configuration: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config)
decrypter, err := sm.Decrypter()
if err != nil {
return result.FromError(fmt.Errorf("getting stack decrypter: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter)
if configErr != nil {
return result.FromError(fmt.Errorf("validating stack config: %w", configErr))
}
Expand Down
14 changes: 12 additions & 2 deletions pkg/cmd/pulumi/up.go
Expand Up @@ -111,7 +111,12 @@ func newUpCmd() *cobra.Command {
return result.FromError(fmt.Errorf("getting stack configuration: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config)
decrypter, err := sm.Decrypter()
if err != nil {
return result.FromError(fmt.Errorf("getting stack decrypter: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter)
if configErr != nil {
return result.FromError(fmt.Errorf("validating stack config: %w", configErr))
}
Expand Down Expand Up @@ -350,7 +355,12 @@ func newUpCmd() *cobra.Command {
return result.FromError(fmt.Errorf("getting stack configuration: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config)
decrypter, err := sm.Decrypter()
if err != nil {
return result.FromError(fmt.Errorf("getting stack decrypter: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter)
if configErr != nil {
return result.FromError(fmt.Errorf("validating stack config: %w", configErr))
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/cmd/pulumi/watch.go
Expand Up @@ -117,7 +117,12 @@ func newWatchCmd() *cobra.Command {
return result.FromError(fmt.Errorf("getting stack configuration: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config)
decrypter, err := sm.Decrypter()
if err != nil {
return result.FromError(fmt.Errorf("getting stack decrypter: %w", err))
}

configErr := workspace.ValidateStackConfigAndApplyProjectConfig(stack, proj, cfg.Config, decrypter)
if configErr != nil {
return result.FromError(fmt.Errorf("validating stack config: %w", configErr))
}
Expand Down
25 changes: 2 additions & 23 deletions pkg/secrets/b64/manager.go
Expand Up @@ -16,9 +16,6 @@
package b64

import (
"context"
"encoding/base64"

"github.com/pulumi/pulumi/pkg/v3/secrets"
"github.com/pulumi/pulumi/sdk/v3/go/common/resource/config"
)
Expand All @@ -34,23 +31,5 @@ type manager struct{}

func (m *manager) Type() string { return Type }
func (m *manager) State() interface{} { return map[string]string{} }
func (m *manager) Encrypter() (config.Encrypter, error) { return &base64Crypter{}, nil }
func (m *manager) Decrypter() (config.Decrypter, error) { return &base64Crypter{}, nil }

type base64Crypter struct{}

func (c *base64Crypter) EncryptValue(ctx context.Context, s string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(s)), nil
}

func (c *base64Crypter) DecryptValue(ctx context.Context, s string) (string, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(b), nil
}

func (c *base64Crypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
return config.DefaultBulkDecrypt(ctx, c, ciphertexts)
}
func (m *manager) Encrypter() (config.Encrypter, error) { return config.Base64Crypter, nil }
func (m *manager) Decrypter() (config.Decrypter, error) { return config.Base64Crypter, nil }
21 changes: 21 additions & 0 deletions sdk/go/common/resource/config/crypt.go
Expand Up @@ -275,3 +275,24 @@ func DefaultBulkDecrypt(ctx context.Context,
}
return secretMap, nil
}

type base64Crypter struct{}

// Base64Crypter is a Crypter that "encrypts" by encoding the string to base64.
var Base64Crypter Crypter = &base64Crypter{}

func (c *base64Crypter) EncryptValue(ctx context.Context, s string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(s)), nil
}

func (c *base64Crypter) DecryptValue(ctx context.Context, s string) (string, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(b), nil
}

func (c *base64Crypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
return DefaultBulkDecrypt(ctx, c, ciphertexts)
}
6 changes: 3 additions & 3 deletions sdk/go/common/resource/config/value.go
Expand Up @@ -140,7 +140,7 @@ func (c Value) ToObject() (interface{}, error) {
}

func (c Value) MarshalJSON() ([]byte, error) {
v, err := c.MarshalValue()
v, err := c.marshalValue()
if err != nil {
return nil, err
}
Expand All @@ -158,7 +158,7 @@ func (c *Value) UnmarshalJSON(b []byte) error {
}

func (c Value) MarshalYAML() (interface{}, error) {
return c.MarshalValue()
return c.marshalValue()
}

func (c *Value) UnmarshalYAML(unmarshal func(interface{}) error) error {
Expand Down Expand Up @@ -202,7 +202,7 @@ func (c *Value) unmarshalValue(unmarshal func(interface{}) error, fix func(inter
return nil
}

func (c Value) MarshalValue() (interface{}, error) {
func (c Value) marshalValue() (interface{}, error) {
if c.object {
return c.unmarshalObjectJSON()
}
Expand Down
104 changes: 86 additions & 18 deletions sdk/go/common/workspace/config.go
Expand Up @@ -64,10 +64,59 @@ func missingProjectConfigurationKeysError(missingProjectKeys []string, stackName
isOrAre)
}

func ValidateStackConfigAndApplyProjectConfig(
type StackName = string
type ProjectConfigKey = string
type StackConfigValidator = func(StackName, ProjectConfigKey, ProjectConfigType, config.Value, config.Decrypter) error

func DefaultStackConfigValidator(
stackName string,
projectConfigKey string,
projectConfigType ProjectConfigType,
stackValue config.Value,
dec config.Decrypter) error {
// First check if the project says this should be secret, and if so that the stack value is
// secure.
if projectConfigType.Secret && !stackValue.Secure() {
validationError := fmt.Errorf(
"Stack '%v' with configuration key '%v' must be encrypted as it's secret",
stackName,
projectConfigKey)
return validationError
}

value, err := stackValue.Value(dec)
if err != nil {
return err
}
// Content will be a JSON string if object is true, so marshal that back into an actual structure
var content interface{} = value
if stackValue.Object() {
err = json.Unmarshal([]byte(value), &content)
if err != nil {
return err
}
}

if !ValidateConfigValue(projectConfigType.Type, projectConfigType.Items, content) {
typeName := InferFullTypeName(projectConfigType.Type, projectConfigType.Items)
validationError := fmt.Errorf(
"Stack '%v' with configuration key '%v' must be of type '%v'",
stackName,
projectConfigKey,
typeName)

return validationError
}

return nil
}

func ValidateStackConfigAndMergeProjectConfig(
stackName string,
project *Project,
stackConfig config.Map) error {
stackConfig config.Map,
lazyDecrypter func() config.Decrypter,
validate StackConfigValidator) error {

if len(project.Config) > 0 {
// only when the project defines config values, do we need to validate the stack config
Expand All @@ -90,6 +139,7 @@ func ValidateStackConfigAndApplyProjectConfig(
}
}

var decrypter config.Decrypter
missingConfigurationKeys := make([]string, 0)
for projectConfigKey, projectConfigType := range project.Config {
var key config.Key
Expand Down Expand Up @@ -143,23 +193,20 @@ func ValidateStackConfigAndApplyProjectConfig(
return setError
}
} else {
// found value on the stack level
// retrieve it and validate it against
// the config defined at the project level
content, contentError := stackValue.MarshalValue()
if contentError != nil {
return contentError
}

if !ValidateConfigValue(projectConfigType.Type, projectConfigType.Items, content) {
typeName := InferFullTypeName(projectConfigType.Type, projectConfigType.Items)
validationError := fmt.Errorf(
"Stack '%v' with configuration key '%v' must be of type '%v'",
stackName,
projectConfigKey,
typeName)
// Validate stack level value against the config defined at the project level
if validate != nil {
// we have a validator
if decrypter == nil && lazyDecrypter != nil {
// initialize the decrypter once
decrypter = lazyDecrypter()
}

return validationError
if decrypter != nil {
validationError := validate(stackName, projectConfigKey, projectConfigType, stackValue, decrypter)
if validationError != nil {
return validationError
}
}
}
}
}
Expand All @@ -172,3 +219,24 @@ func ValidateStackConfigAndApplyProjectConfig(

return nil
}

func ValidateStackConfigAndApplyProjectConfig(
stackName string,
project *Project,
stackConfig config.Map,
dec config.Decrypter) error {
decrypter := func() config.Decrypter {
return dec
}

return ValidateStackConfigAndMergeProjectConfig(
stackName, project, stackConfig, decrypter, DefaultStackConfigValidator)
}

// ApplyConfigDefaults applies the default values for the project configuration onto the stack configuration
// without validating the contents of stack config values.
// This is because sometimes during pulumi config ls and pulumi config get, if users are
// using PassphraseDecrypter, we don't want to always prompt for the values when not necessary
func ApplyProjectConfig(stackName string, project *Project, stackConfig config.Map) error {
return ValidateStackConfigAndMergeProjectConfig(stackName, project, stackConfig, nil, nil)
}

0 comments on commit 8a77eb3

Please sign in to comment.