go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth_service/internal/realmsinternals/expansion.go (about) 1 // Copyright 2023 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 realmsinternals 16 17 import ( 18 "errors" 19 "fmt" 20 "sort" 21 "strings" 22 23 "google.golang.org/protobuf/proto" 24 25 "go.chromium.org/luci/common/data/sortby" 26 "go.chromium.org/luci/common/data/stringset" 27 lucierr "go.chromium.org/luci/common/errors" 28 realmsconf "go.chromium.org/luci/common/proto/realms" 29 "go.chromium.org/luci/server/auth/realms" 30 "go.chromium.org/luci/server/auth/service/protocol" 31 32 "go.chromium.org/luci/auth_service/internal/configs/validation" 33 "go.chromium.org/luci/auth_service/internal/permissions" 34 ) 35 36 var ( 37 // ErrFinalized is used when the ConditionsSet has already been finalized 38 // and further modifications are attempted. 39 ErrFinalized = errors.New("conditions set has already been finalized") 40 41 // ErrRoleNotFound is used when a role requested is not found in the internal permissionsDB. 42 ErrRoleNotFound = errors.New("role does not exist in internal representation") 43 44 // ErrImpossibleRole is used when there is an attempt to expand a role that is not allowed. 45 ErrImpossibleRole = errors.New("role is impossible, does not include one of the approved prefixes") 46 ) 47 48 // ConditionsSet normalizes and dedups conditions, maps them to integers. 49 // Assumes all incoming realmsconf.Condition are immutable and dedups 50 // them by pointer, as well as by normalized values. 51 // Also assumes the set of all possible *objects* ever passed to indexes(...) was 52 // also passed to addCond(...) first (so it could build id => index map). 53 // 54 // This makes hot indexes(...) function fast by allowing to lookup ids instead 55 // of (potentially huge) protobuf message values. 56 type ConditionsSet struct { 57 // normalized is a mapping from a serialized normalized protocol.Condition 58 // to a pair (normalized *protocol.Condition, its unique index) 59 normalized map[string]*conditionMapTuple 60 61 // indexMapping from serialized realms_config to its index. 62 indexMapping map[*realmsconf.Condition]uint32 63 64 // finalized is true if finalize() was called, see finalize for more info. 65 finalized bool 66 } 67 68 // conditionMapTuple is to represent the entries of normalized, reflects 69 // what index a Condition is tied to. 70 type conditionMapTuple struct { 71 cond *protocol.Condition 72 idx uint32 73 } 74 75 // addCond adds a *Condition from realms.cfg definition to the set if it's 76 // not already there. 77 // 78 // Returns ErrFinalized -- if set has already been finalized 79 func (cs *ConditionsSet) addCond(cond *realmsconf.Condition) error { 80 if cs.finalized { 81 return ErrFinalized 82 } 83 if _, ok := cs.indexMapping[cond]; ok { 84 return nil 85 } 86 87 norm := &protocol.Condition{} 88 var attr string 89 90 if cond.GetRestrict() != nil { 91 condValues := make([]string, len(cond.GetRestrict().GetValues())) 92 copy(condValues, cond.GetRestrict().GetValues()) 93 condSet := stringset.NewFromSlice(condValues...) 94 attr = cond.GetRestrict().GetAttribute() 95 norm.Op = &protocol.Condition_Restrict{ 96 Restrict: &protocol.Condition_AttributeRestriction{ 97 Attribute: attr, 98 Values: condSet.ToSortedSlice(), 99 }, 100 } 101 } 102 103 idx := uint32(len(cs.normalized)) 104 if condTup, ok := cs.normalized[conditionKey(norm)]; ok { 105 idx = condTup.idx 106 } 107 cs.normalized[conditionKey(norm)] = &conditionMapTuple{norm, idx} 108 cs.indexMapping[cond] = idx 109 return nil 110 } 111 112 // conditionKey generates a key by serializing a protocol.Condition. 113 func conditionKey(cond *protocol.Condition) string { 114 key, err := proto.Marshal(cond) 115 if err != nil { 116 return "" 117 } 118 return string(key) 119 } 120 121 // sortConditions sorts a given conditions slice by attribute first 122 // then by values. 123 func sortConditions(conds []*protocol.Condition) { 124 sort.Slice(conds, sortby.Chain{ 125 func(i, j int) bool { 126 return conds[i].GetRestrict().GetAttribute() < conds[j].GetRestrict().GetAttribute() 127 }, 128 func(i, j int) bool { 129 iValsLen, jValsLen := len(conds[i].GetRestrict().GetValues()), len(conds[j].GetRestrict().GetValues()) 130 iVals, jVals := conds[i].GetRestrict().GetValues(), conds[j].GetRestrict().GetValues() 131 if iValsLen == jValsLen { 132 for idx, iVal := range iVals { 133 if iVal == jVals[idx] { 134 continue 135 } 136 return iVal < jVals[idx] 137 } 138 } 139 return iValsLen < jValsLen 140 }, 141 }.Use) 142 } 143 144 // finalize finalizes the set by preventing any future addCond calls. 145 // 146 // Sorts the list of stored conditions by attribute first then by values. 147 // returns the final sorted list of protocol.Condition. 148 // 149 // Returns nil if ConditionSet is already finalized or if 150 // ConditionsSet is empty. 151 // 152 // Indexes returned by indexes() will refer to the indexes in this list. 153 func (cs *ConditionsSet) finalize() []*protocol.Condition { 154 if cs.finalized { 155 return nil 156 } 157 cs.finalized = true 158 159 conds := []*protocol.Condition{} 160 for _, curr := range cs.normalized { 161 conds = append(conds, curr.cond) 162 } 163 164 sortConditions(conds) 165 166 oldToNew := map[uint32]uint32{} 167 168 for idx, cond := range conds { 169 old := cs.normalized[conditionKey(cond)] 170 oldToNew[old.idx] = uint32(idx) 171 } 172 173 for key, old := range cs.indexMapping { 174 cs.indexMapping[key] = oldToNew[old] 175 } 176 177 if len(conds) == 0 { 178 return nil 179 } 180 181 return conds 182 } 183 184 // indexes returns a sorted slice of indexes 185 // 186 // Can be called only after finalize(). All given conditions must have previously 187 // been put into the set via addCond(). The returned indexes can have fewer 188 // elements if some conditions in conds are equivalent. 189 // 190 // The returned indexes is essentially a compact encoding of the overall AND 191 // condition expression in a binding. 192 func (cs *ConditionsSet) indexes(conds []*realmsconf.Condition) []uint32 { 193 if !cs.finalized { 194 return nil 195 } 196 if conds == nil { 197 return nil 198 } 199 if len(conds) == 1 { 200 if idx, ok := cs.indexMapping[conds[0]]; ok { 201 return []uint32{idx} 202 } 203 return nil 204 } 205 206 indexesSet := emptyIndexSet() 207 208 for _, cond := range conds { 209 v, ok := cs.indexMapping[cond] 210 if !ok { 211 return nil 212 } 213 indexesSet.add(v) 214 } 215 216 return indexesSet.toSortedSlice() 217 } 218 219 // indexSet is a set data structure for managing indexes when expanding realms and permissions. 220 type indexSet struct { 221 set map[uint32]struct{} 222 } 223 224 // add adds a given uint32 to the index set. 225 func (is *indexSet) add(v uint32) { 226 is.set[v] = struct{}{} 227 } 228 229 // IndexSetFromSlice converts a given slice of indexes and returns an IndexSet from them. 230 func IndexSetFromSlice(src []uint32) *indexSet { 231 res := emptyIndexSet() 232 for _, val := range src { 233 res.set[val] = struct{}{} 234 } 235 return res 236 } 237 238 // emptyIndexSet initializes and returns an empty IndexSet. 239 func emptyIndexSet() *indexSet { 240 return &indexSet{make(map[uint32]struct{})} 241 } 242 243 // update adds all indexes from other set. 244 func (is *indexSet) update(other *indexSet) { 245 for k := range other.set { 246 is.add(k) 247 } 248 } 249 250 // toSlice converts an IndexSet to a slice and returns it. 251 func (is *indexSet) toSlice() []uint32 { 252 res := make([]uint32, 0, len(is.set)) 253 for k := range is.set { 254 res = append(res, k) 255 } 256 return res 257 } 258 259 // toSortedSlice converts an IndexSet to a slice and then sorts the indexes, returning the 260 // result. 261 func (is *indexSet) toSortedSlice() []uint32 { 262 res := is.toSlice() 263 sort.Slice(res, func(i, j int) bool { 264 return res[i] < res[j] 265 }) 266 return res 267 } 268 269 // RolesExpander keeps track of permissions and role -> [permission] expansions. 270 // 271 // Permissions are represented internally as integers to speed up set operations. 272 // 273 // Should be used only with validated realmsconf.RealmsCfg. 274 type RolesExpander struct { 275 // builtinRoles is a mapping from roleName -> *permissions.Role 276 // these are generated from the permissions.cfg and translated to permissions 277 // db which is where these roles come from. If a role is not found here 278 // then it has not been defined in the permissions.cfg. This is assumed 279 // final state and should not be modified. 280 builtinRoles map[string]*permissions.Role 281 282 // customRoles is a mapping from roleName -> *realmsconf.CustomRole 283 // this mapping will be generated from permissionsDB and is defined in 284 // permissisions.go when the DB is initialized. This is assumed final 285 // state and should not be modifed. 286 customRoles map[string]*realmsconf.CustomRole 287 288 // permissions is a mapping from permission name to the internal index 289 // all permissions are converted to a uint32 index for faster queries 290 // this is the list of all declared permissions, this is initially definied 291 // in permissions.cfg and initialized in permissionsDB. This is assumed 292 // final state and should not be modified. 293 permissions map[string]uint32 294 295 // roles contains role to permissions mapping, keyed by roleName 296 // this mapping contains a set of all the permissions a given role 297 // is associated with. 298 roles map[string]*indexSet 299 } 300 301 // permIndex returns an internal index that represents the given permission string. 302 func (re *RolesExpander) permIndex(name string) uint32 { 303 idx, ok := re.permissions[name] 304 if !ok { 305 idx = uint32(len(re.permissions)) 306 re.permissions[name] = idx 307 } 308 return idx 309 } 310 311 // permIndexes returns internal indexes representing the given permission strings. 312 func (re *RolesExpander) permIndexes(names ...string) []uint32 { 313 res := make([]uint32, len(names)) 314 for idx, name := range names { 315 res[idx] = re.permIndex(name) 316 } 317 return res 318 } 319 320 // role returns an IndexSet of permissions for a given role. 321 // 322 // returns 323 // 324 // ErrRoleNotFound - if given roleName doesn't exist in permissionsDB 325 // ErrImpossibleRole - if roleName format is invalid 326 func (re *RolesExpander) role(roleName string) (*indexSet, error) { 327 if perms, ok := re.roles[roleName]; ok { 328 return perms, nil 329 } 330 331 var perms *indexSet 332 if strings.HasPrefix(roleName, validation.PrefixBuiltinRole) { 333 role, ok := re.builtinRoles[roleName] 334 if !ok { 335 return nil, lucierr.Annotate(ErrRoleNotFound, "builtinRole: %s", roleName).Err() 336 } 337 perms = IndexSetFromSlice(re.permIndexes(role.Permissions.ToSortedSlice()...)) 338 } else if strings.HasPrefix(roleName, validation.PrefixCustomRole) { 339 customRole, ok := re.customRoles[roleName] 340 if !ok { 341 return nil, lucierr.Annotate(ErrRoleNotFound, "customRole: %s", roleName).Err() 342 } 343 perms = IndexSetFromSlice(re.permIndexes(customRole.GetPermissions()...)) 344 for _, parent := range customRole.Extends { 345 parentRole, err := re.role(parent) 346 if err != nil { 347 return nil, err 348 } 349 perms.update(parentRole) 350 } 351 } else { 352 return nil, ErrImpossibleRole 353 } 354 355 if perms == nil { 356 perms = emptyIndexSet() 357 } 358 359 re.roles[roleName] = perms 360 return perms, nil 361 } 362 363 // sortedPermissions returns a sorted slice of permissions and slice 364 // mapping old -> new indexes. 365 func (re *RolesExpander) sortedPermissions() ([]string, []uint32) { 366 perms := make([]string, 0, len(re.permissions)) 367 for k := range re.permissions { 368 perms = append(perms, k) 369 } 370 sort.Strings(perms) 371 372 mapping := make([]uint32, len(re.permissions)) 373 for newIdx, perm := range perms { 374 oldIdx := re.permissions[perm] 375 mapping[oldIdx] = uint32(newIdx) 376 } 377 return perms, mapping 378 } 379 380 // RealmsExpander helps traverse the realm inheritance graph. 381 type RealmsExpander struct { 382 // rolesExpander will handle role expansion for the realms. 383 rolesExpander *RolesExpander 384 // condsSet will handle the expansion for conditions in realms. 385 condsSet *ConditionsSet 386 // realms is a mapping from realm name -> *realmsconf.Realm. 387 realms map[string]*realmsconf.Realm 388 // data is a mapping from realm name -> *protocol.RealmData. 389 data map[string]*protocol.RealmData 390 } 391 392 // parents returns the list of immediate parents given a realm. 393 // includes @root realm by default since all realms implicitly 394 // inherit from it. 395 func parents(realm *realmsconf.Realm) []string { 396 if realm.GetName() == realms.RootRealm { 397 return nil 398 } 399 pRealms := []string{} 400 pRealms = append(pRealms, realms.RootRealm) 401 for _, name := range realm.Extends { 402 if name != realms.RootRealm { 403 pRealms = append(pRealms, name) 404 } 405 } 406 return pRealms 407 } 408 409 // principalBindings binds a principal to a set of 410 // permissions and conditions 411 type principalBindings struct { 412 // name is the name of this principal, can be a user, group, glob 413 name string 414 // permissions contains the indexes of permissions bound to this principal 415 permissions *indexSet 416 // conditions contains the indexes of conditions related to this principal 417 conditions []uint32 418 } 419 420 // perPrincipalBindings returns a slice of principalBindings. 421 // 422 // Visits all bindings in the realm and its parent realms. Returns a lot 423 // of duplicates. It's the caller's job to skip them. 424 func (rlme *RealmsExpander) perPrincipalBindings(realm string) ([]*principalBindings, error) { 425 r, ok := rlme.realms[realm] 426 if !ok { 427 return nil, fmt.Errorf("realm %s not found in RealmsExpander", realm) 428 } 429 if r.GetName() != realm { 430 return nil, fmt.Errorf("given realm: %s does not match name found internally: %s", realm, r.GetName()) 431 } 432 pBindings := []*principalBindings{} 433 for _, b := range r.Bindings { 434 // set of permissions associated with this role 435 perms, err := rlme.rolesExpander.role(b.GetRole()) 436 if err != nil { 437 return nil, lucierr.Annotate(err, "there was an issue fetching permissions for this binding role").Err() 438 } 439 440 // sorted conditions associated with this binding 441 // conditions must be finalized at this point 442 conds := rlme.condsSet.indexes(b.GetConditions()) 443 for _, principal := range b.GetPrincipals() { 444 pBindings = append(pBindings, &principalBindings{principal, perms, conds}) 445 } 446 } 447 448 // go through parents and get the bindings too 449 for _, parent := range parents(r) { 450 parentBindings, err := rlme.perPrincipalBindings(parent) 451 if err != nil { 452 return nil, fmt.Errorf("failed when getting parent bindings for %s", realm) 453 } 454 pBindings = append(pBindings, parentBindings...) 455 } 456 return pBindings, nil 457 } 458 459 // realmData returns calculated protocol.RealmData for a given realm. 460 func (rlme *RealmsExpander) realmData(name string, extends []*protocol.RealmData) (*protocol.RealmData, error) { 461 _, ok := rlme.data[name] 462 if !ok { 463 rlm, found := rlme.realms[name] 464 if !found { 465 return nil, fmt.Errorf("realm %s not found in realms mapping", name) 466 } 467 for _, p := range parents(rlm) { 468 data, err := rlme.realmData(p, extends) 469 if err != nil { 470 return nil, err 471 } 472 extends = append(extends, data) 473 } 474 rlme.data[name] = deriveRealmData(rlm, extends) 475 } 476 return rlme.data[name], nil 477 } 478 479 // deriveRealmData calculates the protocol.RealmData from the realm config and parent data. 480 func deriveRealmData(realm *realmsconf.Realm, extends []*protocol.RealmData) *protocol.RealmData { 481 enforceInService := stringset.NewFromSlice(realm.EnforceInService...) 482 for _, d := range extends { 483 enforceInService.AddAll(d.GetEnforceInService()) 484 } 485 if len(enforceInService) == 0 { 486 return nil 487 } 488 return &protocol.RealmData{ 489 EnforceInService: enforceInService.ToSortedSlice(), 490 } 491 }