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
143 lines
5.0 KiB
Go
143 lines
5.0 KiB
Go
// Copyright © 2026 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package sql
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/gofrs/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/ory/keto/internal/relationtuple"
|
|
)
|
|
|
|
// existsFor returns the EXISTS subquery text that buildFoundExpr produces for the given subject.
|
|
func existsFor(t *testing.T, subject relationtuple.Subject) string {
|
|
t.Helper()
|
|
subjectSQL, _, err := whereSubject(subject)
|
|
require.NoError(t, err)
|
|
return `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
|
|
` + subjectSQL + `
|
|
)`
|
|
}
|
|
|
|
func TestBuildFoundExpr(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("case=subject as SubjectID", func(t *testing.T) {
|
|
subject := &relationtuple.SubjectID{ID: uuid.Must(uuid.NewV4())}
|
|
existsExpr := existsFor(t, subject)
|
|
|
|
// when SubjectID is used, no `allowedSubjectSets` is expected
|
|
t.Run("no allowed types: EXISTS runs for all rows", func(t *testing.T) {
|
|
expr, args, err := buildFoundExpr(nil, subject)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, existsExpr, expr)
|
|
assert.Equal(t, []any{subject.ID}, args)
|
|
})
|
|
})
|
|
|
|
t.Run("case=subject as SubjectSet", func(t *testing.T) {
|
|
subject := &relationtuple.SubjectSet{
|
|
Namespace: "User",
|
|
Object: uuid.Must(uuid.NewV4()),
|
|
}
|
|
existsExpr := existsFor(t, subject)
|
|
|
|
t.Run("no allowed types: EXISTS runs for all rows", func(t *testing.T) {
|
|
expr, args, err := buildFoundExpr(nil, subject)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, existsExpr, expr)
|
|
assert.Equal(t, []any{subject.Namespace, subject.Object, subject.Relation}, args)
|
|
})
|
|
|
|
// All types have AllowsDirect=false: OPL forbids direct membership for every type,
|
|
// so the found column is always false without touching the inner table.
|
|
t.Run("all AllowsDirect=false: found is always false", func(t *testing.T) {
|
|
types := []relationtuple.SubjectSetType{
|
|
{Namespace: "Group", Relation: "member", AllowsDirect: false},
|
|
{Namespace: "Org", Relation: "admin", AllowsDirect: false},
|
|
}
|
|
expr, args, err := buildFoundExpr(types, subject)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "false", expr)
|
|
assert.Nil(t, args)
|
|
})
|
|
|
|
// All types allow direct: the WHERE clause already restricts rows to those types,
|
|
// so EXISTS runs unconditionally — no extra condition needed.
|
|
t.Run("all AllowsDirect=true: plain EXISTS, no extra conditions ", func(t *testing.T) {
|
|
types := []relationtuple.SubjectSetType{
|
|
{Namespace: "Group", Relation: "member", AllowsDirect: true},
|
|
{Namespace: "Org", Relation: "admin", AllowsDirect: true},
|
|
}
|
|
expr, args, err := buildFoundExpr(types, subject)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, existsExpr, expr)
|
|
assert.Equal(t, []any{subject.Namespace, subject.Object, subject.Relation}, args)
|
|
})
|
|
|
|
// Mixed: EXISTS is gated to fire only for the AllowsDirect=true type;
|
|
// the false type is excluded from the gate even though it passes the WHERE filter.
|
|
t.Run("mixed AllowsDirect: EXISTS gated only on allowed type", func(t *testing.T) {
|
|
types := []relationtuple.SubjectSetType{
|
|
{Namespace: "Group", Relation: "member", AllowsDirect: true},
|
|
{Namespace: "Org", Relation: "admin", AllowsDirect: false},
|
|
}
|
|
expr, args, err := buildFoundExpr(types, subject)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t,
|
|
`(`+existsExpr+` AND (current.subject_set_namespace, current.subject_set_relation) IN ((?, ?)))`,
|
|
expr,
|
|
)
|
|
assert.Equal(t, []any{subject.Namespace, subject.Object, subject.Relation, "Group", "member"}, args)
|
|
})
|
|
|
|
t.Run("mixed AllowsDirect: multiple allowed types with not-allowed type", func(t *testing.T) {
|
|
types := []relationtuple.SubjectSetType{
|
|
{Namespace: "Group", Relation: "member", AllowsDirect: true},
|
|
{Namespace: "Org", Relation: "admin", AllowsDirect: true},
|
|
{Namespace: "Team", Relation: "lead", AllowsDirect: false},
|
|
}
|
|
expr, args, err := buildFoundExpr(types, subject)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t,
|
|
`(`+existsExpr+` AND (current.subject_set_namespace, current.subject_set_relation) IN ((?, ?), (?, ?)))`,
|
|
expr,
|
|
)
|
|
assert.Equal(t, []any{subject.Namespace, subject.Object, subject.Relation, "Group", "member", "Org", "admin"}, args)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestBuildSubjectSetTypeSQL(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("no types: no filter", func(t *testing.T) {
|
|
sqlFragment, args := buildSubjectSetTypeSQL(nil)
|
|
assert.Empty(t, sqlFragment)
|
|
assert.Nil(t, args)
|
|
})
|
|
|
|
t.Run("multiple types share one IN list", func(t *testing.T) {
|
|
sqlFragment, args := buildSubjectSetTypeSQL([]relationtuple.SubjectSetType{
|
|
{Namespace: "Group", Relation: "member"},
|
|
{Namespace: "Org", Relation: "admin"},
|
|
})
|
|
assert.Equal(t, "\nAND (current.subject_set_namespace, current.subject_set_relation) IN ((?, ?), (?, ?))", sqlFragment)
|
|
assert.Equal(t, []any{"Group", "member", "Org", "admin"}, args)
|
|
})
|
|
}
|