Guards
Guards are the core access control mechanism in slush. This guide covers writing, composing, and testing guards.
Guard Signature
A guard is a function that validates context:
type Guard func(ctx context.Context) error
- Return
nilto allow access - Return an error to deny access (wrapped with
ErrAccessDenied)
Basic Guards
Static guard
Always allows or denies:
func allowAll(ctx context.Context) error {
return nil
}
func denyAll(ctx context.Context) error {
return errors.New("access denied")
}
Context-checking guard
Validates values in context:
func requireAuth(ctx context.Context) error {
if auth.UserFromContext(ctx) == nil {
return errors.New("unauthenticated")
}
return nil
}
Parameterized Guards
Guards that take configuration return a Guard:
func requireRole(role string) slush.Guard {
return func(ctx context.Context) error {
user := auth.UserFromContext(ctx)
if user == nil {
return errors.New("unauthenticated")
}
if user.Role != role {
return fmt.Errorf("requires role: %s", role)
}
return nil
}
}
// Usage
slush.Register[AdminAPI](key, api).
Guard(requireRole("admin"))
Multiple parameters
func requirePermissions(perms ...string) slush.Guard {
return func(ctx context.Context) error {
user := auth.UserFromContext(ctx)
if user == nil {
return errors.New("unauthenticated")
}
for _, perm := range perms {
if !user.HasPermission(perm) {
return fmt.Errorf("missing permission: %s", perm)
}
}
return nil
}
}
slush.Register[Database](key, db).
Guard(requirePermissions("db:read", "db:write"))
Composing Guards
Chaining
Guards chain via .Guard():
slush.Register[Database](key, db).
Guard(requireAuth).
Guard(requireRole("service")).
Guard(requirePermission("db:access"))
Guards run in order; first error stops the chain.
AND composition
All guards must pass:
func and(guards ...slush.Guard) slush.Guard {
return func(ctx context.Context) error {
for _, g := range guards {
if err := g(ctx); err != nil {
return err
}
}
return nil
}
}
slush.Register[Database](key, db).
Guard(and(requireAuth, requireRole("admin")))
OR composition
At least one guard must pass:
func or(guards ...slush.Guard) slush.Guard {
return func(ctx context.Context) error {
var lastErr error
for _, g := range guards {
if err := g(ctx); err == nil {
return nil
} else {
lastErr = err
}
}
return lastErr
}
}
// Allow admins OR the resource owner
slush.Register[Resource](key, r).
Guard(or(requireRole("admin"), requireOwner))
NOT composition
Invert a guard:
func not(g slush.Guard) slush.Guard {
return func(ctx context.Context) error {
if err := g(ctx); err != nil {
return nil // Original failed, so NOT passes
}
return errors.New("guard passed when it should fail")
}
}
Stateful Guards
Guards can maintain state across invocations:
Rate limiting
func rateLimit(perSecond int) slush.Guard {
limiter := rate.NewLimiter(rate.Limit(perSecond), perSecond)
return func(ctx context.Context) error {
if !limiter.Allow() {
return errors.New("rate limited")
}
return nil
}
}
slush.Register[ExternalAPI](key, api).
Guard(rateLimit(100))
Circuit breaker
func circuitBreaker(threshold int, resetAfter time.Duration) slush.Guard {
var (
mu sync.Mutex
failures int
openedAt time.Time
)
return func(ctx context.Context) error {
mu.Lock()
defer mu.Unlock()
// Check if circuit is open
if failures >= threshold {
if time.Since(openedAt) < resetAfter {
return errors.New("circuit open")
}
// Reset after cooldown
failures = 0
}
return nil
}
}
Async-Safe Guards
Guards may be called concurrently. Ensure thread safety:
func countingGuard() (slush.Guard, func() int) {
var count atomic.Int64
guard := func(ctx context.Context) error {
count.Add(1)
return nil
}
getCount := func() int {
return int(count.Load())
}
return guard, getCount
}
Error Messages
Provide clear, actionable error messages:
// Bad
return errors.New("denied")
// Good
return errors.New("missing permission: db:write")
// Better (with context)
return fmt.Errorf("user %s lacks permission: db:write", user.ID)
Error messages appear in:
- The wrapped error returned by
Use - capitan events (
KeyErrorfield) - Logs if you're logging access denials
Testing Guards
Guards are plain functions—test them directly:
func TestRequireAuth(t *testing.T) {
// Without user
err := requireAuth(context.Background())
if err == nil {
t.Error("expected error without user")
}
// With user
ctx := auth.WithUser(context.Background(), &User{ID: 1})
err = requireAuth(ctx)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestRequireRole(t *testing.T) {
guard := requireRole("admin")
tests := []struct {
name string
user *User
wantErr bool
}{
{"no user", nil, true},
{"wrong role", &User{Role: "user"}, true},
{"correct role", &User{Role: "admin"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
if tt.user != nil {
ctx = auth.WithUser(ctx, tt.user)
}
err := guard(ctx)
if (err != nil) != tt.wantErr {
t.Errorf("got err=%v, wantErr=%v", err, tt.wantErr)
}
})
}
}
Next Steps
- sctx Integration — Cryptographic guards
- Testing Guide — Test services with guards
- Types Reference — Guard type documentation