Skip to content

Commit

Permalink
Added Keycloak integration test
Browse files Browse the repository at this point in the history
Adds Keycloak integration test to DataflowOAuthIT
Adds Authorities mapping test similar to keycloak role usage.
Added scripts to src/local for testing keycloak locally with preconfigured roles / group and user.
  • Loading branch information
corneil committed Dec 6, 2024
1 parent a576e5c commit bc49898
Show file tree
Hide file tree
Showing 36 changed files with 4,940 additions and 103 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci-it-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ jobs:
- name: Run Security IT
shell: bash
run: |
./mvnw clean install -DskipTests -T 1C -s .settings.xml -pl spring-cloud-dataflow-server -am -B --no-transfer-progress
./mvnw -s .settings.xml \
-pl spring-cloud-dataflow-server \
-Dgroups=oauth \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,33 @@ public void testMapConstructorWithIncompleteRoleMappings() throws Exception {
}

@Test
public void testThat3MappedAuthoritiesAreReturned() throws Exception {
Map<String, String> roleMappings = new HashMap<>();
roleMappings.put("ROLE_MANAGE", "dataflow_manage");
roleMappings.put("ROLE_VIEW", "dataflow_view");
roleMappings.put("ROLE_CREATE", "dataflow_create");
roleMappings.put("ROLE_MODIFY", "dataflow_modify");
roleMappings.put("ROLE_DEPLOY", "dataflow_deploy");
roleMappings.put("ROLE_DESTROY", "dataflow_destroy");
roleMappings.put("ROLE_SCHEDULE", "dataflow_schedule");

ProviderRoleMapping providerRoleMapping = new ProviderRoleMapping();
providerRoleMapping.setMapOauthScopes(true);
providerRoleMapping.getRoleMappings().putAll(roleMappings);

Set<String> roles = new HashSet<>();
roles.add("dataflow_manage");
roles.add("dataflow_view");
roles.add("dataflow_deploy");

DefaultAuthoritiesMapper defaultAuthoritiesMapper = new DefaultAuthoritiesMapper("uaa", providerRoleMapping);
Collection<? extends GrantedAuthority> authorities = defaultAuthoritiesMapper.mapScopesToAuthorities("uaa",
roles, null);

assertThat(authorities).hasSize(3);
assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
.containsExactlyInAnyOrder("ROLE_DEPLOY", "ROLE_MANAGE", "ROLE_VIEW");
}
public void testThat7MappedAuthoritiesAreReturned() throws Exception {
Map<String, String> roleMappings = new HashMap<>();
roleMappings.put("ROLE_MANAGE", "foo-manage");
Expand Down
11 changes: 11 additions & 0 deletions spring-cloud-dataflow-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
Expand Down Expand Up @@ -104,6 +109,12 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.dasniko</groupId>
<artifactId>testcontainers-keycloak</artifactId>
<version>3.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ spring:
url: "{repository}/org/springframework/cloud/spring-cloud-dataflow-shell/{version}/spring-cloud-dataflow-shell-{version}.jar"
checksum-sha1-url: "{repository}/org/springframework/cloud/spring-cloud-dataflow-shell/{version}/spring-cloud-dataflow-shell-{version}.jar.sha1"
checksum-sha256-url: "{repository}/org/springframework/cloud/spring-cloud-dataflow-shell/{version}/spring-cloud-dataflow-shell-{version}.jar.sha256"

jpa:
hibernate:
ddl-auto: none
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -91,8 +90,10 @@ protected static class EmptyConfig {
ClusterContainer.from(TagNames.DB2_11_5_8_0, "icr.io/db2_community/db2:11.5.8.0", TagNames.DB2)
);

public final static List<ClusterContainer> OAUTH_CONTAINERS = Collections.singletonList(
ClusterContainer.from(TagNames.UAA_4_32, "springcloud/scdf-uaa-test:4.32", TagNames.UAA)
public final static List<ClusterContainer> OAUTH_CONTAINERS = Arrays.asList(
ClusterContainer.from(TagNames.UAA_4_32, "springcloud/scdf-uaa-test:4.32", TagNames.UAA),
ClusterContainer.from(TagNames.KEYCLOAK_25, "quay.io/keycloak/keycloak:25.0", TagNames.KEYCLOAK),
ClusterContainer.from(TagNames.KEYCLOAK_26, "quay.io/keycloak/keycloak:26.0", TagNames.KEYCLOAK)
);

@Autowired
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ class DataflowOAuthIT extends AbstractDataflowTests {
private final Logger log = LoggerFactory.getLogger(DataflowOAuthIT.class);

@Test
void securedSetup() throws Exception {
log.info("Running testSecuredSetup()");
void securedSetupUAA() throws Exception {
log.info("Running securedSetupUAA()");
this.dataflowCluster.startIdentityProvider(TagNames.UAA_4_32);
this.dataflowCluster.startSkipper(TagNames.SKIPPER_main);
this.dataflowCluster.startDataflow(TagNames.DATAFLOW_main);
Expand Down Expand Up @@ -92,4 +92,60 @@ void securedSetup() throws Exception {
}
}
}

@Test
void securedSetupKeycloak() throws Exception {
log.info("Running securedSetupKeycloak()");
this.dataflowCluster.startIdentityProvider(TagNames.KEYCLOAK_26);
this.dataflowCluster.startSkipper(TagNames.SKIPPER_main);
this.dataflowCluster.startDataflow(TagNames.DATAFLOW_main);

// we can't do oauth flow from host due to how oauth works as we
// need proper networking, so use separate tools container to run
// curl command as we support basic auth and if we get good response
// oauth is working with dataflow and skipper.

AtomicReference<String> stderr = new AtomicReference<>();
try {
with().pollInterval(5, TimeUnit.SECONDS)
.and()
.await()
.ignoreExceptions()
.atMost(90, TimeUnit.SECONDS)
.untilAsserted(() -> {
log.info("Checking auth using curl");
ExecResult cmdResult = execInToolsContainer("curl", "-v", "-u", "joe:password", "http://dataflow:9393/about");
String response = cmdResult.getStdout();
if (StringUtils.hasText(response)) {
log.info("Response is {}", response);
}
if(StringUtils.hasText(cmdResult.getStderr())) {
log.error(cmdResult.getStderr());
}
stderr.set(cmdResult.getStderr());
assertThat(response).contains("\"authenticated\":true");
assertThat(response).contains("\"username\":\"joe\"");
stderr.set("");
});
log.info("Checking without credentials using curl");
ExecResult cmdResult = execInToolsContainer("curl", "-v", "-f", "http://dataflow:9393/about");
String response = cmdResult.getStdout();
if (StringUtils.hasText(response)) {
log.info("Response is {}", response);
}
response = cmdResult.getStderr();
if(StringUtils.hasText(response)) {
log.warn("Error is {}", response);
}
stderr.set(cmdResult.getStderr());
assertThat(cmdResult.getExitCode()).isNotZero();
stderr.set("");
}
finally {
String msg = stderr.get();
if (StringUtils.hasText(msg)) {
log.error("curl error: {}", msg);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ public abstract class TagNames {
public static final String OAUTH = "oauth";
public static final String PERFORMANCE = "performance";
public static final String UAA = "uaa";
public static final String KEYCLOAK = "keycloak";
public static final String KEYCLOAK_25 = "keycloak_25";
public static final String KEYCLOAK_26 = "keycloak_26";

public static final String UAA_4_32 = "uaa_4_32";

public static final String SKIPPER = "skipper";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.springframework.cloud.dataflow.unit.test;

import java.util.concurrent.atomic.AtomicBoolean;

import dasniko.testcontainers.keycloak.KeycloakContainer;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.dataflow.rest.client.DataFlowOperations;
import org.springframework.cloud.dataflow.rest.client.config.DataFlowClientAutoConfiguration;
import org.springframework.cloud.dataflow.rest.resource.about.AboutResource;
import org.springframework.cloud.dataflow.server.single.DataFlowServerApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

import static org.assertj.core.api.Assertions.assertThat;

@ActiveProfiles("keycloak")
@SpringBootTest(classes = { DataFlowServerApplication.class },
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@Testcontainers
@Disabled("Determine how to run app and test client in different contexts")
public class DataFlowAuthenticationTests {

private static final Logger logger = LoggerFactory.getLogger(DataFlowAuthenticationTests.class);

@Container
static KeycloakContainer keycloakContainer = new KeycloakContainer("keycloak/keycloak:25.0")
.withRealmImportFiles("/dataflow-realm.json", "/dataflow-users-0.json")
.withAdminUsername("admin")
.withAdminPassword("admin")
.withExposedPorts(8080, 9000)
.withLogConsumer(outputFrame -> {
switch (outputFrame.getType()) {
case STDERR:
logger.error(outputFrame.getUtf8StringWithoutLineEnding());
break;
default:
logger.info(outputFrame.getUtf8StringWithoutLineEnding());
}
});

@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("keycloak.url", keycloakContainer::getAuthServerUrl);
}

@Test
void testAuthentication() throws Exception {
try (ConfigurableApplicationContext applicationContext = SpringApplication.run(CommandLineApp.class,
"--spring.profiles.active=keycloak-client",
"--spring.cloud.dataflow.client.authentication.basic.username=joe",
"--spring.cloud.dataflow.client.authentication.basic.password=password",
"--keycloak.url=" + keycloakContainer.getAuthServerUrl(),
"--spring.cloud.dataflow.client.authentication.token-uri=" + keycloakContainer.getAuthServerUrl()
+ "/realms/dataflow/protocol/openid-connect/token")) {
DataFlowOperations dataFlowOperations = applicationContext.getBean(DataFlowOperations.class);
assertThat(dataFlowOperations).isNotNull();
AboutResource aboutResource = dataFlowOperations.aboutOperation().get();
assertThat(aboutResource).isNotNull();
assertThat(aboutResource.getSecurityInfo()).isNotNull();
assertThat(aboutResource.getSecurityInfo().isAuthenticated()).isTrue();
assertThat(aboutResource.getSecurityInfo().getUsername()).isEqualTo("joe");
CommandLineApp.completed.set(true);
}
finally {
CommandLineApp.completed.set(true);
}
}

@SpringBootApplication
@ImportAutoConfiguration(DataFlowClientAutoConfiguration.class)
public static class CommandLineApp implements CommandLineRunner {

public static AtomicBoolean completed = new AtomicBoolean(false);

@Override
public void run(String... args) throws Exception {
Awaitility.await().until(() -> completed.get());
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
spring:
cloud:
dataflow:
security:
authorization:
provider-role-mappings:
keycloak:
map-group-claims: true
role-mappings:
ROLE_VIEW: dataflow_view
ROLE_CREATE: dataflow_create
ROLE_MANAGE: dataflow_manage
ROLE_DEPLOY: dataflow_deploy
ROLE_DESTROY: dataflow_destroy
ROLE_MODIFY: dataflow_modify
ROLE_SCHEDULE: dataflow_schedule
client:
authentication:
client-id: 'dataflow'
client-secret: '090RucamvekrMLyGHMr4lkHX9xhAlsqK'
oauth2:
client-registration-id: keycloak
scope: openid, roles
security:
oauth2:
client:
provider:
keycloak:
issuer-uri: '${keycloak.url}/realms/dataflow'
jwk-set-uri: '${keycloak.url}/realms/dataflow/protocol/openid-connect/certs'
token-uri: '${keycloak.url}/realms/dataflow/protocol/openid-connect/token'
user-info-uri: '${keycloak.url}/realms/dataflow/protocol/openid-connect/userinfo'
user-name-attribute: 'user_name'
authorization-uri: '${keycloak.url}/realms/dataflow/protocol/openid-connect/auth'
registration:
keycloak:
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
client-id: 'dataflow'
client-name: 'dataflow'
client-secret: '090RucamvekrMLyGHMr4lkHX9xhAlsqK'
provider: 'keycloak'
authorization-grant-type: 'authorization_code'
# client-authentication-method: # unsure of value
scope:
- openid
- roles
resourceserver:
opaquetoken:
introspection-uri: ${keycloak.url}/realms/dataflow/protocol/openid-connect/token/introspect
client-id: 'dataflow'
client-secret: '090RucamvekrMLyGHMr4lkHX9xhAlsqK'
authorization:
check-token-access: isAuthenticated()
logging:
level:
org.springframework.security: debug
org.springframework.web: debug
org.springframework.cloud.dataflow: debug
org.springframework.cloud.common: debug
org.apache.hc: debug
org.apache.http: debug
threshold:
console: debug
Loading

0 comments on commit bc49898

Please sign in to comment.