mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-02 19:41:57 +00:00
pkg/security no longer contains SQL. Every provider calls a store interface
from lookup, implemented by a procedure backend (Postgres stored procedures,
the default there) and a direct backend (dialect-driven SQL for postgres,
sqlite, mysql and mssql with configurable table and column names).
- add sectypes, lookup, lookup/{dialect,procedure,direct,backends,ddl,conformance}
- split totp and providers sub packages out of the core package
- replace SQLNames/TableNames/QueryMode with lookup.Config (see breaking_changes.md)
- direct backend now covers column/row security and API-key login
- move txsettings SQL to lookup.ApplyTxSettings; remove password.go
- move schema scripts under lookup/, add reference DDL per dialect
- add a shared conformance suite; run it on sqlite, and on Postgres in a
podman/docker container (RESOLVESPEC_TEST_CONTAINERS=1)
- fix procedure schema bugs found on real Postgres: duplicate p_data
parameter, JSON null arrays, expires_at timezone casts, passkey list
GROUP BY, missing resolvespec_passkey_login; accept zone-less timestamps
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
package backends
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"testing"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
_ "github.com/microsoft/go-mssqldb"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
|
|
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/conformance"
|
|
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/ddl"
|
|
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/dialect"
|
|
)
|
|
|
|
// Every backend/dialect runs the same suite. SQLite runs always. The others run only when a
|
|
// DSN is set, and only against a database you are happy to add rows to (all names the suite
|
|
// creates carry a unique "cf<hex>_" prefix and are removed afterwards):
|
|
//
|
|
// RESOLVESPEC_TEST_PG_DSN Postgres with the procedure schema installed
|
|
// (lookup/database_schema.sql + keystore_schema.sql): procedure mode
|
|
// RESOLVESPEC_TEST_PG_DIRECT_DSN Postgres for direct mode; ddl/postgres.sql is applied if the
|
|
// tables are missing. Do not point it at the procedure schema:
|
|
// the column types differ.
|
|
// RESOLVESPEC_TEST_MYSQL_DSN MySQL (needs a "mysql" database/sql driver linked into the test binary)
|
|
// RESOLVESPEC_TEST_MSSQL_DSN SQL Server (driver "sqlserver")
|
|
func TestConformance(t *testing.T) {
|
|
t.Run("sqlite/direct", func(t *testing.T) {
|
|
runConformance(t, sqliteDB(t), "sqlite", lookup.Config{Mode: lookup.ModeDirect}, false)
|
|
})
|
|
t.Run("sqlite/default", func(t *testing.T) {
|
|
runConformance(t, sqliteDB(t), "sqlite", lookup.Config{}, false)
|
|
})
|
|
|
|
real := []struct {
|
|
name, env, driver, dialect string
|
|
cfg lookup.Config
|
|
applyDDL bool
|
|
}{
|
|
{"postgres/procedure", "RESOLVESPEC_TEST_PG_DSN", "pgx", "postgres", lookup.Config{Mode: lookup.ModeProcedure}, false},
|
|
{"postgres/direct", "RESOLVESPEC_TEST_PG_DIRECT_DSN", "pgx", "postgres", lookup.Config{Mode: lookup.ModeDirect}, true},
|
|
{"mysql/direct", "RESOLVESPEC_TEST_MYSQL_DSN", "mysql", "mysql", lookup.Config{}, true},
|
|
{"mssql/direct", "RESOLVESPEC_TEST_MSSQL_DSN", "sqlserver", "mssql", lookup.Config{}, true},
|
|
}
|
|
for _, r := range real {
|
|
t.Run(r.name, func(t *testing.T) {
|
|
dsn := os.Getenv(r.env)
|
|
if dsn == "" {
|
|
t.Skipf("%s not set", r.env)
|
|
}
|
|
runOnServer(t, r.driver, dsn, r.dialect, r.cfg, r.applyDDL)
|
|
})
|
|
}
|
|
}
|
|
|
|
// runOnServer opens dsn, optionally applies the reference DDL, and runs the suite.
|
|
func runOnServer(t *testing.T, driver, dsn, dialectName string, cfg lookup.Config, applyDDL bool) {
|
|
t.Helper()
|
|
if !slices.Contains(sql.Drivers(), driver) {
|
|
t.Skipf("database/sql driver %q is not linked into this test binary", driver)
|
|
}
|
|
db, err := sql.Open(driver, dsn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
if err := db.Ping(); err != nil {
|
|
t.Fatalf("ping: %v", err)
|
|
}
|
|
if applyDDL {
|
|
stmts, err := ddl.Statements(dialectName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range stmts {
|
|
if _, err := db.Exec(s); err != nil {
|
|
t.Fatalf("apply ddl: %v\n%s", err, s)
|
|
}
|
|
}
|
|
}
|
|
runConformance(t, db, dialectName, cfg, true)
|
|
}
|
|
|
|
func runConformance(t *testing.T, db *sql.DB, dialectName string, cfg lookup.Config, shared bool) {
|
|
t.Helper()
|
|
d, err := dialect.Get(dialectName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg.Dialect = dialectName
|
|
p, err := New(db, cfg, Options{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var b [3]byte
|
|
_, _ = rand.Read(b[:])
|
|
env := conformance.Env{Provider: p, DB: db, Dialect: d, Prefix: "cf" + hex.EncodeToString(b[:]) + "_"}
|
|
if shared {
|
|
env.Cleanup = func(t *testing.T) { cleanup(t, db, d, env.Prefix) }
|
|
}
|
|
conformance.Run(t, env)
|
|
}
|
|
|
|
// cleanup deletes the rows a conformance run created, by prefix. Child rows go with their user
|
|
// through the foreign keys; tables without one are cleaned explicitly.
|
|
func cleanup(t *testing.T, db *sql.DB, d dialect.Dialect, prefix string) {
|
|
t.Helper()
|
|
like := prefix + "%"
|
|
for _, q := range []struct{ table, col string }{
|
|
{"oauth_codes", "code"},
|
|
{"oauth_clients", "client_id"},
|
|
{"token_blacklist", "token"},
|
|
{"sec_column_rules", "schema_name"},
|
|
{"sec_row_rules", "schema_name"},
|
|
{"users", "username"},
|
|
} {
|
|
if _, err := db.Exec(fmt.Sprintf("DELETE FROM %s WHERE %s LIKE %s", q.table, q.col, d.Placeholder(1)), like); err != nil {
|
|
t.Logf("cleanup %s: %v", q.table, err)
|
|
}
|
|
}
|
|
}
|