feat(auth): Phase 2 — audited-users UserPicker + Tier-3 Keycloak Admin lookup

This commit is contained in:
Александр Зимин
2026-05-26 12:01:09 +00:00
parent f1fcabc9da
commit a4ba8735d0
8 changed files with 142 additions and 122 deletions
@@ -61,4 +61,29 @@ public interface AuditLogRepository extends JpaRepository<AuditLog, Long> {
@Param("from") OffsetDateTime from,
@Param("to") OffsetDateTime to,
Pageable pageable);
/**
* Distinct user_id значения из всего audit log. Используется
* {@code UserDisplayService.listAuditedUsers} для построения
* UserPicker'а в /audit (фильтрация по «тем, кто реально появлялся
* в audit», а не по всем юзерам realm).
*
* <p>Сортировка по самому свежему {@code event_time} — недавно
* активные юзеры сверху picker'а.
*
* <p>Лимит передаём через {@link Pageable} в controller'е — JPQL
* не умеет {@code LIMIT} в обычном SELECT-выражении.
*/
@Query("""
SELECT a.userId
FROM AuditLog a
WHERE a.userId IS NOT NULL
GROUP BY a.userId
ORDER BY MAX(a.eventTime) DESC
""")
List<String> findDistinctUserIds(Pageable pageable);
default List<String> findDistinctUserIds(int limit) {
return findDistinctUserIds(org.springframework.data.domain.PageRequest.of(0, Math.max(1, limit)));
}
}
@@ -17,13 +17,15 @@ import java.time.OffsetDateTime;
*
* <p>Source semantics:
* <ul>
* <li>{@link Source#JWT_CAPTURE} — written by JwtUserCaptureFilter
* on every authenticated request. Highest accuracy (claims as
* the user actually saw them).</li>
* <li>{@link Source#KEYCLOAK_SYNC} — written by the periodic
* Phase 3 scheduled bulk sync from Keycloak Admin API.</li>
* <li>{@link Source#KEYCLOAK_ON_DEMAND} — written when a cache miss
* triggers a one-shot Keycloak Admin lookup. Phase 2.</li>
* <li>{@link Source#KEYCLOAK_SYNC} — written by the scheduled bulk
* refresh job from Keycloak Admin API ({@code UserDisplayCacheRefreshJob},
* default every 6h). Primary populator after Phase 2.</li>
* <li>{@link Source#KEYCLOAK_ON_DEMAND} — written when a cache miss on
* {@code find(sub)} triggers a one-shot Keycloak Admin lookup.</li>
* <li>{@link Source#JWT_CAPTURE} — legacy. Previously written by
* {@code JwtUserCaptureFilter} on every authenticated request;
* filter removed in Phase 2. Enum value retained for historical
* rows in existing prod databases.</li>
* </ul>
*/
@Entity
@@ -45,7 +47,7 @@ public class UserDisplayCacheEntry {
@Enumerated(EnumType.STRING)
@Column(name = "source", nullable = false, length = 32)
private Source source = Source.JWT_CAPTURE;
private Source source = Source.KEYCLOAK_SYNC;
@Column(name = "synced_at", nullable = false)
private OffsetDateTime syncedAt;