Move sdk/helper/random -> helper/random (#9226)

* This package is new for 1.5 so this is not a breaking change.
* This is being moved because this code was originally intended to be used
within plugins, however the design of password policies has changed such
that this is no longer needed. Thus, this code doesn't need to be in the
public SDK.
This commit is contained in:
Michael Golowka 2020-06-17 14:24:38 -06:00 committed by GitHub
parent 4ea3a0f4ae
commit f77bcc53c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 29 additions and 716 deletions

View File

@ -158,7 +158,6 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=

View File

@ -10,8 +10,8 @@ import (
"github.com/hashicorp/vault/helper/testhelpers/docker"
logicaltest "github.com/hashicorp/vault/helper/testhelpers/logical"
"github.com/hashicorp/vault/sdk/helper/base62"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/random"
"github.com/hashicorp/vault/sdk/logical"
rabbithole "github.com/michaelklishin/rabbit-hole"
"github.com/mitchellh/mapstructure"
@ -163,7 +163,10 @@ func TestBackend_roleWithPasswordPolicy(t *testing.T) {
}
backendConfig := logical.TestBackendConfig()
backendConfig.System.(*logical.StaticSystemView).SetPasswordPolicy("testpolicy", random.DefaultStringGenerator)
passGen := func() (password string, err error) {
return base62.Random(30)
}
backendConfig.System.(*logical.StaticSystemView).SetPasswordPolicy("testpolicy", passGen)
b, _ := Factory(context.Background(), backendConfig)
cleanup, uri, _ := prepareRabbitMQTestContainer(t)

View File

@ -86,6 +86,8 @@ type ExtendedSystemView interface {
ForwardGenericRequest(context.Context, *Request) (*Response, error)
}
type PasswordGenerator func() (password string, err error)
type StaticSystemView struct {
DefaultLeaseTTLVal time.Duration
MaxLeaseTTLVal time.Duration
@ -101,7 +103,7 @@ type StaticSystemView struct {
Features license.Features
VaultVersion string
PluginEnvironment *PluginEnvironment
PasswordPolicies map[string]PasswordPolicy
PasswordPolicies map[string]PasswordGenerator
}
type noopAuditor struct{}
@ -192,14 +194,14 @@ func (d StaticSystemView) GeneratePasswordFromPolicy(ctx context.Context, policy
if !exists {
return "", fmt.Errorf("password policy not found")
}
return policy.Generate(ctx, nil)
return policy()
}
func (d *StaticSystemView) SetPasswordPolicy(name string, policy PasswordPolicy) {
func (d *StaticSystemView) SetPasswordPolicy(name string, generator PasswordGenerator) {
if d.PasswordPolicies == nil {
d.PasswordPolicies = map[string]PasswordPolicy{}
d.PasswordPolicies = map[string]PasswordGenerator{}
}
d.PasswordPolicies[name] = policy
d.PasswordPolicies[name] = generator
}
func (d *StaticSystemView) DeletePasswordPolicy(name string) (existed bool) {

View File

@ -9,7 +9,6 @@ import (
"github.com/golang/protobuf/proto"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/random"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/plugin/pb"
"google.golang.org/grpc"
@ -242,30 +241,13 @@ func TestSystem_GRPC_pluginEnv(t *testing.T) {
func TestSystem_GRPC_GeneratePasswordFromPolicy(t *testing.T) {
policyName := "testpolicy"
expectedPolicy := &random.StringGenerator{
Length: 8,
Rules: []random.Rule{
&random.CharsetRule{
Charset: random.LowercaseRuneset,
MinChars: 1,
},
&random.CharsetRule{
Charset: random.UppercaseRuneset,
MinChars: 1,
},
&random.CharsetRule{
Charset: random.NumericRuneset,
MinChars: 1,
},
&random.CharsetRule{
Charset: random.ShortSymbolRuneset,
MinChars: 1,
},
},
expectedPassword := "87354qtnjgrehiogd9u0t43"
passGen := func() (password string, err error) {
return expectedPassword, nil
}
sys := &logical.StaticSystemView{
PasswordPolicies: map[string]logical.PasswordPolicy{
policyName: logical.PasswordPolicy(expectedPolicy),
PasswordPolicies: map[string]logical.PasswordGenerator{
policyName: passGen,
},
}
@ -287,15 +269,7 @@ func TestSystem_GRPC_GeneratePasswordFromPolicy(t *testing.T) {
t.Fatalf("no error expected, got: %s", err)
}
passRunes := []rune(password)
if len(passRunes) != expectedPolicy.Length {
t.Fatalf("Generated password should have length %d but was %d", expectedPolicy.Length, len(passRunes))
}
for _, rule := range expectedPolicy.Rules {
if !rule.Pass(passRunes) {
t.Fatalf("Password [%s] did not pass rule: %#v", password, rule)
}
if password != expectedPassword {
t.Fatalf("Actual password: %s\nExpected password: %s", password, expectedPassword)
}
}

View File

@ -8,10 +8,10 @@ import (
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/random"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/license"
"github.com/hashicorp/vault/sdk/helper/pluginutil"
"github.com/hashicorp/vault/sdk/helper/random"
"github.com/hashicorp/vault/sdk/helper/wrapping"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/version"

View File

@ -28,12 +28,12 @@ import (
"github.com/hashicorp/vault/helper/metricsutil"
"github.com/hashicorp/vault/helper/monitor"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/random"
"github.com/hashicorp/vault/physical/raft"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/parseutil"
"github.com/hashicorp/vault/sdk/helper/random"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/helper/wrapping"
"github.com/hashicorp/vault/sdk/logical"

View File

@ -22,10 +22,10 @@ import (
"github.com/hashicorp/vault/helper/builtinplugins"
"github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/helper/random"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/random"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/sdk/version"

View File

@ -1,150 +0,0 @@
package random
import (
"fmt"
"reflect"
"unicode/utf8"
"github.com/hashicorp/hcl"
"github.com/mitchellh/mapstructure"
)
// 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)
}
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
}

View File

@ -1,35 +0,0 @@
package random
import (
"fmt"
)
type ruleConstructor func(map[string]interface{}) (Rule, error)
var (
// defaultRuleNameMapping is the default mapping of HCL rule names to the appropriate rule constructor.
// Add to this map when adding a new Rule type to be recognized in HCL.
defaultRuleNameMapping = map[string]ruleConstructor{
"charset": ParseCharset,
}
defaultRegistry = Registry{
Rules: defaultRuleNameMapping,
}
)
// Registry of HCL rule names to rule constructors.
type Registry struct {
// Rules maps names of rules to a constructor for the rule
Rules map[string]ruleConstructor
}
func (r Registry) parseRule(ruleType string, ruleData map[string]interface{}) (rule Rule, err error) {
constructor, exists := r.Rules[ruleType]
if !exists {
return nil, fmt.Errorf("unrecognized rule type %s", ruleType)
}
rule, err = constructor(ruleData)
return rule, err
}

View File

@ -1,91 +0,0 @@
package random
import (
"fmt"
"github.com/mitchellh/mapstructure"
)
// Rule to assert on string values.
type Rule interface {
// Pass should return true if the provided value passes any assertions this Rule is making.
Pass(value []rune) bool
// Type returns the name of the rule as associated in the registry
Type() string
}
// CharsetRule requires a certain number of characters from the specified charset.
type CharsetRule struct {
// CharsetRule is the list of rules that candidate strings must contain a minimum number of.
Charset runes `mapstructure:"charset" json:"charset"`
// MinChars indicates the minimum (inclusive) number of characters from the charset that should appear in the string.
MinChars int `mapstructure:"min-chars" json:"min-chars"`
}
// ParseCharset from the provided data map. The data map is expected to be parsed from HCL.
func ParseCharset(data map[string]interface{}) (rule Rule, err error) {
cr := &CharsetRule{}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Metadata: nil,
Result: cr,
DecodeHook: stringToRunesFunc,
})
if err != nil {
return nil, fmt.Errorf("unable to decode charset restriction: %w", err)
}
err = decoder.Decode(data)
if err != nil {
return nil, fmt.Errorf("failed to parse charset restriction: %w", err)
}
return *cr, nil
}
func (c CharsetRule) Type() string {
return "charset"
}
// Chars returns the charset that this rule is looking for.
func (c CharsetRule) Chars() []rune {
return c.Charset
}
func (c CharsetRule) MinLength() int {
return c.MinChars
}
// Pass returns true if the provided candidate string has a minimum number of chars in it.
// This adheres to the Rule interface
func (c CharsetRule) Pass(value []rune) bool {
if c.MinChars <= 0 {
return true
}
count := 0
for _, r := range value {
// charIn is sometimes faster than a map lookup because the data is so small
// This is being kept rather than converted to a map to keep the code cleaner,
// otherwise there would need to be additional parsing logic.
if charIn(r, c.Charset) {
count++
if count >= c.MinChars {
return true
}
}
}
return false
}
func charIn(search rune, charset []rune) bool {
for _, r := range charset {
if search == r {
return true
}
}
return false
}

View File

@ -1,88 +0,0 @@
package random
import (
"encoding/json"
"fmt"
"github.com/mitchellh/mapstructure"
)
// serializableRules is a slice of rules that can be marshalled to JSON in an HCL format
type serializableRules []Rule
// MarshalJSON in an HCL-friendly way
func (r serializableRules) MarshalJSON() (b []byte, err error) {
// Example:
// [
// {
// "testrule": [
// {
// "string": "teststring",
// "int": 123
// }
// ]
// },
// {
// "charset": [
// {
// "charset": "abcde",
// "min-chars": 2
// }
// ]
// }
// ]
data := []map[string][]map[string]interface{}{} // Totally not confusing at all
for _, rule := range r {
ruleData := map[string]interface{}{}
err = mapstructure.Decode(rule, &ruleData)
if err != nil {
return nil, fmt.Errorf("unable to decode rule: %w", err)
}
ruleMap := map[string][]map[string]interface{}{
rule.Type(): []map[string]interface{}{
ruleData,
},
}
data = append(data, ruleMap)
}
b, err = json.Marshal(data)
return b, err
}
func (r *serializableRules) UnmarshalJSON(data []byte) (err error) {
mapData := []map[string]interface{}{}
err = json.Unmarshal(data, &mapData)
if err != nil {
return err
}
rules, err := parseRules(defaultRegistry, mapData)
if err != nil {
return err
}
*r = rules
return nil
}
type runes []rune
func (r runes) Len() int { return len(r) }
func (r runes) Less(i, j int) bool { return r[i] < r[j] }
func (r runes) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
// MarshalJSON converts the runes to a string for smaller JSON and easier readability
func (r runes) MarshalJSON() (b []byte, err error) {
return json.Marshal(string(r))
}
// UnmarshalJSON converts a string to []rune
func (r *runes) UnmarshalJSON(data []byte) (err error) {
var str string
err = json.Unmarshal(data, &str)
if err != nil {
return err
}
*r = []rune(str)
return nil
}

View File

@ -1,302 +0,0 @@
package random
import (
"context"
"crypto/rand"
"fmt"
"io"
"math"
"sort"
"time"
"unicode"
"github.com/hashicorp/go-multierror"
)
var (
LowercaseCharset = sortCharset("abcdefghijklmnopqrstuvwxyz")
UppercaseCharset = sortCharset("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
NumericCharset = sortCharset("0123456789")
FullSymbolCharset = sortCharset("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
ShortSymbolCharset = sortCharset("-")
AlphabeticCharset = sortCharset(UppercaseCharset + LowercaseCharset)
AlphaNumericCharset = sortCharset(AlphabeticCharset + NumericCharset)
AlphaNumericShortSymbolCharset = sortCharset(AlphaNumericCharset + ShortSymbolCharset)
AlphaNumericFullSymbolCharset = sortCharset(AlphaNumericCharset + FullSymbolCharset)
LowercaseRuneset = []rune(LowercaseCharset)
UppercaseRuneset = []rune(UppercaseCharset)
NumericRuneset = []rune(NumericCharset)
FullSymbolRuneset = []rune(FullSymbolCharset)
ShortSymbolRuneset = []rune(ShortSymbolCharset)
AlphabeticRuneset = []rune(AlphabeticCharset)
AlphaNumericRuneset = []rune(AlphaNumericCharset)
AlphaNumericShortSymbolRuneset = []rune(AlphaNumericShortSymbolCharset)
AlphaNumericFullSymbolRuneset = []rune(AlphaNumericFullSymbolCharset)
// DefaultStringGenerator has reasonable default rules for generating strings
DefaultStringGenerator = &StringGenerator{
Length: 20,
Rules: []Rule{
CharsetRule{
Charset: LowercaseRuneset,
MinChars: 1,
},
CharsetRule{
Charset: UppercaseRuneset,
MinChars: 1,
},
CharsetRule{
Charset: NumericRuneset,
MinChars: 1,
},
CharsetRule{
Charset: ShortSymbolRuneset,
MinChars: 1,
},
},
}
)
func sortCharset(chars string) string {
r := runes(chars)
sort.Sort(r)
return string(r)
}
// StringGenerator generats random strings from the provided charset & adhering to a set of rules. The set of rules
// are things like CharsetRule which requires a certain number of characters from a sub-charset.
type StringGenerator struct {
// Length of the string to generate.
Length int `mapstructure:"length" json:"length"`
// Rules the generated strings must adhere to.
Rules serializableRules `mapstructure:"-" json:"rule"` // This is "rule" in JSON so it matches the HCL property type
// CharsetRule to choose runes from. This is computed from the rules, not directly configurable
charset runes
}
// Generate a random string from the charset and adhering to the provided rules.
// The io.Reader is optional. If not provided, it will default to the reader from crypto/rand
func (g *StringGenerator) Generate(ctx context.Context, rng io.Reader) (str string, err error) {
if _, hasTimeout := ctx.Deadline(); !hasTimeout {
var cancel func()
ctx, cancel = context.WithTimeout(ctx, 1*time.Second) // Ensure there's a timeout on the context
defer cancel()
}
// Ensure the generator is configured well since it may be manually created rather than parsed from HCL
err = g.validateConfig()
if err != nil {
return "", err
}
LOOP:
for {
select {
case <-ctx.Done():
return "", fmt.Errorf("timed out generating string")
default:
str, err = g.generate(rng)
if err != nil {
return "", err
}
if str == "" {
continue LOOP
}
return str, err
}
}
}
func (g *StringGenerator) generate(rng io.Reader) (str string, err error) {
// If performance improvements need to be made, this can be changed to read a batch of
// potential strings at once rather than one at a time. This will significantly
// improve performance, but at the cost of added complexity.
candidate, err := randomRunes(rng, g.charset, g.Length)
if err != nil {
return "", fmt.Errorf("unable to generate random characters: %w", err)
}
for _, rule := range g.Rules {
if !rule.Pass(candidate) {
return "", nil
}
}
// Passed all rules
return string(candidate), nil
}
const (
// maxCharsetLen is the maximum length a charset is allowed to be when generating a candidate string.
// This is the total number of numbers available for selecting an index out of the charset slice.
maxCharsetLen = 256
)
// randomRunes creates a random string based on the provided charset. The charset is limited to 255 characters, but
// could be expanded if needed. Expanding the maximum charset size will decrease performance because it will need to
// combine bytes into a larger integer using binary.BigEndian.Uint16() function.
func randomRunes(rng io.Reader, charset []rune, length int) (candidate []rune, err error) {
if len(charset) == 0 {
return nil, fmt.Errorf("no charset specified")
}
if len(charset) > maxCharsetLen {
return nil, fmt.Errorf("charset is too long: limited to %d characters", math.MaxUint8)
}
if length <= 0 {
return nil, fmt.Errorf("unable to generate a zero or negative length runeset")
}
// This can't always select indexes from [0-maxCharsetLen) because it could introduce bias to the character selection.
// For instance, if the length of the charset is [a-zA-Z0-9-] (length of 63):
// RNG ranges: [0-62][63-125][126-188][189-251] will equally select from the entirety of the charset. However,
// the RNG values [252-255] will select the first 4 characters of the charset while ignoring the remaining 59.
// This results in a bias towards the front of the charset.
//
// To avoid this, we determine the largest integer multiplier of the charset length that is <= maxCharsetLen
// For instance, if the maxCharsetLen is 256 (the size of one byte) and the charset is length 63, the multiplier
// equals 4:
// 256/63 => 4.06
// Trunc(4.06) => 4
// Multiply by the charset length
// Subtract 1 to account for 0-based counting and you get the max index value: 251
maxAllowedRNGValue := (maxCharsetLen/len(charset))*len(charset) - 1
// rngBufferMultiplier increases the size of the RNG buffer to account for lost
// indexes due to the maxAllowedRNGValue
rngBufferMultiplier := 1.0
// Don't set a multiplier if we are able to use the entire range of indexes
if maxAllowedRNGValue < maxCharsetLen {
// Anything more complicated than an arbitrary percentage appears to have little practical performance benefit
rngBufferMultiplier = 1.5
}
// Default to the standard crypto reader if one isn't provided
if rng == nil {
rng = rand.Reader
}
charsetLen := byte(len(charset))
runes := make([]rune, 0, length)
for len(runes) < length {
// Generate a bunch of indexes
data := make([]byte, int(float64(length)*rngBufferMultiplier))
numBytes, err := rng.Read(data)
if err != nil {
return nil, err
}
// Append characters until either we're out of indexes or the length is long enough
for i := 0; i < numBytes; i++ {
// Be careful to ensure that maxAllowedRNGValue isn't >= 256 as it will overflow and this
// comparison will prevent characters from being selected from the charset
if data[i] > byte(maxAllowedRNGValue) {
continue
}
index := data[i]
if len(charset) != maxCharsetLen {
index = index % charsetLen
}
r := charset[index]
runes = append(runes, r)
if len(runes) == length {
break
}
}
}
return runes, nil
}
// validateConfig of the generator to ensure that we can successfully generate a string.
func (g *StringGenerator) validateConfig() (err error) {
merr := &multierror.Error{}
// Ensure the sum of minimum lengths in the rules doesn't exceed the length specified
minLen := getMinLength(g.Rules)
if g.Length <= 0 {
merr = multierror.Append(merr, fmt.Errorf("length must be > 0"))
} else if g.Length < minLen {
merr = multierror.Append(merr, fmt.Errorf("specified rules require at least %d characters but %d is specified", minLen, g.Length))
}
// Ensure we have a charset & all characters are printable
if len(g.charset) == 0 {
// Yes this is mutating the generator but this is done so we don't have to compute this on every generation
g.charset = getChars(g.Rules)
}
if len(g.charset) == 0 {
merr = multierror.Append(merr, fmt.Errorf("no charset specified"))
} else {
for _, r := range g.charset {
if !unicode.IsPrint(r) {
merr = multierror.Append(merr, fmt.Errorf("non-printable character in charset"))
break
}
}
}
return merr.ErrorOrNil()
}
// getMinLength from the rules using the optional interface: `MinLength() int`
func getMinLength(rules []Rule) (minLen int) {
type minLengthProvider interface {
MinLength() int
}
for _, rule := range rules {
mlp, ok := rule.(minLengthProvider)
if !ok {
continue
}
minLen += mlp.MinLength()
}
return minLen
}
// getChars from the rules using the optional interface: `Chars() []rune`
func getChars(rules []Rule) (chars []rune) {
type charsetProvider interface {
Chars() []rune
}
for _, rule := range rules {
cp, ok := rule.(charsetProvider)
if !ok {
continue
}
chars = append(chars, cp.Chars()...)
}
return deduplicateRunes(chars)
}
// deduplicateRunes returns a new slice of sorted & de-duplicated runes
func deduplicateRunes(original []rune) (deduped []rune) {
if len(original) == 0 {
return nil
}
m := map[rune]bool{}
dedupedRunes := []rune(nil)
for _, r := range original {
if m[r] {
continue
}
m[r] = true
dedupedRunes = append(dedupedRunes, r)
}
// They don't have to be sorted, but this is being done to make the charset easier to visualize
sort.Sort(runes(dedupedRunes))
return dedupedRunes
}

View File

@ -86,6 +86,8 @@ type ExtendedSystemView interface {
ForwardGenericRequest(context.Context, *Request) (*Response, error)
}
type PasswordGenerator func() (password string, err error)
type StaticSystemView struct {
DefaultLeaseTTLVal time.Duration
MaxLeaseTTLVal time.Duration
@ -101,7 +103,7 @@ type StaticSystemView struct {
Features license.Features
VaultVersion string
PluginEnvironment *PluginEnvironment
PasswordPolicies map[string]PasswordPolicy
PasswordPolicies map[string]PasswordGenerator
}
type noopAuditor struct{}
@ -192,14 +194,14 @@ func (d StaticSystemView) GeneratePasswordFromPolicy(ctx context.Context, policy
if !exists {
return "", fmt.Errorf("password policy not found")
}
return policy.Generate(ctx, nil)
return policy()
}
func (d *StaticSystemView) SetPasswordPolicy(name string, policy PasswordPolicy) {
func (d *StaticSystemView) SetPasswordPolicy(name string, generator PasswordGenerator) {
if d.PasswordPolicies == nil {
d.PasswordPolicies = map[string]PasswordPolicy{}
d.PasswordPolicies = map[string]PasswordGenerator{}
}
d.PasswordPolicies[name] = policy
d.PasswordPolicies[name] = generator
}
func (d *StaticSystemView) DeletePasswordPolicy(name string) (existed bool) {

1
vendor/modules.txt vendored
View File

@ -489,7 +489,6 @@ github.com/hashicorp/vault/sdk/helper/pathmanager
github.com/hashicorp/vault/sdk/helper/pluginutil
github.com/hashicorp/vault/sdk/helper/pointerutil
github.com/hashicorp/vault/sdk/helper/policyutil
github.com/hashicorp/vault/sdk/helper/random
github.com/hashicorp/vault/sdk/helper/salt
github.com/hashicorp/vault/sdk/helper/strutil
github.com/hashicorp/vault/sdk/helper/tlsutil