go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/serviceaccounts/config.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package serviceaccounts
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  
    21  	"google.golang.org/protobuf/encoding/prototext"
    22  
    23  	"go.chromium.org/luci/common/data/stringset"
    24  	"go.chromium.org/luci/config/validation"
    25  
    26  	"go.chromium.org/luci/tokenserver/api/admin/v1"
    27  	"go.chromium.org/luci/tokenserver/appengine/impl/utils/policy"
    28  )
    29  
    30  const configFileName = "project_owned_accounts.cfg"
    31  
    32  // Mapping is a queryable representation of project_owned_accounts.cfg.
    33  type Mapping struct {
    34  	revision         string                          // config revision this policy is imported from
    35  	pairs            map[projectAccountPair]struct{} // allowed (project, account) pairs
    36  	useProjectScoped stringset.Set                   // LUCI projects opted-in into using project-scoped accounts for minting tokens
    37  }
    38  
    39  type projectAccountPair struct {
    40  	project string // e.g. "chromium"
    41  	account string // e.g. "ci-builder@..."
    42  }
    43  
    44  // UseProjectScopedAccount returns true if the token server should use
    45  // project-scoped accounts when minting tokens in context of the given LUCI
    46  // project.
    47  func (m *Mapping) UseProjectScopedAccount(project string) bool {
    48  	return m.useProjectScoped.Has(project)
    49  }
    50  
    51  // CanProjectUseAccount returns true if the given project is allowed to mint
    52  // tokens of the given service account.
    53  //
    54  // The project name is extracted from a realm name and it can be "@internal"
    55  // for internal realms.
    56  func (m *Mapping) CanProjectUseAccount(project, account string) bool {
    57  	_, yes := m.pairs[projectAccountPair{project, account}]
    58  	return yes
    59  }
    60  
    61  // ConfigRevision is part of policy.Queryable interface.
    62  func (m *Mapping) ConfigRevision() string {
    63  	return m.revision
    64  }
    65  
    66  // MappingCache is a stateful object with parsed project_owned_accounts.cfg.
    67  //
    68  // It uses policy.Policy internally to manage datastore-cached copy of imported
    69  // service accounts configs.
    70  //
    71  // Use NewMappingCache() to create a new instance. Each instance owns its own
    72  // in-memory cache, but uses the same shared datastore cache.
    73  //
    74  // There's also a process global instance of MappingCache (GlobalMappingCache
    75  // var) which is used by the main process. Unit tests don't use it though to
    76  // avoid relying on shared state.
    77  type MappingCache struct {
    78  	policy policy.Policy // holds cached *Mapping
    79  }
    80  
    81  // GlobalMappingCache is the process-wide mapping cache.
    82  var GlobalMappingCache = NewMappingCache()
    83  
    84  // NewMappingCache properly initializes MappingCache instance.
    85  func NewMappingCache() *MappingCache {
    86  	return &MappingCache{
    87  		policy: policy.Policy{
    88  			Name:     configFileName,       // used as part of datastore keys
    89  			Fetch:    fetchConfigs,         // see below
    90  			Validate: validateConfigBundle, // see config_validation.go
    91  			Prepare:  prepareMapping,       // see below
    92  		},
    93  	}
    94  }
    95  
    96  // ImportConfigs refetches project_owned_accounts.cfg and updates the datastore.
    97  //
    98  // Called from cron.
    99  func (mc *MappingCache) ImportConfigs(ctx context.Context) (rev string, err error) {
   100  	return mc.policy.ImportConfigs(ctx)
   101  }
   102  
   103  // SetupConfigValidation registers the config validation rules.
   104  func (mc *MappingCache) SetupConfigValidation(rules *validation.RuleSet) {
   105  	rules.Add("services/${appid}", configFileName, func(ctx *validation.Context, configSet, path string, content []byte) error {
   106  		cfg := &admin.ServiceAccountsProjectMapping{}
   107  		if err := prototext.Unmarshal(content, cfg); err != nil {
   108  			ctx.Errorf("not a valid ServiceAccountsProjectMapping proto message - %s", err)
   109  		} else {
   110  			validateMappingCfg(ctx, cfg)
   111  		}
   112  		return nil
   113  	})
   114  }
   115  
   116  // Mapping returns in-memory copy of the mapping, ready for querying.
   117  func (mc *MappingCache) Mapping(ctx context.Context) (*Mapping, error) {
   118  	q, err := mc.policy.Queryable(ctx)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	return q.(*Mapping), nil
   123  }
   124  
   125  // fetchConfigs loads proto messages with the mapping from the config.
   126  func fetchConfigs(ctx context.Context, f policy.ConfigFetcher) (policy.ConfigBundle, error) {
   127  	cfg := &admin.ServiceAccountsProjectMapping{}
   128  	if err := f.FetchTextProto(ctx, configFileName, cfg); err != nil {
   129  		return nil, err
   130  	}
   131  	return policy.ConfigBundle{configFileName: cfg}, nil
   132  }
   133  
   134  // prepareMapping converts validated configs into *Mapping.
   135  //
   136  // Returns it as a policy.Queryable object to satisfy policy.Policy API.
   137  func prepareMapping(ctx context.Context, cfg policy.ConfigBundle, revision string) (policy.Queryable, error) {
   138  	parsed, ok := cfg[configFileName].(*admin.ServiceAccountsProjectMapping)
   139  	if !ok {
   140  		return nil, fmt.Errorf("wrong type of %s - %T", configFileName, cfg[configFileName])
   141  	}
   142  
   143  	pairs := map[projectAccountPair]struct{}{}
   144  	for _, m := range parsed.Mapping {
   145  		for _, project := range m.Project {
   146  			for _, account := range m.ServiceAccount {
   147  				pairs[projectAccountPair{project, account}] = struct{}{}
   148  			}
   149  		}
   150  	}
   151  
   152  	return &Mapping{
   153  		revision:         revision,
   154  		pairs:            pairs,
   155  		useProjectScoped: stringset.NewFromSlice(parsed.UseProjectScopedAccount...),
   156  	}, nil
   157  }