| Index: tokenserver/appengine/impl/serviceaccounts/config_validation.go
|
| diff --git a/tokenserver/appengine/impl/serviceaccounts/config_validation.go b/tokenserver/appengine/impl/serviceaccounts/config_validation.go
|
| index 907e33c085a2b8d79a0fbc7cc60e50446c48d826..8732375358a7cc7fcbaad6360cd9c01dbc7a2b15 100644
|
| --- a/tokenserver/appengine/impl/serviceaccounts/config_validation.go
|
| +++ b/tokenserver/appengine/impl/serviceaccounts/config_validation.go
|
| @@ -15,7 +15,12 @@
|
| package serviceaccounts
|
|
|
| import (
|
| + "fmt"
|
| + "strings"
|
| +
|
| "github.com/luci/luci-go/common/config/validation"
|
| + "github.com/luci/luci-go/common/data/stringset"
|
| + "github.com/luci/luci-go/server/auth/identity"
|
|
|
| "github.com/luci/luci-go/tokenserver/api/admin/v1"
|
| "github.com/luci/luci-go/tokenserver/appengine/impl/utils/policy"
|
| @@ -30,5 +35,89 @@ func validateConfigs(bundle policy.ConfigBundle, ctx *validation.Context) {
|
| return
|
| }
|
|
|
| - // TODO(vadimsh): Validate cfg.Rules.
|
| + names := stringset.New(0)
|
| + accounts := map[string]string{} // service account -> 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.Error("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.
|
| + for _, account := range rule.ServiceAccount {
|
| + if name, ok := accounts[account]; ok {
|
| + ctx.Error("service account %q is mentioned by more than one rule (%q and %q)", account, name, rule.Name)
|
| + } else {
|
| + accounts[account] = rule.Name
|
| + }
|
| + }
|
| +
|
| + validateRule(fmt.Sprintf("rule #%d: %q", i+1, rule.Name), rule, ctx)
|
| + }
|
| +}
|
| +
|
| +// validateRule checks single ServiceAccountRule proto.
|
| +func validateRule(title string, r *admin.ServiceAccountRule, ctx *validation.Context) {
|
| + ctx.Enter(title)
|
| + defer ctx.Exit()
|
| +
|
| + if r.Name == "" {
|
| + ctx.Error(`"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("service_account", r.ServiceAccount, ctx)
|
| + validateScopes("allowed_scope", r.AllowedScope, ctx)
|
| + validateIdSet("end_user", r.EndUser, ctx)
|
| + validateIdSet("proxy", r.Proxy, ctx)
|
| +
|
| + if r.MaxGrantValidityDuration != 0 {
|
| + switch {
|
| + case r.MaxGrantValidityDuration < 0:
|
| + ctx.Error(`"max_grant_validity_duration" must be positive`)
|
| + case r.MaxGrantValidityDuration > maxAllowedMaxGrantValidityDuration:
|
| + ctx.Error(`"max_grant_validity_duration" must not exceed %d`, maxAllowedMaxGrantValidityDuration)
|
| + }
|
| + }
|
| +}
|
| +
|
| +func validateEmails(field string, emails []string, ctx *validation.Context) {
|
| + 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.Error("bad email %q - %s", email, err)
|
| + }
|
| + }
|
| +}
|
| +
|
| +func validateScopes(field string, scopes []string, ctx *validation.Context) {
|
| + ctx.Enter("%q", field)
|
| + defer ctx.Exit()
|
| + for _, scope := range scopes {
|
| + if !strings.HasPrefix(scope, "https://www.googleapis.com/") {
|
| + ctx.Error("bad scope %q", scope)
|
| + }
|
| + }
|
| +}
|
| +
|
| +func validateIdSet(field string, ids []string, ctx *validation.Context) {
|
| + ctx.Enter("%q", field)
|
| + defer ctx.Exit()
|
| + for _, entry := range ids {
|
| + if strings.HasPrefix(entry, "group:") {
|
| + if entry[len("group:"):] == "" {
|
| + ctx.Error("bad group entry - no group name")
|
| + }
|
| + } else if _, err := identity.MakeIdentity(entry); err != nil {
|
| + ctx.Error("bad identity %q - %s", entry, err)
|
| + }
|
| + }
|
| }
|
|
|