Skip to content

feat(security): unified multi-tenant / security / quota entrypoint (issue #5304) - #5324

Merged
qqeasonchen merged 1 commit into
apache:developfrom
qqeasonchen:feat/5304-security-gate
Sep 2, 2026
Merged

feat(security): unified multi-tenant / security / quota entrypoint (issue #5304)#5324
qqeasonchen merged 1 commit into
apache:developfrom
qqeasonchen:feat/5304-security-gate

Conversation

@qqeasonchen

@qqeasonchen qqeasonchen commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

Closes #5304 — a unified multi-tenant / security / quota entrypoint: one RequestContext flows through publish / subscribe / ACK / Connector / A2A, and SecurityGate centralizes ACL enforcement, quota accounting and audit emission in a single place.

Why

TLS, mTLS and rate limiting exist today as point capabilities: the HTTP server hand-builds FilterContext at two call sites, the A2A gateway has no auth at all, connector CRUD is ungated, there is no quota beyond a per-topic token bucket inside UniIngressService, and no audit trail. Every protocol re-derives identity its own way, and AclFilter matches actions with null because the intent isn't carried.

What changes

New package org.apache.eventmesh.runtime.security.gate (11 files)

Class Role
RequestContext Immutable identity + intent: tenantId, principal, roles, scopes, credential, remoteAddress, source, traceContext, quotaKey, operation (PUBLISH/SUBSCRIBE/ACK/CONNECTOR/A2A/ADMIN). quotaKey defaults to tenant → client → "anonymous" (unauthenticated traffic shares one bucket instead of bypassing quota). Bridges losslessly to the legacy FilterContext.
SecurityGate The single check(RequestContext, EventMeshFrame) entrypoint. Ordering: auth/ACL first (delegates to the existing FilterChain — no policy duplication), then quota (a denied request never consumes quota), then audit for every outcome. Audit exceptions are swallowed — auditing can never fail a request.
QuotaManager (SPI) tryAcquire/release per (quotaKey, Resource)CONNECTIONS / SUBSCRIPTIONS / THROUGHPUT / BACKLOG. QuotaManager.unlimited() preserves current behavior.
TenantQuotaManager In-memory default: per-tenant gauges + fixed-window throughput counters; hot path is one AtomicLong compare.
AuditSink (SPI) + LoggingAuditSink One structured INFO line per authorized op, WARN for denials/quota. SIEM implementations plug the same SPI.
GateDecision allow / deny(401/403) / quotaExceeded(429 + exhausted resource).
ConnectorAccessDeniedException ConnectorScheduler denial surface.

Wiring — opt-in; null gate = existing behavior, zero regression

Path Hook Operation
HTTP UniHttpServer.withSecurityGate(gate)checkSecurity() gates every endpoint (op inferred from path), publish endpoint passes the real frame PUBLISH / SUBSCRIBE / ACK / ADMIN
A2A A2AGatewayHttpHandler.withSecurityGate(gate) — every /a2a/* request gated in channelRead0 (principal = Authorization header) A2A
Connector ConnectorScheduler.withSecurityGate(gate)createConnector() gated; denial throws ConnectorAccessDeniedException CONNECTOR

The legacy filterChain-only configuration keeps working unchanged; the gate reuses it rather than replacing it.

Acceptance criteria (#5304)

  • A single RequestContext flows through publish / subscribe / ACK / Connector / A2A paths
  • ACL and quota enforcement is centralized, not scattered per protocol
  • Audit events are emitted for authorized operations

Tests

SecurityGateTest — 10 tests: principal/quotaKey fallback chain, ACL-action mapping per operation, allow + audit emission, ACL deny (403, no quota consumption), throughput quota (429 + per-tenant isolation + window roll), subscription acquire/release cycle, backlog release, throwing-audit-sink isolation.

eventmesh-runtime: 10/10 passed
./gradlew :eventmesh-runtime:check  →  BUILD SUCCESSFUL (compile + test + checkstyle + spotbugs)

Out of scope (follow-ups)

  • Boot wiring — composing SecurityGate (chain + quota config + sink) in UniRuntime from eventmesh.properties; this PR ships the SPIs, default impls and per-entrypoint hooks so operators can install a gate today and the boot wiring is a small config-reading change.
  • Meta-backed quotas/audit — per-tenant quota overrides and audit persistence to the Meta layer (same hot-swap pattern as AclFilter.setRules).
  • Tenant namespace enforcement on topic namestenantA. prefix policy on top of ACL rules.

…ssue apache#5304)

Introduce a single RequestContext-based security gate so identity, policy
(ACL), quota and audit flow through one path instead of being re-derived per
protocol.

New package org.apache.eventmesh.runtime.security.gate:

  RequestContext — immutable identity + intent context (tenantId, principal,
    roles, scopes, credential, remoteAddress, source, traceContext, quotaKey,
    operation PUBLISH/SUBSCRIBE/ACK/CONNECTOR/A2A/ADMIN). quotaKey defaults to
    tenant → client → "anonymous" so unauthenticated traffic shares one bucket
    instead of bypassing quota. toFilterContext() bridges losslessly to the
    legacy FilterContext consumed by existing filters.

  SecurityGate — the single check entrypoint. Ordering: auth/ACL first (via
    the existing FilterChain — TokenAuthFilter / AclFilter / ...), then quota
    (an authenticated-but-denied request never touches the quota counter), and
    audit emitted for every decision (ALLOWED / DENIED / QUOTA_EXCEEDED).
    Audit exceptions are swallowed — auditing can never fail a request.

  QuotaManager (SPI) + TenantQuotaManager (in-memory default) — per-quotaKey
    accounting for CONNECTIONS / SUBSCRIPTIONS / THROUGHPUT (fixed window) /
    BACKLOG, with acquire/release pairs so gauge-style resources don't leak.
    QuotaManager.unlimited() preserves current behavior when unconfigured.

  AuditSink (SPI) + LoggingAuditSink (default) — one structured INFO line per
    authorized op, WARN for denials; SIEM-backed implementations plug in via
    the same SPI.

  GateDecision — allow / deny(401|403) / quotaExceeded(429 + resource).

Wiring (opt-in; null gate = existing behavior, zero regression):

  UniHttpServer.withSecurityGate(gate) — checkSecurity() and the publish
    endpoint build a RequestContext (operation inferred from the request
    path) and run the gate; the legacy filterChain-only path is retained.

  A2AGatewayHttpHandler.withSecurityGate(gate) — every /a2a/* request is
    gated in channelRead0 (operation A2A; principal = Authorization header),
    rejected with 401/403/429 as appropriate.

  ConnectorScheduler.withSecurityGate(gate) — createConnector() runs the gate
    (operation CONNECTOR, topic = connector topic, client = connector client)
    and throws ConnectorAccessDeniedException when denied.

Acceptance criteria (apache#5304):

  [x] A single RequestContext flows through publish / subscribe / ACK /
      Connector / A2A paths.
  [x] ACL and quota enforcement is centralized in SecurityGate, not scattered
      per protocol.
  [x] Audit events are emitted for authorized operations (LoggingAuditSink
      default; SPI for SIEM).

Tests: SecurityGateTest — 10 tests covering principal/quotaKey fallbacks,
ACL-action mapping, allow + audit, ACL deny (403, no quota consumption),
throughput quota (429, per-tenant isolation, window roll), subscription
counting with release, backlog release, audit-sink failure isolation.

Closes apache#5304.
@qqeasonchen
qqeasonchen merged commit 796d740 into apache:develop Sep 2, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Architecture Review][P1] Unified multi-tenant / security / quota entrypoint

1 participant