summaryrefslogtreecommitdiff
path: root/config/policy.go
blob: 25fe02a5919963feab1f9ddca1aebdef3eb2bcf9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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
}