go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/authdb/internal/realmset/realmset.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 realmset provides queryable representation of LUCI Realms DB. 16 // 17 // Used internally by authdb.Snapshot. 18 package realmset 19 20 import ( 21 "context" 22 "sort" 23 "strings" 24 25 "go.chromium.org/luci/common/data/stringset" 26 "go.chromium.org/luci/common/errors" 27 28 "go.chromium.org/luci/server/auth/authdb/internal/conds" 29 "go.chromium.org/luci/server/auth/authdb/internal/graph" 30 "go.chromium.org/luci/server/auth/realms" 31 "go.chromium.org/luci/server/auth/service/protocol" 32 ) 33 34 // ExpectedAPIVersion is the supported value of api_version field. 35 // 36 // See Build implementation for details. 37 const ExpectedAPIVersion = 1 38 39 // Realms is a queryable representation of realms.Realms proto. 40 type Realms struct { 41 perms map[string]PermissionIndex // permission name -> its index 42 names stringset.Set // just names of all defined realms 43 realms map[realmAndPerm]Bindings // <realm, perm> -> who has it under which conditions 44 data map[string]*protocol.RealmData // per-realm attached RealmData 45 46 // Used by QueryBindings: perm -> project -> [(realm, bindings)]. 47 bindingsIdx map[PermissionIndex]map[string][]RealmBindings 48 } 49 50 // PermissionIndex is used in place of permission names. 51 // 52 // Note: should match an int type used in `permissions` field in the proto. 53 type PermissionIndex uint32 54 55 // Binding represents a set of principals and a condition when it can be used. 56 // 57 // See Bindings(...) method for more details. 58 type Binding struct { 59 Condition *conds.Condition // nil if the binding is unconditional 60 Groups graph.SortedNodeSet 61 Idents stringset.Set 62 } 63 64 // badness is an overall heuristic score of how complex this binding to 65 // evaluate and how likely it will apply. 66 // 67 // 0 means "easy to evaluate, high likelihood of applying". Used to sort 68 // bindings in Bindings array returned by Bindings(...). 69 func (b *Binding) badness() int { 70 // TODO(vadimsh): This can be improved. For example, bindings with groups 71 // that contain globs (like `user:*`) are more likely to apply and should 72 // have lower badness. 73 if b.Condition == nil { 74 return 0 75 } 76 return 1 77 } 78 79 // Bindings is a list of bindings in a single realm for a single permission. 80 type Bindings []Binding 81 82 // Check returns true of any of the bindings in the list are applying. 83 // 84 // Checks conditions on `attrs` and memberships of the identity represented by 85 // `q`. 86 func (b Bindings) Check(ctx context.Context, q *graph.MembershipsQueryCache, attrs realms.Attrs) bool { 87 for _, binding := range b { 88 if binding.Condition == nil || binding.Condition.Eval(ctx, attrs) { 89 switch { 90 case binding.Idents.Has(string(q.Identity)): 91 return true // was granted the permission explicitly in the ACL 92 case q.IsMemberOfAny(binding.Groups): 93 return true // has the permission through a group 94 } 95 } 96 } 97 return false 98 } 99 100 // RealmBindings is a realm name plus bindings for a single permission there. 101 // 102 // Used as part of QueryBindings return value. 103 type RealmBindings struct { 104 // Realms is a full realm name as "<project>:<name>". 105 Realm string 106 // Bindings is a list of bindings for a permission passed to QueryBindings. 107 Bindings Bindings 108 } 109 110 // realmAndPerm is used as a composite key in `realms` map. 111 type realmAndPerm struct { 112 realm string 113 perm PermissionIndex 114 } 115 116 // PermissionIndex returns an index of the given permission. 117 // 118 // It can be passed to Bindings(...). Returns (0, false) if there's no such 119 // permission in the Realms DB. 120 func (r *Realms) PermissionIndex(perm realms.Permission) (idx PermissionIndex, ok bool) { 121 idx, ok = r.perms[perm.Name()] 122 return 123 } 124 125 // HasRealm returns true if the given realm exists in the DB. 126 func (r *Realms) HasRealm(realm string) bool { 127 return r.names.Has(realm) 128 } 129 130 // Data returns RealmData attached to a realm or nil if none. 131 func (r *Realms) Data(realm string) *protocol.RealmData { 132 return r.data[realm] 133 } 134 135 // Bindings returns representation of bindings that define who has the requested 136 // permission in the given realm. 137 // 138 // Each returned binding is a tuple (condition, groups, identities): 139 // - Condition: a predicate over realms.Attrs map that evaluates to true if 140 // this binding is "active". Inactive bindings should be skipped. 141 // - Groups: a set of groups with principals that have the permission, 142 // represented by a sorted slice of group indexes in a graph.QueryableGraph 143 // which was passed to Build(). 144 // - Identities: a set of identity strings that were specified in the realm 145 // ACL directly (not via a group). 146 // 147 // The permission should be specified as its index obtained via PermissionIndex. 148 // 149 // The realm name is not validated. Unknown or invalid realms are silently 150 // treated as empty. No fallback to @root happens. 151 // 152 // Returns nil if the requested permission is not mentioned in any binding in 153 // the realm at all. 154 func (r *Realms) Bindings(realm string, perm PermissionIndex) Bindings { 155 return r.realms[realmAndPerm{realm, perm}] 156 } 157 158 // QueryBindings returns **all** bindings for the given permission across all 159 // realms and projects. 160 // 161 // The result is a map "project name => list of (realm, bindings for the 162 // requested permission in this realm)". It includes only projects and realms 163 // that have bindings for the queried permission. The order of items in the list 164 // is not well-defined. 165 // 166 // This information is available only for permission flagged with 167 // UsedInQueryRealms. Returns `ok == false` if `perm` was not flagged. 168 func (r *Realms) QueryBindings(perm PermissionIndex) (map[string][]RealmBindings, bool) { 169 res, ok := r.bindingsIdx[perm] 170 return res, ok 171 } 172 173 // Build constructs Realms from the proto message, the group graph and 174 // permissions registered by the processes. 175 // 176 // Only registered permissions will be queriable. Bindings with all other 177 // permissions will be ignored to save RAM. 178 func Build(r *protocol.Realms, qg *graph.QueryableGraph, registered map[realms.Permission]realms.PermissionFlags) (*Realms, error) { 179 // Do not use realms.Realms we don't understand. Better to go offline 180 // completely than mistakenly allow access to something private by 181 // misinterpreting realm rules (e.g. if a new hypothetical DENY rule is 182 // misinterpreted as ALLOW). 183 // 184 // Bumping `api_version` (if it ever happens) should be done extremely 185 // carefully in multiple stages: 186 // 1. Update components.auth to understand both new and old api_version. 187 // 2. Redeploy *everything*. 188 // 3. Update Auth Service to generate realms.Realms using the new API. 189 if r.ApiVersion != ExpectedAPIVersion { 190 return nil, errors.Reason( 191 "Realms proto has api_version %d not compatible with this service (it expects %d)", 192 r.ApiVersion, ExpectedAPIVersion).Err() 193 } 194 195 // Build map: permission name -> its index (since Binding messages operate 196 // with indexes). Using ints as keys is also slightly faster than strings. 197 perms := make(map[string]PermissionIndex, len(r.Permissions)) 198 for idx, perm := range r.Permissions { 199 perms[perm.Name] = PermissionIndex(idx) 200 } 201 202 // Build a set of permission indexes the process is interested in checking. 203 // All other permissions will simply be ignored to avoid wasting RAM on them 204 // (they won't be checked anyway). 205 activePerms := make(map[PermissionIndex]struct{}, len(registered)) 206 for perm := range registered { 207 if idx, ok := perms[perm.Name()]; ok { 208 activePerms[idx] = struct{}{} 209 } 210 } 211 212 // Gather names of all realms for HasRealm check. 213 names := stringset.New(len(r.Realms)) 214 for _, realm := range r.Realms { 215 names.Add(realm.Name) 216 } 217 218 // This is the `realms` map under construction. We'll shrink its memory 219 // footprint at the end. 220 type bindingKey struct { 221 realmAndPerm 222 cond *conds.Condition 223 } 224 realmsToBe := map[bindingKey]principalSet{} 225 counts := map[realmAndPerm]int{} 226 227 // A caching factory of Conditions for conditional bindings. 228 conds := conds.NewBuilder(r.Conditions) 229 230 // interner is used to deduplicate memory used to store identity names. 231 interner := stringInterner{} 232 233 // Visit all bindings in all realms and update principal sets in realmsToBe. 234 for _, realm := range r.Realms { 235 for _, binding := range realm.Bindings { 236 // Categorize 'principals' into groups and identity strings. 237 groups, idents := categorizePrincipals(binding.Principals, qg, interner) 238 if len(groups) == 0 && len(idents) == 0 { 239 continue 240 } 241 242 // Build a condition predicate (`nil` means "no condition"). If such 243 // predicate was already seen before, returns the existing condition, so 244 // using Condition pointers in map keys is OK. Returns an error if 245 // a condition index in binding.Conditions is out of bounds or the 246 // condition is malformed. This should not happen in a valid AuthDB. 247 cond, err := conds.Condition(binding.Conditions) 248 if err != nil { 249 return nil, errors.Annotate(err, "invalid binding %q in realm %q", binding, realm.Name).Err() 250 } 251 252 // Add principals into the corresponding principal sets in realmsToBe. 253 for _, permIdx := range binding.Permissions { 254 permIdx := PermissionIndex(permIdx) 255 if _, yes := activePerms[permIdx]; !yes { 256 continue 257 } 258 key := bindingKey{realmAndPerm{realm.Name, permIdx}, cond} 259 if ps, ok := realmsToBe[key]; ok { 260 ps.add(groups, idents) 261 } else { 262 realmsToBe[key] = newPrincipalSet(groups, idents) 263 counts[key.realmAndPerm] += 1 264 } 265 } 266 } 267 } 268 269 // Replace identically looking group sets with references to a single copy. 270 // Collect conditional bindings for the same (realm, perm) key into an array, 271 // since we'll need to evaluate them sequentially when serving HasPermission 272 // checks. 273 realmMap := make(map[realmAndPerm]Bindings, len(counts)) 274 dedupper := graph.NodeSetDedupper{} 275 for key, ps := range realmsToBe { 276 groups, idents := ps.finalize(dedupper) 277 if realmMap[key.realmAndPerm] == nil { 278 realmMap[key.realmAndPerm] = make(Bindings, 0, counts[key.realmAndPerm]) 279 } 280 realmMap[key.realmAndPerm] = append(realmMap[key.realmAndPerm], Binding{ 281 Condition: key.cond, 282 Groups: groups, 283 Idents: idents, 284 }) 285 } 286 287 // Order bindings by "badness" of checking (easiest to check first) and 288 // chances of applying. Right now this uses a very simplistic heuristic: 289 // unconditional bindings are easier to check and more likely to apply than 290 // conditional ones. 291 for _, bindings := range realmMap { 292 sort.Slice(bindings, func(l, r int) bool { 293 if bl, br := bindings[l].badness(), bindings[r].badness(); bl != br { 294 return bl < br 295 } 296 // Order bindings of equal "badness" deterministically based on index of 297 // their conditions (which ultimately depends on order of data in Realms 298 // proto). This simplifies tests and makes HasPermission check 299 // performance more deterministic too. 300 idxLeft := 0 301 if bindings[l].Condition != nil { 302 idxLeft = bindings[l].Condition.Index() + 1 303 } 304 idxRight := 0 305 if bindings[r].Condition != nil { 306 idxRight = bindings[r].Condition.Index() + 1 307 } 308 return idxLeft < idxRight 309 }) 310 } 311 312 // Extract attached per-realm data into a queryable map. 313 count := 0 314 for _, realm := range r.Realms { 315 if realm.Data != nil { 316 count++ 317 } 318 } 319 data := make(map[string]*protocol.RealmData, count) 320 for _, realm := range r.Realms { 321 if realm.Data != nil { 322 data[realm.Name] = realm.Data 323 } 324 } 325 326 // For all permissions with UsedInQueryRealms flag, build a data set with all 327 // realms that have this permission. This allows skipping unrelated realms 328 // in QueryRealms. Note that we'll reuse Bindings slices from `realmMap`, so 329 // we pay extra RAM only for actual mapping. 330 bindingsIdx := make(map[PermissionIndex]map[string][]RealmBindings, len(registered)) 331 for perm, flags := range registered { 332 if flags&realms.UsedInQueryRealms != 0 { 333 if permIdx, ok := perms[perm.Name()]; ok { 334 bindingsIdx[permIdx] = map[string][]RealmBindings{} 335 } 336 } 337 } 338 for realmAndPerm, bindings := range realmMap { 339 if projToBindings, ok := bindingsIdx[realmAndPerm.perm]; ok { 340 proj, _ := realms.Split(realmAndPerm.realm) 341 projToBindings[proj] = append(projToBindings[proj], RealmBindings{ 342 Realm: realmAndPerm.realm, 343 Bindings: bindings, 344 }) 345 } 346 } 347 348 return &Realms{ 349 perms: perms, 350 names: names, 351 realms: realmMap, 352 data: data, 353 bindingsIdx: bindingsIdx, 354 }, nil 355 } 356 357 // stringInterner implements string interning to save some memory. 358 type stringInterner map[string]string 359 360 // intern returns an interned copy of 's'. 361 func (si stringInterner) intern(s string) string { 362 if existing, ok := si[s]; ok { 363 return existing 364 } 365 si[s] = s 366 return s 367 } 368 369 // categorizePrincipals splits a list of principals into a list of groups 370 // (identified by their indexes in a QueryableGraph) and list of identity names. 371 // 372 // Unknown groups are silently skipped. 373 func categorizePrincipals(p []string, qg *graph.QueryableGraph, interner stringInterner) (groups []graph.NodeIndex, idents []string) { 374 for _, principal := range p { 375 if strings.HasPrefix(principal, "group:") { 376 if idx, ok := qg.GroupIndex(strings.TrimPrefix(principal, "group:")); ok { 377 groups = append(groups, idx) 378 } 379 } else { 380 idents = append(idents, interner.intern(principal)) 381 } 382 } 383 return 384 } 385 386 // principalSet represents a set of groups and identities. 387 // 388 // It is used transiently when constructing the final memory-optimized set in 389 // Build. 390 type principalSet struct { 391 groups graph.NodeSet 392 idents stringset.Set 393 } 394 395 func newPrincipalSet(groups []graph.NodeIndex, idents []string) principalSet { 396 ps := principalSet{ 397 groups: make(graph.NodeSet, len(groups)), 398 idents: stringset.New(len(idents)), 399 } 400 ps.add(groups, idents) 401 return ps 402 } 403 404 func (ps principalSet) add(groups []graph.NodeIndex, idents []string) { 405 for _, idx := range groups { 406 ps.groups.Add(idx) 407 } 408 for _, ident := range idents { 409 ps.idents.Add(ident) 410 } 411 } 412 413 // finalize produces a memory-optimized representation of this principal set. 414 // 415 // It replace identically looking group sets with references to a single copy 416 // using the given dedupper. It also throws away zero-length sets replacing them 417 // with nils. 418 // 419 // Non-empty identity sets are kept as is without any dedupping, assuming using 420 // identities in Realm ACLs directly is rare and not worth optimizing for (on 421 // top of the string interning optimization we've already done). 422 func (ps principalSet) finalize(dedupper graph.NodeSetDedupper) (graph.SortedNodeSet, stringset.Set) { 423 var groups graph.SortedNodeSet 424 if len(ps.groups) > 0 { 425 groups = dedupper.Dedup(ps.groups) 426 } 427 var idents stringset.Set 428 if ps.idents.Len() > 0 { 429 idents = ps.idents 430 } 431 return groups, idents 432 }