From d3d7e540173b44239570bba6bbdad6557f3ba42d Mon Sep 17 00:00:00 2001 From: Bastiaan Date: Tue, 11 Jun 2024 17:34:14 +0200 Subject: [PATCH 01/68] Run tests on feature branches --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d0b1196a..93eb74e84 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,7 @@ name: Main # Run in master and dev branches and in all pull requests to those branches on: push: - branches: [ master, dev ] + branches: [ master, dev, feature/* ] pull_request: {} env: From ad6c73e23c250abf777065f05b1eff7b6fe2845c Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 5 Aug 2024 23:18:26 +0100 Subject: [PATCH 02/68] Update front end to use auth code login --- src/main/webapp/app/home/home.component.ts | 52 +++++++------------ .../app/shared/auth/auth-oauth2.service.ts | 48 +++++++---------- .../app/shared/auth/principal.service.ts | 1 - .../app/shared/login/login.component.ts | 28 +--------- .../webapp/app/shared/login/login.service.ts | 32 ++++++------ 5 files changed, 55 insertions(+), 106 deletions(-) diff --git a/src/main/webapp/app/home/home.component.ts b/src/main/webapp/app/home/home.component.ts index d0e01f247..13c1a9734 100644 --- a/src/main/webapp/app/home/home.component.ts +++ b/src/main/webapp/app/home/home.component.ts @@ -1,17 +1,15 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute, Router } from '@angular/router'; +import { first } from 'rxjs/operators'; import { - LoginModalService, ProjectService, Principal, Project, OrganizationService, + LoginService, } from '../shared'; -import {Observable, of, Subscription} from "rxjs"; -import { EventManager } from "../shared/util/event-manager.service"; -import { switchMap } from "rxjs/operators"; -import {SessionService} from "../shared/session/session.service"; -import {environment} from "../../environments/environment"; +import { Subscription } from "rxjs"; @Component({ selector: 'jhi-home', @@ -29,41 +27,31 @@ export class HomeComponent { private loginUrl = 'api/redirect/login'; constructor( - public principal: Principal, - private loginModalService: LoginModalService, - public projectService: ProjectService, - public organizationService: OrganizationService, + public principal: Principal, + public projectService: ProjectService, + public organizationService: OrganizationService, + private route: ActivatedRoute, + private loginService: LoginService, ) { this.subscriptions = new Subscription(); } - // ngOnInit() { - // this.loadRelevantProjects(); - // } - // - // ngOnDestroy() { - // this.subscriptions.unsubscribe(); - // } - // - // private loadRelevantProjects() { - // this.subscriptions.add(this.principal.account$ - // .pipe( - // switchMap(account => { - // if (account) { - // return this.userService.findProject(account.login); - // } else { - // return of([]); - // } - // }) - // ) - // .subscribe(projects => this.projects = projects)); - // } + ngOnInit() { + this.subscriptions.add(this.route.queryParams.subscribe((params) => { + const token = params['access_token']; + if (token) this.loginService.login(token).pipe(first()).toPromise() + })); + } + + ngOnDestroy() { + this.subscriptions.unsubscribe(); + } trackId(index: number, item: Project) { return item.projectName; } login() { - window.location.href = this.loginUrl + window.location.href = this.loginUrl } } diff --git a/src/main/webapp/app/shared/auth/auth-oauth2.service.ts b/src/main/webapp/app/shared/auth/auth-oauth2.service.ts index 96f3d0e4f..d8b8e9d50 100644 --- a/src/main/webapp/app/shared/auth/auth-oauth2.service.ts +++ b/src/main/webapp/app/shared/auth/auth-oauth2.service.ts @@ -2,48 +2,36 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { map, switchMap } from "rxjs/operators"; -import {SessionService} from "../session/session.service"; -import {environment} from "../../../environments/environment"; +import { map } from 'rxjs/operators'; +import { SessionService } from '../session/session.service'; @Injectable({ providedIn: 'root' }) export class AuthServerProvider { - logoutUrl; constructor( - private http: HttpClient, - private sessionService: SessionService, + private http: HttpClient, + private sessionService: SessionService ) { - sessionService.logoutUrl$.subscribe( - url => this.logoutUrl = url - ) + sessionService.logoutUrl$.subscribe((url) => (this.logoutUrl = url)); } - login(credentials): Observable { - const body = new HttpParams() - .append('client_id', 'ManagementPortalapp') - .append('username', credentials.username) - .append('password', credentials.password) - .append('grant_type', 'password'); - const headers = new HttpHeaders() - .append('Content-Type', 'application/x-www-form-urlencoded') - .append('Accept', 'application/json'); - - return this.http.post('oauth/token', body, {headers, observe: 'body'}, ) - .pipe( - switchMap((tokenData: TokenData) => { - const authHeaders = new HttpHeaders() - .append('Authorization', 'Bearer ' + tokenData.access_token); - return this.http.post('api/login', null, { - headers: authHeaders, observe: 'body', withCredentials: true - }); - }), - ); + login(accessToken: string): Observable { + const authHeaders = new HttpHeaders().append( + 'Authorization', + 'Bearer ' + accessToken, + ); + return this.http.post('api/login', null, { + headers: authHeaders, + observe: 'body', + withCredentials: true, + }) } logout() { - window.location.href = this.logoutUrl + `&return_to=${window.location.href}`; + return this.http + .post('api/logout', { observe: 'body' }) + .pipe(map(() => {})); } } diff --git a/src/main/webapp/app/shared/auth/principal.service.ts b/src/main/webapp/app/shared/auth/principal.service.ts index 25d2666fb..4b6d379f2 100644 --- a/src/main/webapp/app/shared/auth/principal.service.ts +++ b/src/main/webapp/app/shared/auth/principal.service.ts @@ -17,7 +17,6 @@ export class Principal { // do not emit multiple duplicate values distinctUntilChanged((a, b) => a === b), ); - this.reset().subscribe(); } /** diff --git a/src/main/webapp/app/shared/login/login.component.ts b/src/main/webapp/app/shared/login/login.component.ts index d91a3a292..26985ec9d 100644 --- a/src/main/webapp/app/shared/login/login.component.ts +++ b/src/main/webapp/app/shared/login/login.component.ts @@ -45,33 +45,7 @@ export class JhiLoginModalComponent implements AfterViewInit { this.activeModal.dismiss('cancel'); } - login() { - this.loginService.login({ - username: this.username, - password: this.password, - rememberMe: this.rememberMe, - }).pipe(first()).toPromise().then(() => { - this.authenticationError = false; - this.activeModal.dismiss('login success'); - if (this.router.url === '/register' || (/activate/.test(this.router.url)) || - this.router.url === '/finishReset' || this.router.url === '/requestReset') { - return this.router.navigate(['']); - } - - this.eventManager.broadcast({ - name: 'authenticationSuccess', - content: 'Sending Authentication Success', - }); - - return this.authService.redirectBeforeUnauthenticated(); - }).catch(() => { - this.authenticationError = true; - }).then((isRedirected) => { - if (!isRedirected) { - return this.router.navigate(['/']); - } - }); - } + login() {} register() { this.activeModal.dismiss('to state register'); diff --git a/src/main/webapp/app/shared/login/login.service.ts b/src/main/webapp/app/shared/login/login.service.ts index e833459e5..7bd2e6682 100644 --- a/src/main/webapp/app/shared/login/login.service.ts +++ b/src/main/webapp/app/shared/login/login.service.ts @@ -16,24 +16,24 @@ export class LoginService { ) { } - login(credentials): Observable { - return this.authServerProvider.login(credentials).pipe( - tap( - (account) => { - this.principal.authenticate(account); - // After the login the language will be changed to - // the language selected by the user during his registration - if (account && account.langKey) { - this.translateService.use(account.langKey); - } - }, - () => this.logout() - ), - ); - } + login(accessToken: string): Observable { + return this.authServerProvider.login(accessToken).pipe( + tap( + (account) => { + this.principal.authenticate(account); + // After the login the language will be changed to + // the language selected by the user during his registration + if (account && account.langKey) { + this.translateService.use(account.langKey); + } + }, + () => this.logout() + ), + ); + } logout() { - this.authServerProvider.logout(); + this.authServerProvider.logout().subscribe(); this.principal.authenticate(null); } } From 8c9fec3da23c13e19c601762487acb4db53e7427 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 6 Aug 2024 00:12:39 +0100 Subject: [PATCH 03/68] Add login endpoint to allow auth code grant login --- .../management/web/rest/LoginEndpoint.kt | 88 +++++++++++++++++-- 1 file changed, 82 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index 12017d8ad..8e2b19d29 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -1,36 +1,112 @@ package org.radarbase.management.web.rest +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonPrimitive +import org.radarbase.auth.exception.IdpException import org.radarbase.management.config.ManagementPortalProperties import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.servlet.view.RedirectView +import java.time.Duration +import java.time.Instant @RestController @RequestMapping("/api") class LoginEndpoint @Autowired constructor( - @Autowired private val managementPortalProperties: ManagementPortalProperties, + private val managementPortalProperties: ManagementPortalProperties, ) { + private val httpClient = + HttpClient(CIO) { + install(HttpTimeout) { + connectTimeoutMillis = Duration.ofSeconds(10).toMillis() + socketTimeoutMillis = Duration.ofSeconds(10).toMillis() + requestTimeoutMillis = Duration.ofSeconds(300).toMillis() + } + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + } + @GetMapping("/redirect/login") - fun loginRedirect(): RedirectView { + suspend fun loginRedirect( + @RequestParam(required = false) code: String?, + ): RedirectView { val redirectView = RedirectView() - redirectView.url = managementPortalProperties.identityServer.loginUrl + - "/login?return_to=" + managementPortalProperties.common.managementPortalBaseUrl + val config = managementPortalProperties + val mpUrl = config.common.baseUrl + + if (code == null) { + redirectView.url = buildAuthUrl(config, mpUrl) + } else { + val accessToken = fetchAccessToken(code, config) + redirectView.url = "$mpUrl/#/?access_token=$accessToken" + } return redirectView } @GetMapping("/redirect/account") fun settingsRedirect(): RedirectView { val redirectView = RedirectView() - redirectView.url = managementPortalProperties.identityServer.loginUrl + "/settings" + redirectView.url = "${managementPortalProperties.identityServer.loginUrl}/settings" return redirectView } + private fun buildAuthUrl(config: ManagementPortalProperties, mpUrl: String): String { + return "${config.authServer.serverUrl}/oauth2/auth?" + + "client_id=${config.frontend.clientId}&" + + "response_type=code&" + + "state=${Instant.now()}&" + + "audience=res_ManagementPortal&" + + "scope=offline&" + + "redirect_uri=$mpUrl/api/redirect/login" + } + + private suspend fun fetchAccessToken( + code: String, + config: ManagementPortalProperties, + ): String { + val tokenUrl = "${config.authServer.serverUrl}/oauth2/token" + val response = + httpClient.post(tokenUrl) { + contentType(ContentType.Application.FormUrlEncoded) + accept(ContentType.Application.Json) + setBody( + Parameters + .build { + append("grant_type", "authorization_code") + append("code", code) + append("redirect_uri", "${config.common.baseUrl}/api/redirect/login") + append("client_id", config.frontend.clientId) + }.formUrlEncode(), + ) + } + + if (response.status.isSuccess()) { + val responseMap = response.body>() + return responseMap["access_token"]?.jsonPrimitive?.content + ?: throw IdpException("Access token not found in response") + } else { + throw IdpException("Unable to get access token") + } + } + companion object { - private val logger = LoggerFactory.getLogger(TokenKeyEndpoint::class.java) + private val logger = LoggerFactory.getLogger(LoginEndpoint::class.java) } } From c09291cbde7dd9a7542aa1f57cb326e7a1e23c4b Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 6 Aug 2024 00:13:06 +0100 Subject: [PATCH 04/68] Add token validator updates to support hydra tokens --- .../config/ManagementPortalProperties.java | 28 +++++++++++++++++++ .../config/OAuth2ServerConfiguration.kt | 1 + ...ManagementPortalJwtAccessTokenConverter.kt | 6 +++- .../ManagementPortalOauthKeyStoreHandler.kt | 9 +++++- 4 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java index 520bd1d69..8704edb96 100644 --- a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java +++ b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java @@ -12,6 +12,8 @@ public class ManagementPortalProperties { private final IdentityServer identityServer = new IdentityServer(); + private final AuthServer authServer = new AuthServer(); + private final Mail mail = new Mail(); private final Frontend frontend = new Frontend(); @@ -34,6 +36,10 @@ public IdentityServer getIdentityServer() { return identityServer; } + public AuthServer getAuthServer() { + return authServer; + } + public ManagementPortalProperties.Mail getMail() { return mail; } @@ -324,6 +330,28 @@ public void setLoginUrl(String loginUrl) { } } + public class AuthServer { + private String serverUrl = null; + private String serverAdminUrl = null; + + public String getServerUrl() { + return serverUrl; + } + + public void setServerUrl(String serverUrl) { + this.serverUrl = serverUrl; + } + + public String getServerAdminUrl() { + return serverAdminUrl; + } + + public void setServerAdminUrl(String serverAdminUrl) { + this.serverAdminUrl = serverAdminUrl; + } + } + + public static class CatalogueServer { private boolean enableAutoImport = false; diff --git a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt index 2586beaf3..e5a7a22bf 100644 --- a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt @@ -242,6 +242,7 @@ class OAuth2ServerConfiguration( fun accessTokenConverter(): ManagementPortalJwtAccessTokenConverter { logger.debug("loading token converter from keystore configurations") return ManagementPortalJwtAccessTokenConverter( + keyStoreHandler.tokenValidator, keyStoreHandler.algorithmForSigning, keyStoreHandler.verifiers, keyStoreHandler.refreshTokenVerifiers diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt index a57826d0a..06bd7aa93 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt @@ -8,6 +8,7 @@ import com.auth0.jwt.exceptions.SignatureVerificationException import com.fasterxml.jackson.core.JsonProcessingException import com.fasterxml.jackson.databind.ObjectMapper import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter +import org.radarbase.auth.authentication.TokenValidator import org.slf4j.LoggerFactory import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken @@ -33,6 +34,7 @@ import java.util.stream.Stream * are significantly smaller than RSA signatures. */ open class ManagementPortalJwtAccessTokenConverter( + validator: TokenValidator, algorithm: Algorithm, verifiers: MutableList, private val refreshTokenVerifiers: List @@ -59,6 +61,7 @@ open class ManagementPortalJwtAccessTokenConverter( field = jwtClaimsSetVerifier } private var algorithm: Algorithm? = null + private var validator: TokenValidator private val verifiers: MutableList /** @@ -72,6 +75,7 @@ open class ManagementPortalJwtAccessTokenConverter( accessToken.setIncludeGrantType(true) tokenConverter = accessToken this.verifiers = verifiers + this.validator = validator setAlgorithm(algorithm) } @@ -229,7 +233,7 @@ open class ManagementPortalJwtAccessTokenConverter( } for (verifier in verifierToUse) { try { - verifier.verify(token) + validator.validateBlocking(token) return claims } catch (sve: SignatureVerificationException) { logger.warn("Client presented a token with an incorrect signature") diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt index 752a295f1..a58026e35 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt @@ -52,6 +52,7 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( private val oauthConfig: Oauth private val verifierPublicKeyAliasList: List private val managementPortalBaseUrl: String + private val authServerUrl: String val verifiers: MutableList val refreshTokenVerifiers: MutableList @@ -79,6 +80,8 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( // No need to check audience with a refresh token: it can be used // to refresh tokens intended for other resources. refreshTokenVerifiers = algorithms.map { algo: Algorithm -> JWT.require(algo).build() }.toMutableList() + authServerUrl = managementPortalProperties.authServer.serverAdminUrl + tokenValidator.refresh() } @Nonnull @@ -228,7 +231,11 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( RES_MANAGEMENT_PORTAL, JwkAlgorithmParser() ), - KratosTokenVerifierLoader(managementPortalProperties.identityServer.publicUrl(), requireAal2 = managementPortalProperties.oauth.requireAal2), + JwksTokenVerifierLoader( + authServerUrl + "/admin/keys/hydra.jwt.access-token", + RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ), ) return TokenValidator(loaderList) } From 4820edc7ebf0dbcf20596dbdf3ec01f052382643 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 6 Aug 2024 11:00:36 +0100 Subject: [PATCH 05/68] Add auth server config to application properties --- src/main/resources/config/application-dev.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 87e51ec0d..b14bc7901 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -116,9 +116,14 @@ managementportal: # The line below can be uncommented to add some hidden fields for UI testing #hiddenSubjectFields: [person_name, date_of_birth, group] identityServer: + adminEmail: admin-email-here@gmail.com serverUrl: http://localhost:4433 - serverAdminUrl: http://localhost:4434 + serverAdminUrl: http://kratos-admin loginUrl: http://localhost:3000 + authServer: + serverUrl: http://localhost:4444 + serverAdminUrl: http://localhost:4445 + # =================================================================== # JHipster specific properties From 6beedc45c56bf27086a549811a80a87d1d6da1c8 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 6 Aug 2024 16:27:54 +0100 Subject: [PATCH 06/68] Fix access token converter --- ...ManagementPortalJwtAccessTokenConverter.kt | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt index 06bd7aa93..88962f0a2 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt @@ -25,6 +25,7 @@ import java.nio.charset.StandardCharsets import java.time.Instant import java.util.* import java.util.stream.Stream +import org.radarbase.auth.exception.TokenValidationException /** * Implementation of [JwtAccessTokenConverter] for the RADAR-base ManagementPortal platform. @@ -231,17 +232,13 @@ open class ManagementPortalJwtAccessTokenConverter( } catch (ex: JsonProcessingException) { throw InvalidTokenException("Invalid token", ex) } - for (verifier in verifierToUse) { - try { - validator.validateBlocking(token) - return claims - } catch (sve: SignatureVerificationException) { - logger.warn("Client presented a token with an incorrect signature") - } catch (ex: JWTVerificationException) { - logger.debug( - "Verifier {} with implementation {} did not accept token: {}", - verifier, verifier.javaClass, ex.message - ) + try { + validator.validateBlocking(token) + Companion.logger.debug("Using token from header") + return claims + } catch (ex: TokenValidationException) { + ex.message?.let { + Companion.logger.info("Failed to validate token from header: {}", it) } } throw InvalidTokenException("No registered validator could authenticate this token") From 4bbb4e88d3b1e0b312abcccfd74fba93dc7115a2 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 13 Aug 2024 19:15:49 +0100 Subject: [PATCH 07/68] Add Ory components docker configurations --- src/main/docker/etc/config/kratos/kratos.yml | 46 +++++------- .../etc/config/kratos/webhook_body.jsonnet | 5 ++ .../non_managementportal/docker-compose.yml | 27 ++++++- src/main/docker/ory_stack.yml | 70 ++++++++++++++++--- 4 files changed, 105 insertions(+), 43 deletions(-) create mode 100644 src/main/docker/etc/config/kratos/webhook_body.jsonnet diff --git a/src/main/docker/etc/config/kratos/kratos.yml b/src/main/docker/etc/config/kratos/kratos.yml index 99a36d1f1..d194bcfc3 100644 --- a/src/main/docker/etc/config/kratos/kratos.yml +++ b/src/main/docker/etc/config/kratos/kratos.yml @@ -2,12 +2,12 @@ dsn: memory serve: public: - base_url: http://127.0.0.1:4433/ + base_url: http://localhost:4433/ admin: base_url: http://kratos:4434/ selfservice: - default_browser_return_url: http://127.0.0.1:3000/ + default_browser_return_url: http://localhost:3000/ allowed_return_urls: - "http://127.0.0.1:3000/" - "http://127.0.0.1:8080/" @@ -19,23 +19,6 @@ selfservice: methods: password: enabled: true - # oidc: - # config: - # providers: - # # social sign-in for google. This needs to be tied to a google account. values below were added by bastiaan - # - id: google_d292689d # this is `` in the Authorization callback URL. DO NOT CHANGE IT ONCE SET! current google callback: http://127.0.0.1:4433/self-service/methods/oidc/callback/google_d292689d - # provider: google - # client_id: 922854293804-r3fhl9tom6uutcq5c8fm4592l1t6s3mh.apps.googleusercontent.com # Replace this with the Client ID - # client_secret: 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 # Replace this with the Client secret - # issuer_url: https://accounts.google.com # Replace this with the providers issuer URL - # mapper_url: "base64://bG9jYWwgY2xhaW1zID0gewogIGVtYWlsX3ZlcmlmaWVkOiBmYWxzZSwKfSArIHN0ZC5leHRWYXIoJ2NsYWltcycpOwoKewogIGlkZW50aXR5OiB7CiAgICB0cmFpdHM6IHsKICAgICAgW2lmICdlbWFpbCcgaW4gY2xhaW1zICYmIGNsYWltcy5lbWFpbF92ZXJpZmllZCB0aGVuICdlbWFpbCcgZWxzZSBudWxsXTogY2xhaW1zLmVtYWlsLAogICAgfSwKICB9LAp9" - # # currently: GitHub example from: https://www.ory.sh/docs/kratos/social-signin/data-mapping - # # Alternatively, use an URL: - # # mapper_url: https://storage.googleapis.com/abc-cde-prd/9cac9717f007808bf17 - # scope: - # - email - # # supported scopes can be found in your providers dev docs - # enabled: true totp: config: issuer: Kratos @@ -45,34 +28,34 @@ selfservice: flows: error: - ui_url: http://127.0.0.1:3000/error + ui_url: http://localhost:3000/error settings: - ui_url: http://127.0.0.1:3000/settings - + ui_url: http://localhost:3000/settings + recovery: enabled: true - ui_url: http://127.0.0.1:3000/recovery - use: link + ui_url: http://localhost:3000/recovery + use: code verification: # our current flow necessitates that users reset their password after they activate an account in managementportal, # this works as verification - ui_url: http://127.0.0.1:3000/verification + ui_url: http://localhost:3000/verification enabled: true - use: link + use: code after: - default_browser_return_url: http://127.0.0.1:3000 + default_browser_return_url: http://localhost:3000/consent logout: after: - default_browser_return_url: http://127.0.0.1:3000/login + default_browser_return_url: http://localhost:3000/login login: - ui_url: http://127.0.0.1:3000/login + ui_url: http://localhost:3000/login registration: - ui_url: http://127.0.0.1:3000/registration + ui_url: http://localhost:3000/registration after: password: hooks: @@ -104,3 +87,6 @@ courier: smtp: connection_uri: smtps://test:test@mailslurper:1025/?skip_ssl_verify=true&disable_starttls=true from_address: noreply@radar-base.org + +oauth2_provider: + url: http://hydra:4445 \ No newline at end of file diff --git a/src/main/docker/etc/config/kratos/webhook_body.jsonnet b/src/main/docker/etc/config/kratos/webhook_body.jsonnet new file mode 100644 index 000000000..3af9998b2 --- /dev/null +++ b/src/main/docker/etc/config/kratos/webhook_body.jsonnet @@ -0,0 +1,5 @@ +function(ctx) { + identity: if std.objectHas(ctx, "identity") then ctx.identity else null, + payload: if std.objectHas(ctx, "flow") && std.objectHas(ctx.flow, "transient_payload") then ctx.flow.transient_payload else null, + cookies: ctx.request_cookies +} diff --git a/src/main/docker/non_managementportal/docker-compose.yml b/src/main/docker/non_managementportal/docker-compose.yml index 3497d6c90..73f2c158d 100644 --- a/src/main/docker/non_managementportal/docker-compose.yml +++ b/src/main/docker/non_managementportal/docker-compose.yml @@ -22,10 +22,10 @@ services: - db - default - kratos-selfservice-ui-node: + radar-self-enrolment-ui: extends: file: ../ory_stack.yml - service: kratos-selfservice-ui-node + service: radar-self-enrolment-ui networks: - ory - default @@ -60,3 +60,26 @@ services: networks: - ory - default + + postgresd-hydra: + extends: + file: ../ory_stack.yml + service: postgresd-hydra + networks: + - ory + + hydra-migrate: + extends: + file: ../ory_stack.yml + service: hydra-migrate + networks: + - ory + + hydra: + extends: + file: ../ory_stack.yml + service: hydra + networks: + - ory + - default + diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index b95835b53..ad254f117 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -1,20 +1,14 @@ version: '3.8' services: - kratos-selfservice-ui-node: + radar-self-enrolment-ui: image: - oryd/kratos-selfservice-ui-node + mpgxvii/radar-self-enrolment-ui:latest environment: - - LOG_LEAK_SENSITIVE_VALUES=true - - KRATOS_PUBLIC_URL=http://kratos:4433 - - KRATOS_ADMIN_URL=http://kratos:4434 - - SECURITY_MODE=standalone - - KRATOS_BROWSER_URL=http://127.0.0.1:4433 - - COOKIE_SECRET=unsafe_cookie_secret - - CSRF_COOKIE_NAME=radar - - CSRF_COOKIE_SECRET=unsafe_csrf_cookie_secret + - ORY_SDK_URL=http://kratos:4433/ + - HYDRA_ADMIN_URL=http://hydra:4445 ports: - - "3000:3000" + - "3000:4455" volumes: - /tmp/ui-node/logs:/root/.npm/_logs @@ -28,6 +22,16 @@ services: restart: unless-stopped environment: - DSN=postgres://kratos:secret@postgresd-kratos/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_HOOK=web_hook + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_METHOD=POST + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_BODY=file:///etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_RESPONSE_IGNORE=true + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_1_HOOK=session + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_HOOK=web_hook + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_METHOD=POST + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects/activate + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=file:///etc/config/kratos/webhook_body.jsonnet command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier volumes: - type: bind @@ -58,3 +62,47 @@ services: ports: - "4436:4436" - "4437:4437" + + hydra-migrate: + image: oryd/hydra:v2.2.0 + environment: + - DSN=postgres://hydra:secret@postgresd-hydra/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + command: migrate sql -e --yes + restart: on-failure + + hydra: + image: oryd/hydra:v2.2.0 + depends_on: + - hydra-migrate + ports: + - "4444:4444" # Public port + - "4445:4445" # Admin port + - "5555:5555" # Port for hydra token user + command: + serve all --dev + restart: on-failure # TODO figure out why we need this (incorporate health check into hydra migrate command?) + environment: + - DSN=postgres://hydra:secret@postgresd-hydra/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + - LOG_LEAK_SENSITIVE_VALUES=true + - URLS_SELF_ISSUER=http://localhost:4444 + - URLS_SELF_PUBLIC=http://localhost:4444 + - URLS_CONSENT=http://localhost:3000/consent + - URLS_LOGIN=http://localhost:3000/login + - URLS_LOGOUT=http://localhost:3000/logout + - URLS_IDENTITY_PROVIDER_PUBLICURL=http://localhost:4433 + - URLS_IDENTITY_PROVIDER_URL=http://localhost:4434 + - SECRETS_SYSTEM=youReallyNeedToChangeThis + - OIDC_SUBJECT_IDENTIFIERS_SUPPORTED_TYPES=public,pairwise + - OIDC_SUBJECT_IDENTIFIERS_PAIRWISE_SALT=youReallyNeedToChangeThis + - STRATEGIES_ACCESS_TOKEN=jwt + - SERVE_PUBLIC_CORS_ENABLED=true + - SERVE_ADMIN_CORS_ENABLED=true + - OAUTH2_ALLOWED_TOP_LEVEL_CLAIMS=scope,roles,authorities,sources,user_name + - OAUTH2_MIRROR_TOP_LEVEL_CLAIMS=false + + postgresd-hydra: + image: postgres:11.8 + environment: + - POSTGRES_USER=hydra + - POSTGRES_PASSWORD=secret + - POSTGRES_DB=hydra From d0d61aaa26b017d204bb8c2267b16bf26d4d4039 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 13 Aug 2024 20:41:22 +0100 Subject: [PATCH 08/68] Fix application properties --- src/main/resources/config/application-prod.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 5fa817e8b..83746c671 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -80,7 +80,7 @@ server: # =================================================================== managementportal: common: - baseUrl: http://my-server-url-to-change-here # Modify according to your server's URL + baseUrl: http://localhost:8080/managementportal # Modify according to your server's URL managementPortalBaseUrl: http://localhost:8080/managementportal privacyPolicyUrl: http://info.thehyve.nl/radar-cns-privacy-policy adminPassword: @@ -101,9 +101,12 @@ managementportal: enableAutoImport: false identityServer: adminEmail: bdegraaf1234@gmail.com - serverUrl: https://radar-k3s-test.thehyve.net/kratos + serverUrl: http://localhost:4433 serverAdminUrl: http://kratos-admin loginUrl: http://localhost:3000 + authServer: + serverUrl: http://localhost:4444 + serverAdminUrl: http://localhost:4445 # =================================================================== # JHipster specific properties From 715ffaa5a990ebdfbad6284a58a0ee87d9058074 Mon Sep 17 00:00:00 2001 From: yatharthranjan Date: Wed, 14 Aug 2024 11:51:30 +0100 Subject: [PATCH 09/68] use single postgres instance for kratos and hydra --- src/main/docker/etc/postgres/init-user-db.sh | 34 +++++++++++++++++++ .../non_managementportal/docker-compose.yml | 14 +++----- src/main/docker/ory_stack.yml | 27 +++++++-------- 3 files changed, 52 insertions(+), 23 deletions(-) create mode 100755 src/main/docker/etc/postgres/init-user-db.sh diff --git a/src/main/docker/etc/postgres/init-user-db.sh b/src/main/docker/etc/postgres/init-user-db.sh new file mode 100755 index 000000000..85a57e75f --- /dev/null +++ b/src/main/docker/etc/postgres/init-user-db.sh @@ -0,0 +1,34 @@ +#! /bin/bash + + set -e + set -u + export PGPASSWORD="$POSTGRES_PASSWORD" + export PGUSER="$POSTGRES_USER" + + function create_user_and_database() { + export PGPASSWORD="$POSTGRES_PASSWORD" + export PGUSER="$POSTGRES_USER" + local database=$1 + local database_exist=$(psql -U $PGUSER -tAc "SELECT 1 FROM pg_database WHERE datname='$database';") + if [[ "$database_exist" == 1 ]]; then + echo "Database $database already exists" + else + echo "Database $database does not exist" + echo " Creating database '$database' for user '$PGUSER'" + + psql -U $PGUSER -v ON_ERROR_STOP=1 <<-EOSQL + CREATE DATABASE "$database"; + GRANT ALL PRIVILEGES ON DATABASE $database TO $PGUSER; +EOSQL + fi + } + + if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then + echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" + #waiting for postgres + for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do + create_user_and_database $db + done + echo "Databases created" + fi + diff --git a/src/main/docker/non_managementportal/docker-compose.yml b/src/main/docker/non_managementportal/docker-compose.yml index 73f2c158d..68a2f0291 100644 --- a/src/main/docker/non_managementportal/docker-compose.yml +++ b/src/main/docker/non_managementportal/docker-compose.yml @@ -13,6 +13,9 @@ networks: driver: bridge internal: true +volumes: + pgdata: + services: managementportal-postgresql: extends: @@ -46,10 +49,10 @@ services: networks: - ory - postgresd-kratos: + postgresd-ory: extends: file: ../ory_stack.yml - service: postgresd-kratos + service: postgresd-ory networks: - ory @@ -61,13 +64,6 @@ services: - ory - default - postgresd-hydra: - extends: - file: ../ory_stack.yml - service: postgresd-hydra - networks: - - ory - hydra-migrate: extends: file: ../ory_stack.yml diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index ad254f117..96685ec5a 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -1,5 +1,8 @@ version: '3.8' +volumes: + pgdata: + services: radar-self-enrolment-ui: image: @@ -21,7 +24,7 @@ services: - "4434:4434" # admin, should be closed in production restart: unless-stopped environment: - - DSN=postgres://kratos:secret@postgresd-kratos/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://ory:secret@postgresd-ory/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_HOOK=web_hook - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_METHOD=POST - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects @@ -42,7 +45,7 @@ services: image: oryd/kratos:v1.0.0 environment: - - DSN=postgres://kratos:secret@postgresd-kratos/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://ory:secret@postgresd-ory/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 volumes: - type: bind source: ./etc/config/kratos @@ -50,12 +53,15 @@ services: command: -c /etc/config/kratos/kratos.yml migrate sql -e --yes restart: on-failure - postgresd-kratos: + postgresd-ory: image: postgres:11.8 environment: - - POSTGRES_USER=kratos + - POSTGRES_USER=ory - POSTGRES_PASSWORD=secret - - POSTGRES_DB=kratos + - POSTGRES_MULTIPLE_DATABASES=kratos,hydra + volumes: + - pgdata:/var/lib/postgresql/data + - ./etc/postgres/init-user-db.sh:/docker-entrypoint-initdb.d/init-user-db.sh mailslurper: image: oryd/mailslurper:latest-smtps @@ -66,7 +72,7 @@ services: hydra-migrate: image: oryd/hydra:v2.2.0 environment: - - DSN=postgres://hydra:secret@postgresd-hydra/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://ory:secret@postgresd-ory/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 command: migrate sql -e --yes restart: on-failure @@ -82,7 +88,7 @@ services: serve all --dev restart: on-failure # TODO figure out why we need this (incorporate health check into hydra migrate command?) environment: - - DSN=postgres://hydra:secret@postgresd-hydra/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://ory:secret@postgresd-ory/hydra?sslmode=disable&max_conns=20&max_idle_conns=4 - LOG_LEAK_SENSITIVE_VALUES=true - URLS_SELF_ISSUER=http://localhost:4444 - URLS_SELF_PUBLIC=http://localhost:4444 @@ -99,10 +105,3 @@ services: - SERVE_ADMIN_CORS_ENABLED=true - OAUTH2_ALLOWED_TOP_LEVEL_CLAIMS=scope,roles,authorities,sources,user_name - OAUTH2_MIRROR_TOP_LEVEL_CLAIMS=false - - postgresd-hydra: - image: postgres:11.8 - environment: - - POSTGRES_USER=hydra - - POSTGRES_PASSWORD=secret - - POSTGRES_DB=hydra From 7fa118c3ee758ae05b0e328d53df1aa87f96bc04 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 14 Aug 2024 11:53:48 +0100 Subject: [PATCH 10/68] Add MP authserve configs --- src/main/docker/managementportal.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/docker/managementportal.yml b/src/main/docker/managementportal.yml index 5487f5cb7..b19a7a337 100644 --- a/src/main/docker/managementportal.yml +++ b/src/main/docker/managementportal.yml @@ -12,8 +12,10 @@ services: - MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET=secret - MANAGEMENTPORTAL_IDENTITYSERVER_ADMINEMAIL=admin-email-here@radar-base.net - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERURL=http://kratos:4433 - - MANAGEMENTPORTAL_IDENTITYSERVER_LOGINURL=http://localhost:3000 + - MANAGEMENTPORTAL_IDENTITYSERVER_LOGINURL=http://radar-self-enrolment-ui:3000 - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERADMINURL=http://kratos:4434 + - MANAGEMENTPORTAL_AUTHSERVER_SERVERURL=http://hydra:4444 + - MANAGEMENTPORTAL_AUTHSERVER_SERVERADMINURL=http://hydra:4445 - JHIPSTER_SLEEP=10 # gives time for the database to boot before the application - JAVA_OPTS=-Xmx512m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 #enables remote debugging ports: From 5234fd2383439629d68b8a72b37808979f890537 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 14 Aug 2024 12:30:57 +0100 Subject: [PATCH 11/68] Remove unused KratosTokenVerifier and Oauth2LoginUiWebConfig --- .../auth/kratos/KratosTokenVerifier.kt | 33 --- .../auth/kratos/KratosTokenVerifierLoader.kt | 14 -- .../config/OAuth2LoginUiWebConfig.kt | 218 ------------------ 3 files changed, 265 deletions(-) delete mode 100644 radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifier.kt delete mode 100644 radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifierLoader.kt delete mode 100644 src/main/java/org/radarbase/management/config/OAuth2LoginUiWebConfig.kt diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifier.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifier.kt deleted file mode 100644 index 321ec1a92..000000000 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifier.kt +++ /dev/null @@ -1,33 +0,0 @@ -package org.radarbase.auth.kratos -import org.radarbase.auth.authentication.TokenVerifier -import org.radarbase.auth.exception.IdpException -import org.radarbase.auth.exception.InsufficientAuthenticationLevelException -import org.radarbase.auth.token.RadarToken -import org.slf4j.LoggerFactory - -//TODO Better error screen for no AAL2 -class KratosTokenVerifier(private val sessionService: SessionService, private val requireAal2: Boolean) : TokenVerifier { - @Throws(IdpException::class) - override suspend fun verify(token: String): RadarToken = try { - val kratosSession = sessionService.getSession(token) - - val radarToken = kratosSession.toDataRadarToken() - if (radarToken.authenticatorAssuranceLevel != RadarToken.AuthenticatorAssuranceLevel.aal2 && requireAal2) - { - val msg = "found a token of with aal: ${radarToken.authenticatorAssuranceLevel}, which is insufficient for this" + - " action" - throw InsufficientAuthenticationLevelException(msg) - } - radarToken - } catch (ex: InsufficientAuthenticationLevelException) { - throw ex - } catch (ex: Throwable) { - throw IdpException("could not verify token", ex) - } - - override fun toString(): String = "org.radarbase.auth.kratos.KratosTokenVerifier" - - companion object { - private val logger = LoggerFactory.getLogger(KratosTokenVerifier::class.java) - } -} diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifierLoader.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifierLoader.kt deleted file mode 100644 index 7c567cc3a..000000000 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosTokenVerifierLoader.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.radarbase.auth.kratos -import org.radarbase.auth.authentication.TokenVerifier -import org.radarbase.auth.authentication.TokenVerifierLoader - -class KratosTokenVerifierLoader(private val serverUrl: String, private val requireAal2: Boolean) : TokenVerifierLoader { - - override suspend fun fetch(): List { - return listOf( - KratosTokenVerifier(SessionService(serverUrl), requireAal2) - ) - } - - override fun toString(): String = "KratosTokenKeyAlgorithmKeyLoader" -} diff --git a/src/main/java/org/radarbase/management/config/OAuth2LoginUiWebConfig.kt b/src/main/java/org/radarbase/management/config/OAuth2LoginUiWebConfig.kt deleted file mode 100644 index fd88d3162..000000000 --- a/src/main/java/org/radarbase/management/config/OAuth2LoginUiWebConfig.kt +++ /dev/null @@ -1,218 +0,0 @@ -package org.radarbase.management.config - -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpStatus -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity -import org.springframework.security.authentication.InsufficientAuthenticationException -import org.springframework.security.core.Authentication -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.oauth2.common.OAuth2AccessToken -import org.springframework.security.oauth2.common.exceptions.OAuth2Exception -import org.springframework.security.oauth2.common.util.OAuth2Utils -import org.springframework.security.oauth2.provider.ClientDetailsService -import org.springframework.security.oauth2.provider.endpoint.TokenEndpoint -import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory -import org.springframework.stereotype.Controller -import org.springframework.web.HttpRequestMethodNotSupportedException -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.SessionAttributes -import org.springframework.web.servlet.ModelAndView -import org.springframework.web.util.HtmlUtils -import java.net.URLEncoder -import java.security.Principal -import java.text.SimpleDateFormat -import java.util.* -import java.util.function.Function -import java.util.stream.Collectors -import java.util.stream.Stream -import javax.servlet.RequestDispatcher -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set - -/** - * Created by dverbeec on 6/07/2017. - */ -@Controller -@SessionAttributes("authorizationRequest") -class OAuth2LoginUiWebConfig( - @Autowired private val tokenEndPoint: TokenEndpoint, - @Autowired private val managementPortalProperties: ManagementPortalProperties -) { - - @Autowired - private val clientDetailsService: ClientDetailsService? = null - - @RequestMapping("/oauth2/authorize") - fun redirect_authorize(request: HttpServletRequest): String { - val returnString = URLEncoder.encode(request.requestURL.toString().replace("oauth2", "oauth") + "?" + request.parameterMap.map{ param -> param.key + "=" + param.value.first()}.joinToString("&"), "UTF-8") - val mpUrl = managementPortalProperties.common.baseUrl - return "redirect:$mpUrl/kratos-ui/login?return_to=$returnString" - } - - @PostMapping( - "/oauth2/token", - consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE], - produces = [MediaType.APPLICATION_FORM_URLENCODED_VALUE] - ) - fun redirect_token(@RequestParam parameters: Map, request: HttpServletRequest, response: HttpServletResponse) { - var dispatcher: RequestDispatcher = request.servletContext.getRequestDispatcher("/oauth/token/") - dispatcher.forward(request, response) - } - - @PostMapping(value = ["/oauth/token"], - consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE] - ) - @Throws( - HttpRequestMethodNotSupportedException::class - ) - fun postAccessToken(@RequestParam parameters: Map, principal: Principal?): - ResponseEntity { - if (principal !is Authentication) { - throw InsufficientAuthenticationException( - "There is no client authentication. Try adding an appropriate authentication filter." - ) - } - - val grant_type = parameters.get("grant_type") - logger.debug("Token request of grant type $grant_type received") - - val clientId: String = parameters.get("client_id") ?: principal.name - var radarPrincipal = RadarPrincipal(clientId, principal) - - val token2 = this.tokenEndPoint.postAccessToken(radarPrincipal, parameters) - return getResponse(token2.body) - } - - fun getResponse(accessToken: OAuth2AccessToken): ResponseEntity { - val headers = HttpHeaders() - headers["Cache-Control"] = "no-store" - headers["Pragma"] = "no-cache" - headers["Content-Type"] = "application/json" - return ResponseEntity(accessToken, headers, HttpStatus.OK) - } - - /** - * Login form for OAuth2 auhorization flows. - * @param request the servlet request - * @param response the servlet response - * @return a ModelAndView to render the form - */ - @RequestMapping("/login") - fun getLogin(request: HttpServletRequest, response: HttpServletResponse?): ModelAndView { - val model = TreeMap() - if (request.parameterMap.containsKey("error")) { - model["loginError"] = true - } - return ModelAndView("login", model) - } - - /** - * Form for a client to confirm authorizing an OAuth client access to the requested resources. - * @param request the servlet request - * @param response the servlet response - * @return a ModelAndView to render the form - */ - @RequestMapping("/oauth/confirm_access") - fun getAccessConfirmation( - request: HttpServletRequest, - response: HttpServletResponse? - ): ModelAndView { - val params = request.parameterMap - val authorizationParameters = Stream.of( - OAuth2Utils.CLIENT_ID, OAuth2Utils.REDIRECT_URI, OAuth2Utils.STATE, - OAuth2Utils.SCOPE, OAuth2Utils.RESPONSE_TYPE - ) - .filter { key: String -> params.containsKey(key) } - .collect(Collectors.toMap(Function.identity(), Function { p: String -> params[p]!![0] })) - val authorizationRequest = DefaultOAuth2RequestFactory( - clientDetailsService - ).createAuthorizationRequest(authorizationParameters) - val model = Collections.singletonMap( - "authorizationRequest", - authorizationRequest - ) - return ModelAndView("authorize", model) - } - - /** - * A page to render errors that arised during an OAuth flow. - * @param req the servlet request - * @return a ModelAndView to render the page - */ - @RequestMapping("/oauth/error") - fun handleOAuthClientError(req: HttpServletRequest): ModelAndView { - val model = TreeMap() - val error = req.getAttribute("error") - // The error summary may contain malicious user input, - // it needs to be escaped to prevent XSS - val errorParams: MutableMap = HashMap() - errorParams["date"] = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US) - .format(Date()) - if (error is OAuth2Exception) { - val oauthError = error - errorParams["status"] = String.format("%d", oauthError.httpErrorCode) - errorParams["code"] = oauthError.oAuth2ErrorCode - errorParams["message"] = oauthError.message?.let { HtmlUtils.htmlEscape(it) } ?: "No error message found" - // transform the additionalInfo map to a comma seperated list of key: value pairs - if (oauthError.additionalInformation != null) { - errorParams["additionalInfo"] = HtmlUtils.htmlEscape( - oauthError.additionalInformation.entries.joinToString(", ") { entry -> entry.key + ": " + entry.value } - ) - } - } - // Copy non-empty entries to the model. Empty entries will not be present in the model, - // so the default value will be rendered in the view. - for ((key, value) in errorParams) { - if (value != "") { - model[key] = value - } - } - return ModelAndView("error", model) - } - - private class RadarPrincipal(private val name: String, private val auth: Authentication) : Principal, Authentication { - - override fun getName(): String { - return name - } - - override fun getAuthorities(): MutableCollection { - return auth.authorities - } - - override fun getCredentials(): Any { - return auth.credentials - } - - override fun getDetails(): Any { - return auth.details - } - - override fun getPrincipal(): Any { - return this - } - - override fun isAuthenticated(): Boolean { - return auth.isAuthenticated - } - - override fun setAuthenticated(isAuthenticated: Boolean) { - auth.isAuthenticated = isAuthenticated - } - - } - - companion object { - private val logger = LoggerFactory.getLogger( - OAuth2LoginUiWebConfig::class.java - ) - } -} From ea477757db207219d5bb4fcb7fe2f94557a6dd6f Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 14 Aug 2024 12:32:00 +0100 Subject: [PATCH 12/68] Update setting of hydra token verifier loader --- .../jwt/ManagementPortalOauthKeyStoreHandler.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt index a58026e35..493cd9af3 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt @@ -7,7 +7,6 @@ import org.radarbase.auth.authentication.TokenValidator import org.radarbase.auth.jwks.JsonWebKeySet import org.radarbase.auth.jwks.JwkAlgorithmParser import org.radarbase.auth.jwks.JwksTokenVerifierLoader -import org.radarbase.auth.kratos.KratosTokenVerifierLoader import org.radarbase.management.config.ManagementPortalProperties import org.radarbase.management.config.ManagementPortalProperties.Oauth import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter.Companion.RES_MANAGEMENT_PORTAL @@ -52,7 +51,6 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( private val oauthConfig: Oauth private val verifierPublicKeyAliasList: List private val managementPortalBaseUrl: String - private val authServerUrl: String val verifiers: MutableList val refreshTokenVerifiers: MutableList @@ -80,7 +78,6 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( // No need to check audience with a refresh token: it can be used // to refresh tokens intended for other resources. refreshTokenVerifiers = algorithms.map { algo: Algorithm -> JWT.require(algo).build() }.toMutableList() - authServerUrl = managementPortalProperties.authServer.serverAdminUrl tokenValidator.refresh() } @@ -231,11 +228,13 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( RES_MANAGEMENT_PORTAL, JwkAlgorithmParser() ), - JwksTokenVerifierLoader( - authServerUrl + "/admin/keys/hydra.jwt.access-token", - RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), + managementPortalProperties.authServer.let { + JwksTokenVerifierLoader( + it.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", + RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ) + }, ) return TokenValidator(loaderList) } From 3d3eec090c20d3c58fdc9dafd9dc5184a396bc4f Mon Sep 17 00:00:00 2001 From: yatharthranjan Date: Wed, 14 Aug 2024 12:38:21 +0100 Subject: [PATCH 13/68] use published dev docker image for UI --- src/main/docker/ory_stack.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index 96685ec5a..ce0d47f17 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -5,8 +5,7 @@ volumes: services: radar-self-enrolment-ui: - image: - mpgxvii/radar-self-enrolment-ui:latest + image: ghcr.io/radar-base/radar-self-enrolment-ui:dev environment: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 From 1951b90890938d200a00d6ad18a371092f15f6ac Mon Sep 17 00:00:00 2001 From: yatharthranjan Date: Wed, 14 Aug 2024 12:46:00 +0100 Subject: [PATCH 14/68] fix port mappings --- src/main/docker/ory_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index ce0d47f17..c2f545d13 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -10,7 +10,7 @@ services: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 ports: - - "3000:4455" + - "3000:3000" volumes: - /tmp/ui-node/logs:/root/.npm/_logs From 85348d6878fa43635d9f94667007c019903cc0e0 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 14 Aug 2024 15:56:12 +0100 Subject: [PATCH 15/68] Update self-enrolment image and login redirect url --- src/main/docker/ory_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index c2f545d13..ba8eb92b4 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -5,7 +5,7 @@ volumes: services: radar-self-enrolment-ui: - image: ghcr.io/radar-base/radar-self-enrolment-ui:dev + image: ghcr.io/radar-base/radar-self-enrolment-ui:feat-consent-module environment: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 From 02e08a43588d24622ce3ded4f8360bdc73ea0987 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 14 Aug 2024 15:56:52 +0100 Subject: [PATCH 16/68] update login redirect url --- src/main/docker/managementportal.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/managementportal.yml b/src/main/docker/managementportal.yml index b19a7a337..6d1d98098 100644 --- a/src/main/docker/managementportal.yml +++ b/src/main/docker/managementportal.yml @@ -12,7 +12,7 @@ services: - MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET=secret - MANAGEMENTPORTAL_IDENTITYSERVER_ADMINEMAIL=admin-email-here@radar-base.net - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERURL=http://kratos:4433 - - MANAGEMENTPORTAL_IDENTITYSERVER_LOGINURL=http://radar-self-enrolment-ui:3000 + - MANAGEMENTPORTAL_IDENTITYSERVER_LOGINURL=http://localhost:3000 - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERADMINURL=http://kratos:4434 - MANAGEMENTPORTAL_AUTHSERVER_SERVERURL=http://hydra:4444 - MANAGEMENTPORTAL_AUTHSERVER_SERVERADMINURL=http://hydra:4445 From d72ff43faf698a1afef70979bbe65a84048f62f1 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 15 Aug 2024 12:24:51 +0100 Subject: [PATCH 17/68] Fix MP endpoint in kratos config --- src/main/docker/ory_stack.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index ba8eb92b4..0a9ef9929 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -23,17 +23,17 @@ services: - "4434:4434" # admin, should be closed in production restart: unless-stopped environment: - - DSN=postgres://ory:secret@postgresd-ory/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://kratos:secret@postgresd-kratos/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_HOOK=web_hook - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_METHOD=POST - - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects - - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_BODY=file:///etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal:8080/managementportal/api/kratos/subjects + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_BODY=/etc/config/kratos/webhook_body.jsonnet - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_RESPONSE_IGNORE=true - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_1_HOOK=session - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_HOOK=web_hook - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_METHOD=POST - - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects/activate - - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=file:///etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_URL=http://managementportal:8080/managementportal/api/kratos/subjects/activate + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=/etc/config/kratos/webhook_body.jsonnet command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier volumes: - type: bind From 1a9dacabd697fe87ff92e5d5e09b7c8b28e8ec00 Mon Sep 17 00:00:00 2001 From: yatharthranjan Date: Mon, 19 Aug 2024 15:00:51 +0100 Subject: [PATCH 18/68] add separate login url for hydra --- .../org/radarbase/management/web/rest/LoginEndpoint.kt | 7 +------ src/main/resources/config/application-dev.yml | 1 + src/main/resources/config/application-prod.yml | 5 +++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index 8e2b19d29..202e701ae 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -6,7 +6,6 @@ import io.ktor.client.engine.cio.* import io.ktor.client.plugins.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* -import io.ktor.client.statement.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json @@ -68,7 +67,7 @@ class LoginEndpoint } private fun buildAuthUrl(config: ManagementPortalProperties, mpUrl: String): String { - return "${config.authServer.serverUrl}/oauth2/auth?" + + return "${config.authServer.loginUrl}/oauth2/auth?" + "client_id=${config.frontend.clientId}&" + "response_type=code&" + "state=${Instant.now()}&" + @@ -105,8 +104,4 @@ class LoginEndpoint throw IdpException("Unable to get access token") } } - - companion object { - private val logger = LoggerFactory.getLogger(LoginEndpoint::class.java) - } } diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index b14bc7901..76a54b5b7 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -123,6 +123,7 @@ managementportal: authServer: serverUrl: http://localhost:4444 serverAdminUrl: http://localhost:4445 + loginUrl: http://localhost:4444 # =================================================================== diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 83746c671..436de6e62 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -105,8 +105,9 @@ managementportal: serverAdminUrl: http://kratos-admin loginUrl: http://localhost:3000 authServer: - serverUrl: http://localhost:4444 - serverAdminUrl: http://localhost:4445 + serverUrl: http://hydra:4444 + serverAdminUrl: http://hydra:4445 + loginUrl: http://localhost:4444 # =================================================================== # JHipster specific properties From bc2b2f691700c43b97c32cff7639aa5b2b62505c Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 19 Aug 2024 18:23:09 +0100 Subject: [PATCH 19/68] Update ManagementPortal propreties with new loginUrl property --- .../management/config/ManagementPortalProperties.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java index 8704edb96..8518f9554 100644 --- a/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java +++ b/src/main/java/org/radarbase/management/config/ManagementPortalProperties.java @@ -333,6 +333,7 @@ public void setLoginUrl(String loginUrl) { public class AuthServer { private String serverUrl = null; private String serverAdminUrl = null; + private String loginUrl = null; public String getServerUrl() { return serverUrl; @@ -349,6 +350,14 @@ public String getServerAdminUrl() { public void setServerAdminUrl(String serverAdminUrl) { this.serverAdminUrl = serverAdminUrl; } + + public String getLoginUrl() { + return loginUrl; + } + + public void setLoginUrl(String loginUrl) { + this.loginUrl = loginUrl; + } } From 57eecfdafd7d14c44c4b014fb42156fdfedc1d86 Mon Sep 17 00:00:00 2001 From: yatharthranjan Date: Tue, 20 Aug 2024 12:39:42 +0100 Subject: [PATCH 20/68] intermediate removal of unneeded components - This disables the components, but these can be removed once tested. --- src/main/docker/managementportal.yml | 3 +- src/main/docker/ory_stack.yml | 2 +- .../config/OAuth2ServerConfiguration.kt | 139 ++++++++++++------ .../security/JwtAuthenticationFilter.kt | 31 ++++ ...ManagementPortalJwtAccessTokenConverter.kt | 28 ++-- .../ManagementPortalOauthKeyStoreHandler.kt | 4 +- .../management/service/MetaTokenService.kt | 4 +- .../management/service/OAuthClientService.kt | 2 +- .../decorator/ProjectMapperDecorator.kt | 12 +- .../management/web/rest/MetaTokenResource.kt | 4 +- .../web/rest/OAuthClientsResource.kt | 4 +- .../management/web/rest/TokenKeyEndpoint.kt | 2 +- 12 files changed, 157 insertions(+), 78 deletions(-) diff --git a/src/main/docker/managementportal.yml b/src/main/docker/managementportal.yml index b19a7a337..ad492395c 100644 --- a/src/main/docker/managementportal.yml +++ b/src/main/docker/managementportal.yml @@ -12,9 +12,10 @@ services: - MANAGEMENTPORTAL_FRONTEND_CLIENT_SECRET=secret - MANAGEMENTPORTAL_IDENTITYSERVER_ADMINEMAIL=admin-email-here@radar-base.net - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERURL=http://kratos:4433 - - MANAGEMENTPORTAL_IDENTITYSERVER_LOGINURL=http://radar-self-enrolment-ui:3000 + - MANAGEMENTPORTAL_IDENTITYSERVER_LOGINURL=http://localhost:3000 - MANAGEMENTPORTAL_IDENTITYSERVER_SERVERADMINURL=http://kratos:4434 - MANAGEMENTPORTAL_AUTHSERVER_SERVERURL=http://hydra:4444 + - MANAGEMENTPORTAL_AUTHSERVER_LOGINURL=http://localhost:4444 - MANAGEMENTPORTAL_AUTHSERVER_SERVERADMINURL=http://hydra:4445 - JHIPSTER_SLEEP=10 # gives time for the database to boot before the application - JAVA_OPTS=-Xmx512m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 #enables remote debugging diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index c2f545d13..ba8eb92b4 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -5,7 +5,7 @@ volumes: services: radar-self-enrolment-ui: - image: ghcr.io/radar-base/radar-self-enrolment-ui:dev + image: ghcr.io/radar-base/radar-self-enrolment-ui:feat-consent-module environment: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 diff --git a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt index 8c00fa2b0..bce2afc2a 100644 --- a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt @@ -1,8 +1,13 @@ package org.radarbase.management.config +import com.auth0.jwt.JWT +import org.radarbase.auth.authentication.TokenValidator import java.util.* import javax.sql.DataSource import org.radarbase.auth.authorization.RoleAuthority +import org.radarbase.auth.jwks.JwkAlgorithmParser +import org.radarbase.auth.jwks.JwksTokenVerifierLoader +import org.radarbase.auth.jwt.JwtTokenVerifier import org.radarbase.management.repository.UserRepository import org.radarbase.management.security.ClaimsTokenEnhancer import org.radarbase.management.security.Http401UnauthorizedEntryPoint @@ -53,9 +58,8 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl @Configuration class OAuth2ServerConfiguration( @Autowired private val dataSource: DataSource, - @Autowired private val passwordEncoder: PasswordEncoder + @Autowired private val passwordEncoder: PasswordEncoder, ) { - @Configuration @Order(-20) protected class LoginConfig( @@ -91,12 +95,25 @@ class OAuth2ServerConfiguration( class JwtAuthenticationFilterConfiguration( @Autowired private val authenticationManager: AuthenticationManager, @Autowired private val userRepository: UserRepository, - @Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler + @Autowired private val managementPortalProperties: ManagementPortalProperties, + //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler ) { + val tokenValidator: TokenValidator + /** Get the default token validator. */ + get() { + val loaderList = listOf( + JwksTokenVerifierLoader( + managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", + ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ), + ) + return TokenValidator(loaderList) + } @Bean fun jwtAuthenticationFilter(): JwtAuthenticationFilter { return JwtAuthenticationFilter( - keyStoreHandler.tokenValidator, + tokenValidator, authenticationManager, userRepository, true @@ -114,17 +131,32 @@ class OAuth2ServerConfiguration( @Configuration @EnableResourceServer protected class ResourceServerConfiguration( - @Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler, - @Autowired private val tokenStore: TokenStore, + //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler, + //@Autowired private val tokenStore: TokenStore, + @Autowired private val managementPortalProperties: ManagementPortalProperties, @Autowired private val http401UnauthorizedEntryPoint: Http401UnauthorizedEntryPoint, @Autowired private val logoutSuccessHandler: LogoutSuccessHandler, @Autowired private val authenticationManager: AuthenticationManager, @Autowired private val userRepository: UserRepository ) : ResourceServerConfigurerAdapter() { + val tokenValidator: TokenValidator + /** Get the default token validator. */ + get() { + val loaderList = listOf( + JwksTokenVerifierLoader( + managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", + ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ), + ) + return TokenValidator(loaderList).apply { + refresh() + } + } fun jwtAuthenticationFilter(): JwtAuthenticationFilter { return JwtAuthenticationFilter( - keyStoreHandler.tokenValidator, authenticationManager, userRepository + tokenValidator, authenticationManager, userRepository ) .skipUrlPattern(HttpMethod.GET, "/management/health") .skipUrlPattern(HttpMethod.POST, "/oauth/token") @@ -192,7 +224,7 @@ class OAuth2ServerConfiguration( @Throws(Exception::class) override fun configure(resources: ResourceServerSecurityConfigurer) { resources.resourceId("res_ManagementPortal") - .tokenStore(tokenStore) + //.tokenStore(tokenStore) .eventPublisher(CustomEventPublisher()) } @@ -212,64 +244,79 @@ class OAuth2ServerConfiguration( @Autowired @Qualifier("authenticationManagerBean") private val authenticationManager: AuthenticationManager, @Autowired private val dataSource: DataSource, @Autowired private val jdbcClientDetailsService: JdbcClientDetailsService, - @Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler + @Autowired private val managementPortalProperties: ManagementPortalProperties, + //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler ) : AuthorizationServerConfigurerAdapter() { - @Bean - protected fun authorizationCodeServices(): AuthorizationCodeServices { - return JdbcAuthorizationCodeServices(dataSource) + val tokenValidator: TokenValidator + get() { + val loaderList = listOf( + JwksTokenVerifierLoader( + managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", + ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser() + ), + ) + return TokenValidator(loaderList).apply { + refresh() + } } - @Bean - fun approvalStore(): ApprovalStore { - return if (jpaProperties.database == Database.POSTGRESQL) { - PostgresApprovalStore(dataSource) - } else { - // to have compatibility for other databases including H2 - JdbcApprovalStore(dataSource) - } - } +// @Bean +// protected fun authorizationCodeServices(): AuthorizationCodeServices { +// return JdbcAuthorizationCodeServices(dataSource) +// } - @Bean - fun tokenEnhancer(): TokenEnhancer { - return ClaimsTokenEnhancer() - } +// @Bean +// fun approvalStore(): ApprovalStore { +// return if (jpaProperties.database == Database.POSTGRESQL) { +// PostgresApprovalStore(dataSource) +// } else { +// // to have compatibility for other databases including H2 +// JdbcApprovalStore(dataSource) +// } +// } - @Bean - fun tokenStore(): TokenStore { - return ManagementPortalJwtTokenStore(accessTokenConverter()) - } +// @Bean +// fun tokenEnhancer(): TokenEnhancer { +// return ClaimsTokenEnhancer() +// } + +// @Bean +// fun tokenStore(): TokenStore { +// return ManagementPortalJwtTokenStore(accessTokenConverter()) +// } @Bean fun accessTokenConverter(): ManagementPortalJwtAccessTokenConverter { logger.debug("loading token converter from keystore configurations") return ManagementPortalJwtAccessTokenConverter( - keyStoreHandler.tokenValidator, - keyStoreHandler.algorithmForSigning, - keyStoreHandler.verifiers, - keyStoreHandler.refreshTokenVerifiers + tokenValidator, +// JWT.require(JwtTokenVerifier.DEFAULT_ALGORITHM) +// .build(), +// keyStoreHandler.refreshTokenVerifiers ) } - @Bean - @Primary - fun tokenServices(tokenStore: TokenStore?): DefaultTokenServices { - val defaultTokenServices = DefaultTokenServices() - defaultTokenServices.setTokenStore(tokenStore) - defaultTokenServices.setSupportRefreshToken(true) - defaultTokenServices.setReuseRefreshToken(false) - return defaultTokenServices - } +// @Bean +// @Primary +// fun tokenServices(tokenStore: TokenStore?): DefaultTokenServices { +// val defaultTokenServices = DefaultTokenServices() +// defaultTokenServices.setTokenStore(tokenStore) +// defaultTokenServices.setSupportRefreshToken(true) +// defaultTokenServices.setReuseRefreshToken(false) +// return defaultTokenServices +// } override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) { val tokenEnhancerChain = TokenEnhancerChain() tokenEnhancerChain.setTokenEnhancers( - listOf(tokenEnhancer(), accessTokenConverter()) + listOf(accessTokenConverter())//tokenEnhancer(), accessTokenConverter()) ) endpoints - .authorizationCodeServices(authorizationCodeServices()) - .approvalStore(approvalStore()) - .tokenStore(tokenStore()) + //.authorizationCodeServices(authorizationCodeServices()) + //.approvalStore(approvalStore()) + //.tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .reuseRefreshTokens(false) .authenticationManager(authenticationManager) diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index 2205c18ac..9679cd9cc 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -64,6 +64,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( httpResponse: HttpServletResponse, chain: FilterChain, ) { + Companion.logger.warn("Request: {}", httpServletRequestToString(httpRequest)) if (CorsUtils.isPreFlightRequest(httpRequest)) { Companion.logger.debug("Skipping JWT check for preflight request") chain.doFilter(httpRequest, httpResponse) @@ -79,6 +80,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( var exMessage = "No token provided" if (stringToken != null) { try { + Companion.logger.warn("Validating token from header: {}", stringToken) token = validator.validateBlocking(stringToken) Companion.logger.debug("Using token from header") } catch (ex: TokenValidationException) { @@ -100,6 +102,33 @@ class JwtAuthenticationFilter @JvmOverloads constructor( chain.doFilter(httpRequest, httpResponse) } + private fun httpServletRequestToString(httpRequest: HttpServletRequest):String { + val buffer = StringBuffer() + buffer.append("Method: ${httpRequest.method}\n") + buffer.append("RequestURI: ${httpRequest.requestURI}\n") + buffer.append("QueryString: ${httpRequest.queryString}\n") + buffer.append("RequestURL: ${httpRequest.requestURL}\n") + buffer.append("Protocol: ${httpRequest.protocol}\n") + buffer.append("Scheme: ${httpRequest.scheme}\n") + buffer.append("ServerName: ${httpRequest.serverName}\n") + buffer.append("ServerPort: ${httpRequest.serverPort}\n") + buffer.append("RemoteAddr: ${httpRequest.remoteAddr}\n") + buffer.append("RemoteHost: ${httpRequest.remoteHost}\n") + buffer.append("RemotePort: ${httpRequest.remotePort}\n") + buffer.append("LocalAddr: ${httpRequest.localAddr}\n") + buffer.append("LocalName: ${httpRequest.localName}\n") + buffer.append("LocalPort: ${httpRequest.localPort}\n") + buffer.append("AuthType: ${httpRequest.authType}\n") + buffer.append("ContentType: ${httpRequest.contentType}\n") + buffer.append("ContentLength: ${httpRequest.contentLength}\n") + buffer.append("CharacterEncoding: ${httpRequest.characterEncoding}\n") + buffer.append("Cookies: ${httpRequest.cookies}\n") + buffer.append("Headers: ${httpRequest.headerNames.toList().map { it to httpRequest.getHeaders(it).toList() }}\n") + buffer.append("Attributes: ${httpRequest.attributeNames.toList().map { it to httpRequest.getAttribute(it) }}\n") + buffer.append("Parameters: ${httpRequest.parameterMap.toList().map { it.first to it.second.toList() }}\n") + return buffer.toString() + } + override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { val shouldNotFilterUrl = ignoreUrls.find { it.matches(httpRequest) } return if (shouldNotFilterUrl != null) { @@ -111,6 +140,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( } private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { + Companion.logger.warn("Token from header: {}", httpRequest.getHeader(HttpHeaders.AUTHORIZATION)) return httpRequest.getHeader(HttpHeaders.AUTHORIZATION) ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } ?.removePrefix(AUTHORIZATION_BEARER_HEADER) @@ -185,6 +215,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) private const val AUTHORIZATION_BEARER_HEADER = "Bearer" private const val TOKEN_ATTRIBUTE = "jwt" + private const val TOKEN_COOKIE_NAME = "ory_kratos_session" /** * Authority references for given user. The user should have its roles mapped diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt index 88962f0a2..7a997ba36 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt @@ -36,9 +36,9 @@ import org.radarbase.auth.exception.TokenValidationException */ open class ManagementPortalJwtAccessTokenConverter( validator: TokenValidator, - algorithm: Algorithm, - verifiers: MutableList, - private val refreshTokenVerifiers: List +// algorithm: Algorithm, +// verifiers: MutableList, +// private val refreshTokenVerifiers: List ) : JwtAccessTokenConverter { private val jsonParser = ObjectMapper().readerFor( MutableMap::class.java @@ -63,7 +63,7 @@ open class ManagementPortalJwtAccessTokenConverter( } private var algorithm: Algorithm? = null private var validator: TokenValidator - private val verifiers: MutableList + //private val verifiers: MutableList /** * Default constructor. @@ -75,9 +75,9 @@ open class ManagementPortalJwtAccessTokenConverter( val accessToken = DefaultAccessTokenConverter() accessToken.setIncludeGrantType(true) tokenConverter = accessToken - this.verifiers = verifiers + //this.verifiers = verifiers this.validator = validator - setAlgorithm(algorithm) + //setAlgorithm(algorithm) } override fun convertAccessToken( @@ -102,9 +102,9 @@ open class ManagementPortalJwtAccessTokenConverter( override fun setAlgorithm(algorithm: Algorithm) { this.algorithm = algorithm - if (verifiers.isEmpty()) { - verifiers.add(JWT.require(algorithm).withAudience(RES_MANAGEMENT_PORTAL).build()) - } +// if (verifiers.isEmpty()) { +// verifiers.add(JWT.require(algorithm).withAudience(RES_MANAGEMENT_PORTAL).build()) +// } } /** @@ -212,7 +212,7 @@ open class ManagementPortalJwtAccessTokenConverter( override fun decode(token: String): Map { val jwt = JWT.decode(token) - val verifierToUse: List +// val verifierToUse: List val claims: MutableMap try { val decodedPayload = String( @@ -227,18 +227,18 @@ open class ManagementPortalJwtAccessTokenConverter( if (jwtClaimsSetVerifier != null) { jwtClaimsSetVerifier!!.verify(claims) } - verifierToUse = - if (claims[JwtAccessTokenConverter.ACCESS_TOKEN_ID] != null) refreshTokenVerifiers else verifiers +// verifierToUse = +// if (claims[JwtAccessTokenConverter.ACCESS_TOKEN_ID] != null) refreshTokenVerifiers else verifiers } catch (ex: JsonProcessingException) { throw InvalidTokenException("Invalid token", ex) } try { validator.validateBlocking(token) - Companion.logger.debug("Using token from header") + logger.debug("Using token from header") return claims } catch (ex: TokenValidationException) { ex.message?.let { - Companion.logger.info("Failed to validate token from header: {}", it) + logger.info("Failed to validate token from header: {}", it) } } throw InvalidTokenException("No registered validator could authenticate this token") diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt index 493cd9af3..eff469292 100644 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt @@ -41,7 +41,7 @@ import kotlin.collections.Map.Entry * [org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory]. However, * this class does not assume a specific key type, while the Spring factory assumes RSA keys. */ -@Component +//@Component class ManagementPortalOauthKeyStoreHandler @Autowired constructor( environment: Environment, servletContext: ServletContext, private val managementPortalProperties: ManagementPortalProperties ) { @@ -73,7 +73,7 @@ class ManagementPortalOauthKeyStoreHandler @Autowired constructor( logger.info("Using Management Portal base-url {}", managementPortalBaseUrl) val algorithms = loadAlgorithmsFromAlias().filter { obj: Algorithm? -> Objects.nonNull(obj) }.toList() verifiers = algorithms.map { algo: Algorithm? -> - JWT.require(algo).withAudience(ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL).build() + JWT.require(algo).withAudience(RES_MANAGEMENT_PORTAL).build() }.toMutableList() // No need to check audience with a refresh token: it can be used // to refresh tokens intended for other resources. diff --git a/src/main/java/org/radarbase/management/service/MetaTokenService.kt b/src/main/java/org/radarbase/management/service/MetaTokenService.kt index 529011bd7..dcbdb0e3d 100644 --- a/src/main/java/org/radarbase/management/service/MetaTokenService.kt +++ b/src/main/java/org/radarbase/management/service/MetaTokenService.kt @@ -37,8 +37,8 @@ import javax.validation.ConstraintViolationException * Service to delegate MetaToken handling. * */ -@Service -@Transactional +//@Service +//@Transactional class MetaTokenService { @Autowired private val metaTokenRepository: MetaTokenRepository? = null diff --git a/src/main/java/org/radarbase/management/service/OAuthClientService.kt b/src/main/java/org/radarbase/management/service/OAuthClientService.kt index 7f09ce8f7..735d6a23f 100644 --- a/src/main/java/org/radarbase/management/service/OAuthClientService.kt +++ b/src/main/java/org/radarbase/management/service/OAuthClientService.kt @@ -28,7 +28,7 @@ import java.util.* * The service layer to handle OAuthClient and Token related functions. * Created by nivethika on 03/08/2018. */ -@Service +//@Service class OAuthClientService( @Autowired private val clientDetailsService: JdbcClientDetailsService, @Autowired private val clientDetailsMapper: ClientDetailsMapper, diff --git a/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt b/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt index 3fae78093..c51341b59 100644 --- a/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt +++ b/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt @@ -23,16 +23,16 @@ abstract class ProjectMapperDecorator : ProjectMapper { @Autowired @Qualifier("delegate") private lateinit var delegate: ProjectMapper @Autowired private lateinit var organizationRepository: OrganizationRepository @Autowired private lateinit var projectRepository: ProjectRepository - @Autowired private lateinit var metaTokenService: MetaTokenService + //@Autowired private lateinit var metaTokenService: MetaTokenService override fun projectToProjectDTO(project: Project?): ProjectDTO? { val dto = delegate.projectToProjectDTO(project) dto?.humanReadableProjectName = project?.attributes?.get(ProjectDTO.HUMAN_READABLE_PROJECT_NAME) - try { - dto?.persistentTokenTimeout = metaTokenService.getMetaTokenTimeout(true, project).toMillis() - } catch (ex: BadRequestException) { - dto?.persistentTokenTimeout = null - } +// try { +// dto?.persistentTokenTimeout = metaTokenService.getMetaTokenTimeout(true, project).toMillis() +// } catch (ex: BadRequestException) { +// dto?.persistentTokenTimeout = null +// } return dto } diff --git a/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt b/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt index bbf0a0b2b..7c9a412a5 100644 --- a/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt +++ b/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt @@ -20,8 +20,8 @@ import org.springframework.web.bind.annotation.RestController import java.net.MalformedURLException import java.time.Duration -@RestController -@RequestMapping("/api") +//@RestController +//@RequestMapping("/api") class MetaTokenResource { @Autowired private val metaTokenService: MetaTokenService? = null diff --git a/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt b/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt index ef7417585..68e9978d7 100644 --- a/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt +++ b/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt @@ -41,8 +41,8 @@ import javax.validation.Valid /** * Created by dverbeec on 5/09/2017. */ -@RestController -@RequestMapping("/api") +//@RestController +//@RequestMapping("/api") class OAuthClientsResource( @Autowired private val oAuthClientService: OAuthClientService, @Autowired private val metaTokenService: MetaTokenService, diff --git a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt index c4fc357ee..e85db0c1b 100644 --- a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt @@ -8,7 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController -@RestController +//@RestController class TokenKeyEndpoint @Autowired constructor( private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler ) { From 03c87ed7180cae4e807dee996c91407625d584a9 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 20 Aug 2024 14:04:01 +0100 Subject: [PATCH 21/68] Fix ory stack configs --- src/main/docker/ory_stack.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index c2f545d13..eb94e4391 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -5,12 +5,12 @@ volumes: services: radar-self-enrolment-ui: - image: ghcr.io/radar-base/radar-self-enrolment-ui:dev + image: ghcr.io/radar-base/radar-self-enrolment-ui:feat-consent-module environment: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 ports: - - "3000:3000" + - "3002:4455" volumes: - /tmp/ui-node/logs:/root/.npm/_logs @@ -26,14 +26,14 @@ services: - DSN=postgres://ory:secret@postgresd-ory/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_HOOK=web_hook - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_METHOD=POST - - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects - - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_BODY=file:///etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal:8080/managementportal/api/kratos/subjects + - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_BODY=/etc/config/kratos/webhook_body.jsonnet - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_RESPONSE_IGNORE=true - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_1_HOOK=session - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_HOOK=web_hook - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_METHOD=POST - - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_URL=http://managementportal-app:8080/managementportal/api/kratos/subjects/activate - - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=file:///etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_URL=http://managementportal:8080/managementportal/api/kratos/subjects/activate + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=/etc/config/kratos/webhook_body.jsonnet command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier volumes: - type: bind From 1882851fc1e6b07594664de3615873263e1249ec Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 22 Aug 2024 15:39:51 +0100 Subject: [PATCH 22/68] Remove OAuth2ServerConfiguration and use single SecurityConfig for auth --- .../config/OAuth2ServerConfiguration.kt | 341 ------------------ .../config/SecurityConfiguration.kt | 181 ++++++---- .../ManagementPortalOauthKeyStoreHandler.kt | 302 ---------------- .../management/web/rest/TokenKeyEndpoint.kt | 6 +- src/main/resources/config/application-dev.yml | 2 +- .../resources/config/application-prod.yml | 2 +- 6 files changed, 114 insertions(+), 720 deletions(-) delete mode 100644 src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt delete mode 100644 src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt diff --git a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt b/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt deleted file mode 100644 index bce2afc2a..000000000 --- a/src/main/java/org/radarbase/management/config/OAuth2ServerConfiguration.kt +++ /dev/null @@ -1,341 +0,0 @@ -package org.radarbase.management.config - -import com.auth0.jwt.JWT -import org.radarbase.auth.authentication.TokenValidator -import java.util.* -import javax.sql.DataSource -import org.radarbase.auth.authorization.RoleAuthority -import org.radarbase.auth.jwks.JwkAlgorithmParser -import org.radarbase.auth.jwks.JwksTokenVerifierLoader -import org.radarbase.auth.jwt.JwtTokenVerifier -import org.radarbase.management.repository.UserRepository -import org.radarbase.management.security.ClaimsTokenEnhancer -import org.radarbase.management.security.Http401UnauthorizedEntryPoint -import org.radarbase.management.security.JwtAuthenticationFilter -import org.radarbase.management.security.PostgresApprovalStore -import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter -import org.radarbase.management.security.jwt.ManagementPortalJwtTokenStore -import org.radarbase.management.security.jwt.ManagementPortalOauthKeyStoreHandler -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.context.annotation.Primary -import org.springframework.core.annotation.Order -import org.springframework.http.HttpMethod -import org.springframework.orm.jpa.vendor.Database -import org.springframework.security.authentication.AuthenticationManager -import org.springframework.security.authentication.DefaultAuthenticationEventPublisher -import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder -import org.springframework.security.config.annotation.web.builders.HttpSecurity -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter -import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.core.Authentication -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder -import org.springframework.security.crypto.password.PasswordEncoder -import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer -import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer -import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer -import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer -import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer -import org.springframework.security.oauth2.provider.approval.ApprovalStore -import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore -import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService -import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices -import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices -import org.springframework.security.oauth2.provider.token.DefaultTokenServices -import org.springframework.security.oauth2.provider.token.TokenEnhancer -import org.springframework.security.oauth2.provider.token.TokenEnhancerChain -import org.springframework.security.oauth2.provider.token.TokenStore -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter -import org.springframework.security.web.authentication.logout.LogoutSuccessHandler - -@Configuration -class OAuth2ServerConfiguration( - @Autowired private val dataSource: DataSource, - @Autowired private val passwordEncoder: PasswordEncoder, -) { - @Configuration - @Order(-20) - protected class LoginConfig( - @Autowired private val authenticationManager: AuthenticationManager, - @Autowired private val jwtAuthenticationFilter: JwtAuthenticationFilter - ) : WebSecurityConfigurerAdapter() { - - @Throws(Exception::class) - override fun configure(http: HttpSecurity) { - http - .formLogin().loginPage("/login").permitAll() - .and() - .authorizeRequests() - .antMatchers("/oauth/token").permitAll() - .and() - .addFilterAfter( - jwtAuthenticationFilter, - UsernamePasswordAuthenticationFilter::class.java - ) - .requestMatchers() - .antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access") - .and() - .authorizeRequests().anyRequest().authenticated() - } - - @Throws(Exception::class) - override fun configure(auth: AuthenticationManagerBuilder) { - auth.parentAuthenticationManager(authenticationManager) - } - } - - @Configuration - class JwtAuthenticationFilterConfiguration( - @Autowired private val authenticationManager: AuthenticationManager, - @Autowired private val userRepository: UserRepository, - @Autowired private val managementPortalProperties: ManagementPortalProperties, - //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler - ) { - val tokenValidator: TokenValidator - /** Get the default token validator. */ - get() { - val loaderList = listOf( - JwksTokenVerifierLoader( - managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", - ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), - ) - return TokenValidator(loaderList) - } - @Bean - fun jwtAuthenticationFilter(): JwtAuthenticationFilter { - return JwtAuthenticationFilter( - tokenValidator, - authenticationManager, - userRepository, - true - ) - } - } - - @Bean - fun jdbcClientDetailsService(): JdbcClientDetailsService { - val clientDetailsService = JdbcClientDetailsService(dataSource) - clientDetailsService.setPasswordEncoder(passwordEncoder) - return clientDetailsService - } - - @Configuration - @EnableResourceServer - protected class ResourceServerConfiguration( - //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler, - //@Autowired private val tokenStore: TokenStore, - @Autowired private val managementPortalProperties: ManagementPortalProperties, - @Autowired private val http401UnauthorizedEntryPoint: Http401UnauthorizedEntryPoint, - @Autowired private val logoutSuccessHandler: LogoutSuccessHandler, - @Autowired private val authenticationManager: AuthenticationManager, - @Autowired private val userRepository: UserRepository - ) : ResourceServerConfigurerAdapter() { - val tokenValidator: TokenValidator - /** Get the default token validator. */ - get() { - val loaderList = listOf( - JwksTokenVerifierLoader( - managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", - ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), - ) - return TokenValidator(loaderList).apply { - refresh() - } - } - - fun jwtAuthenticationFilter(): JwtAuthenticationFilter { - return JwtAuthenticationFilter( - tokenValidator, authenticationManager, userRepository - ) - .skipUrlPattern(HttpMethod.GET, "/management/health") - .skipUrlPattern(HttpMethod.POST, "/oauth/token") - .skipUrlPattern(HttpMethod.GET, "/api/meta-token/*") - .skipUrlPattern(HttpMethod.GET, "/api/public/projects") - .skipUrlPattern(HttpMethod.GET, "/api/sitesettings") - .skipUrlPattern(HttpMethod.GET, "/api/redirect/**") - .skipUrlPattern(HttpMethod.GET, "/api/logout-url") - .skipUrlPattern(HttpMethod.GET, "/oauth2/authorize") - .skipUrlPattern(HttpMethod.GET, "/images/**") - .skipUrlPattern(HttpMethod.GET, "/css/**") - .skipUrlPattern(HttpMethod.GET, "/js/**") - .skipUrlPattern(HttpMethod.GET, "/radar-baseRR.png") - } - - @Throws(Exception::class) - override fun configure(http: HttpSecurity) { - http - .exceptionHandling() - .authenticationEntryPoint(http401UnauthorizedEntryPoint) - .and() - .addFilterBefore( - jwtAuthenticationFilter(), - UsernamePasswordAuthenticationFilter::class.java - ) - .authorizeRequests() - .antMatchers("/oauth/**").permitAll() - .and() - .logout().invalidateHttpSession(true) - .logoutUrl("/api/logout") - .logoutSuccessHandler(logoutSuccessHandler) - .and() - .headers() - .frameOptions() - .disable() - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) - .and() - .addFilterBefore( - jwtAuthenticationFilter(), - UsernamePasswordAuthenticationFilter::class.java - ) - .authorizeRequests() - .antMatchers(HttpMethod.OPTIONS, "/**").permitAll() - .antMatchers("/api/register") - .hasAnyAuthority(RoleAuthority.SYS_ADMIN_AUTHORITY) - .antMatchers("/api/profile-info").permitAll() - .antMatchers("/api/sitesettings").permitAll() - .antMatchers("/api/public/projects").permitAll() - .antMatchers("/api/logout-url").permitAll() - .antMatchers("/api/**") - .authenticated() // Allow management/health endpoint to all to allow kubernetes to be able to - // detect the health of the service - .antMatchers("/oauth/token").permitAll() - .antMatchers("/management/health").permitAll() - .antMatchers("/management/**") - .hasAnyAuthority(RoleAuthority.SYS_ADMIN_AUTHORITY) - .antMatchers("/v2/api-docs/**").permitAll() - .antMatchers("/swagger-resources/configuration/ui").permitAll() - .antMatchers("/swagger-ui/index.html") - .hasAnyAuthority(RoleAuthority.SYS_ADMIN_AUTHORITY) - } - - @Throws(Exception::class) - override fun configure(resources: ResourceServerSecurityConfigurer) { - resources.resourceId("res_ManagementPortal") - //.tokenStore(tokenStore) - .eventPublisher(CustomEventPublisher()) - } - - protected class CustomEventPublisher : DefaultAuthenticationEventPublisher() { - override fun publishAuthenticationSuccess(authentication: Authentication) { - // OAuth2AuthenticationProcessingFilter publishes an authentication success audit - // event for EVERY successful OAuth request to our API resources, this is way too - // much so we override the event publisher to not publish these events. - } - } - } - - @Configuration - @EnableAuthorizationServer - protected class AuthorizationServerConfiguration( - @Autowired private val jpaProperties: JpaProperties, - @Autowired @Qualifier("authenticationManagerBean") private val authenticationManager: AuthenticationManager, - @Autowired private val dataSource: DataSource, - @Autowired private val jdbcClientDetailsService: JdbcClientDetailsService, - @Autowired private val managementPortalProperties: ManagementPortalProperties, - //@Autowired private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler - ) : AuthorizationServerConfigurerAdapter() { - - val tokenValidator: TokenValidator - get() { - val loaderList = listOf( - JwksTokenVerifierLoader( - managementPortalProperties.authServer.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", - ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), - ) - return TokenValidator(loaderList).apply { - refresh() - } - } - -// @Bean -// protected fun authorizationCodeServices(): AuthorizationCodeServices { -// return JdbcAuthorizationCodeServices(dataSource) -// } - -// @Bean -// fun approvalStore(): ApprovalStore { -// return if (jpaProperties.database == Database.POSTGRESQL) { -// PostgresApprovalStore(dataSource) -// } else { -// // to have compatibility for other databases including H2 -// JdbcApprovalStore(dataSource) -// } -// } - -// @Bean -// fun tokenEnhancer(): TokenEnhancer { -// return ClaimsTokenEnhancer() -// } - -// @Bean -// fun tokenStore(): TokenStore { -// return ManagementPortalJwtTokenStore(accessTokenConverter()) -// } - - @Bean - fun accessTokenConverter(): ManagementPortalJwtAccessTokenConverter { - logger.debug("loading token converter from keystore configurations") - return ManagementPortalJwtAccessTokenConverter( - tokenValidator, -// JWT.require(JwtTokenVerifier.DEFAULT_ALGORITHM) -// .build(), -// keyStoreHandler.refreshTokenVerifiers - ) - } - -// @Bean -// @Primary -// fun tokenServices(tokenStore: TokenStore?): DefaultTokenServices { -// val defaultTokenServices = DefaultTokenServices() -// defaultTokenServices.setTokenStore(tokenStore) -// defaultTokenServices.setSupportRefreshToken(true) -// defaultTokenServices.setReuseRefreshToken(false) -// return defaultTokenServices -// } - - override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) { - val tokenEnhancerChain = TokenEnhancerChain() - tokenEnhancerChain.setTokenEnhancers( - listOf(accessTokenConverter())//tokenEnhancer(), accessTokenConverter()) - ) - endpoints - //.authorizationCodeServices(authorizationCodeServices()) - //.approvalStore(approvalStore()) - //.tokenStore(tokenStore()) - .tokenEnhancer(tokenEnhancerChain) - .reuseRefreshTokens(false) - .authenticationManager(authenticationManager) - } - - override fun configure(oauthServer: AuthorizationServerSecurityConfigurer) { - oauthServer.allowFormAuthenticationForClients() - .checkTokenAccess("isAuthenticated()") - .tokenKeyAccess("permitAll()") - .passwordEncoder(BCryptPasswordEncoder()) - } - - @Throws(Exception::class) - override fun configure(clients: ClientDetailsServiceConfigurer) { - clients.withClientDetails(jdbcClientDetailsService) - } - } - - companion object { - private val logger = LoggerFactory.getLogger(OAuth2ServerConfiguration::class.java) - } -} diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 311c1badb..3a05aae8f 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -1,6 +1,11 @@ package org.radarbase.management.config +import org.radarbase.auth.authentication.TokenValidator +import org.radarbase.auth.jwks.JwkAlgorithmParser +import org.radarbase.auth.jwks.JwksTokenVerifierLoader +import org.radarbase.management.repository.UserRepository import org.radarbase.management.security.Http401UnauthorizedEntryPoint +import org.radarbase.management.security.JwtAuthenticationFilter // Make sure to import this import org.radarbase.management.security.RadarAuthenticationProvider import org.springframework.beans.factory.BeanInitializationException import org.springframework.beans.factory.annotation.Autowired @@ -17,9 +22,8 @@ import org.springframework.security.config.annotation.web.builders.WebSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.config.http.SessionCreationPolicy -import org.springframework.security.core.userdetails.UserDetailsService -import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.authentication.logout.LogoutSuccessHandler import tech.jhipster.security.AjaxLogoutSuccessHandler import javax.annotation.PostConstruct @@ -28,80 +32,115 @@ import javax.annotation.PostConstruct @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) class SecurityConfiguration -/** Security configuration constructor. */ @Autowired constructor( - private val authenticationManagerBuilder: AuthenticationManagerBuilder, - private val userDetailsService: UserDetailsService, - private val applicationEventPublisher: ApplicationEventPublisher, - private val passwordEncoder: PasswordEncoder -) : WebSecurityConfigurerAdapter() { - @PostConstruct - fun init() { - try { - authenticationManagerBuilder - .userDetailsService(userDetailsService) - .passwordEncoder(passwordEncoder) - .and() - .authenticationProvider(RadarAuthenticationProvider()) - .authenticationEventPublisher( - DefaultAuthenticationEventPublisher(applicationEventPublisher) - ) - } catch (e: Exception) { - throw BeanInitializationException("Security configuration failed", e) +/** Security configuration constructor. */ + @Autowired + constructor( + private val authenticationManagerBuilder: AuthenticationManagerBuilder, + private val applicationEventPublisher: ApplicationEventPublisher, + private val userRepository: UserRepository, + @Autowired private val managementPortalProperties: ManagementPortalProperties, + ) : WebSecurityConfigurerAdapter() { + val tokenValidator: TokenValidator + /** Get the default token validator. */ + get() { + val loaderList = + listOf( + JwksTokenVerifierLoader( + managementPortalProperties.authServer.serverAdminUrl + + "/admin/keys/hydra.jwt.access-token", + RES_MANAGEMENT_PORTAL, + JwkAlgorithmParser(), + ), + ) + return TokenValidator(loaderList) + } + + @PostConstruct + fun init() { + try { + authenticationManagerBuilder + .authenticationProvider(RadarAuthenticationProvider()) + .authenticationEventPublisher( + DefaultAuthenticationEventPublisher(applicationEventPublisher), + ) + } catch (e: Exception) { + throw BeanInitializationException("Security configuration failed", e) + } } - } - @Bean - fun logoutSuccessHandler(): LogoutSuccessHandler { - return AjaxLogoutSuccessHandler() - } + @Bean fun logoutSuccessHandler(): LogoutSuccessHandler = AjaxLogoutSuccessHandler() - @Bean - fun http401UnauthorizedEntryPoint(): Http401UnauthorizedEntryPoint { - return Http401UnauthorizedEntryPoint() - } + @Bean + fun http401UnauthorizedEntryPoint(): Http401UnauthorizedEntryPoint = Http401UnauthorizedEntryPoint() - override fun configure(web: WebSecurity) { - web.ignoring() - .antMatchers("/") - .antMatchers("/*.{js,ico,css,html}") - .antMatchers(HttpMethod.OPTIONS, "/**") - .antMatchers("/app/**/*.{js,html}") - .antMatchers("/bower_components/**") - .antMatchers("/i18n/**") - .antMatchers("/content/**") - .antMatchers("/swagger-ui/**") - .antMatchers("/api-docs/**") - .antMatchers("/swagger-ui.html") - .antMatchers("/api-docs{,.json,.yml}") - .antMatchers("/api/register") - .antMatchers("/api/logout-url") - .antMatchers("/api/profile-info") - .antMatchers("/api/activate") - .antMatchers("/api/redirect/**") - .antMatchers("/api/account/reset_password/init") - .antMatchers("/api/account/reset_password/finish") - .antMatchers("/test/**") - .antMatchers("/management/health") - .antMatchers(HttpMethod.GET, "/api/meta-token/**") - } + @Bean + fun jwtAuthenticationFilter(): JwtAuthenticationFilter = + JwtAuthenticationFilter(tokenValidator, authenticationManager(), userRepository) + .skipUrlPattern(HttpMethod.GET, "/management/health") + .skipUrlPattern(HttpMethod.POST, "/oauth/token") + .skipUrlPattern(HttpMethod.GET, "/api/meta-token/*") + .skipUrlPattern(HttpMethod.GET, "/api/public/projects") + .skipUrlPattern(HttpMethod.GET, "/api/sitesettings") + .skipUrlPattern(HttpMethod.GET, "/api/redirect/**") + .skipUrlPattern(HttpMethod.GET, "/api/profile-info") + .skipUrlPattern(HttpMethod.GET, "/api/logout-url") + .skipUrlPattern(HttpMethod.GET, "/oauth2/authorize") + .skipUrlPattern(HttpMethod.GET, "/images/**") + .skipUrlPattern(HttpMethod.GET, "/css/**") + .skipUrlPattern(HttpMethod.GET, "/js/**") + .skipUrlPattern(HttpMethod.GET, "/radar-baseRR.png") - @Throws(Exception::class) - public override fun configure(http: HttpSecurity) { - http - .httpBasic().realmName("ManagementPortal") - .and() - .sessionManagement() - .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) - } + override fun configure(web: WebSecurity) { + web.ignoring() + .antMatchers("/") + .antMatchers("/*.{js,ico,css,html}") + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/app/**/*.{js,html}") + .antMatchers("/bower_components/**") + .antMatchers("/i18n/**") + .antMatchers("/content/**") + .antMatchers("/swagger-ui/**") + .antMatchers("/api-docs/**") + .antMatchers("/swagger-ui.html") + .antMatchers("/api-docs{,.json,.yml}") + .antMatchers("/api/login") + .antMatchers("/api/logout-url") + .antMatchers("/api/profile-info") + .antMatchers("/api/activate") + .antMatchers("/api/redirect/**") + .antMatchers("/api/account/reset_password/init") + .antMatchers("/api/account/reset_password/finish") + .antMatchers("/test/**") + .antMatchers("/management/health") + .antMatchers(HttpMethod.GET, "/api/meta-token/**") + } - @Bean - @Throws(Exception::class) - override fun authenticationManagerBean(): AuthenticationManager { - return super.authenticationManagerBean() - } + @Throws(Exception::class) + public override fun configure(http: HttpSecurity) { + http + .csrf().disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + .and() + .exceptionHandling() + .authenticationEntryPoint(http401UnauthorizedEntryPoint()) + .and() + .addFilterBefore( + jwtAuthenticationFilter(), + UsernamePasswordAuthenticationFilter::class.java, + ) + .authorizeRequests() + .anyRequest().authenticated() + } + + @Bean + @Throws(Exception::class) + override fun authenticationManagerBean(): AuthenticationManager = super.authenticationManagerBean() - @Bean - fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension { - return SecurityEvaluationContextExtension() + @Bean + fun securityEvaluationContextExtension(): SecurityEvaluationContextExtension = SecurityEvaluationContextExtension() + + companion object { + const val RES_MANAGEMENT_PORTAL = "res_ManagementPortal" + } } -} diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt deleted file mode 100644 index eff469292..000000000 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalOauthKeyStoreHandler.kt +++ /dev/null @@ -1,302 +0,0 @@ -package org.radarbase.management.security.jwt - -import com.auth0.jwt.JWT -import com.auth0.jwt.JWTVerifier -import com.auth0.jwt.algorithms.Algorithm -import org.radarbase.auth.authentication.TokenValidator -import org.radarbase.auth.jwks.JsonWebKeySet -import org.radarbase.auth.jwks.JwkAlgorithmParser -import org.radarbase.auth.jwks.JwksTokenVerifierLoader -import org.radarbase.management.config.ManagementPortalProperties -import org.radarbase.management.config.ManagementPortalProperties.Oauth -import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter.Companion.RES_MANAGEMENT_PORTAL -import org.radarbase.management.security.jwt.algorithm.EcdsaJwtAlgorithm -import org.radarbase.management.security.jwt.algorithm.JwtAlgorithm -import org.radarbase.management.security.jwt.algorithm.RsaJwtAlgorithm -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.core.env.Environment -import org.springframework.core.io.ClassPathResource -import org.springframework.core.io.Resource -import org.springframework.stereotype.Component -import java.io.IOException -import java.lang.IllegalArgumentException -import java.security.KeyPair -import java.security.KeyStore -import java.security.KeyStoreException -import java.security.NoSuchAlgorithmException -import java.security.PrivateKey -import java.security.UnrecoverableKeyException -import java.security.cert.CertificateException -import java.security.interfaces.ECPrivateKey -import java.security.interfaces.RSAPrivateKey -import java.util.* -import java.util.AbstractMap.SimpleImmutableEntry -import javax.annotation.Nonnull -import javax.servlet.ServletContext -import kotlin.collections.Map.Entry - -/** - * Similar to Spring's - * [org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory]. However, - * this class does not assume a specific key type, while the Spring factory assumes RSA keys. - */ -//@Component -class ManagementPortalOauthKeyStoreHandler @Autowired constructor( - environment: Environment, servletContext: ServletContext, private val managementPortalProperties: ManagementPortalProperties -) { - private val password: CharArray - private val store: KeyStore - private val loadedResource: Resource - private val oauthConfig: Oauth - private val verifierPublicKeyAliasList: List - private val managementPortalBaseUrl: String - val verifiers: MutableList - val refreshTokenVerifiers: MutableList - - /** - * Keystore factory. This tries to load the first valid keystore listed in resources. - * - * @throws IllegalArgumentException if none of the provided resources can be used to load a - * keystore. - */ - init { - checkOAuthConfig(managementPortalProperties) - oauthConfig = managementPortalProperties.oauth - password = oauthConfig.keyStorePassword.toCharArray() - val loadedStore: Entry = loadStore() - loadedResource = loadedStore.key - store = loadedStore.value - verifierPublicKeyAliasList = loadVerifiersPublicKeyAliasList() - managementPortalBaseUrl = - ("http://localhost:" + environment.getProperty("server.port") + servletContext.contextPath) - logger.info("Using Management Portal base-url {}", managementPortalBaseUrl) - val algorithms = loadAlgorithmsFromAlias().filter { obj: Algorithm? -> Objects.nonNull(obj) }.toList() - verifiers = algorithms.map { algo: Algorithm? -> - JWT.require(algo).withAudience(RES_MANAGEMENT_PORTAL).build() - }.toMutableList() - // No need to check audience with a refresh token: it can be used - // to refresh tokens intended for other resources. - refreshTokenVerifiers = algorithms.map { algo: Algorithm -> JWT.require(algo).build() }.toMutableList() - tokenValidator.refresh() - } - - @Nonnull - private fun loadStore(): Entry { - for (resource in KEYSTORE_PATHS) { - if (!resource.exists()) { - logger.debug("JWT key store {} does not exist. Ignoring this resource", resource) - continue - } - try { - val fileName = Objects.requireNonNull(resource.filename).lowercase() - val type = if (fileName.endsWith(".pfx") || fileName.endsWith(".p12")) "PKCS12" else "jks" - val localStore = KeyStore.getInstance(type) - localStore.load(resource.inputStream, password) - logger.debug("Loaded JWT key store {}", resource) - if (localStore != null) - return SimpleImmutableEntry(resource, localStore) - } catch (ex: CertificateException) { - logger.error("Cannot load JWT key store", ex) - } catch (ex: NoSuchAlgorithmException) { - logger.error("Cannot load JWT key store", ex) - } catch (ex: KeyStoreException) { - logger.error("Cannot load JWT key store", ex) - } catch (ex: IOException) { - logger.error("Cannot load JWT key store", ex) - } - } - throw IllegalArgumentException( - "Cannot load any of the given JWT key stores " + KEYSTORE_PATHS - ) - } - - private fun loadVerifiersPublicKeyAliasList(): List { - val publicKeyAliases: MutableList = ArrayList() - oauthConfig.signingKeyAlias?.let { publicKeyAliases.add(it) } - if (oauthConfig.checkingKeyAliases != null) { - publicKeyAliases.addAll(oauthConfig.checkingKeyAliases!!) - } - return publicKeyAliases - } - - /** - * Returns configured public keys of token verifiers. - * @return List of public keys for token verification. - */ - fun loadJwks(): JsonWebKeySet { - return JsonWebKeySet(verifierPublicKeyAliasList.map { alias: String -> this.getKeyPair(alias) } - .map { keyPair: KeyPair? -> getJwtAlgorithm(keyPair) }.mapNotNull { obj: JwtAlgorithm? -> obj?.jwk }) - } - - /** - * Load default verifiers from configured keystore and aliases. - */ - private fun loadAlgorithmsFromAlias(): Collection { - return verifierPublicKeyAliasList - .map { alias: String -> this.getKeyPair(alias) } - .mapNotNull { keyPair -> getJwtAlgorithm(keyPair) } - .map { obj: JwtAlgorithm -> obj.algorithm } - } - - val algorithmForSigning: Algorithm - /** - * Returns the signing algorithm extracted based on signing alias configured from keystore. - * @return signing algorithm. - */ - get() { - val signKey = oauthConfig.signingKeyAlias - logger.debug("Using JWT signing key {}", signKey) - val keyPair = getKeyPair(signKey) ?: throw IllegalArgumentException( - "Cannot load JWT signing key " + signKey + " from JWT key store." - ) - return getAlgorithmFromKeyPair(keyPair) - } - - /** - * Get a key pair from the store using the store password. - * @param alias key pair alias - * @return loaded key pair or `null` if the key store does not contain a loadable key with - * given alias. - * @throws IllegalArgumentException if the key alias password is wrong or the key cannot - * loaded. - */ - private fun getKeyPair(alias: String): KeyPair? { - return getKeyPair(alias, password) - } - - /** - * Get a key pair from the store with a given alias and password. - * @param alias key pair alias - * @param password key pair password - * @return loaded key pair or `null` if the key store does not contain a loadable key with - * given alias. - * @throws IllegalArgumentException if the key alias password is wrong or the key cannot - * load. - */ - private fun getKeyPair(alias: String, password: CharArray): KeyPair? { - return try { - val key = store.getKey(alias, password) as PrivateKey? - if (key == null) { - logger.warn( - "JWT key store {} does not contain private key pair for alias {}", loadedResource, alias - ) - return null - } - val cert = store.getCertificate(alias) - if (cert == null) { - logger.warn( - "JWT key store {} does not contain certificate pair for alias {}", loadedResource, alias - ) - return null - } - val publicKey = cert.publicKey - if (publicKey == null) { - logger.warn( - "JWT key store {} does not contain public key pair for alias {}", loadedResource, alias - ) - return null - } - KeyPair(publicKey, key) - } catch (ex: NoSuchAlgorithmException) { - logger.warn( - "JWT key store {} contains unknown algorithm for key pair with alias {}: {}", - loadedResource, - alias, - ex.toString() - ) - null - } catch (ex: UnrecoverableKeyException) { - throw IllegalArgumentException( - "JWT key store $loadedResource contains unrecoverable key pair with alias $alias (the password may be wrong)", - ex - ) - } catch (ex: KeyStoreException) { - throw IllegalArgumentException( - "JWT key store $loadedResource contains unrecoverable key pair with alias $alias (the password may be wrong)", - ex - ) - } - } - - val tokenValidator: TokenValidator - /** Get the default token validator. */ - get() { - val loaderList = listOf( - JwksTokenVerifierLoader( - managementPortalBaseUrl + "/oauth/token_key", - RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ), - managementPortalProperties.authServer.let { - JwksTokenVerifierLoader( - it.serverAdminUrl + "/admin/keys/hydra.jwt.access-token", - RES_MANAGEMENT_PORTAL, - JwkAlgorithmParser() - ) - }, - ) - return TokenValidator(loaderList) - } - - companion object { - private val logger = LoggerFactory.getLogger( - ManagementPortalOauthKeyStoreHandler::class.java - ) - private val KEYSTORE_PATHS = listOf( - ClassPathResource("/config/keystore.p12"), ClassPathResource("/config/keystore.jks") - ) - - private fun checkOAuthConfig(managementPortalProperties: ManagementPortalProperties) { - val oauthConfig = managementPortalProperties.oauth - if (oauthConfig.keyStorePassword.isEmpty()) { - logger.error("oauth.keyStorePassword is empty") - throw IllegalArgumentException("oauth.keyStorePassword is empty") - } - if (oauthConfig.signingKeyAlias == null || oauthConfig.signingKeyAlias!!.isEmpty()) { - logger.error("oauth.signingKeyAlias is empty") - throw IllegalArgumentException("OauthConfig is not provided") - } - } - - /** - * Returns extracted [Algorithm] from the KeyPair. - * @param keyPair to find algorithm. - * @return extracted algorithm. - */ - private fun getAlgorithmFromKeyPair(keyPair: KeyPair): Algorithm { - val alg = getJwtAlgorithm(keyPair) ?: throw IllegalArgumentException( - "KeyPair type " + keyPair.private.algorithm + " is unknown." - ) - return alg.algorithm - } - - /** - * Get the JWT algorithm to sign or verify JWTs with. - * @param keyPair key pair for signing/verifying. - * @return algorithm or `null` if the key type is unknown. - */ - private fun getJwtAlgorithm(keyPair: KeyPair?): JwtAlgorithm? { - if (keyPair == null) { - return null - } - val privateKey = keyPair.private - return when (privateKey) { - is ECPrivateKey -> { - EcdsaJwtAlgorithm(keyPair) - } - - is RSAPrivateKey -> { - RsaJwtAlgorithm(keyPair) - } - - else -> { - logger.warn( - "No JWT algorithm found for key type {}", privateKey.javaClass - ) - null - } - } - } - } -} diff --git a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt index e85db0c1b..2e7e5c400 100644 --- a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt @@ -2,7 +2,6 @@ package org.radarbase.management.web.rest import io.micrometer.core.annotation.Timed import org.radarbase.auth.jwks.JsonWebKeySet -import org.radarbase.management.security.jwt.ManagementPortalOauthKeyStoreHandler import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping @@ -10,11 +9,10 @@ import org.springframework.web.bind.annotation.RestController //@RestController class TokenKeyEndpoint @Autowired constructor( - private val keyStoreHandler: ManagementPortalOauthKeyStoreHandler ) { @get:Timed @get:GetMapping("/oauth/token_key") - val key: JsonWebKeySet + val key: JsonWebKeySet? /** * Get the verification key for the token signatures. The principal has to * be provided only if the key is secret @@ -23,7 +21,7 @@ class TokenKeyEndpoint @Autowired constructor( */ get() { logger.debug("Requesting verifier public keys...") - return keyStoreHandler.loadJwks() + return null } companion object { diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index 76a54b5b7..13a6d3fd2 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -101,7 +101,7 @@ managementportal: from: ManagementPortal@localhost frontend: clientId: ManagementPortalapp - clientSecret: my-secret-token-to-change-in-production + clientSecret: secret accessTokenValiditySeconds: 14400 refreshTokenValiditySeconds: 259200 sessionTimeout : 86400 # session for rft cookie diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 436de6e62..01ae180aa 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -89,7 +89,7 @@ managementportal: from: ManagementPortal@localhost frontend: clientId: ManagementPortalapp - clientSecret: + clientSecret: secret accessTokenValiditySeconds: 14400 refreshTokenValiditySeconds: 259200 sessionTimeout: 86400 From 7600ae8f0fd341de76d87717358bf4e59a2d70be Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 22 Aug 2024 23:53:47 +0100 Subject: [PATCH 23/68] Refactor JwtAuthenticationFilter to accept jwt and session data --- .../config/SecurityConfiguration.kt | 2 +- .../security/JwtAuthenticationFilter.kt | 280 +++++++----------- .../auth/authentication/OAuthHelper.kt | 2 +- 3 files changed, 105 insertions(+), 179 deletions(-) diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 3a05aae8f..446f691b4 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -75,7 +75,7 @@ class SecurityConfiguration @Bean fun jwtAuthenticationFilter(): JwtAuthenticationFilter = - JwtAuthenticationFilter(tokenValidator, authenticationManager(), userRepository) + JwtAuthenticationFilter(tokenValidator, authenticationManager()) .skipUrlPattern(HttpMethod.GET, "/management/health") .skipUrlPattern(HttpMethod.POST, "/oauth/token") .skipUrlPattern(HttpMethod.GET, "/api/meta-token/*") diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index 9679cd9cc..e665cfed1 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -1,249 +1,175 @@ package org.radarbase.management.security import io.ktor.http.* +import java.io.IOException +import java.time.Instant +import javax.annotation.Nonnull +import javax.servlet.FilterChain +import javax.servlet.ServletException +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse +import javax.servlet.http.HttpSession import org.radarbase.auth.authentication.TokenValidator -import org.radarbase.auth.authorization.AuthorityReference -import org.radarbase.auth.authorization.RoleAuthority import org.radarbase.auth.exception.TokenValidationException import org.radarbase.auth.token.RadarToken -import org.radarbase.management.domain.Role -import org.radarbase.management.domain.User -import org.radarbase.management.repository.UserRepository -import org.radarbase.management.web.rest.util.HeaderUtil.parseCookies import org.slf4j.LoggerFactory import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.core.Authentication import org.springframework.security.core.context.SecurityContextHolder -import org.springframework.security.oauth2.provider.OAuth2Authentication import org.springframework.security.web.util.matcher.AntPathRequestMatcher import org.springframework.web.cors.CorsUtils import org.springframework.web.filter.OncePerRequestFilter -import java.io.IOException -import java.time.Instant -import javax.annotation.Nonnull -import javax.servlet.FilterChain -import javax.servlet.ServletException -import javax.servlet.http.HttpServletRequest -import javax.servlet.http.HttpServletResponse -import javax.servlet.http.HttpSession - -/** - * Authentication filter using given validator. - * @param validator validates the JWT token. - * @param authenticationManager authentication manager to pass valid authentication to. - * @param userRepository user repository to retrieve user details from. - * @param isOptional do not fail if no authentication is provided - */ -class JwtAuthenticationFilter @JvmOverloads constructor( - private val validator: TokenValidator, - private val authenticationManager: AuthenticationManager, - private val userRepository: UserRepository, - private val isOptional: Boolean = false +class JwtAuthenticationFilter( + private val validator: TokenValidator, + private val authenticationManager: AuthenticationManager, + private val isOptional: Boolean = false ) : OncePerRequestFilter() { + private val ignoreUrls: MutableList = mutableListOf() - /** - * Do not use JWT authentication for given paths and HTTP method. - * @param method HTTP method - * @param antPatterns Ant wildcard pattern - * @return the current filter - */ fun skipUrlPattern(method: HttpMethod, vararg antPatterns: String?): JwtAuthenticationFilter { - for (pattern in antPatterns) { - ignoreUrls.add(AntPathRequestMatcher(pattern, method.name)) + antPatterns.forEach { pattern -> + pattern?.let { ignoreUrls.add(AntPathRequestMatcher(it, method.name)) } } return this } @Throws(IOException::class, ServletException::class) override fun doFilterInternal( - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - chain: FilterChain, + httpRequest: HttpServletRequest, + httpResponse: HttpServletResponse, + chain: FilterChain, ) { - Companion.logger.warn("Request: {}", httpServletRequestToString(httpRequest)) + logger.debug("Processing request: ${httpRequest.requestURI}") + if (CorsUtils.isPreFlightRequest(httpRequest)) { - Companion.logger.debug("Skipping JWT check for preflight request") + logger.debug("Skipping JWT check for preflight request") chain.doFilter(httpRequest, httpResponse) return } val existingAuthentication = SecurityContextHolder.getContext().authentication + val stringToken = tokenFromHeader(httpRequest) + var token: RadarToken? = null - if (existingAuthentication.isAnonymous || existingAuthentication is OAuth2Authentication) { - val session = httpRequest.getSession(false) - val stringToken = tokenFromHeader(httpRequest) - var token: RadarToken? = null - var exMessage = "No token provided" - if (stringToken != null) { - try { - Companion.logger.warn("Validating token from header: {}", stringToken) - token = validator.validateBlocking(stringToken) - Companion.logger.debug("Using token from header") - } catch (ex: TokenValidationException) { - ex.message?.let { exMessage = it } - Companion.logger.info("Failed to validate token from header: {}", exMessage) - } - } - if (token == null) { - token = session?.radarToken - ?.takeIf { Instant.now() < it.expiresAt } - if (token != null) { - Companion.logger.debug("Using token from session") - } - } - if (!validateToken(token, httpRequest, httpResponse, session, exMessage)) { - return - } + if (stringToken != null) { + token = validateTokenFromHeader(stringToken, httpRequest) + } + + if (token == null && existingAuthentication.isAnonymous) { + token = validateTokenFromSession(httpRequest.session) } - chain.doFilter(httpRequest, httpResponse) - } - private fun httpServletRequestToString(httpRequest: HttpServletRequest):String { - val buffer = StringBuffer() - buffer.append("Method: ${httpRequest.method}\n") - buffer.append("RequestURI: ${httpRequest.requestURI}\n") - buffer.append("QueryString: ${httpRequest.queryString}\n") - buffer.append("RequestURL: ${httpRequest.requestURL}\n") - buffer.append("Protocol: ${httpRequest.protocol}\n") - buffer.append("Scheme: ${httpRequest.scheme}\n") - buffer.append("ServerName: ${httpRequest.serverName}\n") - buffer.append("ServerPort: ${httpRequest.serverPort}\n") - buffer.append("RemoteAddr: ${httpRequest.remoteAddr}\n") - buffer.append("RemoteHost: ${httpRequest.remoteHost}\n") - buffer.append("RemotePort: ${httpRequest.remotePort}\n") - buffer.append("LocalAddr: ${httpRequest.localAddr}\n") - buffer.append("LocalName: ${httpRequest.localName}\n") - buffer.append("LocalPort: ${httpRequest.localPort}\n") - buffer.append("AuthType: ${httpRequest.authType}\n") - buffer.append("ContentType: ${httpRequest.contentType}\n") - buffer.append("ContentLength: ${httpRequest.contentLength}\n") - buffer.append("CharacterEncoding: ${httpRequest.characterEncoding}\n") - buffer.append("Cookies: ${httpRequest.cookies}\n") - buffer.append("Headers: ${httpRequest.headerNames.toList().map { it to httpRequest.getHeaders(it).toList() }}\n") - buffer.append("Attributes: ${httpRequest.attributeNames.toList().map { it to httpRequest.getAttribute(it) }}\n") - buffer.append("Parameters: ${httpRequest.parameterMap.toList().map { it.first to it.second.toList() }}\n") - return buffer.toString() + if (!validateAndSetAuthentication(token, httpRequest, httpResponse)) { + return + } + + chain.doFilter(httpRequest, httpResponse) } - override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { - val shouldNotFilterUrl = ignoreUrls.find { it.matches(httpRequest) } - return if (shouldNotFilterUrl != null) { - Companion.logger.debug("Skipping JWT check for {} request", shouldNotFilterUrl) - true - } else { - false + private fun validateTokenFromHeader( + tokenString: String, + httpRequest: HttpServletRequest + ): RadarToken? { + return try { + logger.debug("Validating token from header: ${tokenString}") + val token = validator.validateBlocking(tokenString) + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + logger.debug("JWT authentication successful") + token + } catch (ex: TokenValidationException) { + logger.warn("Token validation failed: ${ex.message}") + null } } - private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { - Companion.logger.warn("Token from header: {}", httpRequest.getHeader(HttpHeaders.AUTHORIZATION)) - return httpRequest.getHeader(HttpHeaders.AUTHORIZATION) - ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } - ?.removePrefix(AUTHORIZATION_BEARER_HEADER) - ?.trim { it <= ' ' } - ?: parseCookies(httpRequest.getHeader(HttpHeaders.COOKIE)).find { it.name == "ory_kratos_session" } - ?.value + private fun validateTokenFromSession(session: HttpSession?): RadarToken? { + val token = session?.radarToken?.takeIf { Instant.now() < it.expiresAt } + if (token != null) { + logger.debug("Using token from session") + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + } + return token } - @Throws(IOException::class) - private fun validateToken( - token: RadarToken?, - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - session: HttpSession?, - exMessage: String?, + private fun validateAndSetAuthentication( + token: RadarToken?, + httpRequest: HttpServletRequest, + httpResponse: HttpServletResponse ): Boolean { return if (token != null) { - val updatedToken = checkUser(token, httpRequest, httpResponse, session) - ?: return false - httpRequest.radarToken = updatedToken - val authentication = RadarAuthentication(updatedToken) - authenticationManager.authenticate(authentication) + httpRequest.radarToken = token + val authentication = createAuthenticationFromToken(token) SecurityContextHolder.getContext().authentication = authentication true - } else if (isOptional) { - logger.debug("Skipping optional token") - true } else { - logger.error("Unauthorized - no valid token provided") - httpResponse.returnUnauthorized(httpRequest, exMessage) + handleUnauthorized(httpRequest, httpResponse, "No valid token provided") false } } - @Throws(IOException::class) - private fun checkUser( - token: RadarToken, - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - session: HttpSession?, - ): RadarToken? { - val userName = token.username ?: return token - val user = userRepository.findOneByLogin(userName) - return if (user != null) { - token.copyWithRoles(user.authorityReferences) - } else { - session?.removeAttribute(TOKEN_ATTRIBUTE) - httpResponse.returnUnauthorized(httpRequest, "User not found") - null + private fun handleUnauthorized( + httpRequest: HttpServletRequest, + httpResponse: HttpServletResponse, + message: String + ) { + if (!isOptional) { + logger.error("Unauthorized - ${message}") + httpResponse.returnUnauthorized(httpRequest, message) + } + } + + private fun createAuthenticationFromToken(token: RadarToken): Authentication { + val authentication = RadarAuthentication(token) + return authenticationManager.authenticate(authentication) + } + + override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { + return ignoreUrls.any { it.matches(httpRequest) }.also { shouldSkip -> + if (shouldSkip) { + logger.debug("Skipping JWT check for ${httpRequest.requestURL}") + } } } + private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { + return httpRequest + .getHeader(HttpHeaders.AUTHORIZATION) + ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } + ?.removePrefix(AUTHORIZATION_BEARER_HEADER) + ?.trim() + } + companion object { - private fun HttpServletResponse.returnUnauthorized(request: HttpServletRequest, message: String?) { + private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) + private const val AUTHORIZATION_BEARER_HEADER = "Bearer" + private const val TOKEN_ATTRIBUTE = "jwt" + + private fun HttpServletResponse.returnUnauthorized( + request: HttpServletRequest, + message: String? + ) { status = HttpServletResponse.SC_UNAUTHORIZED setHeader(HttpHeaders.WWW_AUTHENTICATE, AUTHORIZATION_BEARER_HEADER) - val fullMessage = if (message != null) { - "\"$message\"" - } else { - "null" - } outputStream.print( - """ + """ {"error": "Unauthorized", "status": "${HttpServletResponse.SC_UNAUTHORIZED}", - message": $fullMessage, + "message": "${message ?: "null"}", "path": "${request.requestURI}"} """.trimIndent() ) } - private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) - private const val AUTHORIZATION_BEARER_HEADER = "Bearer" - private const val TOKEN_ATTRIBUTE = "jwt" - private const val TOKEN_COOKIE_NAME = "ory_kratos_session" - - /** - * Authority references for given user. The user should have its roles mapped - * from the database. - * @return set of authority references. - */ - val User.authorityReferences: Set - get() = roles.mapTo(HashSet()) { role: Role? -> - val auth = role?.role - val referent = when (auth?.scope) { - RoleAuthority.Scope.GLOBAL -> null - RoleAuthority.Scope.ORGANIZATION -> role.organization?.name - RoleAuthority.Scope.PROJECT -> role.project?.projectName - null -> null - } - AuthorityReference(auth!!, referent) - } - - - - @get:JvmStatic - @set:JvmStatic var HttpSession.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) - @get:JvmStatic - @set:JvmStatic var HttpServletRequest.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) @@ -252,7 +178,7 @@ class JwtAuthenticationFilter @JvmOverloads constructor( get() { this ?: return true return authorities.size == 1 && - authorities.firstOrNull()?.authority == "ROLE_ANONYMOUS" + authorities.firstOrNull()?.authority == "ROLE_ANONYMOUS" } } } diff --git a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt index 0ab493a0f..5aa0cd8e7 100644 --- a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt +++ b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt @@ -128,7 +128,7 @@ object OAuthHelper { Mockito.`when`(userRepository.findOneByLogin(ArgumentMatchers.anyString())).thenReturn( createAdminUser() ) - return JwtAuthenticationFilter(createTokenValidator(), { auth: Authentication? -> auth }, userRepository) + return JwtAuthenticationFilter(createTokenValidator(), { auth: Authentication? -> auth }) } /** From 54aa7ff981a80c88b09d7e5c1157b566de9d389a Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 22 Aug 2024 23:58:42 +0100 Subject: [PATCH 24/68] Remove unused services --- build.gradle | 4 +- ...ManagementPortalJwtAccessTokenConverter.kt | 251 ----------------- .../jwt/ManagementPortalJwtTokenStore.kt | 189 ------------- .../management/service/MetaTokenService.kt | 252 ------------------ .../management/service/OAuthClientService.kt | 175 ------------ .../decorator/ProjectMapperDecorator.kt | 7 - .../management/web/rest/MetaTokenResource.kt | 91 ------- .../web/rest/OAuthClientsResource.kt | 227 ---------------- .../auth/authentication/OAuthHelper.kt | 6 +- .../service/MetaTokenServiceTest.kt | 136 ---------- .../web/rest/OAuthClientsResourceIntTest.kt | 212 --------------- 11 files changed, 4 insertions(+), 1546 deletions(-) delete mode 100644 src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt delete mode 100644 src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtTokenStore.kt delete mode 100644 src/main/java/org/radarbase/management/service/MetaTokenService.kt delete mode 100644 src/main/java/org/radarbase/management/service/OAuthClientService.kt delete mode 100644 src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt delete mode 100644 src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt delete mode 100644 src/test/java/org/radarbase/management/service/MetaTokenServiceTest.kt delete mode 100644 src/test/java/org/radarbase/management/web/rest/OAuthClientsResourceIntTest.kt diff --git a/build.gradle b/build.gradle index 83c1dbf69..b76d738a9 100644 --- a/build.gradle +++ b/build.gradle @@ -175,7 +175,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-autoconfigure") implementation "org.springframework.boot:spring-boot-starter-mail" runtimeOnly "org.springframework.boot:spring-boot-starter-logging" - runtimeOnly ("org.springframework.boot:spring-boot-starter-data-jpa") { + runtimeOnly("org.springframework.boot:spring-boot-starter-data-jpa") { exclude group: 'org.hibernate', module: 'hibernate-entitymanager' } implementation "org.springframework.security:spring-security-data" @@ -184,7 +184,7 @@ dependencies { exclude module: 'spring-boot-starter-tomcat' } runtimeOnly "org.springframework.boot:spring-boot-starter-security" - implementation ("org.springframework.boot:spring-boot-starter-undertow") + implementation("org.springframework.boot:spring-boot-starter-undertow") implementation "org.hibernate:hibernate-core" implementation "org.hibernate:hibernate-envers" diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt deleted file mode 100644 index 7a997ba36..000000000 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtAccessTokenConverter.kt +++ /dev/null @@ -1,251 +0,0 @@ -package org.radarbase.management.security.jwt - -import com.auth0.jwt.JWT -import com.auth0.jwt.JWTVerifier -import com.auth0.jwt.algorithms.Algorithm -import com.auth0.jwt.exceptions.JWTVerificationException -import com.auth0.jwt.exceptions.SignatureVerificationException -import com.fasterxml.jackson.core.JsonProcessingException -import com.fasterxml.jackson.databind.ObjectMapper -import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter -import org.radarbase.auth.authentication.TokenValidator -import org.slf4j.LoggerFactory -import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken -import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken -import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken -import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken -import org.springframework.security.oauth2.common.OAuth2AccessToken -import org.springframework.security.oauth2.common.exceptions.InvalidTokenException -import org.springframework.security.oauth2.provider.OAuth2Authentication -import org.springframework.security.oauth2.provider.token.AccessTokenConverter -import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter -import org.springframework.security.oauth2.provider.token.store.JwtClaimsSetVerifier -import org.springframework.util.Assert -import java.nio.charset.StandardCharsets -import java.time.Instant -import java.util.* -import java.util.stream.Stream -import org.radarbase.auth.exception.TokenValidationException - -/** - * Implementation of [JwtAccessTokenConverter] for the RADAR-base ManagementPortal platform. - * - * - * This class can accept an EC keypair as well as an RSA keypair for signing. EC signatures - * are significantly smaller than RSA signatures. - */ -open class ManagementPortalJwtAccessTokenConverter( - validator: TokenValidator, -// algorithm: Algorithm, -// verifiers: MutableList, -// private val refreshTokenVerifiers: List -) : JwtAccessTokenConverter { - private val jsonParser = ObjectMapper().readerFor( - MutableMap::class.java - ) - private val tokenConverter: AccessTokenConverter - - /** - * Returns JwtClaimsSetVerifier. - * - * @return the [JwtClaimsSetVerifier] used to verify the claim(s) in the JWT Claims Set - */ - var jwtClaimsSetVerifier: JwtClaimsSetVerifier? = null - /** - * Sets JwtClaimsSetVerifier instance. - * - * @param jwtClaimsSetVerifier the [JwtClaimsSetVerifier] used to verify the claim(s) - * in the JWT Claims Set - */ - set(jwtClaimsSetVerifier) { - Assert.notNull(jwtClaimsSetVerifier, "jwtClaimsSetVerifier cannot be null") - field = jwtClaimsSetVerifier - } - private var algorithm: Algorithm? = null - private var validator: TokenValidator - //private val verifiers: MutableList - - /** - * Default constructor. - * Creates [ManagementPortalJwtAccessTokenConverter] with - * [DefaultAccessTokenConverter] as the accessTokenConverter with explicitly including - * grant_type claim. - */ - init { - val accessToken = DefaultAccessTokenConverter() - accessToken.setIncludeGrantType(true) - tokenConverter = accessToken - //this.verifiers = verifiers - this.validator = validator - //setAlgorithm(algorithm) - } - - override fun convertAccessToken( - token: OAuth2AccessToken, - authentication: OAuth2Authentication - ): Map { - return tokenConverter.convertAccessToken(token, authentication) - } - - override fun extractAccessToken(value: String, map: Map?): OAuth2AccessToken { - var mapCopy = map?.toMutableMap() - - if (mapCopy?.containsKey(AccessTokenConverter.EXP) == true) { - mapCopy[AccessTokenConverter.EXP] = (mapCopy[AccessTokenConverter.EXP] as Int).toLong() - } - return tokenConverter.extractAccessToken(value, mapCopy) - } - - override fun extractAuthentication(map: Map?): OAuth2Authentication { - return tokenConverter.extractAuthentication(map) - } - - override fun setAlgorithm(algorithm: Algorithm) { - this.algorithm = algorithm -// if (verifiers.isEmpty()) { -// verifiers.add(JWT.require(algorithm).withAudience(RES_MANAGEMENT_PORTAL).build()) -// } - } - - /** - * Simplified the existing enhancing logic of - * [JwtAccessTokenConverter.enhance]. - * Keeping the same logic. - * - * - * - * It mainly adds token-id for access token and access-token-id and token-id for refresh - * token to the additional information. - * - * - * @param accessToken accessToken to enhance. - * @param authentication current authentication of the token. - * @return enhancedToken. - */ - override fun enhance( - accessToken: OAuth2AccessToken, - authentication: OAuth2Authentication - ): OAuth2AccessToken { - // create new instance of token to enhance - val resultAccessToken = DefaultOAuth2AccessToken(accessToken) - // set additional information for access token - val additionalInfoAccessToken: MutableMap = HashMap(accessToken.additionalInformation) - - // add token id if not available - var accessTokenId = accessToken.value - if (!additionalInfoAccessToken.containsKey(JwtAccessTokenConverter.TOKEN_ID)) { - additionalInfoAccessToken[JwtAccessTokenConverter.TOKEN_ID] = accessTokenId - } else { - accessTokenId = additionalInfoAccessToken[JwtAccessTokenConverter.TOKEN_ID] as String? - } - resultAccessToken.additionalInformation = additionalInfoAccessToken - resultAccessToken.value = encode(accessToken, authentication) - - // add additional information for refresh-token - val refreshToken = accessToken.refreshToken - if (refreshToken != null) { - val refreshTokenToEnhance = DefaultOAuth2AccessToken(accessToken) - refreshTokenToEnhance.value = refreshToken.value - // Refresh tokens do not expire unless explicitly of the right type - refreshTokenToEnhance.expiration = null - refreshTokenToEnhance.scope = accessToken.scope - // set info of access token to refresh-token and add token-id and access-token-id for - // reference. - val refreshTokenInfo: MutableMap = HashMap(accessToken.additionalInformation) - refreshTokenInfo[JwtAccessTokenConverter.TOKEN_ID] = refreshTokenToEnhance.value - refreshTokenInfo[JwtAccessTokenConverter.ACCESS_TOKEN_ID] = accessTokenId - refreshTokenToEnhance.additionalInformation = refreshTokenInfo - val encodedRefreshToken: DefaultOAuth2RefreshToken - if (refreshToken is ExpiringOAuth2RefreshToken) { - val expiration = refreshToken.expiration - refreshTokenToEnhance.expiration = expiration - encodedRefreshToken = DefaultExpiringOAuth2RefreshToken( - encode(refreshTokenToEnhance, authentication), expiration - ) - } else { - encodedRefreshToken = DefaultOAuth2RefreshToken( - encode(refreshTokenToEnhance, authentication) - ) - } - resultAccessToken.refreshToken = encodedRefreshToken - } - return resultAccessToken - } - - override fun isRefreshToken(token: OAuth2AccessToken): Boolean { - return token.additionalInformation?.containsKey(JwtAccessTokenConverter.ACCESS_TOKEN_ID) == true - } - - override fun encode(accessToken: OAuth2AccessToken, authentication: OAuth2Authentication): String { - // we need to override the encode method as well, Spring security does not know about - // ECDSA, so it can not set the 'alg' header claim of the JWT to the correct value; here - // we use the auth0 JWT implementation to create a signed, encoded JWT. - val claims = convertAccessToken(accessToken, authentication) - val builder = JWT.create() - - // add the string array claims - Stream.of("aud", "sources", "roles", "authorities", "scope") - .filter { key: String -> claims.containsKey(key) } - .forEach { claim: String -> - builder.withArrayClaim( - claim, - (claims[claim] as Collection).toTypedArray() - ) - } - - // add the string claims - Stream.of("sub", "iss", "user_name", "client_id", "grant_type", "jti", "ati") - .filter { key: String -> claims.containsKey(key) } - .forEach { claim: String -> builder.withClaim(claim, claims[claim] as String?) } - - // add the date claims, they are in seconds since epoch, we need milliseconds - Stream.of("exp", "iat") - .filter { key: String -> claims.containsKey(key) } - .forEach { claim: String -> - builder.withClaim( - claim, - Date.from(Instant.ofEpochSecond((claims[claim] as Long?)!!)) - ) - } - return builder.sign(algorithm) - } - - override fun decode(token: String): Map { - val jwt = JWT.decode(token) -// val verifierToUse: List - val claims: MutableMap - try { - val decodedPayload = String( - Base64.getUrlDecoder().decode(jwt.payload), - StandardCharsets.UTF_8 - ) - claims = jsonParser.readValue(decodedPayload) - if (claims.containsKey(AccessTokenConverter.EXP) && claims[AccessTokenConverter.EXP] is Int) { - val intValue = claims[AccessTokenConverter.EXP] as Int? - claims[AccessTokenConverter.EXP] = intValue!! - } - if (jwtClaimsSetVerifier != null) { - jwtClaimsSetVerifier!!.verify(claims) - } -// verifierToUse = -// if (claims[JwtAccessTokenConverter.ACCESS_TOKEN_ID] != null) refreshTokenVerifiers else verifiers - } catch (ex: JsonProcessingException) { - throw InvalidTokenException("Invalid token", ex) - } - try { - validator.validateBlocking(token) - logger.debug("Using token from header") - return claims - } catch (ex: TokenValidationException) { - ex.message?.let { - logger.info("Failed to validate token from header: {}", it) - } - } - throw InvalidTokenException("No registered validator could authenticate this token") - } - - companion object { - const val RES_MANAGEMENT_PORTAL = "res_ManagementPortal" - private val logger = LoggerFactory.getLogger(ManagementPortalJwtAccessTokenConverter::class.java) - } -} diff --git a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtTokenStore.kt b/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtTokenStore.kt deleted file mode 100644 index 512b66433..000000000 --- a/src/main/java/org/radarbase/management/security/jwt/ManagementPortalJwtTokenStore.kt +++ /dev/null @@ -1,189 +0,0 @@ -package org.radarbase.management.security.jwt - -import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken -import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken -import org.springframework.security.oauth2.common.OAuth2AccessToken -import org.springframework.security.oauth2.common.OAuth2RefreshToken -import org.springframework.security.oauth2.common.exceptions.InvalidTokenException -import org.springframework.security.oauth2.provider.OAuth2Authentication -import org.springframework.security.oauth2.provider.approval.Approval -import org.springframework.security.oauth2.provider.approval.Approval.ApprovalStatus -import org.springframework.security.oauth2.provider.approval.ApprovalStore -import org.springframework.security.oauth2.provider.token.TokenStore -import java.util.* - -/** - * Adapted version of [org.springframework.security.oauth2.provider.token.store.JwtTokenStore] - * which uses interface [JwtAccessTokenConverter] instead of tied instance. - * - * - * - * A [TokenStore] implementation that just reads data from the tokens themselves. - * Not really a store since it never persists anything, and methods like - * [.getAccessToken] always return null. But - * nevertheless a useful tool since it translates access tokens to and - * from authentications. Use this wherever a[TokenStore] is needed, - * but remember to use the same [JwtAccessTokenConverter] - * instance (or one with the same verifier) as was used when the tokens were minted. - * - * - * @author Dave Syer - * @author nivethika - */ -class ManagementPortalJwtTokenStore : TokenStore { - private val jwtAccessTokenConverter: JwtAccessTokenConverter - private var approvalStore: ApprovalStore? = null - - /** - * Create a ManagementPortalJwtTokenStore with this token converter - * (should be shared with the DefaultTokenServices if used). - * - * @param jwtAccessTokenConverter JwtAccessTokenConverter used in the application. - */ - constructor(jwtAccessTokenConverter: JwtAccessTokenConverter) { - this.jwtAccessTokenConverter = jwtAccessTokenConverter - } - - /** - * Create a ManagementPortalJwtTokenStore with this token converter - * (should be shared with the DefaultTokenServices if used). - * - * @param jwtAccessTokenConverter JwtAccessTokenConverter used in the application. - * @param approvalStore TokenApprovalStore used in the application. - */ - constructor( - jwtAccessTokenConverter: JwtAccessTokenConverter, - approvalStore: ApprovalStore? - ) { - this.jwtAccessTokenConverter = jwtAccessTokenConverter - this.approvalStore = approvalStore - } - - /** - * ApprovalStore to be used to validate and restrict refresh tokens. - * - * @param approvalStore the approvalStore to set - */ - fun setApprovalStore(approvalStore: ApprovalStore?) { - this.approvalStore = approvalStore - } - - override fun readAuthentication(token: OAuth2AccessToken): OAuth2Authentication { - return readAuthentication(token.value) - } - - override fun readAuthentication(token: String): OAuth2Authentication { - return jwtAccessTokenConverter.extractAuthentication(jwtAccessTokenConverter.decode(token)) - } - - override fun storeAccessToken(token: OAuth2AccessToken, authentication: OAuth2Authentication) { - // this is not really a store where we persist - } - - override fun readAccessToken(tokenValue: String): OAuth2AccessToken { - val accessToken = convertAccessToken(tokenValue) - if (jwtAccessTokenConverter.isRefreshToken(accessToken)) { - throw InvalidTokenException("Encoded token is a refresh token") - } - return accessToken - } - - private fun convertAccessToken(tokenValue: String): OAuth2AccessToken { - return jwtAccessTokenConverter - .extractAccessToken(tokenValue, jwtAccessTokenConverter.decode(tokenValue)) - } - - override fun removeAccessToken(token: OAuth2AccessToken) { - // this is not really store where we persist - } - - override fun storeRefreshToken( - refreshToken: OAuth2RefreshToken, - authentication: OAuth2Authentication - ) { - // this is not really store where we persist - } - - override fun readRefreshToken(tokenValue: String): OAuth2RefreshToken? { - if (approvalStore != null) { - val authentication = readAuthentication(tokenValue) - if (authentication.userAuthentication != null) { - val userId = authentication.userAuthentication.name - val clientId = authentication.oAuth2Request.clientId - val approvals = approvalStore!!.getApprovals(userId, clientId) - val approvedScopes: MutableCollection = HashSet() - for (approval in approvals) { - if (approval.isApproved) { - approvedScopes.add(approval.scope) - } - } - if (!approvedScopes.containsAll(authentication.oAuth2Request.scope)) { - return null - } - } - } - val encodedRefreshToken = convertAccessToken(tokenValue) - return createRefreshToken(encodedRefreshToken) - } - - private fun createRefreshToken(encodedRefreshToken: OAuth2AccessToken): OAuth2RefreshToken { - if (!jwtAccessTokenConverter.isRefreshToken(encodedRefreshToken)) { - throw InvalidTokenException("Encoded token is not a refresh token") - } - return if (encodedRefreshToken.expiration != null) { - DefaultExpiringOAuth2RefreshToken( - encodedRefreshToken.value, - encodedRefreshToken.expiration - ) - } else DefaultOAuth2RefreshToken(encodedRefreshToken.value) - } - - override fun readAuthenticationForRefreshToken(token: OAuth2RefreshToken): OAuth2Authentication { - return readAuthentication(token.value) - } - - override fun removeRefreshToken(token: OAuth2RefreshToken) { - remove(token.value) - } - - private fun remove(token: String) { - if (approvalStore != null) { - val auth = readAuthentication(token) - val clientId = auth.oAuth2Request.clientId - val user = auth.userAuthentication - if (user != null) { - val approvals: MutableCollection = ArrayList() - for (scope in auth.oAuth2Request.scope) { - approvals.add( - Approval( - user.name, clientId, scope, Date(), - ApprovalStatus.APPROVED - ) - ) - } - approvalStore!!.revokeApprovals(approvals) - } - } - } - - override fun removeAccessTokenUsingRefreshToken(refreshToken: OAuth2RefreshToken) { - // this is not really store where we persist - } - - override fun getAccessToken(authentication: OAuth2Authentication): OAuth2AccessToken? { - // We don't want to accidentally issue a token, and we have no way to reconstruct - // the refresh token - return null - } - - override fun findTokensByClientIdAndUserName( - clientId: String, - userName: String - ): Collection { - return emptySet() - } - - override fun findTokensByClientId(clientId: String): Collection { - return emptySet() - } -} diff --git a/src/main/java/org/radarbase/management/service/MetaTokenService.kt b/src/main/java/org/radarbase/management/service/MetaTokenService.kt deleted file mode 100644 index dcbdb0e3d..000000000 --- a/src/main/java/org/radarbase/management/service/MetaTokenService.kt +++ /dev/null @@ -1,252 +0,0 @@ -package org.radarbase.management.service - -import org.radarbase.management.config.ManagementPortalProperties -import org.radarbase.management.domain.MetaToken -import org.radarbase.management.domain.Project -import org.radarbase.management.domain.Subject -import org.radarbase.management.repository.MetaTokenRepository -import org.radarbase.management.security.NotAuthorizedException -import org.radarbase.management.service.dto.ClientPairInfoDTO -import org.radarbase.management.service.dto.TokenDTO -import org.radarbase.management.web.rest.MetaTokenResource -import org.radarbase.management.web.rest.errors.BadRequestException -import org.radarbase.management.web.rest.errors.EntityName -import org.radarbase.management.web.rest.errors.ErrorConstants -import org.radarbase.management.web.rest.errors.InvalidStateException -import org.radarbase.management.web.rest.errors.NotFoundException -import org.radarbase.management.web.rest.errors.RequestGoneException -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import java.net.MalformedURLException -import java.net.URISyntaxException -import java.net.URL -import java.time.Duration -import java.time.Instant -import java.time.format.DateTimeParseException -import java.util.* -import java.util.function.Consumer -import javax.validation.ConstraintViolationException - -/** - * Created by nivethika. - * - * - * Service to delegate MetaToken handling. - * - */ -//@Service -//@Transactional -class MetaTokenService { - @Autowired - private val metaTokenRepository: MetaTokenRepository? = null - - @Autowired - private val oAuthClientService: OAuthClientService? = null - - @Autowired - private val managementPortalProperties: ManagementPortalProperties? = null - - @Autowired - private val subjectService: SubjectService? = null - - /** - * Save a metaToken. - * - * @param metaToken the entity to save - * @return the persisted entity - */ - fun save(metaToken: MetaToken): MetaToken { - log.debug("Request to save MetaToken : {}", metaToken) - return metaTokenRepository!!.save(metaToken) - } - - /** - * Get one project by id. - * - * @param tokenName the id of the entity - * @return the entity - */ - @Throws(MalformedURLException::class) - fun fetchToken(tokenName: String): TokenDTO { - log.debug("Request to get Token : {}", tokenName) - val metaToken = getToken(tokenName) - // process the response if the token is not fetched or not expired - return if (metaToken.isValid) { - val refreshToken = oAuthClientService!!.createAccessToken( - metaToken.subject!!.user!!, - metaToken.clientId!! - ) - .refreshToken - .value - - // create response - val result = TokenDTO( - refreshToken, - URL(managementPortalProperties!!.common.baseUrl), - subjectService!!.getPrivacyPolicyUrl(metaToken.subject!!) - ) - - // change fetched status to true. - if (!metaToken.isFetched()) { - metaToken.fetched(true) - save(metaToken) - } - result - } else { - throw RequestGoneException( - "Token $tokenName already fetched or expired. ", - EntityName.META_TOKEN, "error.TokenCannotBeSent" - ) - } - } - - /** - * Gets a token from databased using the tokenName. - * - * @param tokenName tokenName. - * @return fetched token as [MetaToken]. - */ - @Transactional(readOnly = true) - fun getToken(tokenName: String): MetaToken { - return metaTokenRepository!!.findOneByTokenName(tokenName) - ?: throw NotFoundException( - "Meta token not found with tokenName", - EntityName.META_TOKEN, - ErrorConstants.ERR_TOKEN_NOT_FOUND, - Collections.singletonMap("tokenName", tokenName) - ) - } - - /** - * Saves a unique meta-token instance, by checking for token-name collision. - * If a collision is detection, we try to save the token with a new tokenName - * @return an unique token - */ - fun saveUniqueToken( - subject: Subject?, - clientId: String?, - fetched: Boolean?, - expiryTime: Instant?, - persistent: Boolean - ): MetaToken { - val metaToken = MetaToken() - .generateName(if (persistent) MetaToken.LONG_ID_LENGTH else MetaToken.SHORT_ID_LENGTH) - .fetched(fetched!!) - .expiryDate(expiryTime) - .subject(subject) - .clientId(clientId) - .persistent(persistent) - return try { - metaTokenRepository!!.save(metaToken) - } catch (e: ConstraintViolationException) { - log.warn("Unique constraint violation catched... Trying to save with new tokenName") - saveUniqueToken(subject, clientId, fetched, expiryTime, persistent) - } - } - - /** - * Creates meta token for oauth-subject pair. - * @param subject to create token for - * @param clientId using which client id - * @param persistent whether to persist the token after it is has been fetched - * @return [ClientPairInfoDTO] to return. - * @throws URISyntaxException when token URI cannot be formed properly. - * @throws MalformedURLException when token URL cannot be formed properly. - */ - @Throws(URISyntaxException::class, MalformedURLException::class, NotAuthorizedException::class) - fun createMetaToken(subject: Subject, clientId: String?, persistent: Boolean): ClientPairInfoDTO { - val timeout = getMetaTokenTimeout(persistent, project = subject.activeProject - ?:throw NotAuthorizedException("Cannot calculate meta-token duration without configured project") - ) - - // tokenName should be generated - val metaToken = saveUniqueToken( - subject, clientId, false, - Instant.now().plus(timeout), persistent - ) - val tokenName = metaToken.tokenName - return if (metaToken.id != null && tokenName != null) { - // get base url from settings - val baseUrl = managementPortalProperties!!.common.managementPortalBaseUrl - // create complete uri string - val tokenUrl = baseUrl + ResourceUriService.getUri(metaToken).getPath() - // create response - ClientPairInfoDTO( - URL(baseUrl), tokenName, - URL(tokenUrl), timeout - ) - } else { - throw InvalidStateException( - "Could not create a valid token", EntityName.OAUTH_CLIENT, - "error.couldNotCreateToken" - ) - } - } - - /** - * Gets the meta-token timeout from config file. If the config is not mentioned or in wrong - * format, it will return default value. - * - * @return meta-token timeout duration. - * @throws BadRequestException if a persistent token is requested but it is not configured. - */ - fun getMetaTokenTimeout(persistent: Boolean, project: Project?): Duration { - val timeoutConfig: String? - val defaultTimeout: Duration - if (persistent) { - timeoutConfig = managementPortalProperties!!.oauth.persistentMetaTokenTimeout - if (timeoutConfig == null || timeoutConfig.isEmpty()) { - throw BadRequestException( - "Cannot create persistent token: not supported in configuration.", - EntityName.META_TOKEN, ErrorConstants.ERR_PERSISTENT_TOKEN_DISABLED - ) - } - defaultTimeout = MetaTokenResource.DEFAULT_PERSISTENT_META_TOKEN_TIMEOUT - } else { - timeoutConfig = managementPortalProperties!!.oauth.metaTokenTimeout - defaultTimeout = MetaTokenResource.DEFAULT_META_TOKEN_TIMEOUT - if (timeoutConfig == null || timeoutConfig.isEmpty()) { - return defaultTimeout - } - } - return try { - Duration.parse(timeoutConfig) - } catch (e: DateTimeParseException) { - // if the token timeout cannot be read, log the error and use the default value. - log.warn( - "Cannot parse meta-token timeout config. Using default value {}", - defaultTimeout, e - ) - defaultTimeout - } - } - - /** - * Expired and fetched tokens are deleted after 1 month. - * - * This is scheduled to get triggered first day of the month. - */ - @Scheduled(cron = "0 0 0 1 * ?") - fun removeStaleTokens() { - log.info("Scheduled scan for expired and fetched meta-tokens starting now") - metaTokenRepository!!.findAllByFetchedOrExpired(Instant.now()) - .forEach(Consumer { metaToken: MetaToken -> - log.info( - "Deleting deleting expired or fetched token {}", - metaToken.tokenName - ) - metaTokenRepository.delete(metaToken) - }) - } - - fun delete(token: MetaToken) { - metaTokenRepository!!.delete(token) - } - - companion object { - private val log = LoggerFactory.getLogger(MetaTokenService::class.java) - } -} diff --git a/src/main/java/org/radarbase/management/service/OAuthClientService.kt b/src/main/java/org/radarbase/management/service/OAuthClientService.kt deleted file mode 100644 index 735d6a23f..000000000 --- a/src/main/java/org/radarbase/management/service/OAuthClientService.kt +++ /dev/null @@ -1,175 +0,0 @@ -package org.radarbase.management.service - -import org.radarbase.management.domain.User -import org.radarbase.management.service.dto.ClientDetailsDTO -import org.radarbase.management.service.mapper.ClientDetailsMapper -import org.radarbase.management.web.rest.errors.ConflictException -import org.radarbase.management.web.rest.errors.EntityName -import org.radarbase.management.web.rest.errors.ErrorConstants -import org.radarbase.management.web.rest.errors.InvalidRequestException -import org.radarbase.management.web.rest.errors.NotFoundException -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken -import org.springframework.security.core.Authentication -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.oauth2.common.OAuth2AccessToken -import org.springframework.security.oauth2.common.util.OAuth2Utils -import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration -import org.springframework.security.oauth2.provider.ClientDetails -import org.springframework.security.oauth2.provider.NoSuchClientException -import org.springframework.security.oauth2.provider.OAuth2Authentication -import org.springframework.security.oauth2.provider.OAuth2Request -import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService -import org.springframework.stereotype.Service -import java.util.* - -/** - * The service layer to handle OAuthClient and Token related functions. - * Created by nivethika on 03/08/2018. - */ -//@Service -class OAuthClientService( - @Autowired private val clientDetailsService: JdbcClientDetailsService, - @Autowired private val clientDetailsMapper: ClientDetailsMapper, - @Autowired private val authorizationServerEndpointsConfiguration: AuthorizationServerEndpointsConfiguration -) { - - fun findAllOAuthClients(): List { - return clientDetailsService.listClientDetails() - } - - /** - * Find ClientDetails by OAuth client id. - * - * @param clientId The client ID to look up - * @return a ClientDetails object with the requested client ID - * @throws NotFoundException If there is no client with the requested ID - */ - fun findOneByClientId(clientId: String?): ClientDetails { - return try { - clientDetailsService.loadClientByClientId(clientId) - } catch (e: NoSuchClientException) { - log.error("Pair client request for unknown client id: {}", clientId) - val errorParams: MutableMap = HashMap() - errorParams["clientId"] = clientId - throw NotFoundException( - "Client not found for client-id", EntityName.Companion.OAUTH_CLIENT, - ErrorConstants.ERR_OAUTH_CLIENT_ID_NOT_FOUND, errorParams - ) - } - } - - /** - * Update Oauth-client with new information. - * - * @param clientDetailsDto information to update. - * @return Updated [ClientDetails] instance. - */ - fun updateOauthClient(clientDetailsDto: ClientDetailsDTO): ClientDetails { - val details: ClientDetails? = clientDetailsMapper.clientDetailsDTOToClientDetails(clientDetailsDto) - // update client. - clientDetailsService.updateClientDetails(details) - val updated = findOneByClientId(clientDetailsDto.clientId) - // updateClientDetails does not update secret, so check for it separately - if (clientDetailsDto.clientSecret != null && clientDetailsDto.clientSecret != updated.clientSecret) { - clientDetailsService.updateClientSecret( - clientDetailsDto.clientId, - clientDetailsDto.clientSecret - ) - } - return findOneByClientId(clientDetailsDto.clientId) - } - - /** - * Deletes an oauth client. - * @param clientId of the auth-client to delete. - */ - fun deleteClientDetails(clientId: String?) { - clientDetailsService.removeClientDetails(clientId) - } - - /** - * Creates new oauth-client. - * - * @param clientDetailsDto data to create oauth-client. - * @return created [ClientDetails]. - */ - fun createClientDetail(clientDetailsDto: ClientDetailsDTO): ClientDetails { - // check if the client id exists - try { - val existingClient = clientDetailsService.loadClientByClientId(clientDetailsDto.clientId) - if (existingClient != null) { - throw ConflictException( - "OAuth client already exists with this id", - EntityName.Companion.OAUTH_CLIENT, ErrorConstants.ERR_CLIENT_ID_EXISTS, - Collections.singletonMap("client_id", clientDetailsDto.clientId) - ) - } - } catch (ex: NoSuchClientException) { - // Client does not exist yet, we can go ahead and create it - log.info( - "No client existing with client-id {}. Proceeding to create new client", - clientDetailsDto.clientId - ) - } - val details: ClientDetails? = clientDetailsMapper.clientDetailsDTOToClientDetails(clientDetailsDto) - // create oauth client. - clientDetailsService.addClientDetails(details) - return findOneByClientId(clientDetailsDto.clientId) - } - - /** - * Internally creates an [OAuth2AccessToken] token using authorization-code flow. This - * method bypasses the usual authorization code flow mechanism, so it should only be used where - * appropriate, e.g., for subject impersonation. - * - * @param clientId oauth client id. - * @param user user of the token. - * @return Created [OAuth2AccessToken] instance. - */ - fun createAccessToken(user: User, clientId: String): OAuth2AccessToken { - val authorities = user.authorities!! - .map { a -> SimpleGrantedAuthority(a) } - // lookup the OAuth client - // getOAuthClient checks if the id exists - val client = findOneByClientId(clientId) - val requestParameters = Collections.singletonMap( - OAuth2Utils.GRANT_TYPE, "authorization_code" - ) - val responseTypes = setOf("code") - val oAuth2Request = OAuth2Request( - requestParameters, clientId, authorities, true, client.scope, - client.resourceIds, null, responseTypes, emptyMap() - ) - val authenticationToken: Authentication = UsernamePasswordAuthenticationToken( - user.login, null, authorities - ) - return authorizationServerEndpointsConfiguration.getEndpointsConfigurer() - .tokenServices - .createAccessToken(OAuth2Authentication(oAuth2Request, authenticationToken)) - } - - companion object { - private val log = LoggerFactory.getLogger(OAuthClientService::class.java) - private const val PROTECTED_KEY = "protected" - - /** - * Checks whether a client is a protected client. - * - * @param details ClientDetails. - */ - fun checkProtected(details: ClientDetails) { - val info = details.additionalInformation - if (Objects.nonNull(info) && info.containsKey(PROTECTED_KEY) && info[PROTECTED_KEY] - .toString().equals("true", ignoreCase = true) - ) { - throw InvalidRequestException( - "Cannot modify protected client", EntityName.Companion.OAUTH_CLIENT, - ErrorConstants.ERR_OAUTH_CLIENT_PROTECTED, - Collections.singletonMap("client_id", details.clientId) - ) - } - } - } -} diff --git a/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt b/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt index c51341b59..afcafb1d9 100644 --- a/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt +++ b/src/main/java/org/radarbase/management/service/mapper/decorator/ProjectMapperDecorator.kt @@ -3,7 +3,6 @@ package org.radarbase.management.service.mapper.decorator import org.radarbase.management.domain.Project import org.radarbase.management.repository.OrganizationRepository import org.radarbase.management.repository.ProjectRepository -import org.radarbase.management.service.MetaTokenService import org.radarbase.management.service.dto.MinimalProjectDetailsDTO import org.radarbase.management.service.dto.ProjectDTO import org.radarbase.management.service.mapper.ProjectMapper @@ -23,16 +22,10 @@ abstract class ProjectMapperDecorator : ProjectMapper { @Autowired @Qualifier("delegate") private lateinit var delegate: ProjectMapper @Autowired private lateinit var organizationRepository: OrganizationRepository @Autowired private lateinit var projectRepository: ProjectRepository - //@Autowired private lateinit var metaTokenService: MetaTokenService override fun projectToProjectDTO(project: Project?): ProjectDTO? { val dto = delegate.projectToProjectDTO(project) dto?.humanReadableProjectName = project?.attributes?.get(ProjectDTO.HUMAN_READABLE_PROJECT_NAME) -// try { -// dto?.persistentTokenTimeout = metaTokenService.getMetaTokenTimeout(true, project).toMillis() -// } catch (ex: BadRequestException) { -// dto?.persistentTokenTimeout = null -// } return dto } diff --git a/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt b/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt deleted file mode 100644 index 7c9a412a5..000000000 --- a/src/main/java/org/radarbase/management/web/rest/MetaTokenResource.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.radarbase.management.web.rest - -import io.micrometer.core.annotation.Timed -import org.radarbase.auth.authorization.EntityDetails -import org.radarbase.auth.authorization.Permission -import org.radarbase.management.security.Constants -import org.radarbase.management.security.NotAuthorizedException -import org.radarbase.management.service.AuthService -import org.radarbase.management.service.MetaTokenService -import org.radarbase.management.service.dto.TokenDTO -import org.radarbase.management.web.rest.OAuthClientsResource -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.http.ResponseEntity -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RestController -import java.net.MalformedURLException -import java.time.Duration - -//@RestController -//@RequestMapping("/api") -class MetaTokenResource { - @Autowired - private val metaTokenService: MetaTokenService? = null - - @Autowired - private val authService: AuthService? = null - - /** - * GET /api/meta-token/:tokenName. - * - * - * Get refresh-token available under this tokenName. - * - * @param tokenName the tokenName given after pairing the subject with client - * @return the client as a [ClientPairInfoDTO] - */ - @GetMapping("/meta-token/{tokenName:" + Constants.TOKEN_NAME_REGEX + "}") - @Timed - @Throws( - MalformedURLException::class - ) - fun getTokenByTokenName(@PathVariable("tokenName") tokenName: String?): ResponseEntity { - log.info("Requesting token with tokenName {}", tokenName) - return ResponseEntity.ok().body(tokenName?.let { metaTokenService!!.fetchToken(it) }) - } - - /** - * DELETE /api/meta-token/:tokenName. - * - * - * Delete refresh-token available under this tokenName. - * - * @param tokenName the tokenName given after pairing the subject with client - * @return the client as a [ClientPairInfoDTO] - */ - @DeleteMapping("/meta-token/{tokenName:" + Constants.TOKEN_NAME_REGEX + "}") - @Timed - @Throws( - NotAuthorizedException::class - ) - fun deleteTokenByTokenName(@PathVariable("tokenName") tokenName: String?): ResponseEntity { - log.info("Requesting token with tokenName {}", tokenName) - val metaToken = tokenName?.let { metaTokenService!!.getToken(it) } - val subject = metaToken?.subject - val project: String = subject!! - .activeProject - ?.projectName - ?: - throw NotAuthorizedException( - "Cannot establish authority of subject without active project affiliation." - ) - val user = subject.user!!.login - authService!!.checkPermission( - Permission.SUBJECT_UPDATE, - { e: EntityDetails -> e.project(project).subject(user) }) - metaTokenService?.delete(metaToken) - return ResponseEntity.noContent().build() - } - - companion object { - private val log = LoggerFactory.getLogger(OAuthClientsResource::class.java) - @JvmField - val DEFAULT_META_TOKEN_TIMEOUT = Duration.ofHours(1) - @JvmField - val DEFAULT_PERSISTENT_META_TOKEN_TIMEOUT = Duration.ofDays(31) - } -} diff --git a/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt b/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt deleted file mode 100644 index 68e9978d7..000000000 --- a/src/main/java/org/radarbase/management/web/rest/OAuthClientsResource.kt +++ /dev/null @@ -1,227 +0,0 @@ -package org.radarbase.management.web.rest - -import io.micrometer.core.annotation.Timed -import org.radarbase.auth.authorization.EntityDetails -import org.radarbase.auth.authorization.Permission -import org.radarbase.management.security.Constants -import org.radarbase.management.security.NotAuthorizedException -import org.radarbase.management.service.AuthService -import org.radarbase.management.service.MetaTokenService -import org.radarbase.management.service.OAuthClientService -import org.radarbase.management.service.ResourceUriService -import org.radarbase.management.service.SubjectService -import org.radarbase.management.service.UserService -import org.radarbase.management.service.dto.ClientDetailsDTO -import org.radarbase.management.service.dto.ClientPairInfoDTO -import org.radarbase.management.service.mapper.ClientDetailsMapper -import org.radarbase.management.web.rest.errors.EntityName -import org.radarbase.management.web.rest.errors.ErrorConstants -import org.radarbase.management.web.rest.errors.NotFoundException -import org.radarbase.management.web.rest.util.HeaderUtil -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.actuate.audit.AuditEvent -import org.springframework.boot.actuate.audit.AuditEventRepository -import org.springframework.http.HttpStatus -import org.springframework.http.ResponseEntity -import org.springframework.security.access.AccessDeniedException -import org.springframework.web.bind.annotation.DeleteMapping -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.PostMapping -import org.springframework.web.bind.annotation.PutMapping -import org.springframework.web.bind.annotation.RequestBody -import org.springframework.web.bind.annotation.RequestMapping -import org.springframework.web.bind.annotation.RequestParam -import org.springframework.web.bind.annotation.RestController -import java.net.MalformedURLException -import java.net.URISyntaxException -import javax.validation.Valid - -/** - * Created by dverbeec on 5/09/2017. - */ -//@RestController -//@RequestMapping("/api") -class OAuthClientsResource( - @Autowired private val oAuthClientService: OAuthClientService, - @Autowired private val metaTokenService: MetaTokenService, - @Autowired private val clientDetailsMapper: ClientDetailsMapper, - @Autowired private val subjectService: SubjectService, - @Autowired private val userService: UserService, - @Autowired private val eventRepository: AuditEventRepository, - @Autowired private val authService: AuthService -) { - - @Throws(NotAuthorizedException::class) - @Timed - @GetMapping("/oauth-clients") - /** - * GET /api/oauth-clients. - * - * - * Retrieve a list of currently registered OAuth clients. - * - * @return the list of registered clients as a list of [ClientDetailsDTO] - */ - fun oAuthClients(): ResponseEntity> { - authService.checkScope(Permission.OAUTHCLIENTS_READ) - val clients = clientDetailsMapper.clientDetailsToClientDetailsDTO(oAuthClientService.findAllOAuthClients()) - return ResponseEntity.ok().body(clients) - } - - /** - * GET /api/oauth-clients/:id. - * - * - * Get details on a specific client. - * - * @param id the client id for which to fetch the details - * @return the client as a [ClientDetailsDTO] - */ - @GetMapping("/oauth-clients/{id:" + Constants.ENTITY_ID_REGEX + "}") - @Timed - @Throws( - NotAuthorizedException::class - ) - fun getOAuthClientById(@PathVariable("id") id: String?): ResponseEntity { - authService.checkPermission(Permission.OAUTHCLIENTS_READ) - - val client = oAuthClientService.findOneByClientId(id) - val clientDTO = clientDetailsMapper.clientDetailsToClientDetailsDTO(client) - - // getOAuthClient checks if the id exists - return ResponseEntity.ok().body(clientDTO) - } - - /** - * PUT /api/oauth-clients. - * - * - * Update an existing OAuth client. - * - * @param clientDetailsDto The client details to update - * @return The updated OAuth client. - */ - @PutMapping("/oauth-clients") - @Timed - @Throws(NotAuthorizedException::class) - fun updateOAuthClient(@RequestBody @Valid clientDetailsDto: ClientDetailsDTO?): ResponseEntity { - authService.checkPermission(Permission.OAUTHCLIENTS_UPDATE) - // getOAuthClient checks if the id exists - OAuthClientService.checkProtected(oAuthClientService.findOneByClientId(clientDetailsDto!!.clientId)) - val updated = oAuthClientService.updateOauthClient(clientDetailsDto) - return ResponseEntity.ok() - .headers( - HeaderUtil.createEntityUpdateAlert( - EntityName.OAUTH_CLIENT, - clientDetailsDto.clientId - ) - ) - .body(clientDetailsMapper.clientDetailsToClientDetailsDTO(updated)) - } - - /** - * DELETE /api/oauth-clients/:id. - * - * - * Delete the OAuth client with the specified client id. - * - * @param id The id of the client to delete - * @return a ResponseEntity indicating success or failure - */ - @DeleteMapping("/oauth-clients/{id:" + Constants.ENTITY_ID_REGEX + "}") - @Timed - @Throws( - NotAuthorizedException::class - ) - fun deleteOAuthClient(@PathVariable id: String?): ResponseEntity { - authService.checkPermission(Permission.OAUTHCLIENTS_DELETE) - // getOAuthClient checks if the id exists - OAuthClientService.checkProtected(oAuthClientService.findOneByClientId(id)) - oAuthClientService.deleteClientDetails(id) - return ResponseEntity.ok().headers(HeaderUtil.createEntityDeletionAlert(EntityName.OAUTH_CLIENT, id)) - .build() - } - - /** - * POST /api/oauth-clients. - * - * - * Register a new oauth client. - * - * @param clientDetailsDto The OAuth client to be registered - * @return a response indicating success or failure - * @throws URISyntaxException if there was a problem formatting the URI to the new entity - */ - @PostMapping("/oauth-clients") - @Timed - @Throws(URISyntaxException::class, NotAuthorizedException::class) - fun createOAuthClient(@RequestBody clientDetailsDto: @Valid ClientDetailsDTO): ResponseEntity { - authService.checkPermission(Permission.OAUTHCLIENTS_CREATE) - val created = oAuthClientService.createClientDetail(clientDetailsDto) - return ResponseEntity.created(ResourceUriService.getUri(clientDetailsDto)) - .headers(HeaderUtil.createEntityCreationAlert(EntityName.OAUTH_CLIENT, created.clientId)) - .body(clientDetailsMapper.clientDetailsToClientDetailsDTO(created)) - } - - /** - * GET /oauth-clients/pair. - * - * - * Generates OAuth2 refresh tokens for the given user, to be used to bootstrap the - * authentication of client apps. This will generate a refresh token which can be used at the - * /oauth/token endpoint to get a new access token and refresh token. - * - * @param login the login of the subject for whom to generate pairing information - * @param clientId the OAuth client id - * @return a ClientPairInfoDTO with status 200 (OK) - */ - @GetMapping("/oauth-clients/pair") - @Timed - @Throws(NotAuthorizedException::class, URISyntaxException::class, MalformedURLException::class) - fun getRefreshToken( - @RequestParam login: String, - @RequestParam(value = "clientId") clientId: String, - @RequestParam(value = "persistent", defaultValue = "false") persistent: Boolean? - ): ResponseEntity { - authService.checkScope(Permission.SUBJECT_UPDATE) - val currentUser = - userService.getUserWithAuthorities() // We only allow this for actual logged in users for now, not for client_credentials - ?: throw AccessDeniedException( - "You must be a logged in user to access this resource" - ) - - // lookup the subject - val subject = subjectService.findOneByLogin(login) - val projectName: String = subject.activeProject - ?.projectName - ?: throw NotFoundException( - "Project for subject $login not found", EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND - ) - - - // Users who can update a subject can also generate a refresh token for that subject - authService.checkPermission( - Permission.SUBJECT_UPDATE, - { e: EntityDetails -> e.project(projectName).subject(login) }) - val cpi = metaTokenService.createMetaToken(subject, clientId, persistent!!) - // generate audit event - eventRepository.add( - AuditEvent( - currentUser.login, "PAIR_CLIENT_REQUEST", - "client_id=$clientId", "subject_login=$login" - ) - ) - log.info( - "[{}] by {}: client_id={}, subject_login={}", "PAIR_CLIENT_REQUEST", currentUser - .login, clientId, login - ) - return ResponseEntity(cpi, HttpStatus.OK) - } - - companion object { - private val log = LoggerFactory.getLogger(OAuthClientsResource::class.java) - } -} diff --git a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt index 5aa0cd8e7..cbf2b75b2 100644 --- a/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt +++ b/src/test/java/org/radarbase/auth/authentication/OAuthHelper.kt @@ -11,7 +11,6 @@ import org.radarbase.management.domain.Role import org.radarbase.management.domain.User import org.radarbase.management.repository.UserRepository import org.radarbase.management.security.JwtAuthenticationFilter -import org.radarbase.management.security.jwt.ManagementPortalJwtAccessTokenConverter import org.slf4j.LoggerFactory import org.springframework.mock.web.MockHttpServletRequest import org.springframework.security.core.Authentication @@ -38,7 +37,7 @@ object OAuthHelper { val AUTHORITIES = arrayOf("ROLE_SYS_ADMIN") val ROLES = arrayOf("ROLE_SYS_ADMIN") val SOURCES = arrayOf() - val AUD = arrayOf(ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL) + val AUD = arrayOf("res_ManagementPortal") const val CLIENT = "unit_test" const val USER = "admin" const val ISS = "RADAR" @@ -110,7 +109,7 @@ object OAuthHelper { validRsaToken = createValidToken(rsa) val verifierList = listOf(ecdsa, rsa) .map { alg: Algorithm? -> - alg?.toTokenVerifier(ManagementPortalJwtAccessTokenConverter.RES_MANAGEMENT_PORTAL) + alg?.toTokenVerifier("res_ManagementPortal") } .requireNoNulls() .toList() @@ -164,7 +163,6 @@ object OAuthHelper { .withArrayClaim("authorities", AUTHORITIES) .withArrayClaim("roles", ROLES) .withArrayClaim("sources", SOURCES) - .withArrayClaim("aud", AUD) .withClaim("client_id", CLIENT) .withClaim("user_name", USER) .withClaim("jti", JTI) diff --git a/src/test/java/org/radarbase/management/service/MetaTokenServiceTest.kt b/src/test/java/org/radarbase/management/service/MetaTokenServiceTest.kt deleted file mode 100644 index 55834309e..000000000 --- a/src/test/java/org/radarbase/management/service/MetaTokenServiceTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -package org.radarbase.management.service - -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.radarbase.management.ManagementPortalTestApp -import org.radarbase.management.domain.MetaToken -import org.radarbase.management.repository.MetaTokenRepository -import org.radarbase.management.service.dto.SubjectDTO -import org.radarbase.management.service.mapper.SubjectMapper -import org.radarbase.management.web.rest.errors.RadarWebApplicationException -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.security.oauth2.provider.ClientDetails -import org.springframework.test.context.junit.jupiter.SpringExtension -import org.springframework.transaction.annotation.Transactional -import java.net.MalformedURLException -import java.time.Duration -import java.time.Instant -import java.util.* - -/** - * Test class for the MetaTokenService class. - * - * @see MetaTokenService - */ -@ExtendWith(SpringExtension::class) -@SpringBootTest(classes = [ManagementPortalTestApp::class]) -@Transactional -internal class MetaTokenServiceTest( - @Autowired private val metaTokenService: MetaTokenService, - @Autowired private val metaTokenRepository: MetaTokenRepository, - @Autowired private val subjectService: SubjectService, - @Autowired private val subjectMapper: SubjectMapper, - @Autowired private val oAuthClientService: OAuthClientService, -) { - private lateinit var clientDetails: ClientDetails - private lateinit var subjectDto: SubjectDTO - - @BeforeEach - fun setUp() { - subjectDto = SubjectServiceTest.createEntityDTO() - subjectDto = subjectService.createSubject(subjectDto)!! - clientDetails = oAuthClientService.createClientDetail(OAuthClientServiceTestUtil.createClient()) - } - - @Test - @Throws(MalformedURLException::class) - fun testSaveThenFetchMetaToken() { - val metaToken = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(false) - .persistent(false) - .expiryDate(Instant.now().plus(Duration.ofHours(1))) - .subject(subjectMapper.subjectDTOToSubject(subjectDto)) - .clientId(clientDetails.clientId) - val saved = metaTokenService.save(metaToken) - Assertions.assertNotNull(saved.id) - Assertions.assertNotNull(saved.tokenName) - Assertions.assertFalse(saved.isFetched()) - Assertions.assertTrue(saved.expiryDate!!.isAfter(Instant.now())) - val tokenName = saved.tokenName - val fetchedToken = metaTokenService.fetchToken(tokenName!!) - Assertions.assertNotNull(fetchedToken) - Assertions.assertNotNull(fetchedToken.refreshToken) - } - - @Test - @Throws(MalformedURLException::class) - fun testGetAFetchedMetaToken() { - val token = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(true) - .persistent(false) - .tokenName("something") - .expiryDate(Instant.now().plus(Duration.ofHours(1))) - .subject(subjectMapper.subjectDTOToSubject(subjectDto)) - val saved = metaTokenService.save(token) - Assertions.assertNotNull(saved.id) - Assertions.assertNotNull(saved.tokenName) - Assertions.assertTrue(saved.isFetched()) - Assertions.assertTrue(saved.expiryDate!!.isAfter(Instant.now())) - val tokenName = saved.tokenName - Assertions.assertThrows( - RadarWebApplicationException::class.java - ) { metaTokenService.fetchToken(tokenName!!) } - } - - @Test - @Throws(MalformedURLException::class) - fun testGetAnExpiredMetaToken() { - val token = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(false) - .persistent(false) - .tokenName("somethingelse") - .expiryDate(Instant.now().minus(Duration.ofHours(1))) - .subject(subjectMapper.subjectDTOToSubject(subjectDto)) - val saved = metaTokenService.save(token) - Assertions.assertNotNull(saved.id) - Assertions.assertNotNull(saved.tokenName) - Assertions.assertFalse(saved.isFetched()) - Assertions.assertTrue(saved.expiryDate!!.isBefore(Instant.now())) - val tokenName = saved.tokenName - Assertions.assertThrows( - RadarWebApplicationException::class.java - ) { metaTokenService.fetchToken(tokenName!!) } - } - - @Test - fun testRemovingExpiredMetaToken() { - val tokenFetched = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(true) - .persistent(false) - .tokenName("something") - .expiryDate(Instant.now().plus(Duration.ofHours(1))) - val tokenExpired = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(false) - .persistent(false) - .tokenName("somethingelse") - .expiryDate(Instant.now().minus(Duration.ofHours(1))) - val tokenNew = MetaToken() - .generateName(MetaToken.SHORT_ID_LENGTH) - .fetched(false) - .persistent(false) - .tokenName("somethingelseandelse") - .expiryDate(Instant.now().plus(Duration.ofHours(1))) - metaTokenRepository.saveAll(Arrays.asList(tokenFetched, tokenExpired, tokenNew)) - metaTokenService.removeStaleTokens() - val availableTokens = metaTokenRepository.findAll() - Assertions.assertEquals(1, availableTokens.size) - } -} diff --git a/src/test/java/org/radarbase/management/web/rest/OAuthClientsResourceIntTest.kt b/src/test/java/org/radarbase/management/web/rest/OAuthClientsResourceIntTest.kt deleted file mode 100644 index cb12ebb4c..000000000 --- a/src/test/java/org/radarbase/management/web/rest/OAuthClientsResourceIntTest.kt +++ /dev/null @@ -1,212 +0,0 @@ -package org.radarbase.management.web.rest - -import org.assertj.core.api.Assertions -import org.hamcrest.Matchers -import org.hamcrest.Matchers.containsInAnyOrder -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.mockito.MockitoAnnotations -import org.radarbase.auth.authentication.OAuthHelper -import org.radarbase.management.ManagementPortalApp -import org.radarbase.management.service.OAuthClientServiceTestUtil -import org.radarbase.management.service.dto.ClientDetailsDTO -import org.radarbase.management.web.rest.errors.ExceptionTranslator -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.test.context.SpringBootTest -import org.springframework.data.web.PageableHandlerMethodArgumentResolver -import org.springframework.http.MediaType -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter -import org.springframework.mock.web.MockFilterConfig -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.oauth2.provider.ClientDetails -import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService -import org.springframework.test.context.junit.jupiter.SpringExtension -import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders -import org.springframework.test.web.servlet.result.MockMvcResultMatchers -import org.springframework.test.web.servlet.setup.MockMvcBuilders -import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder -import org.springframework.transaction.annotation.Transactional -import java.util.function.Consumer - -/** - * Test class for the ProjectResource REST controller. - * - * @see ProjectResource - */ -@ExtendWith(SpringExtension::class) -@SpringBootTest(classes = [ManagementPortalApp::class]) -internal class OAuthClientsResourceIntTest @Autowired constructor( - @Autowired private val oauthClientsResource: OAuthClientsResource, - @Autowired private val clientDetailsService: JdbcClientDetailsService, - @Autowired private val jacksonMessageConverter: MappingJackson2HttpMessageConverter, - @Autowired private val pageableArgumentResolver: PageableHandlerMethodArgumentResolver, - @Autowired private val exceptionTranslator: ExceptionTranslator, -) { - private lateinit var restOauthClientMvc: MockMvc - private lateinit var details: ClientDetailsDTO - private var databaseSizeBeforeCreate: Int = 0 - private lateinit var clientDetailsList: List - - @BeforeEach - @Throws(Exception::class) - fun setUp() { - MockitoAnnotations.openMocks(this) - val filter = OAuthHelper.createAuthenticationFilter() - filter.init(MockFilterConfig()) - restOauthClientMvc = - MockMvcBuilders.standaloneSetup(oauthClientsResource).setCustomArgumentResolvers(pageableArgumentResolver) - .setControllerAdvice(exceptionTranslator).setMessageConverters(jacksonMessageConverter) - .addFilter(filter).defaultRequest( - MockMvcRequestBuilders.get("/").with(OAuthHelper.bearerToken()) - ).build() - databaseSizeBeforeCreate = clientDetailsService.listClientDetails().size - - // Create the OAuth Client - details = OAuthClientServiceTestUtil.createClient() - restOauthClientMvc.perform( - MockMvcRequestBuilders.post("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isCreated()) - - // Validate the Project in the database - clientDetailsList = clientDetailsService.listClientDetails() - Assertions.assertThat(clientDetailsList).hasSize(databaseSizeBeforeCreate + 1) - } - - @Test - @Transactional - @Throws(Exception::class) - fun createAndFetchOAuthClient() { - // fetch the created oauth client and check the json result - restOauthClientMvc.perform( - MockMvcRequestBuilders.get("/api/oauth-clients/" + details.clientId).accept(MediaType.APPLICATION_JSON) - ).andExpect( - MockMvcResultMatchers.status().isOk() - ).andExpect( - MockMvcResultMatchers.jsonPath("$.clientId").value(Matchers.equalTo(details.clientId)) - ).andExpect(MockMvcResultMatchers.jsonPath("$.clientSecret").value(Matchers.nullValue())).andExpect( - MockMvcResultMatchers.jsonPath("$.accessTokenValiditySeconds").value( - Matchers.equalTo( - details.accessTokenValiditySeconds?.toInt() - ) - ) - ).andExpect( - MockMvcResultMatchers.jsonPath("$.refreshTokenValiditySeconds").value( - Matchers.equalTo( - details.refreshTokenValiditySeconds?.toInt() - ) - ) - ).andExpect( - MockMvcResultMatchers.jsonPath("$.scope") - .value(containsInAnyOrder(details.scope?.map { Matchers.equalTo(it) })) - ).andExpect(MockMvcResultMatchers.jsonPath("$.autoApproveScopes") - .value(containsInAnyOrder(details.autoApproveScopes?.map { Matchers.equalTo(it) }))) - .andExpect(MockMvcResultMatchers.jsonPath("$.authorizedGrantTypes") - .value(containsInAnyOrder(details.authorizedGrantTypes?.map { Matchers.equalTo(it) }))).andExpect( - MockMvcResultMatchers.jsonPath("$.authorities").value( - containsInAnyOrder(details.authorities?.map { Matchers.equalTo(it) }) - ) - ) - - val testDetails = - clientDetailsList.stream().filter { d: ClientDetails -> d.clientId == details.clientId }.findFirst() - .orElseThrow() - Assertions.assertThat(testDetails.clientSecret).startsWith("$2a$10$") - Assertions.assertThat(testDetails.scope).containsExactlyInAnyOrderElementsOf( - details.scope - ) - Assertions.assertThat(testDetails.resourceIds).containsExactlyInAnyOrderElementsOf( - details.resourceIds - ) - Assertions.assertThat(testDetails.authorizedGrantTypes).containsExactlyInAnyOrderElementsOf( - details.authorizedGrantTypes - ) - details.autoApproveScopes?.forEach(Consumer { scope: String? -> - Assertions.assertThat( - testDetails.isAutoApprove( - scope - ) - ).isTrue() - }) - Assertions.assertThat(testDetails.accessTokenValiditySeconds).isEqualTo( - details.accessTokenValiditySeconds?.toInt() - ) - Assertions.assertThat(testDetails.refreshTokenValiditySeconds).isEqualTo( - details.refreshTokenValiditySeconds?.toInt() - ) - Assertions.assertThat(testDetails.authorities.stream().map { obj: GrantedAuthority -> obj.authority }) - .containsExactlyInAnyOrderElementsOf(details.authorities) - Assertions.assertThat(testDetails.additionalInformation).containsAllEntriesOf( - details.additionalInformation - ) - } - - @Test - @Transactional - @Throws(Exception::class) - fun duplicateOAuthClient() { - restOauthClientMvc.perform( - MockMvcRequestBuilders.post("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isConflict()) - } - - @Test - @Transactional - @Throws(Exception::class) - fun updateOAuthClient() { - // update the client - details.refreshTokenValiditySeconds = 20L - restOauthClientMvc.perform( - MockMvcRequestBuilders.put("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isOk()) - - // fetch the client - clientDetailsList = clientDetailsService.listClientDetails() - Assertions.assertThat(clientDetailsList).hasSize(databaseSizeBeforeCreate + 1) - val testDetails = - clientDetailsList.stream().filter { d: ClientDetails -> d.clientId == details.clientId }.findFirst() - .orElseThrow() - Assertions.assertThat(testDetails.refreshTokenValiditySeconds).isEqualTo(20) - } - - @Test - @Transactional - @Throws(Exception::class) - fun deleteOAuthClient() { - restOauthClientMvc.perform( - MockMvcRequestBuilders.delete("/api/oauth-clients/" + details.clientId) - .contentType(TestUtil.APPLICATION_JSON_UTF8).content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isOk()) - val clientDetailsList = clientDetailsService.listClientDetails() - Assertions.assertThat(clientDetailsList.size).isEqualTo(databaseSizeBeforeCreate) - } - - @Test - @Transactional - @Throws(Exception::class) - fun cannotModifyProtected() { - // first change our test client to be protected - details.additionalInformation!!["protected"] = "true" - restOauthClientMvc.perform( - MockMvcRequestBuilders.put("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isOk()) - - // expect we can not delete it now - restOauthClientMvc.perform( - MockMvcRequestBuilders.delete("/api/oauth-clients/" + details.clientId) - .contentType(TestUtil.APPLICATION_JSON_UTF8).content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isForbidden()) - - // expect we can not update it now - details.refreshTokenValiditySeconds = 20L - restOauthClientMvc.perform( - MockMvcRequestBuilders.put("/api/oauth-clients").contentType(TestUtil.APPLICATION_JSON_UTF8) - .content(TestUtil.convertObjectToJsonBytes(details)) - ).andExpect(MockMvcResultMatchers.status().isForbidden()) - } -} From 5e6f20dde0621852fc36f0ea7468bc014023c319 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 00:00:50 +0100 Subject: [PATCH 25/68] Restore ory stack changes --- src/main/docker/ory_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index eb94e4391..acac23a8c 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -10,7 +10,7 @@ services: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 ports: - - "3002:4455" + - "3000:3000" volumes: - /tmp/ui-node/logs:/root/.npm/_logs From eaa7d33545ebf919004556b56a8eda7f85caa838 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 12:54:47 +0100 Subject: [PATCH 26/68] Update JwtAuthenticationFilter issues: make sure existing auth is checked and security context is cleared after --- .../config/SecurityConfiguration.kt | 5 +- .../security/JwtAuthenticationFilter.kt | 174 +++++++++--------- 2 files changed, 86 insertions(+), 93 deletions(-) diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 446f691b4..0b1c33885 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -76,6 +76,9 @@ class SecurityConfiguration @Bean fun jwtAuthenticationFilter(): JwtAuthenticationFilter = JwtAuthenticationFilter(tokenValidator, authenticationManager()) + .skipUrlPattern(HttpMethod.GET, "/") + .skipUrlPattern(HttpMethod.GET, "/*.{js,ico,css,html}") + .skipUrlPattern(HttpMethod.GET, "/i18n/**") .skipUrlPattern(HttpMethod.GET, "/management/health") .skipUrlPattern(HttpMethod.POST, "/oauth/token") .skipUrlPattern(HttpMethod.GET, "/api/meta-token/*") @@ -103,10 +106,10 @@ class SecurityConfiguration .antMatchers("/api-docs/**") .antMatchers("/swagger-ui.html") .antMatchers("/api-docs{,.json,.yml}") - .antMatchers("/api/login") .antMatchers("/api/logout-url") .antMatchers("/api/profile-info") .antMatchers("/api/activate") + .antMatchers("/api/sitesettings") .antMatchers("/api/redirect/**") .antMatchers("/api/account/reset_password/init") .antMatchers("/api/account/reset_password/finish") diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index e665cfed1..d2c4de919 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -31,8 +31,8 @@ class JwtAuthenticationFilter( private val ignoreUrls: MutableList = mutableListOf() fun skipUrlPattern(method: HttpMethod, vararg antPatterns: String?): JwtAuthenticationFilter { - antPatterns.forEach { pattern -> - pattern?.let { ignoreUrls.add(AntPathRequestMatcher(it, method.name)) } + for (pattern in antPatterns) { + ignoreUrls.add(AntPathRequestMatcher(pattern, method.name)) } return this } @@ -41,131 +41,121 @@ class JwtAuthenticationFilter( override fun doFilterInternal( httpRequest: HttpServletRequest, httpResponse: HttpServletResponse, - chain: FilterChain, + chain: FilterChain ) { - logger.debug("Processing request: ${httpRequest.requestURI}") - - if (CorsUtils.isPreFlightRequest(httpRequest)) { - logger.debug("Skipping JWT check for preflight request") - chain.doFilter(httpRequest, httpResponse) - return - } - - val existingAuthentication = SecurityContextHolder.getContext().authentication - val stringToken = tokenFromHeader(httpRequest) - var token: RadarToken? = null - - if (stringToken != null) { - token = validateTokenFromHeader(stringToken, httpRequest) - } + try { + if (CorsUtils.isPreFlightRequest(httpRequest)) { + logger.debug("Skipping JWT check for ${httpRequest.requestURI}") + chain.doFilter(httpRequest, httpResponse) + return + } + val stringToken = tokenFromHeader(httpRequest) + var token: RadarToken? = null + var exMessage = "No token provided" + + if (stringToken != null) { + try { + logger.warn("Validating token from header: $stringToken") + token = validator.validateBlocking(stringToken) + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + logger.debug("JWT authentication successful") + } catch (ex: TokenValidationException) { + exMessage = ex.message ?: exMessage + logger.info("Failed to validate token from header: $exMessage") + } + } - if (token == null && existingAuthentication.isAnonymous) { - token = validateTokenFromSession(httpRequest.session) - } + if (token == null) { + val existingAuthentication = SecurityContextHolder.getContext().authentication + if (existingAuthentication != null && + existingAuthentication.isAuthenticated && + !existingAuthentication.isAnonymous + ) { + logger.info("Existing authentication found: ${existingAuthentication}") + chain.doFilter(httpRequest, httpResponse) + return + } + + val session = httpRequest.getSession(false) + token = session?.radarToken?.takeIf { Instant.now() < it.expiresAt } + if (token != null) { + logger.debug("Using token from session") + val authentication = createAuthenticationFromToken(token) + SecurityContextHolder.getContext().authentication = authentication + } + } - if (!validateAndSetAuthentication(token, httpRequest, httpResponse)) { - return + if (!validateToken(token, httpRequest, httpResponse)) { + return + } + chain.doFilter(httpRequest, httpResponse) + } finally { + SecurityContextHolder.clearContext() } + } - chain.doFilter(httpRequest, httpResponse) + private fun createAuthenticationFromToken(token: RadarToken): Authentication { + val authentication = RadarAuthentication(token) + return authenticationManager.authenticate(authentication) } - private fun validateTokenFromHeader( - tokenString: String, - httpRequest: HttpServletRequest - ): RadarToken? { - return try { - logger.debug("Validating token from header: ${tokenString}") - val token = validator.validateBlocking(tokenString) - val authentication = createAuthenticationFromToken(token) - SecurityContextHolder.getContext().authentication = authentication - logger.debug("JWT authentication successful") - token - } catch (ex: TokenValidationException) { - logger.warn("Token validation failed: ${ex.message}") - null + override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { + val shouldNotFilterUrl = ignoreUrls.find { it.matches(httpRequest) } + return if (shouldNotFilterUrl != null) { + logger.debug("Skipping JWT check for ${httpRequest.requestURI}") + true + } else { + false } } - private fun validateTokenFromSession(session: HttpSession?): RadarToken? { - val token = session?.radarToken?.takeIf { Instant.now() < it.expiresAt } - if (token != null) { - logger.debug("Using token from session") - val authentication = createAuthenticationFromToken(token) - SecurityContextHolder.getContext().authentication = authentication - } - return token + private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { + return httpRequest + .getHeader(HttpHeaders.AUTHORIZATION) + ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } + ?.removePrefix(AUTHORIZATION_BEARER_HEADER) + ?.trim { it <= ' ' } } - private fun validateAndSetAuthentication( + private fun validateToken( token: RadarToken?, httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse + httpResponse: HttpServletResponse, ): Boolean { return if (token != null) { httpRequest.radarToken = token - val authentication = createAuthenticationFromToken(token) + val authentication = RadarAuthentication(token) + authenticationManager.authenticate(authentication) SecurityContextHolder.getContext().authentication = authentication true + } else if (isOptional) { + logger.debug("Skipping optional token check for ${httpRequest.requestURI}") + true } else { - handleUnauthorized(httpRequest, httpResponse, "No valid token provided") + logger.error("Unauthorized - no valid token provided for ${httpRequest.requestURI}") + httpResponse.returnUnauthorized(httpRequest) false } } - private fun handleUnauthorized( - httpRequest: HttpServletRequest, - httpResponse: HttpServletResponse, - message: String - ) { - if (!isOptional) { - logger.error("Unauthorized - ${message}") - httpResponse.returnUnauthorized(httpRequest, message) - } - } - - private fun createAuthenticationFromToken(token: RadarToken): Authentication { - val authentication = RadarAuthentication(token) - return authenticationManager.authenticate(authentication) - } - - override fun shouldNotFilter(@Nonnull httpRequest: HttpServletRequest): Boolean { - return ignoreUrls.any { it.matches(httpRequest) }.also { shouldSkip -> - if (shouldSkip) { - logger.debug("Skipping JWT check for ${httpRequest.requestURL}") - } - } - } - - private fun tokenFromHeader(httpRequest: HttpServletRequest): String? { - return httpRequest - .getHeader(HttpHeaders.AUTHORIZATION) - ?.takeIf { it.startsWith(AUTHORIZATION_BEARER_HEADER) } - ?.removePrefix(AUTHORIZATION_BEARER_HEADER) - ?.trim() - } - companion object { - private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) - private const val AUTHORIZATION_BEARER_HEADER = "Bearer" - private const val TOKEN_ATTRIBUTE = "jwt" - - private fun HttpServletResponse.returnUnauthorized( - request: HttpServletRequest, - message: String? - ) { + private fun HttpServletResponse.returnUnauthorized(request: HttpServletRequest) { status = HttpServletResponse.SC_UNAUTHORIZED setHeader(HttpHeaders.WWW_AUTHENTICATE, AUTHORIZATION_BEARER_HEADER) outputStream.print( """ {"error": "Unauthorized", "status": "${HttpServletResponse.SC_UNAUTHORIZED}", - "message": "${message ?: "null"}", "path": "${request.requestURI}"} - """.trimIndent() + """.trimIndent() ) } + private val logger = LoggerFactory.getLogger(JwtAuthenticationFilter::class.java) + private const val AUTHORIZATION_BEARER_HEADER = "Bearer" + private const val TOKEN_ATTRIBUTE = "jwt" + var HttpSession.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) From 960a3b55bfcd0473a9860cebf4177780f01d9015 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 12:59:15 +0100 Subject: [PATCH 27/68] Restore deleted annotations --- .../radarbase/management/security/JwtAuthenticationFilter.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index d2c4de919..811c712b1 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -156,10 +156,14 @@ class JwtAuthenticationFilter( private const val AUTHORIZATION_BEARER_HEADER = "Bearer" private const val TOKEN_ATTRIBUTE = "jwt" + @get:JvmStatic + @set:JvmStatic var HttpSession.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) + @get:JvmStatic + @set:JvmStatic var HttpServletRequest.radarToken: RadarToken? get() = getAttribute(TOKEN_ATTRIBUTE) as RadarToken? set(value) = setAttribute(TOKEN_ATTRIBUTE, value) From c11734eb0cc26a96ad90dd21c81354d36b76399d Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 13:30:43 +0100 Subject: [PATCH 28/68] Move access token fetching to AuthService --- .../management/service/AuthService.kt | 113 ++++++++++++---- .../management/web/rest/LoginEndpoint.kt | 121 +++++------------- 2 files changed, 121 insertions(+), 113 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/AuthService.kt b/src/main/java/org/radarbase/management/service/AuthService.kt index 07929fc7e..6433af584 100644 --- a/src/main/java/org/radarbase/management/service/AuthService.kt +++ b/src/main/java/org/radarbase/management/service/AuthService.kt @@ -1,61 +1,91 @@ package org.radarbase.management.service +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.http.* +import io.ktor.serialization.kotlinx.json.* +import java.time.Duration +import java.util.* +import java.util.function.Consumer +import javax.annotation.Nullable import kotlinx.coroutines.runBlocking +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.jsonPrimitive import org.radarbase.auth.authorization.* +import org.radarbase.auth.exception.IdpException import org.radarbase.auth.token.RadarToken +import org.radarbase.management.config.ManagementPortalProperties import org.radarbase.management.security.NotAuthorizedException +import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service -import java.util.* -import java.util.function.Consumer -import javax.annotation.Nullable @Service class AuthService( - @Nullable - private val token: RadarToken?, - private val oracle: AuthorizationOracle, + @Nullable private val token: RadarToken?, + private val oracle: AuthorizationOracle, + @Autowired private val managementPortalProperties: ManagementPortalProperties, ) { + private val httpClient = + HttpClient(CIO) { + install(HttpTimeout) { + connectTimeoutMillis = Duration.ofSeconds(10).toMillis() + socketTimeoutMillis = Duration.ofSeconds(10).toMillis() + requestTimeoutMillis = Duration.ofSeconds(300).toMillis() + } + install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } + } /** - * Check whether given [token] would have the [permission] scope in any of its roles. This doesn't - * check whether [token] has access to a specific entity or global access. + * Check whether given [token] would have the [permission] scope in any of its roles. This + * doesn't check whether [token] has access to a specific entity or global access. * @throws NotAuthorizedException if identity does not have scope */ @Throws(NotAuthorizedException::class) fun checkScope(permission: Permission) { - val token = token ?: throw NotAuthorizedException("User without authentication does not have permission.") + val token = + token + ?: throw NotAuthorizedException( + "User without authentication does not have permission." + ) if (!oracle.hasScope(token, permission)) { throw NotAuthorizedException( - "User ${token.username} with client ${token.clientId} does not have permission $permission" + "User ${token.username} with client ${token.clientId} does not have permission $permission" ) } } /** - * Check whether [token] has permission [permission], regarding given entity from [builder]. - * The permission is checked both for its - * own entity scope and for the [EntityDetails.minimumEntityOrNull] entity scope. + * Check whether [token] has permission [permission], regarding given entity from [builder]. The + * permission is checked both for its own entity scope and for the + * [EntityDetails.minimumEntityOrNull] entity scope. * @throws NotAuthorizedException if identity does not have permission */ @JvmOverloads @Throws(NotAuthorizedException::class) fun checkPermission( - permission: Permission, - builder: Consumer? = null, - scope: Permission.Entity = permission.entity, + permission: Permission, + builder: Consumer? = null, + scope: Permission.Entity = permission.entity, ) { - val token = token ?: throw NotAuthorizedException("User without authentication does not have permission.") + val token = + token + ?: throw NotAuthorizedException( + "User without authentication does not have permission." + ) // entitydetails builder is null means we require global permission val entity = if (builder != null) entityDetailsBuilder(builder) else EntityDetails.global - val hasPermission = runBlocking { - oracle.hasPermission(token, permission, entity, scope) - } + val hasPermission = runBlocking { oracle.hasPermission(token, permission, entity, scope) } if (!hasPermission) { throw NotAuthorizedException( - "User ${token.username} with client ${token.clientId} does not have permission $permission to scope " + - "$scope of $entity" + "User ${token.username} with client ${token.clientId} does not have permission $permission to scope " + + "$scope of $entity" ) } } @@ -65,11 +95,42 @@ class AuthService( return oracle.referentsByScope(token, permission) } - fun mayBeGranted(role: RoleAuthority, permission: Permission): Boolean = with(oracle) { - role.mayBeGranted(permission) - } + fun mayBeGranted(role: RoleAuthority, permission: Permission): Boolean = + with(oracle) { role.mayBeGranted(permission) } fun mayBeGranted(authorities: Collection, permission: Permission): Boolean { - return authorities.any{ mayBeGranted(it, permission) } + return authorities.any { mayBeGranted(it, permission) } + } + + suspend fun fetchAccessToken(code: String): String { + val tokenUrl = "${managementPortalProperties.authServer.serverUrl}/oauth2/token" + val response = + httpClient.post(tokenUrl) { + contentType(ContentType.Application.FormUrlEncoded) + accept(ContentType.Application.Json) + setBody( + Parameters.build { + append("grant_type", "authorization_code") + append("code", code) + append( + "redirect_uri", + "${managementPortalProperties.common.baseUrl}/api/redirect/login" + ) + append( + "client_id", + managementPortalProperties.frontend.clientId + ) + } + .formUrlEncode(), + ) + } + + if (response.status.isSuccess()) { + val responseMap = response.body>() + return responseMap["access_token"]?.jsonPrimitive?.content + ?: throw IdpException("Access token not found in response") + } else { + throw IdpException("Unable to get access token") + } } } diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index 202e701ae..a61b73b6d 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -1,107 +1,54 @@ package org.radarbase.management.web.rest -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.engine.cio.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.request.* -import io.ktor.http.* -import io.ktor.serialization.kotlinx.json.* -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.jsonPrimitive -import org.radarbase.auth.exception.IdpException +import java.time.Instant import org.radarbase.management.config.ManagementPortalProperties -import org.slf4j.LoggerFactory +import org.radarbase.management.service.AuthService import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.servlet.view.RedirectView -import java.time.Duration -import java.time.Instant @RestController @RequestMapping("/api") class LoginEndpoint - @Autowired - constructor( +@Autowired +constructor( private val managementPortalProperties: ManagementPortalProperties, - ) { - private val httpClient = - HttpClient(CIO) { - install(HttpTimeout) { - connectTimeoutMillis = Duration.ofSeconds(10).toMillis() - socketTimeoutMillis = Duration.ofSeconds(10).toMillis() - requestTimeoutMillis = Duration.ofSeconds(300).toMillis() - } - install(ContentNegotiation) { - json(Json { ignoreUnknownKeys = true }) - } - } + @Autowired private val authService: AuthService +) { - @GetMapping("/redirect/login") - suspend fun loginRedirect( + @GetMapping("/redirect/login") + suspend fun loginRedirect( @RequestParam(required = false) code: String?, - ): RedirectView { - val redirectView = RedirectView() - val config = managementPortalProperties - val mpUrl = config.common.baseUrl - - if (code == null) { - redirectView.url = buildAuthUrl(config, mpUrl) - } else { - val accessToken = fetchAccessToken(code, config) - redirectView.url = "$mpUrl/#/?access_token=$accessToken" - } - return redirectView - } - - @GetMapping("/redirect/account") - fun settingsRedirect(): RedirectView { - val redirectView = RedirectView() - redirectView.url = "${managementPortalProperties.identityServer.loginUrl}/settings" - return redirectView - } - - private fun buildAuthUrl(config: ManagementPortalProperties, mpUrl: String): String { - return "${config.authServer.loginUrl}/oauth2/auth?" + - "client_id=${config.frontend.clientId}&" + - "response_type=code&" + - "state=${Instant.now()}&" + - "audience=res_ManagementPortal&" + - "scope=offline&" + - "redirect_uri=$mpUrl/api/redirect/login" + ): RedirectView { + val redirectView = RedirectView() + + if (code == null) { + redirectView.url = buildAuthUrl() + } else { + val accessToken = authService.fetchAccessToken(code) + redirectView.url = + "${managementPortalProperties.common.baseUrl}/#/?access_token=$accessToken" } + return redirectView + } - private suspend fun fetchAccessToken( - code: String, - config: ManagementPortalProperties, - ): String { - val tokenUrl = "${config.authServer.serverUrl}/oauth2/token" - val response = - httpClient.post(tokenUrl) { - contentType(ContentType.Application.FormUrlEncoded) - accept(ContentType.Application.Json) - setBody( - Parameters - .build { - append("grant_type", "authorization_code") - append("code", code) - append("redirect_uri", "${config.common.baseUrl}/api/redirect/login") - append("client_id", config.frontend.clientId) - }.formUrlEncode(), - ) - } + @GetMapping("/redirect/account") + fun settingsRedirect(): RedirectView { + val redirectView = RedirectView() + redirectView.url = "${managementPortalProperties.identityServer.loginUrl}/settings" + return redirectView + } - if (response.status.isSuccess()) { - val responseMap = response.body>() - return responseMap["access_token"]?.jsonPrimitive?.content - ?: throw IdpException("Access token not found in response") - } else { - throw IdpException("Unable to get access token") - } - } + private fun buildAuthUrl(): String { + return "${managementPortalProperties.authServer.loginUrl}/oauth2/auth?" + + "client_id=${managementPortalProperties.frontend.clientId}&" + + "response_type=code&" + + "state=${Instant.now()}&" + + "audience=res_ManagementPortal&" + + "scope=offline&" + + "redirect_uri=${managementPortalProperties.common.baseUrl}/api/redirect/login" } +} From 2988114858a7cd4b05c6849af4f8ceef233b55e1 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 13:30:59 +0100 Subject: [PATCH 29/68] Invalidate session on logout --- .../org/radarbase/management/config/SecurityConfiguration.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 0b1c33885..230b39652 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -134,6 +134,9 @@ class SecurityConfiguration ) .authorizeRequests() .anyRequest().authenticated() + .and() + .logout().invalidateHttpSession(true) + .logoutUrl("/api/logout") } @Bean From 98110f71444befac83dc5b3aae8085420579759d Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 13:32:51 +0100 Subject: [PATCH 30/68] Update kratos postgres --- src/main/docker/ory_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index 0a9ef9929..acac23a8c 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -23,7 +23,7 @@ services: - "4434:4434" # admin, should be closed in production restart: unless-stopped environment: - - DSN=postgres://kratos:secret@postgresd-kratos/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 + - DSN=postgres://ory:secret@postgresd-ory/kratos?sslmode=disable&max_conns=20&max_idle_conns=4 - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_HOOK=web_hook - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_METHOD=POST - SELFSERVICE_FLOWS_REGISTRATION_AFTER_PASSWORD_HOOKS_0_CONFIG_URL=http://managementportal:8080/managementportal/api/kratos/subjects From 2c4683c34d9eeac9fa75460fc1719714aa4bf98a Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 14:04:45 +0100 Subject: [PATCH 31/68] Fix error component login url and remove unnecessary logs --- .../radarbase/management/security/JwtAuthenticationFilter.kt | 1 - src/main/webapp/app/layouts/error/error.component.ts | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt index 811c712b1..d60b5e8ab 100644 --- a/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt +++ b/src/main/java/org/radarbase/management/security/JwtAuthenticationFilter.kt @@ -72,7 +72,6 @@ class JwtAuthenticationFilter( existingAuthentication.isAuthenticated && !existingAuthentication.isAnonymous ) { - logger.info("Existing authentication found: ${existingAuthentication}") chain.doFilter(httpRequest, httpResponse) return } diff --git a/src/main/webapp/app/layouts/error/error.component.ts b/src/main/webapp/app/layouts/error/error.component.ts index c439100c1..31ac48f78 100644 --- a/src/main/webapp/app/layouts/error/error.component.ts +++ b/src/main/webapp/app/layouts/error/error.component.ts @@ -15,7 +15,6 @@ export class ErrorComponent implements OnInit, OnDestroy { error403: boolean; modalRef: NgbModalRef; private routeSubscription: Subscription; - private loginUrl = 'oauth/login'; constructor( private loginModalService: LoginModalService, @@ -35,6 +34,6 @@ export class ErrorComponent implements OnInit, OnDestroy { } login() { - window.location.href = this.loginUrl; + window.location.href = ''; } } \ No newline at end of file From 12b7253469c90a525d779d88910d81715dd8d94e Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 14:05:35 +0100 Subject: [PATCH 32/68] Update dependencies --- build.gradle | 13 +++++++------ gradle.properties | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/build.gradle b/build.gradle index 83c1dbf69..f1b3720b6 100644 --- a/build.gradle +++ b/build.gradle @@ -159,11 +159,12 @@ dependencies { implementation("io.ktor:ktor-client-cio") implementation("io.ktor:ktor-client-content-negotiation") implementation("io.ktor:ktor-serialization-kotlinx-json") - runtimeOnly "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" - implementation "com.fasterxml.jackson.datatype:jackson-datatype-hibernate5" - implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-csv" + implementation("com.fasterxml.jackson.core:jackson-core:${jackson_version}") + runtimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jackson_version}") + implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:${jackson_version}") + implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv:${jackson_version}") implementation "com.fasterxml.jackson.core:jackson-annotations" - implementation "com.fasterxml.jackson.core:jackson-databind" + implementation("com.fasterxml.jackson.core:jackson-databind:${jackson_version}") implementation "com.hazelcast:hazelcast:${hazelcast_version}" implementation "com.hazelcast:hazelcast-spring:${hazelcast_version}" runtimeOnly "com.hazelcast:hazelcast-hibernate53:${hazelcast_hibernate_version}" @@ -175,7 +176,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-autoconfigure") implementation "org.springframework.boot:spring-boot-starter-mail" runtimeOnly "org.springframework.boot:spring-boot-starter-logging" - runtimeOnly ("org.springframework.boot:spring-boot-starter-data-jpa") { + runtimeOnly("org.springframework.boot:spring-boot-starter-data-jpa") { exclude group: 'org.hibernate', module: 'hibernate-entitymanager' } implementation "org.springframework.security:spring-security-data" @@ -184,7 +185,7 @@ dependencies { exclude module: 'spring-boot-starter-tomcat' } runtimeOnly "org.springframework.boot:spring-boot-starter-security" - implementation ("org.springframework.boot:spring-boot-starter-undertow") + implementation("org.springframework.boot:spring-boot-starter-undertow") implementation "org.hibernate:hibernate-core" implementation "org.hibernate:hibernate-envers" diff --git a/gradle.properties b/gradle.properties index 2d5eaf130..6684b33e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -39,7 +39,7 @@ micrometer_version=1.12.3 hibernate_orm_version=6.4.4.Final hibernate_validator_version=8.0.0.Final testcontainers_version=1.19.7 -undertow_version=2.2.33.Final +undertow_version=2.2.34.Final kotlin.code.style=official org.gradle.vfs.watch=true From afbe7b93445470c63c6c9c9097cf3ffde3417916 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 14:18:06 +0100 Subject: [PATCH 33/68] Fix jackson version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6684b33e0..34bb975dd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ thymeleaf_version=3.1.2.RELEASE spring_session_version=2021.2.0 gatling_version=3.8.4 mapstruct_version=1.5.5.Final -jackson_version=2.16.1 +jackson_version=2.15.0 javax_xml_bind_version=2.3.3 javax_jaxb_core_version=2.3.0.1 javax_jaxb_runtime_version=2.3.8 From 6a07d690d0673360c41ae0b19ba17c24bdc17e94 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 14:37:18 +0100 Subject: [PATCH 34/68] Fix jackson version --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f1b3720b6..173868dd7 100644 --- a/build.gradle +++ b/build.gradle @@ -163,7 +163,7 @@ dependencies { runtimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jackson_version}") implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:${jackson_version}") implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-csv:${jackson_version}") - implementation "com.fasterxml.jackson.core:jackson-annotations" + implementation "com.fasterxml.jackson.core:jackson-annotations:${jackson_version}" implementation("com.fasterxml.jackson.core:jackson-databind:${jackson_version}") implementation "com.hazelcast:hazelcast:${hazelcast_version}" implementation "com.hazelcast:hazelcast-spring:${hazelcast_version}" From 57419935186e7b97249a526294da4836bd5bb335 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 23 Aug 2024 15:43:12 +0100 Subject: [PATCH 35/68] Remove unused TokenKeyEndpoint --- .../config/SecurityConfiguration.kt | 2 +- .../management/web/rest/TokenKeyEndpoint.kt | 30 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) delete mode 100644 src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index 230b39652..abe0ad339 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -5,7 +5,7 @@ import org.radarbase.auth.jwks.JwkAlgorithmParser import org.radarbase.auth.jwks.JwksTokenVerifierLoader import org.radarbase.management.repository.UserRepository import org.radarbase.management.security.Http401UnauthorizedEntryPoint -import org.radarbase.management.security.JwtAuthenticationFilter // Make sure to import this +import org.radarbase.management.security.JwtAuthenticationFilter import org.radarbase.management.security.RadarAuthenticationProvider import org.springframework.beans.factory.BeanInitializationException import org.springframework.beans.factory.annotation.Autowired diff --git a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt deleted file mode 100644 index 2e7e5c400..000000000 --- a/src/main/java/org/radarbase/management/web/rest/TokenKeyEndpoint.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.radarbase.management.web.rest - -import io.micrometer.core.annotation.Timed -import org.radarbase.auth.jwks.JsonWebKeySet -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.RestController - -//@RestController -class TokenKeyEndpoint @Autowired constructor( -) { - @get:Timed - @get:GetMapping("/oauth/token_key") - val key: JsonWebKeySet? - /** - * Get the verification key for the token signatures. The principal has to - * be provided only if the key is secret - * - * @return the key used to verify tokens - */ - get() { - logger.debug("Requesting verifier public keys...") - return null - } - - companion object { - private val logger = LoggerFactory.getLogger(TokenKeyEndpoint::class.java) - } -} From 50dc20b32d1333e42a844322ac496eda6db26126 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 28 Aug 2024 16:00:58 +0100 Subject: [PATCH 36/68] Add kratos webhook endpoint for creating subjects through webhook --- .../radarbase/auth/kratos/KratosSessionDTO.kt | 26 +- .../config/SecurityConfiguration.kt | 2 + .../management/service/IdentityService.kt | 387 ++++++++++-------- .../management/service/SubjectService.kt | 381 ++++++++++------- .../service/dto/KratosSubjectWebhookDTO.kt | 28 ++ .../management/web/rest/KratosEndpoint.kt | 114 ++++++ 6 files changed, 604 insertions(+), 334 deletions(-) create mode 100644 src/main/java/org/radarbase/management/service/dto/KratosSubjectWebhookDTO.kt create mode 100644 src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt index 218792df8..805c71a68 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt @@ -46,14 +46,14 @@ class KratosSessionDTO( @Serializable class Identity( - val id: String? = null, + var id: String? = null, val schema_id: String? = null, val schema_url: String? = null, val state: String? = null, @Serializable(with = InstantSerializer::class) val state_changed_at: Instant? = null, val traits: Traits? = null, - val metadata_public: Metadata? = null, + var metadata_public: Metadata? = null, @Serializable(with = InstantSerializer::class) val created_at: Instant? = null, @Serializable(with = InstantSerializer::class) @@ -63,16 +63,16 @@ class KratosSessionDTO( fun parseRoles(): Set = buildSet { - if (metadata_public?.authorities?.isNotEmpty() == true) { - for (roleValue in metadata_public.authorities) { + metadata_public?.authorities?.takeIf { it.isNotEmpty() }?.let { authorities -> + for (roleValue in authorities) { val authority = RoleAuthority.valueOfAuthorityOrNull(roleValue) if (authority?.scope == RoleAuthority.Scope.GLOBAL) { add(AuthorityReference(authority)) } } - } - if (metadata_public?.roles?.isNotEmpty() == true) { - for (roleValue in metadata_public.roles) { + } + metadata_public?.roles?.takeIf { it.isNotEmpty() }?.let { roles -> + for (roleValue in roles) { val role = RoleAuthority.valueOfAuthorityOrNull(roleValue) if (role?.scope == RoleAuthority.Scope.GLOBAL) { add(AuthorityReference(role)) @@ -91,12 +91,12 @@ class KratosSessionDTO( @Serializable class Metadata ( - val roles: List, - val authorities: Set, - val scope: List, - val sources: List, - val aud: List, - val mp_login: String? + val roles: List = emptyList(), + val authorities: Set = emptySet(), + val scope: List = emptyList(), + val sources: List = emptyList(), + val aud: List = emptyList(), + val mp_login: String? = null, ) fun toDataRadarToken() : DataRadarToken { diff --git a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt index abe0ad339..efa983229 100644 --- a/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt +++ b/src/main/java/org/radarbase/management/config/SecurityConfiguration.kt @@ -87,6 +87,7 @@ class SecurityConfiguration .skipUrlPattern(HttpMethod.GET, "/api/redirect/**") .skipUrlPattern(HttpMethod.GET, "/api/profile-info") .skipUrlPattern(HttpMethod.GET, "/api/logout-url") + .skipUrlPattern(HttpMethod.POST, "/api/kratos/**") .skipUrlPattern(HttpMethod.GET, "/oauth2/authorize") .skipUrlPattern(HttpMethod.GET, "/images/**") .skipUrlPattern(HttpMethod.GET, "/css/**") @@ -111,6 +112,7 @@ class SecurityConfiguration .antMatchers("/api/activate") .antMatchers("/api/sitesettings") .antMatchers("/api/redirect/**") + .antMatchers("/api/kratos/**") .antMatchers("/api/account/reset_password/init") .antMatchers("/api/account/reset_password/finish") .antMatchers("/test/**") diff --git a/src/main/java/org/radarbase/management/service/IdentityService.kt b/src/main/java/org/radarbase/management/service/IdentityService.kt index 672626188..e0b0bd202 100644 --- a/src/main/java/org/radarbase/management/service/IdentityService.kt +++ b/src/main/java/org/radarbase/management/service/IdentityService.kt @@ -8,6 +8,7 @@ import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* +import java.time.Duration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json @@ -16,214 +17,272 @@ import org.radarbase.auth.authorization.RoleAuthority import org.radarbase.auth.exception.IdpException import org.radarbase.auth.kratos.KratosSessionDTO import org.radarbase.management.config.ManagementPortalProperties -import org.radarbase.management.domain.Role import org.radarbase.management.domain.User +import org.radarbase.management.service.dto.UserDTO import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.time.Duration -/** - * Service class for managing identities. - */ +/** Service class for managing identities. */ @Service @Transactional -class IdentityService( - @Autowired private val managementPortalProperties: ManagementPortalProperties, - @Autowired private val authService: AuthService +class IdentityService +@Autowired +constructor( + private val managementPortalProperties: ManagementPortalProperties, + private val authService: AuthService ) { - private val httpClient = HttpClient(CIO).config { - install(HttpTimeout) { - connectTimeoutMillis = Duration.ofSeconds(10).toMillis() - socketTimeoutMillis = Duration.ofSeconds(10).toMillis() - requestTimeoutMillis = Duration.ofSeconds(300).toMillis() - } - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - coerceInputValues = true - }) - } - } + private val httpClient = + HttpClient(CIO) { + install(HttpTimeout) { + connectTimeoutMillis = Duration.ofSeconds(10).toMillis() + socketTimeoutMillis = Duration.ofSeconds(10).toMillis() + requestTimeoutMillis = Duration.ofSeconds(300).toMillis() + } + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + coerceInputValues = true + } + ) + } + } - lateinit var adminUrl: String - lateinit var publicUrl: String + private val adminUrl = managementPortalProperties.identityServer.adminUrl() + private val publicUrl = managementPortalProperties.identityServer.publicUrl() init { - adminUrl = managementPortalProperties.identityServer.adminUrl() - publicUrl = managementPortalProperties.identityServer.publicUrl() - - log.debug("kratos serverUrl set to ${managementPortalProperties.identityServer.publicUrl()}") - log.debug("kratos serverAdminUrl set to ${managementPortalProperties.identityServer.adminUrl()}") + log.debug("Kratos serverUrl set to $publicUrl") + log.debug("Kratos serverAdminUrl set to $adminUrl") } - /** Save a [User] to the IDP as an identity. Returns the generated [KratosSessionDTO.Identity] */ + /** + * Convert a [User] to a [KratosSessionDTO.Identity] object. + * @param user The object to convert + * @return the newly created DTO object + */ @Throws(IdpException::class) - suspend fun saveAsIdentity(user: User): KratosSessionDTO.Identity? { - val kratosIdentity: KratosSessionDTO.Identity? - - withContext(Dispatchers.IO) { - val identity = createIdentity(user) - - val postRequestBuilder = HttpRequestBuilder().apply { - url("${adminUrl}/admin/identities") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - setBody(identity) - } - val response = httpClient.post(postRequestBuilder) - - if (response.status.isSuccess()) { - kratosIdentity = response.body() - log.debug("saved identity for user ${user.login} to IDP as ${kratosIdentity.id}") - } else { - throw IdpException( - "couldn't save Kratos ID to server at " + adminUrl, + private fun createIdentity(user: User): KratosSessionDTO.Identity = + try { + KratosSessionDTO.Identity( + schema_id = "researcher", + traits = KratosSessionDTO.Traits(email = user.email), + metadata_public = + KratosSessionDTO.Metadata( + aud = emptyList(), + sources = emptyList(), + roles = + user.roles.mapNotNull { role -> + role.authority?.name?.let { auth -> + when (role.role?.scope) { + RoleAuthority.Scope.GLOBAL -> auth + RoleAuthority.Scope.ORGANIZATION -> + "${role.organization!!.name}:$auth" + RoleAuthority.Scope.PROJECT -> + "${role.project!!.projectName}:$auth" + null -> null + } + } + }, + authorities = user.authorities, + scope = + Permission.scopes().filter { scope -> + authService.mayBeGranted( + user.roles.mapNotNull { it.role }, + Permission.ofScope(scope) + ) + }, + mp_login = user.login + ) ) + } catch (e: Throwable) { + val message = "Could not convert user ${user.login} to identity" + log.error(message) + throw IdpException(message, e) } - } - - return kratosIdentity - } - /** Update a [User] as to the IDP as an identity. Returns the updated [KratosSessionDTO.Identity] */ + /** + * Save a [User] to the IDP as an identity. Returns the generated [KratosSessionDTO.Identity] + */ @Throws(IdpException::class) - suspend fun updateAssociatedIdentity(user: User): KratosSessionDTO.Identity? { - val kratosIdentity: KratosSessionDTO.Identity? - - user.identity ?: throw IdpException( - "user ${user.login} could not be updated on the IDP. No identity was set", - ) - - withContext(Dispatchers.IO) { - val identity = createIdentity(user) - val response = httpClient.put { - url("${adminUrl}/admin/identities/${user.identity}") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - setBody(identity) + suspend fun saveAsIdentity(user: User): KratosSessionDTO.Identity = + withContext(Dispatchers.IO) { + val identity = createIdentity(user) + val response = + httpClient.post { + url("$adminUrl/admin/identities") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + setBody(identity) + } + + if (response.status.isSuccess()) { + response.body().also { + log.debug("Saved identity for user ${user.login} to IDP as ${it.id}") + } + } else { + throw IdpException("Couldn't save Kratos ID to server at $adminUrl") + } } + /** + * Update a [User] to the IDP as an identity. Returns the updated [KratosSessionDTO.Identity] + */ + @Throws(IdpException::class) + suspend fun updateAssociatedIdentity(user: User): KratosSessionDTO.Identity = + withContext(Dispatchers.IO) { + val identityId = + user.identity + ?: throw IdpException( + "User ${user.login} could not be updated on the IDP. No identity was set" + ) - if (response.status.isSuccess()) { - kratosIdentity = response.body() - log.debug("Updated identity for user ${user.login} to IDP as ${kratosIdentity.id}") - } else { - throw IdpException( - "Couldn't update identity to server at $adminUrl" - ) - } - } + val identity = createIdentity(user) + val response = + httpClient.put { + url("$adminUrl/admin/identities/$identityId") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + setBody(identity) + } - return kratosIdentity - } + if (response.status.isSuccess()) { + response.body().also { + log.debug("Updated identity for user ${user.login} on IDP as ${it.id}") + } + } else { + throw IdpException("Couldn't update identity on server at $adminUrl") + } + } - /** Delete a [User] as to the IDP as an identity. */ + /** + * Update [KratosSessionDTO.Identity] metadata with user roles. Returns the updated + * [KratosSessionDTO.Identity] + */ @Throws(IdpException::class) - suspend fun deleteAssociatedIdentity(userIdentity: String?) { - withContext(Dispatchers.IO) { - userIdentity ?: throw IdpException( - "user with ID ${userIdentity} could not be deleted from the IDP. No identity was set" - ) - - val response = httpClient.delete { - url("${adminUrl}/admin/identities/${userIdentity}") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) + suspend fun updateIdentityMetadataWithRoles( + identity: KratosSessionDTO.Identity, + user: UserDTO + ): KratosSessionDTO.Identity = + withContext(Dispatchers.IO) { + val updatedIdentity = identity.copy(metadata_public = getIdentityMetadata(user)) + + val response = + httpClient.put { + url("$adminUrl/admin/identities/${updatedIdentity.id}") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + setBody(updatedIdentity) + } + + if (response.status.isSuccess()) { + response.body().also { + log.debug("Updated identity for ${it.id}") + } + } else { + throw IdpException("Couldn't update identity on server at $adminUrl") + } } + /** Delete a [User] from the IDP as an identity. */ + @Throws(IdpException::class) + suspend fun deleteAssociatedIdentity(userIdentity: String?) = + withContext(Dispatchers.IO) { + val identityId = + userIdentity + ?: throw IdpException( + "User with ID $userIdentity could not be deleted from the IDP. No identity was set" + ) - if (response.status.isSuccess()) { - log.debug("Deleted identity for user ${userIdentity}") - } else { - throw IdpException( - "Couldn't delete identity from server at " + managementPortalProperties.identityServer.serverUrl - ) + val response = + httpClient.delete { + url("$adminUrl/admin/identities/$identityId") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + } + + if (response.status.isSuccess()) { + log.debug("Deleted identity for user $identityId") + } else { + throw IdpException("Couldn't delete identity from server at $adminUrl") + } } - } - } /** - * Convert a [User] to a [KratosSessionDTO.Identity] object. + * Convert a [UserDTO] to a [KratosSessionDTO.Metadata] object. * @param user The object to convert * @return the newly created DTO object */ @Throws(IdpException::class) - private fun createIdentity(user: User): KratosSessionDTO.Identity { - try { - return KratosSessionDTO.Identity( - schema_id = "user", - traits = KratosSessionDTO.Traits(email = user.email), - metadata_public = KratosSessionDTO.Metadata( - aud = emptyList(), - sources = emptyList(), //empty at the time of creation - roles = user.roles.mapNotNull { role: Role -> - val auth = role.authority?.name - when (role.role?.scope) { - RoleAuthority.Scope.GLOBAL -> auth - RoleAuthority.Scope.ORGANIZATION -> role.organization!!.name + ":" + auth - RoleAuthority.Scope.PROJECT -> role.project!!.projectName + ":" + auth - null -> null - } - }.toList(), - authorities = user.authorities, - scope = Permission.scopes().filter { scope -> - val permission = Permission.ofScope(scope) - val auths = user.roles.mapNotNull { it.role } - - return@filter authService.mayBeGranted(auths, permission) - }, - mp_login = user.login + fun getIdentityMetadata(user: UserDTO): KratosSessionDTO.Metadata = + try { + KratosSessionDTO.Metadata( + aud = emptyList(), + sources = emptyList(), + roles = + user.roles.orEmpty().mapNotNull { role -> + role.authorityName?.let { auth -> + when { + role.projectName != null -> "${role.projectName}:$auth" + role.organizationName != null -> + "${role.organizationName}:$auth" + else -> auth + } + } + }, + authorities = user.authorities.orEmpty(), + scope = + Permission.scopes().filter { scope -> + authService.mayBeGranted( + user.roles?.mapNotNull { + RoleAuthority.valueOfAuthority(it.authorityName!!) + } + ?: emptyList(), + Permission.ofScope(scope) + ) + }, + mp_login = user.login ) - ) - } - catch (e: Throwable){ - val message = "could not convert user ${user.login} to identity" - log.error(message) - throw IdpException(message, e) - } - } + } catch (e: Throwable) { + val message = "Could not convert user ${user.login} to identity" + log.error(message) + throw IdpException(message, e) + } /** - * get a recovery link from the identityprovider in the response, which expires in 24 hours. + * Get a recovery link from the identity provider, which expires in 24 hours. * @param user The user for whom the recovery link is requested. * @return The recovery link obtained from the server response. - * @throws IdpException If there is an issue with the identity or if the recovery link cannot be obtained from the server. + * @throws IdpException If there is an issue with the identity or if the recovery link cannot be + * obtained. */ @Throws(IdpException::class) - suspend fun getRecoveryLink(user: User): String { - val recoveryLink: String - - user.identity ?: throw IdpException( - "user ${user.login} could not be recovered on the IDP. No identity was set", - ) - - withContext(Dispatchers.IO) { - val response = httpClient.post { - url("${adminUrl}/admin/recovery/link") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - setBody( - mapOf( - "expires_in" to "24h", - "identity_id" to user.identity - ) - ) - } + suspend fun getRecoveryLink(user: User): String = + withContext(Dispatchers.IO) { + val identityId = + user.identity + ?: throw IdpException( + "User ${user.login} could not be recovered on the IDP. No identity was set" + ) - if (response.status.isSuccess()) { - recoveryLink = response.body>()["recovery_link"]!! - log.debug("recovery link for user ${user.login} is $recoveryLink") - } else { - throw IdpException( - "couldn't get recovery link from server at $adminUrl" - ) - } - } + val response = + httpClient.post { + url("$adminUrl/admin/recovery/link") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + setBody(mapOf("expires_in" to "24h", "identity_id" to identityId)) + } - return recoveryLink - } + if (response.status.isSuccess()) { + response.body>()["recovery_link"]!!.also { + log.debug("Recovery link for user ${user.login} is $it") + } + } else { + throw IdpException("Couldn't get recovery link from server at $adminUrl") + } + } companion object { private val log = LoggerFactory.getLogger(IdentityService::class.java) diff --git a/src/main/java/org/radarbase/management/service/SubjectService.kt b/src/main/java/org/radarbase/management/service/SubjectService.kt index ad281e4c4..056e1d496 100644 --- a/src/main/java/org/radarbase/management/service/SubjectService.kt +++ b/src/main/java/org/radarbase/management/service/SubjectService.kt @@ -1,5 +1,13 @@ package org.radarbase.management.service +import java.net.MalformedURLException +import java.net.URL +import java.time.ZonedDateTime +import java.util.* +import java.util.function.Consumer +import java.util.function.Function +import java.util.function.Predicate +import javax.annotation.Nonnull import org.hibernate.envers.query.AuditEntity import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission @@ -40,33 +48,23 @@ import org.springframework.data.domain.Page import org.springframework.data.history.Revision import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional -import java.net.MalformedURLException -import java.net.URL -import java.time.ZonedDateTime -import java.util.* -import java.util.function.Consumer -import java.util.function.Function -import java.util.function.Predicate -import javax.annotation.Nonnull -/** - * Created by nivethika on 26-5-17. - */ +/** Created by nivethika on 26-5-17. */ @Service @Transactional class SubjectService( - @Autowired private val subjectMapper: SubjectMapper, - @Autowired private val projectMapper: ProjectMapper, - @Autowired private val subjectRepository: SubjectRepository, - @Autowired private val sourceRepository: SourceRepository, - @Autowired private val sourceMapper: SourceMapper, - @Autowired private val roleRepository: RoleRepository, - @Autowired private val groupRepository: GroupRepository, - @Autowired private val revisionService: RevisionService, - @Autowired private val managementPortalProperties: ManagementPortalProperties, - @Autowired private val passwordService: PasswordService, - @Autowired private val authorityRepository: AuthorityRepository, - @Autowired private val authService: AuthService + @Autowired private val subjectMapper: SubjectMapper, + @Autowired private val projectMapper: ProjectMapper, + @Autowired private val subjectRepository: SubjectRepository, + @Autowired private val sourceRepository: SourceRepository, + @Autowired private val sourceMapper: SourceMapper, + @Autowired private val roleRepository: RoleRepository, + @Autowired private val groupRepository: GroupRepository, + @Autowired private val revisionService: RevisionService, + @Autowired private val managementPortalProperties: ManagementPortalProperties, + @Autowired private val passwordService: PasswordService, + @Autowired private val authorityRepository: AuthorityRepository, + @Autowired private val authService: AuthService ) { /** @@ -76,9 +74,9 @@ class SubjectService( * @return the newly created subject */ @Transactional - fun createSubject(subjectDto: SubjectDTO): SubjectDTO? { + fun createSubject(subjectDto: SubjectDTO, activated: Boolean? = true): SubjectDTO? { val subject = subjectMapper.subjectDTOToSubject(subjectDto) ?: throw NullPointerException() - //assign roles + // assign roles val user = subject.user val project = projectMapper.projectDTOToProject(subjectDto.project) val projectParticipantRole = getProjectParticipantRole(project, RoleAuthority.PARTICIPANT) @@ -95,13 +93,15 @@ class SubjectService( user.langKey = "en" user.resetDate = ZonedDateTime.now() // default subject is activated. - user.activated = true - //set if any devices are set as assigned + user.activated = activated!! + // set if any devices are set as assigned if (subject.sources.isNotEmpty()) { - subject.sources.forEach(Consumer { s: Source -> - s.assigned = true - s.subject(subject) - }) + subject.sources.forEach( + Consumer { s: Source -> + s.assigned = true + s.subject(subject) + } + ) } if (subject.enrollmentDate == null) { subject.enrollmentDate = ZonedDateTime.now() @@ -110,14 +110,29 @@ class SubjectService( return subjectMapper.subjectToSubjectReducedProjectDTO(subjectRepository.save(subject)) } + fun createSubject(id: String, projectDto: ProjectDTO): SubjectDTO? { + return createSubject( + SubjectDTO().apply { + login = id + project = projectDto + }, + activated = false + ) + } + private fun getSubjectGroup(project: Project?, groupName: String?): Group? { return if (project == null || groupName == null) { null - } else groupRepository.findByProjectIdAndName(project.id, groupName) ?: throw BadRequestException( - "Group " + groupName + " does not exist in project " + project.projectName, - EntityName.GROUP, - ErrorConstants.ERR_GROUP_NOT_FOUND - ) + } else + groupRepository.findByProjectIdAndName(project.id, groupName) + ?: throw BadRequestException( + "Group " + + groupName + + " does not exist in project " + + project.projectName, + EntityName.GROUP, + ErrorConstants.ERR_GROUP_NOT_FOUND + ) } /** @@ -128,14 +143,13 @@ class SubjectService( * @throws java.util.NoSuchElementException if the authority name is not in the database */ private fun getProjectParticipantRole(project: Project?, authority: RoleAuthority): Role { - val ans: Role? = roleRepository.findOneByProjectIdAndAuthorityName( - project?.id, authority.authority - ) + val ans: Role? = + roleRepository.findOneByProjectIdAndAuthorityName(project?.id, authority.authority) return if (ans == null) { val subjectRole = Role() - val auth: Authority = authorityRepository.findByAuthorityName( - authority.authority - ) ?: authorityRepository.save(Authority(authority)) + val auth: Authority = + authorityRepository.findByAuthorityName(authority.authority) + ?: authorityRepository.save(Authority(authority)) subjectRole.authority = auth subjectRole.project = project @@ -157,52 +171,69 @@ class SubjectService( } val subjectFromDb = ensureSubject(newSubjectDto) val sourcesToUpdate = subjectFromDb.sources - //set only the devices assigned to a subject as assigned + // set only the devices assigned to a subject as assigned subjectMapper.safeUpdateSubjectFromDTO(newSubjectDto, subjectFromDb) sourcesToUpdate.addAll(subjectFromDb.sources) - subjectFromDb.sources.forEach(Consumer { s: Source -> - s.subject(subjectFromDb).assigned = true }) + subjectFromDb.sources.forEach( + Consumer { s: Source -> s.subject(subjectFromDb).assigned = true } + ) sourceRepository.saveAll(sourcesToUpdate) // update participant role subjectFromDb.user!!.roles = updateParticipantRoles(subjectFromDb, newSubjectDto) // Set group - subjectFromDb.group = getSubjectGroup( - subjectFromDb.activeProject, newSubjectDto.group - ) + subjectFromDb.group = getSubjectGroup(subjectFromDb.activeProject, newSubjectDto.group) return subjectMapper.subjectToSubjectReducedProjectDTO( - subjectRepository.save(subjectFromDb) + subjectRepository.save(subjectFromDb) ) } + fun activateSubject(login: String): SubjectDTO? { + val subject = findOneByLogin(login) + subject.user!!.activated = true + return subjectMapper.subjectToSubjectReducedProjectDTO(subjectRepository.save(subject)) + } + private fun updateParticipantRoles(subject: Subject, subjectDto: SubjectDTO): MutableSet { if (subjectDto.project == null || subjectDto.project!!.projectName == null) { return subject.user!!.roles } - val existingRoles = subject.user!!.roles.map { - // make participant inactive in projects that do not match the new project - if (it.authority!!.name == RoleAuthority.PARTICIPANT.authority && it.project!!.projectName != subjectDto.project!!.projectName) { - return@map getProjectParticipantRole(it.project, RoleAuthority.INACTIVE_PARTICIPANT) - } else { - // do not modify other roles. - return@map it - } - }.toMutableSet() + val existingRoles = + subject.user!! + .roles + .map { + // make participant inactive in projects that do not match the new + // project + if (it.authority!!.name == RoleAuthority.PARTICIPANT.authority && + it.project!!.projectName != + subjectDto.project!!.projectName + ) { + return@map getProjectParticipantRole( + it.project, + RoleAuthority.INACTIVE_PARTICIPANT + ) + } else { + // do not modify other roles. + return@map it + } + } + .toMutableSet() // Ensure that given project is present val newProjectRole = - getProjectParticipantRole(projectMapper.projectDTOToProject(subjectDto.project), RoleAuthority.PARTICIPANT) + getProjectParticipantRole( + projectMapper.projectDTOToProject(subjectDto.project), + RoleAuthority.PARTICIPANT + ) existingRoles.add(newProjectRole) return existingRoles - } /** * Discontinue the given subject. * - * - * A discontinued subject is not deleted from the database, but will be prevented from - * logging into the system, sending data, or otherwise interacting with the system. + * A discontinued subject is not deleted from the database, but will be prevented from logging + * into the system, sending data, or otherwise interacting with the system. * * @param subjectDto the subject to discontinue * @return the discontinued subject @@ -222,13 +253,12 @@ class SubjectService( private fun ensureSubject(subjectDto: SubjectDTO): Subject { return try { subjectDto.id?.let { subjectRepository.findById(it).get() } - ?: throw Exception("invalid subject ${subjectDto.login}: No ID") - } - catch(e: Throwable) { + ?: throw Exception("invalid subject ${subjectDto.login}: No ID") + } catch (e: Throwable) { throw NotFoundException( - "Subject with ID " + subjectDto.id + " not found.", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND + "Subject with ID " + subjectDto.id + " not found.", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND ) } } @@ -240,12 +270,14 @@ class SubjectService( * @param subject The subject for which to unassign all sources */ private fun unassignAllSources(subject: Subject) { - subject.sources.forEach(Consumer { source: Source -> - source.assigned = false - source.subject = null - source.deleted = true - sourceRepository.save(source) - }) + subject.sources.forEach( + Consumer { source: Source -> + source.assigned = false + source.subject = null + source.deleted = true + sourceRepository.save(source) + } + ) subject.sources.clear() } @@ -257,20 +289,28 @@ class SubjectService( */ @Transactional fun assignOrUpdateSource( - subject: Subject, sourceType: SourceType, project: Project?, sourceRegistrationDto: MinimalSourceDetailsDTO + subject: Subject, + sourceType: SourceType, + project: Project?, + sourceRegistrationDto: MinimalSourceDetailsDTO ): MinimalSourceDetailsDTO { val assignedSource: Source if (sourceRegistrationDto.sourceId != null) { // update meta-data and source-name for existing sources assignedSource = updateSourceAssignedSubject(subject, sourceRegistrationDto) } else if (sourceType.canRegisterDynamically!!) { - val sources = subjectRepository.findSubjectSourcesBySourceType( - subject.user!!.login, sourceType.producer, sourceType.model, sourceType.catalogVersion - ) + val sources = + subjectRepository.findSubjectSourcesBySourceType( + subject.user!!.login, + sourceType.producer, + sourceType.model, + sourceType.catalogVersion + ) // create a source and register metadata // we allow only one source of a source-type per subject if (sources.isNullOrEmpty()) { - var source = Source(sourceType).project(project).sourceType(sourceType).subject(subject) + var source = + Source(sourceType).project(project).sourceType(sourceType).subject(subject) source.assigned = true source.attributes += sourceRegistrationDto.attributes // if source name is provided update source name @@ -281,10 +321,11 @@ class SubjectService( // make sure there is no source available on the same name. if (sourceRepository.findOneBySourceName(source.sourceName!!) != null) { throw ConflictException( - "SourceName already in use. Cannot create a " + "source with existing source-name ", - EntityName.SUBJECT, - ErrorConstants.ERR_SOURCE_NAME_EXISTS, - Collections.singletonMap("source-name", source.sourceName) + "SourceName already in use. Cannot create a " + + "source with existing source-name ", + EntityName.SUBJECT, + ErrorConstants.ERR_SOURCE_NAME_EXISTS, + Collections.singletonMap("source-name", source.sourceName) ) } source = sourceRepository.save(source) @@ -292,19 +333,20 @@ class SubjectService( subject.sources.add(source) } else { throw ConflictException( - "A Source of SourceType with the specified producer, model and version" + " was already registered for subject login", - EntityName.SUBJECT, - ErrorConstants.ERR_SOURCE_TYPE_EXISTS, - sourceTypeAttributes(sourceType, subject) + "A Source of SourceType with the specified producer, model and version" + + " was already registered for subject login", + EntityName.SUBJECT, + ErrorConstants.ERR_SOURCE_TYPE_EXISTS, + sourceTypeAttributes(sourceType, subject) ) } } else { // new source since sourceId == null, but canRegisterDynamically == false throw BadRequestException( - "The source type is not eligible for dynamic " + "registration", - EntityName.SOURCE_TYPE, - "error.InvalidDynamicSourceRegistration", - sourceTypeAttributes(sourceType, subject) + "The source type is not eligible for dynamic " + "registration", + EntityName.SOURCE_TYPE, + "error.InvalidDynamicSourceRegistration", + sourceTypeAttributes(sourceType, subject) ) } subjectRepository.save(subject) @@ -319,21 +361,24 @@ class SubjectService( * @return Updated [Source] instance. */ private fun updateSourceAssignedSubject( - subject: Subject, sourceRegistrationDto: MinimalSourceDetailsDTO + subject: Subject, + sourceRegistrationDto: MinimalSourceDetailsDTO ): Source { // for manually registered devices only add meta-data - val source = subjectRepository.findSubjectSourcesBySourceId( - subject.user?.login, sourceRegistrationDto.sourceId - ) + val source = + subjectRepository.findSubjectSourcesBySourceId( + subject.user?.login, + sourceRegistrationDto.sourceId + ) if (source == null) { val errorParams: MutableMap = HashMap() errorParams["sourceId"] = sourceRegistrationDto.sourceId.toString() errorParams["subject-login"] = subject.user?.login throw NotFoundException( - "No source with source-id to assigned to the subject with subject-login", - EntityName.SUBJECT, - ErrorConstants.ERR_SOURCE_NOT_FOUND, - errorParams + "No source with source-id to assigned to the subject with subject-login", + EntityName.SUBJECT, + ErrorConstants.ERR_SOURCE_NOT_FOUND, + errorParams ) } @@ -353,7 +398,10 @@ class SubjectService( */ fun getSources(subject: Subject): List { val sources = subjectRepository.findSourcesBySubjectLogin(subject.user?.login) - if (sources.isEmpty()) throw org.webjars.NotFoundException("Could not find sources for user ${subject.user}") + if (sources.isEmpty()) + throw org.webjars.NotFoundException( + "Could not find sources for user ${subject.user}" + ) return sourceMapper.sourcesToMinimalSourceDetailsDTOs(sources) } @@ -367,12 +415,13 @@ class SubjectService( unassignAllSources(subject) subjectRepository.delete(subject) log.debug("Deleted Subject: {}", subject) - } ?: throw NotFoundException( - "subject not found for given login.", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login) - ) + } + ?: throw NotFoundException( + "subject not found for given login.", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login) + ) } /** @@ -384,10 +433,14 @@ class SubjectService( fun findSubjectSourcesFromRevisions(subject: Subject): List? { val revisions = subject.id?.let { subjectRepository.findRevisions(it) } // collect distinct sources in a set - val sources: List? = revisions?.content?.flatMap { p: Revision -> p.entity.sources } - ?.distinctBy { obj: Source -> obj.sourceId } - - return sources?.map { p: Source -> sourceMapper.sourceToMinimalSourceDetailsDTO(p) }?.toList() + val sources: List? = + revisions?.content + ?.flatMap { p: Revision -> p.entity.sources } + ?.distinctBy { obj: Source -> obj.sourceId } + + return sources + ?.map { p: Source -> sourceMapper.sourceToMinimalSourceDetailsDTO(p) } + ?.toList() } /** @@ -396,25 +449,30 @@ class SubjectService( * @param login the login of the subject * @param revision the revision number * @return the subject at the given revision - * @throws NotFoundException if there was no subject with the given login at the given - * revision number + * @throws NotFoundException if there was no subject with the given login at the given revision + * number */ @Throws(NotFoundException::class, NotAuthorizedException::class) fun findRevision(login: String?, revision: Int?): SubjectDTO { // first get latest known version of the subject, if it's deleted we can't load the entity // directly by e.g. findOneByLogin val latest = getLatestRevision(login) - authService.checkPermission(Permission.SUBJECT_READ, { e: EntityDetails -> - e.project(latest.project?.projectName).subject(latest.login) - }) + authService.checkPermission( + Permission.SUBJECT_READ, + { e: EntityDetails -> e.project(latest.project?.projectName).subject(latest.login) } + ) return revisionService.findRevision( - revision, latest.id, Subject::class.java, subjectMapper::subjectToSubjectReducedProjectDTO - ) ?: throw NotFoundException( - "subject not found for given login and revision.", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login) + revision, + latest.id, + Subject::class.java, + subjectMapper::subjectToSubjectReducedProjectDTO ) + ?: throw NotFoundException( + "subject not found for given login and revision.", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login) + ) } /** @@ -426,26 +484,33 @@ class SubjectService( */ @Throws(NotFoundException::class) fun getLatestRevision(login: String?): SubjectDTO { - val user = revisionService.getLatestRevisionForEntity( - User::class.java, listOf(AuditEntity.property("login").eq(login)) - ).orElseThrow { - NotFoundException( - "Subject latest revision not found " + "for login", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login) - ) - } as UserDTO + val user = + revisionService.getLatestRevisionForEntity( + User::class.java, + listOf(AuditEntity.property("login").eq(login)) + ) + .orElseThrow { + NotFoundException( + "Subject latest revision not found " + "for login", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login) + ) + } as + UserDTO return revisionService.getLatestRevisionForEntity( - Subject::class.java, listOf(AuditEntity.property("user").eq(user)) - ).orElseThrow { - NotFoundException( - "Subject latest revision not found " + "for login", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login) - ) - } as SubjectDTO + Subject::class.java, + listOf(AuditEntity.property("user").eq(user)) + ) + .orElseThrow { + NotFoundException( + "Subject latest revision not found " + "for login", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login) + ) + } as + SubjectDTO } /** @@ -456,9 +521,12 @@ class SubjectService( @Nonnull fun findOneByLogin(login: String?): Subject { val subject = subjectRepository.findOneWithEagerBySubjectLogin(login) - return subject ?: throw NotFoundException( - "Subject not found with login", EntityName.SUBJECT, ErrorConstants.ERR_SUBJECT_NOT_FOUND - ) + return subject + ?: throw NotFoundException( + "Subject not found with login", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND + ) } /** @@ -471,18 +539,14 @@ class SubjectService( // but the page should always be zero // since the lastLoadedId param defines the offset // within the query specification - return subjectRepository.findAll( - SubjectSpecification(criteria), criteria.pageable - ) + return subjectRepository.findAll(SubjectSpecification(criteria), criteria.pageable) } /** * Gets relevant privacy-policy-url for this subject. * - * * If the active project of the subject has a valid privacy-policy-url returns that url. - * Otherwise, it loads the default URL from ManagementPortal configurations that is - * general. + * Otherwise, it loads the default URL from ManagementPortal configurations that is general. * * @param subject to get relevant policy url * @return URL of privacy policy for this token @@ -490,8 +554,9 @@ class SubjectService( fun getPrivacyPolicyUrl(subject: Subject): URL { // load default url from config - val policyUrl: String = subject.activeProject?.attributes?.get(ProjectDTO.PRIVACY_POLICY_URL) - ?: managementPortalProperties.common.privacyPolicyUrl + val policyUrl: String = + subject.activeProject?.attributes?.get(ProjectDTO.PRIVACY_POLICY_URL) + ?: managementPortalProperties.common.privacyPolicyUrl return try { URL(policyUrl) } catch (e: MalformedURLException) { @@ -499,10 +564,11 @@ class SubjectService( params["url"] = policyUrl params["message"] = e.message throw InvalidStateException( - "No valid privacy-policy Url configured. Please " + "verify your project's privacy-policy url and/or general url config", - EntityName.OAUTH_CLIENT, - ErrorConstants.ERR_NO_VALID_PRIVACY_POLICY_URL_CONFIGURED, - params + "No valid privacy-policy Url configured. Please " + + "verify your project's privacy-policy url and/or general url config", + EntityName.OAUTH_CLIENT, + ErrorConstants.ERR_NO_VALID_PRIVACY_POLICY_URL_CONFIGURED, + params ) } } @@ -510,7 +576,8 @@ class SubjectService( companion object { private val log = LoggerFactory.getLogger(SubjectService::class.java) private fun sourceTypeAttributes( - sourceType: SourceType, subject: Subject + sourceType: SourceType, + subject: Subject ): Map { val errorParams: MutableMap = HashMap() errorParams["producer"] = sourceType.producer diff --git a/src/main/java/org/radarbase/management/service/dto/KratosSubjectWebhookDTO.kt b/src/main/java/org/radarbase/management/service/dto/KratosSubjectWebhookDTO.kt new file mode 100644 index 000000000..23ad51849 --- /dev/null +++ b/src/main/java/org/radarbase/management/service/dto/KratosSubjectWebhookDTO.kt @@ -0,0 +1,28 @@ +package org.radarbase.management.service.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.JsonSetter +import com.fasterxml.jackson.annotation.Nulls +import java.io.Serializable +import java.time.LocalDate +import java.time.ZonedDateTime +import java.util.* +import org.radarbase.auth.kratos.KratosSessionDTO + +/** + * A DTO for the Subject entity. + */ +class KratosSubjectWebhookDTO : Serializable { + @JsonInclude(JsonInclude.Include.NON_NULL) + var identity: KratosSessionDTO.Identity? = null + + @JsonInclude(JsonInclude.Include.NON_NULL) + var payload: Map? = null + + @JsonInclude(JsonInclude.Include.NON_NULL) + var cookies: Map? = null + + companion object { + private const val serialVersionUID = 1L + } +} \ No newline at end of file diff --git a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt new file mode 100644 index 000000000..b7e39a858 --- /dev/null +++ b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt @@ -0,0 +1,114 @@ +package org.radarbase.management.web.rest + +import io.micrometer.core.annotation.Timed +import java.net.URISyntaxException +import org.radarbase.auth.kratos.KratosSessionDTO +import org.radarbase.auth.kratos.SessionService +import org.radarbase.management.config.ManagementPortalProperties +import org.radarbase.management.repository.SubjectRepository +import org.radarbase.management.security.NotAuthorizedException +import org.radarbase.management.service.* +import org.radarbase.management.service.dto.KratosSubjectWebhookDTO +import org.radarbase.management.service.mapper.SubjectMapper +import org.radarbase.management.web.rest.errors.EntityName +import org.radarbase.management.web.rest.errors.NotFoundException +import org.radarbase.management.web.rest.util.HeaderUtil +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/api/kratos") +class KratosEndpoint +@Autowired +constructor( + @Autowired private val subjectService: SubjectService, + @Autowired private val subjectRepository: SubjectRepository, + @Autowired private val projectService: ProjectService, + @Autowired private val userService: UserService, + @Autowired private val identityService: IdentityService, + @Autowired private val managementPortalProperties: ManagementPortalProperties, + @Autowired private val subjectMapper: SubjectMapper, +) { + private var sessionService: SessionService = + SessionService(managementPortalProperties.identityServer.publicUrl()) + + /** + * POST /subjects : Create a new subject. + * + * @param subjectDto the subjectDto to create + * @return the ResponseEntity with status 201 (Created) and with body the new subjectDto, or + * with status 400 (Bad Request) if the subject has already an ID + * @throws URISyntaxException if the Location URI syntax is incorrect + */ + @PostMapping("/subjects") + @Timed + @Throws(URISyntaxException::class, NotAuthorizedException::class) + suspend fun createSubject( + @RequestBody webhookDTO: KratosSubjectWebhookDTO, + ): ResponseEntity { + val kratosIdentity = + webhookDTO.identity ?: throw IllegalArgumentException("Identity is required") + + if (!kratosIdentity.schema_id.equals(KRATOS_SUBJECT_SCHEMA)) + throw IllegalArgumentException("Cannot create non-subject users") + + val id = kratosIdentity.id ?: throw IllegalArgumentException("Identity ID is required") + val projectId = + webhookDTO.payload?.get("project_id") + ?: throw NotAuthorizedException("Cannot create subject without project") + val projectDto = + projectService.findOneByName(projectId) + ?: throw NotFoundException( + "Project not found: $projectId", + EntityName.PROJECT, + "projectNotFound" + ) + val subjectDto = + subjectService.createSubject(id, projectDto) + ?: throw IllegalStateException("Failed to create subject for ID: $id") + val user = + userService.getUserWithAuthoritiesByLogin(subjectDto.login!!) + ?: throw NotFoundException( + "User not found with login: ${subjectDto.login}", + EntityName.USER, + "userNotFound" + ) + + identityService.updateIdentityMetadataWithRoles(kratosIdentity, user) + return ResponseEntity.created(ResourceUriService.getUri(subjectDto)) + .headers(HeaderUtil.createEntityCreationAlert(EntityName.SUBJECT, id)) + .build() + } + + @PostMapping("/subjects/activate") + @Timed + @Throws(URISyntaxException::class, NotAuthorizedException::class) + suspend fun activateSubject( + @RequestBody webhookDTO: KratosSubjectWebhookDTO, + ): ResponseEntity { + val id = webhookDTO.identity?.id ?: throw IllegalArgumentException("Subject ID is required") + val token = + webhookDTO.cookies?.get("ory_kratos_session") + ?: throw IllegalArgumentException("Session token is required") + val kratosIdentity = sessionService.getSession(token).identity + if (!hasPermission(kratosIdentity, id)) { + throw NotAuthorizedException("Not authorized to activate subject") + } + subjectService.activateSubject(id) + return ResponseEntity.ok() + .headers(HeaderUtil.createEntityUpdateAlert(EntityName.SUBJECT, id)) + .build() + } + + private fun hasPermission( + kratosIdentity: KratosSessionDTO.Identity, + identityId: String?, + ): Boolean = kratosIdentity.id == identityId + + companion object { + private val logger = LoggerFactory.getLogger(KratosEndpoint::class.java) + private val KRATOS_SUBJECT_SCHEMA = "subject" + } +} \ No newline at end of file From d1588ba17a1ffd076454e55339fd9ea7d4e9c079 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 28 Aug 2024 16:01:26 +0100 Subject: [PATCH 37/68] Separate out subject, researcher, admin identities --- ...a.user.json => identity.schema.admin.json} | 4 +- .../identity.schema.researcher.json | 37 +++++++++++++++++++ .../identities/identity.schema.subject.json | 37 +++++++++++++++++++ src/main/docker/etc/config/kratos/kratos.yml | 10 +++-- 4 files changed, 83 insertions(+), 5 deletions(-) rename src/main/docker/etc/config/kratos/identities/{identity.schema.user.json => identity.schema.admin.json} (95%) create mode 100644 src/main/docker/etc/config/kratos/identities/identity.schema.researcher.json create mode 100644 src/main/docker/etc/config/kratos/identities/identity.schema.subject.json diff --git a/src/main/docker/etc/config/kratos/identities/identity.schema.user.json b/src/main/docker/etc/config/kratos/identities/identity.schema.admin.json similarity index 95% rename from src/main/docker/etc/config/kratos/identities/identity.schema.user.json rename to src/main/docker/etc/config/kratos/identities/identity.schema.admin.json index 060b3fa3c..b127b798c 100644 --- a/src/main/docker/etc/config/kratos/identities/identity.schema.user.json +++ b/src/main/docker/etc/config/kratos/identities/identity.schema.admin.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "user", - "title": "user", + "$id": "admin", + "title": "admin", "type": "object", "properties": { "traits": { diff --git a/src/main/docker/etc/config/kratos/identities/identity.schema.researcher.json b/src/main/docker/etc/config/kratos/identities/identity.schema.researcher.json new file mode 100644 index 000000000..e8d1d0990 --- /dev/null +++ b/src/main/docker/etc/config/kratos/identities/identity.schema.researcher.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "researcher", + "title": "researcher", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 5, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": ["email"] + } + }, + "additionalProperties": false +} diff --git a/src/main/docker/etc/config/kratos/identities/identity.schema.subject.json b/src/main/docker/etc/config/kratos/identities/identity.schema.subject.json new file mode 100644 index 000000000..b87c21988 --- /dev/null +++ b/src/main/docker/etc/config/kratos/identities/identity.schema.subject.json @@ -0,0 +1,37 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "subject", + "title": "subject", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 5, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + }, + "totp": { + "account_name": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + } + }, + "required": ["email"] + } + }, + "additionalProperties": false +} diff --git a/src/main/docker/etc/config/kratos/kratos.yml b/src/main/docker/etc/config/kratos/kratos.yml index d194bcfc3..36a824e98 100644 --- a/src/main/docker/etc/config/kratos/kratos.yml +++ b/src/main/docker/etc/config/kratos/kratos.yml @@ -78,10 +78,14 @@ hashers: key_length: 16 identity: - default_schema_id: user + default_schema_id: subject schemas: - - id: user - url: file:///etc/config/kratos/identities/identity.schema.user.json + - id: subject + url: file:///etc/config/kratos/identities/identity.schema.subject.json + - id: researcher + url: file:///etc/config/kratos/identities/identity.schema.researcher.json + - id: admin + url: file:///etc/config/kratos/identities/identity.schema.admin.json courier: smtp: From fc283147ce2e02bb914b98b89ae8c3b6b0d5a22e Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 28 Aug 2024 16:35:51 +0100 Subject: [PATCH 38/68] Fix Kratos Identity class --- .../src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt index 805c71a68..b89b38ae7 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt @@ -45,7 +45,7 @@ class KratosSessionDTO( ) @Serializable - class Identity( + data class Identity( var id: String? = null, val schema_id: String? = null, val schema_url: String? = null, From b81e0fd61898d730f3e7d87ad30f63e35a692e20 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 28 Aug 2024 16:48:36 +0100 Subject: [PATCH 39/68] Fix formatting --- .../management/service/IdentityService.kt | 316 +++++++++--------- 1 file changed, 158 insertions(+), 158 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/IdentityService.kt b/src/main/java/org/radarbase/management/service/IdentityService.kt index e0b0bd202..4ad5c41dd 100644 --- a/src/main/java/org/radarbase/management/service/IdentityService.kt +++ b/src/main/java/org/radarbase/management/service/IdentityService.kt @@ -8,7 +8,6 @@ import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.request.* import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* -import java.time.Duration import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json @@ -23,17 +22,18 @@ import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.time.Duration /** Service class for managing identities. */ @Service @Transactional class IdentityService -@Autowired -constructor( + @Autowired + constructor( private val managementPortalProperties: ManagementPortalProperties, - private val authService: AuthService -) { - private val httpClient = + private val authService: AuthService, + ) { + private val httpClient = HttpClient(CIO) { install(HttpTimeout) { connectTimeoutMillis = Duration.ofSeconds(10).toMillis() @@ -42,60 +42,60 @@ constructor( } install(ContentNegotiation) { json( - Json { - ignoreUnknownKeys = true - coerceInputValues = true - } + Json { + ignoreUnknownKeys = true + coerceInputValues = true + }, ) } } - private val adminUrl = managementPortalProperties.identityServer.adminUrl() - private val publicUrl = managementPortalProperties.identityServer.publicUrl() + private val adminUrl = managementPortalProperties.identityServer.adminUrl() + private val publicUrl = managementPortalProperties.identityServer.publicUrl() - init { - log.debug("Kratos serverUrl set to $publicUrl") - log.debug("Kratos serverAdminUrl set to $adminUrl") - } + init { + log.debug("Kratos serverUrl set to $publicUrl") + log.debug("Kratos serverAdminUrl set to $adminUrl") + } - /** - * Convert a [User] to a [KratosSessionDTO.Identity] object. - * @param user The object to convert - * @return the newly created DTO object - */ - @Throws(IdpException::class) - private fun createIdentity(user: User): KratosSessionDTO.Identity = + /** + * Convert a [User] to a [KratosSessionDTO.Identity] object. + * @param user The object to convert + * @return the newly created DTO object + */ + @Throws(IdpException::class) + private fun createIdentity(user: User): KratosSessionDTO.Identity = try { KratosSessionDTO.Identity( - schema_id = "researcher", - traits = KratosSessionDTO.Traits(email = user.email), - metadata_public = - KratosSessionDTO.Metadata( - aud = emptyList(), - sources = emptyList(), - roles = - user.roles.mapNotNull { role -> - role.authority?.name?.let { auth -> - when (role.role?.scope) { - RoleAuthority.Scope.GLOBAL -> auth - RoleAuthority.Scope.ORGANIZATION -> - "${role.organization!!.name}:$auth" - RoleAuthority.Scope.PROJECT -> - "${role.project!!.projectName}:$auth" - null -> null - } - } - }, - authorities = user.authorities, - scope = - Permission.scopes().filter { scope -> - authService.mayBeGranted( - user.roles.mapNotNull { it.role }, - Permission.ofScope(scope) - ) - }, - mp_login = user.login - ) + schema_id = "researcher", + traits = KratosSessionDTO.Traits(email = user.email), + metadata_public = + KratosSessionDTO.Metadata( + aud = emptyList(), + sources = emptyList(), + roles = + user.roles.mapNotNull { role -> + role.authority?.name?.let { auth -> + when (role.role?.scope) { + RoleAuthority.Scope.GLOBAL -> auth + RoleAuthority.Scope.ORGANIZATION -> + "${role.organization!!.name}:$auth" + RoleAuthority.Scope.PROJECT -> + "${role.project!!.projectName}:$auth" + null -> null + } + } + }, + authorities = user.authorities, + scope = + Permission.scopes().filter { scope -> + authService.mayBeGranted( + user.roles.mapNotNull { it.role }, + Permission.ofScope(scope), + ) + }, + mp_login = user.login, + ), ) } catch (e: Throwable) { val message = "Could not convert user ${user.login} to identity" @@ -103,20 +103,20 @@ constructor( throw IdpException(message, e) } - /** - * Save a [User] to the IDP as an identity. Returns the generated [KratosSessionDTO.Identity] - */ - @Throws(IdpException::class) - suspend fun saveAsIdentity(user: User): KratosSessionDTO.Identity = + /** + * Save a [User] to the IDP as an identity. Returns the generated [KratosSessionDTO.Identity] + */ + @Throws(IdpException::class) + suspend fun saveAsIdentity(user: User): KratosSessionDTO.Identity = withContext(Dispatchers.IO) { val identity = createIdentity(user) val response = - httpClient.post { - url("$adminUrl/admin/identities") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - setBody(identity) - } + httpClient.post { + url("$adminUrl/admin/identities") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + setBody(identity) + } if (response.status.isSuccess()) { response.body().also { @@ -127,26 +127,26 @@ constructor( } } - /** - * Update a [User] to the IDP as an identity. Returns the updated [KratosSessionDTO.Identity] - */ - @Throws(IdpException::class) - suspend fun updateAssociatedIdentity(user: User): KratosSessionDTO.Identity = + /** + * Update a [User] to the IDP as an identity. Returns the updated [KratosSessionDTO.Identity] + */ + @Throws(IdpException::class) + suspend fun updateAssociatedIdentity(user: User): KratosSessionDTO.Identity = withContext(Dispatchers.IO) { val identityId = - user.identity - ?: throw IdpException( - "User ${user.login} could not be updated on the IDP. No identity was set" - ) + user.identity + ?: throw IdpException( + "User ${user.login} could not be updated on the IDP. No identity was set", + ) val identity = createIdentity(user) val response = - httpClient.put { - url("$adminUrl/admin/identities/$identityId") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - setBody(identity) - } + httpClient.put { + url("$adminUrl/admin/identities/$identityId") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + setBody(identity) + } if (response.status.isSuccess()) { response.body().also { @@ -157,25 +157,25 @@ constructor( } } - /** - * Update [KratosSessionDTO.Identity] metadata with user roles. Returns the updated - * [KratosSessionDTO.Identity] - */ - @Throws(IdpException::class) - suspend fun updateIdentityMetadataWithRoles( + /** + * Update [KratosSessionDTO.Identity] metadata with user roles. Returns the updated + * [KratosSessionDTO.Identity] + */ + @Throws(IdpException::class) + suspend fun updateIdentityMetadataWithRoles( identity: KratosSessionDTO.Identity, - user: UserDTO - ): KratosSessionDTO.Identity = + user: UserDTO, + ): KratosSessionDTO.Identity = withContext(Dispatchers.IO) { val updatedIdentity = identity.copy(metadata_public = getIdentityMetadata(user)) val response = - httpClient.put { - url("$adminUrl/admin/identities/${updatedIdentity.id}") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - setBody(updatedIdentity) - } + httpClient.put { + url("$adminUrl/admin/identities/${updatedIdentity.id}") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + setBody(updatedIdentity) + } if (response.status.isSuccess()) { response.body().also { @@ -186,22 +186,22 @@ constructor( } } - /** Delete a [User] from the IDP as an identity. */ - @Throws(IdpException::class) - suspend fun deleteAssociatedIdentity(userIdentity: String?) = + /** Delete a [User] from the IDP as an identity. */ + @Throws(IdpException::class) + suspend fun deleteAssociatedIdentity(userIdentity: String?) = withContext(Dispatchers.IO) { val identityId = - userIdentity - ?: throw IdpException( - "User with ID $userIdentity could not be deleted from the IDP. No identity was set" - ) + userIdentity + ?: throw IdpException( + "User with ID $userIdentity could not be deleted from the IDP. No identity was set", + ) val response = - httpClient.delete { - url("$adminUrl/admin/identities/$identityId") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - } + httpClient.delete { + url("$adminUrl/admin/identities/$identityId") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + } if (response.status.isSuccess()) { log.debug("Deleted identity for user $identityId") @@ -210,40 +210,40 @@ constructor( } } - /** - * Convert a [UserDTO] to a [KratosSessionDTO.Metadata] object. - * @param user The object to convert - * @return the newly created DTO object - */ - @Throws(IdpException::class) - fun getIdentityMetadata(user: UserDTO): KratosSessionDTO.Metadata = + /** + * Convert a [UserDTO] to a [KratosSessionDTO.Metadata] object. + * @param user The object to convert + * @return the newly created DTO object + */ + @Throws(IdpException::class) + fun getIdentityMetadata(user: UserDTO): KratosSessionDTO.Metadata = try { KratosSessionDTO.Metadata( - aud = emptyList(), - sources = emptyList(), - roles = - user.roles.orEmpty().mapNotNull { role -> - role.authorityName?.let { auth -> - when { - role.projectName != null -> "${role.projectName}:$auth" - role.organizationName != null -> - "${role.organizationName}:$auth" - else -> auth - } - } - }, - authorities = user.authorities.orEmpty(), - scope = - Permission.scopes().filter { scope -> - authService.mayBeGranted( - user.roles?.mapNotNull { - RoleAuthority.valueOfAuthority(it.authorityName!!) - } - ?: emptyList(), - Permission.ofScope(scope) - ) - }, - mp_login = user.login + aud = emptyList(), + sources = emptyList(), + roles = + user.roles.orEmpty().mapNotNull { role -> + role.authorityName?.let { auth -> + when { + role.projectName != null -> "${role.projectName}:$auth" + role.organizationName != null -> + "${role.organizationName}:$auth" + else -> auth + } + } + }, + authorities = user.authorities.orEmpty(), + scope = + Permission.scopes().filter { scope -> + authService.mayBeGranted( + user.roles?.mapNotNull { + RoleAuthority.valueOfAuthority(it.authorityName!!) + } + ?: emptyList(), + Permission.ofScope(scope), + ) + }, + mp_login = user.login, ) } catch (e: Throwable) { val message = "Could not convert user ${user.login} to identity" @@ -251,29 +251,29 @@ constructor( throw IdpException(message, e) } - /** - * Get a recovery link from the identity provider, which expires in 24 hours. - * @param user The user for whom the recovery link is requested. - * @return The recovery link obtained from the server response. - * @throws IdpException If there is an issue with the identity or if the recovery link cannot be - * obtained. - */ - @Throws(IdpException::class) - suspend fun getRecoveryLink(user: User): String = + /** + * Get a recovery link from the identity provider, which expires in 24 hours. + * @param user The user for whom the recovery link is requested. + * @return The recovery link obtained from the server response. + * @throws IdpException If there is an issue with the identity or if the recovery link cannot be + * obtained. + */ + @Throws(IdpException::class) + suspend fun getRecoveryLink(user: User): String = withContext(Dispatchers.IO) { val identityId = - user.identity - ?: throw IdpException( - "User ${user.login} could not be recovered on the IDP. No identity was set" - ) + user.identity + ?: throw IdpException( + "User ${user.login} could not be recovered on the IDP. No identity was set", + ) val response = - httpClient.post { - url("$adminUrl/admin/recovery/link") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - setBody(mapOf("expires_in" to "24h", "identity_id" to identityId)) - } + httpClient.post { + url("$adminUrl/admin/recovery/link") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + setBody(mapOf("expires_in" to "24h", "identity_id" to identityId)) + } if (response.status.isSuccess()) { response.body>()["recovery_link"]!!.also { @@ -284,7 +284,7 @@ constructor( } } - companion object { - private val log = LoggerFactory.getLogger(IdentityService::class.java) + companion object { + private val log = LoggerFactory.getLogger(IdentityService::class.java) + } } -} From 8a4e061c3e556f59dbfd74e364d42a24d45a927e Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 3 Sep 2024 16:02:11 +0100 Subject: [PATCH 40/68] Update verification webhook config --- src/main/docker/ory_stack.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index acac23a8c..4d3dccc7f 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -34,6 +34,7 @@ services: - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_METHOD=POST - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_URL=http://managementportal:8080/managementportal/api/kratos/subjects/activate - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_BODY=/etc/config/kratos/webhook_body.jsonnet + - SELFSERVICE_FLOWS_VERIFICATION_AFTER_HOOKS_0_CONFIG_RESPONSE_IGNORE=true command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier volumes: - type: bind From d95a03b7e03998cae89f76624af7a186f1e08323 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 9 Sep 2024 11:25:59 +0100 Subject: [PATCH 41/68] Remove separate user identity property --- .../java/org/radarbase/management/service/UserService.kt | 7 +++---- .../java/org/radarbase/management/service/dto/UserDTO.kt | 3 --- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/UserService.kt b/src/main/java/org/radarbase/management/service/UserService.kt index ade4f6123..c8b91831e 100644 --- a/src/main/java/org/radarbase/management/service/UserService.kt +++ b/src/main/java/org/radarbase/management/service/UserService.kt @@ -169,8 +169,8 @@ class UserService @Autowired constructor( user.activated = true user.roles = getUserRoles(userDto.roles, mutableSetOf()) - try{ - user.identity = identityService.saveAsIdentity(user)?.id + try { + identityService.saveAsIdentity(user) } catch (e: Throwable) { log.warn("could not save user ${user.login} as identity", e) @@ -383,8 +383,7 @@ class UserService @Autowired constructor( // there is no identity for this user, so we create it and save it to the IDP val id = identityService.saveAsIdentity(user) - // then save the identifier and update our database - user.identity = id?.id + return userMapper.userToUserDTO(user) ?: throw Exception("Admin user could not be converted to DTO") } diff --git a/src/main/java/org/radarbase/management/service/dto/UserDTO.kt b/src/main/java/org/radarbase/management/service/dto/UserDTO.kt index 659e230d6..a69e0c533 100644 --- a/src/main/java/org/radarbase/management/service/dto/UserDTO.kt +++ b/src/main/java/org/radarbase/management/service/dto/UserDTO.kt @@ -24,9 +24,6 @@ open class UserDTO { var authorities: Set? = null var accessToken: String? = null - /** Identifier for association with the identity service provider. - * Null if not linked to an external identity. */ - var identity: String? = null override fun toString(): String { return ("UserDTO{" + "login='" + login + '\'' From 54600d3f83a2846dec53324807dcb4ccab923862 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 9 Sep 2024 16:01:13 +0100 Subject: [PATCH 42/68] Remove kratosId from user dialog --- .../user-management-dialog.component.html | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/main/webapp/app/admin/user-management/user-management-dialog.component.html b/src/main/webapp/app/admin/user-management/user-management-dialog.component.html index 2f280b0d0..6cc941d9e 100644 --- a/src/main/webapp/app/admin/user-management/user-management-dialog.component.html +++ b/src/main/webapp/app/admin/user-management/user-management-dialog.component.html @@ -34,20 +34,6 @@ -
- - - -
- - -
-
-
Date: Mon, 9 Sep 2024 16:02:28 +0100 Subject: [PATCH 43/68] Send activation email from Kratos directly --- .../radarbase/auth/kratos/KratosSessionDTO.kt | 6 +++ .../management/service/IdentityService.kt | 43 ++++++++++--------- .../management/service/UserService.kt | 4 +- .../management/web/rest/UserResource.kt | 16 +------ 4 files changed, 32 insertions(+), 37 deletions(-) diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt index b89b38ae7..17c998416 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt @@ -44,6 +44,12 @@ class KratosSessionDTO( val location: String, ) + @Serializable + data class Verification( + val id: String? = null, + val type: String? = null + ) + @Serializable data class Identity( var id: String? = null, diff --git a/src/main/java/org/radarbase/management/service/IdentityService.kt b/src/main/java/org/radarbase/management/service/IdentityService.kt index 4ad5c41dd..7eee8308d 100644 --- a/src/main/java/org/radarbase/management/service/IdentityService.kt +++ b/src/main/java/org/radarbase/management/service/IdentityService.kt @@ -252,35 +252,38 @@ class IdentityService } /** - * Get a recovery link from the identity provider, which expires in 24 hours. - * @param user The user for whom the recovery link is requested. - * @return The recovery link obtained from the server response. - * @throws IdpException If there is an issue with the identity or if the recovery link cannot be - * obtained. + * Sends a Kratos activation email to the specified user. */ @Throws(IdpException::class) - suspend fun getRecoveryLink(user: User): String = + suspend fun sendActivationEmail(user: User): String = withContext(Dispatchers.IO) { - val identityId = - user.identity - ?: throw IdpException( - "User ${user.login} could not be recovered on the IDP. No identity was set", - ) + val flowResponse = + httpClient.get { + url("$publicUrl/self-service/verification/api") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + }.body() - val response = + val flowId = flowResponse.id + + if (flowId == null) { + throw IdpException("Failed to initiate verification flow for ${user.email}") + } + + val activationResponse = httpClient.post { - url("$adminUrl/admin/recovery/link") + url("$publicUrl/self-service/verification?flow=$flowId") contentType(ContentType.Application.Json) accept(ContentType.Application.Json) - setBody(mapOf("expires_in" to "24h", "identity_id" to identityId)) + setBody(mapOf("email" to user.email, "method" to "code")) } - if (response.status.isSuccess()) { - response.body>()["recovery_link"]!!.also { - log.debug("Recovery link for user ${user.login} is $it") - } - } else { - throw IdpException("Couldn't get recovery link from server at $adminUrl") + if (!activationResponse.status.isSuccess()) { + throw IdpException("Failed to trigger verification email for ${user.email}") + } + + flowId.also { + log.debug("Activation email sent for user ${user.login} with flow ID $it") } } diff --git a/src/main/java/org/radarbase/management/service/UserService.kt b/src/main/java/org/radarbase/management/service/UserService.kt index c8b91831e..5f1736963 100644 --- a/src/main/java/org/radarbase/management/service/UserService.kt +++ b/src/main/java/org/radarbase/management/service/UserService.kt @@ -502,8 +502,8 @@ class UserService @Autowired constructor( } @Throws(IdpException::class) - suspend fun getRecoveryLink(user: User): String { - return identityService.getRecoveryLink(user) + suspend fun sendActivationEmail(user: User): String { + return identityService.sendActivationEmail(user) } companion object { diff --git a/src/main/java/org/radarbase/management/web/rest/UserResource.kt b/src/main/java/org/radarbase/management/web/rest/UserResource.kt index 455402df7..6b0a51a23 100644 --- a/src/main/java/org/radarbase/management/web/rest/UserResource.kt +++ b/src/main/java/org/radarbase/management/web/rest/UserResource.kt @@ -11,7 +11,6 @@ import org.radarbase.management.repository.filters.UserFilter import org.radarbase.management.security.Constants import org.radarbase.management.security.NotAuthorizedException import org.radarbase.management.service.AuthService -import org.radarbase.management.service.MailService import org.radarbase.management.service.ResourceUriService import org.radarbase.management.service.UserService import org.radarbase.management.service.dto.RoleDTO @@ -74,7 +73,6 @@ import java.util.* @RequestMapping("/api") class UserResource( @Autowired private val userRepository: UserRepository, - @Autowired private val mailService: MailService, @Autowired private val userService: UserService, @Autowired private val subjectRepository: SubjectRepository, @Autowired private val managementPortalProperties: ManagementPortalProperties, @@ -121,19 +119,7 @@ class UserResource( } else { val newUser: User; newUser = userService.createUser(managedUserVm) - - val recoveryLink = userService.getRecoveryLink(newUser) - - mailService.sendEmail( - newUser.email, - "Account Activation", - "Please click the link to activate your account:\n\n" + - "$recoveryLink \n\n" + - "Please activate your account before the link expires in 24 hours, and activate 2FA to enable" + - " access to the managementportal", - false, - false - ) + userService.sendActivationEmail(newUser) ResponseEntity.created(ResourceUriService.getUri(newUser)).headers( HeaderUtil.createAlert( From 60f0fa455df3087dedfad01a4c1533b1177c401a Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 10 Sep 2024 20:11:34 +0100 Subject: [PATCH 44/68] Add support for projects in Kratos identity --- .../java/org/radarbase/auth/kratos/KratosSessionDTO.kt | 9 +++++++++ src/main/docker/etc/config/kratos/kratos.yml | 2 +- .../org/radarbase/management/web/rest/KratosEndpoint.kt | 6 +++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt index 17c998416..8aa60e54e 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt @@ -93,6 +93,15 @@ class KratosSessionDTO( class Traits ( val name: String? = null, val email: String? = null, + val projects: List? = null, + ) + + @Serializable + class Projects ( + val id: String? = null, + val name: String? = null, + val eligibility: Map? = null, + val consent: Map? = null, ) @Serializable diff --git a/src/main/docker/etc/config/kratos/kratos.yml b/src/main/docker/etc/config/kratos/kratos.yml index 36a824e98..b355cfbed 100644 --- a/src/main/docker/etc/config/kratos/kratos.yml +++ b/src/main/docker/etc/config/kratos/kratos.yml @@ -45,7 +45,7 @@ selfservice: enabled: true use: code after: - default_browser_return_url: http://localhost:3000/consent + default_browser_return_url: http://localhost:3000/study-consent logout: after: diff --git a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt index b7e39a858..bc4265f49 100644 --- a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt @@ -55,11 +55,11 @@ constructor( throw IllegalArgumentException("Cannot create non-subject users") val id = kratosIdentity.id ?: throw IllegalArgumentException("Identity ID is required") - val projectId = - webhookDTO.payload?.get("project_id") + val project = + kratosIdentity.traits.projects?.firstOrNull() ?: throw NotAuthorizedException("Cannot create subject without project") val projectDto = - projectService.findOneByName(projectId) + projectService.findOneByName(project.id!!) ?: throw NotFoundException( "Project not found: $projectId", EntityName.PROJECT, From 157b91576f01e2f27fab6e8a28fd98612086b8a3 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 11 Sep 2024 10:35:34 +0100 Subject: [PATCH 45/68] Fix getting project in webhook --- .../java/org/radarbase/management/web/rest/KratosEndpoint.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt index bc4265f49..3d976ba8f 100644 --- a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt @@ -56,12 +56,12 @@ constructor( val id = kratosIdentity.id ?: throw IllegalArgumentException("Identity ID is required") val project = - kratosIdentity.traits.projects?.firstOrNull() + kratosIdentity.traits!!.projects?.firstOrNull() ?: throw NotAuthorizedException("Cannot create subject without project") val projectDto = projectService.findOneByName(project.id!!) ?: throw NotFoundException( - "Project not found: $projectId", + "Project not found: ${project.id!!}", EntityName.PROJECT, "projectNotFound" ) From 6f1110adaea9b4b8bd202ca791b4da8700b06288 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 13 Sep 2024 14:28:47 +0100 Subject: [PATCH 46/68] Fix scopes --- .../java/org/radarbase/management/web/rest/LoginEndpoint.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index a61b73b6d..d775afcf6 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -48,7 +48,7 @@ constructor( "response_type=code&" + "state=${Instant.now()}&" + "audience=res_ManagementPortal&" + - "scope=offline&" + + "scope=SOURCEDATA.CREATE SOURCETYPE.UPDATE SOURCETYPE.DELETE AUTHORITY.UPDATE MEASUREMENT.DELETE PROJECT.READ AUDIT.CREATE USER.DELETE AUTHORITY.DELETE SUBJECT.DELETE MEASUREMENT.UPDATE SOURCEDATA.UPDATE SUBJECT.READ USER.UPDATE SOURCETYPE.CREATE AUTHORITY.READ USER.CREATE SOURCE.CREATE SOURCE.READ SUBJECT.CREATE ROLE.UPDATE ROLE.READ MEASUREMENT.READ PROJECT.UPDATE PROJECT.DELETE ROLE.DELETE SOURCE.DELETE SOURCETYPE.READ ROLE.CREATE SOURCEDATA.DELETE SUBJECT.UPDATE SOURCE.UPDATE PROJECT.CREATE AUDIT.READ MEASUREMENT.CREATE AUDIT.DELETE AUDIT.UPDATE AUTHORITY.CREATE USER.READ SOURCEDATA.READ&" + "redirect_uri=${managementPortalProperties.common.baseUrl}/api/redirect/login" } } From d52b9df7b543bd8b8c4b2992a3489cb8463c8310 Mon Sep 17 00:00:00 2001 From: Pauline Date: Fri, 13 Sep 2024 14:29:30 +0100 Subject: [PATCH 47/68] Update self-enrolment-ui tag --- src/main/docker/ory_stack.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index 4d3dccc7f..8caff14cd 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -5,7 +5,7 @@ volumes: services: radar-self-enrolment-ui: - image: ghcr.io/radar-base/radar-self-enrolment-ui:feat-consent-module + image: ghcr.io/radar-base/radar-self-enrolment-ui:feat-initial-components environment: - ORY_SDK_URL=http://kratos:4433/ - HYDRA_ADMIN_URL=http://hydra:4445 From 96c289095727aae638743a7a1a65875e68741504 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 25 Sep 2024 17:13:44 +0100 Subject: [PATCH 48/68] Add Ory config updates for jwt claims and ui env vars --- src/main/docker/ory_stack.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/docker/ory_stack.yml b/src/main/docker/ory_stack.yml index 8caff14cd..3bad1585e 100644 --- a/src/main/docker/ory_stack.yml +++ b/src/main/docker/ory_stack.yml @@ -5,10 +5,11 @@ volumes: services: radar-self-enrolment-ui: - image: ghcr.io/radar-base/radar-self-enrolment-ui:feat-initial-components + image: ghcr.io/radar-base/radar-self-enrolment-ui:dev environment: - - ORY_SDK_URL=http://kratos:4433/ + - ORY_SDK_URL=http://kratos:4433 - HYDRA_ADMIN_URL=http://hydra:4445 + - HYDRA_PUBLIC_URL=http://hydra:4444 ports: - "3000:3000" volumes: @@ -101,6 +102,7 @@ services: - OIDC_SUBJECT_IDENTIFIERS_SUPPORTED_TYPES=public,pairwise - OIDC_SUBJECT_IDENTIFIERS_PAIRWISE_SALT=youReallyNeedToChangeThis - STRATEGIES_ACCESS_TOKEN=jwt + - STRATEGIES_JWT_SCOPE_CLAIM=both - SERVE_PUBLIC_CORS_ENABLED=true - SERVE_ADMIN_CORS_ENABLED=true - OAUTH2_ALLOWED_TOP_LEVEL_CLAIMS=scope,roles,authorities,sources,user_name From f2af9015cb0f892dc1633dc54f1649c2fe2208f3 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 25 Sep 2024 17:14:02 +0100 Subject: [PATCH 49/68] Check if subject is equal to clientId when grant type is not present --- radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt b/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt index 03910caf4..ba0511b28 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt @@ -98,7 +98,7 @@ interface RadarToken { * @return true if the client credentials flow was certainly used, false otherwise. */ val isClientCredentials: Boolean - get() = grantType == CLIENT_CREDENTIALS + get() = grantType == CLIENT_CREDENTIALS || subject == clientId fun copyWithRoles(roles: Set): RadarToken From dda937288d092b1e65dec895de7c72e5ee0cdef2 Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 26 Sep 2024 14:08:54 +0100 Subject: [PATCH 50/68] Add check for null subject before checking if equal to clientId --- radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt b/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt index ba0511b28..d9c99950b 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/token/RadarToken.kt @@ -98,7 +98,7 @@ interface RadarToken { * @return true if the client credentials flow was certainly used, false otherwise. */ val isClientCredentials: Boolean - get() = grantType == CLIENT_CREDENTIALS || subject == clientId + get() = grantType == CLIENT_CREDENTIALS || (subject != null && subject == clientId) fun copyWithRoles(roles: Set): RadarToken From c4ff72f5fa9d9d07b5ba5e56c170fadd1160ef82 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sat, 28 Sep 2024 20:15:33 +0100 Subject: [PATCH 51/68] Add specify project user id when creating subject --- .../java/org/radarbase/management/web/rest/KratosEndpoint.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt index 3d976ba8f..6b25b7ba2 100644 --- a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt @@ -58,6 +58,7 @@ constructor( val project = kratosIdentity.traits!!.projects?.firstOrNull() ?: throw NotAuthorizedException("Cannot create subject without project") + val projectUserId = project.userId ?: throw IllegalArgumentException("Project user ID is required") val projectDto = projectService.findOneByName(project.id!!) ?: throw NotFoundException( @@ -66,7 +67,7 @@ constructor( "projectNotFound" ) val subjectDto = - subjectService.createSubject(id, projectDto) + subjectService.createSubject(projectUserId, projectDto) ?: throw IllegalStateException("Failed to create subject for ID: $id") val user = userService.getUserWithAuthoritiesByLogin(subjectDto.login!!) From 30ac797ab27d737d90af849f8039c21e0345b40d Mon Sep 17 00:00:00 2001 From: Pauline Date: Sat, 28 Sep 2024 20:36:41 +0100 Subject: [PATCH 52/68] Update Project class to include user id --- .../main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt index 8aa60e54e..c03751113 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt @@ -93,12 +93,13 @@ class KratosSessionDTO( class Traits ( val name: String? = null, val email: String? = null, - val projects: List? = null, + val projects: List? = null, ) @Serializable - class Projects ( + class Project ( val id: String? = null, + val userId: String? = null, val name: String? = null, val eligibility: Map? = null, val consent: Map? = null, From 580959cb48bac793d2bc5165e0d882ec8aa54376 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sat, 28 Sep 2024 20:38:21 +0100 Subject: [PATCH 53/68] Update subject activation endpoint to use project user id --- .../org/radarbase/management/web/rest/KratosEndpoint.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt index 6b25b7ba2..666f64557 100644 --- a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt @@ -94,10 +94,15 @@ constructor( webhookDTO.cookies?.get("ory_kratos_session") ?: throw IllegalArgumentException("Session token is required") val kratosIdentity = sessionService.getSession(token).identity + val project = + kratosIdentity.traits!!.projects?.firstOrNull() + ?: throw NotAuthorizedException("Cannot create subject without project") + val projectUserId = project.userId ?: throw IllegalArgumentException("Project user ID is required") + if (!hasPermission(kratosIdentity, id)) { throw NotAuthorizedException("Not authorized to activate subject") } - subjectService.activateSubject(id) + subjectService.activateSubject(projectUserId) return ResponseEntity.ok() .headers(HeaderUtil.createEntityUpdateAlert(EntityName.SUBJECT, id)) .build() From fcfe2716a940a7ab415217a1eb26248f5c3c61c2 Mon Sep 17 00:00:00 2001 From: Pauline Date: Wed, 9 Oct 2024 01:19:00 +0800 Subject: [PATCH 54/68] Update Authservice and LoginEndpoint configs --- .../org/radarbase/management/service/AuthService.kt | 12 ++++++++---- .../radarbase/management/web/rest/LoginEndpoint.kt | 4 ++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/AuthService.kt b/src/main/java/org/radarbase/management/service/AuthService.kt index 6433af584..6fe1adddd 100644 --- a/src/main/java/org/radarbase/management/service/AuthService.kt +++ b/src/main/java/org/radarbase/management/service/AuthService.kt @@ -33,8 +33,8 @@ class AuthService( private val httpClient = HttpClient(CIO) { install(HttpTimeout) { - connectTimeoutMillis = Duration.ofSeconds(10).toMillis() - socketTimeoutMillis = Duration.ofSeconds(10).toMillis() + connectTimeoutMillis = Duration.ofSeconds(20).toMillis() + socketTimeoutMillis = Duration.ofSeconds(20).toMillis() requestTimeoutMillis = Duration.ofSeconds(300).toMillis() } install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } @@ -104,8 +104,12 @@ class AuthService( suspend fun fetchAccessToken(code: String): String { val tokenUrl = "${managementPortalProperties.authServer.serverUrl}/oauth2/token" + val clientId = managementPortalProperties.frontend.clientId + val clientSecret = managementPortalProperties.frontend.clientSecret + val authHeader = "Basic " + Base64.getEncoder().encodeToString("$clientId:$clientSecret".toByteArray()) val response = httpClient.post(tokenUrl) { + headers { append(HttpHeaders.Authorization, authHeader) } contentType(ContentType.Application.FormUrlEncoded) accept(ContentType.Application.Json) setBody( @@ -114,11 +118,11 @@ class AuthService( append("code", code) append( "redirect_uri", - "${managementPortalProperties.common.baseUrl}/api/redirect/login" + "${managementPortalProperties.common.managementPortalBaseUrl}/api/redirect/login" ) append( "client_id", - managementPortalProperties.frontend.clientId + clientId ) } .formUrlEncode(), diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index d775afcf6..1e34d7b46 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -30,7 +30,7 @@ constructor( } else { val accessToken = authService.fetchAccessToken(code) redirectView.url = - "${managementPortalProperties.common.baseUrl}/#/?access_token=$accessToken" + "${managementPortalProperties.common.managementPortalBaseUrl}/#/?access_token=$accessToken" } return redirectView } @@ -49,6 +49,6 @@ constructor( "state=${Instant.now()}&" + "audience=res_ManagementPortal&" + "scope=SOURCEDATA.CREATE SOURCETYPE.UPDATE SOURCETYPE.DELETE AUTHORITY.UPDATE MEASUREMENT.DELETE PROJECT.READ AUDIT.CREATE USER.DELETE AUTHORITY.DELETE SUBJECT.DELETE MEASUREMENT.UPDATE SOURCEDATA.UPDATE SUBJECT.READ USER.UPDATE SOURCETYPE.CREATE AUTHORITY.READ USER.CREATE SOURCE.CREATE SOURCE.READ SUBJECT.CREATE ROLE.UPDATE ROLE.READ MEASUREMENT.READ PROJECT.UPDATE PROJECT.DELETE ROLE.DELETE SOURCE.DELETE SOURCETYPE.READ ROLE.CREATE SOURCEDATA.DELETE SUBJECT.UPDATE SOURCE.UPDATE PROJECT.CREATE AUDIT.READ MEASUREMENT.CREATE AUDIT.DELETE AUDIT.UPDATE AUTHORITY.CREATE USER.READ SOURCEDATA.READ&" + - "redirect_uri=${managementPortalProperties.common.baseUrl}/api/redirect/login" + "redirect_uri=${managementPortalProperties.common.managementPortalBaseUrl}/api/redirect/login" } } From 968dae88474014f8997bef4bc39c2768c06ae026 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 25 Nov 2024 23:57:36 +0000 Subject: [PATCH 55/68] Refactor IdentityService to remove similar methods --- .../management/service/IdentityService.kt | 193 ++++++++---------- 1 file changed, 80 insertions(+), 113 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/IdentityService.kt b/src/main/java/org/radarbase/management/service/IdentityService.kt index 7eee8308d..3bb76918a 100644 --- a/src/main/java/org/radarbase/management/service/IdentityService.kt +++ b/src/main/java/org/radarbase/management/service/IdentityService.kt @@ -16,8 +16,9 @@ import org.radarbase.auth.authorization.RoleAuthority import org.radarbase.auth.exception.IdpException import org.radarbase.auth.kratos.KratosSessionDTO import org.radarbase.management.config.ManagementPortalProperties +import org.radarbase.management.domain.Role +import org.radarbase.management.domain.Subject import org.radarbase.management.domain.User -import org.radarbase.management.service.dto.UserDTO import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @@ -59,53 +60,59 @@ class IdentityService } /** - * Convert a [User] to a [KratosSessionDTO.Identity] object. - * @param user The object to convert - * @return the newly created DTO object + * Builds metadata for a user based on roles, authorities, and sources. */ - @Throws(IdpException::class) - private fun createIdentity(user: User): KratosSessionDTO.Identity = + private fun buildMetadata( + roles: Set, + authorities: Set, + login: String, + sources: List = emptyList(), + ): KratosSessionDTO.Metadata = try { - KratosSessionDTO.Identity( - schema_id = "researcher", - traits = KratosSessionDTO.Traits(email = user.email), - metadata_public = - KratosSessionDTO.Metadata( - aud = emptyList(), - sources = emptyList(), - roles = - user.roles.mapNotNull { role -> - role.authority?.name?.let { auth -> - when (role.role?.scope) { - RoleAuthority.Scope.GLOBAL -> auth - RoleAuthority.Scope.ORGANIZATION -> - "${role.organization!!.name}:$auth" - RoleAuthority.Scope.PROJECT -> - "${role.project!!.projectName}:$auth" - null -> null - } - } - }, - authorities = user.authorities, - scope = - Permission.scopes().filter { scope -> - authService.mayBeGranted( - user.roles.mapNotNull { it.role }, - Permission.ofScope(scope), - ) - }, - mp_login = user.login, - ), + KratosSessionDTO.Metadata( + aud = emptyList(), + sources = sources, + roles = + roles.mapNotNull { role -> + role.authority?.name?.let { auth -> + when (role.role?.scope) { + RoleAuthority.Scope.GLOBAL -> auth + RoleAuthority.Scope.ORGANIZATION -> + "${role.organization!!.name}:$auth" + RoleAuthority.Scope.PROJECT -> + "${role.project!!.projectName}:$auth" + null -> null + } + } + }, + authorities = authorities, + scope = + Permission.scopes().filter { scope -> + authService.mayBeGranted( + roles.mapNotNull { it.role }, + Permission.ofScope(scope), + ) + }, + mp_login = login, ) } catch (e: Throwable) { - val message = "Could not convert user ${user.login} to identity" + val message = "Could not build metadata for user $login" log.error(message) throw IdpException(message, e) } - /** - * Save a [User] to the IDP as an identity. Returns the generated [KratosSessionDTO.Identity] - */ + private fun createIdentity(user: User): KratosSessionDTO.Identity = + KratosSessionDTO.Identity( + schema_id = "researcher", + traits = KratosSessionDTO.Traits(email = user.email), + metadata_public = + buildMetadata( + roles = user.roles, + authorities = user.authorities, + login = user.login!!, + ), + ) + @Throws(IdpException::class) suspend fun saveAsIdentity(user: User): KratosSessionDTO.Identity = withContext(Dispatchers.IO) { @@ -127,19 +134,19 @@ class IdentityService } } - /** - * Update a [User] to the IDP as an identity. Returns the updated [KratosSessionDTO.Identity] - */ @Throws(IdpException::class) - suspend fun updateAssociatedIdentity(user: User): KratosSessionDTO.Identity = + suspend fun updateAssociatedIdentity( + user: User, + subject: Subject? = null, + ): KratosSessionDTO.Identity = withContext(Dispatchers.IO) { val identityId = user.identity - ?: throw IdpException( - "User ${user.login} could not be updated on the IDP. No identity was set", - ) + ?: subject?.externalId ?: throw IdpException("User has no identity") - val identity = createIdentity(user) + val identity = getExistingIdentity(identityId) + val sources = subject?.sources?.map { it.sourceId.toString() } ?: emptyList() + identity.metadata_public = getIdentityMetadataWithRoles(user, sources) val response = httpClient.put { url("$adminUrl/admin/identities/$identityId") @@ -157,36 +164,39 @@ class IdentityService } } - /** - * Update [KratosSessionDTO.Identity] metadata with user roles. Returns the updated - * [KratosSessionDTO.Identity] - */ @Throws(IdpException::class) - suspend fun updateIdentityMetadataWithRoles( - identity: KratosSessionDTO.Identity, - user: UserDTO, - ): KratosSessionDTO.Identity = + suspend fun getExistingIdentity(identityId: String): KratosSessionDTO.Identity = withContext(Dispatchers.IO) { - val updatedIdentity = identity.copy(metadata_public = getIdentityMetadata(user)) - val response = - httpClient.put { - url("$adminUrl/admin/identities/${updatedIdentity.id}") + httpClient.get { + url("$adminUrl/admin/identities/$identityId") contentType(ContentType.Application.Json) accept(ContentType.Application.Json) - setBody(updatedIdentity) } if (response.status.isSuccess()) { response.body().also { - log.debug("Updated identity for ${it.id}") + log.debug("Retrieved identity for ${it.id}") } } else { - throw IdpException("Couldn't update identity on server at $adminUrl") + throw IdpException("Couldn't retrieve identity from server at $adminUrl") } } - /** Delete a [User] from the IDP as an identity. */ + @Throws(IdpException::class) + suspend fun getIdentityMetadataWithRoles( + user: User, + sources: List, + ): KratosSessionDTO.Metadata = + withContext(Dispatchers.IO) { + buildMetadata( + roles = user.roles, + authorities = user.authorities, + login = user.login!!, + sources = sources, + ) + } + @Throws(IdpException::class) suspend fun deleteAssociatedIdentity(userIdentity: String?) = withContext(Dispatchers.IO) { @@ -210,59 +220,16 @@ class IdentityService } } - /** - * Convert a [UserDTO] to a [KratosSessionDTO.Metadata] object. - * @param user The object to convert - * @return the newly created DTO object - */ - @Throws(IdpException::class) - fun getIdentityMetadata(user: UserDTO): KratosSessionDTO.Metadata = - try { - KratosSessionDTO.Metadata( - aud = emptyList(), - sources = emptyList(), - roles = - user.roles.orEmpty().mapNotNull { role -> - role.authorityName?.let { auth -> - when { - role.projectName != null -> "${role.projectName}:$auth" - role.organizationName != null -> - "${role.organizationName}:$auth" - else -> auth - } - } - }, - authorities = user.authorities.orEmpty(), - scope = - Permission.scopes().filter { scope -> - authService.mayBeGranted( - user.roles?.mapNotNull { - RoleAuthority.valueOfAuthority(it.authorityName!!) - } - ?: emptyList(), - Permission.ofScope(scope), - ) - }, - mp_login = user.login, - ) - } catch (e: Throwable) { - val message = "Could not convert user ${user.login} to identity" - log.error(message) - throw IdpException(message, e) - } - - /** - * Sends a Kratos activation email to the specified user. - */ @Throws(IdpException::class) suspend fun sendActivationEmail(user: User): String = withContext(Dispatchers.IO) { val flowResponse = - httpClient.get { - url("$publicUrl/self-service/verification/api") - contentType(ContentType.Application.Json) - accept(ContentType.Application.Json) - }.body() + httpClient + .get { + url("$publicUrl/self-service/verification/api") + contentType(ContentType.Application.Json) + accept(ContentType.Application.Json) + }.body() val flowId = flowResponse.id From a18bc568b7c08f9f4cdf049626d93cf74f4339f8 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 25 Nov 2024 23:58:22 +0000 Subject: [PATCH 56/68] Add support for updating identity server with changes in Subject and User in MP --- .../management/service/SubjectService.kt | 30 +++++++++++++++---- .../management/service/UserService.kt | 15 ++++++++-- .../management/web/rest/KratosEndpoint.kt | 15 ++-------- .../management/web/rest/SubjectResource.kt | 6 ++-- 4 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/SubjectService.kt b/src/main/java/org/radarbase/management/service/SubjectService.kt index 056e1d496..3b0b0d9e5 100644 --- a/src/main/java/org/radarbase/management/service/SubjectService.kt +++ b/src/main/java/org/radarbase/management/service/SubjectService.kt @@ -64,7 +64,9 @@ class SubjectService( @Autowired private val managementPortalProperties: ManagementPortalProperties, @Autowired private val passwordService: PasswordService, @Autowired private val authorityRepository: AuthorityRepository, - @Autowired private val authService: AuthService + @Autowired private val authService: AuthService, + @Autowired private val userService: UserService, + @Autowired private val identityService: IdentityService ) { /** @@ -74,7 +76,7 @@ class SubjectService( * @return the newly created subject */ @Transactional - fun createSubject(subjectDto: SubjectDTO, activated: Boolean? = true): SubjectDTO? { + suspend fun createSubject(subjectDto: SubjectDTO, activated: Boolean? = true): SubjectDTO? { val subject = subjectMapper.subjectDTOToSubject(subjectDto) ?: throw NullPointerException() // assign roles val user = subject.user @@ -107,14 +109,24 @@ class SubjectService( subject.enrollmentDate = ZonedDateTime.now() } sourceRepository.saveAll(subject.sources) - return subjectMapper.subjectToSubjectReducedProjectDTO(subjectRepository.save(subject)) + + val savedSubject = subjectRepository.save(subject) + val subjectDto = subjectMapper.subjectToSubjectReducedProjectDTO(savedSubject) + + // Update identity server identity with roles + userService.getUserWithAuthoritiesByLogin(login = subjectDto?.login!!)?.let { user -> + identityService.updateAssociatedIdentity(user, subject) + } + + return subjectDto } - fun createSubject(id: String, projectDto: ProjectDTO): SubjectDTO? { + suspend fun createSubject(id: String, projectDto: ProjectDTO, externalId: String): SubjectDTO? { return createSubject( SubjectDTO().apply { login = id project = projectDto + this.externalId = externalId }, activated = false ) @@ -165,7 +177,7 @@ class SubjectService( * @return the updated subject */ @Transactional - fun updateSubject(newSubjectDto: SubjectDTO): SubjectDTO? { + suspend fun updateSubject(newSubjectDto: SubjectDTO): SubjectDTO? { if (newSubjectDto.id == null) { return createSubject(newSubjectDto) } @@ -288,7 +300,7 @@ class SubjectService( * updates meta-data. */ @Transactional - fun assignOrUpdateSource( + suspend fun assignOrUpdateSource( subject: Subject, sourceType: SourceType, project: Project?, @@ -350,6 +362,12 @@ class SubjectService( ) } subjectRepository.save(subject) + + // Update identity server identity with roles + userService.getUserWithAuthoritiesByLogin(login = subject.user?.login!!)?.let { user -> + identityService.updateAssociatedIdentity(user, subject) + } + return sourceMapper.sourceToMinimalSourceDetailsDTO(assignedSource) } diff --git a/src/main/java/org/radarbase/management/service/UserService.kt b/src/main/java/org/radarbase/management/service/UserService.kt index 5f1736963..011e89a64 100644 --- a/src/main/java/org/radarbase/management/service/UserService.kt +++ b/src/main/java/org/radarbase/management/service/UserService.kt @@ -401,16 +401,27 @@ class UserService @Autowired constructor( } /** - * Get the user with the given login. + * Get the user dto with the given login. * @param login the login * @return an [Optional] which holds the user if one was found with the given login, * and is empty otherwise */ @Transactional(readOnly = true) - fun getUserWithAuthoritiesByLogin(login: String): UserDTO? { + fun getUserDtoWithAuthoritiesByLogin(login: String): UserDTO? { return userMapper.userToUserDTO(userRepository.findOneWithRolesByLogin(login)) } + /** + * Get the user with the given login. + * @param login the login + * @return an [Optional] which holds the user if one was found with the given login, + * and is empty otherwise + */ + @Transactional(readOnly = true) + fun getUserWithAuthoritiesByLogin(login: String): User? { + return userRepository.findOneWithRolesByLogin(login) + } + @Transactional(readOnly = true) /** * Get the current user. diff --git a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt index 666f64557..a76711c13 100644 --- a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt @@ -9,7 +9,6 @@ import org.radarbase.management.repository.SubjectRepository import org.radarbase.management.security.NotAuthorizedException import org.radarbase.management.service.* import org.radarbase.management.service.dto.KratosSubjectWebhookDTO -import org.radarbase.management.service.mapper.SubjectMapper import org.radarbase.management.web.rest.errors.EntityName import org.radarbase.management.web.rest.errors.NotFoundException import org.radarbase.management.web.rest.util.HeaderUtil @@ -26,10 +25,7 @@ constructor( @Autowired private val subjectService: SubjectService, @Autowired private val subjectRepository: SubjectRepository, @Autowired private val projectService: ProjectService, - @Autowired private val userService: UserService, - @Autowired private val identityService: IdentityService, @Autowired private val managementPortalProperties: ManagementPortalProperties, - @Autowired private val subjectMapper: SubjectMapper, ) { private var sessionService: SessionService = SessionService(managementPortalProperties.identityServer.publicUrl()) @@ -48,6 +44,7 @@ constructor( suspend fun createSubject( @RequestBody webhookDTO: KratosSubjectWebhookDTO, ): ResponseEntity { + logger.debug("REST request to create subject : $webhookDTO") val kratosIdentity = webhookDTO.identity ?: throw IllegalArgumentException("Identity is required") @@ -67,17 +64,9 @@ constructor( "projectNotFound" ) val subjectDto = - subjectService.createSubject(projectUserId, projectDto) + subjectService.createSubject(projectUserId, projectDto, id) ?: throw IllegalStateException("Failed to create subject for ID: $id") - val user = - userService.getUserWithAuthoritiesByLogin(subjectDto.login!!) - ?: throw NotFoundException( - "User not found with login: ${subjectDto.login}", - EntityName.USER, - "userNotFound" - ) - identityService.updateIdentityMetadataWithRoles(kratosIdentity, user) return ResponseEntity.created(ResourceUriService.getUri(subjectDto)) .headers(HeaderUtil.createEntityCreationAlert(EntityName.SUBJECT, id)) .build() diff --git a/src/main/java/org/radarbase/management/web/rest/SubjectResource.kt b/src/main/java/org/radarbase/management/web/rest/SubjectResource.kt index 3bba61551..51dcd16bd 100644 --- a/src/main/java/org/radarbase/management/web/rest/SubjectResource.kt +++ b/src/main/java/org/radarbase/management/web/rest/SubjectResource.kt @@ -84,7 +84,7 @@ class SubjectResource( @PostMapping("/subjects") @Timed @Throws(URISyntaxException::class, NotAuthorizedException::class) - fun createSubject(@RequestBody subjectDto: SubjectDTO): ResponseEntity { + suspend fun createSubject(@RequestBody subjectDto: SubjectDTO): ResponseEntity { log.debug("REST request to save Subject : {}", subjectDto) val projectName = getProjectName(subjectDto) authService.checkPermission(Permission.SUBJECT_CREATE, { e: EntityDetails -> e.project(projectName) }) @@ -135,7 +135,7 @@ class SubjectResource( @PutMapping("/subjects") @Timed @Throws(URISyntaxException::class, NotAuthorizedException::class) - fun updateSubject(@RequestBody subjectDto: SubjectDTO): ResponseEntity { + suspend fun updateSubject(@RequestBody subjectDto: SubjectDTO): ResponseEntity { log.debug("REST request to update Subject : {}", subjectDto) if (subjectDto.id == null) { return createSubject(subjectDto) @@ -385,7 +385,7 @@ class SubjectResource( ) @Timed @Throws(URISyntaxException::class, NotAuthorizedException::class) - fun assignSources( + suspend fun assignSources( @PathVariable login: String?, @RequestBody sourceDto: MinimalSourceDetailsDTO ): ResponseEntity { From ac468e418b3bc778f41ee5db939aee318df0c296 Mon Sep 17 00:00:00 2001 From: Pauline Date: Mon, 25 Nov 2024 23:58:43 +0000 Subject: [PATCH 57/68] Rename userService method --- .../java/org/radarbase/management/web/rest/UserResource.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/radarbase/management/web/rest/UserResource.kt b/src/main/java/org/radarbase/management/web/rest/UserResource.kt index 6b0a51a23..3c8eeca3b 100644 --- a/src/main/java/org/radarbase/management/web/rest/UserResource.kt +++ b/src/main/java/org/radarbase/management/web/rest/UserResource.kt @@ -214,7 +214,7 @@ class UserResource( log.debug("REST request to get User : {}", login) authService.checkPermission(Permission.USER_READ, { e: EntityDetails -> e.user(login) }) return ResponseUtil.wrapOrNotFound( - Optional.ofNullable(userService.getUserWithAuthoritiesByLogin(login)) + Optional.ofNullable(userService.getUserDtoWithAuthoritiesByLogin(login)) ) } @@ -251,7 +251,7 @@ class UserResource( log.debug("REST request to read User roles: {}", login) authService.checkPermission(Permission.ROLE_READ, { e: EntityDetails -> e.user(login) }) return ResponseUtil.wrapOrNotFound( - Optional.ofNullable(userService.getUserWithAuthoritiesByLogin(login).let { obj: UserDTO? -> obj?.roles }) + Optional.ofNullable(userService.getUserDtoWithAuthoritiesByLogin(login).let { obj: UserDTO? -> obj?.roles }) ) } From a20309884bb94cb6b3c743a426b53ddd3c500270 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 26 Nov 2024 00:05:25 +0000 Subject: [PATCH 58/68] Save identity id when creating user --- src/main/java/org/radarbase/management/service/UserService.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/radarbase/management/service/UserService.kt b/src/main/java/org/radarbase/management/service/UserService.kt index 011e89a64..2e65bd81a 100644 --- a/src/main/java/org/radarbase/management/service/UserService.kt +++ b/src/main/java/org/radarbase/management/service/UserService.kt @@ -170,7 +170,7 @@ class UserService @Autowired constructor( user.roles = getUserRoles(userDto.roles, mutableSetOf()) try { - identityService.saveAsIdentity(user) + user.identity = identityService.saveAsIdentity(user)?.id } catch (e: Throwable) { log.warn("could not save user ${user.login} as identity", e) @@ -383,6 +383,8 @@ class UserService @Autowired constructor( // there is no identity for this user, so we create it and save it to the IDP val id = identityService.saveAsIdentity(user) + // then save the identifier and update our database + user.identity = id?.id return userMapper.userToUserDTO(user) ?: throw Exception("Admin user could not be converted to DTO") From ca378a0acd4f4f8b1e230407e778ea9f58dff484 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 26 Nov 2024 00:06:46 +0000 Subject: [PATCH 59/68] Format SubjectService --- .../management/service/SubjectService.kt | 409 +++++++++--------- 1 file changed, 214 insertions(+), 195 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/SubjectService.kt b/src/main/java/org/radarbase/management/service/SubjectService.kt index 3b0b0d9e5..26f03f467 100644 --- a/src/main/java/org/radarbase/management/service/SubjectService.kt +++ b/src/main/java/org/radarbase/management/service/SubjectService.kt @@ -1,13 +1,5 @@ package org.radarbase.management.service -import java.net.MalformedURLException -import java.net.URL -import java.time.ZonedDateTime -import java.util.* -import java.util.function.Consumer -import java.util.function.Function -import java.util.function.Predicate -import javax.annotation.Nonnull import org.hibernate.envers.query.AuditEntity import org.radarbase.auth.authorization.EntityDetails import org.radarbase.auth.authorization.Permission @@ -48,27 +40,34 @@ import org.springframework.data.domain.Page import org.springframework.data.history.Revision import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional +import java.net.MalformedURLException +import java.net.URL +import java.time.ZonedDateTime +import java.util.* +import java.util.function.Consumer +import java.util.function.Function +import java.util.function.Predicate +import javax.annotation.Nonnull /** Created by nivethika on 26-5-17. */ @Service @Transactional class SubjectService( - @Autowired private val subjectMapper: SubjectMapper, - @Autowired private val projectMapper: ProjectMapper, - @Autowired private val subjectRepository: SubjectRepository, - @Autowired private val sourceRepository: SourceRepository, - @Autowired private val sourceMapper: SourceMapper, - @Autowired private val roleRepository: RoleRepository, - @Autowired private val groupRepository: GroupRepository, - @Autowired private val revisionService: RevisionService, - @Autowired private val managementPortalProperties: ManagementPortalProperties, - @Autowired private val passwordService: PasswordService, - @Autowired private val authorityRepository: AuthorityRepository, - @Autowired private val authService: AuthService, - @Autowired private val userService: UserService, - @Autowired private val identityService: IdentityService + @Autowired private val subjectMapper: SubjectMapper, + @Autowired private val projectMapper: ProjectMapper, + @Autowired private val subjectRepository: SubjectRepository, + @Autowired private val sourceRepository: SourceRepository, + @Autowired private val sourceMapper: SourceMapper, + @Autowired private val roleRepository: RoleRepository, + @Autowired private val groupRepository: GroupRepository, + @Autowired private val revisionService: RevisionService, + @Autowired private val managementPortalProperties: ManagementPortalProperties, + @Autowired private val passwordService: PasswordService, + @Autowired private val authorityRepository: AuthorityRepository, + @Autowired private val authService: AuthService, + @Autowired private val userService: UserService, + @Autowired private val identityService: IdentityService, ) { - /** * Create a new subject. * @@ -76,7 +75,10 @@ class SubjectService( * @return the newly created subject */ @Transactional - suspend fun createSubject(subjectDto: SubjectDTO, activated: Boolean? = true): SubjectDTO? { + suspend fun createSubject( + subjectDto: SubjectDTO, + activated: Boolean? = true, + ): SubjectDTO? { val subject = subjectMapper.subjectDTOToSubject(subjectDto) ?: throw NullPointerException() // assign roles val user = subject.user @@ -99,10 +101,10 @@ class SubjectService( // set if any devices are set as assigned if (subject.sources.isNotEmpty()) { subject.sources.forEach( - Consumer { s: Source -> - s.assigned = true - s.subject(subject) - } + Consumer { s: Source -> + s.assigned = true + s.subject(subject) + }, ) } if (subject.enrollmentDate == null) { @@ -121,31 +123,37 @@ class SubjectService( return subjectDto } - suspend fun createSubject(id: String, projectDto: ProjectDTO, externalId: String): SubjectDTO? { - return createSubject( - SubjectDTO().apply { - login = id - project = projectDto - this.externalId = externalId - }, - activated = false + suspend fun createSubject( + id: String, + projectDto: ProjectDTO, + externalId: String, + ): SubjectDTO? = + createSubject( + SubjectDTO().apply { + login = id + project = projectDto + this.externalId = externalId + }, + activated = false, ) - } - private fun getSubjectGroup(project: Project?, groupName: String?): Group? { - return if (project == null || groupName == null) { + private fun getSubjectGroup( + project: Project?, + groupName: String?, + ): Group? = + if (project == null || groupName == null) { null - } else - groupRepository.findByProjectIdAndName(project.id, groupName) - ?: throw BadRequestException( - "Group " + - groupName + - " does not exist in project " + - project.projectName, - EntityName.GROUP, - ErrorConstants.ERR_GROUP_NOT_FOUND - ) - } + } else { + groupRepository.findByProjectIdAndName(project.id, groupName) + ?: throw BadRequestException( + "Group " + + groupName + + " does not exist in project " + + project.projectName, + EntityName.GROUP, + ErrorConstants.ERR_GROUP_NOT_FOUND, + ) + } /** * Fetch Participant role of the project if available, otherwise create a new Role and assign. @@ -154,20 +162,25 @@ class SubjectService( * @return relevant Participant role * @throws java.util.NoSuchElementException if the authority name is not in the database */ - private fun getProjectParticipantRole(project: Project?, authority: RoleAuthority): Role { + private fun getProjectParticipantRole( + project: Project?, + authority: RoleAuthority, + ): Role { val ans: Role? = - roleRepository.findOneByProjectIdAndAuthorityName(project?.id, authority.authority) + roleRepository.findOneByProjectIdAndAuthorityName(project?.id, authority.authority) return if (ans == null) { val subjectRole = Role() val auth: Authority = - authorityRepository.findByAuthorityName(authority.authority) - ?: authorityRepository.save(Authority(authority)) + authorityRepository.findByAuthorityName(authority.authority) + ?: authorityRepository.save(Authority(authority)) subjectRole.authority = auth subjectRole.project = project roleRepository.save(subjectRole) subjectRole - } else ans + } else { + ans + } } /** @@ -187,7 +200,7 @@ class SubjectService( subjectMapper.safeUpdateSubjectFromDTO(newSubjectDto, subjectFromDb) sourcesToUpdate.addAll(subjectFromDb.sources) subjectFromDb.sources.forEach( - Consumer { s: Source -> s.subject(subjectFromDb).assigned = true } + Consumer { s: Source -> s.subject(subjectFromDb).assigned = true }, ) sourceRepository.saveAll(sourcesToUpdate) // update participant role @@ -195,7 +208,7 @@ class SubjectService( // Set group subjectFromDb.group = getSubjectGroup(subjectFromDb.activeProject, newSubjectDto.group) return subjectMapper.subjectToSubjectReducedProjectDTO( - subjectRepository.save(subjectFromDb) + subjectRepository.save(subjectFromDb), ) } @@ -205,37 +218,39 @@ class SubjectService( return subjectMapper.subjectToSubjectReducedProjectDTO(subjectRepository.save(subject)) } - private fun updateParticipantRoles(subject: Subject, subjectDto: SubjectDTO): MutableSet { + private fun updateParticipantRoles( + subject: Subject, + subjectDto: SubjectDTO, + ): MutableSet { if (subjectDto.project == null || subjectDto.project!!.projectName == null) { return subject.user!!.roles } val existingRoles = - subject.user!! - .roles - .map { - // make participant inactive in projects that do not match the new - // project - if (it.authority!!.name == RoleAuthority.PARTICIPANT.authority && - it.project!!.projectName != - subjectDto.project!!.projectName - ) { - return@map getProjectParticipantRole( - it.project, - RoleAuthority.INACTIVE_PARTICIPANT - ) - } else { - // do not modify other roles. - return@map it - } - } - .toMutableSet() + subject.user!! + .roles + .map { + // make participant inactive in projects that do not match the new + // project + if (it.authority!!.name == RoleAuthority.PARTICIPANT.authority && + it.project!!.projectName != + subjectDto.project!!.projectName + ) { + return@map getProjectParticipantRole( + it.project, + RoleAuthority.INACTIVE_PARTICIPANT, + ) + } else { + // do not modify other roles. + return@map it + } + }.toMutableSet() // Ensure that given project is present val newProjectRole = - getProjectParticipantRole( - projectMapper.projectDTOToProject(subjectDto.project), - RoleAuthority.PARTICIPANT - ) + getProjectParticipantRole( + projectMapper.projectDTOToProject(subjectDto.project), + RoleAuthority.PARTICIPANT, + ) existingRoles.add(newProjectRole) return existingRoles @@ -262,18 +277,17 @@ class SubjectService( return subjectMapper.subjectToSubjectReducedProjectDTO(subjectRepository.save(subject)) } - private fun ensureSubject(subjectDto: SubjectDTO): Subject { - return try { + private fun ensureSubject(subjectDto: SubjectDTO): Subject = + try { subjectDto.id?.let { subjectRepository.findById(it).get() } - ?: throw Exception("invalid subject ${subjectDto.login}: No ID") + ?: throw Exception("invalid subject ${subjectDto.login}: No ID") } catch (e: Throwable) { throw NotFoundException( - "Subject with ID " + subjectDto.id + " not found.", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND + "Subject with ID " + subjectDto.id + " not found.", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, ) } - } /** * Unassign all sources from a subject. This method saves the unassigned sources, but does NOT @@ -283,12 +297,12 @@ class SubjectService( */ private fun unassignAllSources(subject: Subject) { subject.sources.forEach( - Consumer { source: Source -> - source.assigned = false - source.subject = null - source.deleted = true - sourceRepository.save(source) - } + Consumer { source: Source -> + source.assigned = false + source.subject = null + source.deleted = true + sourceRepository.save(source) + }, ) subject.sources.clear() } @@ -301,10 +315,10 @@ class SubjectService( */ @Transactional suspend fun assignOrUpdateSource( - subject: Subject, - sourceType: SourceType, - project: Project?, - sourceRegistrationDto: MinimalSourceDetailsDTO + subject: Subject, + sourceType: SourceType, + project: Project?, + sourceRegistrationDto: MinimalSourceDetailsDTO, ): MinimalSourceDetailsDTO { val assignedSource: Source if (sourceRegistrationDto.sourceId != null) { @@ -312,17 +326,17 @@ class SubjectService( assignedSource = updateSourceAssignedSubject(subject, sourceRegistrationDto) } else if (sourceType.canRegisterDynamically!!) { val sources = - subjectRepository.findSubjectSourcesBySourceType( - subject.user!!.login, - sourceType.producer, - sourceType.model, - sourceType.catalogVersion - ) + subjectRepository.findSubjectSourcesBySourceType( + subject.user!!.login, + sourceType.producer, + sourceType.model, + sourceType.catalogVersion, + ) // create a source and register metadata // we allow only one source of a source-type per subject if (sources.isNullOrEmpty()) { var source = - Source(sourceType).project(project).sourceType(sourceType).subject(subject) + Source(sourceType).project(project).sourceType(sourceType).subject(subject) source.assigned = true source.attributes += sourceRegistrationDto.attributes // if source name is provided update source name @@ -333,11 +347,11 @@ class SubjectService( // make sure there is no source available on the same name. if (sourceRepository.findOneBySourceName(source.sourceName!!) != null) { throw ConflictException( - "SourceName already in use. Cannot create a " + - "source with existing source-name ", - EntityName.SUBJECT, - ErrorConstants.ERR_SOURCE_NAME_EXISTS, - Collections.singletonMap("source-name", source.sourceName) + "SourceName already in use. Cannot create a " + + "source with existing source-name ", + EntityName.SUBJECT, + ErrorConstants.ERR_SOURCE_NAME_EXISTS, + Collections.singletonMap("source-name", source.sourceName), ) } source = sourceRepository.save(source) @@ -345,20 +359,20 @@ class SubjectService( subject.sources.add(source) } else { throw ConflictException( - "A Source of SourceType with the specified producer, model and version" + - " was already registered for subject login", - EntityName.SUBJECT, - ErrorConstants.ERR_SOURCE_TYPE_EXISTS, - sourceTypeAttributes(sourceType, subject) + "A Source of SourceType with the specified producer, model and version" + + " was already registered for subject login", + EntityName.SUBJECT, + ErrorConstants.ERR_SOURCE_TYPE_EXISTS, + sourceTypeAttributes(sourceType, subject), ) } } else { // new source since sourceId == null, but canRegisterDynamically == false throw BadRequestException( - "The source type is not eligible for dynamic " + "registration", - EntityName.SOURCE_TYPE, - "error.InvalidDynamicSourceRegistration", - sourceTypeAttributes(sourceType, subject) + "The source type is not eligible for dynamic " + "registration", + EntityName.SOURCE_TYPE, + "error.InvalidDynamicSourceRegistration", + sourceTypeAttributes(sourceType, subject), ) } subjectRepository.save(subject) @@ -379,24 +393,24 @@ class SubjectService( * @return Updated [Source] instance. */ private fun updateSourceAssignedSubject( - subject: Subject, - sourceRegistrationDto: MinimalSourceDetailsDTO + subject: Subject, + sourceRegistrationDto: MinimalSourceDetailsDTO, ): Source { // for manually registered devices only add meta-data val source = - subjectRepository.findSubjectSourcesBySourceId( - subject.user?.login, - sourceRegistrationDto.sourceId - ) + subjectRepository.findSubjectSourcesBySourceId( + subject.user?.login, + sourceRegistrationDto.sourceId, + ) if (source == null) { val errorParams: MutableMap = HashMap() errorParams["sourceId"] = sourceRegistrationDto.sourceId.toString() errorParams["subject-login"] = subject.user?.login throw NotFoundException( - "No source with source-id to assigned to the subject with subject-login", - EntityName.SUBJECT, - ErrorConstants.ERR_SOURCE_NOT_FOUND, - errorParams + "No source with source-id to assigned to the subject with subject-login", + EntityName.SUBJECT, + ErrorConstants.ERR_SOURCE_NOT_FOUND, + errorParams, ) } @@ -416,10 +430,11 @@ class SubjectService( */ fun getSources(subject: Subject): List { val sources = subjectRepository.findSourcesBySubjectLogin(subject.user?.login) - if (sources.isEmpty()) - throw org.webjars.NotFoundException( - "Could not find sources for user ${subject.user}" - ) + if (sources.isEmpty()) { + throw org.webjars.NotFoundException( + "Could not find sources for user ${subject.user}", + ) + } return sourceMapper.sourcesToMinimalSourceDetailsDTOs(sources) } @@ -434,12 +449,12 @@ class SubjectService( subjectRepository.delete(subject) log.debug("Deleted Subject: {}", subject) } - ?: throw NotFoundException( - "subject not found for given login.", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login) - ) + ?: throw NotFoundException( + "subject not found for given login.", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login), + ) } /** @@ -452,13 +467,14 @@ class SubjectService( val revisions = subject.id?.let { subjectRepository.findRevisions(it) } // collect distinct sources in a set val sources: List? = - revisions?.content - ?.flatMap { p: Revision -> p.entity.sources } - ?.distinctBy { obj: Source -> obj.sourceId } + revisions + ?.content + ?.flatMap { p: Revision -> p.entity.sources } + ?.distinctBy { obj: Source -> obj.sourceId } return sources - ?.map { p: Source -> sourceMapper.sourceToMinimalSourceDetailsDTO(p) } - ?.toList() + ?.map { p: Source -> sourceMapper.sourceToMinimalSourceDetailsDTO(p) } + ?.toList() } /** @@ -471,26 +487,29 @@ class SubjectService( * number */ @Throws(NotFoundException::class, NotAuthorizedException::class) - fun findRevision(login: String?, revision: Int?): SubjectDTO { + fun findRevision( + login: String?, + revision: Int?, + ): SubjectDTO { // first get latest known version of the subject, if it's deleted we can't load the entity // directly by e.g. findOneByLogin val latest = getLatestRevision(login) authService.checkPermission( - Permission.SUBJECT_READ, - { e: EntityDetails -> e.project(latest.project?.projectName).subject(latest.login) } + Permission.SUBJECT_READ, + { e: EntityDetails -> e.project(latest.project?.projectName).subject(latest.login) }, ) return revisionService.findRevision( - revision, - latest.id, - Subject::class.java, - subjectMapper::subjectToSubjectReducedProjectDTO + revision, + latest.id, + Subject::class.java, + subjectMapper::subjectToSubjectReducedProjectDTO, ) - ?: throw NotFoundException( - "subject not found for given login and revision.", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login) - ) + ?: throw NotFoundException( + "subject not found for given login and revision.", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login), + ) } /** @@ -503,32 +522,32 @@ class SubjectService( @Throws(NotFoundException::class) fun getLatestRevision(login: String?): SubjectDTO { val user = - revisionService.getLatestRevisionForEntity( - User::class.java, - listOf(AuditEntity.property("login").eq(login)) - ) - .orElseThrow { - NotFoundException( - "Subject latest revision not found " + "for login", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login) - ) - } as - UserDTO - return revisionService.getLatestRevisionForEntity( - Subject::class.java, - listOf(AuditEntity.property("user").eq(user)) - ) - .orElseThrow { + revisionService + .getLatestRevisionForEntity( + User::class.java, + listOf(AuditEntity.property("login").eq(login)), + ).orElseThrow { NotFoundException( - "Subject latest revision not found " + "for login", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND, - Collections.singletonMap("subjectLogin", login) + "Subject latest revision not found " + "for login", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login), ) } as - SubjectDTO + UserDTO + return revisionService + .getLatestRevisionForEntity( + Subject::class.java, + listOf(AuditEntity.property("user").eq(user)), + ).orElseThrow { + NotFoundException( + "Subject latest revision not found " + "for login", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + Collections.singletonMap("subjectLogin", login), + ) + } as + SubjectDTO } /** @@ -540,11 +559,11 @@ class SubjectService( fun findOneByLogin(login: String?): Subject { val subject = subjectRepository.findOneWithEagerBySubjectLogin(login) return subject - ?: throw NotFoundException( - "Subject not found with login", - EntityName.SUBJECT, - ErrorConstants.ERR_SUBJECT_NOT_FOUND - ) + ?: throw NotFoundException( + "Subject not found with login", + EntityName.SUBJECT, + ErrorConstants.ERR_SUBJECT_NOT_FOUND, + ) } /** @@ -570,11 +589,10 @@ class SubjectService( * @return URL of privacy policy for this token */ fun getPrivacyPolicyUrl(subject: Subject): URL { - // load default url from config val policyUrl: String = - subject.activeProject?.attributes?.get(ProjectDTO.PRIVACY_POLICY_URL) - ?: managementPortalProperties.common.privacyPolicyUrl + subject.activeProject?.attributes?.get(ProjectDTO.PRIVACY_POLICY_URL) + ?: managementPortalProperties.common.privacyPolicyUrl return try { URL(policyUrl) } catch (e: MalformedURLException) { @@ -582,20 +600,21 @@ class SubjectService( params["url"] = policyUrl params["message"] = e.message throw InvalidStateException( - "No valid privacy-policy Url configured. Please " + - "verify your project's privacy-policy url and/or general url config", - EntityName.OAUTH_CLIENT, - ErrorConstants.ERR_NO_VALID_PRIVACY_POLICY_URL_CONFIGURED, - params + "No valid privacy-policy Url configured. Please " + + "verify your project's privacy-policy url and/or general url config", + EntityName.OAUTH_CLIENT, + ErrorConstants.ERR_NO_VALID_PRIVACY_POLICY_URL_CONFIGURED, + params, ) } } companion object { private val log = LoggerFactory.getLogger(SubjectService::class.java) + private fun sourceTypeAttributes( - sourceType: SourceType, - subject: Subject + sourceType: SourceType, + subject: Subject, ): Map { val errorParams: MutableMap = HashMap() errorParams["producer"] = sourceType.producer From 5e52a1f66f012f280ac7b803f745aa639a6429b7 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 26 Nov 2024 00:10:01 +0000 Subject: [PATCH 60/68] Fix tests --- ...capIntegrationWorkFlowOnServiceLevelTest.kt | 2 +- .../management/service/SubjectServiceTest.kt | 2 +- .../web/rest/GroupResourceIntTest.kt | 2 +- .../web/rest/SubjectResourceIntTest.kt | 18 +++++++++--------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/test/java/org/radarbase/management/service/RedcapIntegrationWorkFlowOnServiceLevelTest.kt b/src/test/java/org/radarbase/management/service/RedcapIntegrationWorkFlowOnServiceLevelTest.kt index e1ca52688..a568145ad 100644 --- a/src/test/java/org/radarbase/management/service/RedcapIntegrationWorkFlowOnServiceLevelTest.kt +++ b/src/test/java/org/radarbase/management/service/RedcapIntegrationWorkFlowOnServiceLevelTest.kt @@ -26,7 +26,7 @@ internal class RedcapIntegrationWorkFlowOnServiceLevelTest { @Autowired private val subjectService: SubjectService? = null @Test - fun testRedcapIntegrationWorkFlowOnServiceLevel() { + suspend fun testRedcapIntegrationWorkFlowOnServiceLevel() { val externalProjectUrl = "MyUrl" val externalProjectId = "MyId" val projectLocation = "London" diff --git a/src/test/java/org/radarbase/management/service/SubjectServiceTest.kt b/src/test/java/org/radarbase/management/service/SubjectServiceTest.kt index 0414c0ecc..96fb158ee 100644 --- a/src/test/java/org/radarbase/management/service/SubjectServiceTest.kt +++ b/src/test/java/org/radarbase/management/service/SubjectServiceTest.kt @@ -28,7 +28,7 @@ class SubjectServiceTest( @Test @Transactional - fun testGetPrivacyPolicyUrl() { + suspend fun testGetPrivacyPolicyUrl() { projectService.save(createEntityDTO().project!!) val c = createEntityDTO() val created = subjectService.createSubject(c) diff --git a/src/test/java/org/radarbase/management/web/rest/GroupResourceIntTest.kt b/src/test/java/org/radarbase/management/web/rest/GroupResourceIntTest.kt index 1c1a80d1a..56033c34a 100644 --- a/src/test/java/org/radarbase/management/web/rest/GroupResourceIntTest.kt +++ b/src/test/java/org/radarbase/management/web/rest/GroupResourceIntTest.kt @@ -293,7 +293,7 @@ internal class GroupResourceIntTest( @Test @Throws(Exception::class) - fun deleteGroupWithSubjects() { + suspend fun deleteGroupWithSubjects() { // Initialize the database groupRepository.saveAndFlush(group) val projectDto = projectMapper.projectToProjectDTO(project) diff --git a/src/test/java/org/radarbase/management/web/rest/SubjectResourceIntTest.kt b/src/test/java/org/radarbase/management/web/rest/SubjectResourceIntTest.kt index de0fc3a96..a438e7bc7 100644 --- a/src/test/java/org/radarbase/management/web/rest/SubjectResourceIntTest.kt +++ b/src/test/java/org/radarbase/management/web/rest/SubjectResourceIntTest.kt @@ -103,7 +103,7 @@ internal class SubjectResourceIntTest( @Test @Transactional @Throws(Exception::class) - fun createSubjectWithExistingId() { + suspend fun createSubjectWithExistingId() { // Create a Subject val subjectDto = subjectService.createSubject(SubjectServiceTest.createEntityDTO()) val databaseSizeBeforeCreate = subjectRepository.findAll().size @@ -122,7 +122,7 @@ internal class SubjectResourceIntTest( @Throws(Exception::class) @Transactional @Test - fun allSubjects() { + suspend fun allSubjects() { // Initialize the database val subjectDto = subjectService.createSubject(SubjectServiceTest.createEntityDTO()) @@ -150,7 +150,7 @@ internal class SubjectResourceIntTest( @Throws(Exception::class) @Transactional @Test - fun subject() { + suspend fun subject() { // Initialize the database val subjectDto = subjectService.createSubject(SubjectServiceTest.createEntityDTO()) @@ -179,7 +179,7 @@ internal class SubjectResourceIntTest( @Test @Transactional @Throws(Exception::class) - fun updateSubject() { + suspend fun updateSubject() { // Initialize the database var subjectDto = subjectService.createSubject(SubjectServiceTest.createEntityDTO()) val databaseSizeBeforeUpdate = subjectRepository.findAll().size @@ -206,7 +206,7 @@ internal class SubjectResourceIntTest( @Test @Transactional @Throws(Exception::class) - fun updateSubjectWithNewProject() { + suspend fun updateSubjectWithNewProject() { // Initialize the database var subjectDto = subjectService.createSubject(SubjectServiceTest.createEntityDTO()) val databaseSizeBeforeUpdate = subjectRepository.findAll().size @@ -259,7 +259,7 @@ internal class SubjectResourceIntTest( @Test @Transactional @Throws(Exception::class) - fun deleteSubject() { + suspend fun deleteSubject() { // Initialize the database val subjectDto = subjectService.createSubject(SubjectServiceTest.createEntityDTO()) val databaseSizeBeforeDelete = subjectRepository.findAll().size @@ -401,7 +401,7 @@ internal class SubjectResourceIntTest( @Throws(Exception::class) @Transactional @Test - fun subjectSources() { + suspend fun subjectSources() { // Initialize the database val subjectDtoToCreate: SubjectDTO = SubjectServiceTest.createEntityDTO() val createdSource = sourceService.save(createSource()) @@ -427,7 +427,7 @@ internal class SubjectResourceIntTest( @Throws(Exception::class) @Transactional @Test - fun subjectSourcesWithQueryParam() { + suspend fun subjectSourcesWithQueryParam() { // Initialize the database val subjectDtoToCreate: SubjectDTO = SubjectServiceTest.createEntityDTO() val createdSource = sourceService.save(createSource()) @@ -453,7 +453,7 @@ internal class SubjectResourceIntTest( @Throws(Exception::class) @Transactional @Test - fun inactiveSubjectSourcesWithQueryParam() { + suspend fun inactiveSubjectSourcesWithQueryParam() { // Initialize the database val subjectDtoToCreate: SubjectDTO = SubjectServiceTest.createEntityDTO() val createdSource = sourceService.save(createSource()) From f5f9b3359511386169ac09fe9ad221caed2341d0 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 26 Nov 2024 00:16:09 +0000 Subject: [PATCH 61/68] Fix tests --- .../org/radarbase/management/web/rest/GroupResourceIntTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/radarbase/management/web/rest/GroupResourceIntTest.kt b/src/test/java/org/radarbase/management/web/rest/GroupResourceIntTest.kt index 56033c34a..ead5bcc7b 100644 --- a/src/test/java/org/radarbase/management/web/rest/GroupResourceIntTest.kt +++ b/src/test/java/org/radarbase/management/web/rest/GroupResourceIntTest.kt @@ -372,7 +372,7 @@ internal class GroupResourceIntTest( @Test @Throws(Exception::class) - fun addSubjectsToGroup() { + suspend fun addSubjectsToGroup() { // Initialize the database groupRepository.saveAndFlush(group) val projectDto = projectMapper.projectToProjectDTO(project) From 0ea522d50c6bd771c8206edbc1c45e2aa58334c3 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 26 Nov 2024 00:59:16 +0000 Subject: [PATCH 62/68] Wrap updating of identity in try catch block in subject service --- .../management/service/SubjectService.kt | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/SubjectService.kt b/src/main/java/org/radarbase/management/service/SubjectService.kt index 26f03f467..ff65fe616 100644 --- a/src/main/java/org/radarbase/management/service/SubjectService.kt +++ b/src/main/java/org/radarbase/management/service/SubjectService.kt @@ -113,14 +113,15 @@ class SubjectService( sourceRepository.saveAll(subject.sources) val savedSubject = subjectRepository.save(subject) - val subjectDto = subjectMapper.subjectToSubjectReducedProjectDTO(savedSubject) - - // Update identity server identity with roles - userService.getUserWithAuthoritiesByLogin(login = subjectDto?.login!!)?.let { user -> - identityService.updateAssociatedIdentity(user, subject) + return subjectMapper.subjectToSubjectReducedProjectDTO(savedSubject).also { + userService.getUserWithAuthoritiesByLogin(login = subjectDto.login!!)?.let { user -> + try { + identityService.updateAssociatedIdentity(user, savedSubject) + } catch (ex: Exception) { + log.error("Failed to update associated identity for user {}: {}", user.login, ex.message) + } + } } - - return subjectDto } suspend fun createSubject( @@ -377,12 +378,15 @@ class SubjectService( } subjectRepository.save(subject) - // Update identity server identity with roles - userService.getUserWithAuthoritiesByLogin(login = subject.user?.login!!)?.let { user -> - identityService.updateAssociatedIdentity(user, subject) + return sourceMapper.sourceToMinimalSourceDetailsDTO(assignedSource).also { + userService.getUserWithAuthoritiesByLogin(login = subject.user?.login!!)?.let { user -> + try { + identityService.updateAssociatedIdentity(user, subject) + } catch (ex: Exception) { + log.error("Failed to update associated identity for user {}: {}", user.login, ex.message) + } + } } - - return sourceMapper.sourceToMinimalSourceDetailsDTO(assignedSource) } /** From 1c27e75db2401d70d251938ba20103a25ff87535 Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 26 Nov 2024 01:19:12 +0000 Subject: [PATCH 63/68] Check IdentityService is enabled before calling methods --- .../management/service/IdentityService.kt | 1 + .../management/service/SubjectService.kt | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/IdentityService.kt b/src/main/java/org/radarbase/management/service/IdentityService.kt index 3bb76918a..cc524e659 100644 --- a/src/main/java/org/radarbase/management/service/IdentityService.kt +++ b/src/main/java/org/radarbase/management/service/IdentityService.kt @@ -53,6 +53,7 @@ class IdentityService private val adminUrl = managementPortalProperties.identityServer.adminUrl() private val publicUrl = managementPortalProperties.identityServer.publicUrl() + public val enabled = !adminUrl.isNullOrEmpty() && !publicUrl.isNullOrEmpty() init { log.debug("Kratos serverUrl set to $publicUrl") diff --git a/src/main/java/org/radarbase/management/service/SubjectService.kt b/src/main/java/org/radarbase/management/service/SubjectService.kt index ff65fe616..23731a51c 100644 --- a/src/main/java/org/radarbase/management/service/SubjectService.kt +++ b/src/main/java/org/radarbase/management/service/SubjectService.kt @@ -114,11 +114,13 @@ class SubjectService( val savedSubject = subjectRepository.save(subject) return subjectMapper.subjectToSubjectReducedProjectDTO(savedSubject).also { - userService.getUserWithAuthoritiesByLogin(login = subjectDto.login!!)?.let { user -> - try { - identityService.updateAssociatedIdentity(user, savedSubject) - } catch (ex: Exception) { - log.error("Failed to update associated identity for user {}: {}", user.login, ex.message) + if (identityService.enabled) { + userService.getUserWithAuthoritiesByLogin(login = subjectDto.login!!)?.let { user -> + try { + identityService.updateAssociatedIdentity(user, savedSubject) + } catch (ex: Exception) { + log.error("Failed to update associated identity for user {}: {}", user.login, ex.message) + } } } } @@ -380,10 +382,12 @@ class SubjectService( return sourceMapper.sourceToMinimalSourceDetailsDTO(assignedSource).also { userService.getUserWithAuthoritiesByLogin(login = subject.user?.login!!)?.let { user -> - try { - identityService.updateAssociatedIdentity(user, subject) - } catch (ex: Exception) { - log.error("Failed to update associated identity for user {}: {}", user.login, ex.message) + if (identityService.enabled) { + try { + identityService.updateAssociatedIdentity(user, subject) + } catch (ex: Exception) { + log.error("Failed to update associated identity for user {}: {}", user.login, ex.message) + } } } } From 3e83eb47beca1dd53df40e07063c88f269e4d3bd Mon Sep 17 00:00:00 2001 From: Pauline Date: Tue, 3 Dec 2024 18:14:45 +0000 Subject: [PATCH 64/68] Fix scopes in login endpoint --- .../java/org/radarbase/management/web/rest/LoginEndpoint.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt index 1e34d7b46..06aebd72a 100644 --- a/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/LoginEndpoint.kt @@ -48,7 +48,7 @@ constructor( "response_type=code&" + "state=${Instant.now()}&" + "audience=res_ManagementPortal&" + - "scope=SOURCEDATA.CREATE SOURCETYPE.UPDATE SOURCETYPE.DELETE AUTHORITY.UPDATE MEASUREMENT.DELETE PROJECT.READ AUDIT.CREATE USER.DELETE AUTHORITY.DELETE SUBJECT.DELETE MEASUREMENT.UPDATE SOURCEDATA.UPDATE SUBJECT.READ USER.UPDATE SOURCETYPE.CREATE AUTHORITY.READ USER.CREATE SOURCE.CREATE SOURCE.READ SUBJECT.CREATE ROLE.UPDATE ROLE.READ MEASUREMENT.READ PROJECT.UPDATE PROJECT.DELETE ROLE.DELETE SOURCE.DELETE SOURCETYPE.READ ROLE.CREATE SOURCEDATA.DELETE SUBJECT.UPDATE SOURCE.UPDATE PROJECT.CREATE AUDIT.READ MEASUREMENT.CREATE AUDIT.DELETE AUDIT.UPDATE AUTHORITY.CREATE USER.READ SOURCEDATA.READ&" + + "scope=SOURCEDATA.CREATE SOURCETYPE.UPDATE SOURCETYPE.DELETE AUTHORITY.UPDATE MEASUREMENT.DELETE PROJECT.READ AUDIT.CREATE USER.DELETE AUTHORITY.DELETE SUBJECT.DELETE MEASUREMENT.UPDATE SOURCEDATA.UPDATE SUBJECT.READ USER.UPDATE SOURCETYPE.CREATE AUTHORITY.READ USER.CREATE SOURCE.CREATE SOURCE.READ SUBJECT.CREATE ROLE.UPDATE ROLE.READ MEASUREMENT.READ PROJECT.UPDATE PROJECT.DELETE ROLE.DELETE SOURCE.DELETE SOURCETYPE.READ ROLE.CREATE SOURCEDATA.DELETE SUBJECT.UPDATE SOURCE.UPDATE PROJECT.CREATE AUDIT.READ MEASUREMENT.CREATE AUDIT.DELETE AUDIT.UPDATE AUTHORITY.CREATE USER.READ ORGANIZATION.READ ORGANIZATION.CREATE ORGANIZATION.UPDATE SOURCEDATA.READ&" + "redirect_uri=${managementPortalProperties.common.managementPortalBaseUrl}/api/redirect/login" } } From d6239391f878e7fbe9ac5ceb20c27a0775c0b79b Mon Sep 17 00:00:00 2001 From: Pauline Date: Thu, 12 Dec 2024 17:08:04 +0000 Subject: [PATCH 65/68] Update IdentityService to patch existing identity instead of replacing --- .../radarbase/auth/kratos/KratosSessionDTO.kt | 13 +++++++++--- .../management/service/IdentityService.kt | 20 +++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt index c03751113..d11a1855d 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encoding.Decoder import org.radarbase.auth.authorization.AuthorityReference +import kotlinx.serialization.json.JsonElement import org.radarbase.auth.authorization.RoleAuthority import org.radarbase.auth.token.DataRadarToken import org.radarbase.auth.token.RadarToken @@ -76,7 +77,7 @@ class KratosSessionDTO( add(AuthorityReference(authority)) } } - } + } metadata_public?.roles?.takeIf { it.isNotEmpty() }?.let { roles -> for (roleValue in roles) { val role = RoleAuthority.valueOfAuthorityOrNull(roleValue) @@ -101,8 +102,6 @@ class KratosSessionDTO( val id: String? = null, val userId: String? = null, val name: String? = null, - val eligibility: Map? = null, - val consent: Map? = null, ) @Serializable @@ -115,6 +114,14 @@ class KratosSessionDTO( val mp_login: String? = null, ) + @Serializable + data class JsonMetadataPatchOperation( + val op: String, + val path: String, + val value: Metadata + ) + + fun toDataRadarToken() : DataRadarToken { return DataRadarToken( roles = this.identity.parseRoles(), diff --git a/src/main/java/org/radarbase/management/service/IdentityService.kt b/src/main/java/org/radarbase/management/service/IdentityService.kt index cc524e659..073e61fe9 100644 --- a/src/main/java/org/radarbase/management/service/IdentityService.kt +++ b/src/main/java/org/radarbase/management/service/IdentityService.kt @@ -10,11 +10,13 @@ import io.ktor.http.* import io.ktor.serialization.kotlinx.json.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.serialization.json.Json +import kotlinx.serialization.* +import kotlinx.serialization.json.* import org.radarbase.auth.authorization.Permission import org.radarbase.auth.authorization.RoleAuthority import org.radarbase.auth.exception.IdpException import org.radarbase.auth.kratos.KratosSessionDTO +import org.radarbase.auth.kratos.KratosSessionDTO.JsonMetadataPatchOperation import org.radarbase.management.config.ManagementPortalProperties import org.radarbase.management.domain.Role import org.radarbase.management.domain.Subject @@ -24,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.Duration +import kotlinx.serialization.Serializable /** Service class for managing identities. */ @Service @@ -141,19 +144,24 @@ class IdentityService subject: Subject? = null, ): KratosSessionDTO.Identity = withContext(Dispatchers.IO) { + val json = Json { ignoreUnknownKeys = true } val identityId = user.identity ?: subject?.externalId ?: throw IdpException("User has no identity") - - val identity = getExistingIdentity(identityId) val sources = subject?.sources?.map { it.sourceId.toString() } ?: emptyList() - identity.metadata_public = getIdentityMetadataWithRoles(user, sources) + val jsonPatchPayload = listOf( + JsonMetadataPatchOperation( + op = "replace", + path = "/metadata_public", + value = getIdentityMetadataWithRoles(user, sources) + ) + ) val response = - httpClient.put { + httpClient.patch { url("$adminUrl/admin/identities/$identityId") contentType(ContentType.Application.Json) accept(ContentType.Application.Json) - setBody(identity) + setBody(json.encodeToString(jsonPatchPayload)) } if (response.status.isSuccess()) { From a80f6624075a4358d04022a7f887f017db594f6e Mon Sep 17 00:00:00 2001 From: Pauline Date: Sun, 12 Jan 2025 22:24:29 +0000 Subject: [PATCH 66/68] Remove unnecessary check if IdentityService is enabled --- .../java/org/radarbase/management/service/IdentityService.kt | 1 - .../java/org/radarbase/management/service/SubjectService.kt | 4 ---- 2 files changed, 5 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/IdentityService.kt b/src/main/java/org/radarbase/management/service/IdentityService.kt index 073e61fe9..ca16bd96b 100644 --- a/src/main/java/org/radarbase/management/service/IdentityService.kt +++ b/src/main/java/org/radarbase/management/service/IdentityService.kt @@ -56,7 +56,6 @@ class IdentityService private val adminUrl = managementPortalProperties.identityServer.adminUrl() private val publicUrl = managementPortalProperties.identityServer.publicUrl() - public val enabled = !adminUrl.isNullOrEmpty() && !publicUrl.isNullOrEmpty() init { log.debug("Kratos serverUrl set to $publicUrl") diff --git a/src/main/java/org/radarbase/management/service/SubjectService.kt b/src/main/java/org/radarbase/management/service/SubjectService.kt index 23731a51c..1f8119764 100644 --- a/src/main/java/org/radarbase/management/service/SubjectService.kt +++ b/src/main/java/org/radarbase/management/service/SubjectService.kt @@ -114,7 +114,6 @@ class SubjectService( val savedSubject = subjectRepository.save(subject) return subjectMapper.subjectToSubjectReducedProjectDTO(savedSubject).also { - if (identityService.enabled) { userService.getUserWithAuthoritiesByLogin(login = subjectDto.login!!)?.let { user -> try { identityService.updateAssociatedIdentity(user, savedSubject) @@ -122,7 +121,6 @@ class SubjectService( log.error("Failed to update associated identity for user {}: {}", user.login, ex.message) } } - } } } @@ -382,13 +380,11 @@ class SubjectService( return sourceMapper.sourceToMinimalSourceDetailsDTO(assignedSource).also { userService.getUserWithAuthoritiesByLogin(login = subject.user?.login!!)?.let { user -> - if (identityService.enabled) { try { identityService.updateAssociatedIdentity(user, subject) } catch (ex: Exception) { log.error("Failed to update associated identity for user {}: {}", user.login, ex.message) } - } } } } From c0b5c013337d4be34ec377807ba9a66a3593ff70 Mon Sep 17 00:00:00 2001 From: Pauline Date: Sun, 12 Jan 2025 22:25:28 +0000 Subject: [PATCH 67/68] Add check for kratos identity before creating subject through webhook --- .../radarbase/management/service/dto/UserDTO.kt | 4 ++++ .../management/web/rest/KratosEndpoint.kt | 16 +++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/radarbase/management/service/dto/UserDTO.kt b/src/main/java/org/radarbase/management/service/dto/UserDTO.kt index a69e0c533..6ce15db93 100644 --- a/src/main/java/org/radarbase/management/service/dto/UserDTO.kt +++ b/src/main/java/org/radarbase/management/service/dto/UserDTO.kt @@ -24,6 +24,10 @@ open class UserDTO { var authorities: Set? = null var accessToken: String? = null + /** Identifier for association with the identity service provider. + * Null if not linked to an external identity. */ + var identity: String? = null + override fun toString(): String { return ("UserDTO{" + "login='" + login + '\'' diff --git a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt index a76711c13..08ee2521e 100644 --- a/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt +++ b/src/main/java/org/radarbase/management/web/rest/KratosEndpoint.kt @@ -25,6 +25,7 @@ constructor( @Autowired private val subjectService: SubjectService, @Autowired private val subjectRepository: SubjectRepository, @Autowired private val projectService: ProjectService, + @Autowired private val identityService: IdentityService, @Autowired private val managementPortalProperties: ManagementPortalProperties, ) { private var sessionService: SessionService = @@ -47,13 +48,18 @@ constructor( logger.debug("REST request to create subject : $webhookDTO") val kratosIdentity = webhookDTO.identity ?: throw IllegalArgumentException("Identity is required") + val id = kratosIdentity.id ?: throw IllegalArgumentException("Identity ID is required") + + // Verify kratos identity exists + val existingIdentity = identityService.getExistingIdentity(id) + if (!existingIdentity.equals(kratosIdentity)) + throw IllegalArgumentException("Kratos identity does not match") if (!kratosIdentity.schema_id.equals(KRATOS_SUBJECT_SCHEMA)) throw IllegalArgumentException("Cannot create non-subject users") - val id = kratosIdentity.id ?: throw IllegalArgumentException("Identity ID is required") val project = - kratosIdentity.traits!!.projects?.firstOrNull() + kratosIdentity.traits?.projects?.firstOrNull() ?: throw NotAuthorizedException("Cannot create subject without project") val projectUserId = project.userId ?: throw IllegalArgumentException("Project user ID is required") val projectDto = @@ -84,8 +90,8 @@ constructor( ?: throw IllegalArgumentException("Session token is required") val kratosIdentity = sessionService.getSession(token).identity val project = - kratosIdentity.traits!!.projects?.firstOrNull() - ?: throw NotAuthorizedException("Cannot create subject without project") + kratosIdentity.traits?.projects?.firstOrNull() + ?: throw NotAuthorizedException("Cannot create subject without project") val projectUserId = project.userId ?: throw IllegalArgumentException("Project user ID is required") if (!hasPermission(kratosIdentity, id)) { @@ -106,4 +112,4 @@ constructor( private val logger = LoggerFactory.getLogger(KratosEndpoint::class.java) private val KRATOS_SUBJECT_SCHEMA = "subject" } -} \ No newline at end of file +} From baa9297673c7edb4fdae4882dd624ed797feb35b Mon Sep 17 00:00:00 2001 From: Pauline Date: Sun, 12 Jan 2025 22:35:10 +0000 Subject: [PATCH 68/68] Revert unnecessary changes --- .../org/radarbase/auth/kratos/KratosSessionDTO.kt | 2 -- .../radarbase/management/service/AuthService.kt | 4 ++-- .../user-management-dialog.component.html | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt index d11a1855d..f81f711e9 100644 --- a/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt +++ b/radar-auth/src/main/java/org/radarbase/auth/kratos/KratosSessionDTO.kt @@ -6,7 +6,6 @@ import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.encoding.Decoder import org.radarbase.auth.authorization.AuthorityReference -import kotlinx.serialization.json.JsonElement import org.radarbase.auth.authorization.RoleAuthority import org.radarbase.auth.token.DataRadarToken import org.radarbase.auth.token.RadarToken @@ -121,7 +120,6 @@ class KratosSessionDTO( val value: Metadata ) - fun toDataRadarToken() : DataRadarToken { return DataRadarToken( roles = this.identity.parseRoles(), diff --git a/src/main/java/org/radarbase/management/service/AuthService.kt b/src/main/java/org/radarbase/management/service/AuthService.kt index 6fe1adddd..b44f9343c 100644 --- a/src/main/java/org/radarbase/management/service/AuthService.kt +++ b/src/main/java/org/radarbase/management/service/AuthService.kt @@ -33,8 +33,8 @@ class AuthService( private val httpClient = HttpClient(CIO) { install(HttpTimeout) { - connectTimeoutMillis = Duration.ofSeconds(20).toMillis() - socketTimeoutMillis = Duration.ofSeconds(20).toMillis() + connectTimeoutMillis = Duration.ofSeconds(10).toMillis() + socketTimeoutMillis = Duration.ofSeconds(10).toMillis() requestTimeoutMillis = Duration.ofSeconds(300).toMillis() } install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true }) } diff --git a/src/main/webapp/app/admin/user-management/user-management-dialog.component.html b/src/main/webapp/app/admin/user-management/user-management-dialog.component.html index 6cc941d9e..2f280b0d0 100644 --- a/src/main/webapp/app/admin/user-management/user-management-dialog.component.html +++ b/src/main/webapp/app/admin/user-management/user-management-dialog.component.html @@ -34,6 +34,20 @@
+
+ + + +
+ + +
+
+