refactor(security): move all database access into pkg/security/lookup

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
This commit is contained in:
Hein
2026-10-01 13:19:44 +02:00
parent 60bd0a6dd3
commit c9fa8c60f2
118 changed files with 11218 additions and 5565 deletions
+162
View File
@@ -0,0 +1,162 @@
// Package backends assembles a lookup.Provider: it builds the procedure and direct stores for
// one database and routes every operation to one of them according to lookup.Config.
// It lives apart from package lookup because both backends import lookup.
package backends
import (
"context"
"database/sql"
"fmt"
"sync"
"github.com/bitechdev/ResolveSpec/pkg/dbtrace"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/direct"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/procedure"
)
// Options are the settings that are not naming or mode.
type Options struct {
// DBFactory is called to obtain a fresh *sql.DB when the current one has been closed.
// Nil disables reconnecting.
DBFactory func() (*sql.DB, error)
// UpgradePasswordHash rewrites a legacy cleartext password as bcrypt after a successful
// direct-mode login. Off by default.
UpgradePasswordHash bool
// NoGroupTables skips the group membership table when loading direct-mode policy rules.
NoGroupTables bool
}
// New builds a Provider for db. cfg is merged with the defaults and validated; the dialect
// is cfg.Dialect or detected from the driver. Every operation's mode is resolved up front so
// an impossible combination (procedure mode on a non-Postgres dialect) fails here, not on the
// first request.
func New(db *sql.DB, cfg lookup.Config, opts Options) (*lookup.Provider, error) {
if db == nil {
return nil, fmt.Errorf("backends: nil database")
}
res, err := cfg.Resolve()
if err != nil {
return nil, err
}
d, err := cfg.ResolveDialect(db)
if err != nil {
return nil, err
}
for _, op := range lookup.AllOps() {
if _, err := res.EffectiveMode(op, d.Name()); err != nil {
return nil, err
}
}
c := &chooser{cfg: res, dialect: d.Name(), procs: res.Procs}
run := procedure.NewDB(db, opts.DBFactory, c.resetProbes)
c.db = run
base, err := direct.NewBase(run, d, res.Schema)
if err != nil {
return nil, err
}
p := procedure.NewPasskey(run, res.Procs)
return &lookup.Provider{
Auth: &authRouter{c: c,
proc: procedure.NewAuth(run, res.Procs),
direct: direct.NewAuth(base, direct.AuthOptions{UpgradePasswordHash: opts.UpgradePasswordHash})},
Keys: &keysRouter{c: c,
proc: procedure.NewKeys(run, res.Procs),
direct: direct.NewKeys(base)},
OAuthClient: &oauthClientRouter{c: c,
proc: procedure.NewOAuthClients(run, res.Procs),
direct: direct.NewOAuthClients(base)},
OAuthUser: &oauthUserRouter{c: c,
proc: procedure.NewOAuthUsers(run, res.Procs),
direct: direct.NewOAuthUsers(base)},
Passkey: &passkeyRouter{c: c, proc: p, direct: direct.NewPasskey(base)},
TOTP: &totpRouter{c: c,
proc: procedure.NewTOTP(run, res.Procs),
direct: direct.NewTOTP(base)},
Policy: &policyRouter{c: c,
proc: procedure.NewPolicy(run, res.Procs),
direct: direct.NewPolicy(base, direct.PolicyOptions{NoGroups: opts.NoGroupTables})},
}, nil
}
// Failed returns a Provider whose every operation returns err. Constructors that cannot
// return an error use it so a bad configuration fails closed on first use.
func Failed(err error) *lookup.Provider {
c := &chooser{fail: err}
return &lookup.Provider{
Auth: &authRouter{c: c},
Keys: &keysRouter{c: c},
OAuthClient: &oauthClientRouter{c: c},
OAuthUser: &oauthUserRouter{c: c},
Passkey: &passkeyRouter{c: c},
TOTP: &totpRouter{c: c},
Policy: &policyRouter{c: c},
}
}
// chooser decides per operation whether the procedure or the direct store runs.
type chooser struct {
cfg *lookup.Resolved
dialect string
procs lookup.ProcNames
db *procedure.DB
probes sync.Map // proc name -> bool
fail error // set by Failed: every operation returns it
}
func (c *chooser) resetProbes() {
c.probes.Range(func(k, _ any) bool { c.probes.Delete(k); return true })
}
// useProc reports whether op should call the stored procedure proc. In auto mode on Postgres
// the catalog is probed once per procedure (cached until a reconnect).
func (c *chooser) useProc(ctx context.Context, op lookup.Op, proc string) (bool, error) {
if c.fail != nil {
return false, c.fail
}
m, err := c.cfg.EffectiveMode(op, c.dialect)
if err != nil {
return false, err
}
switch m {
case lookup.ModeProcedure:
return true, nil
case lookup.ModeAuto:
if v, ok := c.probes.Load(proc); ok {
return v.(bool), nil
}
exists := probeProcedure(ctx, c.db.Get(), proc)
c.probes.Store(proc, exists)
return exists, nil
}
return false, nil
}
// probeProcedure asks the Postgres catalog whether a function exists. Any failure counts as
// "does not exist" so the probe can never block an operation.
func probeProcedure(ctx context.Context, db *sql.DB, proc string) (exists bool) {
if db == nil {
return false
}
defer func() { _ = recover() }()
dbtrace.Raw(ctx, "probe.pg_proc")
if err := db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM pg_proc WHERE proname = $1 LIMIT 1)`, proc).Scan(&exists); err != nil {
return false
}
return exists
}
// pick returns the store that should serve op.
func pick[T any](c *chooser, ctx context.Context, op lookup.Op, proc string, p, d T) (T, error) {
use, err := c.useProc(ctx, op, proc)
if err != nil {
var zero T
return zero, err
}
if use {
return p, nil
}
return d, nil
}
@@ -0,0 +1,119 @@
package backends
import (
"context"
"database/sql"
"errors"
"testing"
"github.com/DATA-DOG/go-sqlmock"
_ "github.com/glebarez/go-sqlite"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/ddl"
"github.com/bitechdev/ResolveSpec/pkg/security/sectypes"
)
func sqliteDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
t.Fatal(err)
}
db.SetMaxOpenConns(1)
t.Cleanup(func() { _ = db.Close() })
ddl, err := ddl.SQL("sqlite")
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec(ddl); err != nil {
t.Fatal(err)
}
return db
}
func TestSQLiteDefaultsToDirect(t *testing.T) {
ctx := context.Background()
p, err := New(sqliteDB(t), lookup.Config{}, Options{})
if err != nil {
t.Fatal(err)
}
reg, err := p.Auth.Register(ctx, sectypes.RegisterRequest{Username: "a", Email: "a@x.io", Password: "pw"})
if err != nil {
t.Fatal(err)
}
if _, err := p.Auth.Session(ctx, reg.Token, "authenticate"); err != nil {
t.Fatal(err)
}
if on, err := p.TOTP.Status(ctx, reg.User.UserID); err != nil || on {
t.Fatalf("%v %v", on, err)
}
}
func TestProcedureModeRejectedOnSQLite(t *testing.T) {
_, err := New(sqliteDB(t), lookup.Config{Overrides: map[lookup.Op]lookup.Mode{lookup.OpLogin: lookup.ModeProcedure}}, Options{})
if err == nil {
t.Fatal("expected error")
}
}
func TestCustomSchemaAndUnknownDialect(t *testing.T) {
if _, err := New(sqliteDB(t), lookup.Config{Dialect: "nosuch"}, Options{}); err == nil {
t.Fatal("unknown dialect accepted")
}
bad := lookup.Config{Schema: lookup.Schema{lookup.EntityUsers: {Name: "x; drop"}}}
if _, err := New(sqliteDB(t), bad, Options{}); err == nil {
t.Fatal("unsafe schema accepted")
}
if _, err := New(nil, lookup.Config{}, Options{}); err == nil {
t.Fatal("nil db accepted")
}
}
func TestPostgresDefaultsToProcedure(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
p, err := New(db, lookup.Config{Dialect: lookup.DialectPostgres}, Options{})
if err != nil {
t.Fatal(err)
}
mock.ExpectQuery("resolvespec_totp_get_status").WithArgs(1).
WillReturnRows(sqlmock.NewRows([]string{"p_success", "p_error", "p_enabled"}).AddRow(true, nil, true))
if on, err := p.TOTP.Status(context.Background(), 1); err != nil || !on {
t.Fatalf("%v %v", on, err)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestPostgresAutoProbesOnce(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
p, err := New(db, lookup.Config{Dialect: lookup.DialectPostgres, Mode: lookup.ModeAuto}, Options{})
if err != nil {
t.Fatal(err)
}
mock.ExpectQuery("pg_proc").WithArgs("resolvespec_totp_get_status").
WillReturnRows(sqlmock.NewRows([]string{"e"}).AddRow(true))
for i := 0; i < 2; i++ { // second call must reuse the cached probe
mock.ExpectQuery("resolvespec_totp_get_status").WithArgs(1).
WillReturnRows(sqlmock.NewRows([]string{"p_success", "p_error", "p_enabled"}).AddRow(true, nil, true))
if on, err := p.TOTP.Status(context.Background(), 1); err != nil || !on {
t.Fatalf("%v %v", on, err)
}
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestFailed(t *testing.T) {
p := Failed(errors.New("boom"))
if _, err := p.Auth.Login(context.Background(), sectypes.LoginRequest{}); err == nil || err.Error() != "boom" {
t.Fatalf("got %v", err)
}
if _, err := p.Policy.RowSecurity(context.Background(), 1, "s", "t"); err == nil {
t.Fatal("want error")
}
}
@@ -0,0 +1,126 @@
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)
}
}
}
@@ -0,0 +1,141 @@
package backends
import (
"bytes"
"context"
"database/sql"
"fmt"
"net"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
)
// Container tests start a throwaway database server with podman or docker (whichever is
// installed, podman first) and run the conformance suite against it. They pull an image, so
// they only run when RESOLVESPEC_TEST_CONTAINERS=1 and not with -short. The container is
// removed when the test ends.
const containerPassword = "Resolve_Spec_1"
func containerRuntime(t *testing.T) string {
t.Helper()
if testing.Short() {
t.Skip("container tests are skipped with -short")
}
if os.Getenv("RESOLVESPEC_TEST_CONTAINERS") != "1" {
t.Skip("set RESOLVESPEC_TEST_CONTAINERS=1 to run tests that start a podman/docker container")
}
for _, rt := range []string{"podman", "docker"} {
if p, err := exec.LookPath(rt); err == nil {
return p
}
}
t.Skip("neither podman nor docker found in PATH")
return ""
}
func run(t *testing.T, timeout time.Duration, name string, args ...string) string {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var out, errb bytes.Buffer
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdout, cmd.Stderr = &out, &errb
if err := cmd.Run(); err != nil {
t.Fatalf("%s %s: %v\n%s", name, strings.Join(args, " "), err, errb.String())
}
return strings.TrimSpace(out.String())
}
// startContainer runs image publishing containerPort on a random localhost port and returns
// the host port. The container is force-removed on cleanup.
func startContainer(t *testing.T, rt, image, containerPort string, env map[string]string) string {
t.Helper()
args := []string{"run", "-d", "--rm", "-p", "127.0.0.1::" + containerPort}
for k, v := range env {
args = append(args, "-e", k+"="+v)
}
args = append(args, image)
id := run(t, 10*time.Minute, rt, args...) // first run may pull the image
t.Cleanup(func() { _ = exec.Command(rt, "rm", "-f", id).Run() })
// "127.0.0.1:49153" (docker may print one line per address family)
out := run(t, 30*time.Second, rt, "port", id, containerPort)
line := strings.Fields(out)[len(strings.Fields(out))-1]
for _, l := range strings.Split(out, "\n") {
if strings.HasPrefix(strings.TrimSpace(l), "127.0.0.1") || strings.Contains(l, " 127.0.0.1:") {
line = l[strings.LastIndex(l, " ")+1:]
break
}
}
_, port, err := net.SplitHostPort(line)
if err != nil {
t.Fatalf("cannot parse published port %q: %v", out, err)
}
return port
}
// waitReady retries until the server accepts queries or the deadline passes.
func waitReady(t *testing.T, driver, dsn string, d time.Duration) *sql.DB {
t.Helper()
deadline := time.Now().Add(d)
var last error
for time.Now().Before(deadline) {
db, err := sql.Open(driver, dsn)
if err == nil {
if last = db.Ping(); last == nil {
t.Cleanup(func() { _ = db.Close() })
return db
}
_ = db.Close()
} else {
last = err
}
time.Sleep(time.Second)
}
t.Fatalf("database did not become ready within %s: %v", d, last)
return nil
}
func TestConformancePostgresContainer(t *testing.T) {
rt := containerRuntime(t)
port := startContainer(t, rt, "docker.io/library/postgres:16-alpine", "5432", map[string]string{"POSTGRES_PASSWORD": containerPassword})
dsn := func(db string) string {
return fmt.Sprintf("postgres://postgres:%s@127.0.0.1:%s/%s?sslmode=disable", containerPassword, port, db)
}
admin := waitReady(t, "pgx", dsn("postgres"), 90*time.Second)
// The official image restarts once during init: make sure the second start is the one we use.
time.Sleep(2 * time.Second)
admin = waitReady(t, "pgx", dsn("postgres"), 60*time.Second)
for _, name := range []string{"cf_proc", "cf_direct"} {
if _, err := admin.Exec("CREATE DATABASE " + name); err != nil {
t.Fatal(err)
}
}
t.Run("procedure", func(t *testing.T) {
db, err := sql.Open("pgx", dsn("cf_proc"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
for _, f := range []string{"../database_schema.sql", "../keystore_schema.sql"} {
b, err := os.ReadFile(f)
if err != nil {
t.Fatal(err)
}
if _, err := db.Exec(string(b)); err != nil {
t.Fatalf("apply %s: %v", f, err)
}
}
runConformance(t, db, "postgres", lookup.Config{Mode: lookup.ModeProcedure}, true)
})
t.Run("direct", func(t *testing.T) {
runOnServer(t, "pgx", dsn("cf_direct"), "postgres", lookup.Config{Mode: lookup.ModeDirect}, true)
})
}
+396
View File
@@ -0,0 +1,396 @@
package backends
import (
"context"
"time"
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
"github.com/bitechdev/ResolveSpec/pkg/security/sectypes"
)
// Routers: each store method asks the chooser which backend serves that operation.
type authRouter struct {
c *chooser
proc, direct lookup.AuthStore
}
var _ lookup.AuthStore = (*authRouter)(nil)
func (r *authRouter) Login(ctx context.Context, req sectypes.LoginRequest) (*sectypes.LoginResponse, error) {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpLogin, r.c.procs.Login, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.Login(ctx, req)
}
func (r *authRouter) Register(ctx context.Context, req sectypes.RegisterRequest) (*sectypes.LoginResponse, error) {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpRegister, r.c.procs.Register, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.Register(ctx, req)
}
func (r *authRouter) Logout(ctx context.Context, req sectypes.LogoutRequest) error {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpLogout, r.c.procs.Logout, r.proc, r.direct)
if err != nil {
return err
}
return st.Logout(ctx, req)
}
func (r *authRouter) Session(ctx context.Context, token, reference string) (*sectypes.UserContext, error) {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpSession, r.c.procs.Session, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.Session(ctx, token, reference)
}
func (r *authRouter) TouchSession(ctx context.Context, token string, user *sectypes.UserContext) error {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpTouchSession, r.c.procs.SessionUpdate, r.proc, r.direct)
if err != nil {
return err
}
return st.TouchSession(ctx, token, user)
}
func (r *authRouter) Refresh(ctx context.Context, refreshToken string) (*sectypes.LoginResponse, error) {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpRefresh, r.c.procs.RefreshToken, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.Refresh(ctx, refreshToken)
}
func (r *authRouter) LoginAPIKey(ctx context.Context, rawKey string, claims map[string]any) (*sectypes.LoginResponse, error) {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpLoginAPIKey, r.c.procs.LoginAPIKey, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.LoginAPIKey(ctx, rawKey, claims)
}
func (r *authRouter) JWTLogin(ctx context.Context, req sectypes.LoginRequest) (*sectypes.LoginResponse, error) {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpJWTLogin, r.c.procs.JWTLogin, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.JWTLogin(ctx, req)
}
func (r *authRouter) JWTLogout(ctx context.Context, req sectypes.LogoutRequest) error {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpJWTLogout, r.c.procs.JWTLogout, r.proc, r.direct)
if err != nil {
return err
}
return st.JWTLogout(ctx, req)
}
func (r *authRouter) ResetRequest(ctx context.Context, req sectypes.PasswordResetRequest) (*sectypes.PasswordResetResponse, error) {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpResetRequest, r.c.procs.PasswordResetRequest, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.ResetRequest(ctx, req)
}
func (r *authRouter) ResetComplete(ctx context.Context, req sectypes.PasswordResetCompleteRequest) error {
st, err := pick[lookup.AuthStore](r.c, ctx, lookup.OpResetComplete, r.c.procs.PasswordResetComplete, r.proc, r.direct)
if err != nil {
return err
}
return st.ResetComplete(ctx, req)
}
type keysRouter struct {
c *chooser
proc, direct lookup.KeyStore
}
var _ lookup.KeyStore = (*keysRouter)(nil)
func (r *keysRouter) Create(ctx context.Context, req sectypes.CreateKeyRequest, keyHash string) (*sectypes.UserKey, error) {
st, err := pick[lookup.KeyStore](r.c, ctx, lookup.OpKeyCreate, r.c.procs.KeystoreCreateKey, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.Create(ctx, req, keyHash)
}
func (r *keysRouter) List(ctx context.Context, userID int, keyType sectypes.KeyType) ([]sectypes.UserKey, error) {
st, err := pick[lookup.KeyStore](r.c, ctx, lookup.OpKeyList, r.c.procs.KeystoreGetUserKeys, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.List(ctx, userID, keyType)
}
func (r *keysRouter) Delete(ctx context.Context, userID int, keyID int64) (string, error) {
st, err := pick[lookup.KeyStore](r.c, ctx, lookup.OpKeyDelete, r.c.procs.KeystoreDeleteKey, r.proc, r.direct)
if err != nil {
return "", err
}
return st.Delete(ctx, userID, keyID)
}
func (r *keysRouter) Validate(ctx context.Context, keyHash string, keyType sectypes.KeyType) (*sectypes.UserKey, error) {
st, err := pick[lookup.KeyStore](r.c, ctx, lookup.OpKeyValidate, r.c.procs.KeystoreValidateKey, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.Validate(ctx, keyHash, keyType)
}
type oauthClientRouter struct {
c *chooser
proc, direct lookup.OAuthClientStore
}
var _ lookup.OAuthClientStore = (*oauthClientRouter)(nil)
func (r *oauthClientRouter) RegisterClient(ctx context.Context, client *sectypes.OAuthServerClient) (*sectypes.OAuthServerClient, error) {
st, err := pick[lookup.OAuthClientStore](r.c, ctx, lookup.OpOAuthRegisterClient, r.c.procs.OAuthRegisterClient, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.RegisterClient(ctx, client)
}
func (r *oauthClientRouter) GetClient(ctx context.Context, clientID string) (*sectypes.OAuthServerClient, error) {
st, err := pick[lookup.OAuthClientStore](r.c, ctx, lookup.OpOAuthGetClient, r.c.procs.OAuthGetClient, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.GetClient(ctx, clientID)
}
func (r *oauthClientRouter) SaveCode(ctx context.Context, code *sectypes.OAuthCode) error {
st, err := pick[lookup.OAuthClientStore](r.c, ctx, lookup.OpOAuthSaveCode, r.c.procs.OAuthSaveCode, r.proc, r.direct)
if err != nil {
return err
}
return st.SaveCode(ctx, code)
}
func (r *oauthClientRouter) ExchangeCode(ctx context.Context, code string) (*sectypes.OAuthCode, error) {
st, err := pick[lookup.OAuthClientStore](r.c, ctx, lookup.OpOAuthExchangeCode, r.c.procs.OAuthExchangeCode, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.ExchangeCode(ctx, code)
}
func (r *oauthClientRouter) Introspect(ctx context.Context, token string) (*sectypes.OAuthTokenInfo, error) {
st, err := pick[lookup.OAuthClientStore](r.c, ctx, lookup.OpOAuthIntrospect, r.c.procs.OAuthIntrospect, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.Introspect(ctx, token)
}
func (r *oauthClientRouter) Revoke(ctx context.Context, token string) error {
st, err := pick[lookup.OAuthClientStore](r.c, ctx, lookup.OpOAuthRevoke, r.c.procs.OAuthRevoke, r.proc, r.direct)
if err != nil {
return err
}
return st.Revoke(ctx, token)
}
type oauthUserRouter struct {
c *chooser
proc, direct lookup.OAuthUserStore
}
var _ lookup.OAuthUserStore = (*oauthUserRouter)(nil)
func (r *oauthUserRouter) GetOrCreateUser(ctx context.Context, user *sectypes.UserContext, provider string) (int, error) {
st, err := pick[lookup.OAuthUserStore](r.c, ctx, lookup.OpOAuthGetOrCreateUser, r.c.procs.OAuthGetOrCreateUser, r.proc, r.direct)
if err != nil {
return 0, err
}
return st.GetOrCreateUser(ctx, user, provider)
}
func (r *oauthUserRouter) CreateSession(ctx context.Context, session lookup.OAuthSession) error {
st, err := pick[lookup.OAuthUserStore](r.c, ctx, lookup.OpOAuthCreateSession, r.c.procs.OAuthCreateSession, r.proc, r.direct)
if err != nil {
return err
}
return st.CreateSession(ctx, session)
}
func (r *oauthUserRouter) GetByRefreshToken(ctx context.Context, refreshToken string) (*lookup.OAuthRefreshSession, error) {
st, err := pick[lookup.OAuthUserStore](r.c, ctx, lookup.OpOAuthGetRefreshToken, r.c.procs.OAuthGetRefreshToken, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.GetByRefreshToken(ctx, refreshToken)
}
func (r *oauthUserRouter) UpdateRefreshToken(ctx context.Context, userID int, oldRefreshToken, newSessionToken, newAccessToken, newRefreshToken string, expiresAt time.Time) error {
st, err := pick[lookup.OAuthUserStore](r.c, ctx, lookup.OpOAuthUpdateRefreshToken, r.c.procs.OAuthUpdateRefreshToken, r.proc, r.direct)
if err != nil {
return err
}
return st.UpdateRefreshToken(ctx, userID, oldRefreshToken, newSessionToken, newAccessToken, newRefreshToken, expiresAt)
}
func (r *oauthUserRouter) GetUser(ctx context.Context, userID int) (*sectypes.UserContext, error) {
st, err := pick[lookup.OAuthUserStore](r.c, ctx, lookup.OpOAuthGetUser, r.c.procs.OAuthGetUser, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.GetUser(ctx, userID)
}
type passkeyRouter struct {
c *chooser
proc, direct lookup.PasskeyStore
}
var _ lookup.PasskeyStore = (*passkeyRouter)(nil)
func (r *passkeyRouter) Store(ctx context.Context, rec lookup.PasskeyCredentialRecord) (int64, error) {
st, err := pick[lookup.PasskeyStore](r.c, ctx, lookup.OpPasskeyStore, r.c.procs.PasskeyStoreCredential, r.proc, r.direct)
if err != nil {
return 0, err
}
return st.Store(ctx, rec)
}
func (r *passkeyRouter) Get(ctx context.Context, credentialID string) (int, uint32, error) {
st, err := pick[lookup.PasskeyStore](r.c, ctx, lookup.OpPasskeyGet, r.c.procs.PasskeyGetCredential, r.proc, r.direct)
if err != nil {
return 0, 0, err
}
return st.Get(ctx, credentialID)
}
func (r *passkeyRouter) UpdateCounter(ctx context.Context, credentialID string, newCounter uint32) (bool, error) {
st, err := pick[lookup.PasskeyStore](r.c, ctx, lookup.OpPasskeyUpdateCounter, r.c.procs.PasskeyUpdateCounter, r.proc, r.direct)
if err != nil {
return false, err
}
return st.UpdateCounter(ctx, credentialID, newCounter)
}
func (r *passkeyRouter) List(ctx context.Context, userID int) ([]sectypes.PasskeyCredential, error) {
st, err := pick[lookup.PasskeyStore](r.c, ctx, lookup.OpPasskeyList, r.c.procs.PasskeyGetUserCredentials, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.List(ctx, userID)
}
func (r *passkeyRouter) Delete(ctx context.Context, userID int, credentialID string) error {
st, err := pick[lookup.PasskeyStore](r.c, ctx, lookup.OpPasskeyDelete, r.c.procs.PasskeyDeleteCredential, r.proc, r.direct)
if err != nil {
return err
}
return st.Delete(ctx, userID, credentialID)
}
func (r *passkeyRouter) Rename(ctx context.Context, userID int, credentialID, name string) error {
st, err := pick[lookup.PasskeyStore](r.c, ctx, lookup.OpPasskeyRename, r.c.procs.PasskeyUpdateName, r.proc, r.direct)
if err != nil {
return err
}
return st.Rename(ctx, userID, credentialID, name)
}
func (r *passkeyRouter) ByUsername(ctx context.Context, username string) (int, []lookup.PasskeyCredentialRef, error) {
st, err := pick[lookup.PasskeyStore](r.c, ctx, lookup.OpPasskeyByUsername, r.c.procs.PasskeyGetCredsByUsername, r.proc, r.direct)
if err != nil {
return 0, nil, err
}
return st.ByUsername(ctx, username)
}
func (r *passkeyRouter) Login(ctx context.Context, userID int, claims map[string]any) (*sectypes.LoginResponse, error) {
st, err := pick[lookup.PasskeyStore](r.c, ctx, lookup.OpPasskeyLogin, r.c.procs.PasskeyLogin, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.Login(ctx, userID, claims)
}
type totpRouter struct {
c *chooser
proc, direct lookup.TOTPStore
}
var _ lookup.TOTPStore = (*totpRouter)(nil)
func (r *totpRouter) Enable(ctx context.Context, userID int, secret string, hashedCodes []string) error {
st, err := pick[lookup.TOTPStore](r.c, ctx, lookup.OpTOTPEnable, r.c.procs.TOTPEnable, r.proc, r.direct)
if err != nil {
return err
}
return st.Enable(ctx, userID, secret, hashedCodes)
}
func (r *totpRouter) Disable(ctx context.Context, userID int) error {
st, err := pick[lookup.TOTPStore](r.c, ctx, lookup.OpTOTPDisable, r.c.procs.TOTPDisable, r.proc, r.direct)
if err != nil {
return err
}
return st.Disable(ctx, userID)
}
func (r *totpRouter) Status(ctx context.Context, userID int) (bool, error) {
st, err := pick[lookup.TOTPStore](r.c, ctx, lookup.OpTOTPStatus, r.c.procs.TOTPGetStatus, r.proc, r.direct)
if err != nil {
return false, err
}
return st.Status(ctx, userID)
}
func (r *totpRouter) Secret(ctx context.Context, userID int) (string, error) {
st, err := pick[lookup.TOTPStore](r.c, ctx, lookup.OpTOTPSecret, r.c.procs.TOTPGetSecret, r.proc, r.direct)
if err != nil {
return "", err
}
return st.Secret(ctx, userID)
}
func (r *totpRouter) RegenerateBackupCodes(ctx context.Context, userID int, hashedCodes []string) error {
st, err := pick[lookup.TOTPStore](r.c, ctx, lookup.OpTOTPRegenerateBackup, r.c.procs.TOTPRegenerateBackup, r.proc, r.direct)
if err != nil {
return err
}
return st.RegenerateBackupCodes(ctx, userID, hashedCodes)
}
func (r *totpRouter) ValidateBackupCode(ctx context.Context, userID int, codeHash string) (bool, error) {
st, err := pick[lookup.TOTPStore](r.c, ctx, lookup.OpTOTPValidateBackupCode, r.c.procs.TOTPValidateBackupCode, r.proc, r.direct)
if err != nil {
return false, err
}
return st.ValidateBackupCode(ctx, userID, codeHash)
}
type policyRouter struct {
c *chooser
proc, direct lookup.PolicyStore
}
var _ lookup.PolicyStore = (*policyRouter)(nil)
func (r *policyRouter) ColumnSecurity(ctx context.Context, userID int, schema, table string) ([]sectypes.ColumnSecurity, error) {
st, err := pick[lookup.PolicyStore](r.c, ctx, lookup.OpColumnSecurity, r.c.procs.ColumnSecurity, r.proc, r.direct)
if err != nil {
return nil, err
}
return st.ColumnSecurity(ctx, userID, schema, table)
}
func (r *policyRouter) RowSecurity(ctx context.Context, userRef any, schema, table string) (sectypes.RowSecurity, error) {
st, err := pick[lookup.PolicyStore](r.c, ctx, lookup.OpRowSecurity, r.c.procs.RowSecurity, r.proc, r.direct)
if err != nil {
return sectypes.RowSecurity{}, err
}
return st.RowSecurity(ctx, userRef, schema, table)
}