snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
218
internal/persistence/sql/traverser.go
Normal file
218
internal/persistence/sql/traverser.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/ory/x/otelx"
|
||||
"github.com/ory/x/sqlcon"
|
||||
"github.com/pkg/errors"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
"github.com/ory/keto/internal/relationtuple"
|
||||
"github.com/ory/keto/ketoapi"
|
||||
)
|
||||
|
||||
type (
|
||||
subjectExpandedRelationTupleRow struct {
|
||||
RelationTuple
|
||||
|
||||
Found bool `db:"found"`
|
||||
}
|
||||
)
|
||||
|
||||
func whereSubject(sub relationtuple.Subject) (sqlFragment string, args []any, err error) {
|
||||
switch s := sub.(type) {
|
||||
case *relationtuple.SubjectID:
|
||||
sqlFragment = "subject_id = ? AND subject_set_namespace IS NULL AND subject_set_object IS NULL AND subject_set_relation IS NULL"
|
||||
args = []any{s.ID}
|
||||
|
||||
case *relationtuple.SubjectSet:
|
||||
sqlFragment = "subject_id IS NULL AND subject_set_namespace = ? AND subject_set_object = ? AND subject_set_relation = ?"
|
||||
args = []any{s.Namespace, s.Object, s.Relation}
|
||||
|
||||
case nil:
|
||||
return "", nil, errors.WithStack(ketoapi.ErrNilSubject())
|
||||
}
|
||||
return sqlFragment, args, nil
|
||||
}
|
||||
|
||||
// buildSubjectSetTypeSQL builds an optional SQL fragment and args to filter
|
||||
// subject-set tuples by (namespace, relation) pairs. Returns empty string and
|
||||
// nil args when allowedSubjectSets is nil (no filter applied).
|
||||
func buildSubjectSetTypeSQL(allowedSubjectSets []relationtuple.SubjectSetType) (string, []any) {
|
||||
if len(allowedSubjectSets) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
args := make([]any, 0, len(allowedSubjectSets)*2)
|
||||
for _, f := range allowedSubjectSets {
|
||||
args = append(args, f.Namespace, f.Relation)
|
||||
}
|
||||
return "\nAND (current.subject_set_namespace, current.subject_set_relation) IN (" + tuplePlaceholders(len(allowedSubjectSets)) + ")", args
|
||||
}
|
||||
|
||||
// tuplePlaceholders returns n comma-separated "(?, ?)" pairs for use in a
|
||||
// row-value IN list.
|
||||
func tuplePlaceholders(n int) string {
|
||||
return strings.TrimSuffix(strings.Repeat("(?, ?), ", n), ", ")
|
||||
}
|
||||
|
||||
// buildFoundExpr builds the SQL expression for the found column.
|
||||
//
|
||||
// When allowedSubjectSets is empty, found is the result of the EXISTS subquery for every row.
|
||||
// When non-empty, AllowsDirect on each entry controls whether found can be true for that type:
|
||||
// - no entries have AllowsDirect: found is always false (literal, no subquery).
|
||||
// - all entries have AllowsDirect: found is the EXISTS result for every row.
|
||||
// - mixed: found can be true only for rows whose type is in the AllowsDirect set
|
||||
func buildFoundExpr(allowedSubjectSets []relationtuple.SubjectSetType, subject relationtuple.Subject) (string, []any, error) {
|
||||
var directTypes []relationtuple.SubjectSetType
|
||||
if len(allowedSubjectSets) > 0 {
|
||||
for _, t := range allowedSubjectSets {
|
||||
if t.AllowsDirect {
|
||||
directTypes = append(directTypes, t)
|
||||
}
|
||||
}
|
||||
if len(directTypes) == 0 {
|
||||
return "false", nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
subjectSQL, subjectArgs, err := whereSubject(subject)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
existsQuery := fmt.Sprintf(`EXISTS(
|
||||
SELECT 1 FROM keto_relation_tuples
|
||||
WHERE nid = current.nid AND
|
||||
namespace = current.subject_set_namespace AND
|
||||
object = current.subject_set_object AND
|
||||
relation = current.subject_set_relation AND
|
||||
%s
|
||||
)`, subjectSQL)
|
||||
|
||||
// No filter, or all types allow direct membership: EXISTS runs for every matched row.
|
||||
if len(allowedSubjectSets) == 0 || len(directTypes) == len(allowedSubjectSets) {
|
||||
return existsQuery, subjectArgs, nil
|
||||
}
|
||||
|
||||
// Only some types allow direct membership: gate the EXISTS result to those
|
||||
// types. The EXISTS subquery may still be evaluated for every returned row.
|
||||
args := make([]any, 0, len(subjectArgs)+len(directTypes)*2)
|
||||
args = append(args, subjectArgs...)
|
||||
for _, t := range directTypes {
|
||||
args = append(args, t.Namespace, t.Relation)
|
||||
}
|
||||
return fmt.Sprintf("(%s AND (current.subject_set_namespace, current.subject_set_relation) IN (%s))", existsQuery, tuplePlaceholders(len(directTypes))), args, nil
|
||||
}
|
||||
|
||||
// TraverseSubjectSetExpansion gets all subject-set pointers for the object#relation and
|
||||
// checks whether the requested subject is a direct member of each. When allowedSubjectSets
|
||||
// is non-empty, only pointers matching the declared (namespace, relation) pairs are returned,
|
||||
// and AllowsDirect on each type controls whether a direct membership check(subquery) is performed.
|
||||
func (t *Persister) TraverseSubjectSetExpansion(ctx context.Context, start *relationtuple.RelationTuple, allowedSubjectSets []relationtuple.SubjectSetType) (res []*relationtuple.TraversalResult, err error) {
|
||||
ctx, span := t.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.TraverseSubjectSetExpansion")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
var rowCount int
|
||||
defer func() { span.SetAttributes(attribute.Int("tuples_loaded", rowCount)) }()
|
||||
|
||||
foundExpr, foundArgs, err := buildFoundExpr(allowedSubjectSets, start.Subject)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subjectSetFilterSQL, subjectSetFilterArgs := buildSubjectSetTypeSQL(allowedSubjectSets)
|
||||
shardID := uuid.Nil
|
||||
for {
|
||||
var (
|
||||
rows []*subjectExpandedRelationTupleRow
|
||||
limit = 1000
|
||||
)
|
||||
queryArgs := make([]any, 0)
|
||||
queryArgs = append(queryArgs, foundArgs...)
|
||||
queryArgs = append(queryArgs, t.NetworkID(ctx), shardID, start.Namespace, start.Object, start.Relation)
|
||||
queryArgs = append(queryArgs, subjectSetFilterArgs...)
|
||||
queryArgs = append(queryArgs, limit)
|
||||
|
||||
err = t.Connection(ctx).RawQuery(fmt.Sprintf(`
|
||||
SELECT current.shard_id AS shard_id,
|
||||
current.subject_set_namespace AS namespace,
|
||||
current.subject_set_object AS object,
|
||||
current.subject_set_relation AS relation,
|
||||
%s AS found
|
||||
FROM keto_relation_tuples AS current
|
||||
WHERE current.nid = ? AND
|
||||
current.shard_id > ? AND
|
||||
current.namespace = ? AND
|
||||
current.object = ? AND
|
||||
current.relation = ? AND
|
||||
current.subject_id IS NULL
|
||||
%s
|
||||
ORDER BY current.shard_id
|
||||
LIMIT ?
|
||||
`, foundExpr, subjectSetFilterSQL),
|
||||
queryArgs...,
|
||||
).All(&rows)
|
||||
if err != nil {
|
||||
return nil, sqlcon.HandleError(err)
|
||||
}
|
||||
rowCount += len(rows)
|
||||
|
||||
for _, r := range rows {
|
||||
to := r.ToInternal()
|
||||
to.Subject = start.Subject
|
||||
res = append(res, &relationtuple.TraversalResult{
|
||||
From: start,
|
||||
To: to,
|
||||
Via: relationtuple.TraversalSubjectSetExpand,
|
||||
Found: r.Found,
|
||||
})
|
||||
if r.Found {
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
if len(rows) == limit {
|
||||
shardID = rows[limit-1].ID
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (t *Persister) FindTupleWithRelations(ctx context.Context, tuple *relationtuple.RelationTuple, relations []string) (_ *relationtuple.RelationTuple, err error) {
|
||||
ctx, span := t.d.Tracer(ctx).Tracer().Start(ctx, "persistence.sql.FindTupleWithRelations")
|
||||
defer otelx.End(span, &err)
|
||||
|
||||
if len(relations) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var rows relationTuples
|
||||
|
||||
query := t.queryWithNetwork(ctx)
|
||||
if err := t.whereQuery(ctx, query, &relationtuple.RelationQuery{
|
||||
Namespace: &tuple.Namespace,
|
||||
Object: &tuple.Object,
|
||||
Subject: tuple.Subject,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = query.Where("relation IN (?)", relations).Limit(1).All(&rows)
|
||||
if err != nil {
|
||||
return nil, sqlcon.HandleError(err)
|
||||
}
|
||||
|
||||
// If we got any rows back, success!
|
||||
if len(rows) > 0 {
|
||||
return rows[0].ToInternal(), nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user