/
config_validation.go
165 lines (147 loc) · 5.45 KB
/
config_validation.go
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
// Copyright 2017 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package serviceaccounts
import (
"fmt"
"strings"
"github.com/TriggerMail/luci-go/auth/identity"
"github.com/TriggerMail/luci-go/common/data/stringset"
"github.com/TriggerMail/luci-go/config/validation"
"github.com/TriggerMail/luci-go/tokenserver/api/admin/v1"
"github.com/TriggerMail/luci-go/tokenserver/appengine/impl/utils/policy"
)
// validateConfigBundle validates the structure of a config bundle fetched by
// fetchConfigs.
func validateConfigBundle(ctx *validation.Context, bundle policy.ConfigBundle) {
ctx.SetFile(serviceAccountsCfg)
cfg, ok := bundle[serviceAccountsCfg].(*admin.ServiceAccountsPermissions)
if ok {
validateServiceAccountsCfg(ctx, cfg)
} else {
ctx.Errorf("unexpectedly wrong proto type %T", cfg)
}
}
// validateServiceAccountsCfg checks deserialized service_accounts.cfg.
func validateServiceAccountsCfg(ctx *validation.Context, cfg *admin.ServiceAccountsPermissions) {
if cfg.Defaults != nil {
validateDefaults(ctx, "defaults", cfg.Defaults)
}
names := stringset.New(0)
accounts := map[string]string{} // service account -> rule name where its defined
groups := map[string]string{} // group with accounts -> rule name where its defined
for i, rule := range cfg.Rules {
// Rule name must be unique. Missing name will be handled by 'validateRule'.
if rule.Name != "" {
if names.Has(rule.Name) {
ctx.Errorf("two rules with identical name %q", rule.Name)
} else {
names.Add(rule.Name)
}
}
// There should be no overlap between service account sets covered by each
// rule. Unfortunately we can't reliably dive into groups, since they may
// change after the config validation step. So compare only top level group
// names, Rules.Rule() method relies on this.
for _, account := range rule.ServiceAccount {
if name, ok := accounts[account]; ok {
ctx.Errorf("service account %q is mentioned by more than one rule (%q and %q)", account, name, rule.Name)
} else {
accounts[account] = rule.Name
}
}
for _, group := range rule.ServiceAccountGroup {
if name, ok := groups[group]; ok {
ctx.Errorf("service account group %q is mentioned by more than one rule (%q and %q)", group, name, rule.Name)
} else {
groups[group] = rule.Name
}
}
validateRule(ctx, fmt.Sprintf("rule #%d: %q", i+1, rule.Name), rule)
}
}
// validateDefaults checks ServiceAccountRuleDefaults proto.
func validateDefaults(ctx *validation.Context, title string, d *admin.ServiceAccountRuleDefaults) {
ctx.Enter(title)
defer ctx.Exit()
validateScopes(ctx, "allowed_scope", d.AllowedScope)
validateMaxGrantValidityDuration(ctx, d.MaxGrantValidityDuration)
}
// validateRule checks single ServiceAccountRule proto.
func validateRule(ctx *validation.Context, title string, r *admin.ServiceAccountRule) {
ctx.Enter(title)
defer ctx.Exit()
if r.Name == "" {
ctx.Errorf(`"name" is required`)
}
// Note: we allow any of the sets to be empty. The rule will just not match
// anything in this case, this is fine.
validateEmails(ctx, "service_account", r.ServiceAccount)
validateGroups(ctx, "service_account_group", r.ServiceAccountGroup)
validateScopes(ctx, "allowed_scope", r.AllowedScope)
validateIDSet(ctx, "end_user", r.EndUser)
validateIDSet(ctx, "proxy", r.Proxy)
validateIDSet(ctx, "trusted_proxy", r.TrustedProxy)
validateMaxGrantValidityDuration(ctx, r.MaxGrantValidityDuration)
}
func validateEmails(ctx *validation.Context, field string, emails []string) {
ctx.Enter("%q", field)
defer ctx.Exit()
for _, email := range emails {
// We reuse 'user:' identity validator, user identities are emails too.
if _, err := identity.MakeIdentity("user:" + email); err != nil {
ctx.Errorf("bad email %q - %s", email, err)
}
}
}
func validateGroups(ctx *validation.Context, field string, groups []string) {
ctx.Enter("%q", field)
defer ctx.Exit()
for _, gr := range groups {
if gr == "" {
ctx.Errorf("the group name must not be empty")
}
}
}
func validateScopes(ctx *validation.Context, field string, scopes []string) {
ctx.Enter("%q", field)
defer ctx.Exit()
for _, scope := range scopes {
if !strings.HasPrefix(scope, "https://www.googleapis.com/") {
ctx.Errorf("bad scope %q", scope)
}
}
}
func validateIDSet(ctx *validation.Context, field string, ids []string) {
ctx.Enter("%q", field)
defer ctx.Exit()
for _, entry := range ids {
if strings.HasPrefix(entry, "group:") {
if entry[len("group:"):] == "" {
ctx.Errorf("bad group entry - no group name")
}
} else if _, err := identity.MakeIdentity(entry); err != nil {
ctx.Errorf("bad identity %q - %s", entry, err)
}
}
}
func validateMaxGrantValidityDuration(ctx *validation.Context, dur int64) {
switch {
case dur == 0:
// valid
case dur < 0:
ctx.Errorf(`"max_grant_validity_duration" must be positive`)
case dur > maxAllowedMaxGrantValidityDuration:
ctx.Errorf(`"max_grant_validity_duration" must not exceed %d`, maxAllowedMaxGrantValidityDuration)
}
}