Bezpieczeństwo i RBAC

Uwierzytelnianie i SSO

Każde żądanie do platformy DataFlow AI jest uwierzytelniane względem jednego realmu Keycloak, zanim dotrze do jakiejkolwiek usługi. Ta strona przedstawia pełną ścieżkę tożsamości — logowanie w przeglądarce, walidację JWT na bramie, propagację nagłówków tożsamości, ponowną walidację w usługach niższego poziomu, cykl życia tokenu oraz wzmocnienia bezpieczeństwa stosowane do każdej odpowiedzi.


Dostawca tożsamości — Keycloak

Platforma deleguje całe uwierzytelnianie do Keycloak 24, działającego jako dostawca tożsamości OIDC / SSO. Jeden realm przechowuje wszystkich klientów, role, grupy i użytkowników.

WłaściwośćWartość
Realmdataflow (nazwa wyświetlana „DataFlow AI")
Eksport realmubackend/docker/keycloak/realm-export.json
Wymóg SSLsslRequired: external
Samodzielna rejestracjaregistrationAllowed: false
Resetowanie hasłaresetPasswordAllowed: true
Edycja nazwy użytkownikaeditUsernameAllowed: false
Logowanie adresem e-mailloginWithEmailAllowed: true
Zduplikowane adresy e-mailduplicateEmailsAllowed: false

Sam Keycloak działa w topologii compose na porcie hosta 8180 (mapowanym na port kontenera 8080) i przechowuje swoje dane w dedykowanej bazie dataflow_keycloak na współdzielonej instancji PostgreSQL.

Ochrona przed atakami brute-force

Realm dataflow włącza wbudowane wykrywanie ataków brute-force w Keycloak. Powtarzające się nieudane logowania stopniowo spowalniają, a następnie tymczasowo blokują konto.

UstawienieWartość
bruteForceProtectedtrue
failureFactor5 niepowodzeń przed zablokowaniem
maxFailureWaitSeconds900 (15 minut)
minimumQuickLoginWaitSeconds60
waitIncrementSeconds60
permanentLockoutfalse (tylko blokada tymczasowa)

Federacja Active Directory

W środowisku produkcyjnym Keycloak federuje użytkowników z korporacyjnego katalogu Polkomtel. Przewodnik administratora opisuje dostawcę federacji użytkowników LDAP wskazujący na ldaps://ad.polkomtel.internal:636 w trybie edycji READ_ONLY, z mapperami grup AD → ról-grup Keycloak oraz okresową synchronizacją zmienionych użytkowników. Użytkownicy uwierzytelniają się przy użyciu istniejących poświadczeń korporacyjnych; ich przynależność do grup AD określa rolę na platformie.

Czasy życia tokenów znajdują się w konsoli administracyjnej

Przewodnik administratora określa czas życia tokenu dostępu na 15 minut oraz bezczynność sesji SSO na 30 minut. Wartości te są konfigurowane w konsoli administracyjnej Keycloak — nie są zapisane w realm-export.json. Domyślny czas życia tokenu dostępu w samym Keycloak wynosi 5 minut, co komentarze w kodzie frontendu wyraźnie podkreślają.


Klienci OAuth2

Realm definiuje dwóch klientów OAuth2 z celowo różnymi profilami zaufania — jednego klienta publicznego dla SPA w przeglądarce oraz jednego klienta poufnego dla wywołań usługa-usługa w backendzie.

KlientTypWłączone przepływyPrzeznaczenie
dataflow-apppubliczny SPAPrzepływ standardowy (authorization code) + bezpośrednie przyznania dostępu; implicit WYŁ.; konta usługowe WYŁ.Frontendowa aplikacja webowa
dataflow-apipoufnyKonta usługowe WŁ.; przepływ standardowy WYŁ.; bezpośrednie przyznania dostępu WYŁ.; implicit WYŁ.Wywołania usługa-usługa w backendzie (client-credentials)

dataflow-app — publiczny klient SPA

Ponieważ aplikacja jednostronicowa nie może przechowywać sekretu, dataflow-app jest klientem publicznym, który polega na PKCE z metodą wyzwania S256 (pkce.code.challenge.method: S256), aby zabezpieczyć wymianę kodu autoryzacji. Jego konfiguracja:

  • Identyfikatory URI przekierowań obejmują lokalne porty deweloperskie (5173, 4173, 3000, 3006) oraz produkcyjne pochodzenie https://etl.exai.cloud/*.
  • Pochodzenia webowe odzwierciedlają identyfikatory URI przekierowań plus token symbolu wieloznacznego +.
  • Domyślne zakresy klienta to web-origins, acr, profile, roles oraz email.
  • Mapowania zakresów przyznają dataflow-app wszystkie sześć ról realmu.

dataflow-api — poufny klient usługowy

dataflow-api jest klientem poufnym używanym w przepływach client-credentials (usługa-usługa). Jego sekret jest dostarczany jako symbol zastępczy REPLACE_IN_KEYCLOAK w eksporcie realmu i musi zostać ustawiony w konsoli administracyjnej Keycloak. Jest to ta sama grupa odbiorców, której wymagają usługi niższego poziomu w claimie aud (zobacz walidacja JWT).

Rozbieżność dokumentacji dotycząca nazwy klienta

Przewodnik administratora §3.1 nazywa klienta SPA dataflow-ui. Rzeczywisty eksport realmu oraz frontend (domyślna wartość VITE_KEYCLOAK_CLIENT_ID) używają dataflow-app. Zawsze ufaj tutaj eksportowi realmu i konfiguracji frontendu, a nie przewodnikowi administratora.


Przepływ logowania OIDC

Frontend używa adaptera keycloak-js. Przy starcie konstruuje singletonową instancję Keycloak (frontend/src/auth/keycloak.ts) z trzech zmiennych środowiskowych:

ZmiennaWartość domyślna
VITE_KEYCLOAK_URLhttp://localhost:8180
VITE_KEYCLOAK_REALMdataflow
VITE_KEYCLOAK_CLIENT_IDdataflow-app

Inicjalizacja (getKeycloakInitOptions) używa onLoad: 'check-sso', cichego sprawdzenia SSO przez /silent-check-sso.html, pkceMethod: 'S256' oraz checkLoginIframe: false.

Diagram sekwencji

 Browser                Keycloak (:8180)            API Gateway (:8085)
   │                          │                            │
   │  (1) load SPA from nginx :3006                          │
   │                          │                            │
   │  (2) check-sso / login redirect (PKCE S256)            │
   │─────────────────────────▶│                            │
   │                          │                            │
   │  (3) user authenticates (corporate AD credentials)      │
   │◀─────────────────────────│                            │
   │                          │                            │
   │  (4) redirect back to origin "/" with authorization code│
   │◀─────────────────────────│                            │
   │                          │                            │
   │  (5) keycloak-js exchanges code + PKCE verifier         │
   │─────────────────────────▶│                            │
   │  (6) access token + refresh token (JWT, RS256)         │
   │◀─────────────────────────│                            │
   │                          │                            │
   │  (7) buildProfileFromKeycloak() parses token claims     │
   │  (8) signalAuthReady() — first API requests unblocked   │
   │                          │                            │
   │  (9) GET /api/v1/...  Authorization: Bearer <JWT>       │
   │────────────────────────────────────────────────────────▶│
   │                          │                            │
   │                  (10) gateway validates JWT, injects   │
   │                       X-User-* headers, proxies on     │
   │◀────────────────────────────────────────────────────────│

Co dzieje się w przeglądarce

  1. AuthProvider.tsx uruchamia keycloakInstance.init(...) wewnątrz Promise.race chronionego 15-sekundowym limitem czasu (KEYCLOAK_INIT_TIMEOUT_MS — wcześniej 4 s, podniesiony, aby naprawić pętlę przekierowań SSO).
  2. Po powodzeniu buildProfileFromKeycloak() parsuje token do KeycloakUserProfile: subject, name, email, realm_access.roles oraz claim workspace_id / workspace.
  3. Instancja Keycloak jest udostępniana jako window.__keycloak na potrzeby diagnostyki.
  4. login() domyślnie ustawia cel przekierowania po zalogowaniu na pochodzenie / — nie na bieżący adres URL — aby użytkownicy nie wracali do /login. logout() przekierowuje do /login.

Bramkowanie pierwszych żądań API

Klient axios (frontend/src/api/client.ts) uzyskuje token dostępu poprzez zarejestrowany akcesor tokenu (registerTokenAccessor). Obietnica signalAuthReady() / waitForAuthReady bramkuje pierwsze żądania aż do zakończenia inicjalizacji Keycloak, zapobiegając pierwszej fali nieuwierzytelnionych wywołań.


Walidacja JWT na bramie

Cały ruch klientów wchodzi przez API Gateway (reaktywny Spring Cloud Gateway, WebFlux) pod /api/v1/**. Brama jest skonfigurowana jako serwer zasobów OAuth2 i waliduje token bearer JWT przy każdym żądaniu.

Plik SecurityConfig.kt bramy wyłącza CSRF, HTTP Basic oraz logowanie formularzowe i podłącza ReactiveKeycloakJwtConverter, aby tłumaczyć role Keycloak na uprawnienia Spring.

Co jest sprawdzane

KontrolaSzczegóły
PodpisRS256, weryfikowany względem punktu końcowego JWKS Keycloak
Wydawca (iss)Musi być równy adresowi URL realmu Keycloak
Wygaśnięcie (exp)Wygasłe tokeny są odrzucane
Grupa odbiorców (aud)Musi zawierać dataflow-api (dataflow.security.jwt.audience, FA-004) — tokeny bez aud są odrzucane

Egzekwowanie grupy odbiorców jest wykonywane przez JwtAuthenticator. ReactiveKeycloakJwtConverter rejestruje każdy zaakceptowany JWT na poziomie INFO (principal, subject, wydawca, wygaśnięcie, role realmu, przyznane uprawnienia).

Ścieżki publiczne

Niewielki zestaw ścieżek całkowicie omija uwierzytelnianie (permitAll): OPTIONS /**, /actuator/**, /actuator/health/**, /api/v1/health oraz /health/**.


Propagacja nagłówków tożsamości

Po zwalidowaniu JWT filtr AuthFilter bramy — GlobalFilter zarejestrowany w kolejności −90 — wyodrębnia claimy tokenu i wstrzykuje sześć nagłówków tożsamości X-User-* do żądania przekazywanego do usług niższego poziomu.

NagłówekŹródło
X-User-Idjwt.subject
X-User-Emailclaim email (rezerwowo <sub>@polkomtel.pl)
X-User-Display-Namename / preferred_username / lokalna część adresu e-mail
X-User-RoleWynik RBACService.mapKeycloakRolesToDataFlowRole(...)
X-Workspace-Idclaim workspace_id (rezerwowo default)
X-User-Groupsclaim groups, połączony przecinkami

Ścieżki publiczne (/actuator, /api/v1/health, /health) omijają filtr. Żądania bez JWT (na przykład żądania tylko z ciasteczkiem lub nieuwierzytelnione) przechodzą bez nagłówków tożsamości i są rejestrowane tylko na poziomie DEBUG.

Usługi niższego poziomu ufają tym nagłówkom

Usługi niższego poziomu konsumują nagłówki X-User-*. W przypadku żądania uwierzytelnionego JWT brama nadpisuje wszystkie sześć nagłówków, więc fałszerstwa dostarczone przez klienta zostają zastąpione. Jednak żądanie przepuszczone bez uwierzytelnienia nie ma usuniętych przychodzących nagłówków X-User-*. Bezpieczeństwo zależy zatem od tego, by brama była jedynym punktem wejścia oraz od tego, by usługi niższego poziomu zawsze ponownie walidowały JWT — co też czynią.


Ponowna walidacja JWKS w usługach servlet

Sześć usług niższego poziomu to aplikacje Spring MVC (servlet). Nie ufają one ślepo bramie — niezależnie ponownie walidują ten sam JWT, co stanowi projekt z dogłębną obroną.

Plik common/security/SecurityConfig każdej usługi jest opatrzony adnotacjami @EnableWebSecurity oraz @EnableMethodSecurity(prePostEnabled = true), działa z bezstanowymi sesjami i wyłącza CSRF. Jako serwer zasobów OAuth2 dekoduje tokeny za pomocą:

NimbusJwtDecoder.withJwkSetUri(
    // spring.security.oauth2.resourceserver.jwt.jwk-set-uri
)

KeycloakJwtConverter mapuje role tokenu na uprawnienia, dzięki czemu @PreAuthorize("hasAnyRole(...)") ocenia rzeczywiste role Keycloak, a nie claimy SCOPE_*.

Po ponownej walidacji SecurityContextPopulatingInterceptor odczytuje nagłówki X-User-* bramy do wątkowo-lokalnego SecurityContextHolder. Klasa danych SecurityContext udostępnia pomocniki isAdmin, isEngineerOrAbove oraz isAnalystOrAbove. Sprzątanie wątkowo-lokalne jest gwarantowane na trzy sposoby (FA-005):

  1. Wywołanie zwrotne afterCompletion interceptora.
  2. SecurityContextCleanupFilter opakowujący każde żądanie /api/* w try/finally.
  3. SecurityContextTaskDecorator dla pul wątków @Async.

Cykl życia tokenu i odświeżanie

Token dostępu jest krótkotrwały i odświeżany cicho w tle, więc użytkownik nigdy nie jest przerywany koniecznością ponownego logowania.

StałaWartośćPrzeznaczenie
TOKEN_REFRESH_BUFFER_SECONDS30Odśwież tyle sekund przed exp
SESSION_TIMEOUT_WARNING_SECONDS60Pokaż modal limitu czasu tyle sekund przed wygaśnięciem
MIN_TOKEN_VALIDITY_SECONDS60Minimalna ważność przekazywana do updateToken
  • Ciche odświeżanieAuthProvider.scheduleTokenRefresh wywołuje kc.updateToken, aby odświeżyć token 30 s przed wygaśnięciem. Jeśli odświeżenie się nie powiedzie, sesja jest czyszczona, a isAuthenticated staje się false.
  • Ostrzeżenie o limicie czasu sesjiSessionTimeoutModal pojawia się 60 s przed wygaśnięciem (zmniejszono z 5 minut, co powodowało pojawianie się modalu natychmiast po zalogowaniu). Akcja „Przedłuż sesję" wywołuje updateToken(-1), aby wymusić odświeżenie.
  • Ponowienie po 401 — interceptor odpowiedzi axios implementuje przepływ 401 → ciche odświeżenie tokenu → ponowienie oryginalnego żądania, dzięki czemu token, który wygasł w trakcie obsługi, jest przezroczyście odnawiany.
  • Wylogowanie — czyści liczniki odświeżania i limitu czasu sesji, resetuje cały stan React i wywołuje keycloakInstance.logout.

Tryb awaryjny deweloperski

Na potrzeby lokalnego developmentu bez działającego Keycloak frontend dostarcza ścieżkę uwierzytelniania mock w frontend/src/auth/devAuth.ts.

  • Gdy VITE_AUTH_MODE=dev lub VITE_AUTH_ENABLED=false (tylko w serwerze deweloperskim Vite), przełącznik person oferuje cztery profile mock — engineer, analyst, admin, steward — które są nieodróżnialne od rzeczywistych profili Keycloak. Deweloperski akcesor tokenu zwraca null.
  • AuthProvider przechodzi w tryb deweloperski tylko wtedy, gdy allowDevModeFallback jest wyraźnie ustawione. W przeciwnym razie niepowodzenie inicjalizacji Keycloak pozostawia użytkownika nieuwierzytelnionym — nie ma żadnego niejawnego przyznania dostępu.

Furtka deweloperska bramy

Backend ma równoległy przełącznik: dataflow.gateway.dev-permit-reads (domyślnie false). Gdy true:

  • Brama dopuszcza wszystkie żądania GET plus kilka zawsze bezpiecznych POST-ów (copilot, ai, catalog/ask, search) bez JWT.
  • Plik SecurityConfig niższego poziomu przyznaje anonimowemu użytkownikowi szeroki zestaw ról.

W produkcji dev-permit-reads musi pozostać false

dev-permit-reads=true dopuszcza nieuwierzytelnione odczyty i przyznaje anonimowemu użytkownikowi szeroki zestaw ról we wszystkich usługach. W produkcji musi być false. Plik compose domyślnie ustawia DATAFLOW_GATEWAY_DEV_PERMIT_READS na true na potrzeby lokalnego developmentu — wdrożenie, które zapomni to nadpisać, ujawniłoby nieuwierzytelnione odczyty.


Wzmocnienie bezpieczeństwa

Każda odpowiedź opuszczająca bramę jest wzmacniana przez łańcuch filtrów per-trasa. Platforma opisuje tę postawę jako zgodną z SOC 2 / OWASP.

Nagłówki bezpieczeństwa

SecurityHeadersFilter działa w kolejności HIGHEST_PRECEDENCE + 10 i zapisuje nagłówki w hooku beforeCommit (aby pojawiły się przed zatwierdzeniem odpowiedzi).

NagłówekWartość
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
X-XSS-Protection0
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policygeolocation=(), camera=(), microphone=()
Cache-Controlno-store (konfigurowalne)
Strict-Transport-Securitymax-age=31536000; includeSubDomains; preload (gdy includeHsts)
Content-Security-Policydefault-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self' (gdy includeCsp)

TLS

Realm egzekwuje sslRequired: external. Brama emituje powyższy nagłówek preload HSTS; przewodnik administratora dodatkowo wymaga, aby Keycloak był serwowany przez HTTPS na porcie 443, a połączenia z bazą danych używały sslmode=verify-full.

CORS

CorsConfig rejestruje CorsWebFilter na /**. Dozwolone pochodzenia pochodzą z dataflow.cors.allowed-origins (domyślnie http://localhost:5173, http://localhost:3000, https://app.dataflow.polkomtel.pl, https://staging.dataflow.polkomtel.pl). Metody to GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD, allowCredentials ma wartość true, a maxAge domyślnie wynosi 3600 s. Ujawniane nagłówki obejmują nagłówki ograniczania liczby żądań, ETag oraz Content-Disposition.

Ograniczanie liczby żądań

RedisRateLimitFilter to limiter z przesuwnym oknem w Redis (zbiory posortowane) kluczowany rate_limit:{clientIp}:{endpoint}. Adres IP klienta jest rozwiązywany z X-Forwarded-For, następnie X-User-Id, następnie adresu zdalnego, a na końcu anonymous.

Grupa punktów końcowychŻądań na minutę
/api/v1/ai30
migration100
pipelines / connections / runs / lineage / monitor / domyślne200

Odpowiedzi zawierają X-RateLimit-Limit, X-RateLimit-Remaining oraz X-RateLimit-Reset; żądanie przekraczające limit zwraca HTTP 429 z Retry-After. Limiter jest domyślnie fail-closed (dataflow.gateway.rate-limit.fail-closed=true, FR-019): jeśli Redis jest niedostępny, brama zwraca HTTP 503 z X-RateLimit-Status: degraded, zamiast po cichu rezygnować z ochrony, a 30-sekundowy bezpiecznik opóźnia ponowienie prób z Redis. Ustawienie fail-closed=false to rezygnacja typu fail-open przeznaczona wyłącznie dla developmentu.

Logowanie audytu

AuditInterceptor przechwytuje POST, PUT, PATCH oraz DELETE na /api/** (pomijając health i actuator). Wykonuje podwójny zapis:

  1. Strukturalny JSON poprzez logger SLF4J com.polkomtel.dataflow.audit — poziom wybierany na podstawie statusu (2xx → INFO, 4xx → WARN, w przeciwnym razie ERROR) — na potrzeby przyjmowania przez SIEM.
  2. Asynchroniczna trwałość w bazie danych poprzez opcjonalny bean AuditPersistence na pojedynczym wątku daemon.

Każdy wpis rejestruje identyfikator zdarzenia, identyfikator żądania, akcję (CREATE / UPDATE / PARTIAL_UPDATE / DELETE), metodę, ścieżkę, typ i identyfikator zasobu, kod statusu, czas trwania, aktora (identyfikator użytkownika / e-mail / rola / workspace z nagłówków X-User-*), adres zdalny, flagę powodzenia, kategorię oraz ewentualny typ i komunikat błędu. Logi audytu są przechowywane przez 365 dni i można je eksportować do SIEM; dostęp odczytu w AuditLogController wymaga hasRole('ADMIN').

Maskowanie PII

PiiMaskingFilter (filtr Spring Cloud Gateway w kolejności LOWEST_PRECEDENCE - 10) jest jedynym filtrem przechwytującym ciała odpowiedzi i jest stosowany do każdej trasy. Rozpoznaje siedem wzorców PII:

WzorzecPrzykładowa maska
EMAILu***@e*****.com
POLISH_PHONE+48***456***
PESEL123****8901
CREDIT_CARD4532****9012
IBANPL12****3456
IP_ADDRESS192.168.*.*
POLISH_POSTAL_CODE00-***

Maskowanie dotyczy tylko odpowiedzi błędów

Po refaktoryzacji z 2026-05-20 MaskingResponseDecorator przepuszcza udane odpowiedzi (status < 400) dosłownie i bez maskowania — tylko odpowiedzi błędów (status ≥ 400) z maskowalnym typem zawartości są buforowane i czyszczone. Zmiana naprawiła zawieszenie z obcięciem strumienia, gdzie DataBufferUtils.join gubił kończący fragment. Podana uzasadnienie jest takie, że udane ładunki JSON przechodzą przez wersjonowane DTO i nie zawierają dowolnych PII. Surowe PII w ciele 2xx nie zostałoby zatem zamaskowane na bramie.


Podsumowanie

WarstwaMechanizm
Dostawca tożsamościKeycloak 24, realm dataflow, federacja AD, ochrona przed brute-force
Logowanie w przeglądarcekeycloak-js, klient publiczny dataflow-app, PKCE S256, check-sso
Walidacja na bramieSerwer zasobów OAuth2 — podpis RS256, iss, exp, aud=dataflow-api
Propagacja tożsamościSześć nagłówków X-User-* wstrzykiwanych przez AuthFilter
Ponowna walidacja w usługach niższego poziomuNimbusJwtDecoder JWKS + wątkowo-lokalny SecurityContext
Cykl życia tokenu15-minutowy token dostępu, ciche odświeżanie, ponowienie po 401
WzmocnienieNagłówki bezpieczeństwa, HSTS/CSP, TLS, CORS, ograniczanie liczby żądań fail-closed, log audytu, maskowanie PII

Autoryzacja — co uwierzytelniona tożsamość może faktycznie robić — jest omówiona osobno w RBAC i uprawnienia.

Poprzednia
Przewodnik administratora