diff options
Diffstat (limited to 'vendor/github.com/authzed/spicedb/pkg/datastore')
15 files changed, 2236 insertions, 0 deletions
diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/caveat.go b/vendor/github.com/authzed/spicedb/pkg/datastore/caveat.go new file mode 100644 index 0000000..85dc7b4 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/caveat.go @@ -0,0 +1,35 @@ +package datastore + +import ( + "context" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +// RevisionedCaveat is a revisioned version of a caveat definition. +type RevisionedCaveat = RevisionedDefinition[*core.CaveatDefinition] + +// CaveatReader offers read operations for caveats +type CaveatReader interface { + // ReadCaveatByName returns a caveat with the provided name. + // It returns an instance of CaveatNotFoundError if not found. + ReadCaveatByName(ctx context.Context, name string) (caveat *core.CaveatDefinition, lastWritten Revision, err error) + + // ListAllCaveats returns all caveats stored in the system. + ListAllCaveats(ctx context.Context) ([]RevisionedCaveat, error) + + // LookupCaveatsWithNames finds all caveats with the matching names. + LookupCaveatsWithNames(ctx context.Context, names []string) ([]RevisionedCaveat, error) +} + +// CaveatStorer offers both read and write operations for Caveats +type CaveatStorer interface { + CaveatReader + + // WriteCaveats stores the provided caveats, and returns the assigned IDs + // Each element of the returning slice corresponds by position to the input slice + WriteCaveats(context.Context, []*core.CaveatDefinition) error + + // DeleteCaveats deletes the provided caveats by name + DeleteCaveats(ctx context.Context, names []string) error +} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/context.go b/vendor/github.com/authzed/spicedb/pkg/datastore/context.go new file mode 100644 index 0000000..1ea451e --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/context.go @@ -0,0 +1 @@ +package datastore diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/counters.go b/vendor/github.com/authzed/spicedb/pkg/datastore/counters.go new file mode 100644 index 0000000..9b90ea4 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/counters.go @@ -0,0 +1,47 @@ +package datastore + +import ( + "context" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +// RelationshipCounter is a struct that represents a count of relationships that match a filter. +type RelationshipCounter struct { + // Name is the name of the counter. + Name string + + // Filter is the filter that the count represents. + Filter *core.RelationshipFilter + + // Count is the count of relationships that match the filter. + Count int + + // ComputedAtRevision is the revision at which the count was last computed. If NoRevision, + // the count has never been computed. + ComputedAtRevision Revision +} + +// CounterRegisterer is an interface for registering and unregistering counts. +type CounterRegisterer interface { + // RegisterCounter registers a count with the provided filter. If the counter already exists, + // returns an error. + RegisterCounter(ctx context.Context, name string, filter *core.RelationshipFilter) error + + // UnregisterCounter unregisters a counter. If the counter did not exist, returns an error. + UnregisterCounter(ctx context.Context, name string) error + + // StoreCounterValue stores a count for the counter with the given name, at the given revision. + // If the counter does not exist, returns an error. + StoreCounterValue(ctx context.Context, name string, value int, computedAtRevision Revision) error +} + +// CounterReader is an interface for reading counts. +type CounterReader interface { + // CountRelationships returns the count of relationships that match the provided counter. If the counter is not + // registered, returns an error. + CountRelationships(ctx context.Context, name string) (int, error) + + // LookupCounters returns all registered counters. + LookupCounters(ctx context.Context) ([]RelationshipCounter, error) +} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/credentials.go b/vendor/github.com/authzed/spicedb/pkg/datastore/credentials.go new file mode 100644 index 0000000..9c4a093 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/credentials.go @@ -0,0 +1,94 @@ +package datastore + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + rdsauth "github.com/aws/aws-sdk-go-v2/feature/rds/auth" + "golang.org/x/exp/maps" + + log "github.com/authzed/spicedb/internal/logging" +) + +// CredentialsProvider allows datastore credentials to be retrieved dynamically +type CredentialsProvider interface { + // Name returns the name of the provider + Name() string + // IsCleartextToken returns true if the token returned represents a token (rather than a password) that must be sent in cleartext to the datastore, or false otherwise. + // This may be used to configure the datastore options to avoid sending a hash of the token instead of its value. + // Note that it is always recommended that communication channel be encrypted. + IsCleartextToken() bool + // Get returns the username and password to use when connecting to the underlying datastore + Get(ctx context.Context, dbEndpoint string, dbUser string) (string, string, error) +} + +var NoCredentialsProvider CredentialsProvider = nil + +type credentialsProviderBuilderFunc func(ctx context.Context) (CredentialsProvider, error) + +const ( + // AWSIAMCredentialProvider generates AWS IAM tokens for authenticating with the datastore (i.e. RDS) + AWSIAMCredentialProvider = "aws-iam" +) + +var BuilderForCredentialProvider = map[string]credentialsProviderBuilderFunc{ + AWSIAMCredentialProvider: newAWSIAMCredentialsProvider, +} + +// CredentialsProviderOptions returns the full set of credential provider names, sorted and quoted into a string. +func CredentialsProviderOptions() string { + ids := maps.Keys(BuilderForCredentialProvider) + sort.Strings(ids) + quoted := make([]string, 0, len(ids)) + for _, id := range ids { + quoted = append(quoted, `"`+id+`"`) + } + return strings.Join(quoted, ", ") +} + +// NewCredentialsProvider create a new CredentialsProvider for the given name +// returns an error if no match is found, of if there is a problem creating the given CredentialsProvider +func NewCredentialsProvider(ctx context.Context, name string) (CredentialsProvider, error) { + builder, ok := BuilderForCredentialProvider[name] + if !ok { + return nil, fmt.Errorf("unknown credentials provider: %s", name) + } + return builder(ctx) +} + +// AWS IAM provider + +func newAWSIAMCredentialsProvider(ctx context.Context) (CredentialsProvider, error) { + awsSdkConfig, err := awsconfig.LoadDefaultConfig(ctx) + if err != nil { + return nil, err + } + return &awsIamCredentialsProvider{awsSdkConfig: awsSdkConfig}, nil +} + +type awsIamCredentialsProvider struct { + awsSdkConfig aws.Config +} + +func (d awsIamCredentialsProvider) Name() string { + return AWSIAMCredentialProvider +} + +func (d awsIamCredentialsProvider) IsCleartextToken() bool { + // The AWS IAM token can be of an arbitrary length and must not be hashed or truncated by the datastore driver + // See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html + return true +} + +func (d awsIamCredentialsProvider) Get(ctx context.Context, dbEndpoint string, dbUser string) (string, string, error) { + authToken, err := rdsauth.BuildAuthToken(ctx, dbEndpoint, d.awsSdkConfig.Region, dbUser, d.awsSdkConfig.Credentials) + if err != nil { + return "", "", err + } + log.Ctx(ctx).Trace().Str("region", d.awsSdkConfig.Region).Str("endpoint", dbEndpoint).Str("user", dbUser).Msg("successfully retrieved IAM auth token for DB") + return dbUser, authToken, nil +} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/datastore.go b/vendor/github.com/authzed/spicedb/pkg/datastore/datastore.go new file mode 100644 index 0000000..24cd25f --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/datastore.go @@ -0,0 +1,992 @@ +package datastore + +import ( + "context" + "fmt" + "iter" + "slices" + "sort" + "strings" + "time" + + "github.com/rs/zerolog" + "google.golang.org/protobuf/types/known/structpb" + + "github.com/authzed/spicedb/pkg/tuple" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" + + "github.com/authzed/spicedb/pkg/datastore/options" + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +var Engines []string + +// SortedEngineIDs returns the full set of engine IDs, sorted. +func SortedEngineIDs() []string { + engines := append([]string{}, Engines...) + sort.Strings(engines) + return engines +} + +// EngineOptions returns the full set of engine IDs, sorted and quoted into a string. +func EngineOptions() string { + ids := SortedEngineIDs() + quoted := make([]string, 0, len(ids)) + for _, id := range ids { + quoted = append(quoted, `"`+id+`"`) + } + return strings.Join(quoted, ", ") +} + +// Ellipsis is a special relation that is assumed to be valid on the right +// hand side of a tuple. +const Ellipsis = "..." + +// RevisionChanges represents the changes in a single transaction. +type RevisionChanges struct { + Revision Revision + + // RelationshipChanges are any relationships that were changed at this revision. + RelationshipChanges []tuple.RelationshipUpdate + + // ChangedDefinitions are any definitions that were added or changed at this revision. + ChangedDefinitions []SchemaDefinition + + // DeletedNamespaces are any namespaces that were deleted. + DeletedNamespaces []string + + // DeletedCaveats are any caveats that were deleted. + DeletedCaveats []string + + // IsCheckpoint, if true, indicates that the datastore has reported all changes + // up until and including the Revision and that no additional schema updates can + // have occurred before this point. + IsCheckpoint bool + + // Metadata is the metadata associated with the revision, if any. + Metadata *structpb.Struct +} + +func (rc RevisionChanges) DebugString() string { + if rc.IsCheckpoint { + return "[checkpoint]" + } + + debugString := "" + + for _, relChange := range rc.RelationshipChanges { + debugString += relChange.DebugString() + "\n" + } + + for _, def := range rc.ChangedDefinitions { + debugString += fmt.Sprintf("Definition: %T:%s\n", def, def.GetName()) + } + + for _, ns := range rc.DeletedNamespaces { + debugString += fmt.Sprintf("DeletedNamespace: %s\n", ns) + } + + for _, caveat := range rc.DeletedCaveats { + debugString += fmt.Sprintf("DeletedCaveat: %s\n", caveat) + } + + return debugString +} + +func (rc RevisionChanges) MarshalZerologObject(e *zerolog.Event) { + e.Str("revision", rc.Revision.String()) + e.Bool("is-checkpoint", rc.IsCheckpoint) + e.Array("deleted-namespaces", strArray(rc.DeletedNamespaces)) + e.Array("deleted-caveats", strArray(rc.DeletedCaveats)) + + changedNames := make([]string, 0, len(rc.ChangedDefinitions)) + for _, cd := range rc.ChangedDefinitions { + changedNames = append(changedNames, fmt.Sprintf("%T:%s", cd, cd.GetName())) + } + + e.Array("changed-definitions", strArray(changedNames)) + e.Int("num-changed-relationships", len(rc.RelationshipChanges)) +} + +// ExpirationFilterOption is the filter option for the expiration field on relationships. +type ExpirationFilterOption int + +const ( + // ExpirationFilterOptionNone indicates that the expiration filter should not be used: + // relationships both with and without expiration will be returned. + ExpirationFilterOptionNone ExpirationFilterOption = iota + + // ExpirationFilterOptionHasExpiration indicates that the expiration filter should only + // return relationships with an expiration. + ExpirationFilterOptionHasExpiration + + // ExpirationFilterOptionNoExpiration indicates that the expiration filter should only + // return relationships without an expiration. + ExpirationFilterOptionNoExpiration +) + +// CaveatFilterOption is the filter option for the caveat name field on relationships. +type CaveatFilterOption int + +const ( + // CaveatFilterOptionNone indicates that the caveat filter should not be used: + // relationships both with and without caveats will be returned. + CaveatFilterOptionNone CaveatFilterOption = iota + + // CaveatFilterOptionHasMatchingCaveat indicates that the caveat filter should only + // return relationships with the matching caveat. + CaveatFilterOptionHasMatchingCaveat + + // CaveatFilterOptionNoCaveat indicates that the caveat filter should only + // return relationships without a caveat. + CaveatFilterOptionNoCaveat +) + +// RelationshipsFilter is a filter for relationships. +type RelationshipsFilter struct { + // OptionalResourceType is the namespace/type for the resources to be found. + OptionalResourceType string + + // OptionalResourceIds are the IDs of the resources to find. If nil empty, any resource ID will be allowed. + // Cannot be used with OptionalResourceIDPrefix. + OptionalResourceIds []string + + // OptionalResourceIDPrefix is the prefix to use for resource IDs. If empty, any prefix is allowed. + // Cannot be used with OptionalResourceIds. + OptionalResourceIDPrefix string + + // OptionalResourceRelation is the relation of the resource to find. If empty, any relation is allowed. + OptionalResourceRelation string + + // OptionalSubjectsSelectors is the selectors to use for subjects of the relationship. If nil, all subjects are allowed. + // If specified, relationships matching *any* selector will be returned. + OptionalSubjectsSelectors []SubjectsSelector + + // OptionalCaveatNameFilter is the filter to use for caveated relationships, filtering by a specific caveat name. + // By default, no caveat filtered is done (one direction or the other). + OptionalCaveatNameFilter CaveatNameFilter + + // OptionalExpirationOption is the filter to use for relationships with or without an expiration. + OptionalExpirationOption ExpirationFilterOption +} + +// Test returns true iff the given relationship is matched by this filter. +func (rf RelationshipsFilter) Test(relationship tuple.Relationship) bool { + if rf.OptionalResourceType != "" && rf.OptionalResourceType != relationship.Resource.ObjectType { + return false + } + + if len(rf.OptionalResourceIds) > 0 && !slices.Contains(rf.OptionalResourceIds, relationship.Resource.ObjectID) { + return false + } + + if rf.OptionalResourceIDPrefix != "" && !strings.HasPrefix(relationship.Resource.ObjectID, rf.OptionalResourceIDPrefix) { + return false + } + + if rf.OptionalResourceRelation != "" && rf.OptionalResourceRelation != relationship.Resource.Relation { + return false + } + + if len(rf.OptionalSubjectsSelectors) > 0 { + for _, selector := range rf.OptionalSubjectsSelectors { + if selector.Test(relationship.Subject) { + return true + } + } + return false + } + + switch rf.OptionalCaveatNameFilter.Option { + case CaveatFilterOptionNone: + // No caveat filter, so no need to check. + + case CaveatFilterOptionHasMatchingCaveat: + if relationship.OptionalCaveat == nil || relationship.OptionalCaveat.CaveatName != rf.OptionalCaveatNameFilter.CaveatName { + return false + } + + case CaveatFilterOptionNoCaveat: + if relationship.OptionalCaveat != nil && relationship.OptionalCaveat.CaveatName != "" { + return false + } + } + + if rf.OptionalExpirationOption == ExpirationFilterOptionHasExpiration && relationship.OptionalExpiration == nil { + return false + } + + if rf.OptionalExpirationOption == ExpirationFilterOptionNoExpiration && relationship.OptionalExpiration != nil { + return false + } + + return true +} + +// CaveatNameFilter is a filter for caveat names. +type CaveatNameFilter struct { + // Option is the filter option to use for the caveat name. + Option CaveatFilterOption + + // CaveatName is the name of the caveat to filter by. Must be specified if option is + // CaveatFilterOptionHasCaveat. + CaveatName string +} + +func WithCaveatName(caveatName string) CaveatNameFilter { + return CaveatNameFilter{ + Option: CaveatFilterOptionHasMatchingCaveat, + CaveatName: caveatName, + } +} + +func WithNoCaveat() CaveatNameFilter { + return CaveatNameFilter{ + Option: CaveatFilterOptionNoCaveat, + } +} + +// CoreFilterFromRelationshipFilter constructs a core RelationshipFilter from a V1 RelationshipsFilter. +func CoreFilterFromRelationshipFilter(filter *v1.RelationshipFilter) *core.RelationshipFilter { + return &core.RelationshipFilter{ + ResourceType: filter.ResourceType, + OptionalResourceId: filter.OptionalResourceId, + OptionalResourceIdPrefix: filter.OptionalResourceIdPrefix, + OptionalRelation: filter.OptionalRelation, + OptionalSubjectFilter: coreFilterFromSubjectsFilter(filter.OptionalSubjectFilter), + } +} + +func coreFilterFromSubjectsFilter(filter *v1.SubjectFilter) *core.SubjectFilter { + if filter == nil { + return nil + } + + return &core.SubjectFilter{ + SubjectType: filter.SubjectType, + OptionalSubjectId: filter.OptionalSubjectId, + OptionalRelation: coreFilterFromSubjectRelationFilter(filter.OptionalRelation), + } +} + +func coreFilterFromSubjectRelationFilter(filter *v1.SubjectFilter_RelationFilter) *core.SubjectFilter_RelationFilter { + if filter == nil { + return nil + } + + return &core.SubjectFilter_RelationFilter{ + Relation: filter.Relation, + } +} + +// RelationshipsFilterFromCoreFilter constructs a datastore RelationshipsFilter from a core RelationshipFilter. +func RelationshipsFilterFromCoreFilter(filter *core.RelationshipFilter) (RelationshipsFilter, error) { + var resourceIds []string + if filter.OptionalResourceId != "" { + resourceIds = []string{filter.OptionalResourceId} + } + + var subjectsSelectors []SubjectsSelector + if filter.OptionalSubjectFilter != nil { + var subjectIds []string + if filter.OptionalSubjectFilter.OptionalSubjectId != "" { + subjectIds = []string{filter.OptionalSubjectFilter.OptionalSubjectId} + } + + relationFilter := SubjectRelationFilter{} + + if filter.OptionalSubjectFilter.OptionalRelation != nil { + relation := filter.OptionalSubjectFilter.OptionalRelation.GetRelation() + if relation != "" { + relationFilter = relationFilter.WithNonEllipsisRelation(relation) + } else { + relationFilter = relationFilter.WithEllipsisRelation() + } + } + + subjectsSelectors = append(subjectsSelectors, SubjectsSelector{ + OptionalSubjectType: filter.OptionalSubjectFilter.SubjectType, + OptionalSubjectIds: subjectIds, + RelationFilter: relationFilter, + }) + } + + if filter.OptionalResourceId != "" && filter.OptionalResourceIdPrefix != "" { + return RelationshipsFilter{}, fmt.Errorf("cannot specify both OptionalResourceId and OptionalResourceIDPrefix") + } + + if filter.ResourceType == "" && filter.OptionalRelation == "" && len(resourceIds) == 0 && filter.OptionalResourceIdPrefix == "" && len(subjectsSelectors) == 0 { + return RelationshipsFilter{}, fmt.Errorf("at least one filter field must be set") + } + + return RelationshipsFilter{ + OptionalResourceType: filter.ResourceType, + OptionalResourceIds: resourceIds, + OptionalResourceIDPrefix: filter.OptionalResourceIdPrefix, + OptionalResourceRelation: filter.OptionalRelation, + OptionalSubjectsSelectors: subjectsSelectors, + }, nil +} + +// RelationshipsFilterFromPublicFilter constructs a datastore RelationshipsFilter from an API-defined RelationshipFilter. +func RelationshipsFilterFromPublicFilter(filter *v1.RelationshipFilter) (RelationshipsFilter, error) { + var resourceIds []string + if filter.OptionalResourceId != "" { + resourceIds = []string{filter.OptionalResourceId} + } + + var subjectsSelectors []SubjectsSelector + if filter.OptionalSubjectFilter != nil { + var subjectIds []string + if filter.OptionalSubjectFilter.OptionalSubjectId != "" { + subjectIds = []string{filter.OptionalSubjectFilter.OptionalSubjectId} + } + + relationFilter := SubjectRelationFilter{} + + if filter.OptionalSubjectFilter.OptionalRelation != nil { + relation := filter.OptionalSubjectFilter.OptionalRelation.GetRelation() + if relation != "" { + relationFilter = relationFilter.WithNonEllipsisRelation(relation) + } else { + relationFilter = relationFilter.WithEllipsisRelation() + } + } + + subjectsSelectors = append(subjectsSelectors, SubjectsSelector{ + OptionalSubjectType: filter.OptionalSubjectFilter.SubjectType, + OptionalSubjectIds: subjectIds, + RelationFilter: relationFilter, + }) + } + + if filter.OptionalResourceId != "" && filter.OptionalResourceIdPrefix != "" { + return RelationshipsFilter{}, fmt.Errorf("cannot specify both OptionalResourceId and OptionalResourceIDPrefix") + } + + if filter.ResourceType == "" && filter.OptionalRelation == "" && len(resourceIds) == 0 && filter.OptionalResourceIdPrefix == "" && len(subjectsSelectors) == 0 { + return RelationshipsFilter{}, fmt.Errorf("at least one filter field must be set") + } + + return RelationshipsFilter{ + OptionalResourceType: filter.ResourceType, + OptionalResourceIds: resourceIds, + OptionalResourceIDPrefix: filter.OptionalResourceIdPrefix, + OptionalResourceRelation: filter.OptionalRelation, + OptionalSubjectsSelectors: subjectsSelectors, + }, nil +} + +// SubjectsSelector is a selector for subjects. +type SubjectsSelector struct { + // OptionalSubjectType is the namespace/type for the subjects to be found, if any. + OptionalSubjectType string + + // OptionalSubjectIds are the IDs of the subjects to find. If nil or empty, any subject ID will be allowed. + OptionalSubjectIds []string + + // RelationFilter is the filter to use for the relation(s) of the subjects. If neither field + // is set, any relation is allowed. + RelationFilter SubjectRelationFilter +} + +// Test returns true iff the given subject is matched by this filter. +func (ss SubjectsSelector) Test(subject tuple.ObjectAndRelation) bool { + if ss.OptionalSubjectType != "" && ss.OptionalSubjectType != subject.ObjectType { + return false + } + + if len(ss.OptionalSubjectIds) > 0 && !slices.Contains(ss.OptionalSubjectIds, subject.ObjectID) { + return false + } + + if !ss.RelationFilter.IsEmpty() { + if ss.RelationFilter.IncludeEllipsisRelation && subject.Relation == tuple.Ellipsis { + return true + } + + if ss.RelationFilter.NonEllipsisRelation != "" && ss.RelationFilter.NonEllipsisRelation != subject.Relation { + return false + } + + if ss.RelationFilter.OnlyNonEllipsisRelations && subject.Relation == tuple.Ellipsis { + return false + } + } + + return true +} + +// SubjectRelationFilter is the filter to use for relation(s) of subjects being queried. +type SubjectRelationFilter struct { + // NonEllipsisRelation is the relation of the subject type to find. If empty, + // IncludeEllipsisRelation must be true. + NonEllipsisRelation string + + // IncludeEllipsisRelation, if true, indicates that the ellipsis relation + // should be included as an option. + IncludeEllipsisRelation bool + + // OnlyNonEllipsisRelations, if true, indicates that only non-ellipsis relations + // should be included. + OnlyNonEllipsisRelations bool +} + +// WithOnlyNonEllipsisRelations indicates that only non-ellipsis relations should be included. +func (sf SubjectRelationFilter) WithOnlyNonEllipsisRelations() SubjectRelationFilter { + sf.OnlyNonEllipsisRelations = true + sf.NonEllipsisRelation = "" + sf.IncludeEllipsisRelation = false + return sf +} + +// WithEllipsisRelation indicates that the subject filter should include the ellipsis relation +// as an option for the subjects' relation. +func (sf SubjectRelationFilter) WithEllipsisRelation() SubjectRelationFilter { + sf.IncludeEllipsisRelation = true + sf.OnlyNonEllipsisRelations = false + return sf +} + +// WithNonEllipsisRelation indicates that the specified non-ellipsis relation should be included as an +// option for the subjects' relation. +func (sf SubjectRelationFilter) WithNonEllipsisRelation(relation string) SubjectRelationFilter { + sf.NonEllipsisRelation = relation + sf.OnlyNonEllipsisRelations = false + return sf +} + +// WithRelation indicates that the specified relation should be included as an +// option for the subjects' relation. +func (sf SubjectRelationFilter) WithRelation(relation string) SubjectRelationFilter { + if relation == tuple.Ellipsis { + return sf.WithEllipsisRelation() + } + return sf.WithNonEllipsisRelation(relation) +} + +// IsEmpty returns true if the subject relation filter is empty. +func (sf SubjectRelationFilter) IsEmpty() bool { + return !sf.IncludeEllipsisRelation && sf.NonEllipsisRelation == "" && !sf.OnlyNonEllipsisRelations +} + +// SubjectsFilter is a filter for subjects. +type SubjectsFilter struct { + // SubjectType is the namespace/type for the subjects to be found. + SubjectType string + + // OptionalSubjectIds are the IDs of the subjects to find. If nil or empty, any subject ID will be allowed. + OptionalSubjectIds []string + + // RelationFilter is the filter to use for the relation(s) of the subjects. If neither field + // is set, any relation is allowed. + RelationFilter SubjectRelationFilter +} + +func (sf SubjectsFilter) AsSelector() SubjectsSelector { + return SubjectsSelector{ + OptionalSubjectType: sf.SubjectType, + OptionalSubjectIds: sf.OptionalSubjectIds, + RelationFilter: sf.RelationFilter, + } +} + +// SchemaDefinition represents a namespace or caveat definition under a schema. +type SchemaDefinition interface { + GetName() string + SizeVT() int +} + +// RevisionedDefinition holds a schema definition and its last updated revision. +type RevisionedDefinition[T SchemaDefinition] struct { + // Definition is the namespace or caveat definition. + Definition T + + // LastWrittenRevision is the revision at which the namespace or caveat was last updated. + LastWrittenRevision Revision +} + +func (rd RevisionedDefinition[T]) GetLastWrittenRevision() Revision { + return rd.LastWrittenRevision +} + +// RevisionedNamespace is a revisioned version of a namespace definition. +type RevisionedNamespace = RevisionedDefinition[*core.NamespaceDefinition] + +// Reader is an interface for reading relationships from the datastore. +type Reader interface { + CaveatReader + CounterReader + + // QueryRelationships reads relationships, starting from the resource side. + QueryRelationships( + ctx context.Context, + filter RelationshipsFilter, + options ...options.QueryOptionsOption, + ) (RelationshipIterator, error) + + // ReverseQueryRelationships reads relationships, starting from the subject. + ReverseQueryRelationships( + ctx context.Context, + subjectsFilter SubjectsFilter, + options ...options.ReverseQueryOptionsOption, + ) (RelationshipIterator, error) + + // ReadNamespaceByName reads a namespace definition and the revision at which it was created or + // last written. It returns an instance of NamespaceNotFoundError if not found. + ReadNamespaceByName(ctx context.Context, nsName string) (ns *core.NamespaceDefinition, lastWritten Revision, err error) + + // ListAllNamespaces lists all namespaces defined. + ListAllNamespaces(ctx context.Context) ([]RevisionedNamespace, error) + + // LookupNamespacesWithNames finds all namespaces with the matching names. + LookupNamespacesWithNames(ctx context.Context, nsNames []string) ([]RevisionedNamespace, error) +} + +type ReadWriteTransaction interface { + Reader + CaveatStorer + CounterRegisterer + + // WriteRelationships takes a list of tuple mutations and applies them to the datastore. + WriteRelationships(ctx context.Context, mutations []tuple.RelationshipUpdate) error + + // DeleteRelationships deletes relationships that match the provided filter, with + // the optional limit. Returns the number of deleted relationships. If a limit + // is provided and reached, the method will return true as the second return value. + // Otherwise, the boolean can be ignored. + DeleteRelationships(ctx context.Context, filter *v1.RelationshipFilter, + options ...options.DeleteOptionsOption, + ) (uint64, bool, error) + + // WriteNamespaces takes proto namespace definitions and persists them. + WriteNamespaces(ctx context.Context, newConfigs ...*core.NamespaceDefinition) error + + // DeleteNamespaces deletes namespaces including associated relationships. + DeleteNamespaces(ctx context.Context, nsNames ...string) error + + // BulkLoad takes a relationship source iterator, and writes all of the + // relationships to the backing datastore in an optimized fashion. This + // method can and will omit checks and otherwise cut corners in the + // interest of performance, and should not be relied upon for OLTP-style + // workloads. + BulkLoad(ctx context.Context, iter BulkWriteRelationshipSource) (uint64, error) +} + +// TxUserFunc is a type for the function that users supply when they invoke a read-write transaction. +type TxUserFunc func(context.Context, ReadWriteTransaction) error + +// ReadyState represents the ready state of the datastore. +type ReadyState struct { + // Message is a human-readable status message for the current state. + Message string + + // IsReady indicates whether the datastore is ready. + IsReady bool +} + +// BulkWriteRelationshipSource is an interface for transferring relationships +// to a backing datastore with a zero-copy methodology. +type BulkWriteRelationshipSource interface { + // Next Returns a pointer to a relation tuple if one is available, or nil if + // there are no more or there was an error. + // + // Note: sources may re-use the same memory address for every tuple, data + // may change on every call to next even if the pointer has not changed. + Next(ctx context.Context) (*tuple.Relationship, error) +} + +type WatchContent int + +const ( + WatchRelationships WatchContent = 1 << 0 + WatchSchema WatchContent = 1 << 1 + WatchCheckpoints WatchContent = 1 << 2 +) + +// WatchOptions are options for a Watch call. +type WatchOptions struct { + // Content is the content to watch. + Content WatchContent + + // CheckpointInterval is the interval to use for checkpointing in the watch. + // If given the zero value, the datastore's default will be used. If smaller + // than the datastore's minimum, the minimum will be used. + CheckpointInterval time.Duration + + // WatchBufferLength is the length of the buffer for the watch channel. If + // given the zero value, the datastore's default will be used. + WatchBufferLength uint16 + + // WatchBufferWriteTimeout is the timeout for writing to the watch channel. + // If given the zero value, the datastore's default will be used. + WatchBufferWriteTimeout time.Duration + + // WatchConnectTimeout is the timeout for connecting to the watch channel. + // If given the zero value, the datastore's default will be used. + // May not be supported by the datastore. + WatchConnectTimeout time.Duration + + // MaximumBufferedChangesByteSize is the maximum byte size of the buffered changes struct. + // If unspecified, no maximum will be enforced. If the maximum is reached before + // the changes can be sent, the watch will be closed with an error. + MaximumBufferedChangesByteSize uint64 + + // EmissionStrategy defines when are changes streamed to the client. If unspecified, changes will be buffered until + // they can be checkpointed, which is the default behavior. + EmissionStrategy EmissionStrategy +} + +// EmissionStrategy describes when changes are emitted to the client. +type EmissionStrategy int + +const ( + // EmitWhenCheckpointedStrategy will buffer changes until a checkpoint is reached. This also means that + // changes will be deduplicated and revisions will be sorted before emission as soon as they can be checkpointed. + EmitWhenCheckpointedStrategy = iota + + // EmitImmediatelyStrategy emits changes as soon as they are available. This means changes will not be buffered, + // and thus will be emitted as soon as they are available, but clients are responsible for buffering, deduplication, + // and sorting revisions. In practical terms that can only happens if Checkpoints have been requested, so enabling + // EmitImmediatelyStrategy without Checkpoints will return an error. + EmitImmediatelyStrategy +) + +// WatchJustRelationships returns watch options for just relationships. +func WatchJustRelationships() WatchOptions { + return WatchOptions{ + Content: WatchRelationships, + } +} + +// WatchJustSchema returns watch options for just schema. +func WatchJustSchema() WatchOptions { + return WatchOptions{ + Content: WatchSchema, + } +} + +// WithCheckpointInterval sets the checkpoint interval on a watch options, returning +// an updated options struct. +func (wo WatchOptions) WithCheckpointInterval(interval time.Duration) WatchOptions { + return WatchOptions{ + Content: wo.Content, + CheckpointInterval: interval, + } +} + +// ReadOnlyDatastore is an interface for reading relationships from the datastore. +type ReadOnlyDatastore interface { + // MetricsID returns an identifier for the datastore for use in metrics. + // This identifier is typically the hostname of the datastore (where applicable) + // and may not be unique; callers should not rely on uniqueness. + MetricsID() (string, error) + + // SnapshotReader creates a read-only handle that reads the datastore at the specified revision. + // Any errors establishing the reader will be returned by subsequent calls. + SnapshotReader(Revision) Reader + + // OptimizedRevision gets a revision that will likely already be replicated + // and will likely be shared amongst many queries. + OptimizedRevision(ctx context.Context) (Revision, error) + + // HeadRevision gets a revision that is guaranteed to be at least as fresh as + // right now. + HeadRevision(ctx context.Context) (Revision, error) + + // CheckRevision checks the specified revision to make sure it's valid and + // hasn't been garbage collected. + CheckRevision(ctx context.Context, revision Revision) error + + // RevisionFromString will parse the revision text and return the specific type of Revision + // used by the specific datastore implementation. + RevisionFromString(serialized string) (Revision, error) + + // Watch notifies the caller about changes to the datastore, based on the specified options. + // + // All events following afterRevision will be sent to the caller. + // + // Errors returned will fall into a few classes: + // - WatchDisconnectedError - the watch has fallen too far behind and has been disconnected. + // - WatchCanceledError - the watch was canceled by the caller. + // - WatchDisabledError - the watch is disabled by being unsupported by the datastore. + // - WatchRetryableError - the watch is retryable, and the caller may retry after some backoff time. + // - InvalidRevisionError - the revision specified has passed the datastore's watch history window and + // the watch cannot be retried. + // - Other errors - the watch should not be retried due to a fatal error. + Watch(ctx context.Context, afterRevision Revision, options WatchOptions) (<-chan RevisionChanges, <-chan error) + + // ReadyState returns a state indicating whether the datastore is ready to accept data. + // Datastores that require database schema creation will return not-ready until the migrations + // have been run to create the necessary tables. + ReadyState(ctx context.Context) (ReadyState, error) + + // Features returns an object representing what features this + // datastore can support. Can make calls to the database, so should + // only be used when a connection is allowed. + Features(ctx context.Context) (*Features, error) + + // OfflineFeatures returns an object representing what features this + // datastore supports code-wise, without making any calls to the database. + OfflineFeatures() (*Features, error) + + // Statistics returns relevant values about the data contained in this cluster. + Statistics(ctx context.Context) (Stats, error) + + // Close closes the data store. + Close() error +} + +// Datastore represents tuple access for a single namespace. +type Datastore interface { + ReadOnlyDatastore + + // ReadWriteTx starts a read/write transaction, which will be committed if no error is + // returned and rolled back if an error is returned. + ReadWriteTx(context.Context, TxUserFunc, ...options.RWTOptionsOption) (Revision, error) +} + +// ParsedExplain represents the parsed output of an EXPLAIN statement. +type ParsedExplain struct { + // IndexesUsed is the list of indexes used in the query. + IndexesUsed []string +} + +// Explainable is an interface for datastores that support EXPLAIN statements. +type Explainable interface { + // BuildExplainQuery builds an EXPLAIN statement for the given SQL and arguments. + BuildExplainQuery(sql string, args []any) (string, []any, error) + + // ParseExplain parses the output of an EXPLAIN statement. + ParseExplain(explain string) (ParsedExplain, error) + + // PreExplainStatements returns any statements that should be run before the EXPLAIN statement. + PreExplainStatements() []string +} + +// SQLDatastore is an interface for datastores that support SQL-based operations. +type SQLDatastore interface { + Datastore + Explainable +} + +// StrictReadDatastore is an interface for datastores that support strict read mode. +type StrictReadDatastore interface { + Datastore + + // IsStrictReadModeEnabled returns whether the datastore is in strict read mode. + IsStrictReadModeEnabled() bool +} + +type strArray []string + +// MarshalZerologArray implements zerolog array marshalling. +func (strs strArray) MarshalZerologArray(a *zerolog.Array) { + for _, val := range strs { + a.Str(val) + } +} + +// StartableDatastore is an optional extension to the datastore interface that, when implemented, +// provides the ability for callers to start background operations on the datastore. +type StartableDatastore interface { + Datastore + + // Start starts any background operations on the datastore. The context provided, if canceled, will + // also cancel the background operation(s) on the datastore. + Start(ctx context.Context) error +} + +// RepairOperation represents a single kind of repair operation that can be run in a repairable +// datastore. +type RepairOperation struct { + // Name is the command-line name for the repair operation. + Name string + + // Description is the human-readable description for the repair operation. + Description string +} + +// RepairableDatastore is an optional extension to the datastore interface that, when implemented, +// provides the ability for callers to repair the datastore's data in some fashion. +type RepairableDatastore interface { + Datastore + + // Repair runs the repair operation on the datastore. + Repair(ctx context.Context, operationName string, outputProgress bool) error + + // RepairOperations returns the available repair operations for the datastore. + RepairOperations() []RepairOperation +} + +// UnwrappableDatastore represents a datastore that can be unwrapped into the underlying +// datastore. +type UnwrappableDatastore interface { + // Unwrap returns the wrapped datastore. + Unwrap() Datastore +} + +// UnwrapAs recursively attempts to unwrap the datastore into the specified type +// In none of the layers of the datastore implement the specified type, nil is returned. +func UnwrapAs[T any](datastore Datastore) T { + var ds T + uwds := datastore + + for { + var ok bool + ds, ok = uwds.(T) + if ok { + break + } + + wds, ok := uwds.(UnwrappableDatastore) + if !ok { + break + } + + uwds = wds.Unwrap() + } + + return ds +} + +// FeatureStatus are the possible statuses for a feature in the datastore. +type FeatureStatus int + +const ( + // FeatureStatusUnknown indicates that the status of the feature is unknown. + // This can be returned, for example, when a call is made to OfflineFeatures + // but the feature requires a call to the database to determine its status. + FeatureStatusUnknown FeatureStatus = iota + + // FeatureSupported indicates that the feature is supported by the datastore. + FeatureSupported + + // FeatureUnsupported indicates that the feature is not supported by the datastore. + FeatureUnsupported +) + +// Feature represents a capability that a datastore can support, plus an +// optional message explaining the feature is available (or not). +type Feature struct { + Status FeatureStatus + Reason string +} + +// Features holds values that represent what features a database can support. +type Features struct { + // Watch is enabled if the underlying datastore can support the Watch api. + Watch Feature + + // ContinuousCheckpointing is enabled if the underlying datastore supports continuous checkpointing + // via the Watch API. If not supported, clients of the Watch API may expect checkpoints only when + // new transactions are committed. + ContinuousCheckpointing Feature + + // WatchEmitsImmediately indicates if the datastore supports the EmitImmediatelyStrategy EmissionStrategy. + // If not supported, clients of the Watch API will receive an error when calling Watch API with + // EmitImmediatelyStrategy option. + WatchEmitsImmediately Feature + + // IntegrityData is enabled if the underlying datastore supports retrieving and storing + // integrity information. + IntegrityData Feature +} + +// ObjectTypeStat represents statistics for a single object type (namespace). +type ObjectTypeStat struct { + // NumRelations is the number of relations defined in a single object type. + NumRelations uint32 + + // NumPermissions is the number of permissions defined in a single object type. + NumPermissions uint32 +} + +// Stats represents statistics for the entire datastore. +type Stats struct { + // UniqueID is a unique string for a single datastore. + UniqueID string + + // EstimatedRelationshipCount is a best-guess estimate of the number of relationships + // in the datastore. Computing it should use a lightweight method such as reading + // table statistics. + EstimatedRelationshipCount uint64 + + // ObjectTypeStatistics returns a slice element for each object type (namespace) + // stored in the datastore. + ObjectTypeStatistics []ObjectTypeStat +} + +// RelationshipIterator is an iterator over matched tuples. It is a single use +// iterator. +type RelationshipIterator iter.Seq2[tuple.Relationship, error] + +func IteratorToSlice(iter RelationshipIterator) ([]tuple.Relationship, error) { + results := make([]tuple.Relationship, 0) + for rel, err := range iter { + if err != nil { + return nil, err + } + results = append(results, rel) + } + return results, nil +} + +// FirstRelationshipIn returns the first relationship found via the iterator, if any. +func FirstRelationshipIn(iter RelationshipIterator) (tuple.Relationship, bool, error) { + for rel, err := range iter { + if err != nil { + return tuple.Relationship{}, false, err + } + + return rel, true, nil + } + + return tuple.Relationship{}, false, nil +} + +// Revision is an interface for a comparable revision type that can be different for +// each datastore implementation. +type Revision interface { + fmt.Stringer + + // Equal returns whether the revisions should be considered equal. + Equal(Revision) bool + + // GreaterThan returns whether the receiver is probably greater than the right hand side. + GreaterThan(Revision) bool + + // LessThan returns whether the receiver is probably less than the right hand side. + LessThan(Revision) bool + + // ByteSortable returns true if the string representation of the Revision is byte sortable, false otherwise. + ByteSortable() bool +} + +type nilRevision struct{} + +func (nilRevision) ByteSortable() bool { + return false +} + +func (nilRevision) Equal(rhs Revision) bool { + return rhs == NoRevision +} + +func (nilRevision) GreaterThan(_ Revision) bool { + return false +} + +func (nilRevision) LessThan(_ Revision) bool { + return true +} + +func (nilRevision) String() string { + return "nil" +} + +// NoRevision is a zero type for the revision that will make changing the +// revision type in the future a bit easier if necessary. Implementations +// should use any time they want to signal an empty/error revision. +var NoRevision Revision = nilRevision{} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/doc.go b/vendor/github.com/authzed/spicedb/pkg/datastore/doc.go new file mode 100644 index 0000000..2ffd9e6 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/doc.go @@ -0,0 +1,2 @@ +// Package datastore contains interfaces and code common to all datastores. +package datastore diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/errors.go b/vendor/github.com/authzed/spicedb/pkg/datastore/errors.go new file mode 100644 index 0000000..84057d5 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/errors.go @@ -0,0 +1,281 @@ +package datastore + +import ( + "errors" + "fmt" + + "github.com/rs/zerolog" + + core "github.com/authzed/spicedb/pkg/proto/core/v1" +) + +// ErrNotFound is a shared interface for not found errors. +type ErrNotFound interface { + IsNotFoundError() bool +} + +// NamespaceNotFoundError occurs when a namespace was not found. +type NamespaceNotFoundError struct { + error + namespaceName string +} + +var _ ErrNotFound = NamespaceNotFoundError{} + +func (err NamespaceNotFoundError) IsNotFoundError() bool { + return true +} + +// NotFoundNamespaceName is the name of the namespace not found. +func (err NamespaceNotFoundError) NotFoundNamespaceName() string { + return err.namespaceName +} + +// MarshalZerologObject implements zerolog object marshalling. +func (err NamespaceNotFoundError) MarshalZerologObject(e *zerolog.Event) { + e.Err(err.error).Str("namespace", err.namespaceName) +} + +// DetailsMetadata returns the metadata for details for this error. +func (err NamespaceNotFoundError) DetailsMetadata() map[string]string { + return map[string]string{ + "definition_name": err.namespaceName, + } +} + +// WatchDisconnectedError occurs when a watch has fallen too far behind and was forcibly disconnected +// as a result. +type WatchDisconnectedError struct{ error } + +// WatchCanceledError occurs when a watch was canceled by the caller. +type WatchCanceledError struct{ error } + +// WatchDisabledError occurs when watch is disabled by being unsupported by the datastore. +type WatchDisabledError struct{ error } + +// ReadOnlyError is returned when the operation cannot be completed because the datastore is in +// read-only mode. +type ReadOnlyError struct{ error } + +// WatchRetryableError is returned when a transient/temporary error occurred in watch and indicates that +// the caller *may* retry the watch after some backoff time. +type WatchRetryableError struct{ error } + +// InvalidRevisionReason is the reason the revision could not be used. +type InvalidRevisionReason int + +const ( + // RevisionStale is the reason returned when a revision is outside the window of + // validity by being too old. + RevisionStale InvalidRevisionReason = iota + + // CouldNotDetermineRevision is the reason returned when a revision for a + // request could not be determined. + CouldNotDetermineRevision +) + +// InvalidRevisionError occurs when a revision specified to a call was invalid. +type InvalidRevisionError struct { + error + revision Revision + reason InvalidRevisionReason +} + +// InvalidRevision is the revision that failed. +func (err InvalidRevisionError) InvalidRevision() Revision { + return err.revision +} + +// Reason is the reason the revision failed. +func (err InvalidRevisionError) Reason() InvalidRevisionReason { + return err.reason +} + +// MarshalZerologObject implements zerolog object marshalling. +func (err InvalidRevisionError) MarshalZerologObject(e *zerolog.Event) { + switch err.reason { + case RevisionStale: + e.Err(err.error).Str("reason", "stale") + case CouldNotDetermineRevision: + e.Err(err.error).Str("reason", "indeterminate") + default: + e.Err(err.error).Str("reason", "unknown") + } +} + +// NewNamespaceNotFoundErr constructs a new namespace not found error. +func NewNamespaceNotFoundErr(nsName string) error { + return NamespaceNotFoundError{ + error: fmt.Errorf("object definition `%s` not found", nsName), + namespaceName: nsName, + } +} + +// NewWatchDisconnectedErr constructs a new watch was disconnected error. +func NewWatchDisconnectedErr() error { + return WatchDisconnectedError{ + error: fmt.Errorf("watch fell too far behind and was disconnected; consider increasing watch buffer size via the flag --datastore-watch-buffer-length"), + } +} + +// NewWatchCanceledErr constructs a new watch was canceled error. +func NewWatchCanceledErr() error { + return WatchCanceledError{ + error: fmt.Errorf("watch was canceled by the caller"), + } +} + +// NewWatchDisabledErr constructs a new watch is disabled error. +func NewWatchDisabledErr(reason string) error { + return WatchDisabledError{ + error: fmt.Errorf("watch is currently disabled: %s", reason), + } +} + +// NewWatchTemporaryErr wraps another error in watch, indicating that the error is likely +// a temporary condition and clients may consider retrying by calling watch again (vs a fatal error). +func NewWatchTemporaryErr(wrapped error) error { + return WatchRetryableError{ + error: fmt.Errorf("watch has failed with a temporary condition: %w. please retry the watch", wrapped), + } +} + +// NewReadonlyErr constructs an error for when a request has failed because +// the datastore has been configured to be read-only. +func NewReadonlyErr() error { + return ReadOnlyError{ + error: fmt.Errorf("datastore is in read-only mode"), + } +} + +// NewInvalidRevisionErr constructs a new invalid revision error. +func NewInvalidRevisionErr(revision Revision, reason InvalidRevisionReason) error { + switch reason { + case RevisionStale: + return InvalidRevisionError{ + error: fmt.Errorf("revision has expired"), + revision: revision, + reason: reason, + } + + default: + return InvalidRevisionError{ + error: fmt.Errorf("revision was invalid"), + revision: revision, + reason: reason, + } + } +} + +// CaveatNameNotFoundError is the error returned when a caveat is not found by its name +type CaveatNameNotFoundError struct { + error + name string +} + +var _ ErrNotFound = CaveatNameNotFoundError{} + +func (err CaveatNameNotFoundError) IsNotFoundError() bool { + return true +} + +// CaveatName returns the name of the caveat that couldn't be found +func (err CaveatNameNotFoundError) CaveatName() string { + return err.name +} + +// NewCaveatNameNotFoundErr constructs a new caveat name not found error. +func NewCaveatNameNotFoundErr(name string) error { + return CaveatNameNotFoundError{ + error: fmt.Errorf("caveat with name `%s` not found", name), + name: name, + } +} + +// DetailsMetadata returns the metadata for details for this error. +func (err CaveatNameNotFoundError) DetailsMetadata() map[string]string { + return map[string]string{ + "caveat_name": err.name, + } +} + +// CounterNotRegisteredError indicates that a counter was not registered. +type CounterNotRegisteredError struct { + error + counterName string +} + +// NewCounterNotRegisteredErr constructs a new counter not registered error. +func NewCounterNotRegisteredErr(counterName string) error { + return CounterNotRegisteredError{ + error: fmt.Errorf("counter with name `%s` not found", counterName), + counterName: counterName, + } +} + +// DetailsMetadata returns the metadata for details for this error. +func (err CounterNotRegisteredError) DetailsMetadata() map[string]string { + return map[string]string{ + "counter_name": err.counterName, + } +} + +// CounterAlreadyRegisteredError indicates that a counter was already registered. +type CounterAlreadyRegisteredError struct { + error + + counterName string + filter *core.RelationshipFilter +} + +// NewCounterAlreadyRegisteredErr constructs a new filter not registered error. +func NewCounterAlreadyRegisteredErr(counterName string, filter *core.RelationshipFilter) error { + return CounterAlreadyRegisteredError{ + error: fmt.Errorf("counter with name `%s` already registered", counterName), + counterName: counterName, + filter: filter, + } +} + +// DetailsMetadata returns the metadata for details for this error. +func (err CounterAlreadyRegisteredError) DetailsMetadata() map[string]string { + subjectType := "" + subjectID := "" + subjectRelation := "" + if err.filter.OptionalSubjectFilter != nil { + subjectType = err.filter.OptionalSubjectFilter.SubjectType + subjectID = err.filter.OptionalSubjectFilter.OptionalSubjectId + + if err.filter.OptionalSubjectFilter.GetOptionalRelation() != nil { + subjectRelation = err.filter.OptionalSubjectFilter.GetOptionalRelation().Relation + } + } + + return map[string]string{ + "counter_name": err.counterName, + "new_filter_resource_type": err.filter.ResourceType, + "new_filter_resource_id": err.filter.OptionalResourceId, + "new_filter_resource_id_prefix": err.filter.OptionalResourceIdPrefix, + "new_filter_relation": err.filter.OptionalRelation, + "new_filter_subject_type": subjectType, + "new_filter_subject_id": subjectID, + "new_filter_subject_relation": subjectRelation, + } +} + +// MaximumChangesSizeExceededError is returned when the maximum size of changes is exceeded. +type MaximumChangesSizeExceededError struct { + error + maxSize uint64 +} + +// NewMaximumChangesSizeExceededError creates a new MaximumChangesSizeExceededError. +func NewMaximumChangesSizeExceededError(maxSize uint64) error { + return MaximumChangesSizeExceededError{fmt.Errorf("maximum changes byte size of %d exceeded", maxSize), maxSize} +} + +var ( + ErrClosedIterator = errors.New("unable to iterate: iterator closed") + ErrCursorsWithoutSorting = errors.New("cursors are disabled on unsorted results") + ErrCursorEmpty = errors.New("cursors are only available after the first result") +) diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/options/options.go b/vendor/github.com/authzed/spicedb/pkg/datastore/options/options.go new file mode 100644 index 0000000..18477f8 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/options/options.go @@ -0,0 +1,123 @@ +package options + +import ( + "context" + + "google.golang.org/protobuf/types/known/structpb" + + "github.com/authzed/spicedb/pkg/datastore/queryshape" + "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/tuple" +) + +//go:generate go run github.com/ecordell/optgen -output zz_generated.query_options.go . QueryOptions ReverseQueryOptions RWTOptions +//go:generate go run github.com/ecordell/optgen -output zz_generated.delete_options.go . DeleteOptions + +// SortOrder is an enum which represents the order in which the caller would like +// the data returned. +type SortOrder int8 + +const ( + // Unsorted lets the underlying datastore choose the order, or no order at all + Unsorted SortOrder = iota + + // ByResource sorts the relationships by the resource component first + ByResource + + // BySubject sorts the relationships by the subject component first. Note that + // BySubject might be quite a bit slower than ByResource, as relationships are + // indexed by resource. + BySubject +) + +type Cursor *tuple.Relationship + +func ToCursor(r tuple.Relationship) Cursor { + spiceerrors.DebugAssert(r.ValidateNotEmpty, "cannot create cursor from empty relationship") + return Cursor(&r) +} + +func ToRelationship(c Cursor) *tuple.Relationship { + if c == nil { + return nil + } + return (*tuple.Relationship)(c) +} + +// SQLCheckAssertionForTest is a function that can be used to assert a condition on the SQL query string. +// Assertions will only be run during testing and only apply in datastores that support SQL. +type SQLCheckAssertionForTest func(sql string) + +// SQLIndexInformation holds the expected index names for a SQL query. +type SQLIndexInformation struct { + // ExpectedIndexNames are the name(s) of the index(es) that are expected to be used by this + // SQL query. + ExpectedIndexNames []string +} + +// SQLExplainCallbackForTest is a callback invoked with the explain plan of the SQL query string. +type SQLExplainCallbackForTest func(ctx context.Context, sql string, args []any, shape queryshape.Shape, explain string, expectedIndexes SQLIndexInformation) error + +// QueryOptions are the options that can affect the results of a normal forward query. +type QueryOptions struct { + Limit *uint64 `debugmap:"visible"` + Sort SortOrder `debugmap:"visible"` + After Cursor `debugmap:"visible"` + SkipCaveats bool `debugmap:"visible"` + SkipExpiration bool `debugmap:"visible"` + + // SQLCheckAssertionForTest is a function that can be used to assert a condition on the SQL query string. + // For testing and validation only. + SQLCheckAssertionForTest SQLCheckAssertionForTest `debugmap:"visible"` + + // SQLExplainCallbackForTest is a callback invoked with the explain plan of the SQL query string. + // For testing and validation only. + SQLExplainCallbackForTest SQLExplainCallbackForTest `debugmap:"visible"` + + // QueryShape is the marked shape of the query. + // For testing and validation only. + QueryShape queryshape.Shape `debugmap:"visible"` +} + +// ReverseQueryOptions are the options that can affect the results of a reverse query. +type ReverseQueryOptions struct { + ResRelation *ResourceRelation `debugmap:"visible"` + + LimitForReverse *uint64 `debugmap:"visible"` + SortForReverse SortOrder `debugmap:"visible"` + AfterForReverse Cursor `debugmap:"visible"` + + // SQLExplainCallbackForTestForReverse is a callback invoked with the explain plan of the SQL query string. + // For testing and validation only. + SQLExplainCallbackForTestForReverse SQLExplainCallbackForTest `debugmap:"visible"` + + // QueryShapeForReverse is the marked shape of the reverse query. + // For testing and validation only. + QueryShapeForReverse queryshape.Shape `debugmap:"visible"` +} + +// ResourceRelation combines a resource object type and relation. +type ResourceRelation struct { + Namespace string + Relation string +} + +// RWTOptions are options that can affect the way a read-write transaction is +// executed. +type RWTOptions struct { + DisableRetries bool `debugmap:"visible"` + Metadata *structpb.Struct `debugmap:"visible"` +} + +// DeleteOptions are the options that can affect the results of a delete relationships +// operation. +type DeleteOptions struct { + DeleteLimit *uint64 `debugmap:"visible"` +} + +var ( + one = uint64(1) + + // LimitOne is a constant *uint64 that can be used with WithLimit requests. + LimitOne = &one +) diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.delete_options.go b/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.delete_options.go new file mode 100644 index 0000000..839c9ad --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.delete_options.go @@ -0,0 +1,65 @@ +// Code generated by github.com/ecordell/optgen. DO NOT EDIT. +package options + +import ( + defaults "github.com/creasty/defaults" + helpers "github.com/ecordell/optgen/helpers" +) + +type DeleteOptionsOption func(d *DeleteOptions) + +// NewDeleteOptionsWithOptions creates a new DeleteOptions with the passed in options set +func NewDeleteOptionsWithOptions(opts ...DeleteOptionsOption) *DeleteOptions { + d := &DeleteOptions{} + for _, o := range opts { + o(d) + } + return d +} + +// NewDeleteOptionsWithOptionsAndDefaults creates a new DeleteOptions with the passed in options set starting from the defaults +func NewDeleteOptionsWithOptionsAndDefaults(opts ...DeleteOptionsOption) *DeleteOptions { + d := &DeleteOptions{} + defaults.MustSet(d) + for _, o := range opts { + o(d) + } + return d +} + +// ToOption returns a new DeleteOptionsOption that sets the values from the passed in DeleteOptions +func (d *DeleteOptions) ToOption() DeleteOptionsOption { + return func(to *DeleteOptions) { + to.DeleteLimit = d.DeleteLimit + } +} + +// DebugMap returns a map form of DeleteOptions for debugging +func (d DeleteOptions) DebugMap() map[string]any { + debugMap := map[string]any{} + debugMap["DeleteLimit"] = helpers.DebugValue(d.DeleteLimit, false) + return debugMap +} + +// DeleteOptionsWithOptions configures an existing DeleteOptions with the passed in options set +func DeleteOptionsWithOptions(d *DeleteOptions, opts ...DeleteOptionsOption) *DeleteOptions { + for _, o := range opts { + o(d) + } + return d +} + +// WithOptions configures the receiver DeleteOptions with the passed in options set +func (d *DeleteOptions) WithOptions(opts ...DeleteOptionsOption) *DeleteOptions { + for _, o := range opts { + o(d) + } + return d +} + +// WithDeleteLimit returns an option that can set DeleteLimit on a DeleteOptions +func WithDeleteLimit(deleteLimit *uint64) DeleteOptionsOption { + return func(d *DeleteOptions) { + d.DeleteLimit = deleteLimit + } +} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.query_options.go b/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.query_options.go new file mode 100644 index 0000000..518db5c --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/options/zz_generated.query_options.go @@ -0,0 +1,300 @@ +// Code generated by github.com/ecordell/optgen. DO NOT EDIT. +package options + +import ( + queryshape "github.com/authzed/spicedb/pkg/datastore/queryshape" + defaults "github.com/creasty/defaults" + helpers "github.com/ecordell/optgen/helpers" + structpb "google.golang.org/protobuf/types/known/structpb" +) + +type QueryOptionsOption func(q *QueryOptions) + +// NewQueryOptionsWithOptions creates a new QueryOptions with the passed in options set +func NewQueryOptionsWithOptions(opts ...QueryOptionsOption) *QueryOptions { + q := &QueryOptions{} + for _, o := range opts { + o(q) + } + return q +} + +// NewQueryOptionsWithOptionsAndDefaults creates a new QueryOptions with the passed in options set starting from the defaults +func NewQueryOptionsWithOptionsAndDefaults(opts ...QueryOptionsOption) *QueryOptions { + q := &QueryOptions{} + defaults.MustSet(q) + for _, o := range opts { + o(q) + } + return q +} + +// ToOption returns a new QueryOptionsOption that sets the values from the passed in QueryOptions +func (q *QueryOptions) ToOption() QueryOptionsOption { + return func(to *QueryOptions) { + to.Limit = q.Limit + to.Sort = q.Sort + to.After = q.After + to.SkipCaveats = q.SkipCaveats + to.SkipExpiration = q.SkipExpiration + to.SQLCheckAssertionForTest = q.SQLCheckAssertionForTest + to.SQLExplainCallbackForTest = q.SQLExplainCallbackForTest + to.QueryShape = q.QueryShape + } +} + +// DebugMap returns a map form of QueryOptions for debugging +func (q QueryOptions) DebugMap() map[string]any { + debugMap := map[string]any{} + debugMap["Limit"] = helpers.DebugValue(q.Limit, false) + debugMap["Sort"] = helpers.DebugValue(q.Sort, false) + debugMap["After"] = helpers.DebugValue(q.After, false) + debugMap["SkipCaveats"] = helpers.DebugValue(q.SkipCaveats, false) + debugMap["SkipExpiration"] = helpers.DebugValue(q.SkipExpiration, false) + debugMap["SQLCheckAssertionForTest"] = helpers.DebugValue(q.SQLCheckAssertionForTest, false) + debugMap["SQLExplainCallbackForTest"] = helpers.DebugValue(q.SQLExplainCallbackForTest, false) + debugMap["QueryShape"] = helpers.DebugValue(q.QueryShape, false) + return debugMap +} + +// QueryOptionsWithOptions configures an existing QueryOptions with the passed in options set +func QueryOptionsWithOptions(q *QueryOptions, opts ...QueryOptionsOption) *QueryOptions { + for _, o := range opts { + o(q) + } + return q +} + +// WithOptions configures the receiver QueryOptions with the passed in options set +func (q *QueryOptions) WithOptions(opts ...QueryOptionsOption) *QueryOptions { + for _, o := range opts { + o(q) + } + return q +} + +// WithLimit returns an option that can set Limit on a QueryOptions +func WithLimit(limit *uint64) QueryOptionsOption { + return func(q *QueryOptions) { + q.Limit = limit + } +} + +// WithSort returns an option that can set Sort on a QueryOptions +func WithSort(sort SortOrder) QueryOptionsOption { + return func(q *QueryOptions) { + q.Sort = sort + } +} + +// WithAfter returns an option that can set After on a QueryOptions +func WithAfter(after Cursor) QueryOptionsOption { + return func(q *QueryOptions) { + q.After = after + } +} + +// WithSkipCaveats returns an option that can set SkipCaveats on a QueryOptions +func WithSkipCaveats(skipCaveats bool) QueryOptionsOption { + return func(q *QueryOptions) { + q.SkipCaveats = skipCaveats + } +} + +// WithSkipExpiration returns an option that can set SkipExpiration on a QueryOptions +func WithSkipExpiration(skipExpiration bool) QueryOptionsOption { + return func(q *QueryOptions) { + q.SkipExpiration = skipExpiration + } +} + +// WithSQLCheckAssertionForTest returns an option that can set SQLCheckAssertionForTest on a QueryOptions +func WithSQLCheckAssertionForTest(sQLCheckAssertionForTest SQLCheckAssertionForTest) QueryOptionsOption { + return func(q *QueryOptions) { + q.SQLCheckAssertionForTest = sQLCheckAssertionForTest + } +} + +// WithSQLExplainCallbackForTest returns an option that can set SQLExplainCallbackForTest on a QueryOptions +func WithSQLExplainCallbackForTest(sQLExplainCallbackForTest SQLExplainCallbackForTest) QueryOptionsOption { + return func(q *QueryOptions) { + q.SQLExplainCallbackForTest = sQLExplainCallbackForTest + } +} + +// WithQueryShape returns an option that can set QueryShape on a QueryOptions +func WithQueryShape(queryShape queryshape.Shape) QueryOptionsOption { + return func(q *QueryOptions) { + q.QueryShape = queryShape + } +} + +type ReverseQueryOptionsOption func(r *ReverseQueryOptions) + +// NewReverseQueryOptionsWithOptions creates a new ReverseQueryOptions with the passed in options set +func NewReverseQueryOptionsWithOptions(opts ...ReverseQueryOptionsOption) *ReverseQueryOptions { + r := &ReverseQueryOptions{} + for _, o := range opts { + o(r) + } + return r +} + +// NewReverseQueryOptionsWithOptionsAndDefaults creates a new ReverseQueryOptions with the passed in options set starting from the defaults +func NewReverseQueryOptionsWithOptionsAndDefaults(opts ...ReverseQueryOptionsOption) *ReverseQueryOptions { + r := &ReverseQueryOptions{} + defaults.MustSet(r) + for _, o := range opts { + o(r) + } + return r +} + +// ToOption returns a new ReverseQueryOptionsOption that sets the values from the passed in ReverseQueryOptions +func (r *ReverseQueryOptions) ToOption() ReverseQueryOptionsOption { + return func(to *ReverseQueryOptions) { + to.ResRelation = r.ResRelation + to.LimitForReverse = r.LimitForReverse + to.SortForReverse = r.SortForReverse + to.AfterForReverse = r.AfterForReverse + to.SQLExplainCallbackForTestForReverse = r.SQLExplainCallbackForTestForReverse + to.QueryShapeForReverse = r.QueryShapeForReverse + } +} + +// DebugMap returns a map form of ReverseQueryOptions for debugging +func (r ReverseQueryOptions) DebugMap() map[string]any { + debugMap := map[string]any{} + debugMap["ResRelation"] = helpers.DebugValue(r.ResRelation, false) + debugMap["LimitForReverse"] = helpers.DebugValue(r.LimitForReverse, false) + debugMap["SortForReverse"] = helpers.DebugValue(r.SortForReverse, false) + debugMap["AfterForReverse"] = helpers.DebugValue(r.AfterForReverse, false) + debugMap["SQLExplainCallbackForTestForReverse"] = helpers.DebugValue(r.SQLExplainCallbackForTestForReverse, false) + debugMap["QueryShapeForReverse"] = helpers.DebugValue(r.QueryShapeForReverse, false) + return debugMap +} + +// ReverseQueryOptionsWithOptions configures an existing ReverseQueryOptions with the passed in options set +func ReverseQueryOptionsWithOptions(r *ReverseQueryOptions, opts ...ReverseQueryOptionsOption) *ReverseQueryOptions { + for _, o := range opts { + o(r) + } + return r +} + +// WithOptions configures the receiver ReverseQueryOptions with the passed in options set +func (r *ReverseQueryOptions) WithOptions(opts ...ReverseQueryOptionsOption) *ReverseQueryOptions { + for _, o := range opts { + o(r) + } + return r +} + +// WithResRelation returns an option that can set ResRelation on a ReverseQueryOptions +func WithResRelation(resRelation *ResourceRelation) ReverseQueryOptionsOption { + return func(r *ReverseQueryOptions) { + r.ResRelation = resRelation + } +} + +// WithLimitForReverse returns an option that can set LimitForReverse on a ReverseQueryOptions +func WithLimitForReverse(limitForReverse *uint64) ReverseQueryOptionsOption { + return func(r *ReverseQueryOptions) { + r.LimitForReverse = limitForReverse + } +} + +// WithSortForReverse returns an option that can set SortForReverse on a ReverseQueryOptions +func WithSortForReverse(sortForReverse SortOrder) ReverseQueryOptionsOption { + return func(r *ReverseQueryOptions) { + r.SortForReverse = sortForReverse + } +} + +// WithAfterForReverse returns an option that can set AfterForReverse on a ReverseQueryOptions +func WithAfterForReverse(afterForReverse Cursor) ReverseQueryOptionsOption { + return func(r *ReverseQueryOptions) { + r.AfterForReverse = afterForReverse + } +} + +// WithSQLExplainCallbackForTestForReverse returns an option that can set SQLExplainCallbackForTestForReverse on a ReverseQueryOptions +func WithSQLExplainCallbackForTestForReverse(sQLExplainCallbackForTestForReverse SQLExplainCallbackForTest) ReverseQueryOptionsOption { + return func(r *ReverseQueryOptions) { + r.SQLExplainCallbackForTestForReverse = sQLExplainCallbackForTestForReverse + } +} + +// WithQueryShapeForReverse returns an option that can set QueryShapeForReverse on a ReverseQueryOptions +func WithQueryShapeForReverse(queryShapeForReverse queryshape.Shape) ReverseQueryOptionsOption { + return func(r *ReverseQueryOptions) { + r.QueryShapeForReverse = queryShapeForReverse + } +} + +type RWTOptionsOption func(r *RWTOptions) + +// NewRWTOptionsWithOptions creates a new RWTOptions with the passed in options set +func NewRWTOptionsWithOptions(opts ...RWTOptionsOption) *RWTOptions { + r := &RWTOptions{} + for _, o := range opts { + o(r) + } + return r +} + +// NewRWTOptionsWithOptionsAndDefaults creates a new RWTOptions with the passed in options set starting from the defaults +func NewRWTOptionsWithOptionsAndDefaults(opts ...RWTOptionsOption) *RWTOptions { + r := &RWTOptions{} + defaults.MustSet(r) + for _, o := range opts { + o(r) + } + return r +} + +// ToOption returns a new RWTOptionsOption that sets the values from the passed in RWTOptions +func (r *RWTOptions) ToOption() RWTOptionsOption { + return func(to *RWTOptions) { + to.DisableRetries = r.DisableRetries + to.Metadata = r.Metadata + } +} + +// DebugMap returns a map form of RWTOptions for debugging +func (r RWTOptions) DebugMap() map[string]any { + debugMap := map[string]any{} + debugMap["DisableRetries"] = helpers.DebugValue(r.DisableRetries, false) + debugMap["Metadata"] = helpers.DebugValue(r.Metadata, false) + return debugMap +} + +// RWTOptionsWithOptions configures an existing RWTOptions with the passed in options set +func RWTOptionsWithOptions(r *RWTOptions, opts ...RWTOptionsOption) *RWTOptions { + for _, o := range opts { + o(r) + } + return r +} + +// WithOptions configures the receiver RWTOptions with the passed in options set +func (r *RWTOptions) WithOptions(opts ...RWTOptionsOption) *RWTOptions { + for _, o := range opts { + o(r) + } + return r +} + +// WithDisableRetries returns an option that can set DisableRetries on a RWTOptions +func WithDisableRetries(disableRetries bool) RWTOptionsOption { + return func(r *RWTOptions) { + r.DisableRetries = disableRetries + } +} + +// WithMetadata returns an option that can set Metadata on a RWTOptions +func WithMetadata(metadata *structpb.Struct) RWTOptionsOption { + return func(r *RWTOptions) { + r.Metadata = metadata + } +} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/pagination/iterator.go b/vendor/github.com/authzed/spicedb/pkg/datastore/pagination/iterator.go new file mode 100644 index 0000000..524e52e --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/pagination/iterator.go @@ -0,0 +1,70 @@ +package pagination + +import ( + "context" + + "github.com/authzed/spicedb/pkg/datastore" + "github.com/authzed/spicedb/pkg/datastore/options" + "github.com/authzed/spicedb/pkg/datastore/queryshape" + "github.com/authzed/spicedb/pkg/tuple" +) + +// NewPaginatedIterator creates an implementation of the datastore.Iterator +// interface that internally paginates over datastore results. +func NewPaginatedIterator( + ctx context.Context, + reader datastore.Reader, + filter datastore.RelationshipsFilter, + pageSize uint64, + order options.SortOrder, + startCursor options.Cursor, + queryShape queryshape.Shape, +) (datastore.RelationshipIterator, error) { + iter, err := reader.QueryRelationships( + ctx, + filter, + options.WithSort(order), + options.WithLimit(&pageSize), + options.WithAfter(startCursor), + options.WithQueryShape(queryShape), + ) + if err != nil { + return nil, err + } + + return func(yield func(tuple.Relationship, error) bool) { + cursor := startCursor + for { + var counter uint64 + for rel, err := range iter { + if !yield(rel, err) { + return + } + + cursor = options.ToCursor(rel) + counter++ + + if counter >= pageSize { + break + } + } + + if counter < pageSize { + return + } + + iter, err = reader.QueryRelationships( + ctx, + filter, + options.WithSort(order), + options.WithLimit(&pageSize), + options.WithAfter(cursor), + options.WithQueryShape(queryShape), + ) + if err != nil { + yield(tuple.Relationship{}, err) + return + } + } + }, nil +} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/queryshape/queryshape.go b/vendor/github.com/authzed/spicedb/pkg/datastore/queryshape/queryshape.go new file mode 100644 index 0000000..4ff1e0f --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/queryshape/queryshape.go @@ -0,0 +1,109 @@ +package queryshape + +// Shape represents the different ways a query can be shaped. +type Shape string + +// Symbol guide: +// *️⃣ - optional +// ✅ - required +// 🆔 - has some sort of filter +// 🅿️ - possibly specified + +const ( + // Unspecified indicates that the shape is not specified. + Unspecified Shape = "unspecified" + + // Varying indicates that the shape can vary. This is used + // for queries whose shape is not known ahead of time. + // + // *️⃣ resource_type, *️⃣ resource_id, *️⃣ resource_relation, *️⃣ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration + Varying = "varying" + + // CheckPermissionSelectDirectSubjects indicates that the query is a permission check + // that selects direct subjects. + // + // The query shape selects a specific relationship based on filling in *all* of it + // relationship fields (except the caveat name, context and expiration). + // + // ✅ resource_type, ✅ resource_id, ✅ resource_relation, ✅ subject_type, ✅ subject_id, ✅ subject_relation, *️⃣ caveat, *️⃣ expiration + CheckPermissionSelectDirectSubjects = "check-permission-select-direct-subjects" + + // CheckPermissionSelectIndirectSubjects indicates that the query is a permission check + // that selects indirect subjects. + // + // The query shape selects a specific relationship based on filling in all fields + // on the resource (except the caveat name, context and expiration) and the relation + // name. The subject type nor ID is filled in and the optional subject relation is + // set to match non-`...`. + // + // ✅ resource_type, ✅ resource_id, ✅ resource_relation, *️⃣ subject_type, *️⃣ subject_id, 🆔 subject_relation, *️⃣ caveat, *️⃣ expiration + CheckPermissionSelectIndirectSubjects = "check-permission-select-indirect-subjects" + + // AllSubjectsForResources indicates that the query is selecting all subjects for a + // given set of resources. + // + // The query shape selects all subjects for a given set of resources, which are fully + // specified by providing the resource type, the resource ID(s) and the relation. + // + // ✅ resource_type, ✅ resource_id, ✅ resource_relation, *️⃣ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration + AllSubjectsForResources = "all-subjects-for-resources" + + // MatchingResourcesForSubject indicates that the query is selecting all resources that + // match a given subject. + // + // The query shape selects all resources that match a given subject, which is specified + // by providing the subject type, the subject ID and (optionally) the subject relation. + // The resource type and relation are filled in, but the resource ID is never specified. + // + // ✅ resource_type, *️⃣ resource_id, ✅ resource_relation, ✅ subject_type, ✅ subject_id, 🅿️ subject_relation, *️⃣ caveat, *️⃣ expiration + MatchingResourcesForSubject = "matching-resources-for-subject" + + // FindResourceOfType indicates that the query is selecting a resource of + // a given type. + // + // The query shape selects a resource of a given type, which is specified by + // providing the resource type. The other fields are never specified. + // + // ✅ resource_type, *️⃣ resource_id, *️⃣ resource_relation, *️⃣ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration + FindResourceOfType = "find-resource-of-type" + + // FindSubjectOfType indicates that the query is selecting a subject of + // a given type. + // + // The query shape selects a subject of a given type, which is specified by + // providing the subject type. The other fields are never specified. + // + // *️⃣ resource_type, *️⃣ resource_id, *️⃣ resource_relation, ✅ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration + FindSubjectOfType = "find-subject-of-type" + + // FindResourceOfTypeAndRelation indicates that the query is selecting a single + // resource of a given type and relation. + // + // The query shape selects a resource of a given type and relation, which are + // specified by providing the resource type and relation. The other fields are never + // specified. + // + // ✅ resource_type, *️⃣ resource_id, ✅ resource_relation, *️⃣ subject_type, *️⃣ subject_id, *️⃣ subject_relation, *️⃣ caveat, *️⃣ expiration + FindResourceOfTypeAndRelation = "find-resource-of-type-and-relation" + + // FindSubjectOfTypeAndRelation indicates that the query is selecting a single + // subject of a given type and relation. + // + // The query shape selects a subject of a given type and relation, which are + // specified by providing the subject type and relation. The other fields are never + // specified. + // + // *️⃣ resource_type, *️⃣ resource_id, *️⃣ resource_relation, ✅ subject_type, *️⃣ subject_id, ✅ subject_relation, *️⃣ caveat, *️⃣ expiration + FindSubjectOfTypeAndRelation = "find-subject-of-type-and-relation" + + // FindResourceRelationForSubjectRelation indicates that the query is selecting a single + // relationship type that matches a given relation type, i.e. `user` or + // `group#member with somecaveat and expiration`. + // + // The query shape selects an allowed subject type for a specific relation on a specific + // resource type. All fields except resource ID are specified here, with subject ID only + // specified if a wildcard. + // + // ✅ resource_type, *️⃣ resource_id, ✅ resource_relation, ✅ subject_type, 🅿️ subject_id, ✅ subject_relation, *️⃣ caveat, *️⃣ expiration + FindResourceRelationForSubjectRelation = "find-resource-relation-for-subject-relation" +) diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/relationshipquerytree.go b/vendor/github.com/authzed/spicedb/pkg/datastore/relationshipquerytree.go new file mode 100644 index 0000000..2163f07 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/relationshipquerytree.go @@ -0,0 +1,23 @@ +package datastore + +type RelationshipQueryOperation int + +const ( + RelationshipQueryNone RelationshipQueryOperation = 0 + RelationshipQueryOr RelationshipQueryOperation = 1 + RelationshipQueryAnd RelationshipQueryOperation = 2 +) + +type RelationshipsQueryTree struct { + op RelationshipQueryOperation + filter RelationshipsFilter + children []RelationshipsQueryTree +} + +func NewRelationshipQueryTree(filter RelationshipsFilter) RelationshipsQueryTree { + return RelationshipsQueryTree{ + op: RelationshipQueryNone, + filter: filter, + children: nil, + } +} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/stats.go b/vendor/github.com/authzed/spicedb/pkg/datastore/stats.go new file mode 100644 index 0000000..f98c405 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/stats.go @@ -0,0 +1,31 @@ +package datastore + +import ( + "github.com/authzed/spicedb/pkg/namespace" + iv1 "github.com/authzed/spicedb/pkg/proto/impl/v1" +) + +// ComputeObjectTypeStats creates a list of object type stats from an input list of +// parsed object types. +func ComputeObjectTypeStats(objTypes []RevisionedNamespace) []ObjectTypeStat { + stats := make([]ObjectTypeStat, 0, len(objTypes)) + + for _, objType := range objTypes { + var relations, permissions uint32 + + for _, rel := range objType.Definition.Relation { + if namespace.GetRelationKind(rel) == iv1.RelationMetadata_PERMISSION { + permissions++ + } else { + relations++ + } + } + + stats = append(stats, ObjectTypeStat{ + NumRelations: relations, + NumPermissions: permissions, + }) + } + + return stats +} diff --git a/vendor/github.com/authzed/spicedb/pkg/datastore/util.go b/vendor/github.com/authzed/spicedb/pkg/datastore/util.go new file mode 100644 index 0000000..55bb8f2 --- /dev/null +++ b/vendor/github.com/authzed/spicedb/pkg/datastore/util.go @@ -0,0 +1,63 @@ +package datastore + +import ( + "context" + + v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" +) + +// DefinitionsOf returns just the schema definitions found in the list of revisioned +// definitions. +func DefinitionsOf[T SchemaDefinition](revisionedDefinitions []RevisionedDefinition[T]) []T { + definitions := make([]T, 0, len(revisionedDefinitions)) + for _, revDef := range revisionedDefinitions { + definitions = append(definitions, revDef.Definition) + } + return definitions +} + +// DeleteAllData deletes all data from the datastore. Should only be used when explicitly requested. +// The data is transactionally deleted, which means it may time out. +func DeleteAllData(ctx context.Context, ds Datastore) error { + _, err := ds.ReadWriteTx(ctx, func(ctx context.Context, rwt ReadWriteTransaction) error { + nsDefs, err := rwt.ListAllNamespaces(ctx) + if err != nil { + return err + } + + // Delete all relationships. + namespaceNames := make([]string, 0, len(nsDefs)) + for _, nsDef := range nsDefs { + _, _, err = rwt.DeleteRelationships(ctx, &v1.RelationshipFilter{ + ResourceType: nsDef.Definition.Name, + }) + if err != nil { + return err + } + namespaceNames = append(namespaceNames, nsDef.Definition.Name) + } + + // Delete all caveats. + caveatDefs, err := rwt.ListAllCaveats(ctx) + if err != nil { + return err + } + + caveatNames := make([]string, 0, len(caveatDefs)) + for _, caveatDef := range caveatDefs { + caveatNames = append(caveatNames, caveatDef.Definition.Name) + } + + if err := rwt.DeleteCaveats(ctx, caveatNames); err != nil { + return err + } + + // Delete all namespaces. + if err := rwt.DeleteNamespaces(ctx, namespaceNames...); err != nil { + return err + } + + return nil + }) + return err +} |
