go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/utils/identityset/identityset.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 identityset implements a set-like structure for identity.Identity.
    16  package identityset
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"sort"
    22  	"strings"
    23  
    24  	"go.chromium.org/luci/auth/identity"
    25  	"go.chromium.org/luci/server/auth"
    26  )
    27  
    28  type identSet map[identity.Identity]struct{}
    29  type groupSet map[string]struct{}
    30  
    31  // Set is a set of identities represented as sets of IDs and groups.
    32  //
    33  // Groups are generally not expanded, but treated as different kind of items,
    34  // so essentially this struct represents two sets: a set of explicitly specified
    35  // identities, and a set of groups. The exception to this rule is IsMember
    36  // function that looks inside the groups.
    37  type Set struct {
    38  	All    bool     // if true, this set contains all possible identities
    39  	IDs    identSet // set of identity.Identity strings
    40  	Groups groupSet // set of group names
    41  }
    42  
    43  // AddIdentity adds a single identity to the set.
    44  //
    45  // The receiver must not be nil.
    46  func (s *Set) AddIdentity(id identity.Identity) {
    47  	if !s.All {
    48  		if s.IDs == nil {
    49  			s.IDs = make(identSet, 1)
    50  		}
    51  		s.IDs[id] = struct{}{}
    52  	}
    53  }
    54  
    55  // AddGroup adds a single group to the set.
    56  //
    57  // The receiver must not be nil.
    58  func (s *Set) AddGroup(group string) {
    59  	if !s.All {
    60  		if s.Groups == nil {
    61  			s.Groups = make(groupSet, 1)
    62  		}
    63  		s.Groups[group] = struct{}{}
    64  	}
    65  }
    66  
    67  // IsEmpty returns true if this set is empty.
    68  //
    69  // 'nil' receiver value is valid and represents an empty set.
    70  func (s *Set) IsEmpty() bool {
    71  	return s == nil || (!s.All && len(s.IDs) == 0 && len(s.Groups) == 0)
    72  }
    73  
    74  // IsMember returns true if the given identity is in the set.
    75  //
    76  // It looks inside the groups too.
    77  //
    78  // 'nil' receiver value is valid and represents an empty set.
    79  func (s *Set) IsMember(c context.Context, id identity.Identity) (bool, error) {
    80  	if s == nil {
    81  		return false, nil
    82  	}
    83  
    84  	if s.All {
    85  		return true, nil
    86  	}
    87  
    88  	if _, ok := s.IDs[id]; ok {
    89  		return true, nil
    90  	}
    91  
    92  	if len(s.Groups) != 0 {
    93  		groups := make([]string, 0, len(s.Groups))
    94  		for gr := range s.Groups {
    95  			groups = append(groups, gr)
    96  		}
    97  		return auth.GetState(c).DB().IsMember(c, id, groups)
    98  	}
    99  
   100  	return false, nil
   101  }
   102  
   103  // IsSubset returns true if this set if a subset of another set.
   104  //
   105  // Two equal sets are considered subsets of each other.
   106  //
   107  // It doesn't attempt to expand groups. Compares IDs and Groups sets separately,
   108  // as independent kinds of entities.
   109  //
   110  // 'nil' receiver and argument values are valid and represent empty sets.
   111  func (s *Set) IsSubset(superset *Set) bool {
   112  	// An empty set is subset of any other set (including empty sets).
   113  	if s.IsEmpty() {
   114  		return true
   115  	}
   116  
   117  	// An empty set is not a superset of any non-empty set.
   118  	if superset.IsEmpty() {
   119  		return false
   120  	}
   121  
   122  	// The universal set is subset of only itself.
   123  	if s.All {
   124  		return superset.All
   125  	}
   126  
   127  	// The universal set is superset of any other set.
   128  	if superset.All {
   129  		return true
   130  	}
   131  
   132  	// Is s.IDs a subset of superset.IDs?
   133  	if len(superset.IDs) < len(s.IDs) {
   134  		return false
   135  	}
   136  	for id := range s.IDs {
   137  		if _, ok := superset.IDs[id]; !ok {
   138  			return false
   139  		}
   140  	}
   141  
   142  	// Is s.Groups a subset of superset.Groups?
   143  	if len(superset.Groups) < len(s.Groups) {
   144  		return false
   145  	}
   146  	for group := range s.Groups {
   147  		if _, ok := superset.Groups[group]; !ok {
   148  			return false
   149  		}
   150  	}
   151  
   152  	return true
   153  }
   154  
   155  // IsSuperset returns true if this set is a super set of another set.
   156  //
   157  // Two equal sets are considered supersets of each other.
   158  //
   159  // 'nil' receiver and argument values are valid and represent empty sets.
   160  func (s *Set) IsSuperset(subset *Set) bool {
   161  	return subset.IsSubset(s)
   162  }
   163  
   164  // ToStrings returns a sorted list of strings representing this set.
   165  //
   166  // See 'FromStrings' for the format of this list.
   167  func (s *Set) ToStrings() []string {
   168  	if s.IsEmpty() {
   169  		return nil
   170  	}
   171  	if s.All {
   172  		return []string{"*"}
   173  	}
   174  	out := make([]string, 0, len(s.IDs)+len(s.Groups))
   175  	for ident := range s.IDs {
   176  		out = append(out, string(ident))
   177  	}
   178  	for group := range s.Groups {
   179  		out = append(out, "group:"+group)
   180  	}
   181  	sort.Strings(out)
   182  	return out
   183  }
   184  
   185  // FromStrings constructs a Set by parsing a slice of strings.
   186  //
   187  // Each string is either:
   188  //   - "<kind>:<id>" identity string.
   189  //   - "group:<name>" group reference.
   190  //   - "*" token to mean "All identities".
   191  //
   192  // Any string that matches 'skip' predicate is skipped.
   193  func FromStrings(str []string, skip func(string) bool) (*Set, error) {
   194  	set := &Set{}
   195  	for _, s := range str {
   196  		if skip != nil && skip(s) {
   197  			continue
   198  		}
   199  		switch {
   200  		case s == "*":
   201  			set.All = true
   202  		case strings.HasPrefix(s, "group:"):
   203  			gr := strings.TrimPrefix(s, "group:")
   204  			if gr == "" {
   205  				return nil, fmt.Errorf("invalid entry %q", s)
   206  			}
   207  			set.AddGroup(gr)
   208  		default:
   209  			id, err := identity.MakeIdentity(s)
   210  			if err != nil {
   211  				return nil, err
   212  			}
   213  			set.AddIdentity(id)
   214  		}
   215  	}
   216  	// If '*' was used, separately listed IDs and groups are redundant.
   217  	if set.All {
   218  		set.Groups = nil
   219  		set.IDs = nil
   220  	}
   221  	return set, nil
   222  }
   223  
   224  // Union returns a union of a list of sets.
   225  func Union(sets ...*Set) *Set {
   226  	estimateIDs := 0
   227  	estimateGroups := 0
   228  
   229  	for _, s := range sets {
   230  		if s == nil {
   231  			continue
   232  		}
   233  		if s.All {
   234  			return &Set{All: true}
   235  		}
   236  		if len(s.IDs) > estimateIDs {
   237  			estimateIDs = len(s.IDs)
   238  		}
   239  		if len(s.Groups) > estimateGroups {
   240  			estimateGroups = len(s.Groups)
   241  		}
   242  	}
   243  
   244  	union := &Set{}
   245  	if estimateIDs != 0 {
   246  		union.IDs = make(identSet, estimateIDs)
   247  	}
   248  	if estimateGroups != 0 {
   249  		union.Groups = make(groupSet, estimateGroups)
   250  	}
   251  
   252  	for _, s := range sets {
   253  		if s == nil {
   254  			continue
   255  		}
   256  		for ident := range s.IDs {
   257  			union.IDs[ident] = struct{}{}
   258  		}
   259  		for group := range s.Groups {
   260  			union.Groups[group] = struct{}{}
   261  		}
   262  	}
   263  
   264  	return union
   265  }
   266  
   267  // Extend returns a throw-away set that has one additional member.
   268  //
   269  // The returned set must not be modified, since it references data of the
   270  // original set (to avoid unnecessary copying).
   271  func Extend(orig *Set, id identity.Identity) *Set {
   272  	if orig.IsEmpty() {
   273  		return &Set{
   274  			IDs: identSet{
   275  				id: struct{}{},
   276  			},
   277  		}
   278  	}
   279  	if orig.All {
   280  		return orig
   281  	}
   282  	if _, ok := orig.IDs[id]; ok {
   283  		return orig
   284  	}
   285  
   286  	// Need to make a copy of orig.IDs to add 'id' there.
   287  	extended := make(identSet, len(orig.IDs)+1)
   288  	for origID := range orig.IDs {
   289  		extended[origID] = struct{}{}
   290  	}
   291  	extended[id] = struct{}{}
   292  
   293  	return &Set{
   294  		IDs:    extended,
   295  		Groups: orig.Groups,
   296  	}
   297  }