Skip to content

Commit

Permalink
kie-issues#262: Spring-Boot 3.0.5 migration: Fix SVG Addon
Browse files Browse the repository at this point in the history
* `keycloak-spring-boot-starter` removal in favour of springboot ouath2
* IT test fixes
  • Loading branch information
pefernan committed Nov 30, 2023
1 parent 9a5216f commit db3f97f
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 55 deletions.
16 changes: 2 additions & 14 deletions springboot/addons/process-svg/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,6 @@
<name>Kogito :: Add-Ons :: Process SVG :: Spring Boot Addon</name>
<description>Springboot Addon to interact with Process SVG Service</description>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>${version.org.keycloak}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.kie.kogito</groupId>
Expand All @@ -62,8 +50,8 @@
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.kie.kogito.svg.auth;

public interface PrincipalAuthTokenReader<T> {

boolean acceptsPrincipal(Object principal);

String readToken(T principal);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.kie.kogito.svg.auth;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

@Service
@ConditionalOnClass({ SecurityContextHolder.class })
public class SpringBootAuthHelper {

private List<PrincipalAuthTokenReader> authTokenReaders;

public SpringBootAuthHelper(@Autowired List<PrincipalAuthTokenReader> authTokenReaders) {
this.authTokenReaders = authTokenReaders;
}

public Optional<String> getAuthToken() {
return Optional.ofNullable(getToken());
}

private String getToken() {
SecurityContext securityContext = SecurityContextHolder.getContext();

if (securityContext == null || securityContext.getAuthentication() == null) {
return null;
}

Object principal = securityContext.getAuthentication().getPrincipal();

return this.authTokenReaders.stream().filter(reader -> reader.acceptsPrincipal(principal)).findFirst()
.map(reader -> "Bearer " + reader.readToken(principal)).orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.kie.kogito.svg.auth.impl;

import org.kie.kogito.svg.auth.PrincipalAuthTokenReader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnClass(Jwt.class)
public class JwtPrincipalAuthTokenReader implements PrincipalAuthTokenReader<Jwt> {

@Override
public boolean acceptsPrincipal(Object principal) {
return principal instanceof Jwt;
}

@Override
public String readToken(Jwt principal) {
return principal.getTokenValue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.kie.kogito.svg.auth.impl;

import org.kie.kogito.svg.auth.PrincipalAuthTokenReader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnClass({ OidcUser.class })
public class OIDCPrincipalAuthTokenReader implements PrincipalAuthTokenReader<OidcUser> {

@Override
public boolean acceptsPrincipal(Object principal) {
return principal instanceof OidcUser;
}

@Override
public String readToken(OidcUser principal) {
return principal.getIdToken().getTokenValue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import jakarta.annotation.PostConstruct;
import org.keycloak.KeycloakPrincipal;
import org.kie.kogito.svg.ProcessSVGException;
import org.kie.kogito.svg.auth.SpringBootAuthHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -32,15 +32,15 @@
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.annotation.PostConstruct;

import static java.util.Collections.emptyList;
import static java.util.Collections.singletonMap;

Expand All @@ -53,16 +53,18 @@ public class SpringBootDataIndexClient implements DataIndexClient {
private RestTemplate restTemplate;
private ObjectMapper objectMapper;

private boolean isKeycloakAdapterAvailable = false;
private Optional<SpringBootAuthHelper> authHelper;

@Autowired
public SpringBootDataIndexClient(
@Value("${kogito.dataindex.http.url:http://localhost:8180}") String dataIndexHttpURL,
@Autowired(required = false) RestTemplate restTemplate,
@Autowired ObjectMapper objectMapper) {
@Autowired ObjectMapper objectMapper,
@Autowired Optional<SpringBootAuthHelper> authHelper) {
this.dataIndexHttpURL = dataIndexHttpURL;
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
this.authHelper = authHelper;
}

@PostConstruct
Expand All @@ -71,21 +73,6 @@ public void initialize() {
restTemplate = new RestTemplate();
LOGGER.debug("No RestTemplate found, creating a default one");
}
try {
Class.forName("org.springframework.security.core.context.SecurityContextHolder");
Class.forName("org.keycloak.KeycloakPrincipal");
setKeycloakAdapterAvailable(true);
} catch (ClassNotFoundException exception) {
LOGGER.debug("No Keycloak Adapter available, continue just propagating received authorization header");
}
}

public boolean isKeycloakAdapterAvailable() {
return isKeycloakAdapterAvailable;
}

public void setKeycloakAdapterAvailable(boolean keycloakAdapterAvailable) {
isKeycloakAdapterAvailable = keycloakAdapterAvailable;
}

@Override
Expand Down Expand Up @@ -120,13 +107,8 @@ protected List<NodeInstance> getNodeInstancesFromResponse(JsonNode response) {
}

protected String getAuthHeader(String authHeader) {
if (isKeycloakAdapterAvailable()) {
SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext != null &&
securityContext.getAuthentication() != null &&
securityContext.getAuthentication().getPrincipal() instanceof KeycloakPrincipal) {
return "Bearer " + ((KeycloakPrincipal) securityContext.getAuthentication().getPrincipal()).getKeycloakSecurityContext().getTokenString();
}
if (authHelper.isPresent()) {
return authHelper.get().getAuthToken().orElse(authHeader);
}
return authHeader;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,25 @@
package org.kie.kogito.svg.dataindex;

import java.util.List;
import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.kie.kogito.svg.ProcessSVGException;
import org.kie.kogito.svg.auth.SpringBootAuthHelper;
import org.kie.kogito.svg.auth.impl.JwtPrincipalAuthTokenReader;
import org.kie.kogito.svg.auth.impl.OIDCPrincipalAuthTokenReader;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

Expand Down Expand Up @@ -80,7 +85,11 @@ public class SpringBootDataIndexClientTest {

@BeforeEach
void setUp() {
client = new SpringBootDataIndexClient("data-indexURL", restTemplate, objectMapper);
client = buildClient(Optional.empty());
}

private SpringBootDataIndexClient buildClient(Optional<SpringBootAuthHelper> authHelper) {
return new SpringBootDataIndexClient("data-indexURL", restTemplate, objectMapper, authHelper);
}

@Test
Expand Down Expand Up @@ -117,27 +126,43 @@ public void testGetNodeInstancesFromProcessInstance() {
}

@Test
public void testAuthHeaderWithSecurityContext() {
public void testAuthHeaderWithSecurityContextOidcUserPrincipal() {
String token = "testToken";
SecurityContext securityContextMock = mock(SecurityContext.class);
Authentication authenticationMock = mock(Authentication.class);
OidcUser principalMock = mock(OidcUser.class);
OidcIdToken tokenMock = mock(OidcIdToken.class);

when(securityContextMock.getAuthentication()).thenReturn(authenticationMock);
when(authenticationMock.getPrincipal()).thenReturn(principalMock);
when(principalMock.getIdToken()).thenReturn(tokenMock);
when(tokenMock.getTokenValue()).thenReturn(token);

SecurityContextHolder.setContext(securityContextMock);
client = buildClient(Optional.of(new SpringBootAuthHelper(List.of(new OIDCPrincipalAuthTokenReader(), new JwtPrincipalAuthTokenReader()))));
assertThat(client.getAuthHeader("")).isEqualTo("Bearer " + token);
}

@Test
public void testAuthHeaderWithSecurityContextJwtPrincipal() {
String token = "testToken";
SecurityContext securityContextMock = mock(SecurityContext.class);
Authentication authenticationMock = mock(Authentication.class);
KeycloakPrincipal principalMock = mock(KeycloakPrincipal.class);
KeycloakSecurityContext keycloakSecurityContextMock = mock(KeycloakSecurityContext.class);
Jwt principalMock = mock(Jwt.class);

when(securityContextMock.getAuthentication()).thenReturn(authenticationMock);
when(authenticationMock.getPrincipal()).thenReturn(principalMock);
when(principalMock.getKeycloakSecurityContext()).thenReturn(keycloakSecurityContextMock);
when(keycloakSecurityContextMock.getTokenString()).thenReturn(token);
when(principalMock.getTokenValue()).thenReturn(token);

SecurityContextHolder.setContext(securityContextMock);
client.setKeycloakAdapterAvailable(true);
client = buildClient(Optional.of(new SpringBootAuthHelper(List.of(new OIDCPrincipalAuthTokenReader(), new JwtPrincipalAuthTokenReader()))));
assertThat(client.getAuthHeader("")).isEqualTo("Bearer " + token);
}

@Test
public void testAuthHeaderWithoutKeycloakSecurityContext() {
public void testAuthHeaderWithoutSecurityContext() {
String authHeader = "Bearer testToken";
client.setKeycloakAdapterAvailable(false);
client = buildClient(Optional.of(new SpringBootAuthHelper(List.of(new OIDCPrincipalAuthTokenReader(), new JwtPrincipalAuthTokenReader()))));
assertThat(client.getAuthHeader(authHeader)).isEqualTo(authHeader);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ void testSignalStartProcess() {
given()
.contentType(ContentType.JSON)
.when()
.get("/signalStart/")
.get("/signalStart")
.then()
.statusCode(200)
.body("$.size()", is(1))
.body("size()", is(1))
.body("[0].message", equalTo("hello world"));
}
@Test
Expand Down

0 comments on commit db3f97f

Please sign in to comment.