Mapa de Endpoints y Roles
AgentFlow API v2 — control de acceso por rol
ADMIN
- Todas las operaciones
- Gestión de usuarios
- Eliminar agentes
- Configuración global
AGENT_MANAGER
- Crear/editar agentes
- Listar agentes
- Ver métricas
- No eliminar usuarios
VIEWER
- Ver agentes (solo lectura)
- Ver ejecuciones
- Sin escritura
- Sin admin
| Método | Ruta | Rol mínimo | Notas |
|---|---|---|---|
| GET | /api/agents | VIEWER | Paginado, filtrado seguro |
| POST | /api/agents | AGENT_MANAGER | Bean Validation completa |
| DELETE | /api/agents/{id} | ADMIN | Soft delete + auditoría |
| GET | /api/admin/users | ADMIN | Solo ADMIN, siempre auditado |
| POST | /api/auth/login | público | Rate limit: 5 req/min por IP |
Autenticación OIDC/JWT
Keycloak como Identity Provider, MicroProfile JWT
Configuración Keycloak: AgentFlow usa realm
agentflow-prod con client ID backend-api. El secreto del cliente nunca debe aparecer en el repositorio.
properties
application.properties
# ── OIDC: Keycloak realm agentflow-prod ──
quarkus.oidc.auth-server-url=https://auth.agentflow.io/realms/agentflow-prod
quarkus.oidc.client-id=backend-api
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET} # ← env var, nunca hardcoded
quarkus.oidc.token.issuer=https://auth.agentflow.io/realms/agentflow-prod
# ── MicroProfile JWT ──
mp.jwt.verify.publickey.location=META-INF/resources/keycloak-public.pem
mp.jwt.verify.issuer=https://auth.agentflow.io/realms/agentflow-prod
# ── SmallRye JWT (claim mapping) ──
smallrye.jwt.path.groups=realm_access/roles # Keycloak pone roles aquí
java
AgentResource.java
@Path("/api/agents")
@Authenticated // ← todo el recurso requiere JWT válido
@Produces(MediaType.APPLICATION_JSON)
public class AgentResource {
@Inject
JsonWebToken jwt;
@Inject
SecurityIdentity identity;
@GET
@RolesAllowed({"VIEWER", "AGENT_MANAGER", "ADMIN"})
public Response listAgents() {
String owner = jwt.getName(); // sub del token
Set<String> roles = jwt.getGroups();
return Response.ok(Map.of(
"user", owner,
"roles", roles,
"agents", agentService.listForUser(owner, identity)
)).build();
}
@DELETE
@Path("/{id}")
@RolesAllowed("ADMIN")
public Response deleteAgent(@PathParam("id") Long id) {
agentService.softDelete(id);
return Response.noContent().build();
}
}
Validación de Entrada
Bean Validation + validadores personalizados para POST /api/agents
java
CreateAgentDto.java
// ✗ MAL — sin validación, cualquier payload pasa
public record CreateAgentDto(String name, String prompt, String model) {}
// ✓ BIEN — validación completa con Bean Validation
public record CreateAgentDto(
@NotBlank
@Size(min = 3, max = 80)
@Pattern(regexp = "^[\\w\\s\\-áéíóúÁÉÍÓÚñÑ]+$", message = "Nombre solo permite letras, números, espacios y guiones")
String name,
@NotBlank
@Size(max = 4000, message = "El prompt no puede superar 4000 caracteres")
String prompt,
@NotNull
@Pattern(regexp = "^(gpt-4o|claude-3|gemini-1\\.5).*$", message = "Modelo no permitido")
String model,
@Valid
@NotNull
AgentConfigDto config // nested validation
) {}
// Endpoint que usa el DTO validado
@POST
@RolesAllowed({"AGENT_MANAGER", "ADMIN"})
public Response createAgent(@Valid @NotNull CreateAgentDto dto) {
Agent agent = agentService.create(dto, identity.getPrincipal().getName());
return Response.status(201).entity(agent).build();
}
Tip: Añade un
ExceptionMapper<ConstraintViolationException> para devolver errores de validación en formato JSON estructurado en lugar del 400 genérico de Quarkus.
Prevención de SQL Injection
Panache con queries parametrizadas — nunca concatenación de strings
java
AgentRepository.java
@ApplicationScoped
public class AgentRepository implements PanacheRepository<Agent> {
// ✗ MAL — nunca concatenar input del usuario en queries
public List<Agent> searchBad(String name) {
return list("name = '" + name + "'"); // ← VULNERABLE
}
// ✓ BIEN — parámetro posicional
public List<Agent> findByName(String name, String owner) {
return list("LOWER(name) LIKE ?1 AND owner = ?2 AND active = true",
"%" + name.toLowerCase() + "%", owner);
}
// ✓ BIEN — named parameters (más legible)
public List<Agent> findActiveByModel(String model, String owner) {
return list("model = :model AND owner = :owner AND active = true",
Parameters.with("model", model).and("owner", owner));
}
// ✓ Query nativa segura (necesaria para FTS)
public List<Agent> fullTextSearch(String query) {
return getEntityManager()
.createNativeQuery(
"SELECT * FROM agents WHERE to_tsvector('spanish', name || ' ' || prompt) @@ plainto_tsquery('spanish', :q)",
Agent.class)
.setParameter("q", query) // ← parametrizado, seguro
.getResultList();
}
}
Cabeceras de Seguridad HTTP
Filter global para todas las respuestas de la API
java
SecurityHeadersFilter.java
@Provider
@Priority(Priorities.HEADER_DECORATOR)
public class SecurityHeadersFilter implements ContainerResponseFilter {
@Override
public void filter(ContainerRequestContext req, ContainerResponseContext res) {
var h = res.getHeaders();
// Clickjacking: AgentFlow es API pura, no necesita frames
h.putSingle("X-Frame-Options", "DENY");
// MIME sniffing
h.putSingle("X-Content-Type-Options", "nosniff");
// HSTS — 1 año en producción
h.putSingle("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload");
// CSP — API JSON, sin scripts inline
h.putSingle("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
// Ocultar tecnología
h.remove("Server");
h.remove("X-Powered-By");
// Referrer policy
h.putSingle("Referrer-Policy", "strict-origin-when-cross-origin");
}
}
properties
CORS — application.properties
quarkus.http.cors=true
quarkus.http.cors.origins=https://app.agentflow.io,https://admin.agentflow.io
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
quarkus.http.cors.headers=accept,authorization,content-type
quarkus.http.cors.access-control-allow-credentials=true
quarkus.http.cors.access-control-max-age=12H
Rate Limiting
Protección brute-force en /api/auth/login — 5 intentos/min por IP real
Seguridad: Nunca uses
X-Forwarded-For directamente — los clientes pueden falsificarlo. Usa getRemoteAddr() del servlet request, o configura quarkus.http.proxy.proxy-address-forwarding=true si hay proxy de confianza.
java
LoginRateLimitFilter.java
@Provider
@Priority(Priorities.AUTHENTICATION - 1) // antes de auth
public class LoginRateLimitFilter implements ContainerRequestFilter {
// 5 intentos por minuto por IP en /api/auth/login
private static final int MAX_ATTEMPTS = 5;
private static final long WINDOW_MS = 60_000;
private final ConcurrentHashMap<String, Deque<Long>> attempts = new ConcurrentHashMap<>();
@Context
HttpServletRequest servletRequest;
@Override
public void filter(ContainerRequestContext ctx) {
if (!ctx.getUriInfo().getPath().startsWith("/api/auth/login")) return;
String ip = servletRequest.getRemoteAddr(); // IP real (o proxy confiable)
Deque<Long> ts = attempts.computeIfAbsent(ip, k -> new ArrayDeque<>());
long now = System.currentTimeMillis();
synchronized (ts) {
// Limpiar timestamps fuera de la ventana
ts.removeIf(t -> (now - t) > WINDOW_MS);
if (ts.size() >= MAX_ATTEMPTS) {
ctx.abortWith(Response.status(429)
.header("Retry-After", 60)
.entity(Map.of("error", "Demasiados intentos. Espera 60 segundos."))
.build());
return;
}
ts.addLast(now);
}
}
}
Gestión de Secretos
Variables de entorno en K8s Secrets + HashiCorp Vault para producción
properties
application.properties — referencias, nunca valores
# ✓ BIEN — referenciar variables de entorno, nunca el valor real
quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:5432/agentflow
quarkus.datasource.username=${DB_USER}
quarkus.datasource.password=${DB_PASSWORD}
quarkus.oidc.credentials.secret=${OIDC_CLIENT_SECRET}
agentflow.openai.api-key=${OPENAI_API_KEY}
agentflow.anthropic.api-key=${ANTHROPIC_API_KEY}
# Vault (producción)
quarkus.vault.url=https://vault.agentflow.io
quarkus.vault.authentication.kubernetes.role=agentflow-api
quarkus.vault.secret-config-source.path=agentflow/prod/secrets
java
LLMProviderService.java — inyección segura de API keys
@ApplicationScoped
public class LLMProviderService {
// Las keys llegan desde Vault o env vars, nunca hardcoded
@ConfigProperty(name = "agentflow.openai.api-key")
String openAiKey;
@ConfigProperty(name = "agentflow.anthropic.api-key")
String anthropicKey;
// Nunca loguear las keys — enmascarar en logs de diagnóstico
public String getMaskedKey(String provider) {
return switch (provider) {
case "openai" -> "sk-...****" + openAiKey.substring(openAiKey.length() - 4);
case "anthropic" -> "sk-ant-...****" + anthropicKey.substring(anthropicKey.length() - 4);
default -> "****";
};
}
}
Auditoría de Operaciones
Log estructurado de acciones sensibles con usuario, recurso y timestamp
java
AuditService.java + uso en AdminResource
@ApplicationScoped
public class AuditService {
private static final Logger AUDIT = Logger.getLogger("agentflow.audit");
@Inject SecurityIdentity identity;
public void log(String action, String resource, String resourceId) {
String user = identity.isAnonymous() ? "anonymous"
: identity.getPrincipal().getName();
String roles = identity.getRoles().toString();
AUDIT.infof(
"AUDIT action=%s resource=%s id=%s user=%s roles=%s ts=%s",
action, resource, resourceId, user, roles, Instant.now()
);
}
}
// Uso en el recurso de administración
@Path("/api/admin")
@Authenticated
public class AdminResource {
@Inject AuditService audit;
@GET
@Path("/users")
@RolesAllowed("ADMIN")
public List<UserDto> listUsers() {
audit.log("LIST", "users", "all");
return userService.findAll();
}
@DELETE
@Path("/users/{id}")
@RolesAllowed("ADMIN")
public Response deleteUser(@PathParam("id") Long id) {
audit.log("DELETE", "user", String.valueOf(id));
userService.delete(id);
return Response.noContent().build();
}
}
Checklist de Seguridad — AgentFlow API
Verificación antes de deploy a producción
-
✓
OIDC/JWT configurado — Keycloak realm agentflow-prod, publicKey.pem en classpath, issuer verificado
-
✓
RBAC con @RolesAllowed — ADMIN, AGENT_MANAGER, VIEWER mapeados a claims JWT de Keycloak
-
✓
Bean Validation en todos los POST/PUT — @Valid en CreateAgentDto y todas las entidades mutables
-
✓
Queries parametrizadas — cero concatenación de strings en Panache/JPQL/nativas
-
✓
Cabeceras de seguridad — X-Frame-Options, HSTS, CSP, nosniff en todas las respuestas
-
✓
Rate limiting en /api/auth/login — máx. 5 intentos/60s por IP real (no X-Forwarded-For)
-
✓
Secretos en Vault/env vars — ningún secreto en application.properties ni en el repositorio
-
✓
Auditoría de operaciones ADMIN — log estructurado con usuario, rol, recurso y timestamp
-
✓
CORS restringido — solo app.agentflow.io y admin.agentflow.io en lista blanca
-
✓
Escaneo CVE de dependencias — mvn dependency-check:check en CI/CD pipeline