mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 19:20:31 +00:00
ApplyRowSecurity only accepted common.SelectQuery, so the BeforeScan hook failed closed on update/delete when a row-security template existed. Type-switch on SelectQuery, UpdateQuery and DeleteQuery; other types still return an error.
722 lines
18 KiB
Go
722 lines
18 KiB
Go
package security
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
|
)
|
|
|
|
// Mock SecurityContext for testing hooks
|
|
type mockSecurityContext struct {
|
|
ctx context.Context
|
|
userID int
|
|
hasUser bool
|
|
schema string
|
|
entity string
|
|
model interface{}
|
|
query interface{}
|
|
result interface{}
|
|
userRef any
|
|
}
|
|
|
|
func (m *mockSecurityContext) GetContext() context.Context {
|
|
return m.ctx
|
|
}
|
|
|
|
func (m *mockSecurityContext) GetUserID() (int, bool) {
|
|
return m.userID, m.hasUser
|
|
}
|
|
|
|
func (m *mockSecurityContext) GetUserRef() (any, bool) {
|
|
if m.userRef != nil {
|
|
return m.userRef, m.hasUser
|
|
}
|
|
return m.userID, m.hasUser
|
|
}
|
|
|
|
func (m *mockSecurityContext) GetSchema() string {
|
|
return m.schema
|
|
}
|
|
|
|
func (m *mockSecurityContext) GetEntity() string {
|
|
return m.entity
|
|
}
|
|
|
|
func (m *mockSecurityContext) GetModel() interface{} {
|
|
return m.model
|
|
}
|
|
|
|
func (m *mockSecurityContext) GetQuery() interface{} {
|
|
return m.query
|
|
}
|
|
|
|
func (m *mockSecurityContext) SetQuery(q interface{}) {
|
|
m.query = q
|
|
}
|
|
|
|
func (m *mockSecurityContext) GetResult() interface{} {
|
|
return m.result
|
|
}
|
|
|
|
func (m *mockSecurityContext) SetResult(r interface{}) {
|
|
m.result = r
|
|
}
|
|
|
|
// Test helper functions
|
|
func TestContains(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
s string
|
|
substr string
|
|
expected bool
|
|
}{
|
|
{"substring at start", "hello world", "hello", true},
|
|
{"substring at end", "hello world", "world", true},
|
|
{"substring in middle", "hello world", "lo wo", false}, // contains only checks prefix/suffix
|
|
{"substring not present", "hello world", "xyz", false},
|
|
{"exact match", "test", "test", true},
|
|
{"empty substring", "test", "", true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := contains(tt.s, tt.substr)
|
|
if result != tt.expected {
|
|
t.Errorf("contains(%q, %q) = %v, want %v", tt.s, tt.substr, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExtractSQLName(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tag string
|
|
expected string
|
|
}{
|
|
{"simple name", "user_id", "user_id"},
|
|
{"column prefix", "column:email", "column:email"}, // Implementation doesn't strip prefix in all cases
|
|
{"with other tags", "id,pk,autoincrement", "id"},
|
|
{"column with comma", "column:user_name,notnull", "column:user_name"}, // Implementation behavior
|
|
{"empty tag", "", ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := extractSQLName(tt.tag)
|
|
if result != tt.expected {
|
|
t.Errorf("extractSQLName(%q) = %q, want %q", tt.tag, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSplitTag(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tag string
|
|
sep rune
|
|
expected []string
|
|
}{
|
|
{"single part", "id", ',', []string{"id"}},
|
|
{"multiple parts", "id,pk,autoincrement", ',', []string{"id", "pk", "autoincrement"}},
|
|
{"empty parts filtered", "id,,pk", ',', []string{"id", "pk"}},
|
|
{"no separator", "singlepart", ',', []string{"singlepart"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := splitTag(tt.tag, tt.sep)
|
|
if len(result) != len(tt.expected) {
|
|
t.Errorf("splitTag(%q) returned %d parts, want %d", tt.tag, len(result), len(tt.expected))
|
|
return
|
|
}
|
|
for i, part := range tt.expected {
|
|
if result[i] != part {
|
|
t.Errorf("splitTag(%q)[%d] = %q, want %q", tt.tag, i, result[i], part)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test loadSecurityRules
|
|
func TestLoadSecurityRules(t *testing.T) {
|
|
t.Run("load rules successfully", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{
|
|
columnSecurity: []ColumnSecurity{
|
|
{Schema: "public", Tablename: "users", Path: []string{"email"}},
|
|
},
|
|
rowSecurity: RowSecurity{
|
|
Schema: "public",
|
|
Tablename: "users",
|
|
Template: "id = {UserID}",
|
|
},
|
|
}
|
|
secList, _ := NewSecurityList(provider)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "users",
|
|
}
|
|
|
|
err := LoadSecurityRules(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
// Verify column security was loaded
|
|
key := "public.users@1"
|
|
if _, ok := secList.ColumnSecurity[key]; !ok {
|
|
t.Error("expected column security to be loaded")
|
|
}
|
|
|
|
// Verify row security was loaded
|
|
if _, ok := secList.RowSecurity[key]; !ok {
|
|
t.Error("expected row security to be loaded")
|
|
}
|
|
})
|
|
|
|
t.Run("no user in context", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{}
|
|
secList, _ := NewSecurityList(provider)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
hasUser: false,
|
|
schema: "public",
|
|
entity: "users",
|
|
}
|
|
|
|
err := LoadSecurityRules(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error with no user, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// recordingQuery is a common.SelectQuery that records Where calls.
|
|
type recordingQuery struct {
|
|
common.SelectQuery
|
|
clauses []string
|
|
args [][]any
|
|
}
|
|
|
|
func (q *recordingQuery) Where(query string, args ...interface{}) common.SelectQuery {
|
|
q.clauses = append(q.clauses, query)
|
|
q.args = append(q.args, args)
|
|
return q
|
|
}
|
|
|
|
// recordingUpdateQuery is a common.UpdateQuery that records Where calls.
|
|
type recordingUpdateQuery struct {
|
|
common.UpdateQuery
|
|
clauses []string
|
|
args [][]any
|
|
}
|
|
|
|
func (q *recordingUpdateQuery) Where(query string, args ...interface{}) common.UpdateQuery {
|
|
q.clauses = append(q.clauses, query)
|
|
q.args = append(q.args, args)
|
|
return q
|
|
}
|
|
|
|
// recordingDeleteQuery is a common.DeleteQuery that records Where calls.
|
|
type recordingDeleteQuery struct {
|
|
common.DeleteQuery
|
|
clauses []string
|
|
args [][]any
|
|
}
|
|
|
|
func (q *recordingDeleteQuery) Where(query string, args ...interface{}) common.DeleteQuery {
|
|
q.clauses = append(q.clauses, query)
|
|
q.args = append(q.args, args)
|
|
return q
|
|
}
|
|
|
|
// Test applyRowSecurity
|
|
func TestApplyRowSecurity(t *testing.T) {
|
|
type TestModel struct {
|
|
ID int `bun:"id,pk"`
|
|
}
|
|
|
|
t.Run("apply row security template", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{
|
|
rowSecurity: RowSecurity{
|
|
Schema: "public",
|
|
Tablename: "orders",
|
|
Template: "user_id = {UserID}",
|
|
HasBlock: false,
|
|
UserID: 1,
|
|
},
|
|
}
|
|
secList, _ := NewSecurityList(provider)
|
|
ctx := context.Background()
|
|
|
|
// Load row security
|
|
_, _ = secList.LoadRowSecurity(ctx, 1, "public", "orders", false)
|
|
|
|
mockQuery := &recordingQuery{}
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: ctx,
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "orders",
|
|
model: &TestModel{},
|
|
query: mockQuery,
|
|
}
|
|
|
|
err := ApplyRowSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
if len(mockQuery.clauses) != 1 || mockQuery.clauses[0] != "user_id = ?" {
|
|
t.Fatalf("expected filter to be attached as %q, got %v", "user_id = ?", mockQuery.clauses)
|
|
}
|
|
if len(mockQuery.args[0]) != 1 || mockQuery.args[0][0] != 1 {
|
|
t.Fatalf("expected bound arg [1], got %v", mockQuery.args[0])
|
|
}
|
|
})
|
|
|
|
t.Run("fails closed when filter cannot be attached", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{rowSecurity: RowSecurity{
|
|
Schema: "public", Tablename: "orders", Template: "user_id = {UserID}", UserID: 1,
|
|
}}
|
|
secList, _ := NewSecurityList(provider)
|
|
ctx := context.Background()
|
|
_, _ = secList.LoadRowSecurity(ctx, 1, "public", "orders", false)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: ctx, userID: 1, hasUser: true, schema: "public", entity: "orders",
|
|
model: &TestModel{}, query: struct{}{},
|
|
}
|
|
if err := ApplyRowSecurity(secCtx, secList); err == nil {
|
|
t.Fatal("expected an error when the query does not support Where")
|
|
}
|
|
})
|
|
|
|
t.Run("filter is attached to update and delete queries", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{rowSecurity: RowSecurity{
|
|
Schema: "public", Tablename: "orders", Template: "user_id = {UserID}", UserID: 1,
|
|
}}
|
|
secList, _ := NewSecurityList(provider)
|
|
ctx := context.Background()
|
|
_, _ = secList.LoadRowSecurity(ctx, 1, "public", "orders", false)
|
|
|
|
uq := &recordingUpdateQuery{}
|
|
dq := &recordingDeleteQuery{}
|
|
for name, q := range map[string]interface{}{"update": uq, "delete": dq} {
|
|
secCtx := &mockSecurityContext{
|
|
ctx: ctx, userID: 1, hasUser: true, schema: "public", entity: "orders",
|
|
model: &TestModel{}, query: q,
|
|
}
|
|
if err := ApplyRowSecurity(secCtx, secList); err != nil {
|
|
t.Fatalf("%s: expected no error, got %v", name, err)
|
|
}
|
|
}
|
|
if len(uq.clauses) != 1 || uq.clauses[0] != "user_id = ?" || uq.args[0][0] != 1 {
|
|
t.Fatalf("update: filter not attached correctly: %v %v", uq.clauses, uq.args)
|
|
}
|
|
if len(dq.clauses) != 1 || dq.clauses[0] != "user_id = ?" || dq.args[0][0] != 1 {
|
|
t.Fatalf("delete: filter not attached correctly: %v %v", dq.clauses, dq.args)
|
|
}
|
|
})
|
|
|
|
t.Run("user context is bound as its id, never rendered into SQL", func(t *testing.T) {
|
|
uc := &UserContext{UserID: 7, SessionID: "sess_secret", UserName: "x' OR '1'='1"}
|
|
provider := &mockSecurityProvider{rowSecurity: RowSecurity{
|
|
Schema: "public", Tablename: "orders", Template: "user_id = {UserID}", UserID: uc,
|
|
}}
|
|
secList, _ := NewSecurityList(provider)
|
|
ctx := context.Background()
|
|
_, _ = secList.LoadRowSecurity(ctx, uc, "public", "orders", false)
|
|
|
|
q := &recordingQuery{}
|
|
secCtx := &mockSecurityContext{
|
|
ctx: ctx, userID: 7, hasUser: true, schema: "public", entity: "orders",
|
|
model: &TestModel{}, query: q, userRef: uc,
|
|
}
|
|
if err := ApplyRowSecurity(secCtx, secList); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(q.clauses) != 1 || strings.Contains(q.clauses[0], "sess_secret") || strings.Contains(q.clauses[0], "OR") {
|
|
t.Fatalf("user data leaked into SQL: %v", q.clauses)
|
|
}
|
|
if q.args[0][0] != 7 {
|
|
t.Fatalf("expected bound user id 7, got %v", q.args[0])
|
|
}
|
|
})
|
|
|
|
t.Run("invalid identifier is rejected", func(t *testing.T) {
|
|
rs := RowSecurity{Schema: "public", Tablename: "orders; DROP TABLE x", Template: "{TableName}.uid = 1"}
|
|
if _, _, err := rs.GetTemplate("id", nil); err == nil {
|
|
t.Fatal("expected invalid identifier error")
|
|
}
|
|
})
|
|
|
|
t.Run("block access", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{
|
|
rowSecurity: RowSecurity{
|
|
Schema: "public",
|
|
Tablename: "secrets",
|
|
HasBlock: true,
|
|
},
|
|
}
|
|
secList, _ := NewSecurityList(provider)
|
|
ctx := context.Background()
|
|
|
|
// Load row security
|
|
_, _ = secList.LoadRowSecurity(ctx, 1, "public", "secrets", false)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: ctx,
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "secrets",
|
|
}
|
|
|
|
err := ApplyRowSecurity(secCtx, secList)
|
|
if err == nil {
|
|
t.Fatal("expected error for blocked access")
|
|
}
|
|
})
|
|
|
|
t.Run("no user in context", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{}
|
|
secList, _ := NewSecurityList(provider)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
hasUser: false,
|
|
schema: "public",
|
|
entity: "orders",
|
|
}
|
|
|
|
err := ApplyRowSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error with no user, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("no row security defined", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{}
|
|
secList, _ := NewSecurityList(provider)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "unknown_table",
|
|
}
|
|
|
|
err := ApplyRowSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error with no security, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test applyColumnSecurity
|
|
func TestApplyColumnSecurityHook(t *testing.T) {
|
|
type User struct {
|
|
ID int `bun:"id,pk"`
|
|
Email string `bun:"email"`
|
|
}
|
|
|
|
t.Run("apply column security to results", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{
|
|
columnSecurity: []ColumnSecurity{
|
|
{
|
|
Schema: "public",
|
|
Tablename: "users",
|
|
Path: []string{"email"},
|
|
Accesstype: "mask",
|
|
UserID: 1,
|
|
MaskStart: 3,
|
|
MaskEnd: 0,
|
|
MaskChar: "*",
|
|
},
|
|
},
|
|
}
|
|
secList, _ := NewSecurityList(provider)
|
|
ctx := context.Background()
|
|
|
|
// Load column security
|
|
_ = secList.LoadColumnSecurity(ctx, 1, "public", "users", false)
|
|
|
|
users := []User{
|
|
{ID: 1, Email: "test@example.com"},
|
|
{ID: 2, Email: "user@test.com"},
|
|
}
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: ctx,
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "users",
|
|
model: &User{},
|
|
result: users,
|
|
}
|
|
|
|
err := ApplyColumnSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
|
|
// Check that result was updated with masked data
|
|
maskedResult := secCtx.GetResult()
|
|
if maskedResult == nil {
|
|
t.Error("expected result to be set")
|
|
}
|
|
})
|
|
|
|
t.Run("no user in context", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{}
|
|
secList, _ := NewSecurityList(provider)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
hasUser: false,
|
|
schema: "public",
|
|
entity: "users",
|
|
}
|
|
|
|
err := ApplyColumnSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error with no user, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("nil result", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{}
|
|
secList, _ := NewSecurityList(provider)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "users",
|
|
result: nil,
|
|
}
|
|
|
|
err := ApplyColumnSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error with nil result, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("nil model", func(t *testing.T) {
|
|
provider := &mockSecurityProvider{}
|
|
secList, _ := NewSecurityList(provider)
|
|
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "users",
|
|
model: nil,
|
|
result: []interface{}{},
|
|
}
|
|
|
|
err := ApplyColumnSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("expected no error with nil model, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test logDataAccess
|
|
func TestLogDataAccess(t *testing.T) {
|
|
t.Run("log access with user", func(t *testing.T) {
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "users",
|
|
}
|
|
|
|
err := LogDataAccess(secCtx)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("log access without user", func(t *testing.T) {
|
|
secCtx := &mockSecurityContext{
|
|
ctx: context.Background(),
|
|
hasUser: false,
|
|
schema: "public",
|
|
entity: "users",
|
|
}
|
|
|
|
err := LogDataAccess(secCtx)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test integration: loading and applying all security
|
|
func TestSecurityIntegration(t *testing.T) {
|
|
type Order struct {
|
|
ID int `bun:"id,pk"`
|
|
UserID int `bun:"user_id"`
|
|
Amount int `bun:"amount"`
|
|
Description string `bun:"description"`
|
|
}
|
|
|
|
provider := &mockSecurityProvider{
|
|
columnSecurity: []ColumnSecurity{
|
|
{
|
|
Schema: "public",
|
|
Tablename: "orders",
|
|
Path: []string{"amount"},
|
|
Accesstype: "mask",
|
|
UserID: 1,
|
|
},
|
|
},
|
|
rowSecurity: RowSecurity{
|
|
Schema: "public",
|
|
Tablename: "orders",
|
|
Template: "user_id = {UserID}",
|
|
HasBlock: false,
|
|
UserID: 1,
|
|
},
|
|
}
|
|
|
|
secList, _ := NewSecurityList(provider)
|
|
ctx := context.Background()
|
|
|
|
t.Run("complete security flow", func(t *testing.T) {
|
|
secCtx := &mockSecurityContext{
|
|
ctx: ctx,
|
|
userID: 1,
|
|
hasUser: true,
|
|
schema: "public",
|
|
entity: "orders",
|
|
model: &Order{},
|
|
query: &recordingQuery{},
|
|
}
|
|
|
|
// Step 1: Load security rules
|
|
err := LoadSecurityRules(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("LoadSecurityRules failed: %v", err)
|
|
}
|
|
|
|
// Step 2: Apply row security
|
|
err = ApplyRowSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("ApplyRowSecurity failed: %v", err)
|
|
}
|
|
|
|
// Step 3: Set some results
|
|
orders := []Order{
|
|
{ID: 1, UserID: 1, Amount: 1000, Description: "Order 1"},
|
|
{ID: 2, UserID: 1, Amount: 2000, Description: "Order 2"},
|
|
}
|
|
secCtx.SetResult(orders)
|
|
|
|
// Step 4: Apply column security
|
|
err = ApplyColumnSecurity(secCtx, secList)
|
|
if err != nil {
|
|
t.Fatalf("ApplyColumnSecurity failed: %v", err)
|
|
}
|
|
|
|
// Step 5: Log access
|
|
err = LogDataAccess(secCtx)
|
|
if err != nil {
|
|
t.Fatalf("LogDataAccess failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("security without user context", func(t *testing.T) {
|
|
secCtx := &mockSecurityContext{
|
|
ctx: ctx,
|
|
hasUser: false,
|
|
schema: "public",
|
|
entity: "orders",
|
|
}
|
|
|
|
// All security operations should handle missing user gracefully
|
|
_ = LoadSecurityRules(secCtx, secList)
|
|
_ = ApplyRowSecurity(secCtx, secList)
|
|
_ = ApplyColumnSecurity(secCtx, secList)
|
|
_ = LogDataAccess(secCtx)
|
|
|
|
// If we reach here without panics, the test passes
|
|
})
|
|
}
|
|
|
|
// Test RowSecurity GetTemplate with various placeholders
|
|
func TestRowSecurityGetTemplateIntegration(t *testing.T) {
|
|
type Model struct {
|
|
OrderID int `bun:"order_id,pk"`
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
rowSec RowSecurity
|
|
pkName string
|
|
expectedPart string // Part of the expected output
|
|
expectedArgs []any
|
|
}{
|
|
{
|
|
name: "with all placeholders",
|
|
rowSec: RowSecurity{
|
|
Schema: "sales",
|
|
Tablename: "orders",
|
|
UserID: 42,
|
|
Template: "{PrimaryKeyName} IN (SELECT {PrimaryKeyName} FROM {SchemaName}.{TableName}_access WHERE user_id = {UserID})",
|
|
},
|
|
pkName: "order_id",
|
|
expectedPart: "order_id IN (SELECT order_id FROM sales.orders_access WHERE user_id = ?)",
|
|
expectedArgs: []any{42},
|
|
},
|
|
{
|
|
name: "simple user filter",
|
|
rowSec: RowSecurity{
|
|
Schema: "public",
|
|
Tablename: "orders",
|
|
UserID: 1,
|
|
Template: "user_id = {UserID}",
|
|
},
|
|
pkName: "id",
|
|
expectedPart: "user_id = ?",
|
|
expectedArgs: []any{1},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
modelType := reflect.TypeOf(Model{})
|
|
result, args, err := tt.rowSec.GetTemplate(tt.pkName, modelType)
|
|
if err != nil {
|
|
t.Fatalf("GetTemplate() error = %v", err)
|
|
}
|
|
|
|
if result != tt.expectedPart {
|
|
t.Errorf("GetTemplate() = %q, want %q", result, tt.expectedPart)
|
|
}
|
|
if !reflect.DeepEqual(args, tt.expectedArgs) {
|
|
t.Errorf("GetTemplate() args = %v, want %v", args, tt.expectedArgs)
|
|
}
|
|
})
|
|
}
|
|
}
|