Skip to content

Access Control Lists (ACL)

Overview

Raiden uses PostgreSQL's Row-Level Security (RLS) to control access to individual rows. ACL rules are defined programmatically using the Acl struct and a fluent Rule builder API. When applied, these rules translate to Postgres RLS policies.

Key concepts:

  • RLS controls access at the row level — enforcement occurs automatically regardless of how users interact with the database.
  • ACL in Raiden provides a Go API to define, enable, and manage RLS policies on your models and storage buckets.
  • The service key can bypass all ACL rules.

Enabling ACL on a Model

Add an Acl field to your model and implement the ConfigureAcl() method:

go
package models

import (
	"github.com/sev-2/raiden"
	"github.com/sev-2/raiden/pkg/builder"
	"github.com/sev-2/raiden/pkg/db"
)

type Articles struct {
	db.ModelBase
	Id        int64  `json:"id,omitempty" column:"name:id;type:bigint;primaryKey;autoIncrement;nullable:false"`
	Title     string `json:"title,omitempty" column:"name:title;type:text;nullable:false"`
	AuthorId  string `json:"author_id,omitempty" column:"name:author_id;type:uuid;nullable:false"`

	Metadata string `json:"-" schema:"public" tableName:"articles" rlsEnable:"true" rlsForced:"false"`

	// Access control
	Acl raiden.Acl
}

func (m *Articles) ConfigureAcl() { 
	m.Acl.Enable()
	m.Acl.Define(
		raiden.Rule("anyone_can_read").
			For("anon", "authenticated").
			To(raiden.CommandSelect),

		raiden.Rule("author_can_insert").
			For("authenticated").
			To(raiden.CommandInsert).
			Check(builder.OwnerIsAuth("author_id")),

		raiden.Rule("author_can_update").
			For("authenticated").
			To(raiden.CommandUpdate).
			Using(builder.OwnerIsAuth("author_id")).
			Check(builder.OwnerIsAuth("author_id")),

		raiden.Rule("author_can_delete").
			For("authenticated").
			To(raiden.CommandDelete).
			Using(builder.OwnerIsAuth("author_id")),
	)
}

Acl Methods

MethodDescription
Enable()Enable RLS on the table
Forced()Force RLS even for the table owner
Define()Define one or more RLS rules

Rule Builder

Create rules using the fluent builder: raiden.Rule(name).

go
raiden.Rule("rule_name").
	For("authenticated").        // target roles
	To(raiden.CommandSelect).    // SQL command
	Using(clause).               // USING clause (row visibility)
	Check(clause).               // WITH CHECK clause (row mutation)
	WithPermissive()             // or WithRestrictive()

Rule Methods

MethodDescription
For(roles...)Specify which database roles this rule applies to
To(command)The SQL command this rule governs
Using(clause)The USING expression — controls which rows are visible
Check(clause)The WITH CHECK expression — controls which rows can be written
WithPermissive()Set mode to PERMISSIVE (default). Multiple permissive policies are OR'd
WithRestrictive()Set mode to RESTRICTIVE. Restrictive policies are AND'd with permissive ones

Commands

ConstantDescription
raiden.CommandAllApplies to all operations
raiden.CommandSelectSELECT (read)
raiden.CommandInsertINSERT
raiden.CommandUpdateUPDATE
raiden.CommandDeleteDELETE

Which Clauses Apply to Which Commands

CommandUSINGWITH CHECK
SELECTYes
INSERTYes
UPDATEYesYes
DELETEYes
ALLYesYes

Expression Builder

The builder package (github.com/sev-2/raiden/pkg/builder) provides a DSL for building type-safe RLS clauses. Import it alongside raiden:

go
import "github.com/sev-2/raiden/pkg/builder"

Common Expressions

go
// Current authenticated user's UUID
builder.AuthUID()

// Check if a column equals the authenticated user
builder.OwnerIsAuth("user_id")

// Check JWT role
builder.RoleIs("admin")
builder.RolesAny("admin", "moderator")

// Column comparisons
builder.Eq("status", builder.String("active"))
builder.Ne("role", builder.String("banned"))
builder.Gt("age", builder.Int64(18))
builder.IsNull("deleted_at")
builder.NotNull("email")

// Logical operators
builder.And(clause1, clause2)
builder.Or(clause1, clause2)
builder.Not(clause1)

// IN / NOT IN
builder.In("status", builder.String("active"), builder.String("pending"))
builder.InStrings("role", "admin", "editor")

// LIKE / pattern matching
builder.Like("name", "%smith%")
builder.ILike("email", "%@example.com")
builder.StartsWith("name", "John")
builder.ContainsText("bio", "golang")

// BETWEEN
builder.Between("age", builder.Int64(18), builder.Int64(65))

// JSON/JSONB
builder.JSONGetText("metadata", "category")
builder.JSONContains("tags", builder.JSONB(`["featured"]`))
builder.JSONHasKey("config", "enabled")

// EXISTS subquery
builder.ExistsFrom("public.team_members",
	builder.Eq("team_members.user_id", builder.AuthUID()),
	builder.Eq("team_members.team_id", builder.Ident("teams.id")),
)

// Multi-tenant helpers
builder.TenantMatch("org_id", "request.jwt.claims", "uuid")
builder.InOrg("organization_id")

Value Constructors

FunctionDescription
builder.String(s)SQL string literal '...'
builder.Int64(n)Integer literal
builder.Float64(f)Float literal
builder.Bool(b)TRUE or FALSE
builder.UUID(s)String cast to ::uuid
builder.Raw(s)Raw trusted SQL fragment
builder.Ident(name)Quoted identifier (e.g., column reference)
builder.AuthUID()auth.uid()
builder.CurrentUser()current_user
builder.Now()now()
builder.Date(s)String cast to ::date
builder.Timestamp(s)String cast to ::timestamp

ACL on Storage

Storage buckets also support ACL via the same API. See Storage for details.

go
type SecureStorage struct {
	raiden.BucketBase
	Acl raiden.Acl
}

func (s *SecureStorage) Name() string {
	return "secure"
}

func (s *SecureStorage) ConfigureAcl() {
	s.Acl.Enable()
	s.Acl.Define(
		raiden.Rule("auth_read").
			For("authenticated").
			To(raiden.CommandSelect).
			Using(builder.OwnerIsAuth("owner")),
	)
}

Storage ACL policies are scoped to the storage.objects table with an automatic bucket_id filter.

INFO

The service key bypasses all ACL rules.

Released under the MIT License.