mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-05 13:01:58 +00:00
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:
@@ -0,0 +1,50 @@
|
||||
// Package ddl holds the reference table schemas for the lookup direct backend, one per
|
||||
// dialect. They use the lookup.DefaultSchema table and column names; copy and adapt them
|
||||
// when you override names through lookup.Config.Schema.
|
||||
//
|
||||
// The Postgres file creates tables only. The stored-procedure schema
|
||||
// (lookup/database_schema.sql) is a separate script with native bytea / text[] columns and
|
||||
// must not be combined with it.
|
||||
package ddl
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed postgres.sql sqlite.sql mysql.sql mssql.sql
|
||||
var files embed.FS
|
||||
|
||||
// SQL returns the schema script for a dialect name ("postgres", "sqlite", "mysql", "mssql").
|
||||
func SQL(dialect string) (string, error) {
|
||||
b, err := files.ReadFile(dialect + ".sql")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("ddl: no reference schema for dialect %q", dialect)
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// Statements returns the schema as separate statements, for drivers that reject
|
||||
// multi-statement execution. Comment-only lines are dropped.
|
||||
func Statements(dialect string) ([]string, error) {
|
||||
s, err := SQL(dialect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []string
|
||||
var cur strings.Builder
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
t := strings.TrimSpace(line)
|
||||
if t == "" || strings.HasPrefix(t, "--") {
|
||||
continue
|
||||
}
|
||||
cur.WriteString(line)
|
||||
cur.WriteString("\n")
|
||||
if strings.HasSuffix(t, ";") {
|
||||
out = append(out, strings.TrimSpace(cur.String()))
|
||||
cur.Reset()
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package ddl_test
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
_ "github.com/glebarez/go-sqlite"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security/lookup"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/security/lookup/ddl"
|
||||
)
|
||||
|
||||
func TestStatementsAllDialects(t *testing.T) {
|
||||
for _, d := range []string{"postgres", "sqlite", "mysql", "mssql"} {
|
||||
st, err := ddl.Statements(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(st) < 12 {
|
||||
t.Errorf("%s: %d statements", d, len(st))
|
||||
}
|
||||
all := strings.Join(st, "\n")
|
||||
for _, tbl := range []string{"users", "user_sessions", "user_keys", "oauth_codes", "sec_group_members", "sec_column_rules", "sec_row_rules"} {
|
||||
if !strings.Contains(all, tbl+" (") {
|
||||
t.Errorf("%s: missing table %s", d, tbl)
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, err := ddl.SQL("oracle"); err == nil {
|
||||
t.Error("unknown dialect must error")
|
||||
}
|
||||
}
|
||||
|
||||
// Every table and column the default schema names must exist in the sqlite reference DDL.
|
||||
func TestSQLiteMatchesDefaultSchema(t *testing.T) {
|
||||
db, err := sql.Open("sqlite", ":memory:")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(1)
|
||||
s, _ := ddl.SQL("sqlite")
|
||||
if _, err := db.Exec(s); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.Exec(s); err != nil {
|
||||
t.Fatalf("script must be re-runnable: %v", err)
|
||||
}
|
||||
sc := lookup.DefaultSchema()
|
||||
for _, tbl := range sc {
|
||||
for _, c := range tbl.Columns {
|
||||
if _, err := db.Exec("SELECT " + c + " FROM " + tbl.Name + " WHERE 1=0"); err != nil {
|
||||
t.Errorf("%s.%s: %v", tbl.Name, c, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
-- Reference schema for the lookup direct backend: Microsoft SQL Server 2016+. Direct backend only. Run each statement separately (see ddl.Statements).
|
||||
-- Table and column names are the lookup.DefaultSchema defaults, override them with lookup.Config.Schema.
|
||||
-- Generated by the project, edit freely for your deployment (types, collations, extra columns).
|
||||
|
||||
-- password: bcrypt hash (nullable for OAuth2 users), legacy cleartext is accepted at login
|
||||
-- roles: comma-separated roles
|
||||
|
||||
IF OBJECT_ID(N'users', N'U') IS NULL
|
||||
CREATE TABLE users (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
username NVARCHAR(255) NOT NULL UNIQUE,
|
||||
email NVARCHAR(255) NOT NULL UNIQUE,
|
||||
password NVARCHAR(255),
|
||||
user_level INT DEFAULT 0,
|
||||
roles NVARCHAR(500),
|
||||
is_active BIT DEFAULT 1,
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
updated_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
last_login_at DATETIME2,
|
||||
program_user_id INT DEFAULT 0,
|
||||
program_user_table NVARCHAR(255) DEFAULT '',
|
||||
remote_id NVARCHAR(255),
|
||||
auth_provider NVARCHAR(50),
|
||||
totp_secret NVARCHAR(255),
|
||||
totp_enabled BIT DEFAULT 0,
|
||||
totp_enabled_at DATETIME2
|
||||
);
|
||||
|
||||
|
||||
IF OBJECT_ID(N'user_sessions', N'U') IS NULL
|
||||
CREATE TABLE user_sessions (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
session_token NVARCHAR(450) NOT NULL UNIQUE,
|
||||
user_id INT NOT NULL,
|
||||
expires_at DATETIME2 NOT NULL,
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
last_activity_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
ip_address NVARCHAR(45),
|
||||
user_agent NVARCHAR(MAX),
|
||||
access_token NVARCHAR(MAX),
|
||||
refresh_token NVARCHAR(MAX),
|
||||
token_type NVARCHAR(50) DEFAULT 'Bearer',
|
||||
auth_provider NVARCHAR(50),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_sessions_user_id' AND object_id = OBJECT_ID(N'user_sessions'))
|
||||
CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_sessions_expires_at' AND object_id = OBJECT_ID(N'user_sessions'))
|
||||
CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at);
|
||||
|
||||
|
||||
IF OBJECT_ID(N'token_blacklist', N'U') IS NULL
|
||||
CREATE TABLE token_blacklist (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
token NVARCHAR(500) NOT NULL,
|
||||
user_id INT,
|
||||
expires_at DATETIME2 NOT NULL,
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
-- code_hash: SHA-256 hex of the backup code
|
||||
|
||||
IF OBJECT_ID(N'user_totp_backup_codes', N'U') IS NULL
|
||||
CREATE TABLE user_totp_backup_codes (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
code_hash NVARCHAR(64) NOT NULL,
|
||||
used BIT DEFAULT 0,
|
||||
used_at DATETIME2,
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_totp_user_id' AND object_id = OBJECT_ID(N'user_totp_backup_codes'))
|
||||
CREATE INDEX idx_totp_user_id ON user_totp_backup_codes(user_id);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_totp_code_hash' AND object_id = OBJECT_ID(N'user_totp_backup_codes'))
|
||||
CREATE INDEX idx_totp_code_hash ON user_totp_backup_codes(code_hash);
|
||||
|
||||
|
||||
-- credential_id: base64 text
|
||||
-- public_key: base64 text
|
||||
-- aaguid: base64 text
|
||||
-- transports: JSON-encoded array
|
||||
|
||||
IF OBJECT_ID(N'user_passkey_credentials', N'U') IS NULL
|
||||
CREATE TABLE user_passkey_credentials (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
credential_id VARCHAR(900) NOT NULL UNIQUE,
|
||||
public_key NVARCHAR(MAX) NOT NULL,
|
||||
attestation_type NVARCHAR(50) DEFAULT 'none',
|
||||
aaguid NVARCHAR(MAX),
|
||||
sign_count INT DEFAULT 0,
|
||||
clone_warning BIT DEFAULT 0,
|
||||
transports NVARCHAR(MAX),
|
||||
backup_eligible BIT DEFAULT 0,
|
||||
backup_state BIT DEFAULT 0,
|
||||
name NVARCHAR(255),
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
last_used_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_passkey_user_id' AND object_id = OBJECT_ID(N'user_passkey_credentials'))
|
||||
CREATE INDEX idx_passkey_user_id ON user_passkey_credentials(user_id);
|
||||
|
||||
|
||||
-- token_hash: SHA-256 hex of the raw token
|
||||
|
||||
IF OBJECT_ID(N'user_password_resets', N'U') IS NULL
|
||||
CREATE TABLE user_password_resets (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
token_hash NVARCHAR(64) NOT NULL UNIQUE,
|
||||
expires_at DATETIME2 NOT NULL,
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
used BIT DEFAULT 0,
|
||||
used_at DATETIME2,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_pw_reset_user_id' AND object_id = OBJECT_ID(N'user_password_resets'))
|
||||
CREATE INDEX idx_pw_reset_user_id ON user_password_resets(user_id);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_pw_reset_expires_at' AND object_id = OBJECT_ID(N'user_password_resets'))
|
||||
CREATE INDEX idx_pw_reset_expires_at ON user_password_resets(expires_at);
|
||||
|
||||
|
||||
-- redirect_uris: JSON-encoded array
|
||||
-- grant_types: JSON-encoded array
|
||||
-- allowed_scopes: JSON-encoded array
|
||||
-- client_secret_hash: SHA-256 hex of the confidential-client secret, NULL for public clients
|
||||
|
||||
IF OBJECT_ID(N'oauth_clients', N'U') IS NULL
|
||||
CREATE TABLE oauth_clients (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
client_id NVARCHAR(255) NOT NULL UNIQUE,
|
||||
redirect_uris NVARCHAR(MAX) NOT NULL,
|
||||
client_name NVARCHAR(255),
|
||||
grant_types NVARCHAR(MAX),
|
||||
allowed_scopes NVARCHAR(MAX),
|
||||
client_secret_hash NVARCHAR(MAX),
|
||||
token_endpoint_auth_method NVARCHAR(30) DEFAULT 'none',
|
||||
is_active BIT DEFAULT 1,
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME()
|
||||
);
|
||||
|
||||
|
||||
-- scopes: JSON-encoded array
|
||||
|
||||
IF OBJECT_ID(N'oauth_codes', N'U') IS NULL
|
||||
CREATE TABLE oauth_codes (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
code NVARCHAR(255) NOT NULL UNIQUE,
|
||||
client_id NVARCHAR(255) NOT NULL,
|
||||
redirect_uri NVARCHAR(MAX) NOT NULL,
|
||||
client_state NVARCHAR(MAX),
|
||||
code_challenge NVARCHAR(255) NOT NULL,
|
||||
code_challenge_method NVARCHAR(10) DEFAULT 'S256',
|
||||
session_token NVARCHAR(MAX) NOT NULL,
|
||||
refresh_token NVARCHAR(MAX),
|
||||
scopes NVARCHAR(MAX),
|
||||
expires_at DATETIME2 NOT NULL,
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME()
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_oauth_codes_expires' AND object_id = OBJECT_ID(N'oauth_codes'))
|
||||
CREATE INDEX idx_oauth_codes_expires ON oauth_codes(expires_at);
|
||||
|
||||
|
||||
-- key_hash: SHA-256 hex
|
||||
-- scopes: JSON-encoded array
|
||||
-- meta: JSON-encoded object
|
||||
|
||||
IF OBJECT_ID(N'user_keys', N'U') IS NULL
|
||||
CREATE TABLE user_keys (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
key_type NVARCHAR(50) NOT NULL,
|
||||
key_hash NVARCHAR(64) NOT NULL UNIQUE,
|
||||
name NVARCHAR(255) NOT NULL DEFAULT '',
|
||||
scopes NVARCHAR(MAX),
|
||||
meta NVARCHAR(MAX),
|
||||
expires_at DATETIME2,
|
||||
created_at DATETIME2 DEFAULT SYSUTCDATETIME(),
|
||||
last_used_at DATETIME2,
|
||||
is_active BIT DEFAULT 1,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_keys_user_id' AND object_id = OBJECT_ID(N'user_keys'))
|
||||
CREATE INDEX idx_user_keys_user_id ON user_keys(user_id);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_user_keys_key_type' AND object_id = OBJECT_ID(N'user_keys'))
|
||||
CREATE INDEX idx_user_keys_key_type ON user_keys(key_type);
|
||||
|
||||
|
||||
-- Optional: omit to use per-user rules only.
|
||||
|
||||
IF OBJECT_ID(N'sec_group_members', N'U') IS NULL
|
||||
CREATE TABLE sec_group_members (
|
||||
group_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
PRIMARY KEY (group_id, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
-- column_path: dot path under the table: col or col.sub.field
|
||||
-- access_type: mask, hide, read, ...
|
||||
-- extra_filters: JSON object
|
||||
|
||||
IF OBJECT_ID(N'sec_column_rules', N'U') IS NULL
|
||||
CREATE TABLE sec_column_rules (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT,
|
||||
group_id INT,
|
||||
schema_name NVARCHAR(255) NOT NULL,
|
||||
table_name NVARCHAR(255) NOT NULL,
|
||||
column_path NVARCHAR(255) NOT NULL,
|
||||
access_type NVARCHAR(50) NOT NULL,
|
||||
mask_start INT DEFAULT 0,
|
||||
mask_end INT DEFAULT 0,
|
||||
mask_invert BIT DEFAULT 0,
|
||||
mask_char NVARCHAR(10) DEFAULT '*',
|
||||
extra_filters NVARCHAR(MAX),
|
||||
is_active BIT NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL))
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_sec_column_rules_table' AND object_id = OBJECT_ID(N'sec_column_rules'))
|
||||
CREATE INDEX idx_sec_column_rules_table ON sec_column_rules(schema_name, table_name);
|
||||
|
||||
|
||||
-- template: SQL fragment, e.g. user_id = {UserID}
|
||||
|
||||
IF OBJECT_ID(N'sec_row_rules', N'U') IS NULL
|
||||
CREATE TABLE sec_row_rules (
|
||||
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
user_id INT,
|
||||
group_id INT,
|
||||
schema_name NVARCHAR(255) NOT NULL,
|
||||
table_name NVARCHAR(255) NOT NULL,
|
||||
template NVARCHAR(MAX),
|
||||
has_block BIT NOT NULL DEFAULT 0,
|
||||
is_active BIT NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL))
|
||||
);
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'idx_sec_row_rules_table' AND object_id = OBJECT_ID(N'sec_row_rules'))
|
||||
CREATE INDEX idx_sec_row_rules_table ON sec_row_rules(schema_name, table_name);
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
-- Reference schema for the lookup direct backend: MySQL 8.0.16+ / MariaDB 10.2+. Direct backend only. Run each statement separately (see ddl.Statements) unless multiStatements=true.
|
||||
-- Table and column names are the lookup.DefaultSchema defaults, override them with lookup.Config.Schema.
|
||||
-- Generated by the project, edit freely for your deployment (types, collations, extra columns).
|
||||
|
||||
-- password: bcrypt hash (nullable for OAuth2 users), legacy cleartext is accepted at login
|
||||
-- roles: comma-separated roles
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255),
|
||||
user_level INT DEFAULT 0,
|
||||
roles VARCHAR(500),
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
created_at DATETIME NULL,
|
||||
updated_at DATETIME NULL,
|
||||
last_login_at DATETIME,
|
||||
program_user_id INT DEFAULT 0,
|
||||
program_user_table VARCHAR(255) DEFAULT '',
|
||||
remote_id VARCHAR(255),
|
||||
auth_provider VARCHAR(50),
|
||||
totp_secret VARCHAR(255),
|
||||
totp_enabled TINYINT(1) DEFAULT 0,
|
||||
totp_enabled_at DATETIME
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_token VARCHAR(500) NOT NULL UNIQUE,
|
||||
user_id INT NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NULL,
|
||||
last_activity_at DATETIME NULL,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
token_type VARCHAR(50) DEFAULT 'Bearer',
|
||||
auth_provider VARCHAR(50),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_user_sessions_user_id (user_id),
|
||||
INDEX idx_user_sessions_expires_at (expires_at)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS token_blacklist (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
token VARCHAR(500) NOT NULL,
|
||||
user_id INT,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
-- code_hash: SHA-256 hex of the backup code
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_totp_backup_codes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
code_hash VARCHAR(64) NOT NULL,
|
||||
used TINYINT(1) DEFAULT 0,
|
||||
used_at DATETIME,
|
||||
created_at DATETIME NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_totp_user_id (user_id),
|
||||
INDEX idx_totp_code_hash (code_hash)
|
||||
);
|
||||
|
||||
|
||||
-- credential_id: base64 text
|
||||
-- public_key: base64 text
|
||||
-- aaguid: base64 text
|
||||
-- transports: JSON-encoded array
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_passkey_credentials (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
credential_id VARCHAR(1400) CHARACTER SET ascii NOT NULL UNIQUE,
|
||||
public_key TEXT NOT NULL,
|
||||
attestation_type VARCHAR(50) DEFAULT 'none',
|
||||
aaguid TEXT,
|
||||
sign_count INT DEFAULT 0,
|
||||
clone_warning TINYINT(1) DEFAULT 0,
|
||||
transports TEXT,
|
||||
backup_eligible TINYINT(1) DEFAULT 0,
|
||||
backup_state TINYINT(1) DEFAULT 0,
|
||||
name VARCHAR(255),
|
||||
created_at DATETIME NULL,
|
||||
last_used_at DATETIME NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_passkey_user_id (user_id)
|
||||
);
|
||||
|
||||
|
||||
-- token_hash: SHA-256 hex of the raw token
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_password_resets (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
token_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NULL,
|
||||
used TINYINT(1) DEFAULT 0,
|
||||
used_at DATETIME,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_pw_reset_user_id (user_id),
|
||||
INDEX idx_pw_reset_expires_at (expires_at)
|
||||
);
|
||||
|
||||
|
||||
-- redirect_uris: JSON-encoded array
|
||||
-- grant_types: JSON-encoded array
|
||||
-- allowed_scopes: JSON-encoded array
|
||||
-- client_secret_hash: SHA-256 hex of the confidential-client secret, NULL for public clients
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_clients (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
client_id VARCHAR(255) NOT NULL UNIQUE,
|
||||
redirect_uris TEXT NOT NULL,
|
||||
client_name VARCHAR(255),
|
||||
grant_types TEXT,
|
||||
allowed_scopes TEXT,
|
||||
client_secret_hash TEXT,
|
||||
token_endpoint_auth_method VARCHAR(30) DEFAULT 'none',
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
created_at DATETIME NULL
|
||||
);
|
||||
|
||||
|
||||
-- scopes: JSON-encoded array
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_codes (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(255) NOT NULL UNIQUE,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
redirect_uri TEXT NOT NULL,
|
||||
client_state TEXT,
|
||||
code_challenge VARCHAR(255) NOT NULL,
|
||||
code_challenge_method VARCHAR(10) DEFAULT 'S256',
|
||||
session_token TEXT NOT NULL,
|
||||
refresh_token TEXT,
|
||||
scopes TEXT,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NULL,
|
||||
INDEX idx_oauth_codes_expires (expires_at)
|
||||
);
|
||||
|
||||
|
||||
-- key_hash: SHA-256 hex
|
||||
-- scopes: JSON-encoded array
|
||||
-- meta: JSON-encoded object
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_keys (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT NOT NULL,
|
||||
key_type VARCHAR(50) NOT NULL,
|
||||
key_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
scopes TEXT,
|
||||
meta TEXT,
|
||||
expires_at DATETIME,
|
||||
created_at DATETIME NULL,
|
||||
last_used_at DATETIME,
|
||||
is_active TINYINT(1) DEFAULT 1,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
INDEX idx_user_keys_user_id (user_id),
|
||||
INDEX idx_user_keys_key_type (key_type)
|
||||
);
|
||||
|
||||
|
||||
-- Optional: omit to use per-user rules only.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_group_members (
|
||||
group_id INT NOT NULL,
|
||||
user_id INT NOT NULL,
|
||||
PRIMARY KEY (group_id, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
-- column_path: dot path under the table: col or col.sub.field
|
||||
-- access_type: mask, hide, read, ...
|
||||
-- extra_filters: JSON object
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_column_rules (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
group_id INT,
|
||||
schema_name VARCHAR(255) NOT NULL,
|
||||
table_name VARCHAR(255) NOT NULL,
|
||||
column_path VARCHAR(255) NOT NULL,
|
||||
access_type VARCHAR(50) NOT NULL,
|
||||
mask_start INT DEFAULT 0,
|
||||
mask_end INT DEFAULT 0,
|
||||
mask_invert TINYINT(1) DEFAULT 0,
|
||||
mask_char VARCHAR(10) DEFAULT '*',
|
||||
extra_filters TEXT,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL)),
|
||||
INDEX idx_sec_column_rules_table (schema_name, table_name)
|
||||
);
|
||||
|
||||
|
||||
-- template: SQL fragment, e.g. user_id = {UserID}
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_row_rules (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
group_id INT,
|
||||
schema_name VARCHAR(255) NOT NULL,
|
||||
table_name VARCHAR(255) NOT NULL,
|
||||
template TEXT,
|
||||
has_block TINYINT(1) NOT NULL DEFAULT 0,
|
||||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL)),
|
||||
INDEX idx_sec_row_rules_table (schema_name, table_name)
|
||||
);
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
-- Reference schema for the lookup direct backend: PostgreSQL (tables only, no stored procedures). Use with lookup.Config{Mode: lookup.ModeDirect}.
|
||||
-- Do not combine with the procedure schema (database_schema.sql): that schema stores credential ids as bytea and
|
||||
-- list columns as text[], this one stores base64 / JSON text which is what the direct backend reads and writes.
|
||||
-- Table and column names are the lookup.DefaultSchema defaults, override them with lookup.Config.Schema.
|
||||
-- Generated by the project, edit freely for your deployment (types, collations, extra columns).
|
||||
|
||||
-- password: bcrypt hash (nullable for OAuth2 users), legacy cleartext is accepted at login
|
||||
-- roles: comma-separated roles
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255),
|
||||
user_level INTEGER DEFAULT 0,
|
||||
roles VARCHAR(500),
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at TIMESTAMP,
|
||||
program_user_id INTEGER DEFAULT 0,
|
||||
program_user_table VARCHAR(255) DEFAULT '',
|
||||
remote_id VARCHAR(255),
|
||||
auth_provider VARCHAR(50),
|
||||
totp_secret VARCHAR(255),
|
||||
totp_enabled BOOLEAN DEFAULT false,
|
||||
totp_enabled_at TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
session_token VARCHAR(500) NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_activity_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
token_type VARCHAR(50) DEFAULT 'Bearer',
|
||||
auth_provider VARCHAR(50),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_expires_at ON user_sessions(expires_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_refresh_token ON user_sessions(refresh_token);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS token_blacklist (
|
||||
id SERIAL PRIMARY KEY,
|
||||
token VARCHAR(500) NOT NULL,
|
||||
user_id INTEGER,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
-- code_hash: SHA-256 hex of the backup code
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_totp_backup_codes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
code_hash VARCHAR(64) NOT NULL,
|
||||
used BOOLEAN DEFAULT false,
|
||||
used_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_totp_user_id ON user_totp_backup_codes(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_totp_code_hash ON user_totp_backup_codes(code_hash);
|
||||
|
||||
|
||||
-- credential_id: base64 text
|
||||
-- public_key: base64 text
|
||||
-- aaguid: base64 text
|
||||
-- transports: JSON-encoded array
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_passkey_credentials (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key TEXT NOT NULL,
|
||||
attestation_type VARCHAR(50) DEFAULT 'none',
|
||||
aaguid TEXT,
|
||||
sign_count INTEGER DEFAULT 0,
|
||||
clone_warning BOOLEAN DEFAULT false,
|
||||
transports TEXT,
|
||||
backup_eligible BOOLEAN DEFAULT false,
|
||||
backup_state BOOLEAN DEFAULT false,
|
||||
name VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_passkey_user_id ON user_passkey_credentials(user_id);
|
||||
|
||||
|
||||
-- token_hash: SHA-256 hex of the raw token
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_password_resets (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
token_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
used BOOLEAN DEFAULT false,
|
||||
used_at TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pw_reset_user_id ON user_password_resets(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pw_reset_expires_at ON user_password_resets(expires_at);
|
||||
|
||||
|
||||
-- redirect_uris: JSON-encoded array
|
||||
-- grant_types: JSON-encoded array
|
||||
-- allowed_scopes: JSON-encoded array
|
||||
-- client_secret_hash: SHA-256 hex of the confidential-client secret, NULL for public clients
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_clients (
|
||||
id SERIAL PRIMARY KEY,
|
||||
client_id VARCHAR(255) NOT NULL UNIQUE,
|
||||
redirect_uris TEXT NOT NULL,
|
||||
client_name VARCHAR(255),
|
||||
grant_types TEXT,
|
||||
allowed_scopes TEXT,
|
||||
client_secret_hash TEXT,
|
||||
token_endpoint_auth_method VARCHAR(30) DEFAULT 'none',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
-- scopes: JSON-encoded array
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_codes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
code VARCHAR(255) NOT NULL UNIQUE,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
redirect_uri TEXT NOT NULL,
|
||||
client_state TEXT,
|
||||
code_challenge VARCHAR(255) NOT NULL,
|
||||
code_challenge_method VARCHAR(10) DEFAULT 'S256',
|
||||
session_token TEXT NOT NULL,
|
||||
refresh_token TEXT,
|
||||
scopes TEXT,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oauth_codes_expires ON oauth_codes(expires_at);
|
||||
|
||||
|
||||
-- key_hash: SHA-256 hex
|
||||
-- scopes: JSON-encoded array
|
||||
-- meta: JSON-encoded object
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_keys (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
key_type VARCHAR(50) NOT NULL,
|
||||
key_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
scopes TEXT,
|
||||
meta TEXT,
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_keys_user_id ON user_keys(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_keys_key_type ON user_keys(key_type);
|
||||
|
||||
|
||||
-- Optional: omit to use per-user rules only.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_group_members (
|
||||
group_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (group_id, user_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
-- column_path: dot path under the table: col or col.sub.field
|
||||
-- access_type: mask, hide, read, ...
|
||||
-- extra_filters: JSON object
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_column_rules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
group_id INTEGER,
|
||||
schema_name TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
column_path TEXT NOT NULL,
|
||||
access_type VARCHAR(50) NOT NULL,
|
||||
mask_start INTEGER DEFAULT 0,
|
||||
mask_end INTEGER DEFAULT 0,
|
||||
mask_invert BOOLEAN DEFAULT false,
|
||||
mask_char VARCHAR(10) DEFAULT '*',
|
||||
extra_filters TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sec_column_rules_table ON sec_column_rules(schema_name, table_name);
|
||||
|
||||
|
||||
-- template: SQL fragment, e.g. user_id = {UserID}
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_row_rules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
group_id INTEGER,
|
||||
schema_name TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
template TEXT,
|
||||
has_block BOOLEAN NOT NULL DEFAULT false,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sec_row_rules_table ON sec_row_rules(schema_name, table_name);
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
-- Reference schema for the lookup direct backend: SQLite. Direct backend only.
|
||||
-- Table and column names are the lookup.DefaultSchema defaults, override them with lookup.Config.Schema.
|
||||
-- Generated by the project, edit freely for your deployment (types, collations, extra columns).
|
||||
|
||||
-- password: bcrypt hash (nullable for OAuth2 users), legacy cleartext is accepted at login
|
||||
-- roles: comma-separated roles
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255),
|
||||
user_level INTEGER DEFAULT 0,
|
||||
roles VARCHAR(500),
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP,
|
||||
last_login_at TIMESTAMP,
|
||||
program_user_id INTEGER DEFAULT 0,
|
||||
program_user_table VARCHAR(255) DEFAULT '',
|
||||
remote_id VARCHAR(255),
|
||||
auth_provider VARCHAR(50),
|
||||
totp_secret VARCHAR(255),
|
||||
totp_enabled BOOLEAN DEFAULT 0,
|
||||
totp_enabled_at TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_token VARCHAR(500) NOT NULL UNIQUE,
|
||||
user_id INTEGER NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP,
|
||||
last_activity_at TIMESTAMP,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
access_token TEXT,
|
||||
refresh_token TEXT,
|
||||
token_type VARCHAR(50) DEFAULT 'Bearer',
|
||||
auth_provider VARCHAR(50)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_expires_at ON user_sessions(expires_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_sessions_refresh_token ON user_sessions(refresh_token);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS token_blacklist (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token VARCHAR(500) NOT NULL,
|
||||
user_id INTEGER,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
-- code_hash: SHA-256 hex of the backup code
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_totp_backup_codes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
code_hash VARCHAR(64) NOT NULL,
|
||||
used BOOLEAN DEFAULT 0,
|
||||
used_at TIMESTAMP,
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_totp_user_id ON user_totp_backup_codes(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_totp_code_hash ON user_totp_backup_codes(code_hash);
|
||||
|
||||
|
||||
-- credential_id: base64 text
|
||||
-- public_key: base64 text
|
||||
-- aaguid: base64 text
|
||||
-- transports: JSON-encoded array
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_passkey_credentials (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
credential_id TEXT NOT NULL UNIQUE,
|
||||
public_key TEXT NOT NULL,
|
||||
attestation_type VARCHAR(50) DEFAULT 'none',
|
||||
aaguid TEXT,
|
||||
sign_count INTEGER DEFAULT 0,
|
||||
clone_warning BOOLEAN DEFAULT 0,
|
||||
transports TEXT,
|
||||
backup_eligible BOOLEAN DEFAULT 0,
|
||||
backup_state BOOLEAN DEFAULT 0,
|
||||
name VARCHAR(255),
|
||||
created_at TIMESTAMP,
|
||||
last_used_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_passkey_user_id ON user_passkey_credentials(user_id);
|
||||
|
||||
|
||||
-- token_hash: SHA-256 hex of the raw token
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_password_resets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP,
|
||||
used BOOLEAN DEFAULT 0,
|
||||
used_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pw_reset_user_id ON user_password_resets(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pw_reset_expires_at ON user_password_resets(expires_at);
|
||||
|
||||
|
||||
-- redirect_uris: JSON-encoded array
|
||||
-- grant_types: JSON-encoded array
|
||||
-- allowed_scopes: JSON-encoded array
|
||||
-- client_secret_hash: SHA-256 hex of the confidential-client secret, NULL for public clients
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_clients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
client_id VARCHAR(255) NOT NULL UNIQUE,
|
||||
redirect_uris TEXT NOT NULL,
|
||||
client_name VARCHAR(255),
|
||||
grant_types TEXT,
|
||||
allowed_scopes TEXT,
|
||||
client_secret_hash TEXT,
|
||||
token_endpoint_auth_method VARCHAR(30) DEFAULT 'none',
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
|
||||
|
||||
-- scopes: JSON-encoded array
|
||||
|
||||
CREATE TABLE IF NOT EXISTS oauth_codes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
code VARCHAR(255) NOT NULL UNIQUE,
|
||||
client_id VARCHAR(255) NOT NULL,
|
||||
redirect_uri TEXT NOT NULL,
|
||||
client_state TEXT,
|
||||
code_challenge VARCHAR(255) NOT NULL,
|
||||
code_challenge_method VARCHAR(10) DEFAULT 'S256',
|
||||
session_token TEXT NOT NULL,
|
||||
refresh_token TEXT,
|
||||
scopes TEXT,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_oauth_codes_expires ON oauth_codes(expires_at);
|
||||
|
||||
|
||||
-- key_hash: SHA-256 hex
|
||||
-- scopes: JSON-encoded array
|
||||
-- meta: JSON-encoded object
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_keys (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
key_type VARCHAR(50) NOT NULL,
|
||||
key_hash VARCHAR(64) NOT NULL UNIQUE,
|
||||
name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
scopes TEXT,
|
||||
meta TEXT,
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP,
|
||||
last_used_at TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_keys_user_id ON user_keys(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_keys_key_type ON user_keys(key_type);
|
||||
|
||||
|
||||
-- Optional: omit to use per-user rules only.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_group_members (
|
||||
group_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (group_id, user_id)
|
||||
);
|
||||
|
||||
|
||||
-- column_path: dot path under the table: col or col.sub.field
|
||||
-- access_type: mask, hide, read, ...
|
||||
-- extra_filters: JSON object
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_column_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
group_id INTEGER,
|
||||
schema_name TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
column_path TEXT NOT NULL,
|
||||
access_type VARCHAR(50) NOT NULL,
|
||||
mask_start INTEGER DEFAULT 0,
|
||||
mask_end INTEGER DEFAULT 0,
|
||||
mask_invert BOOLEAN DEFAULT 0,
|
||||
mask_char VARCHAR(10) DEFAULT '*',
|
||||
extra_filters TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sec_column_rules_table ON sec_column_rules(schema_name, table_name);
|
||||
|
||||
|
||||
-- template: SQL fragment, e.g. user_id = {UserID}
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sec_row_rules (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
group_id INTEGER,
|
||||
schema_name TEXT NOT NULL,
|
||||
table_name TEXT NOT NULL,
|
||||
template TEXT,
|
||||
has_block BOOLEAN NOT NULL DEFAULT 0,
|
||||
is_active BOOLEAN NOT NULL DEFAULT 1,
|
||||
CHECK ((user_id IS NULL AND group_id IS NOT NULL) OR (user_id IS NOT NULL AND group_id IS NULL))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sec_row_rules_table ON sec_row_rules(schema_name, table_name);
|
||||
|
||||
Reference in New Issue
Block a user