rls

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 11, 2026 License: MIT Imports: 9 Imported by: 0

Documentation

Overview

Package rls provides PostgreSQL Row-Level Security helpers for SystemForge.

Index

Constants

This section is empty.

Variables

View Source
var ErrNotInTransaction = errors.New("rls: SET LOCAL needs a transaction, use rls.WithTenant")

ErrNotInTransaction is returned when a session variable is set through a *sql.DB or *sql.Conn. SET LOCAL lasts only until the end of the current transaction; outside one it is a no-op that the database reports as a warning, so the query that follows would run with no tenant. Run the work in a transaction, for example with WithTenant.

View Source
var SystemForgeTables = []string{
	"sf_principal_memberships",
}

SystemForgeTables returns the list of SystemForge tables that need RLS. These are the tables with tenant-scoped data.

Functions

func BypassRLS

func BypassRLS(roleName string) string

BypassRLS returns SQL to grant RLS bypass to a role. This is typically used for admin/migration roles.

func ContextWithTenant

func ContextWithTenant(ctx context.Context, tenantID uuid.UUID) context.Context

ContextWithTenant returns a new context with the tenant ID attached.

func ContextWithTenantAndUser

func ContextWithTenantAndUser(ctx context.Context, tenantID, userID uuid.UUID) context.Context

ContextWithTenantAndUser returns a new context with both tenant and user IDs.

func ContextWithUser

func ContextWithUser(ctx context.Context, userID uuid.UUID) context.Context

ContextWithUser returns a new context with the user ID attached.

func HasTenant

func HasTenant(ctx context.Context) bool

HasTenant checks if a tenant ID is present in the context.

func HasUser

func HasUser(ctx context.Context) bool

HasUser checks if a user ID is present in the context.

func NoBypassRLS

func NoBypassRLS(roleName string) string

NoBypassRLS returns SQL to revoke RLS bypass from a role.

func TenantIDFromContext

func TenantIDFromContext(ctx context.Context) uuid.UUID

TenantIDFromContext extracts the tenant ID from the context. Returns uuid.Nil if no tenant ID is present.

func TenantIDString

func TenantIDString(ctx context.Context) string

TenantIDString returns the tenant ID as a string, or empty string if not present.

func UserIDFromContext

func UserIDFromContext(ctx context.Context) uuid.UUID

UserIDFromContext extracts the user ID from the context. Returns uuid.Nil if no user ID is present.

func UserIDString

func UserIDString(ctx context.Context) string

UserIDString returns the user ID as a string, or empty string if not present.

func WithTenant

func WithTenant(ctx context.Context, db *sql.DB, helper *Helper, tenantID, userID uuid.UUID, fn TxFunc) error

WithTenant executes a function within a transaction with RLS context set. The tenant and user session variables are set before the function executes, and the transaction is committed if the function returns nil.

Example:

err := rls.WithTenant(ctx, db, helper, tenantID, userID, func(tx *sql.Tx) error {
    _, err := tx.ExecContext(ctx, "INSERT INTO items (name) VALUES ($1)", name)
    return err
})

func WithTenantFromContext

func WithTenantFromContext(ctx context.Context, db *sql.DB, helper *Helper, fn TxFunc) error

WithTenantFromContext executes a function within a transaction using tenant and user IDs from the context.

func WithTenantOpts

func WithTenantOpts(ctx context.Context, db *sql.DB, helper *Helper, opts *TxOptions, fn TxFunc) error

WithTenantOpts executes a function within a transaction with RLS context and custom options.

Types

type Config

type Config struct {
	// TenantColumn is the name of the column used for tenant isolation.
	// Defaults to "organization_id".
	TenantColumn string

	// UserColumn is the name of the column used for user identification.
	// Defaults to "user_id".
	UserColumn string

	// SessionVariable is the PostgreSQL session variable for the current tenant.
	// Defaults to "app.current_tenant".
	SessionVariable string

	// UserSessionVariable is the PostgreSQL session variable for the current user.
	// Defaults to "app.current_user".
	UserSessionVariable string
}

Config holds RLS configuration.

func DefaultConfig

func DefaultConfig() *Config

DefaultConfig returns the default RLS configuration.

type ConnWithRLS

type ConnWithRLS struct {
	*sql.Conn
	// contains filtered or unexported fields
}

ConnWithRLS wraps sql.Conn to ensure RLS context is set.

func GetConnWithRLS deprecated

func GetConnWithRLS(ctx context.Context, db *sql.DB, cfg *Config) (*ConnWithRLS, error)

GetConnWithRLS gets a connection from the pool with RLS context set.

Deprecated: see NewConnWithRLS.

func NewConnWithRLS deprecated

func NewConnWithRLS(ctx context.Context, conn *sql.Conn, cfg *Config) (*ConnWithRLS, error)

NewConnWithRLS wraps a connection with RLS context injection.

Deprecated: SET LOCAL on a connection outside a transaction has no effect, so it returns ErrNotInTransaction whenever a tenant or user is present. Run the work in rls.WithTenant instead.

type ContextInjector

type ContextInjector struct {
	// contains filtered or unexported fields
}

ContextInjector creates a function that can be used to inject RLS context into database connections. This is useful for connection pool hooks.

func NewContextInjector

func NewContextInjector(cfg *Config) *ContextInjector

NewContextInjector creates a new context injector.

func (*ContextInjector) InjectContext deprecated

func (ci *ContextInjector) InjectContext(ctx context.Context, conn *sql.Conn) error

InjectContext sets RLS session variables on a connection.

Deprecated: SET LOCAL on a connection outside a transaction has no effect, so it returns ErrNotInTransaction whenever a tenant or user is present. Run the work in rls.WithTenant instead.

type DBWithRLS

type DBWithRLS struct {
	*sql.DB
	// contains filtered or unexported fields
}

DBWithRLS wraps a database connection to automatically set RLS context for each connection obtained from the pool.

func NewDBWithRLS

func NewDBWithRLS(db *sql.DB, cfg *Config) *DBWithRLS

NewDBWithRLS creates a new RLS-aware database wrapper.

type EntDriver

type EntDriver struct {
	// contains filtered or unexported fields
}

EntDriver wraps an Ent SQL driver to automatically set RLS context. This ensures RLS is set for every database operation.

func NewEntDriver

func NewEntDriver(cfg *Config) *EntDriver

NewEntDriver creates a new RLS-aware Ent driver wrapper.

type EntHook

type EntHook struct {
	// contains filtered or unexported fields
}

EntHook provides integration with Ent ORM for automatic RLS context.

Usage with Ent:

hook := rls.NewEntHook(db, rls.DefaultConfig())

client := ent.NewClient(
    ent.Driver(drv),
    ent.Hook(hook.SetContext()),
)

func NewEntHook

func NewEntHook(db *sql.DB, cfg *Config) *EntHook

NewEntHook creates a new Ent RLS hook.

func (*EntHook) SetContextFromContext deprecated

func (h *EntHook) SetContextFromContext(ctx context.Context) error

SetContextFromContext sets RLS context using tenant/user from Go context.

Deprecated: it sets the variables on the pool, not on a transaction, where SET LOCAL has no effect, so it now returns ErrNotInTransaction whenever a tenant or user is present. Run the work in rls.WithTenant instead.

type Executor

type Executor interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
}

Executor is an interface for executing SQL queries. Implemented by *sql.DB, *sql.Tx, and similar types.

type Helper

type Helper struct {
	// contains filtered or unexported fields
}

Helper provides RLS operations for PostgreSQL.

func NewHelper

func NewHelper(cfg *Config) *Helper

NewHelper creates a new RLS helper with the given configuration.

func (*Helper) ClearContext

func (h *Helper) ClearContext(ctx context.Context, db Executor) error

ClearContext clears the tenant and user session variables.

func (*Helper) SetContext

func (h *Helper) SetContext(ctx context.Context, db Executor, tenantID, userID string) error

SetContext sets both tenant and user in the database session.

func (*Helper) SetTenant

func (h *Helper) SetTenant(ctx context.Context, db Executor, tenantID string) error

SetTenant sets the current tenant in the database session. This should be called at the start of each request/transaction.

db must be a transaction: SetTenant returns ErrNotInTransaction for a *sql.DB or *sql.Conn.

func (*Helper) SetUser

func (h *Helper) SetUser(ctx context.Context, db Executor, userID string) error

SetUser sets the current user in the database session.

db must be a transaction, as for SetTenant.

type Middleware

type Middleware struct {
	// contains filtered or unexported fields
}

Middleware provides HTTP middleware that automatically sets PostgreSQL RLS context from JWT claims.

func NewMiddleware

func NewMiddleware(db *sql.DB, cfg *Config) *Middleware

NewMiddleware creates a new RLS middleware.

func (*Middleware) RequireTenant

func (m *Middleware) RequireTenant() func(http.Handler) http.Handler

RequireTenant returns middleware that ensures a tenant context is present. Returns 403 Forbidden if no organization is in the JWT claims.

func (*Middleware) SetRLSContext

func (m *Middleware) SetRLSContext() func(http.Handler) http.Handler

SetRLSContext returns middleware that puts the authenticated caller's organization and principal on the request context, where WithTx and WithTenant pick them up to scope a transaction.

It does not touch the database. PostgreSQL's SET LOCAL lasts only for a transaction, and a middleware that runs before the handler has none, so setting the variables here would scope nothing. Do the work through WithTx.

This middleware should be used AFTER authentication middleware.

Usage with Chi:

r.Use(middleware.ChiAuth(jwtService))
r.Use(rlsMiddleware.SetRLSContext())

func (*Middleware) WithTx added in v0.14.0

func (m *Middleware) WithTx(ctx context.Context, fn TxFunc) error

WithTx runs fn in a transaction scoped to the tenant and user that SetRLSContext put on ctx. The session variables are set inside the transaction, which is the only place they take effect.

type MigrationConfig

type MigrationConfig struct {
	// Tables to apply RLS to. If empty, applies to SystemForge tables.
	Tables []string

	// TenantColumn is the column name for tenant isolation.
	// Defaults to "organization_id".
	TenantColumn string

	// SessionVariable is the PostgreSQL session variable for tenant ID.
	// Defaults to "app.current_tenant".
	SessionVariable string

	// BypassRoles are PostgreSQL roles that should bypass RLS.
	// Typically includes migration and admin roles.
	BypassRoles []string

	// AppRole is the PostgreSQL role used by the application.
	// RLS policies will be created for this role.
	AppRole string
}

MigrationConfig configures RLS migration behavior.

func DefaultMigrationConfig

func DefaultMigrationConfig() *MigrationConfig

DefaultMigrationConfig returns default migration configuration.

type Migrator

type Migrator struct {
	// contains filtered or unexported fields
}

Migrator handles RLS migration for SystemForge tables.

func NewMigrator

func NewMigrator(db *sql.DB, cfg *MigrationConfig) *Migrator

NewMigrator creates a new RLS migrator.

func (*Migrator) DisableRLS

func (m *Migrator) DisableRLS(ctx context.Context, table string) error

DisableRLS disables RLS on a table and drops policies.

func (*Migrator) EnableRLS

func (m *Migrator) EnableRLS(ctx context.Context, table string) error

EnableRLS enables RLS on a table and creates standard policies.

func (*Migrator) GenerateMigrationSQL

func (m *Migrator) GenerateMigrationSQL() string

GenerateMigrationSQL generates SQL for RLS migration without executing it. Useful for reviewing or including in migration files.

func (*Migrator) GenerateRollbackSQL

func (m *Migrator) GenerateRollbackSQL() string

GenerateRollbackSQL generates SQL to undo RLS migration.

func (*Migrator) GrantBypass

func (m *Migrator) GrantBypass(ctx context.Context, role string) error

GrantBypass grants RLS bypass to a role.

func (*Migrator) MigrateSystemForge

func (m *Migrator) MigrateSystemForge(ctx context.Context) error

MigrateSystemForge applies RLS policies to SystemForge tables.

func (*Migrator) RevokeBypass

func (m *Migrator) RevokeBypass(ctx context.Context, role string) error

RevokeBypass revokes RLS bypass from a role.

func (*Migrator) VerifyRLS

func (m *Migrator) VerifyRLS(ctx context.Context) ([]TableRLSStatus, error)

VerifyRLS checks that RLS is properly configured on tables.

type PolicySQL

type PolicySQL struct {
	// contains filtered or unexported fields
}

PolicySQL generates SQL for creating an RLS policy.

func NewPolicySQL

func NewPolicySQL(tableName, policyName string, cfg *Config) *PolicySQL

NewPolicySQL creates a new PolicySQL builder.

func (*PolicySQL) CreateAllPolicies

func (p *PolicySQL) CreateAllPolicies() string

CreateAllPolicies returns SQL to enable RLS and create all CRUD policies.

func (*PolicySQL) CreateDeletePolicy

func (p *PolicySQL) CreateDeletePolicy() string

CreateDeletePolicy returns SQL to create a DELETE policy.

func (*PolicySQL) CreateInsertPolicy

func (p *PolicySQL) CreateInsertPolicy() string

CreateInsertPolicy returns SQL to create an INSERT policy.

func (*PolicySQL) CreateSelectPolicy

func (p *PolicySQL) CreateSelectPolicy() string

CreateSelectPolicy returns SQL to create a SELECT policy.

func (*PolicySQL) CreateUpdatePolicy

func (p *PolicySQL) CreateUpdatePolicy() string

CreateUpdatePolicy returns SQL to create an UPDATE policy.

func (*PolicySQL) DropAllPolicies

func (p *PolicySQL) DropAllPolicies() string

DropAllPolicies returns SQL to drop all policies and disable RLS.

func (*PolicySQL) DropPolicy

func (p *PolicySQL) DropPolicy(operation string) string

DropPolicy returns SQL to drop a policy.

func (*PolicySQL) EnableRLS

func (p *PolicySQL) EnableRLS() string

EnableRLS returns SQL to enable RLS on the table.

func (*PolicySQL) ForceRLS

func (p *PolicySQL) ForceRLS() string

ForceRLS returns SQL to force RLS even for table owners.

type TableRLSStatus

type TableRLSStatus struct {
	Table       string
	RLSEnabled  bool
	RLSForced   bool
	HasPolicies bool
	Policies    []string
}

TableRLSStatus represents RLS status for a table.

type TestHelper

type TestHelper struct {
	// contains filtered or unexported fields
}

TestHelper provides testing utilities for RLS.

func NewTestHelper

func NewTestHelper(t *testing.T, db *sql.DB, cfg *Config) *TestHelper

NewTestHelper creates a new RLS test helper.

func (*TestHelper) AsTenant

func (th *TestHelper) AsTenant(tenantID uuid.UUID, fn func(ctx context.Context, tx *sql.Tx))

AsTenant executes a test function with only tenant context (no specific user).

func (*TestHelper) AsUser

func (th *TestHelper) AsUser(tenantID, userID uuid.UUID, fn func(ctx context.Context, tx *sql.Tx))

AsUser executes a test function with RLS context set for a specific user and tenant.

Example:

th := rls.NewTestHelper(t, db, nil)
th.AsUser(tenantID, userID, func(ctx context.Context, tx *sql.Tx) {
    // Queries here are scoped to tenantID
    rows, err := tx.QueryContext(ctx, "SELECT * FROM items")
    // ... assertions
})

func (*TestHelper) AssertCanRead

func (th *TestHelper) AssertCanRead(tenantID, userID uuid.UUID, table, whereClause string, args ...any)

AssertCanRead verifies that a tenant can read specific data.

func (*TestHelper) AssertCannotRead

func (th *TestHelper) AssertCannotRead(tenantID, userID uuid.UUID, table, whereClause string, args ...any)

AssertCannotRead verifies that a tenant cannot read specific data.

func (*TestHelper) AssertTenantIsolation

func (th *TestHelper) AssertTenantIsolation(table, insertCol, valueCol string)

AssertTenantIsolation verifies that RLS properly isolates data between tenants. It inserts data as tenantA, then verifies tenantB cannot see it.

func (*TestHelper) SetupTestTenant

func (th *TestHelper) SetupTestTenant(name string) (tenantID uuid.UUID, cleanup func())

SetupTestTenant creates a test tenant (organization) and returns a cleanup function.

func (*TestHelper) SetupTestUser

func (th *TestHelper) SetupTestUser(tenantID uuid.UUID, email, role string) (userID uuid.UUID, cleanup func())

SetupTestUser creates a human principal that is a member of a tenant with the given role, and returns its principal ID and a cleanup function.

func (*TestHelper) WithoutRLS

func (th *TestHelper) WithoutRLS(fn func(ctx context.Context, tx *sql.Tx))

WithoutRLS executes a test function without RLS context. Useful for setup/teardown or cross-tenant assertions.

type TxFunc

type TxFunc func(tx *sql.Tx) error

TxFunc is a function that executes within a transaction.

type TxManager

type TxManager struct {
	// contains filtered or unexported fields
}

TxManager manages transactions with automatic RLS context.

func NewTxManager

func NewTxManager(db *sql.DB, cfg *Config) *TxManager

NewTxManager creates a new transaction manager.

func (*TxManager) Begin

func (m *TxManager) Begin(ctx context.Context, tenantID, userID uuid.UUID) (*sql.Tx, error)

Begin starts a new transaction with RLS context set. The caller is responsible for committing or rolling back.

func (*TxManager) BeginFromContext

func (m *TxManager) BeginFromContext(ctx context.Context) (*sql.Tx, error)

BeginFromContext starts a transaction using context tenant/user.

func (*TxManager) WithTenant

func (m *TxManager) WithTenant(ctx context.Context, tenantID, userID uuid.UUID, fn TxFunc) error

WithTenant executes a function within a tenant-scoped transaction.

func (*TxManager) WithTenantFromContext

func (m *TxManager) WithTenantFromContext(ctx context.Context, fn TxFunc) error

WithTenantFromContext executes a function using context tenant/user.

type TxOptions

type TxOptions struct {
	// TenantID is the tenant to scope the transaction to.
	TenantID uuid.UUID

	// UserID is the user executing the transaction.
	UserID uuid.UUID

	// IsolationLevel sets the transaction isolation level.
	IsolationLevel sql.IsolationLevel

	// ReadOnly marks the transaction as read-only.
	ReadOnly bool
}

TxOptions configures transaction behavior.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL