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

This commit is contained in:
upstream-mirror
2026-09-16 01:08:34 +05:00
commit 9456a4e922
2560 changed files with 120911 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
//go:build !nocrdb && !nocockroach
package dbx
import (
"testing"
"github.com/ory/x/sqlcon/dockertest"
)
func RunCockroach(t testing.TB, testDB string) (string, error) {
url := dockertest.RunTestCockroachDBWithVersion(t, "latest-v25.4")
if err := createDB(t, url, testDB); err != nil {
return "", err
}
return withDbName(url, testDB), nil
}

View File

@@ -0,0 +1,22 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
//go:build !nomysql
package dbx
import (
"testing"
"time"
"github.com/ory/x/sqlcon/dockertest"
)
func RunMySQL(t testing.TB, testDB string) (string, error) {
url := dockertest.RunTestMySQLWithVersion(t, "8.0")
time.Sleep(1 * time.Second)
if err := createDB(t, url, testDB); err != nil {
return "", err
}
return withDbName(url, testDB), nil
}

View File

@@ -0,0 +1,12 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
//go:build nocrdb || nocockroach
package dbx
import (
"testing"
)
func RunCockroach(testing.TB, string) (string, error) { return "", nil }

View File

@@ -0,0 +1,12 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
//go:build nomysql
package dbx
import (
"testing"
)
func RunMySQL(testing.TB, string) (string, error) { return "", nil }

View File

@@ -0,0 +1,12 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
//go:build nopostgres
package dbx
import (
"testing"
)
func RunPostgres(testing.TB, string) (string, error) { return "", nil }

View File

@@ -0,0 +1,20 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
//go:build !nopostgres
package dbx
import (
"testing"
"github.com/ory/x/sqlcon/dockertest"
)
func RunPostgres(t testing.TB, testDB string) (string, error) {
url := dockertest.RunTestPostgreSQLWithVersion(t, "16")
if err := createDB(t, url, testDB); err != nil {
return "", err
}
return withDbName(url, testDB), nil
}

View File

@@ -0,0 +1,19 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package dbx
import (
"testing"
"github.com/ory/x/dbal"
)
func GetSqlite(t testing.TB) *DsnT {
return &DsnT{
MigrateUp: true,
MigrateDown: false,
Conn: dbal.NewSQLiteTestDatabase(t),
Name: "sqlite",
}
}

View File

@@ -0,0 +1,188 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package dbx
import (
"crypto/rand"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/go-sql-driver/mysql"
"github.com/stretchr/testify/require"
"github.com/tidwall/sjson"
"golang.org/x/sync/errgroup"
"github.com/ory/pop/v6"
)
type DsnT struct {
Name string
Conn string
MigrateUp, MigrateDown bool
}
const mySQLSchema = "mysql://"
func mySQLWithDbName(dsn string, db string) string {
cfg, err := mysql.ParseDSN(strings.TrimPrefix(dsn, mySQLSchema))
if err != nil {
return ""
}
cfg.DBName = db
return mySQLSchema + cfg.FormatDSN()
}
func withDbName(dsn string, db string) string {
// Special case for mysql, because their URLs are not parsable.
if strings.HasPrefix(dsn, mySQLSchema) {
return mySQLWithDbName(dsn, db)
}
u, err := url.Parse(dsn)
if err != nil {
return ""
}
u.Path = db
return u.String()
}
func dbName() string {
var buf [20]byte
_, _ = rand.Read(buf[:])
return fmt.Sprintf("testdb_%x", buf)
}
func openAndPing(url string) (*pop.Connection, error) {
conn, err := pop.NewConnection(&pop.ConnectionDetails{URL: url})
if err != nil {
return nil, fmt.Errorf("failed to connect to %q: %w", url, err)
}
for range 120 {
fmt.Println("trying to open connection to", url)
if err := conn.Open(); err != nil {
// return nil, fmt.Errorf("failed to open connection: %w", err)
time.Sleep(1 * time.Second)
continue
}
if err := Ping(conn); err == nil {
break
}
time.Sleep(1 * time.Second)
}
if err := Ping(conn); err != nil {
_ = conn.Close()
return nil, fmt.Errorf("failed to ping: %w", err)
}
return conn, nil
}
func createDB(t testing.TB, url string, dbName string) (err error) {
var conn *pop.Connection
if conn, err = openAndPing(url); err != nil {
return fmt.Errorf("failed to connect to %q: %w", url, err)
}
t.Cleanup(func() {
if err := conn.Close(); err != nil {
t.Logf("could not close connection for %q in %q: %v", dbName, url, err)
}
})
if err := conn.RawQuery("CREATE DATABASE " + dbName).Exec(); err != nil {
return fmt.Errorf("failed to create db %q in %q: %w", dbName, url, err)
}
t.Cleanup(func() {
if err := conn.RawQuery("DROP DATABASE " + dbName).Exec(); err != nil {
t.Logf("could not drop database %q in %q: %v", dbName, url, err)
}
})
return
}
func GetDSNs(t testing.TB) []*DsnT {
dsns := []*DsnT{GetSqlite(t)}
if !testing.Short() {
var mysql, postgres, cockroach string
testDB := dbName()
var eg errgroup.Group
eg.Go(func() (err error) {
postgres, err = RunPostgres(t, testDB)
return
})
eg.Go(func() (err error) {
mysql, err = RunMySQL(t, testDB)
return
})
eg.Go(func() (err error) {
cockroach, err = RunCockroach(t, testDB)
return
})
require.NoError(t, eg.Wait())
if mysql != "" {
dsns = append(dsns, &DsnT{
Name: "mysql",
Conn: mysql,
MigrateUp: true,
MigrateDown: true,
})
}
if postgres != "" {
dsns = append(dsns, &DsnT{
Name: "postgres",
Conn: postgres,
MigrateUp: true,
MigrateDown: true,
})
}
if cockroach != "" {
dsns = append(dsns, &DsnT{
Name: "cockroach",
Conn: cockroach,
MigrateUp: true,
MigrateDown: true,
})
}
}
require.NotZero(t, len(dsns), "expected to run against at least one database")
return dsns
}
func ConfigFile(t testing.TB, values map[string]interface{}) string {
dir := t.TempDir()
fn := filepath.Join(dir, "keto.json")
c := []byte("{}")
for key, val := range values {
var err error
c, err = sjson.SetBytes(c, key, val)
require.NoError(t, err)
}
require.NoError(t, os.WriteFile(fn, c, 0600))
return fn
}
type pinger interface{ Ping() error }
func Ping(conn *pop.Connection) error {
p, ok := conn.Store.(pinger)
if !ok {
return fmt.Errorf("connection not opened: Store is %T", conn.Store)
}
return p.Ping()
}

View File

@@ -0,0 +1,64 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package dbx
import (
"testing"
"github.com/ory/pop/v6"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
//nolint:gosec // Test DSNs with dummy credentials, not real secrets.
func Test_withDbName(t *testing.T) {
type args struct {
dsn string
db string
}
tests := []struct {
name string
args args
want string
}{{
name: "postgres",
args: args{
dsn: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable",
db: "mydb",
},
want: "postgres://postgres:postgres@localhost:5432/mydb?sslmode=disable",
}, {
name: "cockroach",
args: args{
dsn: "cockroach://root@localhost:49364/defaultdb?sslmode=disable",
db: "foo",
},
want: "cockroach://root@localhost:49364/foo?sslmode=disable",
}, {
name: "mysql",
args: args{
dsn: "mysql://root:secret@(localhost:49394)/mysql?parseTime=true&multiStatements=true",
db: "testdb",
},
want: "mysql://root:secret@tcp(localhost:49394)/testdb?multiStatements=true&parseTime=true",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, withDbName(tt.args.dsn, tt.args.db))
})
}
}
func Test_GetDSNs_can_connect_to_each_db(t *testing.T) {
for _, db := range GetDSNs(t) {
t.Run("dsn="+db.Name, func(t *testing.T) {
t.Parallel()
conn, err := pop.NewConnection(&pop.ConnectionDetails{URL: db.Conn})
require.NoError(t, err)
assert.NoError(t, conn.Open())
assert.NoError(t, Ping(conn))
assert.NoError(t, conn.Close())
})
}
}

View File

@@ -0,0 +1,53 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package graph
import (
"context"
"fmt"
"sync"
"github.com/ory/keto/internal/relationtuple"
)
type contextKey string
const visitedMapKey = contextKey("visitedMap")
type stringSet struct {
m map[string]struct{}
l sync.Mutex
}
func newStringSet() *stringSet {
return &stringSet{m: make(map[string]struct{})}
}
func (s *stringSet) addNoDuplicate(el fmt.Stringer) bool {
s.l.Lock()
defer s.l.Unlock()
if _, found := s.m[el.String()]; found {
return true
}
s.m[el.String()] = struct{}{}
return false
}
func InitVisited(ctx context.Context) context.Context {
if _, ok := ctx.Value(visitedMapKey).(*stringSet); !ok {
ctx = context.WithValue(ctx, visitedMapKey, newStringSet())
}
return ctx
}
func CheckAndAddVisited(ctx context.Context, current relationtuple.Subject) (context.Context, bool) {
set, ok := ctx.Value(visitedMapKey).(*stringSet)
if !ok {
set = newStringSet()
ctx = context.WithValue(ctx, visitedMapKey, set)
}
return ctx, set.addNoDuplicate(current.UniqueID())
}

View File

@@ -0,0 +1,127 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package graph
import (
"context"
"sync"
"testing"
"github.com/gofrs/uuid"
"github.com/ory/keto/internal/relationtuple"
"github.com/stretchr/testify/assert"
)
func TestEngineUtilsProvider_CheckVisited(t *testing.T) {
a, b, c, d, e := uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4()), uuid.Must(uuid.NewV4())
t.Run("case=finds cycle", func(t *testing.T) {
linkedList := []relationtuple.SubjectSet{{
Namespace: "1",
Object: a,
Relation: "connected",
}, {
Namespace: "1",
Object: b,
Relation: "connected",
}, {
Namespace: "1",
Object: c,
Relation: "connected",
}, {
Namespace: "1",
Object: b,
Relation: "connected",
}, {
Namespace: "1",
Object: d,
Relation: "connected",
}}
ctx := context.Background()
var isThereACycle bool
for i := range linkedList {
ctx, isThereACycle = CheckAndAddVisited(ctx, &linkedList[i])
if isThereACycle {
break
}
}
assert.Equal(t, isThereACycle, true)
})
t.Run("case=ignores if no cycle", func(t *testing.T) {
list := []relationtuple.SubjectSet{{
Namespace: "1",
Object: a,
Relation: "connected",
}, {
Namespace: "1",
Object: b,
Relation: "connected",
}, {
Namespace: "1",
Object: c,
Relation: "connected",
}, {
Namespace: "1",
Object: d,
Relation: "connected",
}, {
Namespace: "1",
Object: e,
Relation: "connected",
}}
ctx := context.Background()
var isThereACycle bool
for i := range list {
ctx, isThereACycle = CheckAndAddVisited(ctx, &list[i])
if isThereACycle {
break
}
}
assert.Equal(t, isThereACycle, false)
})
t.Run("case=no race condition during adding", func(t *testing.T) {
racyObj := uuid.Must(uuid.NewV4())
otherObj := uuid.Must(uuid.NewV4())
// we repeat this test a few times to ensure we don't have a race condition
// the race detector alone was not able to catch it
for i := 0; i < 500; i++ {
subject := &relationtuple.SubjectSet{
Namespace: "default",
Object: racyObj,
Relation: "connected",
}
ctx, _ := CheckAndAddVisited(
context.Background(),
&relationtuple.SubjectSet{Object: otherObj},
)
var wg sync.WaitGroup
var aCycle, bCycle bool
var aCtx, bCtx context.Context
wg.Add(2)
go func() {
aCtx, aCycle = CheckAndAddVisited(ctx, subject)
wg.Done()
}()
go func() {
bCtx, bCycle = CheckAndAddVisited(ctx, subject)
wg.Done()
}()
wg.Wait()
// one should be true, and one false
assert.False(t, aCycle && bCycle)
assert.True(t, aCycle || bCycle)
assert.Equal(t, aCtx, bCtx)
}
})
}

16
internal/x/helpers.go Normal file
View File

@@ -0,0 +1,16 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package x
import (
"github.com/gofrs/uuid"
)
func UUIDs(n int) []uuid.UUID {
res := make([]uuid.UUID, n)
for i := range res {
res[i] = uuid.Must(uuid.NewV4())
}
return res
}

24
internal/x/max_depth.go Normal file
View File

@@ -0,0 +1,24 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package x
import (
"net/url"
"strconv"
"github.com/ory/herodot"
)
func GetMaxDepthFromQuery(q url.Values) (int, error) {
if !q.Has("max-depth") {
return 0, nil
}
maxDepth, err := strconv.ParseInt(q.Get("max-depth"), 0, 0)
if err != nil {
return 0, herodot.ErrBadRequest().WithErrorf("unable to parse 'max-depth' query parameter to int: %s", err)
}
return int(maxDepth), err
}

19
internal/x/registry.go Normal file
View File

@@ -0,0 +1,19 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package x
import (
"context"
"connectrpc.com/connect"
"github.com/gofrs/uuid"
)
type NetworkIDProvider interface {
NetworkID(context.Context) uuid.UUID
}
type HandlerOptionsProvider interface {
HandlerOptions() []connect.HandlerOption
}

View File

@@ -0,0 +1,75 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package validate
import (
"fmt"
"io"
"net/http"
"strings"
"github.com/ory/herodot"
)
type Validator func(r *http.Request) (ok bool, reason string)
// All runs all validators and returns an error if any of them fail. It returns
// a ErrBadRequest with all failed validation messages.
func All(r *http.Request, validator ...Validator) error {
var reasons []string
for _, v := range validator {
if ok, msg := v(r); !ok {
reasons = append(reasons, msg)
}
}
if len(reasons) > 0 {
return herodot.ErrBadRequest().WithReason(strings.Join(reasons, "; "))
}
return nil
}
// NoExtraQueryParams returns a validator that checks if the request has any
// query parameters that are not in the except list.
func NoExtraQueryParams(except ...string) Validator {
return func(req *http.Request) (ok bool, reason string) {
allowed := make(map[string]struct{}, len(except))
for _, e := range except {
allowed[e] = struct{}{}
}
for key := range req.URL.Query() {
if _, found := allowed[key]; !found {
return false, fmt.Sprintf("query parameter key %q unknown", key)
}
}
return true, ""
}
}
// QueryParamsContainsOneOf returns a validator that checks if the request has
// at least one of the specified query parameters.
func QueryParamsContainsOneOf(keys ...string) Validator {
return func(req *http.Request) (ok bool, reason string) {
oneOfKeys := make(map[string]struct{}, len(keys))
for _, k := range keys {
oneOfKeys[k] = struct{}{}
}
for key := range req.URL.Query() {
if _, found := oneOfKeys[key]; found {
return true, ""
}
}
return false, fmt.Sprintf("query parameters must specify at least one of the following: %s", strings.Join(keys, ", "))
}
}
// HasEmptyBody returns a validator that checks if the request body is empty.
func HasEmptyBody() Validator {
return func(r *http.Request) (ok bool, reason string) {
_, err := r.Body.Read([]byte{})
if err != io.EOF {
return false, "body is not empty"
}
return true, ""
}
}

View File

@@ -0,0 +1,116 @@
// Copyright © 2023 Ory Corp
// SPDX-License-Identifier: Apache-2.0
package validate_test
import (
"io"
"net/http"
"net/url"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ory/keto/internal/x/validate"
)
func toURL(t *testing.T, s string) *url.URL {
u, err := url.Parse(s)
require.NoError(t, err)
return u
}
func TestValidateNoExtraParams(t *testing.T) {
for _, tt := range []struct {
name string
req *http.Request
assertErr assert.ErrorAssertionFunc
}{
{
name: "empty",
req: &http.Request{URL: toURL(t, "https://example.com")},
assertErr: assert.NoError,
},
{
name: "all params",
req: &http.Request{URL: toURL(t, "https://example.com?foo=1&bar=baz")},
assertErr: assert.NoError,
},
{
name: "extra params",
req: &http.Request{URL: toURL(t, "https://example.com?foo=1&bar=2&baz=3")},
assertErr: assert.Error,
},
} {
t.Run("case="+tt.name, func(t *testing.T) {
err := validate.All(tt.req, validate.NoExtraQueryParams("foo", "bar"))
tt.assertErr(t, err)
})
}
}
func TestQueryParamsContainsOneOf(t *testing.T) {
for _, tt := range []struct {
name string
req *http.Request
assertErr assert.ErrorAssertionFunc
}{
{
name: "empty",
req: &http.Request{URL: toURL(t, "https://example.com")},
assertErr: assert.Error,
},
{
name: "other",
req: &http.Request{URL: toURL(t, "https://example.com?a=1&b=2&c=3")},
assertErr: assert.Error,
},
{
name: "one",
req: &http.Request{URL: toURL(t, "https://example.com?foo=1")},
assertErr: assert.NoError,
},
{
name: "all params",
req: &http.Request{URL: toURL(t, "https://example.com?foo=1&bar=baz")},
assertErr: assert.NoError,
},
{
name: "extra params",
req: &http.Request{URL: toURL(t, "https://example.com?foo=1&bar=2&baz=3")},
assertErr: assert.NoError,
},
} {
t.Run("case="+tt.name, func(t *testing.T) {
err := validate.All(tt.req, validate.QueryParamsContainsOneOf("foo", "bar"))
tt.assertErr(t, err)
})
}
}
func TestValidateHasEmptyBody(t *testing.T) {
for _, tt := range []struct {
name string
req *http.Request
assertErr assert.ErrorAssertionFunc
}{
{
name: "empty body",
req: &http.Request{Body: io.NopCloser(strings.NewReader(""))},
assertErr: assert.NoError,
},
{
name: "non-empty body",
req: &http.Request{Body: io.NopCloser(strings.NewReader("content"))},
assertErr: assert.Error,
},
} {
t.Run("case="+tt.name, func(t *testing.T) {
err := validate.All(tt.req, validate.HasEmptyBody())
tt.assertErr(t, err)
})
}
}