package config
|
|
|
|
import (
|
|
"errors"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/utahta/go-openuri"
|
|
"github.com/ghodss/yaml"
|
|
)
|
|
|
|
// Policy validates a number of rules against a given configuration.
|
|
//
|
|
type Policy struct {
|
|
Enforcements []Enforcement `json:"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 := ResolveJSONPath(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 `json:"path"`
|
|
Rule string `json:"rule"`
|
|
}
|
|
|
|
// ReadYAMLPolicy converts YAML input to JSON and returns a new Policy struct.
|
|
//
|
|
func ReadYAMLPolicy(data []byte) (*Policy, error) {
|
|
jsonData, err := yaml.YAMLToJSON(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ReadPolicy(jsonData)
|
|
}
|
|
|
|
// ReadPolicy unmarshals the given YAML/json bytes into a new Policy struct.
|
|
//
|
|
func ReadPolicy(data []byte) (*Policy, error) {
|
|
var policy Policy
|
|
err := json.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 ReadYAMLPolicy(data)
|
|
}
|
|
|
|
// ResolveJSONPath returns the config value found at the given JSON-ish
|
|
// namespace/path (e.g. "variants.production.runs.as").
|
|
//
|
|
func ResolveJSONPath(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 := ResolveJSONPath(path, v.Field(i).Interface()); err == nil {
|
|
return subsubcfg, nil
|
|
}
|
|
}
|
|
|
|
if name == resolveJSONTagName(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 ResolveJSONPath(parts[1], subcfg)
|
|
}
|
|
|
|
return subcfg, nil
|
|
}
|