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  }