Ir al contenido

Modelo de Seguridad

Wippy implementa control de acceso basado en atributos. Cada solicitud lleva un actor (quién) y un scope (qué políticas aplican). Las políticas evalúan el acceso basándose en la acción, el recurso y los metadatos tanto del actor como del recurso.

flowchart LR
A[Actor + Scope] --> PE[Policy Evaluation] --> AD[Allow/Deny]
A -.->|Identity<br/>Metadata| PE
PE -.->|Conditions<br/>actor, resource, action| AD
TipoDescripción
security.policyPolítica declarativa con condiciones
security.policy.exprPolítica basada en expresiones
security.token_storeAlmacenamiento y validación de tokens

Un actor representa quién está realizando una acción.

local security = require("security")
-- Crear actor con metadatos
local actor = security.new_actor("user:123", {
role = "admin",
team = "backend",
department = "engineering",
clearance = 3
})
-- Acceder a propiedades del actor
local id = actor:id() -- "user:123"
local meta = actor:meta() -- {role="admin", ...}
-- Obtener el actor actual desde el contexto
local actor = security.actor()
if not actor then
return nil, errors.new("UNAUTHORIZED", "No actor in context")
end

Las políticas definen reglas de acceso con acciones, recursos, condiciones y efectos.

src/security/_index.yaml
version: "1.0"
namespace: app.security
entries:
# Acceso total de administrador
- name: admin_policy
kind: security.policy
policy:
actions: "*"
resources: "*"
effect: allow
conditions:
- field: actor.meta.role
operator: eq
value: admin
groups:
- admin
# Acceso solo de lectura
- name: readonly_policy
kind: security.policy
policy:
actions:
- "*.read"
- "*.get"
- "*.list"
resources: "*"
effect: allow
groups:
- default
# Acceso del propietario del recurso
- name: owner_policy
kind: security.policy
policy:
actions:
- read
- write
- delete
resources: "document:*"
effect: allow
conditions:
- field: meta.owner
operator: eq
value_from: actor.id
groups:
- default
# Denegar confidencial sin nivel de autorización
- name: deny_confidential
kind: security.policy
policy:
actions: "*"
resources: "document:*"
effect: deny
conditions:
- field: meta.classification
operator: eq
value: confidential
- field: actor.meta.clearance
operator: lt
value: 3
groups:
- security
policy:
actions: "*" | "action" | ["action1", "action2"]
resources: "*" | "resource" | ["res1", "res2"]
effect: allow | deny
conditions: # Opcional
- field: "field.path"
operator: "eq"
value: "static_value"
# O
value_from: "other.field.path"

Para lógica compleja, use políticas de expresión:

- name: flexible_access
kind: security.policy.expr
policy:
actions:
- read
- write
resources: "file:*"
effect: allow
expression: |
(actor.meta.role == "editor" && action == "write") ||
(action == "read" && meta.public == true) ||
actor.id == meta.owner
groups:
- editors

Las condiciones permiten la evaluación dinámica de políticas basada en actor, acción, recurso y metadatos.

RutaDescripción
actor.idIdentificador único del actor
actor.meta.*Metadatos del actor (admite anidamiento)
actionLa acción que se está realizando
resourceEl identificador del recurso
meta.*Metadatos del recurso
OperadorDescripciónEjemplo
eqIgualactor.meta.role eq "admin"
neNo igualmeta.status ne "deleted"
ltMenor quemeta.priority lt 5
gtMayor queactor.meta.clearance gt 2
lteMenor o igualmeta.size lte 1000
gteMayor o igualactor.meta.level gte 3
inValor en arregloaction in ["read", "write"]
ninValor no en arreglometa.status nin ["deleted", "archived"]
existsEl campo existemeta.owner exists true
nexistsEl campo no existemeta.deleted nexists true
containsString contieneresource contains "sensitive"
ncontainsString no contieneresource ncontains "public"
matchesCoincide con regexresource matches "^doc:.*"
nmatchesNo coincide con regexactor.id nmatches "^system:.*"
# Coincidir con rol del actor
conditions:
- field: actor.meta.role
operator: eq
value: admin
# Comparar campos
conditions:
- field: meta.owner
operator: eq
value_from: actor.id
# Comparación numérica
conditions:
- field: actor.meta.clearance
operator: gte
value: 3
# Pertenencia a arreglo
conditions:
- field: actor.meta.role
operator: in
value:
- admin
- moderator
# Coincidencia de patrón
conditions:
- field: resource
operator: matches
value: "^api:/v[0-9]+/admin/.*"
# Múltiples condiciones (AND)
conditions:
- field: actor.meta.department
operator: eq
value: engineering
- field: meta.environment
operator: eq
value: production

Los scopes combinan múltiples políticas en un contexto de seguridad.

local security = require("security")
-- Obtener políticas
local admin_policy = security.policy("app.security:admin_policy")
local readonly_policy = security.policy("app.security:readonly_policy")
-- Crear scope con políticas
local scope = security.new_scope()
scope = scope:with(admin_policy)
scope = scope:with(readonly_policy)
-- Los scopes son inmutables - :with() devuelve un nuevo scope

Cargar todas las políticas de un grupo:

-- Cargar scope con todas las políticas del grupo
local scope, err = security.named_scope("app.security:admin")

Las políticas se asignan a grupos mediante el campo groups:

- name: admin_policy
kind: security.policy
policy:
# ...
groups:
- admin # Esta política está en el grupo "admin"
- default # Puede estar en varios grupos
-- Agregar política
local new_scope = scope:with(policy)
-- Eliminar política
local new_scope = scope:without("app.security:temp_policy")
-- Verificar si una política está en el scope
local has = scope:contains("app.security:admin_policy")
-- Obtener todas las políticas
local policies = scope:policies()
1. Verifica cada política en el scope
2. Si ALGUNA política devuelve Deny → El resultado es Deny
3. Si hay al menos un Allow y ningún Deny → El resultado es Allow
4. Sin políticas aplicables → El resultado es Undefined
ResultadoSignificado
allowAcceso concedido
denyAcceso denegado explícitamente
undefinedNinguna política coincidió
-- Evaluar directamente
local result = scope:evaluate(actor, "read", "document:123", {
owner = "user:456",
classification = "internal"
})
if result == "deny" then
return nil, errors.new("FORBIDDEN", "Access denied")
elseif result == "undefined" then
-- Ninguna política coincidió - depende del modo estricto
end
-- Verificar contra el actor y scope del contexto actual
local allowed = security.can("read", "document:123", {
owner = "user:456"
})
if not allowed then
return nil, errors.new("FORBIDDEN", "Access denied")
end

Los almacenes de tokens proporcionan creación, validación y revocación seguras de tokens.

src/auth/_index.yaml
version: "1.0"
namespace: app.auth
entries:
# Registrar variable de entorno
- name: os_env
kind: env.storage.os
- name: AUTH_SECRET_KEY
kind: env.variable
variable: AUTH_SECRET_KEY
storage: app.auth:os_env
# Almacén respaldo para tokens
- name: token_data
kind: store.memory
lifecycle:
auto_start: true
# Almacén de tokens
- name: tokens
kind: security.token_store
store: app.auth:token_data
token_length: 32
default_expiration: "24h"
token_key_env: "AUTH_SECRET_KEY"
OpciónPredeterminadoDescripción
storerequeridoReferencia al almacén clave-valor de respaldo
token_length32Tamaño del token en bytes (256 bits)
default_expiration24hTTL predeterminado del token
token_keyningunoClave de firma HMAC-SHA256 (valor directo)
token_key_envningunoNombre de la variable de entorno para la clave de firma

Use token_key_env en producción para evitar incrustar secretos en las entradas. Consulte Sistema de Entorno para registrar variables de entorno.

local security = require("security")
-- Obtener almacén de tokens
local store, err = security.token_store("app.auth:tokens")
if err then
return nil, err
end
-- Crear actor y scope
local actor = security.new_actor("user:123", {
role = "user",
email = "user@example.com"
})
local scope, _ = security.named_scope("app.security:default")
-- Crear token
local token, err = store:create(actor, scope, {
expiration = "7d", -- Sobrescribir expiración predeterminada
meta = {
device = "mobile",
ip = "192.168.1.1"
}
})
if err then
return nil, err
end
-- Formato del token: base64_token.hmac_signature (si token_key está definido)
-- Ejemplo: "dGVzdHRva2VuMTIz.a1b2c3d4e5f6"
-- Validar token
local actor, scope, err = store:validate(token)
if err then
return nil, errors.new("UNAUTHORIZED", "Invalid token")
end
-- Actor y scope se reconstruyen desde los datos almacenados
print(actor:id()) -- "user:123"
-- Revocar un token
local ok, err = store:revoke(token)
-- Cerrar el almacén cuando termine
store:close()

El contexto de seguridad se propaga a través de las llamadas a funciones.

local funcs = require("funcs")
-- Llamar función con contexto de seguridad
local result, err = funcs.new()
:with_actor(actor)
:with_scope(scope)
:call("app.api:protected_endpoint", data)
ComponenteHereda
ActorSí - se pasa a llamadas hijas
ScopeSí - se pasa a llamadas hijas
Modo estrictoNo - es a nivel de aplicación

Las funciones heredan el contexto de seguridad del llamador. Los procesos generados comienzan sin contexto.

Configure la seguridad predeterminada para servicios:

- name: worker_service
kind: process.lua
source: file://worker.lua
lifecycle:
auto_start: true
security:
actor:
id: "service:worker"
meta:
role: worker
service: true
policies:
- app.security:worker_policy
groups:
- workers

Active el modo estricto para denegar el acceso cuando falte el contexto de seguridad:

wippy.yaml
security:
strict_mode: true
ModoContexto AusenteComportamiento
NormalSin actor/scopePermite (permisivo)
EstrictoSin actor/scopeDeniega (seguro por defecto)

Validación de token en un handler HTTP:

local http = require("http")
local security = require("security")
local function protected_handler()
local req = http.request()
local res = http.response()
-- Extraer y validar token
local auth = req:header("Authorization")
if not auth then
return res:set_status(401):write_json({error = "Missing authorization"})
end
local token = auth:gsub("^Bearer%s+", "")
local store, _ = security.token_store("app.auth:tokens")
local actor, scope, err = store:validate(token)
if err then
return res:set_status(401):write_json({error = "Invalid token"})
end
-- Verificar permiso
if not security.can("api.users.read", "users") then
return res:set_status(403):write_json({error = "Forbidden"})
end
res:write_json({user = actor:id()})
end
return { handler = protected_handler }

Creación de token durante el login:

local actor = security.new_actor("user:" .. user.id, {role = user.role})
local scope, _ = security.named_scope("app.security:" .. user.role)
local store, _ = security.token_store("app.auth:tokens")
local token, err = store:create(actor, scope, {expiration = "24h"})
  1. Privilegio mínimo - Otorgue los permisos mínimos requeridos
  2. Denegar por defecto - Use políticas de permiso explícitas, active el modo estricto
  3. Use grupos de políticas - Organice las políticas por rol/función
  4. Firme los tokens - Siempre defina token_key_env en producción
  5. Expiración corta - Use tiempos de vida cortos para operaciones sensibles
  6. Condicione sobre el contexto - Prefiera condiciones dinámicas frente a políticas estáticas
  7. Audite acciones sensibles - Registre operaciones relevantes para la seguridad
FunciónDescripción
security.actor()Obtiene el actor actual desde el contexto
security.scope()Obtiene el scope actual desde el contexto
security.can(action, resource, meta?)Verifica permiso
security.new_actor(id, meta?)Crea un nuevo actor
security.new_scope(policies?)Crea un scope vacío o con políticas iniciales
security.policy(id)Obtiene política por ID
security.named_scope(group_id)Obtiene scope con todas las políticas del grupo
security.token_store(id)Obtiene un almacén de tokens