blob: c2d4ec3cf468dafae13de1fd79c54ddfee0d893a [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package identityset implements a set-like structure for identity.Identity.
package identityset
import (
"context"
"fmt"
"sort"
"strings"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/server/auth"
)
type identSet map[identity.Identity]struct{}
type groupSet map[string]struct{}
// Set is a set of identities represented as sets of IDs and groups.
//
// Groups are generally not expanded, but treated as different kind of items,
// so essentially this struct represents two sets: a set of explicitly specified
// identities, and a set of groups. The exception to this rule is IsMember
// function that looks inside the groups.
type Set struct {
All bool // if true, this set contains all possible identities
IDs identSet // set of identity.Identity strings
Groups groupSet // set of group names
}
// AddIdentity adds a single identity to the set.
//
// The receiver must not be nil.
func (s *Set) AddIdentity(id identity.Identity) {
if !s.All {
if s.IDs == nil {
s.IDs = make(identSet, 1)
}
s.IDs[id] = struct{}{}
}
}
// AddGroup adds a single group to the set.
//
// The receiver must not be nil.
func (s *Set) AddGroup(group string) {
if !s.All {
if s.Groups == nil {
s.Groups = make(groupSet, 1)
}
s.Groups[group] = struct{}{}
}
}
// IsEmpty returns true if this set is empty.
//
// 'nil' receiver value is valid and represents an empty set.
func (s *Set) IsEmpty() bool {
return s == nil || (!s.All && len(s.IDs) == 0 && len(s.Groups) == 0)
}
// IsMember returns true if the given identity is in the set.
//
// It looks inside the groups too.
//
// 'nil' receiver value is valid and represents an empty set.
func (s *Set) IsMember(c context.Context, id identity.Identity) (bool, error) {
if s == nil {
return false, nil
}
if s.All {
return true, nil
}
if _, ok := s.IDs[id]; ok {
return true, nil
}
if len(s.Groups) != 0 {
groups := make([]string, 0, len(s.Groups))
for gr := range s.Groups {
groups = append(groups, gr)
}
return auth.GetState(c).DB().IsMember(c, id, groups)
}
return false, nil
}
// IsSubset returns true if this set if a subset of another set.
//
// Two equal sets are considered subsets of each other.
//
// It doesn't attempt to expand groups. Compares IDs and Groups sets separately,
// as independent kinds of entities.
//
// 'nil' receiver and argument values are valid and represent empty sets.
func (s *Set) IsSubset(superset *Set) bool {
// An empty set is subset of any other set (including empty sets).
if s.IsEmpty() {
return true
}
// An empty set is not a superset of any non-empty set.
if superset.IsEmpty() {
return false
}
// The universal set is subset of only itself.
if s.All {
return superset.All
}
// The universal set is superset of any other set.
if superset.All {
return true
}
// Is s.IDs a subset of superset.IDs?
if len(superset.IDs) < len(s.IDs) {
return false
}
for id := range s.IDs {
if _, ok := superset.IDs[id]; !ok {
return false
}
}
// Is s.Groups a subset of superset.Groups?
if len(superset.Groups) < len(s.Groups) {
return false
}
for group := range s.Groups {
if _, ok := superset.Groups[group]; !ok {
return false
}
}
return true
}
// IsSuperset returns true if this set is a super set of another set.
//
// Two equal sets are considered supersets of each other.
//
// 'nil' receiver and argument values are valid and represent empty sets.
func (s *Set) IsSuperset(subset *Set) bool {
return subset.IsSubset(s)
}
// ToStrings returns a sorted list of strings representing this set.
//
// See 'FromStrings' for the format of this list.
func (s *Set) ToStrings() []string {
if s.IsEmpty() {
return nil
}
if s.All {
return []string{"*"}
}
out := make([]string, 0, len(s.IDs)+len(s.Groups))
for ident := range s.IDs {
out = append(out, string(ident))
}
for group := range s.Groups {
out = append(out, "group:"+group)
}
sort.Strings(out)
return out
}
// FromStrings constructs a Set by parsing a slice of strings.
//
// Each string is either:
// * "<kind>:<id>" identity string.
// * "group:<name>" group reference.
// * "*" token to mean "All identities".
//
// Any string that matches 'skip' predicate is skipped.
func FromStrings(str []string, skip func(string) bool) (*Set, error) {
set := &Set{}
for _, s := range str {
if skip != nil && skip(s) {
continue
}
switch {
case s == "*":
set.All = true
case strings.HasPrefix(s, "group:"):
gr := strings.TrimPrefix(s, "group:")
if gr == "" {
return nil, fmt.Errorf("invalid entry %q", s)
}
set.AddGroup(gr)
default:
id, err := identity.MakeIdentity(s)
if err != nil {
return nil, err
}
set.AddIdentity(id)
}
}
// If '*' was used, separately listed IDs and groups are redundant.
if set.All {
set.Groups = nil
set.IDs = nil
}
return set, nil
}
// Union returns a union of a list of sets.
func Union(sets ...*Set) *Set {
estimateIDs := 0
estimateGroups := 0
for _, s := range sets {
if s == nil {
continue
}
if s.All {
return &Set{All: true}
}
if len(s.IDs) > estimateIDs {
estimateIDs = len(s.IDs)
}
if len(s.Groups) > estimateGroups {
estimateGroups = len(s.Groups)
}
}
union := &Set{}
if estimateIDs != 0 {
union.IDs = make(identSet, estimateIDs)
}
if estimateGroups != 0 {
union.Groups = make(groupSet, estimateGroups)
}
for _, s := range sets {
if s == nil {
continue
}
for ident := range s.IDs {
union.IDs[ident] = struct{}{}
}
for group := range s.Groups {
union.Groups[group] = struct{}{}
}
}
return union
}
// Extend returns a throw-away set that has one additional member.
//
// The returned set must not be modified, since it references data of the
// original set (to avoid unnecessary copying).
func Extend(orig *Set, id identity.Identity) *Set {
if orig.IsEmpty() {
return &Set{
IDs: identSet{
id: struct{}{},
},
}
}
if orig.All {
return orig
}
if _, ok := orig.IDs[id]; ok {
return orig
}
// Need to make a copy of orig.IDs to add 'id' there.
extended := make(identSet, len(orig.IDs)+1)
for origID := range orig.IDs {
extended[origID] = struct{}{}
}
extended[id] = struct{}{}
return &Set{
IDs: extended,
Groups: orig.Groups,
}
}