mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2025-12-13 17:10:36 +00:00
435 lines
11 KiB
Go
435 lines
11 KiB
Go
package security
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
// Mock implementations for testing composite provider
|
|
type mockAuth struct {
|
|
loginResp *LoginResponse
|
|
loginErr error
|
|
logoutErr error
|
|
authUser *UserContext
|
|
authErr error
|
|
supportsRefresh bool
|
|
supportsValidate bool
|
|
}
|
|
|
|
func (m *mockAuth) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
|
|
return m.loginResp, m.loginErr
|
|
}
|
|
|
|
func (m *mockAuth) Logout(ctx context.Context, req LogoutRequest) error {
|
|
return m.logoutErr
|
|
}
|
|
|
|
func (m *mockAuth) Authenticate(r *http.Request) (*UserContext, error) {
|
|
return m.authUser, m.authErr
|
|
}
|
|
|
|
// Optional interface implementations
|
|
func (m *mockAuth) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
|
|
if !m.supportsRefresh {
|
|
return nil, errors.New("not supported")
|
|
}
|
|
return m.loginResp, m.loginErr
|
|
}
|
|
|
|
func (m *mockAuth) ValidateToken(ctx context.Context, token string) (bool, error) {
|
|
if !m.supportsValidate {
|
|
return false, errors.New("not supported")
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
type mockColSec struct {
|
|
rules []ColumnSecurity
|
|
err error
|
|
supportsCache bool
|
|
}
|
|
|
|
func (m *mockColSec) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error) {
|
|
return m.rules, m.err
|
|
}
|
|
|
|
func (m *mockColSec) ClearCache(ctx context.Context, userID int, schema, table string) error {
|
|
if !m.supportsCache {
|
|
return errors.New("not supported")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type mockRowSec struct {
|
|
rowSec RowSecurity
|
|
err error
|
|
supportsCache bool
|
|
}
|
|
|
|
func (m *mockRowSec) GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error) {
|
|
return m.rowSec, m.err
|
|
}
|
|
|
|
func (m *mockRowSec) ClearCache(ctx context.Context, userID int, schema, table string) error {
|
|
if !m.supportsCache {
|
|
return errors.New("not supported")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Test NewCompositeSecurityProvider
|
|
func TestNewCompositeSecurityProvider(t *testing.T) {
|
|
t.Run("with all valid providers", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, err := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if composite == nil {
|
|
t.Fatal("expected non-nil composite provider")
|
|
}
|
|
})
|
|
|
|
t.Run("with nil authenticator", func(t *testing.T) {
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
_, err := NewCompositeSecurityProvider(nil, colSec, rowSec)
|
|
if err == nil {
|
|
t.Fatal("expected error with nil authenticator")
|
|
}
|
|
})
|
|
|
|
t.Run("with nil column security provider", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
_, err := NewCompositeSecurityProvider(auth, nil, rowSec)
|
|
if err == nil {
|
|
t.Fatal("expected error with nil column security provider")
|
|
}
|
|
})
|
|
|
|
t.Run("with nil row security provider", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{}
|
|
|
|
_, err := NewCompositeSecurityProvider(auth, colSec, nil)
|
|
if err == nil {
|
|
t.Fatal("expected error with nil row security provider")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test CompositeSecurityProvider authentication delegation
|
|
func TestCompositeSecurityProviderAuth(t *testing.T) {
|
|
userCtx := &UserContext{
|
|
UserID: 1,
|
|
UserName: "testuser",
|
|
}
|
|
|
|
t.Run("login delegates to authenticator", func(t *testing.T) {
|
|
auth := &mockAuth{
|
|
loginResp: &LoginResponse{
|
|
Token: "abc123",
|
|
User: userCtx,
|
|
},
|
|
}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
req := LoginRequest{Username: "test", Password: "pass"}
|
|
|
|
resp, err := composite.Login(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if resp.Token != "abc123" {
|
|
t.Errorf("expected token abc123, got %s", resp.Token)
|
|
}
|
|
})
|
|
|
|
t.Run("logout delegates to authenticator", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
req := LogoutRequest{Token: "abc123", UserID: 1}
|
|
|
|
err := composite.Logout(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("authenticate delegates to authenticator", func(t *testing.T) {
|
|
auth := &mockAuth{
|
|
authUser: userCtx,
|
|
}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
|
|
user, err := composite.Authenticate(req)
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if user.UserID != 1 {
|
|
t.Errorf("expected UserID 1, got %d", user.UserID)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test CompositeSecurityProvider security provider delegation
|
|
func TestCompositeSecurityProviderSecurity(t *testing.T) {
|
|
t.Run("get column security delegates to column provider", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{
|
|
rules: []ColumnSecurity{
|
|
{Schema: "public", Tablename: "users", Path: []string{"email"}},
|
|
},
|
|
}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
rules, err := composite.GetColumnSecurity(ctx, 1, "public", "users")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if len(rules) != 1 {
|
|
t.Errorf("expected 1 rule, got %d", len(rules))
|
|
}
|
|
})
|
|
|
|
t.Run("get row security delegates to row provider", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{
|
|
rowSec: RowSecurity{
|
|
Schema: "public",
|
|
Tablename: "orders",
|
|
Template: "user_id = {UserID}",
|
|
},
|
|
}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
rowSecResult, err := composite.GetRowSecurity(ctx, 1, "public", "orders")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if rowSecResult.Template != "user_id = {UserID}" {
|
|
t.Errorf("expected template 'user_id = {UserID}', got %s", rowSecResult.Template)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test CompositeSecurityProvider optional interfaces
|
|
func TestCompositeSecurityProviderOptionalInterfaces(t *testing.T) {
|
|
t.Run("refresh token with support", func(t *testing.T) {
|
|
auth := &mockAuth{
|
|
supportsRefresh: true,
|
|
loginResp: &LoginResponse{
|
|
Token: "new-token",
|
|
},
|
|
}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
resp, err := composite.RefreshToken(ctx, "old-token")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if resp.Token != "new-token" {
|
|
t.Errorf("expected token new-token, got %s", resp.Token)
|
|
}
|
|
})
|
|
|
|
t.Run("refresh token without support", func(t *testing.T) {
|
|
auth := &mockAuth{
|
|
supportsRefresh: false,
|
|
}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
_, err := composite.RefreshToken(ctx, "token")
|
|
if err == nil {
|
|
t.Fatal("expected error when refresh not supported")
|
|
}
|
|
})
|
|
|
|
t.Run("validate token with support", func(t *testing.T) {
|
|
auth := &mockAuth{
|
|
supportsValidate: true,
|
|
}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
valid, err := composite.ValidateToken(ctx, "token")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
if !valid {
|
|
t.Error("expected token to be valid")
|
|
}
|
|
})
|
|
|
|
t.Run("validate token without support", func(t *testing.T) {
|
|
auth := &mockAuth{
|
|
supportsValidate: false,
|
|
}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
_, err := composite.ValidateToken(ctx, "token")
|
|
if err == nil {
|
|
t.Fatal("expected error when validate not supported")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test CompositeSecurityProvider cache clearing
|
|
func TestCompositeSecurityProviderClearCache(t *testing.T) {
|
|
t.Run("clear cache with support", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{supportsCache: true}
|
|
rowSec := &mockRowSec{supportsCache: true}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
err := composite.ClearCache(ctx, 1, "public", "users")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("clear cache without support", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{supportsCache: false}
|
|
rowSec := &mockRowSec{supportsCache: false}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
// Should not error even if providers don't support cache
|
|
// (they just won't implement the interface)
|
|
err := composite.ClearCache(ctx, 1, "public", "users")
|
|
if err != nil {
|
|
// It's ok if this errors, as the providers don't implement Cacheable
|
|
t.Logf("cache clear returned error as expected: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("clear cache with partial support", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{supportsCache: true}
|
|
rowSec := &mockRowSec{supportsCache: false}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
err := composite.ClearCache(ctx, 1, "public", "users")
|
|
// Should succeed for column security even if row security fails
|
|
if err == nil {
|
|
t.Log("cache clear succeeded partially")
|
|
} else {
|
|
t.Logf("cache clear returned error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test error propagation
|
|
func TestCompositeSecurityProviderErrorPropagation(t *testing.T) {
|
|
t.Run("login error propagates", func(t *testing.T) {
|
|
auth := &mockAuth{
|
|
loginErr: errors.New("invalid credentials"),
|
|
}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
_, err := composite.Login(ctx, LoginRequest{})
|
|
if err == nil {
|
|
t.Fatal("expected error to propagate")
|
|
}
|
|
})
|
|
|
|
t.Run("authenticate error propagates", func(t *testing.T) {
|
|
auth := &mockAuth{
|
|
authErr: errors.New("invalid token"),
|
|
}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
|
|
_, err := composite.Authenticate(req)
|
|
if err == nil {
|
|
t.Fatal("expected error to propagate")
|
|
}
|
|
})
|
|
|
|
t.Run("column security error propagates", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{
|
|
err: errors.New("failed to load column security"),
|
|
}
|
|
rowSec := &mockRowSec{}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
_, err := composite.GetColumnSecurity(ctx, 1, "public", "users")
|
|
if err == nil {
|
|
t.Fatal("expected error to propagate")
|
|
}
|
|
})
|
|
|
|
t.Run("row security error propagates", func(t *testing.T) {
|
|
auth := &mockAuth{}
|
|
colSec := &mockColSec{}
|
|
rowSec := &mockRowSec{
|
|
err: errors.New("failed to load row security"),
|
|
}
|
|
|
|
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
|
|
ctx := context.Background()
|
|
|
|
_, err := composite.GetRowSecurity(ctx, 1, "public", "orders")
|
|
if err == nil {
|
|
t.Fatal("expected error to propagate")
|
|
}
|
|
})
|
|
}
|