Go e Open Policy Agent (OPA): Policy as Code

Open Policy Agent (OPA) é um motor de políticas open-source que permite unificar policy management em toda a stack. Desde API authorization até Kubernetes admission control, OPA proporciona uma linguagem declarativa (Rego) para definir políticas.

Neste guia, você aprenderá a integrar OPA com aplicações Go para implementar authorization flexível e auditável.

Índice

  1. O que é OPA?
  2. Linguagem Rego
  3. Go SDK
  4. API Authorization
  5. Policy Testing
  6. Integração com Middleware
  7. Bundles e Atualização Dinâmica

O que é OPA?

Casos de Uso

  • API Authorization: Permitir/negar acesso a endpoints
  • Data Filtering: Filtrar dados baseado em permissões
  • Kubernetes: Admission control policies
  • Terraform: Policy enforcement para infraestrutura
  • Service Mesh: Envoy/Istio authorization

Arquitetura

┌─────────────────────────────────────────────────────────────┐
│                   Aplicação Go                             │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  HTTP Request + JWT                                 │   │
│  │  {user: "alice", action: "read", resource: "document"}│   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │ Query                             │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              OPA (via Go SDK)                       │   │
│  │  ┌──────────────┐      ┌───────────────────────┐    │   │
│  │  │  Rego Policy │      │       Data            │    │   │
│  │  │              │      │  {users, roles, etc}  │    │   │
│  │  │ allow { ... }│      │                       │    │   │
│  │  └──────────────┘      └───────────────────────┘    │   │
│  └──────────────────────┬──────────────────────────────┘   │
│                         │ Result: true/false                │
│                         ▼                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              Response (Allow/Deny)                  │   │
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

Linguagem Rego

Fundamentos

# policy/authz.rego
package authz

# Importa dados
default allow := false

# Regra básica: admin pode tudo
allow {
    input.user.role == "admin"
}

# Regra: owner pode acessar seus próprios recursos
allow {
    input.user.id == input.resource.owner_id
}

# Regra: usuários com permissão específica
allow {
    permission := data.permissions[input.user.id][_]
    permission.action == input.action
    permission.resource == input.resource.type
}

# Helper: check role
check_role(role) {
    input.user.role == role
}

Políticas de API

# policy/api.rego
package api

import future.keywords.if
import future.keywords.in

# Default deny
default allow := false

# GET /users - apenas admins ou usuários autenticados
allow if {
    input.method == "GET"
    input.path == ["users"]
    input.user.authenticated
}

# GET /users/:id - próprio usuário ou admin
allow if {
    input.method == "GET"
    input.path == ["users", user_id]
    input.user.id == user_id
}

allow if {
    input.method == "GET"
    input.path == ["users", _]
    input.user.role == "admin"
}

# POST /orders - qualquer usuário autenticado
allow if {
    input.method == "POST"
    input.path == ["orders"]
    input.user.authenticated
}

# Rate limiting check
rate_limit_ok if {
    not data.rate_limits[input.user.id].count > 100
}

# Composição de políticas
allow if {
    check_permission
    rate_limit_ok
    business_hours
}

check_permission if {
    some permission in data.permissions[input.user.role]
    permission.resource == input.resource
    permission.action == input.action
}

business_hours if {
    to_number(input.time) >= 9
    to_number(input.time) <= 18
}

Go SDK

Instalação

go get github.com/open-policy-agent/opa/rego
go get github.com/open-policy-agent/opa/storage/inmem

Uso Básico

package opa

import (
    "context"
    "embed"
    "fmt"

    "github.com/open-policy-agent/opa/rego"
    "github.com/open-policy-agent/opa/storage/inmem"
)

//go:embed policy/*.rego
var policyFS embed.FS

type Authorizer struct {
    query rego.PreparedEvalQuery
}

func NewAuthorizer() (*Authorizer, error) {
    // Carrega políticas do embed.FS
    policy, err := policyFS.ReadFile("policy/authz.rego")
    if err != nil {
        return nil, err
    }

    // Compila e prepara query
    r := rego.New(
        rego.Query("data.authz.allow"),
        rego.Module("authz.rego", string(policy)),
    )

    query, err := r.PrepareForEval(context.Background())
    if err != nil {
        return nil, fmt.Errorf("falha ao compilar políticas: %w", err)
    }

    return &Authorizer{query: query}, nil
}

func (a *Authorizer) Authorize(ctx context.Context, input map[string]interface{}) (bool, error) {
    results, err := a.query.Eval(ctx, rego.EvalInput(input))
    if err != nil {
        return false, err
    }

    if len(results) == 0 {
        return false, nil
    }

    // Extrai resultado booleano
    allowed, ok := results[0].Expressions[0].Value.(bool)
    if !ok {
        return false, fmt.Errorf("resultado inválido")
    }

    return allowed, nil
}

Com Dados Dinâmicos

package opa

import (
    "context"
    "encoding/json"

    "github.com/open-policy-agent/opa/ast"
    "github.com/open-policy-agent/opa/rego"
    "github.com/open-policy-agent/opa/storage"
    "github.com/open-policy-agent/opa/storage/inmem"
)

type PolicyEngine struct {
    store storage.Store
}

func NewPolicyEngine() *PolicyEngine {
    return &PolicyEngine{
        store: inmem.NewFromObject(map[string]interface{}{}),
    }
}

func (pe *PolicyEngine) LoadData(ctx context.Context, path string, data interface{}) error {
    jsonData, err := json.Marshal(data)
    if err != nil {
        return err
    }

    var jsonValue interface{}
    if err := json.Unmarshal(jsonData, &jsonValue); err != nil {
        return err
    }

    txn := storage.NewTransactionOrDie(ctx, pe.store, storage.WriteParams)
    defer pe.store.Abort(ctx, txn)

    if err := pe.store.Write(ctx, txn, storage.AddOp, storage.MustParsePath(path), jsonValue); err != nil {
        return err
    }

    return pe.store.Commit(ctx, txn)
}

func (pe *PolicyEngine) Evaluate(ctx context.Context, query string, input interface{}) (rego.ResultSet, error) {
    r := rego.New(
        rego.Query(query),
        rego.Store(pe.store),
        rego.Input(input),
    )

    return r.Eval(ctx)
}

API Authorization

Middleware de Autorização

package middleware

import (
    "context"
    "encoding/json"
    "net/http"
    "strings"

    "myapp/opa"
)

type contextKey string

const userContextKey contextKey = "user"

type AuthMiddleware struct {
    authorizer *opa.Authorizer
}

func NewAuthMiddleware(authorizer *opa.Authorizer) *AuthMiddleware {
    return &AuthMiddleware{authorizer: authorizer}
}

func (m *AuthMiddleware) Authorize(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Extrai usuário do context (setado por auth middleware anterior)
        user, ok := r.Context().Value(userContextKey).(User)
        if !ok {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        // Prepara input para OPA
        input := map[string]interface{}{
            "method":   r.Method,
            "path":     splitPath(r.URL.Path),
            "user":     user,
            "headers":  r.Header,
            "time":     getCurrentHour(),
        }

        // Avalia política
        allowed, err := m.authorizer.Authorize(r.Context(), input)
        if err != nil {
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }

        if !allowed {
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func splitPath(path string) []string {
    return strings.Split(strings.Trim(path, "/"), "/")
}

Uso em Handlers

package main

import (
    "net/http"

    "myapp/middleware"
    "myapp/opa"
)

func main() {
    authorizer, _ := opa.NewAuthorizer()
    authMiddleware := middleware.NewAuthMiddleware(authorizer)

    mux := http.NewServeMux()

    // Protege rotas
    mux.Handle("/api/", authMiddleware.Authorize(apiHandler()))
    mux.Handle("/admin/", authMiddleware.Authorize(adminHandler()))

    http.ListenAndServe(":8080", mux)
}

Policy Testing

Testes em Rego

# policy/api_test.rego
package api

test_allow_get_users_if_admin {
    allow with input as {
        "method": "GET",
        "path": ["users"],
        "user": {"role": "admin", "authenticated": true}
    }
}

test_deny_get_users_if_not_authenticated {
    not allow with input as {
        "method": "GET",
        "path": ["users"],
        "user": {"authenticated": false}
    }
}

test_allow_get_own_user {
    allow with input as {
        "method": "GET",
        "path": ["users", "user-123"],
        "user": {"id": "user-123", "authenticated": true}
    }
}

test_deny_access_outside_business_hours {
    not allow with input as {
        "method": "POST",
        "path": ["orders"],
        "user": {"authenticated": true},
        "time": 22
    }
}

Testes em Go

package opa_test

import (
    "context"
    "testing"

    "myapp/opa"
)

func TestAuthorizer(t *testing.T) {
    auth, err := opa.NewAuthorizer()
    if err != nil {
        t.Fatal(err)
    }

    tests := []struct {
        name     string
        input    map[string]interface{}
        expected bool
    }{
        {
            name: "admin_can_access_anything",
            input: map[string]interface{}{
                "method": "DELETE",
                "path":   []string{"users", "123"},
                "user":   map[string]interface{}{"role": "admin"},
            },
            expected: true,
        },
        {
            name: "user_can_access_own_resource",
            input: map[string]interface{}{
                "method": "GET",
                "path":   []string{"users", "user-123"},
                "user": map[string]interface{}{
                    "id": "user-123",
                },
                "resource": map[string]interface{}{
                    "owner_id": "user-123",
                },
            },
            expected: true,
        },
        {
            name: "user_cannot_access_others_resource",
            input: map[string]interface{}{
                "method": "GET",
                "path":   []string{"users", "user-456"},
                "user": map[string]interface{}{
                    "id": "user-123",
                },
                "resource": map[string]interface{}{
                    "owner_id": "user-456",
                },
            },
            expected: false,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := auth.Authorize(context.Background(), tt.input)
            if err != nil {
                t.Fatal(err)
            }
            if result != tt.expected {
                t.Errorf("expected %v, got %v", tt.expected, result)
            }
        })
    }
}

Bundles e Atualização Dinâmica

OPA Server

package main

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/open-policy-agent/opa/sdk"
)

func main() {
    ctx := context.Background()

    // Configura OPA com bundle remoto
    config := []byte(`
{
    "services": {
        "bundle_service": {
            "url": "https://bundles.example.com",
            "credentials": {
                "bearer": {
                    "token": "${TOKEN}"
                }
            }
        }
    },
    "bundles": {
        "authz": {
            "service": "bundle_service",
            "resource": "bundles/authz.tar.gz",
            "polling": {
                "min_delay_seconds": 60,
                "max_delay_seconds": 120
            }
        }
    }
}
`)

    opa, err := sdk.New(ctx, sdk.Options{
        ID:     "opa-instance",
        Config: bytes.NewReader(config),
    })
    if err != nil {
        log.Fatal(err)
    }
    defer opa.Stop(ctx)

    // Usa OPA para queries
    if result, err := opa.Decision(ctx, sdk.DecisionOptions{
        Path:  "authz/allow",
        Input: map[string]interface{}{"user": "alice"},
    }); err != nil {
        log.Fatal(err)
    } else {
        log.Printf("Decision: %v", result.Result)
    }

    // Aguarda sinal de término
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
}

Hot Reload

package opa

import (
    "context"
    "sync"
    "time"

    "github.com/fsnotify/fsnotify"
    "github.com/open-policy-agent/opa/rego"
)

type HotReloadAuthorizer struct {
    mu        sync.RWMutex
    query     rego.PreparedEvalQuery
    policyDir string
}

func NewHotReloadAuthorizer(policyDir string) (*HotReloadAuthorizer, error) {
    ha := &HotReloadAuthorizer{policyDir: policyDir}
    
    if err := ha.reload(); err != nil {
        return nil, err
    }

    // Inicia watcher
    go ha.watch()

    return ha, nil
}

func (ha *HotReloadAuthorizer) reload() error {
    // Carrega e compila políticas
    modules := make(map[string]string)
    // ... carrega arquivos .rego

    r := rego.New(
        rego.Query("data.authz.allow"),
        rego.LoadedModules(modules),
    )

    query, err := r.PrepareForEval(context.Background())
    if err != nil {
        return err
    }

    ha.mu.Lock()
    ha.query = query
    ha.mu.Unlock()

    return nil
}

func (ha *HotReloadAuthorizer) watch() {
    watcher, _ := fsnotify.NewWatcher()
    defer watcher.Close()

    watcher.Add(ha.policyDir)

    for {
        select {
        case event, ok := <-watcher.Events:
            if !ok {
                return
            }
            if event.Op&fsnotify.Write == fsnotify.Write {
                // Recarrega após delay (debounce)
                time.Sleep(100 * time.Millisecond)
                ha.reload()
            }
        }
    }
}

func (ha *HotReloadAuthorizer) Authorize(ctx context.Context, input map[string]interface{}) (bool, error) {
    ha.mu.RLock()
    query := ha.query
    ha.mu.RUnlock()

    results, err := query.Eval(ctx, rego.EvalInput(input))
    // ... processa resultado
}

Conclusão

Neste guia, você aprendeu:

Rego: Linguagem de políticas declarativa ✅ Go SDK: Integração com aplicações Go ✅ Authorization: Middleware para APIs ✅ Testing: Testes de políticas em Rego e Go ✅ Deployment: Bundles e hot reload

Próximos Passos

  1. Go e Vault - Secrets management
  2. Go Security - Segurança em Go
  3. Go Microservices - Arquitetura completa

FAQ

Q: OPA é apenas para authorization? R: Não. Pode ser usado para data filtering, admission control, config validation, etc.

Q: Rego é difícil de aprender? R: Tem curva de aprendizado, mas é poderosa. Comece com exemplos simples.

Q: OPA afeta performance? R: Avaliação é rápida (microssegundos). OPA Server pode ser colocado no mesmo host.

Q: Posso usar OPA sem servidor? R: Sim, use o Go SDK para embed no seu aplicativo.