Add Shared Signals Framework Transmitter capability#48256
Open
thomasdarimont wants to merge 219 commits intokeycloak:mainfrom
Open
Add Shared Signals Framework Transmitter capability#48256thomasdarimont wants to merge 219 commits intokeycloak:mainfrom
thomasdarimont wants to merge 219 commits intokeycloak:mainfrom
Conversation
6b46c3e to
279353a
Compare
Contributor
Author
|
@pedroigor @sguilhen as discussed here is the initial PR with a fully featured SSF Transmitter implementation without the SSF Receiver support. The SSF feature is now aligned with the structure of the SCIM feature as multiple sub modules: |
Contributor
Author
Unreported flaky test detectedIf the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR. org.keycloak.testsuite.model.singleUseObject.SingleUseObjectModelTest#testClusterKeycloak CI - Store Model Tests |
c3fe2c5 to
5218eae
Compare
This was referenced Apr 20, 2026
5218eae to
008a191
Compare
This was referenced Apr 20, 2026
Open
e6aa710 to
b6b32c2
Compare
9be57a4 to
df16cab
Compare
Preparation for the file-by-file SSF runtime cutover. Adds the generic-outbox plumbing the dispatcher / poll service / stream service migrations will reach for, in parallel with the existing SsfEventStore plumbing so call sites can migrate one at a time without breaking the world. Build stays green. - OutboxStore: new enqueueHeld(...) mirroring enqueuePending; both delegate to a shared enqueueInStatus helper. Closes the gap with SSF's existing enqueueHeldPush / enqueueHeldPoll semantics so the dispatcher's HELD-on-paused-stream branch has a generic landing spot. - SsfTransmitterContext: parallel outboxStoreFactory field plus outboxStore(session) / outboxStoreFactory() accessors. Legacy eventStore* accessors stay for the duration of the migration so existing callers keep compiling. - DefaultSsfTransmitterProviderFactory: new createOutboxStore(session) factory method (default new OutboxStore(session)), threaded into createTransmitterContext alongside the existing createPendingEventStore. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Redirects the dispatcher's four enqueue sites from SsfEventStore to OutboxStore. After this commit the SSF dispatcher writes new rows into OUTBOX_ENTRY (entry_kind = "ssf-push" / "ssf-poll") instead of SSF_PENDING_EVENT. - Constructor overloads: Function<KeycloakSession, SsfEventStore> -> Function<KeycloakSession, OutboxStore>; field renamed eventStoreFactory -> outboxStoreFactory. - enqueueForPush -> enqueuePending(SsfOutboxKinds.PUSH, ...) with streamId mapped onto the new container_id column and the SET's jti as correlation_id. - enqueueForPoll -> enqueuePending(SsfOutboxKinds.POLL, ...). - holdEvent (PUSH/RISC_PUSH) -> enqueueHeld(SsfOutboxKinds.PUSH, ...). - holdEvent (POLL/RISC_POLL) -> enqueueHeld(SsfOutboxKinds.POLL, ...). - Javadoc references to SsfEventStore / SsfEventStatus updated to OutboxStore / OutboxEntryStatus. - DefaultSsfTransmitterProviderFactory.createDispatcher now passes ctx.outboxStoreFactory() into the dispatcher. Intermediate state worth noting: the legacy SsfPushOutboxDrainerTask is still the only drainer scheduled, and it reads SSF_PENDING_EVENT, not OUTBOX_ENTRY. Events the dispatcher writes after this commit will sit in OUTBOX_ENTRY until the drainer cutover (next commit) wires up OutboxDrainerTask + SsfPushDeliveryHandler. Build remains green; do not run a server between this commit and the drainer cutover. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Replaces the SSF-specific drainer + cleanup task in the transmitter
factory with the generic OutboxDrainerTask and OutboxCleanupTask. The
SsfPushDeliveryHandler from the earlier prep commit is now the
delivery brain the drainer hands rows to.
- scheduleOutboxDrainer constructs an OutboxDrainerTask with an
OutboxConfig for entryKind="ssf-push" (batchSize, OutboxBackoff
using DEFAULT_HTTP_PUSH_CURVE, deadLetter / delivered retention,
pendingMaxAge), a SsfPushDeliveryHandler wired to createPushDelivery
+ metricsBinder, and createOutboxStore as the per-session DAO
factory. Timer task name stays "SsfPushOutboxDrainerTask" so
existing dashboards / log filters match. Startup log line picks
up the entryKind label.
- outboxDrainerMaxAttempts default switches to
OutboxBackoff.DEFAULT_MAX_ATTEMPTS (same value, generic source).
- Cleanup listener in createProviderEventListener now translates
RealmRemovedEvent / ClientRemovedEvent into OutboxCleanupTask
runs (CLIENT scope renamed to OWNER for the generic version).
submitOutboxCleanup queues two tasks per event, one per SSF
entryKind ("ssf-push" + "ssf-poll"), so realm/client removal
cleans up both PUSH and POLL outbox rows.
- createDrainerTask, createOutboxConfig, createSsfPushDeliveryHandler,
createOutboxCleanUpTask: new generic-outbox-shaped factory methods
replacing createSsfPushOutboxDrainerTaskConfig and the legacy
cleanup-task helper.
- Imports for SsfPushOutboxDrainerTask, SsfPushOutboxDrainerTaskConfig,
SsfPushOutboxBackoff, SsfOutboxCleanupTask removed; OutboxBackoff,
OutboxCleanupTask, OutboxConfig, OutboxDrainerTask, SsfOutboxKinds,
SsfPushDeliveryHandler added. Two javadoc references updated to
point at the generic types.
Runtime state after this commit:
- SSF dispatcher writes -> OUTBOX_ENTRY -> generic drainer for
entryKind=ssf-push -> SsfPushDeliveryHandler -> receiver. PUSH
delivery is now end-to-end on the new infrastructure.
- The legacy SsfPushOutboxDrainerTask is no longer scheduled. Any
rows still in SSF_PENDING_EVENT go un-drained until the table
drops in the dead-code cleanup commit; acceptable for an
experimental feature with no real users.
- POLL endpoint still reads from SsfEventStore (old table) — that
cuts over in the next commit.
- Stream lifecycle and admin REST still use SsfEventStore — pending.
Do not run a server between this commit and the POLL cutover: new
POLL events are written to OUTBOX_ENTRY but PollDeliveryService still
reads SSF_PENDING_EVENT.
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Redirects the SSF POLL endpoint's ack / nack / read / count call sites from SsfEventStore to OutboxStore with entryKind="ssf-poll". After this commit POLL receivers read from OUTBOX_ENTRY directly, matching where the dispatcher already writes. - Field/parameter eventStore: SsfEventStore -> outboxStore: OutboxStore. - Ack -> outboxStore.ackPendingForOwner(SsfOutboxKinds.POLL, ...). - Nack -> outboxStore.nackPendingForOwner(SsfOutboxKinds.POLL, ...). - Read -> outboxStore.lockPendingForOwner(SsfOutboxKinds.POLL, ...). - moreAvailable count -> outboxStore.countForOwnerByStatus( SsfOutboxKinds.POLL, ownerId, OutboxEntryStatus.PENDING). - Row mapping: row.getJti() -> row.getCorrelationId(), row.getEncodedSet() -> row.getPayload(). - Imports: drop SsfEventEntity / SsfEventStatus / SsfEventStore; add OutboxEntryEntity / OutboxEntryStatus / OutboxStore / SsfOutboxKinds. - DefaultSsfTransmitterProviderFactory.createPollDelivery now passes provider.context().outboxStore(session) into the service. Runtime state after this commit: - PUSH and POLL are both end-to-end on the new infrastructure. - Stream lifecycle ops (pause/resume/disable, events_requested narrowing, push <-> poll migration) still reach into SsfEventStore from StreamService — pending in the next commit. - Admin REST endpoints (stats, delete, queued) still on SsfEventStore — pending. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Redirects every SSF stream-lifecycle SsfEventStore touchpoint in StreamService to the generic OutboxStore. After this commit the SSF runtime is fully consistent on the new outbox: dispatcher writes, drainer + handler reads, POLL reads, and stream lifecycle all share the OUTBOX_ENTRY table. - Field/constructor: eventStoreFactory: Function<KeycloakSession, SsfEventStore> -> outboxStoreFactory: Function<KeycloakSession, OutboxStore>. - Push <-> poll delivery-method change: rewritten as a kind migration. The new method maps to SsfOutboxKinds.PUSH or POLL; the current kind is the opposite; one call to migrateEntryKindForOwner re-tags the queued backlog. The legacy SsfEventEntity.DELIVERY_METHOD_* constants are no longer needed. - events_requested narrow: deadLetterQueuedForOwnerNotMatchingTypes is called once per kind (PUSH + POLL) and the count is summed. DEAD_LETTER_REASON_EVENT_TYPE_NO_LONGER_REQUESTED moves from SsfEventStore onto StreamService as a public static final so the log/audit string survives the SsfEventStore deletion. - Stream-delete cascade: deleteByOwner(PUSH, ownerId) + deleteByOwner(POLL, ownerId). - Stream-status transitions in updateStreamStatus: * disabled -> deleteQueuedByOwner per kind (SSF spec: a disabled stream MUST NOT transmit AND will not hold). * enabled-> releaseHeldForOwner per kind. * enabled -> paused -> holdPendingForOwner per kind. - DefaultSsfTransmitterProvider.streamService() passes context.outboxStoreFactory() into the service. - Imports: drop SsfEventEntity / SsfEventStore; add OutboxStore / SsfOutboxKinds. Runtime state after this commit: - PUSH, POLL, stream lifecycle, and realm/client cleanup are all end-to-end on OUTBOX_ENTRY. - Admin REST endpoints (stats, delete, queued, getPendingEvent) still use SsfEventStore -- 2c covers those. - Dead code (SsfEventEntity, SsfEventStore, SsfEventStatus, SsfPushOutboxDrainerTask, SsfPushOutboxDrainerTaskConfig, SsfPushOutboxBackoff, SsfOutboxCleanupTask) and the legacy SSF_PENDING_EVENT table land in 2d. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Redirects every admin REST endpoint that touched SsfEventStore to OutboxStore. SSF push and poll rows live under separate kinds in the generic outbox; the admin endpoints aggregate across both so the REST shape doesn't expose the implementation split. - Imports: drop SsfEventEntity / SsfEventStatus / SsfEventStore; add OutboxEntryEntity / OutboxEntryStatus / OutboxStore / SsfOutboxKinds. - SSF_OUTBOX_KINDS = List.of(SsfOutboxKinds.PUSH, SsfOutboxKinds.POLL): the two kinds the admin endpoints loop over. - getEventStats (realm), getClientEventStats (per-client): loop both kinds, call countStatusesFor* / oldestCreatedAtPerStatusFor*, merge into one SsfEventStatsRepresentation. New private helpers mergeCounts (sum), mergeOldest (min), toEventStatsRepresentation (rep builder lifted out of the inline duplication). - deleteEvents / deleteClientEvents (status[+olderThan]): parse OutboxEntryStatus.valueOf(...), loop kinds, sum the deleted counts. Per-client now uses deleteByOwnerAndStatus(OlderThan). - deleteQueuedEvents / deleteClientQueuedEvents: loop both kinds via deleteQueuedByRealm / deleteQueuedByOwner. - getPendingEvent: single-row jti lookup tries both kinds via findByOwnerAndCorrelationId, returns the first non-null match; 404 if neither matches. - toPendingEventRepresentation: takes OutboxEntryEntity; field mapping correlationId -> jti, entryType -> eventType, containerId -> streamId, payload -> encodedSet/decodedSet, entryKind -> deliveryMethod via new deliveryMethodLabel helper (ssf-push -> "PUSH", ssf-poll -> "POLL", unknown kinds pass through). Wire shape of SsfPendingEventRepresentation is unchanged. - One javadoc reference SsfEventStatus#QUEUED -> OutboxEntryStatus#QUEUED. Runtime state after this commit: - The full SSF stack (dispatcher, drainer, POLL service, stream lifecycle, realm/client cleanup, admin REST) runs against OUTBOX_ENTRY. SsfEventStore / SsfEventEntity / SsfEventStatus / SsfPushOutboxDrainerTask / SsfPushOutboxDrainerTaskConfig / SsfPushOutboxBackoff / SsfOutboxCleanupTask now have zero callers and are dead code. The dead-code deletion + SSF_PENDING_EVENT drop + legacy changelog / JpaEntityProvider removal land in 2d. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
The "Pending" prefix was a holdover from when the rep only surfaced via the PENDING-only lookup endpoint. The rep already covers any outbox status (PENDING, HELD, DELIVERED, DEAD_LETTER) — fields like deliveredAt, attempts, and lastError confirm the broader contract — so dropping the prefix matches what the class actually returns. - SsfPendingEventRepresentation.java -> SsfEventRepresentation.java via git mv so blame/history follows. - Class declaration renamed; class-level javadoc updated to call out that the rep covers any status rather than only PENDING. - SsfAdminResource: import, both @Schema(implementation = ...) references, return type, and two javadoc @link references updated. Helper method toPendingEventRepresentation renamed to toEventRepresentation for consistency. - Wire shape (field names, JSON keys) is unchanged. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
…ated Strips out the legacy SSF push outbox now that the dispatcher, drainer, POLL service, stream lifecycle, realm/client cleanup, and admin REST all run against the generic OutboxStore from earlier commits. Pure deletion + a handful of stragglers retyped against the new types; no behavior change. Production code retyped: - SsfMetricsBinder: RealmStatus record's status field flips from SsfEventStatus to OutboxEntryStatus. - SsfTransmitterContext: drop eventStoreFactory field, constructor parameter, eventStore(session), and eventStoreFactory(). Only the outboxStore / outboxStoreFactory accessors remain. - DefaultSsfTransmitterProviderFactory: drop createPendingEventStore, remove the legacy factory parameter from createTransmitterContext, drop the SsfEventStore import. createOutboxStore is the canonical extension point now. - StreamService.DEAD_LETTER_REASON_EVENT_TYPE_NO_LONGER_REQUESTED: value restored to "event_type_no_longer_requested" to keep the audit string (and the existing test assertion) stable. - SsfEventStatsRepresentation, PollRequest, SsfPushDeliveryHandler, OutboxStore, OutboxEntryEntity: javadoc / comment references to deleted SSF types replaced with the generic equivalents or removed. - SsfTransmitterStreamManagementTests: runtime portions migrated to OutboxStore / OutboxEntryEntity / OutboxEntryStatus / SsfOutboxKinds.PUSH. Deleted (15 files): - 7 SSF-specific outbox classes: SsfEventEntity, SsfEventStatus, SsfEventStore, SsfPushOutboxBackoff, SsfPushOutboxDrainerTask, SsfPushOutboxDrainerTaskConfig, SsfOutboxCleanupTask. - 2 JPA-provider plumbing classes: SsfJpaEntityProvider, SsfJpaEntityProviderFactory; their store/jpa parent directory is empty and removed. - 1 META-INF service file (org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory) that pointed at the deleted factory. - 4 SSF-specific Liquibase changelogs (ssf-changelog-master.xml + 1.0.0 / 1.0.1 / 1.0.2). The SSF_PENDING_EVENT table never reached a real release, so no <dropTable> migration is needed — the changelog simply disappears. - SsfEventStoreTests.java — its referenced types are gone. The next commit re-creates it as OutboxStoreTests against the generic store, preserving coverage. Build state after this commit: - The full SSF stack and the generic outbox infrastructure both compile; production code no longer references any deleted symbol. - The legacy SsfEventStoreTests is gone; the OutboxStoreTests rewrite (next turn) restores the test coverage. - The timer task name "SsfPushOutboxDrainerTask" intentionally survives as a stable string identifier so ops dashboards / log filters keep matching. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
…the generic store
Restores the test coverage that was deleted in the dead-code cleanup
commit. Tests now exercise the generic OutboxStore against synthetic
entryKind values ("test-kind" + "test-kind-other") rather than SSF
push/poll, so the suite verifies generic store semantics independent
of any specific consumer.
- Enqueue: defaults, dedup on (entryKind, ownerId, correlationId),
no dedup across kinds, enqueueHeld persists with HELD status.
- Drainer reads: lockDueForDrain — next_attempt_at gating, batch
limit, entryKind filtering.
- Row transitions: markDelivered (clears lastError, stamps
deliveredAt), recordFailure (bumps attempts, schedules retry,
truncates last_error to 2048), markDeadLetter (terminal,
bumps attempts), promoteStaleQueuedToDeadLetter (promotes
PENDING + HELD past the cutoff to DEAD_LETTER; terminal rows
preserved; attempts NOT bumped).
- Stats: countStatusesForRealm / countStatusesForOwner /
oldestCreatedAtPerStatusForRealm / oldestCreatedAtPerStatusForOwner
with cross-realm and cross-owner isolation; statuses with zero
rows absent (no synthetic zero-rows).
- POLL reader: lockPendingForOwner (arrival order, owner filter),
countForOwnerByStatus, ackPendingForOwner (silently scoped,
idempotent), nackPendingForOwner (records receiver-supplied
reason in lastError).
- Owner lifecycle: releaseHeldForOwner, holdPendingForOwner,
deadLetterQueuedForOwner, deadLetterQueuedForOwnerNotMatchingTypes
(with empty-allow-list fallback), migrateEntryKindForOwner
(queued migrate, terminal preserved, sibling-owner isolation).
- Admin / cascade deletes: deleteByRealm, deleteByRealmAndStatus[OlderThan],
deleteByOwnerAndStatus, deleteQueuedByRealm, deleteQueuedByOwner.
- Retention purges: purgeDeliveredOlderThan, purgeDeadLetterOlderThan.
- Cleanup task: realm-scope drain, owner-scope drain with
sibling-owner survival.
The file lives at ssf/tests/base/src/test/java/.../outbox/OutboxStoreTests.java
because that's where the runOnServer integration-test scaffolding is
wired. Extracting to a more neutral location can come later if the
outbox grows another consumer that wants its own integration tests.
The test config enables Profile.Feature.SSF since the SSF transmitter
is currently the only registered consumer of the outbox machinery.
Coverage parity with the deleted SsfEventStoreTests plus a few new
generic-only tests: cross-kind dedup isolation, lockDueForDrain
filtering on entryKind, dedicated enqueueHeld test.
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Per Keycloak team request: nest the durable event-queue machinery under org.keycloak.events.* alongside the existing event-listener and event-storage infrastructure rather than sitting at the top level. Pure rename, no behavior change. - 7 source files moved via git mv from model/jpa/src/main/java/org/keycloak/outbox/ to model/jpa/src/main/java/org/keycloak/events/outbox/: OutboxBackoff, OutboxCleanupTask, OutboxConfig, OutboxDeliveryHandler, OutboxDeliveryOutcome, OutboxDrainerTask, OutboxStore. Empty old directory removed. - Package declaration on each moved file updated. - Imports rewritten across the tree (model/jpa, tests/base, ssf/transmitter, ssf/services, ssf/tests/base). - Three javadoc references in jpa-changelog-26.7.0.xml updated to point at the new package. The OutboxEntryEntity / OutboxEntryStatus pair stays in org.keycloak.models.jpa.entities — that's the established convention for JPA entities and there's no reason to move them. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
…rectly Replace the context-mediated outboxStoreFactory()/outboxStore() lookups in DefaultSsfTransmitterProviderFactory with this::createOutboxStore, matching the pattern already used by createDrainerTask and createOutboxCleanUpTask. Keeps the factory the single owner of how an OutboxStore is built so subclasses overriding createOutboxStore see the override across every wiring site. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Production code lives in org.keycloak.events.outbox; the tests should mirror that. Now nested under the existing org.keycloak.tests.events package, so Base2TestSuite picks them up transitively via the existing @SelectPackages entry — no explicit suite registration needed. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Failed push deliveries previously left "delivery failed" in last_error regardless of cause, e.g. a DNS lookup failure, receiver replying 503, malformed stream config all looked the same in the admin view. The rich detail was logged at WARN/ERROR by PushDeliveryService and immediately discarded. Two-step propagation: - PushDeliveryService.deliverEvent now returns a structured PushDeliveryOutcome (delivered / httpFailure / transportFailure / invalidConfig) instead of a bare boolean. The HTTP status + response body or the underlying exception class + message are carried up intact. - OutboxDeliveryHandler#deliver returns a new OutboxDeliveryResult that pairs the outcome with an errorMessage string. The drainer persists errorMessage into last_error, so the admin grid + log scans see the receiver's status / the transport exception directly. SsfPushDeliveryHandler.formatLastError builds the one-line summary: HTTP 503 https://...: Failed to fetch JWKS from transmitter... UnknownHostException https://...: ssf.caep.dev: nodename nor servname... InvalidStreamConfig: missing endpoint URL Detail fields are capped at 1KB so the column (VARCHAR 2048) absorbs the prefix + URL + detail comfortably; the store's truncateError caps the final write as defense in depth. Deliberately not adding a structured metadata channel: every retry would rewrite a CLOB JSON document, and the prior failure detail would linger on DELIVERED rows until the 24h retention purge — bloat without enough operational upside over a richer last_error line. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
getServiceAccountClientLink() returns the internal client UUID (set via ClientManager.enableServiceAccount → user.setServiceAccountClientLink(client.getId())), not the public clientId. Compare against client.getId() so the gate correctly accepts the receiver's own SA bearer and rejects regular users (link == null) and other clients' SAs (link == other UUID). Adds four SA-gate tests to SsfTransmitterStreamManagementTests: SA bearer accepted, regular-user bearer rejected, opt-out via ssf.requireServiceAccount=false, and a runOnServer white-box test for the cross-SA branch that can't be reached via the public token endpoint. Refactors the password-grant helper onto the test-framework OAuthClient. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Receiver clients supply the push delivery URL; without a gate the transmitter would POST signed SETs to anything they declare — loopback, cloud IMDS, RFC 1918. Adds a per-receiver allow-list ssf.validPushUrls (## delimited, redirect-URI match semantics with bare-* rejection) and a per-receiver ssf.allowedDeliveryMethods family gate. Receiver-supplied URLs must be https and resolve to a public host unless the allow-insecure-push-targets SPI flag is set (default false). Validator factory-pluggable via SsfTransmitterServiceBuilder#createPushUrlValidator and cached on SsfTransmitterContext. On rejection the server log carries a suggested allow-list entry derived from the URL so operators can fix the client attribute in one step. Updates real-push test fixtures (SPI flag + ssf.validPushUrls on receiver clients) and adds unit + integration coverage for the gate's match semantics and rejection cases. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
URL allow-list validation now runs before the duplicate-stream guard and before applyLegacyFields. Two upsides: - A receiver with both a stale stream and a misconfigured push URL gets the actionable URL diagnostic (400) first instead of the duplicate error (409) that hides it. - applyLegacyFields writes a client attribute on the SSE_CAEP path; moving it after URL validation makes "rejection means no client state mutation" a guarantee rather than a coincidence. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
New "Delivery" section between Authentication and Events surfaces the two SSRF-gate attributes: - ssf.allowedDeliveryMethods — pair of Push/Poll checkboxes. Both rendered as checked when the attribute is unset (matches the server-side blank-default behaviour). At least one must remain checked; toggling off the last one silently re-enables it so the UI never produces an ambiguous blank value the server would read back as "both allowed". - ssf.validPushUrls — MultiLineInput list editor (one URL per row, add/remove buttons), conditionally rendered only when Push is in the selected methods. Stored as ##-separated via stringify, same shape as request_uris and post.logout.redirect.uris. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Two concurrent enqueue paths producing the same (entryKind, ownerId, correlationId) triple, e.g. admin emit retries across nodes, retried synthetic emits with a stable jti, and similar, could trip UC_OUTBOX_KIND_OWNER_CORRELATION between the in-memory dedup find and the persist, surfacing as an unhandled PersistenceException that obscured whether the event was captured. Replace the persist+flush+catch path with an HQL INSERT ... VALUES ... ON CONFLICT DO NOTHING named query (Hibernate 6.5+, dialect- portable). On a race the storage engine no-ops our insert, executeUpdate returns 0, and we re-fetch the racing row to return its id. No exception thrown, no JTA rollback-only marker, the caller's surrounding transaction now survives the race cleanly. JDBC batching is preserved (no forced flush). Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
ssf.stream.eventsSupported, ssf.stream.eventsRequested, and ssf.stream.eventsDelivered were persisted as full canonical event- type URIs (~60 chars each, e.g. https://schemas.openid.net/secevent/caep/event-type/credential-change), which add up across the per-stream cap of 32 entries and pressure the per-client attribute size budget. Compact at storage time via the existing SsfEventRegistry: each URI is rewritten to its short alias (CaepCredentialChange etc.) on save and re-expanded to the canonical URI on load. Custom event types whose provider factory did not register an alias pass through unchanged, so the optimisation is opportunistic rather than mandatory. The wire shape (StreamConfig in memory and the receiver- facing JSON) stays canonical-URI; only the at-rest representation changes. Backwards-compatible: read-side accepts both forms, so receivers that pre-date this change continue to load correctly, and the next stream save (any PATCH / PUT / status flip) rewrites them to aliases automatically. ssf.supportedEvents (per-client allow-list, authored against the alias vocabulary by the admin UI) was already in this shape and is unchanged. Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Removes 20 unused event classes (6 CAEP, 14 RISC including the
RiscEvent base) plus their STANDARD_EVENT_FACTORIES registrations
in DefaultSsfEventProviderFactory and the matching cases in
SsfEventValidationTest.
Kept: CaepEvent (base), CaepCredentialChange (+ its
CaepCredentialChangeChangeTypeDeserializer), CaepSessionRevoked —
the v1 emittable set, also covers SSE CAEP / ABM interop.
EMITTABLE_EVENT_TYPES drops CaepDeviceComplianceChange so the
admin emit endpoint stops advertising an event the transmitter
can't materialise. NATIVELY_EMITTED_EVENT_TYPES was already
{CaepCredentialChange, CaepSessionRevoked} and stays.
Future event catalogues (full RISC, additional CAEP) plug back in
via SsfEventProviderFactory without re-introducing the deleted
classes. RISC delivery-method URIs in Ssf are kept, as those are
transport identifiers, distinct from RISC event types.
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
…ctManagementResource Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
scheduleTransmitterInitiatedAsyncStreamVerification spun up a fresh
single-thread ScheduledExecutorService per stream-create, then shut
it down. Bypasses Keycloak's executor lifecycle and burns an OS
thread for one ~1.5s task.
Hand the dispatch to ExecutorsProvider's named pool
("ssf-stream-verification") instead, with the verification delay
provided by CompletableFuture.delayedExecutor. The JDK's shared
timer waits without holding a worker thread, then forwards to the
managed pool when due. Operators can tune the pool via the
standard executor SPI configuration the same way every other
Keycloak managed pool is tuned.
No change to verification timing or transaction semantics; the
sub-session begin/commit body is preserved verbatim.
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Switch OUTBOX_ENTRY.PAYLOAD and METADATA from CLOB/@lob to NCLOB/@Nationalized so payloads round-trip cleanly under databases configured for national-character storage (Oracle NLS, SQL Server NVARCHAR/NTEXT defaults). Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Operators can now externalize an SSF stream's push-delivery
authorization header by setting the client attribute to
${vault.x}; ClientStreamStore resolves it on read so the secret
no longer has to live in the database. A no-op round-trip from
the receiver preserves the placeholder rather than overwriting
it with the resolved plaintext, and StreamService rejects vault
syntax submitted via the receiver-facing API to prevent a rogue
receiver from indirectly reading arbitrary vault entries.
Renames validateFieldLengths to validateFieldConstraints since
it now enforces both length caps and content rules.
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
Signed-off-by: Thomas Darimont <thomas.darimont@googlemail.com>
1fc3ec8 to
cfa9ae3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Shared Signals Framework support to Keycloak in the SSF Transmitter role: Keycloak signs Security Event Tokens (SETs, RFC 8417) describing realm/user/session/credential events and delivers them to OAuth clients
registered as SSF Receivers, either by HTTP PUSH (RFC 8935) or HTTP POLL (RFC 8936).
Targets the OpenID Shared Signals Framework 1.0 (Final) specification plus the CAEP Interoperability Profile 1.0. Ships the legacy SSE CAEP profile alongside for Apple Business Manager / Apple School Manager interop, since Apple device-fleet enrolment is a concrete drive-use case.
Gated behind
Profile.Feature.SSFexperimental, opt-in.Background
Issue #43614 originally proposed SSF Receiver support (Keycloak ingesting SETs from upstream IdPs / risk engines). After exploring both sides, we're shipping the Transmitter first (see #48254) because it covers the strongest community asks (federate Keycloak events to downstream SaaS, Apple device fleet revoke flow) and lets us validate the SSF data-plane against real receivers before designing the harder "action mapping" question on the Receiver side. Receiver support remains on the roadmap and is tracked separately via #43614.
Scope (experimental)
In:
Out (tracked as separate follow-up issues):
returnImmediately=falsehonoured)Tasks
Profile.Feature.SSF(experimental, off by default)ssf.transmitterEnabledtoggle; per-clientssf.enabledtogglecaep.devkeycloak_ssf_*Documentation
A more detailed description for the design Design + behaviour can be found here: (Design Document)
Fixes #48254
Signed-off-by: Thomas Darimont thomas.darimont@googlemail.com
This PR was partially co-authored with Claude AI