// Copyright © 2023 Ory Corp // SPDX-License-Identifier: Apache-2.0 package relationtuple import ( "context" "sync" "github.com/gofrs/uuid" keysetpagination "github.com/ory/x/pagination/keysetpagination_v2" rts "github.com/ory/keto/gen/go/ory/keto/relation_tuples/v1alpha2" "github.com/ory/keto/ketoapi" ) type ( ManagerProvider interface { RelationTupleManager() Manager } // SubjectSetType identifies a subject-set pointer by its target namespace and // relation, as declared in OPL. AllowsDirect indicates that OPL permits a // direct subject match for this type. SubjectSetType struct { Namespace string Relation string AllowsDirect bool } Manager interface { GetRelationTuples(ctx context.Context, query *RelationQuery, options ...keysetpagination.Option) ([]*RelationTuple, *keysetpagination.Paginator, error) ExistsRelationTuples(ctx context.Context, query *RelationQuery) (bool, error) WriteRelationTuples(ctx context.Context, rs ...*RelationTuple) error DeleteRelationTuples(ctx context.Context, rs ...*RelationTuple) error DeleteAllRelationTuples(ctx context.Context, query *RelationQuery) error TransactRelationTuples(ctx context.Context, insert []*RelationTuple, delete []*RelationTuple) error // TraverseSubjectSetExpansion gets all subject sets for the tuple's // object#relation and checks whether the tuple's subject is a member of // each. When allowedSubjectSets is non-empty, only subject-set pointers // matching the declared (namespace, relation) pairs are considered. TraverseSubjectSetExpansion(ctx context.Context, tuple *RelationTuple, allowedSubjectSets []SubjectSetType) ([]*TraversalResult, error) // FindTupleWithRelations returns the first tuple that matches the given // tuple on namespace, object, and subject and has any of the given // relations, or nil if there is none. FindTupleWithRelations(ctx context.Context, tuple *RelationTuple, relations []string) (*RelationTuple, error) // Transaction runs f inside a database transaction. Nested calls reuse // the outer transaction. Transaction(ctx context.Context, f func(ctx context.Context) error) error // WithLatestSnapshot runs f inside a snapshot-isolated read transaction, // ensuring all SQL queries within f see a consistent database state. WithLatestSnapshot(ctx context.Context, f func(ctx context.Context) error) error } SubjectID struct { ID uuid.UUID `json:"id"` } RelationQuery struct { Namespace *string `json:"namespace"` Object *uuid.UUID `json:"object"` Relation *string `json:"relation"` Subject Subject `json:"subject_id,omitempty"` } TupleData interface { GetSubject() *rts.Subject GetObject() string GetNamespace() string GetRelation() string } Subject interface { Equals(Subject) bool UniqueID() uuid.UUID String() string } RelationTuple struct { Namespace string `json:"namespace"` Object uuid.UUID `json:"object"` Relation string `json:"relation"` Subject Subject `json:"subject"` } InternalRelationTuples []*RelationTuple SubjectSet struct { Namespace string `json:"namespace"` Object uuid.UUID `json:"object"` Relation string `json:"relation"` } // TODO(hperl): Also use a ketoapi.Tree here. Tree struct { Type ketoapi.TreeNodeType `json:"type"` Subject Subject `json:"subject"` Children []*Tree `json:"children,omitempty"` Truncation *Truncation `json:"truncation,omitempty"` } Truncation struct { Reason TruncationReason `json:"reason,omitempty"` Cursor *ExpandCursor `json:"cursor,omitempty"` } TruncationReason string ExpandCursorKind string ExpandCursor struct { Kind ExpandCursorKind `json:"kind"` SubjectSet *SubjectSet `json:"subject_set,omitempty"` NextPageToken keysetpagination.PageToken `json:"next_page_token,omitempty"` // TraverseRelation is the ComputedRelation in TupleToSubjectSet relation TraverseRelation *string `json:"traverse_relation,omitempty"` } ) const ( ExpandCursorKindDirect ExpandCursorKind = "direct" ExpandCursorKindTTU ExpandCursorKind = "ttu" ) type ( TraversalResult struct { From *RelationTuple To *RelationTuple Via Traversal Found bool } Traversal string ) const ( TraversalUnknown Traversal = "unknown" TraversalSubjectSetExpand Traversal = "subject set expand" TraversalComputedUserset Traversal = "computed userset" TraversalTupleToUserset Traversal = "tuple to userset" ) const ( TruncationReasonDepthLimit TruncationReason = "depth_limit" TruncationReasonTupleLimit TruncationReason = "tuple_limit" TruncationReasonCycle TruncationReason = "cycle" ) var _, _ Subject = (*SubjectID)(nil), (*SubjectSet)(nil) func (s *SubjectID) Equals(other Subject) bool { uv, ok := other.(*SubjectID) if !ok { return false } return uv.ID == s.ID } func (s *SubjectID) UniqueID() uuid.UUID { return s.ID } func (s *SubjectID) String() string { return s.ID.String() } func (s *SubjectSet) Equals(other Subject) bool { uv, ok := other.(*SubjectSet) if !ok { return false } return uv.Relation == s.Relation && uv.Object == s.Object && uv.Namespace == s.Namespace } func (s *SubjectSet) UniqueID() uuid.UUID { return uuid.NewV5(s.Object, s.Namespace+"-"+s.Relation) } func (s *SubjectSet) String() string { if s == nil { return "" } return s.Namespace + ":" + s.Object.String() + "#" + s.Relation } func (t *RelationTuple) ToQuery() *RelationQuery { return &RelationQuery{ Namespace: &t.Namespace, Object: &t.Object, Relation: &t.Relation, Subject: t.Subject, } } func (t *RelationTuple) String() string { if t == nil { return "" } subject := "" if t.Subject != nil { subject = t.Subject.String() } return t.Namespace + ":" + t.Object.String() + "#" + t.Relation + "@" + subject } func (t *RelationTuple) FromProto(proto *rts.RelationTuple) *RelationTuple { // TODO(hperl) return t } func (t *RelationTuple) ToProto() *rts.RelationTuple { // TODO(hperl) return &rts.RelationTuple{} } type ManagerWrapper struct { Reg ManagerProvider PageOpts []keysetpagination.Option RequestedPages []keysetpagination.PageToken // lock is necessary so that GetRelationTuples() is safe for concurrency. requestedPagesLock sync.Mutex } var ( _ Manager = (*ManagerWrapper)(nil) _ ManagerProvider = (*ManagerWrapper)(nil) ) func NewManagerWrapper(_ any, reg ManagerProvider, options ...keysetpagination.Option) *ManagerWrapper { return &ManagerWrapper{ Reg: reg, PageOpts: options, } } func (t *ManagerWrapper) GetRelationTuples(ctx context.Context, query *RelationQuery, options ...keysetpagination.Option) ([]*RelationTuple, *keysetpagination.Paginator, error) { p, err := keysetpagination.NewPaginator(options...) if err != nil { return nil, nil, err } t.requestedPagesLock.Lock() defer t.requestedPagesLock.Unlock() t.RequestedPages = append(t.RequestedPages, p.PageToken()) return t.Reg.RelationTupleManager().GetRelationTuples(ctx, query, append(t.PageOpts, options...)...) } func (t *ManagerWrapper) ExistsRelationTuples(ctx context.Context, query *RelationQuery) (bool, error) { return t.Reg.RelationTupleManager().ExistsRelationTuples(ctx, query) } func (t *ManagerWrapper) WriteRelationTuples(ctx context.Context, rs ...*RelationTuple) error { return t.Reg.RelationTupleManager().WriteRelationTuples(ctx, rs...) } func (t *ManagerWrapper) DeleteRelationTuples(ctx context.Context, rs ...*RelationTuple) error { return t.Reg.RelationTupleManager().DeleteRelationTuples(ctx, rs...) } func (t *ManagerWrapper) DeleteAllRelationTuples(ctx context.Context, query *RelationQuery) error { return t.Reg.RelationTupleManager().DeleteAllRelationTuples(ctx, query) } func (t *ManagerWrapper) TransactRelationTuples(ctx context.Context, insert []*RelationTuple, delete []*RelationTuple) error { return t.Reg.RelationTupleManager().TransactRelationTuples(ctx, insert, delete) } func (t *ManagerWrapper) TraverseSubjectSetExpansion(ctx context.Context, tuple *RelationTuple, allowedSubjectSets []SubjectSetType) ([]*TraversalResult, error) { return t.Reg.RelationTupleManager().TraverseSubjectSetExpansion(ctx, tuple, allowedSubjectSets) } func (t *ManagerWrapper) FindTupleWithRelations(ctx context.Context, tuple *RelationTuple, relations []string) (*RelationTuple, error) { return t.Reg.RelationTupleManager().FindTupleWithRelations(ctx, tuple, relations) } func (t *ManagerWrapper) Transaction(ctx context.Context, f func(ctx context.Context) error) error { return t.Reg.RelationTupleManager().Transaction(ctx, f) } func (t *ManagerWrapper) WithLatestSnapshot(ctx context.Context, f func(ctx context.Context) error) error { return t.Reg.RelationTupleManager().WithLatestSnapshot(ctx, f) } func (t *ManagerWrapper) RelationTupleManager() Manager { return t }