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
- O que é OPA?
- Linguagem Rego
- Go SDK
- API Authorization
- Policy Testing
- Integração com Middleware
- 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
- Go e Vault - Secrets management
- Go Security - Segurança em Go
- 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.