- Published on
Row Level Security using Go and PostgreSQL
What is Row Level Security (RLS)?
Row level security (RLS for short) is a PostgreSQL security feature provided by the PostgreSQL database. It allows database administrators to define policies to control how specific rows of data display and operate for one or more user roles.
You can create row level security policies for specific commands like SELECT, INSERT, UPDATE, and DELETE or you can specify it for ALL commands.
What to consider when implementing RLS
- Enable RLS on a table using
ALTER TABLE ... ENABLE ROW LEVEL SECURITY
. Without this, policies won’t be applied. - Define RLS policies with the
CREATE POLICY
command for each table. - Policies control specific operations like
SELECT
,UPDATE
, andDELETE
. - They apply only to rows that meet a specified SQL condition.
USING
clauses filter existing rows based on the policy condition.WITH CHECK
clauses validate new rows before insertion or modification.- Policies can share the same name across multiple tables, allowing reuse.
- Superusers and roles with the
BYPASSRLS
attribute always bypass the row security system when accessing a table.
Important to note
- Enabling and disabling row security, as well as adding policies to a table, is always the privilege of the table owner only.
- Each policy has a name and multiple policies can be defined for a table.
- When multiple policies apply to a given query, they are combined using either OR (for permissive policies, which are the default) or using AND (for restrictive policies).
- A given role has the privileges of all roles that they are a member of.
RLS Example (using Go and GORM)
package main
import (
"fmt"
"gorm.io/gorm"
"gorm.io/driver/postgres"
)
// Tenant represents a customer organization
type Tenant struct {
gorm.Model
Name string
}
// User represents an application user associated with a tenant
type User struct {
gorm.Model
Email string
TenantID uint
Tenant Tenant
}
// Data represents any tenant-specific data
type Data struct {
gorm.Model
Content string
TenantID uint
Tenant Tenant
}
// TenantMiddleware wraps the database connection with tenant context
type TenantMiddleware struct {
DB *gorm.DB
CurrentTenantID uint
}
// Initialize RLS policies and create necessary functions
func InitializeRLS(db *gorm.DB) error {
// Create the security policy function
_, err := db.Raw(`
CREATE OR REPLACE FUNCTION tenant_security_policy(tenant_id bigint)
RETURNS boolean AS $$
BEGIN
RETURN (current_setting('app.current_tenant_id')::bigint = tenant_id);
END;
$$ LANGUAGE plpgsql;
`).Exec()
if err != nil {
return fmt.Errorf("failed to create security policy function: %v", err)
}
// Enable RLS on the data table
_, err = db.Raw(`
ALTER TABLE data ENABLE ROW LEVEL SECURITY;
`).Exec()
if err != nil {
return fmt.Errorf("failed to enable RLS: %v", err)
}
// Create the RLS policy
_, err = db.Raw(`
CREATE POLICY tenant_isolation_policy ON data
FOR ALL
USING (tenant_security_policy(tenant_id));
`).Exec()
if err != nil {
return fmt.Errorf("failed to create RLS policy: %v", err)
}
return nil
}
// NewTenantMiddleware creates a new middleware instance
func NewTenantMiddleware(db *gorm.DB) *TenantMiddleware {
return &TenantMiddleware{
DB: db,
}
}
// SetTenant sets the current tenant context
func (tm *TenantMiddleware) SetTenant(tenantID uint) error {
// Set the tenant ID in the PostgreSQL session
_, err := tm.DB.Raw(
"SET app.current_tenant_id = ?",
tenantID,
).Exec()
if err != nil {
return fmt.Errorf("failed to set tenant context: %v", err)
}
tm.CurrentTenantID = tenantID
return nil
}
// GetDB returns a database connection scoped to the current tenant
func (tm *TenantMiddleware) GetDB() *gorm.DB {
return tm.DB.Where("tenant_id = ?", tm.CurrentTenantID)
}
// Example usage
func main() {
// Connect to database
dsn := "host=localhost user=postgres password=postgres dbname=multitenantdb port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect to database")
}
// Auto-migrate the schemas
db.AutoMigrate(&Tenant{}, &User{}, &Data{})
// Initialize RLS
if err := InitializeRLS(db); err != nil {
panic(fmt.Sprintf("failed to initialize RLS: %v", err))
}
// Create tenant middleware
tm := NewTenantMiddleware(db)
// Example of using the tenant-scoped database
if err := tm.SetTenant(1); err != nil {
panic(fmt.Sprintf("failed to set tenant: %v", err))
}
// All operations will now be scoped to tenant 1
tenantDB := tm.GetDB()
// Create data for tenant 1
data := Data{
Content: "Tenant 1's data",
TenantID: 1,
}
tenantDB.Create(&data)
// Query data (will only return tenant 1's data)
var results []Data
tenantDB.Find(&results)
}
Limitations and Considerations
- RLS may not be suitable for cases requiring complex cross-tenant operations
- Additional care is needed when performing bulk operations
- Backup and restore operations need to account for RLS policies