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—
INSERT—Yes
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.