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ść |
|---|---|
| Realm | dataflow (nazwa wyświetlana „DataFlow AI") |
| Eksport realmu | backend/docker/keycloak/realm-export.json |
| Wymóg SSL | sslRequired: external |
| Samodzielna rejestracja | registrationAllowed: false |
| Resetowanie hasła | resetPasswordAllowed: true |
| Edycja nazwy użytkownika | editUsernameAllowed: false |
| Logowanie adresem e-mail | loginWithEmailAllowed: true |
| Zduplikowane adresy e-mail | duplicateEmailsAllowed: 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.
| Ustawienie | Wartość |
|---|---|
bruteForceProtected | true |
failureFactor | 5 niepowodzeń przed zablokowaniem |
maxFailureWaitSeconds | 900 (15 minut) |
minimumQuickLoginWaitSeconds | 60 |
waitIncrementSeconds | 60 |
permanentLockout | false (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.
| Klient | Typ | Włączone przepływy | Przeznaczenie |
|---|---|---|---|
dataflow-app | publiczny SPA | Przepływ standardowy (authorization code) + bezpośrednie przyznania dostępu; implicit WYŁ.; konta usługowe WYŁ. | Frontendowa aplikacja webowa |
dataflow-api | poufny | Konta 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 pochodzeniehttps://etl.exai.cloud/*. - Pochodzenia webowe odzwierciedlają identyfikatory URI przekierowań plus token symbolu wieloznacznego
+. - Domyślne zakresy klienta to
web-origins,acr,profile,rolesorazemail. - Mapowania zakresów przyznają
dataflow-appwszystkie 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:
| Zmienna | Wartość domyślna |
|---|---|
VITE_KEYCLOAK_URL | http://localhost:8180 |
VITE_KEYCLOAK_REALM | dataflow |
VITE_KEYCLOAK_CLIENT_ID | dataflow-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
AuthProvider.tsxuruchamiakeycloakInstance.init(...)wewnątrzPromise.racechronionego 15-sekundowym limitem czasu (KEYCLOAK_INIT_TIMEOUT_MS— wcześniej 4 s, podniesiony, aby naprawić pętlę przekierowań SSO).- Po powodzeniu
buildProfileFromKeycloak()parsuje token doKeycloakUserProfile: subject, name, email,realm_access.rolesoraz claimworkspace_id/workspace. - Instancja Keycloak jest udostępniana jako
window.__keycloakna potrzeby diagnostyki. 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
| Kontrola | Szczegóły |
|---|---|
| Podpis | RS256, 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-Id | jwt.subject |
X-User-Email | claim email (rezerwowo <sub>@polkomtel.pl) |
X-User-Display-Name | name / preferred_username / lokalna część adresu e-mail |
X-User-Role | Wynik RBACService.mapKeycloakRolesToDataFlowRole(...) |
X-Workspace-Id | claim workspace_id (rezerwowo default) |
X-User-Groups | claim 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):
- Wywołanie zwrotne
afterCompletioninterceptora. SecurityContextCleanupFilteropakowujący każde żądanie/api/*wtry/finally.SecurityContextTaskDecoratordla 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ła | Wartość | Przeznaczenie |
|---|---|---|
TOKEN_REFRESH_BUFFER_SECONDS | 30 | Odśwież tyle sekund przed exp |
SESSION_TIMEOUT_WARNING_SECONDS | 60 | Pokaż modal limitu czasu tyle sekund przed wygaśnięciem |
MIN_TOKEN_VALIDITY_SECONDS | 60 | Minimalna ważność przekazywana do updateToken |
- Ciche odświeżanie —
AuthProvider.scheduleTokenRefreshwywołujekc.updateToken, aby odświeżyć token 30 s przed wygaśnięciem. Jeśli odświeżenie się nie powiedzie, sesja jest czyszczona, aisAuthenticatedstaje sięfalse. - Ostrzeżenie o limicie czasu sesji —
SessionTimeoutModalpojawia 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łujeupdateToken(-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=devlubVITE_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 zwracanull. AuthProviderprzechodzi w tryb deweloperski tylko wtedy, gdyallowDevModeFallbackjest 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
SecurityConfigniż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łówek | Wartość |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | DENY |
X-XSS-Protection | 0 |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | geolocation=(), camera=(), microphone=() |
Cache-Control | no-store (konfigurowalne) |
Strict-Transport-Security | max-age=31536000; includeSubDomains; preload (gdy includeHsts) |
Content-Security-Policy | default-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/ai | 30 |
migration | 100 |
| pipelines / connections / runs / lineage / monitor / domyślne | 200 |
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:
- Strukturalny JSON poprzez logger SLF4J
com.polkomtel.dataflow.audit— poziom wybierany na podstawie statusu (2xx →INFO, 4xx →WARN, w przeciwnym razieERROR) — na potrzeby przyjmowania przez SIEM. - Asynchroniczna trwałość w bazie danych poprzez opcjonalny bean
AuditPersistencena 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:
| Wzorzec | Przykładowa maska |
|---|---|
EMAIL | u***@e*****.com |
POLISH_PHONE | +48***456*** |
PESEL | 123****8901 |
CREDIT_CARD | 4532****9012 |
IBAN | PL12****3456 |
IP_ADDRESS | 192.168.*.* |
POLISH_POSTAL_CODE | 00-*** |
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
| Warstwa | Mechanizm |
|---|---|
| Dostawca tożsamości | Keycloak 24, realm dataflow, federacja AD, ochrona przed brute-force |
| Logowanie w przeglądarce | keycloak-js, klient publiczny dataflow-app, PKCE S256, check-sso |
| Walidacja na bramie | Serwer zasobów OAuth2 — podpis RS256, iss, exp, aud=dataflow-api |
| Propagacja tożsamości | Sześć nagłówków X-User-* wstrzykiwanych przez AuthFilter |
| Ponowna walidacja w usługach niższego poziomu | NimbusJwtDecoder JWKS + wątkowo-lokalny SecurityContext |
| Cykl życia tokenu | 15-minutowy token dostępu, ciche odświeżanie, ponowienie po 401 |
| Wzmocnienie | Nagłó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.