blob: bb2272620cc1d8e415596b5569e06783e5fbf7b6 [file] [log] [blame]
// 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"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/common/data/stringset"
"go.chromium.org/luci/config/validation"
"go.chromium.org/luci/tokenserver/api/admin/v1"
"go.chromium.org/luci/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)
}
}