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 }