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:
20
internal/x/dbx/dsn_cockroach.go
Normal file
20
internal/x/dbx/dsn_cockroach.go
Normal 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
|
||||
}
|
||||
22
internal/x/dbx/dsn_mysql.go
Normal file
22
internal/x/dbx/dsn_mysql.go
Normal 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
|
||||
}
|
||||
12
internal/x/dbx/dsn_nocockroach.go
Normal file
12
internal/x/dbx/dsn_nocockroach.go
Normal 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 }
|
||||
12
internal/x/dbx/dsn_nomysql.go
Normal file
12
internal/x/dbx/dsn_nomysql.go
Normal 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 }
|
||||
12
internal/x/dbx/dsn_nopostgres.go
Normal file
12
internal/x/dbx/dsn_nopostgres.go
Normal 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 }
|
||||
20
internal/x/dbx/dsn_postgres.go
Normal file
20
internal/x/dbx/dsn_postgres.go
Normal 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
|
||||
}
|
||||
19
internal/x/dbx/dsn_sqlite.go
Normal file
19
internal/x/dbx/dsn_sqlite.go
Normal 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",
|
||||
}
|
||||
}
|
||||
188
internal/x/dbx/dsn_testutils.go
Normal file
188
internal/x/dbx/dsn_testutils.go
Normal 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()
|
||||
}
|
||||
64
internal/x/dbx/dsn_testutils_test.go
Normal file
64
internal/x/dbx/dsn_testutils_test.go
Normal 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())
|
||||
})
|
||||
}
|
||||
}
|
||||
53
internal/x/graph/graph_utils.go
Normal file
53
internal/x/graph/graph_utils.go
Normal 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())
|
||||
}
|
||||
127
internal/x/graph/graph_utils_test.go
Normal file
127
internal/x/graph/graph_utils_test.go
Normal 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
16
internal/x/helpers.go
Normal 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
24
internal/x/max_depth.go
Normal 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
19
internal/x/registry.go
Normal 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
|
||||
}
|
||||
75
internal/x/validate/validate.go
Normal file
75
internal/x/validate/validate.go
Normal 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, ""
|
||||
}
|
||||
}
|
||||
116
internal/x/validate/validate_test.go
Normal file
116
internal/x/validate/validate_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user