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:
parent
4ea3a0f4ae
commit
f77bcc53c4
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue