diff --git a/calls.http b/calls.http index 200ae773..75c442f7 100644 --- a/calls.http +++ b/calls.http @@ -10,6 +10,7 @@ Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &client_id=odh-mobility-datacollector-development &client_secret=7bd46f8f-c296-416d-a13d-dc81e68d0830 +&scope=openid ### Get access token for the writer (TEST DB) # @name login @@ -33,7 +34,7 @@ grant_type=password #@host=https://mobility.share.opendatahub.com @host=http://localhost:8999 -@host=https://mobility.share.opendatahub.testingmachine.eu +#@host=https://mobility.share.opendatahub.testingmachine.eu @authtoken = {{login.response.body.access_token}} diff --git a/writer/pom.xml b/writer/pom.xml index 50ddc464..424dd9fe 100644 --- a/writer/pom.xml +++ b/writer/pom.xml @@ -46,6 +46,10 @@ SPDX-License-Identifier: CC0-1.0 org.springframework.boot spring-boot-starter-security + + org.springframework.security + spring-security-config + org.springframework.boot spring-boot-starter-oauth2-client @@ -68,10 +72,6 @@ SPDX-License-Identifier: CC0-1.0 spring-boot-starter-test test - - org.springframework.security - spring-security-config - @@ -111,7 +111,6 @@ SPDX-License-Identifier: CC0-1.0 runtime - diff --git a/writer/src/main/java/it/bz/idm/bdp/writer/config/WebSecurity.java b/writer/src/main/java/it/bz/idm/bdp/writer/config/WebSecurity.java index 6e535eb1..151fb2ab 100644 --- a/writer/src/main/java/it/bz/idm/bdp/writer/config/WebSecurity.java +++ b/writer/src/main/java/it/bz/idm/bdp/writer/config/WebSecurity.java @@ -4,13 +4,22 @@ package it.bz.idm.bdp.writer.config; +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Collectors; + import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; 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.EnableWebSecurity; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; @@ -33,17 +42,48 @@ public AuthenticationManager authenticationManager(HttpSecurity http) throws Exc return http.getSharedObject(AuthenticationManagerBuilder.class).build(); } + // For some reason, spring does not read the role claim from the jwt. + // Since we're basing our authorization on roles, we have to extend the spring security jwt converter to get that functionality. + // see https://stackoverflow.com/questions/65518172/spring-security-cant-extract-roles-from-jwt for reference + // + // Note that this is pretty specific to our use case and only maps roles. If we ever need scope or other claims, + // implement that here (and make it a separate class implementing the Converter interface) + @SuppressWarnings("unchecked") + private Converter jwtConverter(){ + JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter(); + jwtConverter.setJwtGrantedAuthoritiesConverter(jwt -> { + // see org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter.java + // This lambda functions as a replacement / reimplementation for that class + Collection roles = new ArrayList<>(); + Object roleClaim = jwt.getClaim("roles"); + if (roleClaim instanceof Collection){ + roles.addAll((Collection) roleClaim); + } + + return roles + .stream() + .map(role -> new SimpleGrantedAuthority(role)) + .collect(Collectors.toList()); + }); + return jwtConverter; + } + @Bean public SecurityFilterChain oauthFilter(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth + // health check always accessible .requestMatchers("/actuator/**") .permitAll() + // Authorize based on role claim ROLE_ADMIN .requestMatchers("/json/**") .hasRole("ADMIN") - .anyRequest().permitAll()); - http.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); + // permitAll is ported over from legacy code, not sure if it doesn't make more sense to deny all other requests + .anyRequest().permitAll()) + .oauth2Client(Customizer.withDefaults()); + // Register the oauth server, and our custom jwt converter + http.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter()))); return http.build(); } } diff --git a/writer/src/main/resources/META-INF/persistence.xml b/writer/src/main/resources/META-INF/persistence.xml index e4988316..3e9ede6b 100644 --- a/writer/src/main/resources/META-INF/persistence.xml +++ b/writer/src/main/resources/META-INF/persistence.xml @@ -13,7 +13,7 @@ SPDX-License-Identifier: CC0-1.0 - + diff --git a/writer/src/main/resources/application.properties b/writer/src/main/resources/application.properties index 4b1e3f62..8a06d34f 100644 --- a/writer/src/main/resources/application.properties +++ b/writer/src/main/resources/application.properties @@ -13,19 +13,11 @@ management.endpoint.health.enabled=true ### Security security.cors.allowedOrigins=${SECURITY_ALLOWED_ORIGINS:*} -#keycloak.auth-server-url=${KEYCLOAK_URL:https://auth.opendatahub.testingmachine.eu/auth} -#keycloak.ssl-required=${KEYCLOAK_SSL_REQUIRED:none} -#keycloak.realm=${KEYCLOAK_REALM:noi} -#keycloak.resource=${KEYCLOAK_CLIENT_ID:odh-mobility-writer-development} -#keycloak.use-resource-role-mappings=true -#keycloak.public-client=false -#keycloak.bearer-only=true -#keycloak.securityConstraints[0].authRoles[0]=ROLE_ADMIN -#keycloak.securityConstraints[0].securityCollections[0].patterns[0]=/json/* - spring.security.oauth2.client.provider.keycloak.issuer-uri=${KEYCLOAK_URL:https://auth.opendatahub.testingmachine.eu/auth}/realms/${KEYCLOAK_REALM:noi} -spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username +#spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username spring.security.oauth2.resourceserver.jwt.issuer-uri=${KEYCLOAK_URL:https://auth.opendatahub.testingmachine.eu/auth}/realms/${KEYCLOAK_REALM:noi} + +spring.security.oauth2.client.registration.keycloak.provider=keycloak spring.security.oauth2.client.registration.keycloak.client-id=${KEYCLOAK_CLIENT_ID:odh-mobility-writer-development} spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.keycloak.scope=openid @@ -45,6 +37,7 @@ hibernate.hikari.maximumPoolSize=${HIBERNATE_MAX_POOL_SIZE:2} spring.datasource.url=jdbc:postgresql://${POSTGRES_SERVER:localhost}:${POSTGRES_PORT:5555}/${POSTGRES_DB:bdp}?currentSchema=${POSTGRES_SCHEMA:intimev2},public spring.datasource.username=${POSTGRES_USERNAME:bdp} spring.datasource.password=${POSTGRES_PASSWORD:password} +spring.datasource.hikari.maximum-pool-size=${HIBERNATE_MAX_POOL_SIZE:2} spring.jpa.hibernate.ddl-auto=none spring.jpa.hibernate.naming.implicit-strategy=it.bz.idm.bdp.dal.util.SchemaGeneratorImplicitNamingStrategy spring.jpa.properties.hibernate.format_sql=true diff --git a/writer/src/test/resources/META-INF/persistence.xml b/writer/src/test/resources/META-INF/persistence.xml index 74b0f928..02941007 100644 --- a/writer/src/test/resources/META-INF/persistence.xml +++ b/writer/src/test/resources/META-INF/persistence.xml @@ -27,7 +27,7 @@ SPDX-License-Identifier: CC0-1.0 it.bz.idm.bdp.dal.MeasurementStringHistory - +