go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/delegation/config.go (about) 1 // Copyright 2016 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 delegation 16 17 import ( 18 "context" 19 "fmt" 20 "strings" 21 22 "google.golang.org/protobuf/encoding/prototext" 23 24 "go.chromium.org/luci/auth/identity" 25 "go.chromium.org/luci/config/validation" 26 27 "go.chromium.org/luci/tokenserver/api/admin/v1" 28 "go.chromium.org/luci/tokenserver/appengine/impl/utils/identityset" 29 "go.chromium.org/luci/tokenserver/appengine/impl/utils/policy" 30 ) 31 32 // delegationCfg is name of the main config file with the policy. 33 // 34 // Also used as a name for the imported configs in the datastore, so change it 35 // very carefully. 36 const delegationCfg = "delegation.cfg" 37 38 const ( 39 // Requestor is magical token that may be used in the config and requests as 40 // a substitute for caller's ID. 41 // 42 // See config.proto for more info. 43 Requestor = "REQUESTOR" 44 45 // Projects is a magical token that can be used in allowed_to_impersonate to 46 // indicate that the caller can impersonate "project:*" identities. 47 // 48 // TODO(vadimsh): Get rid of it. 49 Projects = "PROJECTS" 50 ) 51 52 // Rules is queryable representation of delegation.cfg rules. 53 type Rules struct { 54 revision string // config revision this policy is imported from 55 rules []*delegationRule // preprocessed policy rules 56 requestors *identityset.Set // union of all 'Requestor' fields in all rules 57 } 58 59 // RulesQuery contains parameters to match against the delegation rules. 60 // 61 // Used by 'FindMatchingRule'. 62 type RulesQuery struct { 63 Requestor identity.Identity // who is requesting the token 64 Delegator identity.Identity // what identity will be delegated/impersonated 65 Audience *identityset.Set // the requested audience set (delegatees) 66 Services *identityset.Set // the requested target services set 67 } 68 69 // delegationRule is preprocessed admin.DelegationRule message. 70 // 71 // This object is used by 'FindMatchingRule'. 72 type delegationRule struct { 73 rule *admin.DelegationRule // the original unaltered rule proto 74 75 requestors *identityset.Set // matched to RulesQuery.Requestor 76 delegators *identityset.Set // matched to RulesQuery.Delegator 77 audience *identityset.Set // matched to RulesQuery.Audience 78 services *identityset.Set // matched to RulesQuery.Services 79 80 addRequestorAsDelegator bool // if true, add RulesQuery.Requestor to 'delegators' set 81 addRequestorToAudience bool // if true, add RulesQuery.Requestor to 'audience' set 82 addProjectsAsDelegators bool // if true, add 'project:*' to 'delegators' set 83 } 84 85 // RulesCache is a stateful object with parsed delegation.cfg rules. 86 // 87 // It uses policy.Policy internally to manage datastore-cached copy of imported 88 // delegation configs. 89 // 90 // Use NewRulesCache() to create a new instance. Each instance owns its own 91 // in-memory cache, but uses same shared datastore cache. 92 // 93 // There's also a process global instance of RulesCache (GlobalRulesCache var) 94 // which is used by the main process. Unit tests don't use it though to avoid 95 // relying on shared state. 96 type RulesCache struct { 97 policy policy.Policy // holds cached *parsedRules 98 } 99 100 // GlobalRulesCache is the process-wide rules cache. 101 var GlobalRulesCache = NewRulesCache() 102 103 // NewRulesCache properly initializes RulesCache instance. 104 func NewRulesCache() *RulesCache { 105 return &RulesCache{ 106 policy: policy.Policy{ 107 Name: delegationCfg, // used as part of datastore keys 108 Fetch: fetchConfigs, // see below 109 Validate: validateConfigBundle, // see config_validation.go 110 Prepare: prepareRules, // see below 111 }, 112 } 113 } 114 115 // ImportConfigs refetches delegation.cfg and updates datastore copy of it. 116 // 117 // Called from cron. 118 func (rc *RulesCache) ImportConfigs(c context.Context) (rev string, err error) { 119 return rc.policy.ImportConfigs(c) 120 } 121 122 // SetupConfigValidation registers the config validation rules. 123 func (rc *RulesCache) SetupConfigValidation(rules *validation.RuleSet) { 124 rules.Add("services/${appid}", delegationCfg, func(ctx *validation.Context, configSet, path string, content []byte) error { 125 cfg := &admin.DelegationPermissions{} 126 if err := prototext.Unmarshal(content, cfg); err != nil { 127 ctx.Errorf("not a valid DelegationPermissions proto message - %s", err) 128 } else { 129 validateDelegationCfg(ctx, cfg) 130 } 131 return nil 132 }) 133 } 134 135 // Rules returns in-memory copy of delegation rules, ready for querying. 136 func (rc *RulesCache) Rules(c context.Context) (*Rules, error) { 137 q, err := rc.policy.Queryable(c) 138 if err != nil { 139 return nil, err 140 } 141 return q.(*Rules), nil 142 } 143 144 // fetchConfigs loads proto messages with rules from the config. 145 func fetchConfigs(c context.Context, f policy.ConfigFetcher) (policy.ConfigBundle, error) { 146 cfg := &admin.DelegationPermissions{} 147 if err := f.FetchTextProto(c, delegationCfg, cfg); err != nil { 148 return nil, err 149 } 150 return policy.ConfigBundle{delegationCfg: cfg}, nil 151 } 152 153 // prepareRules converts validated configs into *Rules. 154 // 155 // Returns them as policy.Queryable object to satisfy policy.Policy API. 156 func prepareRules(c context.Context, cfg policy.ConfigBundle, revision string) (policy.Queryable, error) { 157 parsed, ok := cfg[delegationCfg].(*admin.DelegationPermissions) 158 if !ok { 159 return nil, fmt.Errorf("wrong type of delegation.cfg - %T", cfg[delegationCfg]) 160 } 161 162 rules := make([]*delegationRule, 0, len(parsed.Rules)+1) 163 for _, msg := range parsed.Rules { 164 rule, err := makeDelegationRule(c, msg) 165 if err != nil { 166 return nil, err 167 } 168 rules = append(rules, rule) 169 } 170 171 // Add an implicit rule that allows trusted LUCI microservices to grab 172 // delegation tokens for 'project:*' identities. "auth-luci-services" is a 173 // magical group also mentioned in luci-py's components.auth. 174 // 175 // TODO(vadimsh): Currently relied on by Buildbucket. Buildbucket should 176 // switch to using 'project:...' identities directly without going through 177 // the token server. 178 rule, err := makeDelegationRule(c, &admin.DelegationRule{ 179 Name: "allow-project-identities", 180 Requestor: []string{"group:auth-luci-services"}, 181 AllowedToImpersonate: []string{Projects}, 182 AllowedAudience: []string{Requestor}, 183 TargetService: []string{"*"}, 184 MaxValidityDuration: 86400, 185 }) 186 if err != nil { 187 panic(err) // should be impossible, this is a hardcoded rule 188 } 189 rules = append(rules, rule) 190 191 requestors := make([]*identityset.Set, len(rules)) 192 for i, r := range rules { 193 requestors[i] = r.requestors 194 } 195 196 return &Rules{ 197 revision: revision, 198 rules: rules, 199 requestors: identityset.Union(requestors...), 200 }, nil 201 } 202 203 // makeDelegationRule preprocesses admin.DelegationRule proto. 204 // 205 // It also double checks that the rule is passing validation. The check may 206 // fail if new code uses old configs, still stored in the datastore. 207 func makeDelegationRule(c context.Context, rule *admin.DelegationRule) (*delegationRule, error) { 208 ctx := &validation.Context{Context: c} 209 validateRule(ctx, rule.Name, rule) 210 if err := ctx.Finalize(); err != nil { 211 return nil, err 212 } 213 214 // The main validation step has been done above. Here we just assert that 215 // everything looks sane (it should). See corresponding chunks of 216 // 'ValidateRule' code. 217 requestors, err := identityset.FromStrings(rule.Requestor, nil) 218 if err != nil { 219 panic(err) 220 } 221 delegators, err := identityset.FromStrings(rule.AllowedToImpersonate, skipRequestorOrProjects) 222 if err != nil { 223 panic(err) 224 } 225 audience, err := identityset.FromStrings(rule.AllowedAudience, skipRequestor) 226 if err != nil { 227 panic(err) 228 } 229 services, err := identityset.FromStrings(rule.TargetService, nil) 230 if err != nil { 231 panic(err) 232 } 233 234 return &delegationRule{ 235 rule: rule, 236 requestors: requestors, 237 delegators: delegators, 238 audience: audience, 239 services: services, 240 addRequestorAsDelegator: sliceHasString(rule.AllowedToImpersonate, Requestor), 241 addRequestorToAudience: sliceHasString(rule.AllowedAudience, Requestor), 242 addProjectsAsDelegators: sliceHasString(rule.AllowedToImpersonate, Projects), 243 }, nil 244 } 245 246 func skipRequestor(s string) bool { 247 return s == Requestor 248 } 249 250 func skipRequestorOrProjects(s string) bool { 251 return s == Requestor || s == Projects 252 } 253 254 func sliceHasString(slice []string, str string) bool { 255 for _, s := range slice { 256 if s == str { 257 return true 258 } 259 } 260 return false 261 } 262 263 // ConfigRevision is part of policy.Queryable interface. 264 func (r *Rules) ConfigRevision() string { 265 return r.revision 266 } 267 268 // IsAuthorizedRequestor returns true if the caller belongs to 'requestor' set 269 // of at least one rule. 270 func (r *Rules) IsAuthorizedRequestor(c context.Context, id identity.Identity) (bool, error) { 271 return r.requestors.IsMember(c, id) 272 } 273 274 // FindMatchingRule finds one and only one rule matching the query. 275 // 276 // If multiple rules match or none rules match, an error is returned. 277 func (r *Rules) FindMatchingRule(c context.Context, q *RulesQuery) (*admin.DelegationRule, error) { 278 var matches []*admin.DelegationRule 279 for _, rule := range r.rules { 280 switch yes, err := rule.matchesQuery(c, q); { 281 case err != nil: 282 return nil, err // usually transient 283 case yes: 284 matches = append(matches, rule.rule) 285 } 286 } 287 288 if len(matches) == 0 { 289 return nil, fmt.Errorf("no matching delegation rules in the config") 290 } 291 292 if len(matches) > 1 { 293 names := make([]string, len(matches)) 294 for i, m := range matches { 295 names[i] = fmt.Sprintf("%q", m.Name) 296 } 297 return nil, fmt.Errorf( 298 "ambiguous request, multiple delegation rules match (%s)", 299 strings.Join(names, ", ")) 300 } 301 302 return matches[0], nil 303 } 304 305 // matchesQuery returns true if this rule matches the query. 306 // 307 // See doc in config.proto, DelegationRule for exact description of when this 308 // happens. Basically, all sets in rule must be supersets of corresponding sets 309 // in RulesQuery. 310 // 311 // May return transient errors. 312 func (rule *delegationRule) matchesQuery(c context.Context, q *RulesQuery) (bool, error) { 313 // Rule's 'requestor' set contains the requestor? 314 switch found, err := rule.requestors.IsMember(c, q.Requestor); { 315 case err != nil: 316 return false, err 317 case !found: 318 return false, nil 319 } 320 321 // Rule's 'delegators' set contains the identity being delegated/impersonated? 322 switch yes, err := rule.matchesDelegator(c, q); { 323 case err != nil: 324 return false, err 325 case !yes: 326 return false, nil 327 } 328 329 // Rule's 'audience' is superset of requested audience? 330 allowedAudience := rule.audience 331 if rule.addRequestorToAudience { 332 allowedAudience = identityset.Extend(allowedAudience, q.Requestor) 333 } 334 if !allowedAudience.IsSuperset(q.Audience) { 335 return false, nil 336 } 337 338 // Rule's allowed targets is superset of requested targets? 339 if !rule.services.IsSuperset(q.Services) { 340 return false, nil 341 } 342 343 return true, nil 344 } 345 346 // matchesDelegator is true if 'q.Delegator' is in 'delegators' set (logically). 347 func (rule *delegationRule) matchesDelegator(c context.Context, q *RulesQuery) (bool, error) { 348 if rule.addProjectsAsDelegators && q.Delegator.Kind() == identity.Project { 349 return true, nil 350 } 351 allowedDelegators := rule.delegators 352 if rule.addRequestorAsDelegator { 353 allowedDelegators = identityset.Extend(allowedDelegators, q.Requestor) 354 } 355 return allowedDelegators.IsMember(c, q.Delegator) 356 }