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  }