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:
@@ -5,7 +5,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -18,36 +17,6 @@ import (
|
||||
"golang.org/x/sync/singleflight"
|
||||
)
|
||||
|
||||
type ColumnSecurity struct {
|
||||
Schema string `json:"schema"`
|
||||
Tablename string `json:"tablename"`
|
||||
Path []string `json:"path"`
|
||||
ExtraFilters map[string]string `json:"extra_filters"`
|
||||
UserID int `json:"user_id"`
|
||||
Accesstype string `json:"accesstype"`
|
||||
MaskStart int `json:"mask_start"`
|
||||
MaskEnd int `json:"mask_end"`
|
||||
MaskInvert bool `json:"mask_invert"`
|
||||
MaskChar string `json:"mask_char"`
|
||||
Control string `json:"control"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
type RowSecurity struct {
|
||||
Schema string `json:"schema"`
|
||||
Tablename string `json:"tablename"`
|
||||
Template string `json:"template"`
|
||||
HasBlock bool `json:"has_block"`
|
||||
// UserID is the opaque user reference the security rules were loaded for.
|
||||
// It may be an int, a string/UUID, or a *UserContext, depending on what the
|
||||
// RowSecurityProvider/SecurityContext.GetUserRef implementation returns.
|
||||
UserID any `json:"user_id"`
|
||||
}
|
||||
|
||||
// safeIdentRe matches an unquoted SQL identifier. Identifiers substituted into a
|
||||
// row-security template must match it; anything else is rejected.
|
||||
var safeIdentRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
// ErrNoRowSecurity is returned by GetRowSecurityTemplate when no row security
|
||||
// entry is loaded for the user and table. It means "no rules", as opposed to a
|
||||
// failure, which callers must treat as fatal.
|
||||
@@ -56,66 +25,6 @@ var ErrNoRowSecurity = errors.New("no row security data")
|
||||
// ErrNoColumnSecurity is the column-security equivalent of ErrNoRowSecurity.
|
||||
var ErrNoColumnSecurity = errors.New("no column security data")
|
||||
|
||||
// userIDScalar reduces the opaque user reference to a scalar that is safe to
|
||||
// bind as a query argument. A *UserContext is reduced to its UserID; other
|
||||
// structured values are rejected rather than stringified into SQL.
|
||||
func userIDScalar(ref any) (any, error) {
|
||||
switch v := ref.(type) {
|
||||
case nil:
|
||||
return nil, fmt.Errorf("row security: no user reference")
|
||||
case *UserContext:
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf("row security: nil user context")
|
||||
}
|
||||
return v.UserID, nil
|
||||
case UserContext:
|
||||
return v.UserID, nil
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
|
||||
return v, nil
|
||||
case string:
|
||||
return v, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("row security: unsupported user reference type %T", ref)
|
||||
}
|
||||
}
|
||||
|
||||
// GetTemplate expands the row-security template into a WHERE clause and its
|
||||
// bind arguments. {PrimaryKeyName}, {TableName} and {SchemaName} are validated
|
||||
// identifiers substituted in place; every {UserID} becomes a `?` placeholder
|
||||
// with the user reference bound as an argument, so user data never reaches the
|
||||
// SQL text.
|
||||
func (m *RowSecurity) GetTemplate(pPrimaryKeyName string, pModelType reflect.Type) (clause string, args []any, err error) {
|
||||
str := m.Template
|
||||
|
||||
for placeholder, ident := range map[string]string{
|
||||
"{PrimaryKeyName}": pPrimaryKeyName,
|
||||
"{TableName}": m.Tablename,
|
||||
"{SchemaName}": m.Schema,
|
||||
} {
|
||||
if !strings.Contains(str, placeholder) {
|
||||
continue
|
||||
}
|
||||
if !safeIdentRe.MatchString(ident) {
|
||||
return "", nil, fmt.Errorf("row security: invalid identifier %q for %s", ident, placeholder)
|
||||
}
|
||||
str = strings.ReplaceAll(str, placeholder, ident)
|
||||
}
|
||||
|
||||
n := strings.Count(str, "{UserID}")
|
||||
if n == 0 {
|
||||
return str, nil, nil
|
||||
}
|
||||
uid, err := userIDScalar(m.UserID)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
args = make([]any, n)
|
||||
for i := range args {
|
||||
args[i] = uid
|
||||
}
|
||||
return strings.ReplaceAll(str, "{UserID}", "?"), args, nil
|
||||
}
|
||||
|
||||
// SecurityList manages security state and caching
|
||||
// It wraps a SecurityProvider and provides caching and utility methods
|
||||
type SecurityList struct {
|
||||
|
||||
Reference in New Issue
Block a user