diff options
author | Dan Duvall <dduvall@wikimedia.org> | 2018-03-06 20:31:58 -0800 |
---|---|---|
committer | Dan Duvall <dduvall@wikimedia.org> | 2018-03-19 15:55:16 -0700 |
commit | eb9b69dd3d710cb7afa1dfb6e23a5987842b21cc (patch) | |
tree | 049b11cc885e4e9f54aac8981c91a1bf3620e7af /config/policy.go | |
parent | 6896e655eb5cc88b90e66979bc2d862eb92cbb9f (diff) | |
download | blubber-eb9b69dd3d710cb7afa1dfb6e23a5987842b21cc.tar.gz |
Allow for configuration policies
Summary:
Implements a rough interface for validating configuration against
arbitrary policy rules. Policies are provided as YAML and passed via the
command line as file paths or remote URIs.
The format of policies is:
enforcements:
- path: <path>
rule: <rule>
Where `<path>` is a YAML-ish path to a config field and `<rule>` is any
expression our config validator understands (expressions built in by the
validator library and custom tags defined in `config.validation.go`).
Example policy:
enforcements:
- path: variants.production.base
rule: oneof=debian:jessie debian:stretch
- path: variants.production.runs.as
rule: ne=foo
- path: variants.production.node.dependencies
rule: isfalse
Command flag parsing was implemented in `main.go` to support the new
`--policy=uri` flag and improve existing handling of `--version` and the
usage statement.
Test Plan: Run `go test ./...`.
Reviewers: thcipriani, demon, hashar, mmodell, #release-engineering-team
Reviewed By: thcipriani, #release-engineering-team
Tags: #release-engineering-team
Differential Revision: https://phabricator.wikimedia.org/D999
Diffstat (limited to 'config/policy.go')
-rw-r--r-- | config/policy.go | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/config/policy.go b/config/policy.go new file mode 100644 index 0000000..25fe02a --- /dev/null +++ b/config/policy.go @@ -0,0 +1,141 @@ +package config + +import ( + "errors" + "fmt" + "io/ioutil" + "reflect" + "strings" + + "github.com/utahta/go-openuri" + "gopkg.in/yaml.v2" +) + +// Policy validates a number of rules against a given configuration. +// +type Policy struct { + Enforcements []Enforcement `yaml:"enforcements"` +} + +// Validate checks the given config against all policy enforcements. +// +func (pol Policy) Validate(config Config) error { + validate := NewValidator() + + for _, enforcement := range pol.Enforcements { + cfg, err := ResolveYAMLPath(enforcement.Path, config) + + if err != nil { + // If the path resolved nothing, there's nothing to enforce + continue + } + + // Flags are a special case in which the True field should be compared + // against the validator, not the struct itself. + if flag, ok := cfg.(Flag); ok { + cfg = flag.True + } + + err = validate.Var(cfg, enforcement.Rule) + + if err != nil { + return fmt.Errorf( + `value for "%s" violates policy rule "%s"`, + enforcement.Path, enforcement.Rule, + ) + } + } + + return nil +} + +// Enforcement represents a policy rule and config path on which to apply it. +// +type Enforcement struct { + Path string `yaml:"path"` + Rule string `yaml:"rule"` +} + +// ReadPolicy unmarshals the given YAML bytes into a new Policy struct. +// +func ReadPolicy(data []byte) (*Policy, error) { + var policy Policy + + err := yaml.Unmarshal(data, &policy) + + if err != nil { + return nil, err + } + + return &policy, err +} + +// ReadPolicyFromURI fetches the policy file from the given URL or file path +// and loads its contents with ReadPolicy. +// +func ReadPolicyFromURI(uri string) (*Policy, error) { + io, err := openuri.Open(uri) + + if err != nil { + return nil, err + } + + defer io.Close() + + data, err := ioutil.ReadAll(io) + + if err != nil { + return nil, err + } + + return ReadPolicy(data) +} + +// ResolveYAMLPath returns the config value found at the given YAML-ish +// namespace/path (e.g. "variants.production.runs.as"). +// +func ResolveYAMLPath(path string, cfg interface{}) (interface{}, error) { + parts := strings.SplitN(path, ".", 2) + name := parts[0] + + v := reflect.ValueOf(cfg) + t := v.Type() + + var subcfg interface{} + + switch t.Kind() { + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + if t.Field(i).Anonymous { + if subsubcfg, err := ResolveYAMLPath(path, v.Field(i).Interface()); err == nil { + return subsubcfg, nil + } + } + + if name == resolveYAMLTagName(t.Field(i)) { + subcfg = v.Field(i).Interface() + break + } + } + + case reflect.Map: + if t.Key().Kind() == reflect.String { + for _, key := range v.MapKeys() { + if key.Interface().(string) == name { + subcfg = v.MapIndex(key).Interface() + break + } + } + } + } + + if subcfg == nil { + return nil, errors.New("invalid path") + } + + if len(parts) > 1 { + return ResolveYAMLPath(parts[1], subcfg) + } + + return subcfg, nil +} |