diff --git a/internal/dispatch/graph/lookupsubjects_test.go b/internal/dispatch/graph/lookupsubjects_test.go index 873d35539a..f8131e5a20 100644 --- a/internal/dispatch/graph/lookupsubjects_test.go +++ b/internal/dispatch/graph/lookupsubjects_test.go @@ -142,7 +142,7 @@ func TestSimpleLookupSubjects(t *testing.T) { foundSubjectIds := []string{} for _, result := range stream.Results() { for _, found := range result.FoundSubjects { - if len(found.ExcludedSubjectIds) > 0 { + if len(found.ExcludedSubjects) > 0 { continue } diff --git a/internal/membership/foundsubject.go b/internal/membership/foundsubject.go index fbb3a03d2b..2f01a53dfe 100644 --- a/internal/membership/foundsubject.go +++ b/internal/membership/foundsubject.go @@ -6,13 +6,14 @@ import ( "strings" core "github.com/authzed/spicedb/pkg/proto/core/v1" + v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" ) // NewFoundSubject creates a new FoundSubject for a subject and a set of its resources. func NewFoundSubject(subject *core.ObjectAndRelation, resources ...*core.ObjectAndRelation) FoundSubject { - return FoundSubject{subject, nil, tuple.NewONRSet(resources...)} + return FoundSubject{subject, nil, nil, tuple.NewONRSet(resources...)} } // FoundSubject contains a single found subject and all the relationships in which that subject @@ -22,7 +23,10 @@ type FoundSubject struct { subject *core.ObjectAndRelation // excludedSubjects are any subjects excluded. Only should be set if subject is a wildcard. - excludedSubjectIds []string + excludedSubjects []FoundSubject + + // caveatExpression is the conditional expression on the found subject. + caveatExpression *v1.CaveatExpression // relations are the relations under which the subject lives that informed the locating // of this subject for the root ONR. @@ -36,8 +40,12 @@ func (fs FoundSubject) GetSubjectId() string { return fs.subject.ObjectId } -func (fs FoundSubject) GetExcludedSubjectIds() []string { - return fs.excludedSubjectIds +func (fs FoundSubject) GetCaveatExpression() *v1.CaveatExpression { + return fs.caveatExpression +} + +func (fs FoundSubject) GetExcludedSubjects() []FoundSubject { + return fs.excludedSubjects } // Subject returns the Subject of the FoundSubject. @@ -58,13 +66,14 @@ func (fs FoundSubject) WildcardType() (string, bool) { // If not a wildcard subject, returns false. func (fs FoundSubject) ExcludedSubjectsFromWildcard() ([]*core.ObjectAndRelation, bool) { if fs.subject.ObjectId == tuple.PublicWildcard { - excludedSubjects := make([]*core.ObjectAndRelation, 0, len(fs.excludedSubjectIds)) - for _, excludedID := range fs.excludedSubjectIds { - excludedSubjects = append(excludedSubjects, &core.ObjectAndRelation{ - Namespace: fs.subject.Namespace, - ObjectId: excludedID, - Relation: fs.subject.Relation, - }) + excludedSubjects := make([]*core.ObjectAndRelation, 0, len(fs.excludedSubjects)) + for _, excludedSubject := range fs.excludedSubjects { + // TODO(jschorr): Fix once we add caveats support to debug tooling + if excludedSubject.caveatExpression != nil { + panic("not yet supported") + } + + excludedSubjects = append(excludedSubjects, excludedSubject.subject) } return excludedSubjects, true @@ -73,6 +82,20 @@ func (fs FoundSubject) ExcludedSubjectsFromWildcard() ([]*core.ObjectAndRelation return []*core.ObjectAndRelation{}, false } +func (fs FoundSubject) excludedSubjectIDs() []string { + excludedSubjects := make([]string, 0, len(fs.excludedSubjects)) + for _, excludedSubject := range fs.excludedSubjects { + // TODO(jschorr): Fix once we add caveats support to debug tooling + if excludedSubject.caveatExpression != nil { + panic("not yet supported") + } + + excludedSubjects = append(excludedSubjects, excludedSubject.subject.ObjectId) + } + + return excludedSubjects +} + // Relationships returns all the relationships in which the subject was found as per the expand. func (fs FoundSubject) Relationships() []*core.ObjectAndRelation { return fs.relationships.AsSlice() @@ -81,6 +104,11 @@ func (fs FoundSubject) Relationships() []*core.ObjectAndRelation { // ToValidationString returns the FoundSubject in a format that is consumable by the validationfile // package. func (fs FoundSubject) ToValidationString() string { + if fs.caveatExpression != nil { + // TODO(jschorr): Implement once we have a format for this. + panic("conditional found subjects not yet supported") + } + onrString := tuple.StringONR(fs.Subject()) excluded, isWildcard := fs.ExcludedSubjectsFromWildcard() if isWildcard && len(excluded) > 0 { diff --git a/internal/membership/trackingsubjectset.go b/internal/membership/trackingsubjectset.go index e55e71f3dc..22ac781ddc 100644 --- a/internal/membership/trackingsubjectset.go +++ b/internal/membership/trackingsubjectset.go @@ -4,7 +4,10 @@ import ( "fmt" "strings" + "github.com/authzed/spicedb/pkg/tuple" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/internal/util" ) @@ -62,24 +65,21 @@ func (tss *TrackingSubjectSet) getSetForKey(key string) util.BaseSubjectSet[Foun parts := strings.Split(key, "#") created := util.NewBaseSubjectSet[FoundSubject]( - func(subjectID string, excludedSubjectIDs []string, sources ...FoundSubject) FoundSubject { + func(subjectID string, caveatExpression *v1.CaveatExpression, excludedSubjects []FoundSubject, sources ...FoundSubject) FoundSubject { fs := NewFoundSubject(&core.ObjectAndRelation{ Namespace: parts[0], ObjectId: subjectID, Relation: parts[1], }) - fs.excludedSubjectIds = excludedSubjectIDs + fs.excludedSubjects = excludedSubjects + fs.caveatExpression = caveatExpression for _, source := range sources { - fs.relationships.UpdateFrom(source.relationships) + if source.relationships != nil { + fs.relationships.UpdateFrom(source.relationships) + } } return fs }, - func(existing FoundSubject, added FoundSubject) FoundSubject { - fs := NewFoundSubject(existing.subject) - fs.excludedSubjectIds = existing.excludedSubjectIds - fs.relationships = existing.relationships.Union(added.relationships) - return fs - }, ) tss.setByType[key] = created return created @@ -151,6 +151,16 @@ func (tss TrackingSubjectSet) removeExact(subjects ...*core.ObjectAndRelation) { } } +func (tss TrackingSubjectSet) getSubjects() []string { + var subjects []string + for _, subjectSet := range tss.setByType { + for _, foundSubject := range subjectSet.AsSlice() { + subjects = append(subjects, tuple.StringONR(foundSubject.subject)) + } + } + return subjects +} + // ToSlice returns a slice of all subjects found in the set. func (tss TrackingSubjectSet) ToSlice() []FoundSubject { subjects := []FoundSubject{} diff --git a/internal/membership/trackingsubjectset_test.go b/internal/membership/trackingsubjectset_test.go index c5d627b2d0..306053f0a9 100644 --- a/internal/membership/trackingsubjectset_test.go +++ b/internal/membership/trackingsubjectset_test.go @@ -45,10 +45,15 @@ func subtract(firstSet *TrackingSubjectSet, sets ...*TrackingSubjectSet) *Tracki } func fs(subjectType string, subjectID string, subjectRel string, excludedSubjectIDs ...string) FoundSubject { + excludedSubjects := make([]FoundSubject, 0, len(excludedSubjectIDs)) + for _, excludedSubjectID := range excludedSubjectIDs { + excludedSubjects = append(excludedSubjects, FoundSubject{subject: ONR(subjectType, excludedSubjectID, subjectRel)}) + } + return FoundSubject{ - subject: ONR(subjectType, subjectID, subjectRel), - excludedSubjectIds: excludedSubjectIDs, - relationships: tuple.NewONRSet(), + subject: ONR(subjectType, subjectID, subjectRel), + excludedSubjects: excludedSubjects, + relationships: tuple.NewONRSet(), } } @@ -330,8 +335,8 @@ func TestTrackingSubjectSet(t *testing.T) { found, ok := tc.set.Get(fs.subject) require.True(ok, "missing expected subject %s", fs.subject) - expectedExcluded := util.NewSet[string](fs.excludedSubjectIds...) - foundExcluded := util.NewSet[string](found.excludedSubjectIds...) + expectedExcluded := util.NewSet[string](fs.excludedSubjectIDs()...) + foundExcluded := util.NewSet[string](found.excludedSubjectIDs()...) require.Len(expectedExcluded.Subtract(foundExcluded).AsSlice(), 0, "mismatch on excluded subjects on %s: expected: %s, found: %s", fs.subject, expectedExcluded, foundExcluded) require.Len(foundExcluded.Subtract(expectedExcluded).AsSlice(), 0, "mismatch on excluded subjects on %s: expected: %s, found: %s", fs.subject, expectedExcluded, foundExcluded) } else { @@ -340,7 +345,7 @@ func TestTrackingSubjectSet(t *testing.T) { tc.set.removeExact(fs.subject) } - require.True(tc.set.IsEmpty()) + require.True(tc.set.IsEmpty(), "Found remaining: %v", tc.set.getSubjects()) }) } } diff --git a/internal/services/v1/permissions.go b/internal/services/v1/permissions.go index ef4bbee20e..8986359a2b 100644 --- a/internal/services/v1/permissions.go +++ b/internal/services/v1/permissions.go @@ -430,9 +430,14 @@ func (ps *permissionServer) LookupSubjects(req *v1.LookupSubjectsRequest, resp v stream := dispatchpkg.NewHandlingDispatchStream(ctx, func(result *dispatch.DispatchLookupSubjectsResponse) error { for _, foundSubject := range result.FoundSubjects { + excludedSubjectIDs := make([]string, 0, len(foundSubject.ExcludedSubjects)) + for _, excludedSubject := range foundSubject.ExcludedSubjects { + excludedSubjectIDs = append(excludedSubjectIDs, excludedSubject.SubjectId) + } + err := resp.Send(&v1.LookupSubjectsResponse{ SubjectObjectId: foundSubject.SubjectId, - ExcludedSubjectIds: foundSubject.ExcludedSubjectIds, + ExcludedSubjectIds: excludedSubjectIDs, LookedUpAt: revisionReadAt, }) if err != nil { diff --git a/internal/util/basesubjectset.go b/internal/util/basesubjectset.go index b47bb47903..de4e79d5a5 100644 --- a/internal/util/basesubjectset.go +++ b/internal/util/basesubjectset.go @@ -3,210 +3,229 @@ package util import ( "golang.org/x/exp/maps" + v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" ) -// Subject is a subject that can be placed into a BaseSubjectSet. -type Subject interface { +// Subject is a subject that can be placed into a BaseSubjectSet. It is defined in a generic +// manner to allow implementations that wrap BaseSubjectSet to add their own additional bookkeeping +// to the base implementation. +type Subject[T any] interface { // GetSubjectId returns the ID of the subject. For wildcards, this should be `*`. GetSubjectId() string - // GetExcludedSubjectIds returns the list of subject IDs excluded. Should only have values - // for wildcards. - GetExcludedSubjectIds() []string + // GetCaveatExpression returns the caveat expression for this subject, if it is conditional. + GetCaveatExpression() *v1.CaveatExpression + + // GetExcludedSubjects returns the list of subjects excluded. Must only have values + // for wildcards and must never be nested. + GetExcludedSubjects() []T } -// BaseSubjectSet defines a set that tracks accessible subjects. It is generic to allow -// other implementations to define the kind of tracking information associated with each subject. +// BaseSubjectSet defines a set that tracks accessible subjects, their exclusions (if wildcards), +// and all conditional expressions applied due to caveats. +// +// It is generic to allow other implementations to define the kind of tracking information +// associated with each subject. // // NOTE: Unlike a traditional set, unions between wildcards and a concrete subject will result // in *both* being present in the set, to maintain the proper set semantics around wildcards. -type BaseSubjectSet[T Subject] struct { - values map[string]T - constructor func(subjectID string, excludedSubjectIDs []string, sources ...T) T - combiner func(existing T, added T) T +type BaseSubjectSet[T Subject[T]] struct { + concrete *concreteSubjectSet[T] + wildcard *wildcardSubjectTracker[T] } // NewBaseSubjectSet creates a new base subject set for use underneath well-typed implementation. // -// The constructor function is a function that returns a new instancre of type T for a particular -// subject ID. -// The combiner function is optional, and if given, is used to combine existing elements in the -// set into a new element. This is typically used in debug packages for tracking of additional -// metadata. -func NewBaseSubjectSet[T Subject]( - constructor func(subjectID string, excludedSubjectIDs []string, sources ...T) T, - combiner func(existing T, added T) T, -) BaseSubjectSet[T] { +// The constructor function returns a new instance of type T for a particular subject ID. +func NewBaseSubjectSet[T Subject[T]](constructor constructor[T]) BaseSubjectSet[T] { return BaseSubjectSet[T]{ - values: map[string]T{}, - constructor: constructor, - combiner: combiner, + concrete: newConcreteSubjectSet[T](constructor), + wildcard: newWildcardSubjectTracker[T](constructor), } } +// constructor defines a function for constructing a new instance of the Subject type T for +// a subject ID, its (optional) conditional expression, any excluded subjects, and any sources +// for bookkeeping. The sources are those other subjects that were combined to create the current +// subject. +type constructor[T Subject[T]] func(subjectID string, conditionalExpression *v1.CaveatExpression, excludedSubjects []T, sources ...T) T + // Add adds the found subject to the set. This is equivalent to a Union operation between the -// existing set of subjects and a set containing the single subject. +// existing set of subjects and a set containing the single subject, but modifies the set +// *in place*. func (bss BaseSubjectSet[T]) Add(foundSubject T) bool { - existing, ok := bss.values[foundSubject.GetSubjectId()] - if !ok { - bss.values[foundSubject.GetSubjectId()] = foundSubject - } - if foundSubject.GetSubjectId() == tuple.PublicWildcard { - if ok { - // Intersect any exceptions, as union between one wildcard and another is a wildcard - // with the exceptions intersected. - // - // As a concrete example, given `user:* - user:tom` and `user:* - user:sarah`, the union - // of the two will be `*`, since each handles the other user. - excludedIds := NewSet[string](existing.GetExcludedSubjectIds()...).IntersectionDifference(NewSet[string](foundSubject.GetExcludedSubjectIds()...)) - bss.values[tuple.PublicWildcard] = bss.constructor(tuple.PublicWildcard, excludedIds.AsSlice(), existing, foundSubject) - } - } else { - // If there is an existing wildcard, remove the subject from its exclusions list. - if existingWildcard, ok := bss.values[tuple.PublicWildcard]; ok { - excludedIds := NewSet[string](existingWildcard.GetExcludedSubjectIds()...) - excludedIds.Remove(foundSubject.GetSubjectId()) - bss.values[tuple.PublicWildcard] = bss.constructor(tuple.PublicWildcard, excludedIds.AsSlice(), existingWildcard) - } + hasExisting := bss.wildcard.has() + bss.wildcard.unionWildcard(foundSubject) + bss.concrete.unionWildcard(foundSubject) + return !hasExisting } - if bss.combiner != nil && ok { - bss.values[foundSubject.GetSubjectId()] = bss.combiner(bss.values[foundSubject.GetSubjectId()], foundSubject) + if len(foundSubject.GetExcludedSubjects()) > 0 { + panic("found excluded subjects for non-wildcard") } - return !ok + _, hasExisting := bss.concrete.get(foundSubject.GetSubjectId()) + bss.wildcard.unionConcrete(foundSubject) + bss.concrete.unionConcrete(foundSubject) + return !hasExisting } // Subtract subtracts the given subject found the set. func (bss BaseSubjectSet[T]) Subtract(foundSubject T) { - // If the subject being removed is a wildcard, then remove any non-excluded items and adjust - // the existing wildcard. if foundSubject.GetSubjectId() == tuple.PublicWildcard { - exclusions := NewSet[string](foundSubject.GetExcludedSubjectIds()...) - for existingSubjectID := range bss.values { - if existingSubjectID == tuple.PublicWildcard { - continue - } - - if !exclusions.Has(existingSubjectID) { - delete(bss.values, existingSubjectID) - } - } + bss.concrete.subtractWildcard(foundSubject) - // Check for an existing wildcard and adjust accordingly. - if existing, ok := bss.values[tuple.PublicWildcard]; ok { - // A subtraction of a wildcard from another wildcard subtracts the exclusions from the second. - // from the first, and places them into the subject set directly. - // - // As a concrete example, given `user:* - user:tom` - `user:* - user:sarah`, the subtraction - // of the two will be `user:sarah`, since sarah is in the first set and not in the second. - existingExclusions := NewSet[string](existing.GetExcludedSubjectIds()...) - for _, subjectID := range foundSubject.GetExcludedSubjectIds() { - if !existingExclusions.Has(subjectID) { - bss.values[subjectID] = bss.constructor(subjectID, nil, foundSubject) - } - } + // NOTE: subtracting a wildcard from another wildcard can result in concrete subjects + // being produced, which are added here, if any. + for _, subject := range bss.wildcard.subtractWildcard(foundSubject) { + bss.concrete.unionConcrete(subject) } - - delete(bss.values, tuple.PublicWildcard) return } - // Remove the subject itself from the set. - delete(bss.values, foundSubject.GetSubjectId()) - - // If wildcard exists within the subject set, add the found subject to the exclusion list. - if wildcard, ok := bss.values[tuple.PublicWildcard]; ok { - exclusions := NewSet[string](wildcard.GetExcludedSubjectIds()...) - exclusions.Add(foundSubject.GetSubjectId()) - bss.values[tuple.PublicWildcard] = bss.constructor(tuple.PublicWildcard, exclusions.AsSlice(), wildcard) + if len(foundSubject.GetExcludedSubjects()) > 0 { + panic("found excluded subjects for non-wildcard") } + + bss.wildcard.subtractConcrete(foundSubject) + bss.concrete.subtractConcrete(foundSubject) } // SubtractAll subtracts the other set of subjects from this set of subtracts, modifying this -// set in place. +// set *in place*. func (bss BaseSubjectSet[T]) SubtractAll(other BaseSubjectSet[T]) { - for _, fs := range other.values { - bss.Subtract(fs) + if wildcard, ok := other.wildcard.get(); ok { + bss.Subtract(wildcard) + } + + for _, concrete := range other.concrete.subjects { + bss.Subtract(concrete) } } // IntersectionDifference performs an intersection between this set and the other set, modifying -// this set in place. +// this set *in place*. func (bss BaseSubjectSet[T]) IntersectionDifference(other BaseSubjectSet[T]) { - // Check if the other set has a wildcard. If so, remove any subjects found in the exclusion - // list. - // - // As a concrete example, given `user:tom` and `user:* - user:sarah`, the intersection should - // return `user:tom`, because everyone but `sarah` (including `tom`) is in the second set. - otherWildcard, hasOtherWildcard := other.values[tuple.PublicWildcard] - if hasOtherWildcard { - exclusion := NewSet[string](otherWildcard.GetExcludedSubjectIds()...) - for subjectID := range bss.values { - if subjectID != tuple.PublicWildcard { - if exclusion.Has(subjectID) { - delete(bss.values, subjectID) - } - } - } - } + // Process the intersection between the *concrete* subjects. + otherWildcard, hasOtherWildcard := other.wildcard.get() + otherWildcardExclusionsMap := other.wildcard.exclusionsMap() - // Remove any concrete subjects, if the other does not have a wildcard. - if !hasOtherWildcard { - for subjectID := range bss.values { - if subjectID != tuple.PublicWildcard { - if _, ok := other.values[subjectID]; !ok { - delete(bss.values, subjectID) - } - } + for _, currentConcreteSubject := range bss.concrete.subjects { + // Check if the other set either has a concrete or a wildcard. If not, remove this entry. + otherConcreteSubject, isConcreteInOtherSite := other.concrete.subjects[currentConcreteSubject.GetSubjectId()] + if isConcreteInOtherSite { + // And together to handle conditionals and sourcing. + bss.concrete.subjects[currentConcreteSubject.GetSubjectId()] = bss.concrete.constructor( + currentConcreteSubject.GetSubjectId(), + caveatAnd(currentConcreteSubject.GetCaveatExpression(), otherConcreteSubject.GetCaveatExpression()), + nil, + currentConcreteSubject, + otherConcreteSubject) + continue } - } - // Handle the case where the current set has a wildcard. We have to do two operations: - // - // 1) If the current set has a wildcard, either add the exclusions together if the other set - // also has a wildcard, or remove it if it did not. - // - // 2) We also add in any other set members that are not in the wildcard's exclusion set, as - // an intersection between a wildcard with exclusions and concrete types will always return - // concrete types as well. - if wildcard, ok := bss.values[tuple.PublicWildcard]; ok { - exclusions := NewSet[string](wildcard.GetExcludedSubjectIds()...) - - if hasOtherWildcard { - toBeExcluded := NewSet[string]() - toBeExcluded.Extend(wildcard.GetExcludedSubjectIds()) - toBeExcluded.Extend(otherWildcard.GetExcludedSubjectIds()) - bss.values[tuple.PublicWildcard] = bss.constructor(tuple.PublicWildcard, toBeExcluded.AsSlice(), wildcard, otherWildcard) - } else { - // Remove this wildcard. - delete(bss.values, tuple.PublicWildcard) + if !hasOtherWildcard { + delete(bss.concrete.subjects, currentConcreteSubject.GetSubjectId()) + continue } - // Add any concrete items from the other set into this set. This is necebssary because an - // intersection between a wildcard and a concrete should always return that concrete, except - // if it is within the wildcard's exclusion list. - // - // As a concrete example, given `user:* - user:tom` and `user:sarah`, the first set contains - // all users except `tom` (and thus includes `sarah`) and the second is `sarah`, so the result - // must include `sarah`. - for subjectID, fs := range other.values { - if subjectID != tuple.PublicWildcard && !exclusions.Has(subjectID) { - bss.values[subjectID] = fs - } + // Check if the concrete subject is excluded or updated. + otherExclusion, isExcluded := otherWildcardExclusionsMap[currentConcreteSubject.GetSubjectId()] + updatedConcrete, remainsConcrete := bss.intersectConcreteAndWildcard(currentConcreteSubject, otherWildcard, otherExclusion, isExcluded) + if !remainsConcrete { + delete(bss.concrete.subjects, currentConcreteSubject.GetSubjectId()) + continue } + + bss.concrete.subjects[currentConcreteSubject.GetSubjectId()] = updatedConcrete } - // If a combiner is defined, run it over all values from both sets. - if bss.combiner != nil { - for subjectID := range bss.values { - if added, ok := other.values[subjectID]; ok { - bss.values[subjectID] = bss.combiner(bss.values[subjectID], added) + // If the current set has a wildcard, add any concrete subjects from the other set that are + // not in the wildcard's exclusion set, and are not in the current set's concrete set. + if currentWildcard, hasCurrentWildcard := bss.wildcard.get(); hasCurrentWildcard { + currentWildcardExclusionsMap := bss.wildcard.exclusionsMap() + for _, otherConcreteSubject := range other.concrete.subjects { + _, isInCurrentSet := bss.concrete.subjects[otherConcreteSubject.GetSubjectId()] + if isInCurrentSet { + // Already present. + continue } + + // Otherwise, add to the concrete set if not in the exclusions of the wildcard or if the + // exclusion is conditional. + exclusion, isExcluded := currentWildcardExclusionsMap[otherConcreteSubject.GetSubjectId()] + updatedConcrete, remainsConcrete := bss.intersectConcreteAndWildcard(otherConcreteSubject, currentWildcard, exclusion, isExcluded) + if !remainsConcrete { + // Nothing to do, since already removed from the set. + continue + } + + bss.concrete.subjects[updatedConcrete.GetSubjectId()] = updatedConcrete } } + + // Update the wildcard. + bss.wildcard.intersectDiffWithWildcard(other.wildcard) +} + +func (bss BaseSubjectSet[T]) intersectConcreteAndWildcard(concrete T, wildcard T, exclusion T, isExcluded bool) (T, bool) { + // Cases: + // - The concrete subject is not excluded and the wildcard is not conditional => concrete is kept + // - The concrete subject is excluded and the wildcard is not conditional => concrete is removed + // - The concrete subject is not excluded but the wildcard is conditional => concrete is kept, but made conditional + // - The concrete subject is excluded and the wildcard is conditional => concrete is removed, since it is always excluded + // - The concrete subject is excluded and the wildcard is conditional and the exclusion is conditional => combined conditional + // - The concrete subject is excluded and the wildcard is not conditional but the exclusion *is* conditional => concrete is made conditional + switch { + case !isExcluded && wildcard.GetCaveatExpression() == nil: + // If the concrete is not excluded and the wildcard conditional is empty, then the concrete is always found. + // Example: {user:tom} & {*} => {user:tom} + return concrete, true + + case isExcluded && wildcard.GetCaveatExpression() == nil: + // If the concrete is excluded and the wildcard conditional is empty, then the concrete is never found. + // Example: {user:tom} & {* - user:tom} => {} + return concrete, false + + case !isExcluded && wildcard.GetCaveatExpression() != nil: + // The concrete subject is only included if the wildcard's caveat is true. + // Example: {user:tom}[acaveat] & {* - user:tom}[somecaveat] => {user:tom}[acaveat && somecaveat] + return bss.concrete.constructor( + concrete.GetSubjectId(), + caveatAnd(concrete.GetCaveatExpression(), wildcard.GetCaveatExpression()), + nil, + concrete, + wildcard), + true + + case isExcluded && exclusion.GetCaveatExpression() == nil: + // If the concrete is excluded and the exclusion is not conditional, then the concrete can never show up, + // regardless of whether the wildcard is conditional. + // Example: {user:tom} & {* - user:tom}[somecaveat] => {} + return concrete, false + + case isExcluded && exclusion.GetCaveatExpression() != nil: + // NOTE: whether the wildcard is itself conditional or not is handled within the expression combinators below. + // The concrete subject is included if the wildcard's caveat is true and the exclusion's caveat is *false*. + // Example: {user:tom}[acaveat] & {* - user:tom[ecaveat]}[wcaveat] => {user:tom[acaveat && wcaveat && !ecaveat]} + return bss.concrete.constructor( + concrete.GetSubjectId(), + caveatAnd( + concrete.GetCaveatExpression(), + caveatAnd( + wildcard.GetCaveatExpression(), + caveatInvert(exclusion.GetCaveatExpression()), + )), + nil, + concrete, + wildcard, + exclusion), + true + } + + return *new(T), false } // UnionWith adds the given subjects to this set, via a union call. @@ -224,32 +243,611 @@ func (bss BaseSubjectSet[T]) UnionWithSet(other BaseSubjectSet[T]) { // Get returns the found subject with the given ID in the set, if any. func (bss BaseSubjectSet[T]) Get(id string) (T, bool) { - found, ok := bss.values[id] - return found, ok + if id == tuple.PublicWildcard { + return bss.wildcard.get() + } + + return bss.concrete.get(id) } // IsEmpty returns whether the subject set is empty. func (bss BaseSubjectSet[T]) IsEmpty() bool { - return len(bss.values) == 0 + return bss.concrete.isEmpty() && !bss.wildcard.has() } // AsSlice returns the contents of the subject set as a slice of found subjects. func (bss BaseSubjectSet[T]) AsSlice() []T { - slice := make([]T, 0, len(bss.values)) - for _, fs := range bss.values { - slice = append(slice, fs) - } - return slice + return append(bss.concrete.asSlice(), bss.wildcard.asSlice()...) } // Clone returns a clone of this subject set. Note that this is a shallow clone. // NOTE: Should only be used when performance is not a concern. func (bss BaseSubjectSet[T]) Clone() BaseSubjectSet[T] { - return BaseSubjectSet[T]{maps.Clone(bss.values), bss.constructor, bss.combiner} + return BaseSubjectSet[T]{ + bss.concrete.clone(), + bss.wildcard.clone(), + } } // UnsafeRemoveExact removes the *exact* matching subject, with no wildcard handling. // This should ONLY be used for testing. func (bss BaseSubjectSet[T]) UnsafeRemoveExact(foundSubject T) { - delete(bss.values, foundSubject.GetSubjectId()) + if foundSubject.GetSubjectId() == tuple.PublicWildcard { + bss.wildcard.wildcard = nil + return + } + + delete(bss.concrete.subjects, foundSubject.GetSubjectId()) +} + +// concreteSubjectSet is a subject set which tracks concrete subjects only. +type concreteSubjectSet[T Subject[T]] struct { + constructor constructor[T] + + // subjects is the map from subject ID of each concrete subject in the set to its associated + // Subject. + subjects map[string]T +} + +func newConcreteSubjectSet[T Subject[T]](constructor constructor[T]) *concreteSubjectSet[T] { + if constructor == nil { + panic("given nil constructor") + } + + return &concreteSubjectSet[T]{ + subjects: map[string]T{}, + constructor: constructor, + } +} + +func (cst *concreteSubjectSet[T]) get(subjectID string) (T, bool) { + found, ok := cst.subjects[subjectID] + return found, ok +} + +func (cst *concreteSubjectSet[T]) isEmpty() bool { + return len(cst.subjects) == 0 +} + +func (cst *concreteSubjectSet[T]) asSlice() []T { + return maps.Values(cst.subjects) +} + +func (cst *concreteSubjectSet[T]) clone() *concreteSubjectSet[T] { + return &concreteSubjectSet[T]{ + constructor: cst.constructor, + subjects: maps.Clone(cst.subjects), + } +} + +func (cst *concreteSubjectSet[T]) unionWildcard(wildcard T) { + // Nothing to do, as a wildcard does not change concretes that are already present. +} + +func (cst *concreteSubjectSet[T]) unionConcrete(concrete T) { + // A union of a concrete subject needs to check if the existing concrete subject is present. + // Cases: + // - If not present => the new concrete subject is used as-is. + // - If already present => the conditionals of each concrete need to be merged. + existing, ok := cst.subjects[concrete.GetSubjectId()] + if !ok { + cst.subjects[concrete.GetSubjectId()] = concrete + return + } + + cst.subjects[concrete.GetSubjectId()] = cst.constructor( + concrete.GetSubjectId(), + shortcircuitedOr(existing.GetCaveatExpression(), concrete.GetCaveatExpression()), + nil, + concrete, existing) +} + +func (cst *concreteSubjectSet[T]) subtractWildcard(wildcard T) { + // Subtraction of a wildcard removes *all* elements of the concrete set, except those that + // are found in the excluded list. If the wildcard *itself* is conditional, then instead of + // items being removed, they are made conditional on the inversion of the wildcard's expression, + // and the exclusion's conditional, if any. + // + // Examples: + // {user:sarah, user:tom} - {*} => {} + // {user:sarah, user:tom} - {*[somecaveat]} => {user:sarah[!somecaveat], user:tom[!somecaveat]} + // {user:sarah, user:tom} - {* - {user:tom}} => {user:tom} + // {user:sarah, user:tom} - {*[somecaveat] - {user:tom}} => {user:sarah[!somecaveat], user:tom} + // {user:sarah, user:tom} - {* - {user:tom[c2]}}[somecaveat] => {user:sarah[!somecaveat], user:tom[c2]} + // {user:sarah[c1], user:tom} - {*[somecaveat] - {user:tom}} => {user:sarah[c1 && !somecaveat], user:tom} + exclusions := exclusionsMapFor(wildcard) + for existingSubjectID, existingSubject := range cst.subjects { + exclusion, isExcluded := exclusions[existingSubjectID] + if !isExcluded { + // If the subject was not excluded within the wildcard, it is either removed directly + // (in the case where the wildcard is not conditional), or has its condition updated to + // reflect that it is only present when the condition for the wildcard is *false*. + if wildcard.GetCaveatExpression() == nil { + delete(cst.subjects, existingSubjectID) + continue + } + + cst.subjects[existingSubjectID] = cst.constructor( + existingSubjectID, + caveatAnd(existingSubject.GetCaveatExpression(), caveatInvert(wildcard.GetCaveatExpression())), + nil, + existingSubject) + continue + } + + // If the exclusion is not conditional, then the subject is always present. + if exclusion.GetCaveatExpression() == nil { + continue + } + + // The conditional of the exclusion is that of the exclusion itself OR the caveatInverted case of + // the wildcard, which would mean the wildcard itself does not apply. + exclusionConditional := caveatOr(caveatInvert(wildcard.GetCaveatExpression()), exclusion.GetCaveatExpression()) + + // If the whole exclusion is not conditional, nothing more to do. + if exclusionConditional == nil { + continue + } + + // Otherwise, add the exclusion's conditional to the entry. + cst.subjects[existingSubjectID] = cst.constructor( + existingSubjectID, + caveatAnd(existingSubject.GetCaveatExpression(), exclusionConditional), + nil, + existingSubject) + } +} + +func (cst *concreteSubjectSet[T]) subtractConcrete(concrete T) { + existing, ok := cst.subjects[concrete.GetSubjectId()] + if !ok { + // If not present, nothing more to do. + return + } + + // Subtraction of a concrete type removes the entry from the concrete list + // *unless* the subtraction is conditional, in which case the conditional is updated + // to remove the element when it is true. + // + // Examples: + // {user:sarah} - {user:tom} => {user:sarah} + // {user:tom} - {user:tom} => {} + // {user:tom[c1]} - {user:tom} => {user:tom} + // {user:tom} - {user:tom[c2]} => {user:tom[!c2]} + // {user:tom[c1]} - {user:tom[c2]} => {user:tom[c1 && !c2]} + if concrete.GetCaveatExpression() == nil { + delete(cst.subjects, concrete.GetSubjectId()) + return + } + + // Otherwise, adjust the conditional of the existing item to remove it if it is true. + cst.subjects[concrete.GetSubjectId()] = cst.constructor( + concrete.GetSubjectId(), + caveatAnd(existing.GetCaveatExpression(), caveatInvert(concrete.GetCaveatExpression())), + nil, + concrete, existing) +} + +func (cst *concreteSubjectSet[T]) intersectDiffWithConcrete(other *concreteSubjectSet[T]) { + // Intersection of concrete subjects sets is a standard intersection operation, where elements + // must be in both sets, with a combination of the two elements into one for conditionals. + for currentSubjectID, existingSubject := range cst.subjects { + // Remove any entries not found in both subject sets, and combine their conditionals. + otherSubject, ok := other.subjects[currentSubjectID] + if !ok { + delete(cst.subjects, currentSubjectID) + continue + } + + // Otherwise, `and` together conditionals. + cst.subjects[currentSubjectID] = cst.constructor( + currentSubjectID, + caveatAnd(existingSubject.GetCaveatExpression(), otherSubject.GetCaveatExpression()), + nil, + existingSubject, + otherSubject) + } +} + +// wildcardSubjectTracker tracks the wildcard found in a subject set, if any. +type wildcardSubjectTracker[T Subject[T]] struct { + constructor func(subjectID string, conditionalExpression *v1.CaveatExpression, excludedSubjects []T, sources ...T) T + wildcard *T +} + +func newWildcardSubjectTracker[T Subject[T]](constructor constructor[T]) *wildcardSubjectTracker[T] { + if constructor == nil { + panic("given nil constructor") + } + + return &wildcardSubjectTracker[T]{constructor: constructor, wildcard: nil} +} + +func (wst *wildcardSubjectTracker[T]) get() (T, bool) { + if wst.wildcard != nil { + return *wst.wildcard, true + } + + return *new(T), false +} + +func (wst *wildcardSubjectTracker[T]) has() bool { + return wst.wildcard != nil +} + +func (wst *wildcardSubjectTracker[T]) asSlice() []T { + if wst.wildcard == nil { + return []T{} + } + + return []T{*wst.wildcard} +} + +func (wst *wildcardSubjectTracker[T]) clone() *wildcardSubjectTracker[T] { + return &wildcardSubjectTracker[T]{ + constructor: wst.constructor, + wildcard: wst.wildcard, + } +} + +func (wst *wildcardSubjectTracker[T]) unionWildcard(foundWildcard T) { + // If there is no existing wildcard, use the one that was found. + if wst.wildcard == nil { + wst.wildcard = &foundWildcard + return + } + + // Otherwise, union together the conditionals for the wildcards and *intersect* their exclusion + // sets. Exclusion sets are intersected because if an exclusion is missing from one wildcard + // but not the other, the missing element will be, by definition, in that other wildcard. + // + // Examples: + // {*} + {*} => {*} + // {* - {user:tom}} + {*} => {*} + // {* - {user:tom}} + {* - {user:sarah}} => {*} + // {* - {user:tom, user:sarah}} + {* - {user:sarah}} => {* - {user:sarah}} + // {*}[c1] + {*} => {*} + // {*}[c1] + {*}[c2] => {*}[c1 || c2] + existing := *wst.wildcard + + exisingConcreteExclusions := newConcreteSubjectSet[T](wst.constructor) + for _, excludedSubject := range existing.GetExcludedSubjects() { + exisingConcreteExclusions.unionConcrete(excludedSubject) + } + + foundConcreteExclusions := newConcreteSubjectSet[T](wst.constructor) + for _, excludedSubject := range foundWildcard.GetExcludedSubjects() { + foundConcreteExclusions.unionConcrete(excludedSubject) + } + + exisingConcreteExclusions.intersectDiffWithConcrete(foundConcreteExclusions) + + constructed := wst.constructor( + tuple.PublicWildcard, + shortcircuitedOr(existing.GetCaveatExpression(), foundWildcard.GetCaveatExpression()), + exisingConcreteExclusions.asSlice(), + existing, + foundWildcard) + wst.wildcard = &constructed +} + +func (wst *wildcardSubjectTracker[T]) unionConcrete(concrete T) { + if wst.wildcard == nil { + // Nothing to do if no wildcard present. + return + } + + // If the concrete is in the exclusion set, remove it if not conditional. Otherwise, mark + // it as conditional. + // + // Examples: + // {*} | {user:tom} => {*} (and user:tom in the concrete) + // {* - {user:tom}} | {user:tom} => {*} (and user:tom in the concrete) + // {* - {user:tom}[c1]} | {user:tom}[c2] => {* - {user:tom}[c1 && !c2]} (and user:tom in the concrete) + existing := *wst.wildcard + updatedExclusions := make([]T, 0, len(existing.GetExcludedSubjects())) + for _, existingExclusion := range existing.GetExcludedSubjects() { + if existingExclusion.GetSubjectId() == concrete.GetSubjectId() { + // If the conditional on the concrete is empty, then the concrete is always present, so + // we remove the exclusion entirely. + if concrete.GetCaveatExpression() == nil { + continue + } + + // Otherwise, the conditional expression for the new exclusion is the existing expression || + // the *inversion* of the concrete's expression, as the exclusion will only apply if the + // concrete subject is not present and the exclusion's expression is true. + exclusionConditionalExpression := caveatOr(caveatInvert(concrete.GetCaveatExpression()), existing.GetCaveatExpression()) + + updatedExclusions = append(updatedExclusions, wst.constructor( + concrete.GetSubjectId(), + exclusionConditionalExpression, + nil, + existingExclusion, + concrete), + ) + } else { + updatedExclusions = append(updatedExclusions, existingExclusion) + } + } + + constructed := wst.constructor( + tuple.PublicWildcard, + existing.GetCaveatExpression(), + updatedExclusions, + existing) + wst.wildcard = &constructed +} + +func (wst *wildcardSubjectTracker[T]) subtractWildcard(foundWildcard T) []T { + if wst.wildcard == nil { + // Nothing to do if no wildcard present. + return nil + } + + // If there is no condition on the wildcard and the new wildcard has no exclusions, then this wildcard goes away. + // Example: {*} - {*} => {} + if foundWildcard.GetCaveatExpression() == nil && len(foundWildcard.GetExcludedSubjects()) == 0 { + wst.wildcard = nil + return nil + } + + // Otherwise, we construct a new wildcard and return any concrete subjects that might result from this subtraction. + existing := *wst.wildcard + existingExclusions := wst.exclusionsMap() + + // Calculate the exclusions which turn into concrete subjects. + // This occurs when a wildcard with exclusions is subtracted from a wildcard + // (with, or without *matching* exclusions). + // + // Example: + // Given the two wildcards `* - {user:sarah}` and `* - {user:tom, user:amy, user:sarah}`, + // the resulting concrete subjects are {user:tom, user:amy} because the first set contains + // `tom` and `amy` (but not `sarah`) and the second set contains all three. + resultingConcreteSubjects := make([]T, 0, len(foundWildcard.GetExcludedSubjects())) + for _, excludedSubject := range foundWildcard.GetExcludedSubjects() { + if existingExclusion, isExistingExclusion := existingExclusions[excludedSubject.GetSubjectId()]; !isExistingExclusion || existingExclusion.GetCaveatExpression() != nil { + // The conditional expression for the now-concrete subject type is the conditional on the provided exclusion + // itself. + // + // As an example, subtracting the wildcards + // {*[caveat1] - {user:tom}} + // - + // {*[caveat3] - {user:sarah[caveat4]}} + // + // the resulting expression to produce a *concrete* `user:sarah` is + // `caveat1 && caveat3 && caveat4`, because the concrete subject only appears if the first + // wildcard applies, the *second* wildcard applies and its exclusion applies. + exclusionConditionalExpression := caveatAnd( + caveatAnd( + existing.GetCaveatExpression(), + foundWildcard.GetCaveatExpression(), + ), + excludedSubject.GetCaveatExpression(), + ) + + // If there is an existing exclusion, then its caveat expression is added as well, but inverted. + // + // As an example, subtracting the wildcards + // {*[caveat1] - {user:tom[caveat2]}} + // - + // {*[caveat3] - {user:sarah[caveat4]}} + // + // the resulting expression to produce a *concrete* `user:sarah` is + // `caveat1 && !caveat2 && caveat3 && caveat4`, because the concrete subject only appears + // if the first wildcard applies, the *second* wildcard applies, the first exclusion + // does *not* apply (ensuring the concrete is in the first wildcard) and the second exclusion + // *does* apply (ensuring it is not in the second wildcard). + if existingExclusion.GetCaveatExpression() != nil { + exclusionConditionalExpression = caveatAnd( + caveatAnd( + caveatAnd( + existing.GetCaveatExpression(), + foundWildcard.GetCaveatExpression(), + ), + caveatInvert(existingExclusion.GetCaveatExpression()), + ), + excludedSubject.GetCaveatExpression(), + ) + } + + resultingConcreteSubjects = append(resultingConcreteSubjects, wst.constructor( + excludedSubject.GetSubjectId(), + exclusionConditionalExpression, + nil, excludedSubject)) + } + } + + // Create the combined conditional: the wildcard can only exist when it is present and the other wildcard is not. + combinedConditionalExpression := caveatAnd(existing.GetCaveatExpression(), caveatInvert(foundWildcard.GetCaveatExpression())) + if combinedConditionalExpression != nil { + constructed := wst.constructor( + tuple.PublicWildcard, + combinedConditionalExpression, + existing.GetExcludedSubjects(), + existing, + foundWildcard) + wst.wildcard = &constructed + } else { + // If there is no combined conditional expression, then the wildcard is completely removed from + // the set. + wst.wildcard = nil + } + + return resultingConcreteSubjects +} + +func (wst *wildcardSubjectTracker[T]) subtractConcrete(concrete T) { + if wst.wildcard == nil { + // Nothing to do if no wildcard present. + return + } + + // Subtracting a concrete type from a wildcard adds the concrete to the exclusions for the wildcard. + // Examples: + // {*} - {user:tom} => {* - {user:tom}} + // {*} - {user:tom[c1]} => {* - {user:tom[c1]}} + // {* - {user:tom[c1]}} - {user:tom} => {* - {user:tom}} + // {* - {user:tom[c1]}} - {user:tom[c2]} => {* - {user:tom[c1 || c2]}} + existing := *wst.wildcard + updatedExclusions := make([]T, 0, len(existing.GetExcludedSubjects())+1) + wasFound := false + for _, existingExclusion := range existing.GetExcludedSubjects() { + if existingExclusion.GetSubjectId() == concrete.GetSubjectId() { + // The conditional expression for the exclusion is a combination on the existing exclusion or + // the new expression. The caveat is short circuited here because if either the exclusion or + // the concrete is non-caveated, then the whole exclusion is non-caveated. + exclusionConditionalExpression := shortcircuitedOr(existingExclusion.GetCaveatExpression(), concrete.GetCaveatExpression()) + + updatedExclusions = append(updatedExclusions, wst.constructor( + concrete.GetSubjectId(), + exclusionConditionalExpression, + nil, + existingExclusion, + concrete), + ) + wasFound = true + } else { + updatedExclusions = append(updatedExclusions, existingExclusion) + } + } + + if !wasFound { + updatedExclusions = append(updatedExclusions, concrete) + } + + constructed := wst.constructor( + tuple.PublicWildcard, + existing.GetCaveatExpression(), + updatedExclusions, + existing) + wst.wildcard = &constructed +} + +func (wst *wildcardSubjectTracker[T]) intersectDiffWithWildcard(other *wildcardSubjectTracker[T]) { + if wst.wildcard == nil { + // Nothing to do if no wildcard present. + return + } + + // If the other wildcard does not exist, change this wildcard to empty. + if other.wildcard == nil { + wst.wildcard = nil + return + } + + // If the other wildcard exists, then the intersection between the two wildcards is an && of + // their conditionals, and a *union* of their exclusions. + existing := *wst.wildcard + foundWildcard := *other.wildcard + + concreteExclusions := newConcreteSubjectSet[T](wst.constructor) + for _, excludedSubject := range existing.GetExcludedSubjects() { + concreteExclusions.unionConcrete(excludedSubject) + } + + for _, excludedSubject := range foundWildcard.GetExcludedSubjects() { + concreteExclusions.unionConcrete(excludedSubject) + } + + constructed := wst.constructor( + tuple.PublicWildcard, + caveatAnd(existing.GetCaveatExpression(), foundWildcard.GetCaveatExpression()), + concreteExclusions.asSlice(), + existing, + foundWildcard) + wst.wildcard = &constructed +} + +// exclusionsMap returns a map from subject ID to the associated exclusion, for each exclusion +// on this wildcard. +func (wst *wildcardSubjectTracker[T]) exclusionsMap() map[string]T { + if wst.wildcard == nil { + return nil + } + + return exclusionsMapFor(*wst.wildcard) +} + +// exclusionsMapFor creates a map of all the exclusions on a wildcard, by subject ID. +func exclusionsMapFor[T Subject[T]](wildcard T) map[string]T { + exclusions := make(map[string]T, len(wildcard.GetExcludedSubjects())) + for _, excludedSubject := range wildcard.GetExcludedSubjects() { + exclusions[excludedSubject.GetSubjectId()] = excludedSubject + } + return exclusions +} + +// shortcircuitedOr combines two caveat expressions via an `||`. If one of the expressions is nil, +// then the entire expression is *short circuited*, and a nil is returned. +func shortcircuitedOr(first *v1.CaveatExpression, second *v1.CaveatExpression) *v1.CaveatExpression { + if first == nil || second == nil { + return nil + } + + return &v1.CaveatExpression{ + OperationOrCaveat: &v1.CaveatExpression_Operation{ + Operation: &v1.CaveatOperation{ + Op: v1.CaveatOperation_OR, + Children: []*v1.CaveatExpression{first, second}, + }, + }, + } +} + +// caveatOr `||`'s together two caveat expressions. If one expression is nil, the other is returned. +func caveatOr(first *v1.CaveatExpression, second *v1.CaveatExpression) *v1.CaveatExpression { + if first == nil { + return second + } + + if second == nil { + return first + } + + return &v1.CaveatExpression{ + OperationOrCaveat: &v1.CaveatExpression_Operation{ + Operation: &v1.CaveatOperation{ + Op: v1.CaveatOperation_OR, + Children: []*v1.CaveatExpression{first, second}, + }, + }, + } +} + +// caveatAnd `&&`'s together two caveat expressions. If one expression is nil, the other is returned. +func caveatAnd(first *v1.CaveatExpression, second *v1.CaveatExpression) *v1.CaveatExpression { + if first == nil { + return second + } + + if second == nil { + return first + } + + return &v1.CaveatExpression{ + OperationOrCaveat: &v1.CaveatExpression_Operation{ + Operation: &v1.CaveatOperation{ + Op: v1.CaveatOperation_AND, + Children: []*v1.CaveatExpression{first, second}, + }, + }, + } +} + +// caveatInvert returns the caveat expression with a `!` placed in front of it. If the expression is +// nil, returns nil. +func caveatInvert(ce *v1.CaveatExpression) *v1.CaveatExpression { + if ce == nil { + return nil + } + + return &v1.CaveatExpression{ + OperationOrCaveat: &v1.CaveatExpression_Operation{ + Operation: &v1.CaveatOperation{ + Op: v1.CaveatOperation_NOT, + Children: []*v1.CaveatExpression{ce}, + }, + }, + } } diff --git a/internal/util/subjectset.go b/internal/util/subjectset.go index 922e23fc00..239fb51d46 100644 --- a/internal/util/subjectset.go +++ b/internal/util/subjectset.go @@ -15,16 +15,13 @@ type SubjectSet struct { // NewSubjectSet creates and returns a new subject set. func NewSubjectSet() SubjectSet { return SubjectSet{ - BaseSubjectSet: BaseSubjectSet[*v1.FoundSubject]{ - values: map[string]*v1.FoundSubject{}, - constructor: func(subjectID string, excludedSubjectIDs []string, sources ...*v1.FoundSubject) *v1.FoundSubject { - return &v1.FoundSubject{ - SubjectId: subjectID, - ExcludedSubjectIds: excludedSubjectIDs, - } - }, - combiner: nil, - }, + BaseSubjectSet: NewBaseSubjectSet[*v1.FoundSubject](func(subjectID string, caveatExpression *v1.CaveatExpression, excludedSubjects []*v1.FoundSubject, sources ...*v1.FoundSubject) *v1.FoundSubject { + return &v1.FoundSubject{ + SubjectId: subjectID, + CaveatExpression: caveatExpression, + ExcludedSubjects: excludedSubjects, + } + }), } } diff --git a/internal/util/subjectset_test.go b/internal/util/subjectset_test.go index 917eb364f4..d776fe002b 100644 --- a/internal/util/subjectset_test.go +++ b/internal/util/subjectset_test.go @@ -5,22 +5,61 @@ import ( "strings" "testing" + "google.golang.org/protobuf/proto" + "github.com/stretchr/testify/require" + core "github.com/authzed/spicedb/pkg/proto/core/v1" v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" "github.com/authzed/spicedb/pkg/tuple" ) +func caveat(name string) *core.ContextualizedCaveat { + return &core.ContextualizedCaveat{ + CaveatName: name, + } +} + +func caveatexpr(name string) *v1.CaveatExpression { + return &v1.CaveatExpression{ + OperationOrCaveat: &v1.CaveatExpression_Caveat{ + Caveat: caveat(name), + }, + } +} + func sub(subjectID string) *v1.FoundSubject { return &v1.FoundSubject{ SubjectId: subjectID, } } +func csub(subjectID string, expr *v1.CaveatExpression) *v1.FoundSubject { + return &v1.FoundSubject{ + SubjectId: subjectID, + CaveatExpression: expr, + } +} + +func cwc(expr *v1.CaveatExpression, exclusions ...*v1.FoundSubject) *v1.FoundSubject { + return &v1.FoundSubject{ + SubjectId: tuple.PublicWildcard, + ExcludedSubjects: exclusions, + CaveatExpression: expr, + } +} + func wc(exclusions ...string) *v1.FoundSubject { + excludedSubjects := make([]*v1.FoundSubject, 0, len(exclusions)) + for _, excludedID := range exclusions { + excludedSubjects = append(excludedSubjects, &v1.FoundSubject{ + SubjectId: excludedID, + }) + } + return &v1.FoundSubject{ - SubjectId: tuple.PublicWildcard, - ExcludedSubjectIds: exclusions, + SubjectId: tuple.PublicWildcard, + ExcludedSubjects: excludedSubjects, } } @@ -117,12 +156,82 @@ func TestSubjectSetAdd(t *testing.T) { []*v1.FoundSubject{wc()}, }, { - "add of two wildcards with same restrictions", + "add of two wildcards with same restrictions", []*v1.FoundSubject{wc("1")}, wc("1", "2"), false, []*v1.FoundSubject{wc("1")}, }, + + // Tests with caveats. + { + "basic add of caveated to non-caveated", + []*v1.FoundSubject{ + csub("foo", caveatexpr("testcaveat")), + sub("bar"), + }, + sub("foo"), + false, + []*v1.FoundSubject{sub("bar"), sub("foo")}, + }, + { + "basic add of non-caveated to caveated", + []*v1.FoundSubject{ + sub("foo"), + sub("bar"), + }, + csub("foo", caveatexpr("testcaveat")), + false, + []*v1.FoundSubject{sub("bar"), sub("foo")}, + }, + { + "basic add of caveated to caveated", + []*v1.FoundSubject{ + csub("foo", caveatexpr("testcaveat")), + sub("bar"), + }, + csub("foo", caveatexpr("anothercaveat")), + false, + []*v1.FoundSubject{ + sub("bar"), + csub("foo", caveatOr(caveatexpr("testcaveat"), caveatexpr("anothercaveat"))), + }, + }, + { + "add of caveated wildcard to non-caveated", + []*v1.FoundSubject{ + cwc(caveatexpr("testcaveat")), + sub("bar"), + }, + wc(), + false, + []*v1.FoundSubject{sub("bar"), wc()}, + }, + { + "add of caveated wildcard to caveated", + []*v1.FoundSubject{ + cwc(caveatexpr("testcaveat")), + sub("bar"), + }, + cwc(caveatexpr("anothercaveat")), + false, + []*v1.FoundSubject{ + sub("bar"), + cwc(caveatOr(caveatexpr("testcaveat"), caveatexpr("anothercaveat"))), + }, + }, + { + "add of wildcard to a caveated sub", + []*v1.FoundSubject{ + csub("bar", caveatexpr("testcaveat")), + }, + wc(), + true, + []*v1.FoundSubject{ + csub("bar", caveatexpr("testcaveat")), + wc(), + }, + }, } for _, tc := range tcs { @@ -143,7 +252,10 @@ func TestSubjectSetAdd(t *testing.T) { stableSortExclusions(expectedSet) stableSortExclusions(computedSet) - require.Equal(t, expectedSet, computedSet) + require.Equal(t, len(expectedSet), len(computedSet), "got: %v", computedSet) + for index := range expectedSet { + require.True(t, proto.Equal(expectedSet[index], computedSet[index]), "got mismatch: %v vs %v", expectedSet[index], computedSet[index]) + } }) } } @@ -189,7 +301,7 @@ func TestSubjectSetSubtract(t *testing.T) { "subtract from wildcard with existing exclusions", []*v1.FoundSubject{sub("bar"), wc("hiya")}, sub("bar"), - []*v1.FoundSubject{wc("bar", "hiya")}, + []*v1.FoundSubject{wc("hiya", "bar")}, }, { "subtract bare wildcard from set", @@ -227,6 +339,274 @@ func TestSubjectSetSubtract(t *testing.T) { wc("sarah", "tom"), []*v1.FoundSubject{sub("tom")}, }, + + // Tests with caveats. + { + "subtract non-caveated from caveated", + []*v1.FoundSubject{csub("foo", caveatexpr("somecaveat"))}, + sub("foo"), + []*v1.FoundSubject{}, + }, + { + "subtract caveated from non-caveated", + []*v1.FoundSubject{sub("foo")}, + csub("foo", caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // The subject should only appear now when the caveat is false. + csub("foo", caveatInvert(caveatexpr("somecaveat"))), + }, + }, + { + "subtract caveated from caveated", + []*v1.FoundSubject{ + csub("foo", caveatexpr("startingcaveat")), + }, + csub("foo", caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // The subject should only appear when its caveat is true, and the subtracted caveat + // is false. + csub("foo", caveatAnd(caveatexpr("startingcaveat"), caveatInvert(caveatexpr("somecaveat")))), + }, + }, + { + "subtract caveated bare wildcard from non-caveated", + []*v1.FoundSubject{sub("foo")}, + cwc(caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // The subject should only appear now when the caveat is false. + csub("foo", caveatInvert(caveatexpr("somecaveat"))), + }, + }, + { + "subtract bare wildcard from caveated", + []*v1.FoundSubject{csub("foo", caveatexpr("something"))}, + wc(), + []*v1.FoundSubject{}, + }, + { + "subtract caveated bare wildcard from caveated", + []*v1.FoundSubject{csub("foo", caveatexpr("something"))}, + cwc(caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // The subject should only appear now when its caveat is true and the wildcard's caveat + // is false. + csub("foo", caveatAnd(caveatexpr("something"), caveatInvert(caveatexpr("somecaveat")))), + }, + }, + { + "subtract wildcard with caveated exception from non-caveated", + []*v1.FoundSubject{sub("foo")}, + cwc(nil, csub("foo", caveatexpr("somecaveat"))), + []*v1.FoundSubject{ + // The subject should only appear now when somecaveat is true, making the exclusion present + // on the wildcard, and thus preventing the wildcard from removing the concrete subject. + csub("foo", caveatexpr("somecaveat")), + }, + }, + { + "subtract wildcard with caveated exception from caveated", + []*v1.FoundSubject{csub("foo", caveatexpr("somecaveat"))}, + cwc(nil, csub("foo", caveatexpr("anothercaveat"))), + []*v1.FoundSubject{ + // The subject should only appear now when both caveats are true. + csub("foo", caveatAnd(caveatexpr("somecaveat"), caveatexpr("anothercaveat"))), + }, + }, + { + "subtract caveated wildcard with caveated exception from caveated", + []*v1.FoundSubject{csub("foo", caveatexpr("somecaveat"))}, + cwc(caveatexpr("wildcardcaveat"), csub("foo", caveatexpr("anothercaveat"))), + []*v1.FoundSubject{ + // The subject should appear when: + // somecaveat && (!wildcardcaveat || anothercaveat) + csub("foo", caveatAnd(caveatexpr("somecaveat"), caveatOr(caveatInvert(caveatexpr("wildcardcaveat")), caveatexpr("anothercaveat")))), + }, + }, + { + "subtract caveated concrete from non-caveated wildcard", + []*v1.FoundSubject{wc()}, + csub("foo", caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // The subject should be a caveated exception on the wildcard. + cwc(nil, csub("foo", caveatexpr("somecaveat"))), + }, + }, + { + "subtract caveated concrete from caveated wildcard", + []*v1.FoundSubject{cwc(caveatexpr("wildcardcaveat"))}, + csub("foo", caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // The subject should be a caveated exception on the wildcard. + cwc(caveatexpr("wildcardcaveat"), csub("foo", caveatexpr("somecaveat"))), + }, + }, + { + "subtract caveated concrete from wildcard with exclusion", + []*v1.FoundSubject{ + wc("foo"), + }, + csub("foo", caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // No change, as `foo` is always removed. + wc("foo"), + }, + }, + { + "subtract caveated concrete from caveated wildcard with exclusion", + []*v1.FoundSubject{ + cwc(caveatexpr("wildcardcaveat"), sub("foo")), + }, + csub("foo", caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // No change, as `foo` is always removed. + cwc(caveatexpr("wildcardcaveat"), sub("foo")), + }, + }, + { + "subtract caveated concrete from caveated wildcard with caveated exclusion", + []*v1.FoundSubject{ + cwc(caveatexpr("wildcardcaveat"), csub("foo", caveatexpr("exclusioncaveat"))), + }, + csub("foo", caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // The subject should be excluded now if *either* caveat is true. + cwc(caveatexpr("wildcardcaveat"), csub("foo", caveatOr(caveatexpr("exclusioncaveat"), caveatexpr("somecaveat")))), + }, + }, + { + "subtract non-caveated bare wildcard from caveated wildcard", + []*v1.FoundSubject{ + cwc(caveatexpr("wildcardcaveat")), + }, + wc(), + []*v1.FoundSubject{}, + }, + { + "subtract caveated bare wildcard from non-caveated wildcard", + []*v1.FoundSubject{ + wc(), + }, + cwc(caveatexpr("wildcardcaveat")), + []*v1.FoundSubject{ + // The new wildcard is caveated on the inversion of the caveat. + cwc(caveatInvert(caveatexpr("wildcardcaveat"))), + }, + }, + { + "subtract non-caveated bare wildcard from caveated wildcard with exclusions", + []*v1.FoundSubject{ + cwc(caveatexpr("wildcardcaveat"), sub("foo")), + }, + wc(), + []*v1.FoundSubject{}, + }, + { + "subtract caveated bare wildcard from non-caveated wildcard with exclusions", + []*v1.FoundSubject{ + wc("foo"), + }, + cwc(caveatexpr("wildcardcaveat")), + []*v1.FoundSubject{ + // The new wildcard is caveated on the inversion of the caveat. + cwc(caveatInvert(caveatexpr("wildcardcaveat")), sub("foo")), + }, + }, + { + "subtract caveated wildcard with caveated exclusion from caveated wildcard with caveated exclusion", + []*v1.FoundSubject{ + cwc(caveatexpr("wildcardcaveat1"), csub("foo", caveatexpr("exclusion1"))), + }, + cwc(caveatexpr("wildcardcaveat2"), csub("foo", caveatexpr("exclusion2"))), + []*v1.FoundSubject{ + cwc( + // The wildcard itself only appears if caveat1 is true and caveat2 is *false*. + caveatAnd(caveatexpr("wildcardcaveat1"), caveatInvert(caveatexpr("wildcardcaveat2"))), + + // The exclusion still only applies if exclusion1 is true, because if it is false, + // it doesn't matter that the subject was excluded from the subtraction. + csub("foo", caveatexpr("exclusion1")), + ), + + // A concrete of foo is produced when all of these are true: + // 1) the first wildcard is present + // 2) the second wildcard is present + // 3) the exclusion on the first wildcard is false, meaning the concrete is present within + // the wildcard + // 4) the exclusion within the second is true, meaning that the concrete was not present + // + // thus causing the expression to become `{*} - {* - {foo}}`, and therefore producing + // `foo` as a concrete. + csub("foo", + caveatAnd( + caveatAnd( + caveatAnd( + caveatexpr("wildcardcaveat1"), + caveatexpr("wildcardcaveat2"), + ), + caveatInvert(caveatexpr("exclusion1")), + ), + caveatexpr("exclusion2"), + ), + ), + }, + }, + { + "subtract caveated wildcard with caveated exclusion from caveated wildcard with different caveated exclusion", + []*v1.FoundSubject{ + cwc(caveatexpr("wildcardcaveat1"), csub("foo", caveatexpr("exclusion1"))), + }, + cwc(caveatexpr("wildcardcaveat2"), csub("bar", caveatexpr("exclusion2"))), + []*v1.FoundSubject{ + cwc( + // The wildcard itself only appears if caveat1 is true and caveat2 is *false*. + caveatAnd(caveatexpr("wildcardcaveat1"), caveatInvert(caveatexpr("wildcardcaveat2"))), + + // The foo exclusion remains the same. + csub("foo", caveatexpr("exclusion1")), + ), + + // Because `bar` is excluded in the *subtraction*, it can appear as a *concrete* subject + // in the scenario where the first wildcard is applied, the second wildcard is applied + // and its own exclusion is true. + // Therefore, bar must be *concretely* in the set if: + // wildcard1 && wildcard2 && exclusion2 + csub("bar", + caveatAnd( + caveatAnd( + caveatexpr("wildcardcaveat1"), + caveatexpr("wildcardcaveat2"), + ), + caveatexpr("exclusion2"), + ), + ), + }, + }, + { + "concretes with wildcard with partially matching exclusions subtracted", + []*v1.FoundSubject{ + sub("foo"), + sub("bar"), + }, + cwc(caveatexpr("caveat"), sub("foo")), + []*v1.FoundSubject{ + sub("foo"), + csub("bar", caveatInvert(caveatexpr("caveat"))), + }, + }, + { + "subtract caveated from caveated (same caveat)", + []*v1.FoundSubject{ + csub("foo", caveatexpr("somecaveat")), + }, + csub("foo", caveatexpr("somecaveat")), + []*v1.FoundSubject{ + // Since no expression simplification is occurring, subtracting a caveated concrete + // subject from another with the *same* caveat, results in an expression that &&'s + // together the expression and its inversion. In practice, this will simplify down to + // nothing, but that happens at a different layer. + csub("foo", caveatAnd(caveatexpr("somecaveat"), caveatInvert(caveatexpr("somecaveat")))), + }, + }, } for _, tc := range tcs { @@ -247,89 +627,229 @@ func TestSubjectSetSubtract(t *testing.T) { stableSortExclusions(expectedSet) stableSortExclusions(computedSet) - require.Equal(t, expectedSet, computedSet) + require.Equal(t, len(expectedSet), len(computedSet), "got: %v", computedSet) + for index := range expectedSet { + require.True(t, proto.Equal(expectedSet[index], computedSet[index]), "got mismatch: expected `%v`, got `%v`", expectedSet[index], computedSet[index]) + } }) } } func TestSubjectSetIntersection(t *testing.T) { tcs := []struct { - name string - existing []*v1.FoundSubject - toIntersect []*v1.FoundSubject - expectedSet []*v1.FoundSubject + name string + existing []*v1.FoundSubject + toIntersect []*v1.FoundSubject + expectedSet []*v1.FoundSubject + expectedInvertedSet []*v1.FoundSubject }{ { "basic intersection, full overlap", []*v1.FoundSubject{sub("foo"), sub("bar")}, []*v1.FoundSubject{sub("foo"), sub("bar")}, []*v1.FoundSubject{sub("foo"), sub("bar")}, + nil, }, { "basic intersection, partial overlap", []*v1.FoundSubject{sub("foo"), sub("bar")}, []*v1.FoundSubject{sub("foo")}, []*v1.FoundSubject{sub("foo")}, + nil, }, { "basic intersection, no overlap", []*v1.FoundSubject{sub("foo"), sub("bar")}, []*v1.FoundSubject{sub("baz")}, []*v1.FoundSubject{}, + nil, }, { "intersection between bare wildcard and concrete", []*v1.FoundSubject{sub("foo")}, []*v1.FoundSubject{wc()}, []*v1.FoundSubject{sub("foo")}, + nil, }, { "intersection between wildcard with exclusions and concrete", []*v1.FoundSubject{sub("foo")}, []*v1.FoundSubject{wc("tom")}, []*v1.FoundSubject{sub("foo")}, + nil, }, { "intersection between wildcard with matching exclusions and concrete", []*v1.FoundSubject{sub("foo")}, []*v1.FoundSubject{wc("foo")}, []*v1.FoundSubject{}, + nil, }, { "intersection between bare wildcards", []*v1.FoundSubject{wc()}, []*v1.FoundSubject{wc()}, []*v1.FoundSubject{wc()}, + nil, }, { "intersection between bare wildcard and one with exclusions", []*v1.FoundSubject{wc()}, []*v1.FoundSubject{wc("1", "2")}, []*v1.FoundSubject{wc("1", "2")}, + nil, }, { "intersection between wildcards", []*v1.FoundSubject{wc("2", "3")}, []*v1.FoundSubject{wc("1", "2")}, []*v1.FoundSubject{wc("1", "2", "3")}, + nil, }, { "intersection wildcard with exclusions and concrete", []*v1.FoundSubject{wc("2", "3")}, []*v1.FoundSubject{sub("4")}, []*v1.FoundSubject{sub("4")}, + nil, }, { "intersection wildcard with matching exclusions and concrete", []*v1.FoundSubject{wc("2", "3", "4")}, []*v1.FoundSubject{sub("4")}, []*v1.FoundSubject{}, + nil, }, { "intersection of wildcards and two concrete types", []*v1.FoundSubject{wc(), sub("1")}, []*v1.FoundSubject{wc(), sub("2")}, []*v1.FoundSubject{wc(), sub("1"), sub("2")}, + nil, + }, + + // Tests with caveats. + { + "intersection of non-caveated concrete and caveated concrete", + []*v1.FoundSubject{sub("1")}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat"))}, + + // Resulting subject is caveated on the caveat. + []*v1.FoundSubject{csub("1", caveatexpr("caveat"))}, + nil, + }, + { + "intersection of caveated concrete and non-caveated concrete", + []*v1.FoundSubject{csub("1", caveatexpr("caveat"))}, + []*v1.FoundSubject{sub("1")}, + + // Resulting subject is caveated on the caveat. + []*v1.FoundSubject{csub("1", caveatexpr("caveat"))}, + nil, + }, + { + "intersection of caveated concrete and caveated concrete", + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat2"))}, + + // Resulting subject is caveated both caveats. + []*v1.FoundSubject{csub("1", caveatAnd(caveatexpr("caveat1"), caveatexpr("caveat2")))}, + + // Inverted result has the caveates reversed in the and expression. + []*v1.FoundSubject{csub("1", caveatAnd(caveatexpr("caveat2"), caveatexpr("caveat1")))}, + }, + { + "intersection of caveated concrete and non-caveated bare wildcard", + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + []*v1.FoundSubject{wc()}, + + // Resulting subject is caveated and concrete. + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + nil, + }, + { + "intersection of non-caveated bare wildcard and caveated concrete", + []*v1.FoundSubject{wc()}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + + // Resulting subject is caveated and concrete. + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + nil, + }, + { + "intersection of caveated concrete and caveated bare wildcard", + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + []*v1.FoundSubject{cwc(caveatexpr("caveat2"))}, + + // Resulting subject is caveated from both and concrete. + []*v1.FoundSubject{csub("1", caveatAnd(caveatexpr("caveat1"), caveatexpr("caveat2")))}, + nil, + }, + { + "intersection of caveated bare wildcard and caveated concrete", + []*v1.FoundSubject{cwc(caveatexpr("caveat2"))}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + + // Resulting subject is caveated from both and concrete. + []*v1.FoundSubject{csub("1", caveatAnd(caveatexpr("caveat1"), caveatexpr("caveat2")))}, + nil, + }, + { + "intersection of caveated concrete and non-caveated wildcard with non-matching exclusion", + []*v1.FoundSubject{wc("2")}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + + // Resulting subject is caveated and concrete. + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + nil, + }, + { + "intersection of caveated concrete and non-caveated wildcard with matching exclusion", + []*v1.FoundSubject{wc("1")}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + + // Empty since the subject was in the exclusion set. + []*v1.FoundSubject{}, + nil, + }, + { + "intersection of caveated concrete and caveated wildcard with non-matching exclusion", + []*v1.FoundSubject{cwc(caveatexpr("wcaveat"), sub("2"))}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + + // Since the wildcard is caveated and has a non-matching exclusion, the caveat is added to + // the subject. + []*v1.FoundSubject{csub("1", caveatAnd(caveatexpr("caveat1"), caveatexpr("wcaveat")))}, + nil, + }, + { + "intersection of caveated concrete and caveated wildcard with matching exclusion", + []*v1.FoundSubject{cwc(caveatexpr("wcaveat"), sub("1"))}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + + // Since the wildcard is caveated but has a matching exclusion, the result is empty. + []*v1.FoundSubject{}, + nil, + }, + { + "intersection of caveated concrete and caveated wildcard with matching caveated exclusion", + []*v1.FoundSubject{cwc(caveatexpr("wcaveat"), csub("1", caveatexpr("ecaveat")))}, + []*v1.FoundSubject{csub("1", caveatexpr("caveat1"))}, + + []*v1.FoundSubject{ + // The concrete is included if its own caveat is true, the wildcard's caveat is true + // and the exclusion's caveat is false. + csub("1", + caveatAnd( + caveatexpr("caveat1"), + caveatAnd( + caveatexpr("wcaveat"), + caveatInvert(caveatexpr("ecaveat")), + ), + ), + ), + }, + nil, }, } @@ -356,7 +876,45 @@ func TestSubjectSetIntersection(t *testing.T) { stableSortExclusions(expectedSet) stableSortExclusions(computedSet) - require.Equal(t, expectedSet, computedSet) + require.Equal(t, len(expectedSet), len(computedSet), "got: %v", computedSet) + for index := range expectedSet { + require.True(t, proto.Equal(expectedSet[index], computedSet[index]), "got mismatch: expected %v vs found %v", expectedSet[index], computedSet[index]) + } + + // Run the intersection inverted, which should always result in the same results. + t.Run("inverted", func(t *testing.T) { + existingSet := NewSubjectSet() + for _, existing := range tc.existing { + require.True(t, existingSet.Add(existing)) + } + + toIntersect := NewSubjectSet() + for _, toAdd := range tc.toIntersect { + require.True(t, toIntersect.Add(toAdd)) + } + + toIntersect.IntersectionDifference(existingSet) + + // The inverted set is necessary for some caveated sets, because their expressions may + // have references in reversed locations. + expectedSet := tc.expectedSet + if tc.expectedInvertedSet != nil { + expectedSet = tc.expectedInvertedSet + } + + computedSet := toIntersect.AsSlice() + + sort.Sort(sortByID(expectedSet)) + sort.Sort(sortByID(computedSet)) + + stableSortExclusions(expectedSet) + stableSortExclusions(computedSet) + + require.Equal(t, len(expectedSet), len(computedSet), "got: %v", computedSet) + for index := range expectedSet { + require.True(t, proto.Equal(expectedSet[index], computedSet[index]), "got inverted mismatch: expected %v vs found %v", expectedSet[index], computedSet[index]) + } + }) }) } } @@ -369,6 +927,6 @@ func (a sortByID) Less(i, j int) bool { return strings.Compare(a[i].SubjectId, a func stableSortExclusions(fss []*v1.FoundSubject) { for _, fs := range fss { - sort.Strings(fs.ExcludedSubjectIds) + sort.Sort(sortByID(fs.ExcludedSubjects)) } } diff --git a/pkg/proto/dispatch/v1/dispatch.pb.go b/pkg/proto/dispatch/v1/dispatch.pb.go index 348ab2038f..011cb29a89 100644 --- a/pkg/proto/dispatch/v1/dispatch.pb.go +++ b/pkg/proto/dispatch/v1/dispatch.pb.go @@ -1230,8 +1230,9 @@ type FoundSubject struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - SubjectId string `protobuf:"bytes,1,opt,name=subject_id,json=subjectId,proto3" json:"subject_id,omitempty"` - ExcludedSubjectIds []string `protobuf:"bytes,2,rep,name=excluded_subject_ids,json=excludedSubjectIds,proto3" json:"excluded_subject_ids,omitempty"` + SubjectId string `protobuf:"bytes,1,opt,name=subject_id,json=subjectId,proto3" json:"subject_id,omitempty"` + CaveatExpression *CaveatExpression `protobuf:"bytes,2,opt,name=caveat_expression,json=caveatExpression,proto3" json:"caveat_expression,omitempty"` + ExcludedSubjects []*FoundSubject `protobuf:"bytes,3,rep,name=excluded_subjects,json=excludedSubjects,proto3" json:"excluded_subjects,omitempty"` } func (x *FoundSubject) Reset() { @@ -1273,9 +1274,16 @@ func (x *FoundSubject) GetSubjectId() string { return "" } -func (x *FoundSubject) GetExcludedSubjectIds() []string { +func (x *FoundSubject) GetCaveatExpression() *CaveatExpression { if x != nil { - return x.ExcludedSubjectIds + return x.CaveatExpression + } + return nil +} + +func (x *FoundSubject) GetExcludedSubjects() []*FoundSubject { + if x != nil { + return x.ExcludedSubjects } return nil } @@ -1814,126 +1822,132 @@ var file_dispatch_v1_dispatch_proto_rawDesc = []byte{ 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, 0x08, 0xfa, 0x42, 0x05, 0x8a, 0x01, 0x02, 0x10, 0x01, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x22, 0x5f, 0x0a, 0x0c, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, - 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, - 0x30, 0x0a, 0x14, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x75, 0x62, 0x6a, - 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x65, - 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, - 0x73, 0x22, 0x99, 0x01, 0x0a, 0x1e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, - 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0e, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x73, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x64, - 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x75, 0x6e, 0x64, - 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0d, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x75, - 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, - 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, - 0x65, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x7d, 0x0a, - 0x0c, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x3b, 0x0a, - 0x0b, 0x61, 0x74, 0x5f, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x42, 0x1a, 0xfa, 0x42, 0x17, 0x72, 0x15, 0x32, 0x13, 0x5e, 0x5b, 0x30, 0x2d, 0x39, - 0x5d, 0x2b, 0x28, 0x5c, 0x2e, 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x29, 0x3f, 0x24, 0x52, 0x0a, - 0x61, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, 0x0f, 0x64, 0x65, - 0x70, 0x74, 0x68, 0x5f, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0d, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x2a, 0x02, 0x20, 0x00, 0x52, 0x0e, 0x64, 0x65, - 0x70, 0x74, 0x68, 0x52, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x22, 0xda, 0x01, 0x0a, - 0x0c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x12, 0x25, 0x0a, - 0x0e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, - 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x70, 0x74, 0x68, 0x5f, 0x72, 0x65, - 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x64, 0x65, - 0x70, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x32, 0x0a, 0x15, 0x63, - 0x61, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x63, 0x61, 0x63, 0x68, - 0x65, 0x64, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x3c, 0x0a, 0x0a, 0x64, 0x65, 0x62, 0x75, 0x67, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x09, 0x64, 0x65, 0x62, 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x4a, 0x04, 0x08, - 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0x46, 0x0a, 0x10, 0x44, 0x65, 0x62, - 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, - 0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x64, - 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x72, 0x61, 0x63, 0x65, 0x52, 0x05, 0x63, 0x68, 0x65, 0x63, - 0x6b, 0x22, 0xf8, 0x03, 0x0a, 0x0f, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x65, 0x62, 0x75, 0x67, - 0x54, 0x72, 0x61, 0x63, 0x65, 0x12, 0x3b, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, - 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, - 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x12, 0x5f, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, - 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x72, 0x61, 0x63, 0x65, - 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x14, 0x72, - 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x43, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x72, 0x61, - 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x69, 0x73, 0x5f, 0x63, - 0x61, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0e, 0x69, 0x73, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x52, 0x65, 0x73, 0x75, - 0x6c, 0x74, 0x12, 0x3f, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x5f, 0x70, 0x72, 0x6f, 0x62, 0x6c, 0x65, - 0x6d, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, - 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x65, 0x62, 0x75, - 0x67, 0x54, 0x72, 0x61, 0x63, 0x65, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x50, 0x72, 0x6f, 0x62, 0x6c, - 0x65, 0x6d, 0x73, 0x1a, 0x5c, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x36, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, - 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, 0x65, 0x63, 0x6b, - 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x39, 0x0a, 0x0c, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, - 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0c, - 0x0a, 0x08, 0x52, 0x45, 0x4c, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, - 0x50, 0x45, 0x52, 0x4d, 0x49, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x32, 0xa0, 0x04, 0x0a, - 0x0f, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x58, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, - 0x6b, 0x12, 0x21, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, - 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, + 0x22, 0xc1, 0x01, 0x0a, 0x0c, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x4a, 0x0a, 0x11, 0x63, 0x61, 0x76, 0x65, 0x61, 0x74, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x64, 0x69, + 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x61, 0x76, 0x65, 0x61, 0x74, + 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x10, 0x63, 0x61, 0x76, 0x65, + 0x61, 0x74, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x46, 0x0a, 0x11, + 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, + 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, 0x75, 0x6e, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x52, 0x10, 0x65, 0x78, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x53, 0x75, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x73, 0x22, 0x99, 0x01, 0x0a, 0x1e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, + 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x0e, 0x66, 0x6f, 0x75, 0x6e, 0x64, + 0x5f, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x19, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x6f, + 0x75, 0x6e, 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x0d, 0x66, 0x6f, 0x75, 0x6e, + 0x64, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x64, 0x69, + 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x22, 0x7d, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x61, + 0x12, 0x3b, 0x0a, 0x0b, 0x61, 0x74, 0x5f, 0x72, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1a, 0xfa, 0x42, 0x17, 0x72, 0x15, 0x32, 0x13, 0x5e, 0x5b, + 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x28, 0x5c, 0x2e, 0x5b, 0x30, 0x2d, 0x39, 0x5d, 0x2b, 0x29, 0x3f, + 0x24, 0x52, 0x0a, 0x61, 0x74, 0x52, 0x65, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x30, 0x0a, + 0x0f, 0x64, 0x65, 0x70, 0x74, 0x68, 0x5f, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x42, 0x07, 0xfa, 0x42, 0x04, 0x2a, 0x02, 0x20, 0x00, 0x52, + 0x0e, 0x64, 0x65, 0x70, 0x74, 0x68, 0x52, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x22, + 0xda, 0x01, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x4d, 0x65, 0x74, 0x61, + 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, + 0x63, 0x68, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x65, 0x70, 0x74, 0x68, + 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, + 0x0d, 0x64, 0x65, 0x70, 0x74, 0x68, 0x52, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x32, + 0x0a, 0x15, 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, + 0x68, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x13, 0x63, + 0x61, 0x63, 0x68, 0x65, 0x64, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x0a, 0x64, 0x65, 0x62, 0x75, 0x67, 0x5f, 0x69, 0x6e, 0x66, 0x6f, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, + 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x62, 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x64, 0x65, 0x62, 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, + 0x4a, 0x04, 0x08, 0x04, 0x10, 0x05, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x22, 0x46, 0x0a, 0x10, + 0x44, 0x65, 0x62, 0x75, 0x67, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x32, 0x0a, 0x05, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x72, 0x61, 0x63, 0x65, 0x52, 0x05, 0x63, + 0x68, 0x65, 0x63, 0x6b, 0x22, 0xf8, 0x03, 0x0a, 0x0f, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x65, + 0x62, 0x75, 0x67, 0x54, 0x72, 0x61, 0x63, 0x65, 0x12, 0x3b, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x64, 0x69, 0x73, 0x70, + 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x07, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5f, 0x0a, 0x16, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x65, 0x62, 0x75, 0x67, 0x54, 0x72, + 0x61, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x14, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x43, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, + 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, 0x65, 0x62, 0x75, 0x67, + 0x54, 0x72, 0x61, 0x63, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x69, + 0x73, 0x5f, 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x69, 0x73, 0x43, 0x61, 0x63, 0x68, 0x65, 0x64, 0x52, + 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12, 0x3f, 0x0a, 0x0c, 0x73, 0x75, 0x62, 0x5f, 0x70, 0x72, 0x6f, + 0x62, 0x6c, 0x65, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x64, 0x69, + 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x44, + 0x65, 0x62, 0x75, 0x67, 0x54, 0x72, 0x61, 0x63, 0x65, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x50, 0x72, + 0x6f, 0x62, 0x6c, 0x65, 0x6d, 0x73, 0x1a, 0x5c, 0x0a, 0x0c, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x36, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, + 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x22, 0x39, 0x0a, 0x0c, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, + 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, 0x4c, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x01, 0x12, + 0x0e, 0x0a, 0x0a, 0x50, 0x45, 0x52, 0x4d, 0x49, 0x53, 0x53, 0x49, 0x4f, 0x4e, 0x10, 0x02, 0x32, + 0xa0, 0x04, 0x0a, 0x0f, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x58, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x12, 0x21, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, + 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x43, 0x68, + 0x65, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, + 0x0e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x12, + 0x22, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, + 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, + 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x0e, 0x44, 0x69, - 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x12, 0x22, 0x2e, 0x64, + 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x12, 0x22, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, - 0x74, 0x63, 0x68, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, - 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x70, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x5b, 0x0a, 0x0e, 0x44, 0x69, 0x73, 0x70, 0x61, - 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x12, 0x22, 0x2e, 0x64, 0x69, 0x73, 0x70, - 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, - 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, - 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, - 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, 0x1a, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, - 0x68, 0x52, 0x65, 0x61, 0x63, 0x68, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x73, 0x12, 0x2e, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x63, 0x68, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, - 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x63, 0x68, 0x61, - 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x75, 0x0a, 0x16, 0x44, 0x69, 0x73, 0x70, - 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x12, 0x2a, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, - 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, - 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, - 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, - 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, 0x6a, 0x65, - 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x42, - 0xaa, 0x01, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, - 0x2e, 0x76, 0x31, 0x42, 0x0d, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x65, 0x64, 0x2f, 0x73, 0x70, 0x69, 0x63, 0x65, 0x64, 0x62, - 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x64, 0x69, 0x73, 0x70, 0x61, - 0x74, 0x63, 0x68, 0x2f, 0x76, 0x31, 0x3b, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x76, - 0x31, 0xa2, 0x02, 0x03, 0x44, 0x58, 0x58, 0xaa, 0x02, 0x0b, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, - 0x63, 0x68, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0b, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, - 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x17, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x5c, 0x56, - 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x0c, - 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x81, 0x01, 0x0a, 0x1a, 0x44, 0x69, 0x73, 0x70, + 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, 0x63, 0x68, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x2e, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, + 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, + 0x63, 0x68, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, + 0x68, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x52, 0x65, 0x61, + 0x63, 0x68, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x75, 0x0a, 0x16, 0x44, + 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x75, 0x62, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x12, 0x2a, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, + 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, + 0x75, 0x70, 0x53, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2b, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x2e, + 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x53, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x30, 0x01, 0x42, 0xaa, 0x01, 0x0a, 0x0f, 0x63, 0x6f, 0x6d, 0x2e, 0x64, 0x69, 0x73, 0x70, 0x61, + 0x74, 0x63, 0x68, 0x2e, 0x76, 0x31, 0x42, 0x0d, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x7a, 0x65, 0x64, 0x2f, 0x73, 0x70, 0x69, 0x63, + 0x65, 0x64, 0x62, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x64, 0x69, + 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x2f, 0x76, 0x31, 0x3b, 0x64, 0x69, 0x73, 0x70, 0x61, 0x74, + 0x63, 0x68, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x44, 0x58, 0x58, 0xaa, 0x02, 0x0b, 0x44, 0x69, 0x73, + 0x70, 0x61, 0x74, 0x63, 0x68, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x0b, 0x44, 0x69, 0x73, 0x70, 0x61, + 0x74, 0x63, 0x68, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x17, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, + 0x68, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0xea, 0x02, 0x0c, 0x44, 0x69, 0x73, 0x70, 0x61, 0x74, 0x63, 0x68, 0x3a, 0x3a, 0x56, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2019,31 +2033,33 @@ var file_dispatch_v1_dispatch_proto_depIdxs = []int32{ 22, // 31: dispatch.v1.DispatchLookupSubjectsRequest.metadata:type_name -> dispatch.v1.ResolverMeta 28, // 32: dispatch.v1.DispatchLookupSubjectsRequest.resource_relation:type_name -> core.v1.RelationReference 28, // 33: dispatch.v1.DispatchLookupSubjectsRequest.subject_relation:type_name -> core.v1.RelationReference - 20, // 34: dispatch.v1.DispatchLookupSubjectsResponse.found_subjects:type_name -> dispatch.v1.FoundSubject - 23, // 35: dispatch.v1.DispatchLookupSubjectsResponse.metadata:type_name -> dispatch.v1.ResponseMeta - 24, // 36: dispatch.v1.ResponseMeta.debug_info:type_name -> dispatch.v1.DebugInformation - 25, // 37: dispatch.v1.DebugInformation.check:type_name -> dispatch.v1.CheckDebugTrace - 7, // 38: dispatch.v1.CheckDebugTrace.request:type_name -> dispatch.v1.DispatchCheckRequest - 6, // 39: dispatch.v1.CheckDebugTrace.resource_relation_type:type_name -> dispatch.v1.CheckDebugTrace.RelationType - 27, // 40: dispatch.v1.CheckDebugTrace.results:type_name -> dispatch.v1.CheckDebugTrace.ResultsEntry - 25, // 41: dispatch.v1.CheckDebugTrace.sub_problems:type_name -> dispatch.v1.CheckDebugTrace - 9, // 42: dispatch.v1.DispatchCheckResponse.ResultsByResourceIdEntry.value:type_name -> dispatch.v1.ResourceCheckResult - 9, // 43: dispatch.v1.CheckDebugTrace.ResultsEntry.value:type_name -> dispatch.v1.ResourceCheckResult - 7, // 44: dispatch.v1.DispatchService.DispatchCheck:input_type -> dispatch.v1.DispatchCheckRequest - 12, // 45: dispatch.v1.DispatchService.DispatchExpand:input_type -> dispatch.v1.DispatchExpandRequest - 14, // 46: dispatch.v1.DispatchService.DispatchLookup:input_type -> dispatch.v1.DispatchLookupRequest - 16, // 47: dispatch.v1.DispatchService.DispatchReachableResources:input_type -> dispatch.v1.DispatchReachableResourcesRequest - 19, // 48: dispatch.v1.DispatchService.DispatchLookupSubjects:input_type -> dispatch.v1.DispatchLookupSubjectsRequest - 8, // 49: dispatch.v1.DispatchService.DispatchCheck:output_type -> dispatch.v1.DispatchCheckResponse - 13, // 50: dispatch.v1.DispatchService.DispatchExpand:output_type -> dispatch.v1.DispatchExpandResponse - 15, // 51: dispatch.v1.DispatchService.DispatchLookup:output_type -> dispatch.v1.DispatchLookupResponse - 18, // 52: dispatch.v1.DispatchService.DispatchReachableResources:output_type -> dispatch.v1.DispatchReachableResourcesResponse - 21, // 53: dispatch.v1.DispatchService.DispatchLookupSubjects:output_type -> dispatch.v1.DispatchLookupSubjectsResponse - 49, // [49:54] is the sub-list for method output_type - 44, // [44:49] is the sub-list for method input_type - 44, // [44:44] is the sub-list for extension type_name - 44, // [44:44] is the sub-list for extension extendee - 0, // [0:44] is the sub-list for field type_name + 10, // 34: dispatch.v1.FoundSubject.caveat_expression:type_name -> dispatch.v1.CaveatExpression + 20, // 35: dispatch.v1.FoundSubject.excluded_subjects:type_name -> dispatch.v1.FoundSubject + 20, // 36: dispatch.v1.DispatchLookupSubjectsResponse.found_subjects:type_name -> dispatch.v1.FoundSubject + 23, // 37: dispatch.v1.DispatchLookupSubjectsResponse.metadata:type_name -> dispatch.v1.ResponseMeta + 24, // 38: dispatch.v1.ResponseMeta.debug_info:type_name -> dispatch.v1.DebugInformation + 25, // 39: dispatch.v1.DebugInformation.check:type_name -> dispatch.v1.CheckDebugTrace + 7, // 40: dispatch.v1.CheckDebugTrace.request:type_name -> dispatch.v1.DispatchCheckRequest + 6, // 41: dispatch.v1.CheckDebugTrace.resource_relation_type:type_name -> dispatch.v1.CheckDebugTrace.RelationType + 27, // 42: dispatch.v1.CheckDebugTrace.results:type_name -> dispatch.v1.CheckDebugTrace.ResultsEntry + 25, // 43: dispatch.v1.CheckDebugTrace.sub_problems:type_name -> dispatch.v1.CheckDebugTrace + 9, // 44: dispatch.v1.DispatchCheckResponse.ResultsByResourceIdEntry.value:type_name -> dispatch.v1.ResourceCheckResult + 9, // 45: dispatch.v1.CheckDebugTrace.ResultsEntry.value:type_name -> dispatch.v1.ResourceCheckResult + 7, // 46: dispatch.v1.DispatchService.DispatchCheck:input_type -> dispatch.v1.DispatchCheckRequest + 12, // 47: dispatch.v1.DispatchService.DispatchExpand:input_type -> dispatch.v1.DispatchExpandRequest + 14, // 48: dispatch.v1.DispatchService.DispatchLookup:input_type -> dispatch.v1.DispatchLookupRequest + 16, // 49: dispatch.v1.DispatchService.DispatchReachableResources:input_type -> dispatch.v1.DispatchReachableResourcesRequest + 19, // 50: dispatch.v1.DispatchService.DispatchLookupSubjects:input_type -> dispatch.v1.DispatchLookupSubjectsRequest + 8, // 51: dispatch.v1.DispatchService.DispatchCheck:output_type -> dispatch.v1.DispatchCheckResponse + 13, // 52: dispatch.v1.DispatchService.DispatchExpand:output_type -> dispatch.v1.DispatchExpandResponse + 15, // 53: dispatch.v1.DispatchService.DispatchLookup:output_type -> dispatch.v1.DispatchLookupResponse + 18, // 54: dispatch.v1.DispatchService.DispatchReachableResources:output_type -> dispatch.v1.DispatchReachableResourcesResponse + 21, // 55: dispatch.v1.DispatchService.DispatchLookupSubjects:output_type -> dispatch.v1.DispatchLookupSubjectsResponse + 51, // [51:56] is the sub-list for method output_type + 46, // [46:51] is the sub-list for method input_type + 46, // [46:46] is the sub-list for extension type_name + 46, // [46:46] is the sub-list for extension extendee + 0, // [0:46] is the sub-list for field type_name } func init() { file_dispatch_v1_dispatch_proto_init() } diff --git a/pkg/proto/dispatch/v1/dispatch.pb.validate.go b/pkg/proto/dispatch/v1/dispatch.pb.validate.go index c456de8e55..c7e7615489 100644 --- a/pkg/proto/dispatch/v1/dispatch.pb.validate.go +++ b/pkg/proto/dispatch/v1/dispatch.pb.validate.go @@ -2415,6 +2415,69 @@ func (m *FoundSubject) validate(all bool) error { // no validation rules for SubjectId + if all { + switch v := interface{}(m.GetCaveatExpression()).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, FoundSubjectValidationError{ + field: "CaveatExpression", + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, FoundSubjectValidationError{ + field: "CaveatExpression", + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(m.GetCaveatExpression()).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return FoundSubjectValidationError{ + field: "CaveatExpression", + reason: "embedded message failed validation", + cause: err, + } + } + } + + for idx, item := range m.GetExcludedSubjects() { + _, _ = idx, item + + if all { + switch v := interface{}(item).(type) { + case interface{ ValidateAll() error }: + if err := v.ValidateAll(); err != nil { + errors = append(errors, FoundSubjectValidationError{ + field: fmt.Sprintf("ExcludedSubjects[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + case interface{ Validate() error }: + if err := v.Validate(); err != nil { + errors = append(errors, FoundSubjectValidationError{ + field: fmt.Sprintf("ExcludedSubjects[%v]", idx), + reason: "embedded message failed validation", + cause: err, + }) + } + } + } else if v, ok := interface{}(item).(interface{ Validate() error }); ok { + if err := v.Validate(); err != nil { + return FoundSubjectValidationError{ + field: fmt.Sprintf("ExcludedSubjects[%v]", idx), + reason: "embedded message failed validation", + cause: err, + } + } + } + + } + if len(errors) > 0 { return FoundSubjectMultiError(errors) } diff --git a/pkg/proto/dispatch/v1/dispatch_vtproto.pb.go b/pkg/proto/dispatch/v1/dispatch_vtproto.pb.go index 524d698d2b..c73de02d17 100644 --- a/pkg/proto/dispatch/v1/dispatch_vtproto.pb.go +++ b/pkg/proto/dispatch/v1/dispatch_vtproto.pb.go @@ -440,12 +440,15 @@ func (m *FoundSubject) CloneVT() *FoundSubject { return (*FoundSubject)(nil) } r := &FoundSubject{ - SubjectId: m.SubjectId, + SubjectId: m.SubjectId, + CaveatExpression: m.CaveatExpression.CloneVT(), } - if rhs := m.ExcludedSubjectIds; rhs != nil { - tmpContainer := make([]string, len(rhs)) - copy(tmpContainer, rhs) - r.ExcludedSubjectIds = tmpContainer + if rhs := m.ExcludedSubjects; rhs != nil { + tmpContainer := make([]*FoundSubject, len(rhs)) + for k, v := range rhs { + tmpContainer[k] = v.CloneVT() + } + r.ExcludedSubjects = tmpContainer } if len(m.unknownFields) > 0 { r.unknownFields = make([]byte, len(m.unknownFields)) @@ -1616,14 +1619,27 @@ func (m *FoundSubject) MarshalToSizedBufferVT(dAtA []byte) (int, error) { i -= len(m.unknownFields) copy(dAtA[i:], m.unknownFields) } - if len(m.ExcludedSubjectIds) > 0 { - for iNdEx := len(m.ExcludedSubjectIds) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.ExcludedSubjectIds[iNdEx]) - copy(dAtA[i:], m.ExcludedSubjectIds[iNdEx]) - i = encodeVarint(dAtA, i, uint64(len(m.ExcludedSubjectIds[iNdEx]))) + if len(m.ExcludedSubjects) > 0 { + for iNdEx := len(m.ExcludedSubjects) - 1; iNdEx >= 0; iNdEx-- { + size, err := m.ExcludedSubjects[iNdEx].MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarint(dAtA, i, uint64(size)) i-- - dAtA[i] = 0x12 + dAtA[i] = 0x1a + } + } + if m.CaveatExpression != nil { + size, err := m.CaveatExpression.MarshalToSizedBufferVT(dAtA[:i]) + if err != nil { + return 0, err } + i -= size + i = encodeVarint(dAtA, i, uint64(size)) + i-- + dAtA[i] = 0x12 } if len(m.SubjectId) > 0 { i -= len(m.SubjectId) @@ -2366,9 +2382,13 @@ func (m *FoundSubject) SizeVT() (n int) { if l > 0 { n += 1 + l + sov(uint64(l)) } - if len(m.ExcludedSubjectIds) > 0 { - for _, s := range m.ExcludedSubjectIds { - l = len(s) + if m.CaveatExpression != nil { + l = m.CaveatExpression.SizeVT() + n += 1 + l + sov(uint64(l)) + } + if len(m.ExcludedSubjects) > 0 { + for _, e := range m.ExcludedSubjects { + l = e.SizeVT() n += 1 + l + sov(uint64(l)) } } @@ -4769,9 +4789,9 @@ func (m *FoundSubject) UnmarshalVT(dAtA []byte) error { iNdEx = postIndex case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field ExcludedSubjectIds", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field CaveatExpression", wireType) } - var stringLen uint64 + var msglen int for shift := uint(0); ; shift += 7 { if shift >= 64 { return ErrIntOverflow @@ -4781,23 +4801,61 @@ func (m *FoundSubject) UnmarshalVT(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= uint64(b&0x7F) << shift + msglen |= int(b&0x7F) << shift if b < 0x80 { break } } - intStringLen := int(stringLen) - if intStringLen < 0 { + if msglen < 0 { return ErrInvalidLength } - postIndex := iNdEx + intStringLen + postIndex := iNdEx + msglen if postIndex < 0 { return ErrInvalidLength } if postIndex > l { return io.ErrUnexpectedEOF } - m.ExcludedSubjectIds = append(m.ExcludedSubjectIds, string(dAtA[iNdEx:postIndex])) + if m.CaveatExpression == nil { + m.CaveatExpression = &CaveatExpression{} + } + if err := m.CaveatExpression.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ExcludedSubjects", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflow + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLength + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLength + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ExcludedSubjects = append(m.ExcludedSubjects, &FoundSubject{}) + if err := m.ExcludedSubjects[len(m.ExcludedSubjects)-1].UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil { + return err + } iNdEx = postIndex default: iNdEx = preIndex diff --git a/proto/internal/dispatch/v1/dispatch.proto b/proto/internal/dispatch/v1/dispatch.proto index 0f1246a294..d907d553e9 100644 --- a/proto/internal/dispatch/v1/dispatch.proto +++ b/proto/internal/dispatch/v1/dispatch.proto @@ -161,7 +161,8 @@ message DispatchLookupSubjectsRequest { message FoundSubject { string subject_id = 1; - repeated string excluded_subject_ids = 2; + CaveatExpression caveat_expression = 2; + repeated FoundSubject excluded_subjects = 3; } message DispatchLookupSubjectsResponse {