// Copyright 2016-2022, Pulumi Corporation. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package config import ( "bytes" "context" "encoding/json" "errors" "fmt" "time" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/pulumi/pulumi/sdk/v3/go/common/util/contract" ) var errSecureReprReserved = errors.New(`maps with the single key "secure" are reserved`) // object is the internal object representation of a single config value. All operations on Value first decode the // Value's string representation into its object representation. Secure strings are stored in objects as ciphertext. type object struct { value any secure bool } // objectType describes the types of values that may be stored in the value field of an object type objectType interface { bool | int64 | float64 | string | []object | map[string]object } // newObject creates a new object with the given representation. func newObject[T objectType](v T) object { return object{value: v} } // newSecureObject creates a new secure object with the given ciphertext. func newSecureObject(ciphertext string) object { return object{value: ciphertext, secure: true} } // Secure returns true if the receiver is a secure string or a composite value that contains a secure string. func (c object) Secure() bool { switch v := c.value.(type) { case []object: for _, v := range v { if v.Secure() { return true } } return false case map[string]object: for _, v := range v { if v.Secure() { return true } } return false case string: return c.secure default: return false } } // Decrypt decrypts any ciphertexts within the object and returns appropriately-shaped Plaintext values. func (c object) Decrypt(ctx context.Context, decrypter Decrypter) (Plaintext, error) { return c.decrypt(ctx, nil, decrypter) } func (c object) decrypt(ctx context.Context, path resource.PropertyPath, decrypter Decrypter) (Plaintext, error) { switch v := c.value.(type) { case bool: return NewPlaintext(v), nil case int64: return NewPlaintext(v), nil case float64: return NewPlaintext(v), nil case string: if !c.secure { return NewPlaintext(v), nil } plaintext, err := decrypter.DecryptValue(ctx, v) if err != nil { return Plaintext{}, fmt.Errorf("%v: %w", path, err) } return NewSecurePlaintext(plaintext), nil case []object: vs := make([]Plaintext, len(v)) for i, v := range v { pv, err := v.decrypt(ctx, append(path, i), decrypter) if err != nil { return Plaintext{}, err } vs[i] = pv } return NewPlaintext(vs), nil case map[string]object: vs := make(map[string]Plaintext, len(v)) for k, v := range v { pv, err := v.decrypt(ctx, append(path, k), decrypter) if err != nil { return Plaintext{}, err } vs[k] = pv } return NewPlaintext(vs), nil case nil: return Plaintext{}, nil default: contract.Failf("unexpected value of type %T", v) return Plaintext{}, nil } } // Merge merges the receiver onto the given base using JSON merge patch semantics. Merge does not modify the receiver or // the base. func (c object) Merge(base object) object { if co, ok := c.value.(map[string]object); ok { if bo, ok := base.value.(map[string]object); ok { mo := make(map[string]object, len(co)) for k, v := range bo { mo[k] = v } for k, v := range co { mo[k] = v.Merge(mo[k]) } return newObject(mo) } } return c } // Get gets the member value at path. The path to the receiver is prefix. func (c object) Get(path resource.PropertyPath) (_ object, ok bool, err error) { if len(path) == 0 { return c, true, nil } switch v := c.value.(type) { case []object: index, ok := path[0].(int) if !ok || index < 0 || index >= len(v) { return object{}, false, nil } elem := v[index] return elem.Get(path[1:]) case map[string]object: key, ok := path[0].(string) if !ok { return object{}, false, nil } elem, ok := v[key] if !ok { return object{}, false, nil } return elem.Get(path[1:]) default: return object{}, false, nil } } // Delete deletes the member value at path. The path to the receiver is prefix. func (c *object) Delete(prefix, path resource.PropertyPath) error { if len(path) == 0 { return nil } prefix = append(prefix, path[0]) switch v := c.value.(type) { case []object: index, ok := path[0].(int) if !ok || index < 0 || index >= len(v) { return nil } if len(path) == 1 { c.value = append(v[:index], v[index+1:]...) return nil } elem := &v[index] return elem.Delete(prefix, path[1:]) case map[string]object: key, ok := path[0].(string) if !ok { return nil } // If we're deleting a property from this object, make sure that the result won't be mistaken for a secure // value when it is encoded. Secure values are encoded as `{"secure": "ciphertext"}`. if len(path) == 1 { if len(v) == 2 { keys := make([]string, 0, 2) for k := range v { if k != key { keys = append(keys, k) } } if len(keys) == 1 && keys[0] == "secure" { if _, ok := v["secure"].value.(string); ok { return fmt.Errorf("%v: %w", prefix, errSecureReprReserved) } } } delete(v, key) return nil } elem, ok := v[key] if !ok { return nil } err := elem.Delete(prefix, path[1:]) v[key] = elem return err default: return nil } } func newContainer(accessor any) any { switch accessor := accessor.(type) { case int: return make([]object, accessor+1) case string: return make(map[string]object) default: contract.Failf("unexpected accessor kind %T", accessor) return nil } } // Set sets the member value at path to new. The path to the receiver is prefix. func (c *object) Set(prefix, path resource.PropertyPath, new object) error { if len(path) == 0 { *c = new return nil } // Check the type of the receiver and create a new container if allowed. switch c.value.(type) { case []object, map[string]object: // OK case nil: // This value is nil. Create a new container ny inferring the container type (i.e. array or object) from the // accessor at the head of the path. c.value = newContainer(path[0]) default: // COMPAT: If this is the first level, we create a new container and overwrite the old value rather than issuing // a type error. if len(prefix) == 1 { c.value, c.secure = newContainer(path[0]), false } else { switch path[0].(type) { case int: return fmt.Errorf("%v: expected an array", prefix) case string: return fmt.Errorf("%v: expected a map", prefix) default: contract.Failf("unreachable") return nil } } } prefix = append(prefix, path[0]) switch v := c.value.(type) { case []object: index, ok := path[0].(int) if !ok { return fmt.Errorf("%v: key for an array must be an int", prefix) } if index < 0 || index > len(v) { return fmt.Errorf("%v: array index out of range", prefix) } if index == len(v) { v = append(v, object{}) c.value = v } elem := &v[index] return elem.Set(prefix, path[1:], new) case map[string]object: key, ok := path[0].(string) if !ok { return fmt.Errorf("%v: key for a map must be a string", prefix) } // If we're adding a property tothis object, make sure that the result won't be mistaken for a secure // value when it is encoded. Secure values are encoded as `{"secure": "ciphertext"}`. if len(path) == 1 && len(v) == 0 && key == "secure" { if _, ok := new.value.(string); ok { return errSecureReprReserved } } elem := v[key] err := elem.Set(prefix, path[1:], new) v[key] = elem return err default: contract.Failf("unreachable") return nil } } // SecureValues returns the plaintext values for any secure strings contained in the receiver. func (c object) SecureValues(dec Decrypter) ([]string, error) { switch v := c.value.(type) { case []object: var values []string for _, v := range v { vs, err := v.SecureValues(dec) if err != nil { return nil, err } values = append(values, vs...) } return values, nil case map[string]object: var values []string for _, v := range v { vs, err := v.SecureValues(dec) if err != nil { return nil, err } values = append(values, vs...) } return values, nil case string: if c.secure { plaintext, err := dec.DecryptValue(context.TODO(), v) if err != nil { return nil, err } return []string{plaintext}, nil } return nil, nil default: return nil, nil } } // marshalValue converts the receiver into a Value. func (c object) marshalValue() (v Value, err error) { v.value, v.secure, v.object, err = c.MarshalString() return } // marshalObjectValue converts the receiver into a shape that is compatible with Value.ToObject(). func (c object) marshalObjectValue(root bool) any { switch v := c.value.(type) { case []object: vs := make([]any, len(v)) for i, v := range v { vs[i] = v.marshalObjectValue(false) } return vs case map[string]object: vs := make(map[string]any, len(v)) for k, v := range v { vs[k] = v.marshalObjectValue(false) } return vs case string: if !root && c.secure { return map[string]any{"secure": c.value} } return c.value default: return c.value } } // MarshalString returns the receiver's string representation. The string representation is accompanied by bools that // indicate whether the receiver is secure and whether it is an object. func (c object) MarshalString() (text string, secure, object bool, err error) { switch v := c.value.(type) { case bool, int64, float64: bytes, err := c.MarshalJSON() return string(bytes), false, false, err case string: return v, c.secure, false, nil default: bytes, err := c.MarshalJSON() if err != nil { return "", false, false, err } return string(bytes), c.Secure(), true, nil } } // UnmarshalString unmarshals the string representation accompanied by secure and object metadata into the receiver. func (c *object) UnmarshalString(text string, secure, object bool) error { if !object { c.value, c.secure = text, secure return nil } return c.UnmarshalJSON([]byte(text)) } func (c object) MarshalJSON() ([]byte, error) { return json.Marshal(c.marshalObject()) } func (c *object) UnmarshalJSON(b []byte) error { dec := json.NewDecoder(bytes.NewReader(b)) dec.UseNumber() var v any err := dec.Decode(&v) if err != nil { return err } *c, err = unmarshalObject(v) return err } func (c object) MarshalYAML() (any, error) { return c.marshalObject(), nil } func (c *object) UnmarshalYAML(unmarshal func(any) error) error { var v any err := unmarshal(&v) if err != nil { return err } *c, err = unmarshalObject(v) return err } // unmarshalObject unmarshals a raw JSON or YAML value into an object. json.Number values are converted to int64 if // possible and float64 otherwise. func unmarshalObject(v any) (object, error) { switch v := v.(type) { case bool: return newObject(v), nil case json.Number: if i, err := v.Int64(); err == nil { return newObject(i), nil } f, err := v.Float64() if err == nil { return newObject(f), nil } return object{}, fmt.Errorf("unrepresentable number %v: %w", v, err) case int: return newObject(int64(v)), nil case int64: return newObject(v), nil case float64: return newObject(v), nil case string: return newObject(v), nil case time.Time: return newObject(v.String()), nil case map[string]any: if ok, ciphertext := isSecureValue(v); ok { return newSecureObject(ciphertext), nil } m := make(map[string]object, len(v)) for k, v := range v { sv, err := unmarshalObject(v) if err != nil { return object{}, err } m[k] = sv } return newObject(m), nil case map[any]any: m := make(map[string]any, len(v)) for k, v := range v { m[fmt.Sprintf("%v", k)] = v } return unmarshalObject(m) case []any: a := make([]object, len(v)) for i, v := range v { sv, err := unmarshalObject(v) if err != nil { return object{}, err } a[i] = sv } return newObject(a), nil case nil: return object{}, nil default: contract.Failf("unexpected wire type %T", v) return object{}, nil } } // marshalObject returns the value that should be passed to the JSON or YAML packages when marshaling the receiver. func (c object) marshalObject() any { if str, ok := c.value.(string); ok && c.secure { type secureValue struct { Secure string `json:"secure" yaml:"secure"` } return secureValue{Secure: str} } return c.value } // isSecureValue returns true if the object is a `map[string]any` of length one with a "secure" property of type string. func isSecureValue(v any) (bool, string) { if m, isMap := v.(map[string]any); isMap && len(m) == 1 { if val, hasSecureKey := m["secure"]; hasSecureKey { if valString, isString := val.(string); isString { return true, valString } } } return false, "" } func (c object) toDecryptedPropertyValue(ctx context.Context, decrypter Decrypter) (resource.PropertyValue, error) { plaintext, err := c.Decrypt(ctx, decrypter) if err != nil { return resource.PropertyValue{}, err } return plaintext.PropertyValue(), nil }