mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-21 22:51:09 +02:00
* HCL dup attr deprecation: pending removal * correct docs * add changelog * better error message for possible common errors * Update website/content/partials/deprecation/duplicate-hcl-attributes.mdx Co-authored-by: Yoko Hyakuna <yoko@hashicorp.com> * Update website/content/partials/deprecation/duplicate-hcl-attributes.mdx Co-authored-by: Luis (LT) Carbonell <lt.carbonell@hashicorp.com> --------- Co-authored-by: Yoko Hyakuna <yoko@hashicorp.com> Co-authored-by: Luis (LT) Carbonell <lt.carbonell@hashicorp.com>
195 lines
5.8 KiB
Go
195 lines
5.8 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package random
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/hashicorp/hcl/hcl/ast"
|
|
hclParser "github.com/hashicorp/hcl/hcl/parser"
|
|
"github.com/mitchellh/mapstructure"
|
|
)
|
|
|
|
// AllowHclDuplicatesEnvVar is an environment variable that allows Vault to revert back to accepting HCL files with
|
|
// duplicate attributes. It's temporary until we finish the deprecation process, at which point this will be removed
|
|
const AllowHclDuplicatesEnvVar = "VAULT_ALLOW_PENDING_REMOVAL_DUPLICATE_HCL_ATTRIBUTES"
|
|
|
|
// ParseAndCheckForDuplicateHclAttributes parses the input JSON/HCL file and if it is HCL it also checks
|
|
// for duplicate keys in the HCL file, allowing callers to handle the issue accordingly. In a future release we'll
|
|
// change the behavior to treat duplicate keys as an error and eventually remove this helper altogether.
|
|
// TODO (HCL_DUP_KEYS_DEPRECATION): remove once not used anymore
|
|
func ParseAndCheckForDuplicateHclAttributes(input string) (res *ast.File, duplicate bool, err error) {
|
|
res, err = hcl.Parse(input)
|
|
if err != nil && strings.Contains(err.Error(), "Each argument can only be defined once") {
|
|
allowHclDuplicatesRaw := os.Getenv(AllowHclDuplicatesEnvVar)
|
|
if allowHclDuplicatesRaw == "" {
|
|
// default is to not allow duplicates
|
|
return nil, false, err
|
|
}
|
|
allowHclDuplicates, envParseErr := strconv.ParseBool(allowHclDuplicatesRaw)
|
|
if envParseErr != nil {
|
|
return nil, false, fmt.Errorf("error parsing %q environment variable: %w", AllowHclDuplicatesEnvVar, err)
|
|
}
|
|
if !allowHclDuplicates {
|
|
return nil, false, err
|
|
}
|
|
|
|
// if allowed by the environment variable, parse again without failing on duplicate attributes
|
|
duplicate = true
|
|
res, err = hclParser.ParseDontErrorOnDuplicateKeys([]byte(input))
|
|
}
|
|
return res, duplicate, err
|
|
}
|
|
|
|
// ParsePolicy is a convenience function for parsing HCL into a StringGenerator.
|
|
// See PolicyParser.ParsePolicy for details.
|
|
func ParsePolicy(raw string) (gen StringGenerator, err error) {
|
|
parser := PolicyParser{
|
|
RuleRegistry: Registry{
|
|
Rules: defaultRuleNameMapping,
|
|
},
|
|
}
|
|
return parser.ParsePolicy(raw)
|
|
}
|
|
|
|
// ParsePolicyBytes is a convenience function for parsing HCL into a StringGenerator.
|
|
// See PolicyParser.ParsePolicy for details.
|
|
func ParsePolicyBytes(raw []byte) (gen StringGenerator, err error) {
|
|
return ParsePolicy(string(raw))
|
|
}
|
|
|
|
// PolicyParser parses string generator configuration from HCL.
|
|
type PolicyParser struct {
|
|
// RuleRegistry maps rule names in HCL to Rule constructors.
|
|
RuleRegistry Registry
|
|
}
|
|
|
|
// ParsePolicy parses the provided HCL into a StringGenerator.
|
|
func (p PolicyParser) ParsePolicy(raw string) (sg StringGenerator, err error) {
|
|
rawData := map[string]interface{}{}
|
|
err = hcl.Decode(&rawData, raw)
|
|
if err != nil {
|
|
return sg, fmt.Errorf("unable to decode: %w", err)
|
|
}
|
|
|
|
// Decode the top level items
|
|
gen := StringGenerator{}
|
|
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
|
Result: &gen,
|
|
DecodeHook: stringToRunesFunc,
|
|
})
|
|
if err != nil {
|
|
return sg, fmt.Errorf("unable to decode configuration: %w", err)
|
|
}
|
|
|
|
err = decoder.Decode(rawData)
|
|
if err != nil {
|
|
return sg, fmt.Errorf("failed to decode configuration: %w", err)
|
|
}
|
|
|
|
// Decode & parse rules
|
|
rawRules, err := getMapSlice(rawData, "rule")
|
|
if err != nil {
|
|
return sg, fmt.Errorf("unable to retrieve rules: %w", err)
|
|
}
|
|
|
|
rules, err := parseRules(p.RuleRegistry, rawRules)
|
|
if err != nil {
|
|
return sg, fmt.Errorf("unable to parse rules: %w", err)
|
|
}
|
|
|
|
gen = StringGenerator{
|
|
Length: gen.Length,
|
|
Rules: rules,
|
|
}
|
|
|
|
err = gen.validateConfig()
|
|
if err != nil {
|
|
return sg, err
|
|
}
|
|
|
|
return gen, nil
|
|
}
|
|
|
|
func parseRules(registry Registry, rawRules []map[string]interface{}) (rules []Rule, err error) {
|
|
for _, rawRule := range rawRules {
|
|
info, err := getRuleInfo(rawRule)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to get rule info: %w", err)
|
|
}
|
|
|
|
rule, err := registry.parseRule(info.ruleType, info.data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse rule %s: %w", info.ruleType, err)
|
|
}
|
|
rules = append(rules, rule)
|
|
}
|
|
|
|
return rules, nil
|
|
}
|
|
|
|
// getMapSlice from the provided map. This will retrieve and type-assert a []map[string]interface{} from the map
|
|
// This will not error if the key does not exist
|
|
// This will return an error if the value at the provided key is not of type []map[string]interface{}
|
|
func getMapSlice(m map[string]interface{}, key string) (mapSlice []map[string]interface{}, err error) {
|
|
rawSlice, exists := m[key]
|
|
if !exists {
|
|
return nil, nil
|
|
}
|
|
|
|
mapSlice = []map[string]interface{}{}
|
|
err = mapstructure.Decode(rawSlice, &mapSlice)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return mapSlice, nil
|
|
}
|
|
|
|
type ruleInfo struct {
|
|
ruleType string
|
|
data map[string]interface{}
|
|
}
|
|
|
|
// getRuleInfo splits the provided HCL-decoded rule into its rule type along with the data associated with it
|
|
func getRuleInfo(rule map[string]interface{}) (data ruleInfo, err error) {
|
|
// There should only be one key, but it's a dynamic key yay!
|
|
for key := range rule {
|
|
slice, err := getMapSlice(rule, key)
|
|
if err != nil {
|
|
return data, fmt.Errorf("unable to get rule data: %w", err)
|
|
}
|
|
|
|
if len(slice) == 0 {
|
|
return data, fmt.Errorf("rule info cannot be empty")
|
|
}
|
|
|
|
data = ruleInfo{
|
|
ruleType: key,
|
|
data: slice[0],
|
|
}
|
|
return data, nil
|
|
}
|
|
return data, fmt.Errorf("rule is empty")
|
|
}
|
|
|
|
// stringToRunesFunc converts a string to a []rune for use in the mapstructure library
|
|
func stringToRunesFunc(from reflect.Kind, to reflect.Kind, data interface{}) (interface{}, error) {
|
|
if from != reflect.String || to != reflect.Slice {
|
|
return data, nil
|
|
}
|
|
|
|
raw := data.(string)
|
|
|
|
if !utf8.ValidString(raw) {
|
|
return nil, fmt.Errorf("invalid UTF8 string")
|
|
}
|
|
return []rune(raw), nil
|
|
}
|