Skip to content

Commit bc49898

Browse files
author
Corneil du Plessis
committed
Added Keycloak integration test
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.
1 parent a576e5c commit bc49898

36 files changed

+4940
-103
lines changed

.github/workflows/ci-it-security.yml

-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ jobs:
3333
- name: Run Security IT
3434
shell: bash
3535
run: |
36-
./mvnw clean install -DskipTests -T 1C -s .settings.xml -pl spring-cloud-dataflow-server -am -B --no-transfer-progress
3736
./mvnw -s .settings.xml \
3837
-pl spring-cloud-dataflow-server \
3938
-Dgroups=oauth \

spring-cloud-common-security-config/spring-cloud-common-security-config-web/src/test/java/org/springframework/cloud/common/security/support/DefaultAuthoritiesMapperTests.java

+27
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,33 @@ public void testMapConstructorWithIncompleteRoleMappings() throws Exception {
9393
}
9494

9595
@Test
96+
public void testThat3MappedAuthoritiesAreReturned() throws Exception {
97+
Map<String, String> roleMappings = new HashMap<>();
98+
roleMappings.put("ROLE_MANAGE", "dataflow_manage");
99+
roleMappings.put("ROLE_VIEW", "dataflow_view");
100+
roleMappings.put("ROLE_CREATE", "dataflow_create");
101+
roleMappings.put("ROLE_MODIFY", "dataflow_modify");
102+
roleMappings.put("ROLE_DEPLOY", "dataflow_deploy");
103+
roleMappings.put("ROLE_DESTROY", "dataflow_destroy");
104+
roleMappings.put("ROLE_SCHEDULE", "dataflow_schedule");
105+
106+
ProviderRoleMapping providerRoleMapping = new ProviderRoleMapping();
107+
providerRoleMapping.setMapOauthScopes(true);
108+
providerRoleMapping.getRoleMappings().putAll(roleMappings);
109+
110+
Set<String> roles = new HashSet<>();
111+
roles.add("dataflow_manage");
112+
roles.add("dataflow_view");
113+
roles.add("dataflow_deploy");
114+
115+
DefaultAuthoritiesMapper defaultAuthoritiesMapper = new DefaultAuthoritiesMapper("uaa", providerRoleMapping);
116+
Collection<? extends GrantedAuthority> authorities = defaultAuthoritiesMapper.mapScopesToAuthorities("uaa",
117+
roles, null);
118+
119+
assertThat(authorities).hasSize(3);
120+
assertThat(authorities.stream().map(authority -> authority.getAuthority()).collect(Collectors.toList()))
121+
.containsExactlyInAnyOrder("ROLE_DEPLOY", "ROLE_MANAGE", "ROLE_VIEW");
122+
}
96123
public void testThat7MappedAuthoritiesAreReturned() throws Exception {
97124
Map<String, String> roleMappings = new HashMap<>();
98125
roleMappings.put("ROLE_MANAGE", "foo-manage");

spring-cloud-dataflow-server/pom.xml

+11
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@
7272
<artifactId>spring-boot-starter-test</artifactId>
7373
<scope>test</scope>
7474
</dependency>
75+
<dependency>
76+
<groupId>org.springframework.security</groupId>
77+
<artifactId>spring-security-test</artifactId>
78+
<scope>test</scope>
79+
</dependency>
7580
<dependency>
7681
<groupId>org.springframework.boot</groupId>
7782
<artifactId>spring-boot-devtools</artifactId>
@@ -104,6 +109,12 @@
104109
<artifactId>junit-jupiter</artifactId>
105110
<scope>test</scope>
106111
</dependency>
112+
<dependency>
113+
<groupId>com.github.dasniko</groupId>
114+
<artifactId>testcontainers-keycloak</artifactId>
115+
<version>3.4.0</version>
116+
<scope>test</scope>
117+
</dependency>
107118
<dependency>
108119
<groupId>org.testcontainers</groupId>
109120
<artifactId>postgresql</artifactId>

spring-cloud-dataflow-server/src/main/resources/application.yml

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ spring:
2828
url: "{repository}/org/springframework/cloud/spring-cloud-dataflow-shell/{version}/spring-cloud-dataflow-shell-{version}.jar"
2929
checksum-sha1-url: "{repository}/org/springframework/cloud/spring-cloud-dataflow-shell/{version}/spring-cloud-dataflow-shell-{version}.jar.sha1"
3030
checksum-sha256-url: "{repository}/org/springframework/cloud/spring-cloud-dataflow-shell/{version}/spring-cloud-dataflow-shell-{version}.jar.sha256"
31+
3132
jpa:
3233
hibernate:
3334
ddl-auto: none

spring-cloud-dataflow-server/src/test/java/org/springframework/cloud/dataflow/integration/test/db/AbstractDataflowTests.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.IOException;
2020
import java.util.ArrayList;
2121
import java.util.Arrays;
22-
import java.util.Collections;
2322
import java.util.List;
2423
import java.util.stream.Collectors;
2524

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

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

9899
@Autowired

spring-cloud-dataflow-server/src/test/java/org/springframework/cloud/dataflow/integration/test/db/container/DataflowCluster.java

+130-53
Large diffs are not rendered by default.

spring-cloud-dataflow-server/src/test/java/org/springframework/cloud/dataflow/integration/test/oauth/DataflowOAuthIT.java

+58-2
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ class DataflowOAuthIT extends AbstractDataflowTests {
4040
private final Logger log = LoggerFactory.getLogger(DataflowOAuthIT.class);
4141

4242
@Test
43-
void securedSetup() throws Exception {
44-
log.info("Running testSecuredSetup()");
43+
void securedSetupUAA() throws Exception {
44+
log.info("Running securedSetupUAA()");
4545
this.dataflowCluster.startIdentityProvider(TagNames.UAA_4_32);
4646
this.dataflowCluster.startSkipper(TagNames.SKIPPER_main);
4747
this.dataflowCluster.startDataflow(TagNames.DATAFLOW_main);
@@ -92,4 +92,60 @@ void securedSetup() throws Exception {
9292
}
9393
}
9494
}
95+
96+
@Test
97+
void securedSetupKeycloak() throws Exception {
98+
log.info("Running securedSetupKeycloak()");
99+
this.dataflowCluster.startIdentityProvider(TagNames.KEYCLOAK_26);
100+
this.dataflowCluster.startSkipper(TagNames.SKIPPER_main);
101+
this.dataflowCluster.startDataflow(TagNames.DATAFLOW_main);
102+
103+
// we can't do oauth flow from host due to how oauth works as we
104+
// need proper networking, so use separate tools container to run
105+
// curl command as we support basic auth and if we get good response
106+
// oauth is working with dataflow and skipper.
107+
108+
AtomicReference<String> stderr = new AtomicReference<>();
109+
try {
110+
with().pollInterval(5, TimeUnit.SECONDS)
111+
.and()
112+
.await()
113+
.ignoreExceptions()
114+
.atMost(90, TimeUnit.SECONDS)
115+
.untilAsserted(() -> {
116+
log.info("Checking auth using curl");
117+
ExecResult cmdResult = execInToolsContainer("curl", "-v", "-u", "joe:password", "http://dataflow:9393/about");
118+
String response = cmdResult.getStdout();
119+
if (StringUtils.hasText(response)) {
120+
log.info("Response is {}", response);
121+
}
122+
if(StringUtils.hasText(cmdResult.getStderr())) {
123+
log.error(cmdResult.getStderr());
124+
}
125+
stderr.set(cmdResult.getStderr());
126+
assertThat(response).contains("\"authenticated\":true");
127+
assertThat(response).contains("\"username\":\"joe\"");
128+
stderr.set("");
129+
});
130+
log.info("Checking without credentials using curl");
131+
ExecResult cmdResult = execInToolsContainer("curl", "-v", "-f", "http://dataflow:9393/about");
132+
String response = cmdResult.getStdout();
133+
if (StringUtils.hasText(response)) {
134+
log.info("Response is {}", response);
135+
}
136+
response = cmdResult.getStderr();
137+
if(StringUtils.hasText(response)) {
138+
log.warn("Error is {}", response);
139+
}
140+
stderr.set(cmdResult.getStderr());
141+
assertThat(cmdResult.getExitCode()).isNotZero();
142+
stderr.set("");
143+
}
144+
finally {
145+
String msg = stderr.get();
146+
if (StringUtils.hasText(msg)) {
147+
log.error("curl error: {}", msg);
148+
}
149+
}
150+
}
95151
}

spring-cloud-dataflow-server/src/test/java/org/springframework/cloud/dataflow/integration/test/tags/TagNames.java

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ public abstract class TagNames {
6363
public static final String OAUTH = "oauth";
6464
public static final String PERFORMANCE = "performance";
6565
public static final String UAA = "uaa";
66+
public static final String KEYCLOAK = "keycloak";
67+
public static final String KEYCLOAK_25 = "keycloak_25";
68+
public static final String KEYCLOAK_26 = "keycloak_26";
69+
6670
public static final String UAA_4_32 = "uaa_4_32";
6771

6872
public static final String SKIPPER = "skipper";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.springframework.cloud.dataflow.unit.test;
2+
3+
import java.util.concurrent.atomic.AtomicBoolean;
4+
5+
import dasniko.testcontainers.keycloak.KeycloakContainer;
6+
import org.awaitility.Awaitility;
7+
import org.junit.jupiter.api.Disabled;
8+
import org.junit.jupiter.api.Test;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import org.testcontainers.junit.jupiter.Container;
12+
import org.testcontainers.junit.jupiter.Testcontainers;
13+
14+
import org.springframework.boot.CommandLineRunner;
15+
import org.springframework.boot.SpringApplication;
16+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
17+
import org.springframework.boot.autoconfigure.SpringBootApplication;
18+
import org.springframework.boot.test.context.SpringBootTest;
19+
import org.springframework.cloud.dataflow.rest.client.DataFlowOperations;
20+
import org.springframework.cloud.dataflow.rest.client.config.DataFlowClientAutoConfiguration;
21+
import org.springframework.cloud.dataflow.rest.resource.about.AboutResource;
22+
import org.springframework.cloud.dataflow.server.single.DataFlowServerApplication;
23+
import org.springframework.context.ConfigurableApplicationContext;
24+
import org.springframework.test.context.ActiveProfiles;
25+
import org.springframework.test.context.DynamicPropertyRegistry;
26+
import org.springframework.test.context.DynamicPropertySource;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
30+
@ActiveProfiles("keycloak")
31+
@SpringBootTest(classes = { DataFlowServerApplication.class },
32+
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
33+
@Testcontainers
34+
@Disabled("Determine how to run app and test client in different contexts")
35+
public class DataFlowAuthenticationTests {
36+
37+
private static final Logger logger = LoggerFactory.getLogger(DataFlowAuthenticationTests.class);
38+
39+
@Container
40+
static KeycloakContainer keycloakContainer = new KeycloakContainer("keycloak/keycloak:25.0")
41+
.withRealmImportFiles("/dataflow-realm.json", "/dataflow-users-0.json")
42+
.withAdminUsername("admin")
43+
.withAdminPassword("admin")
44+
.withExposedPorts(8080, 9000)
45+
.withLogConsumer(outputFrame -> {
46+
switch (outputFrame.getType()) {
47+
case STDERR:
48+
logger.error(outputFrame.getUtf8StringWithoutLineEnding());
49+
break;
50+
default:
51+
logger.info(outputFrame.getUtf8StringWithoutLineEnding());
52+
}
53+
});
54+
55+
@DynamicPropertySource
56+
static void configureProperties(DynamicPropertyRegistry registry) {
57+
registry.add("keycloak.url", keycloakContainer::getAuthServerUrl);
58+
}
59+
60+
@Test
61+
void testAuthentication() throws Exception {
62+
try (ConfigurableApplicationContext applicationContext = SpringApplication.run(CommandLineApp.class,
63+
"--spring.profiles.active=keycloak-client",
64+
"--spring.cloud.dataflow.client.authentication.basic.username=joe",
65+
"--spring.cloud.dataflow.client.authentication.basic.password=password",
66+
"--keycloak.url=" + keycloakContainer.getAuthServerUrl(),
67+
"--spring.cloud.dataflow.client.authentication.token-uri=" + keycloakContainer.getAuthServerUrl()
68+
+ "/realms/dataflow/protocol/openid-connect/token")) {
69+
DataFlowOperations dataFlowOperations = applicationContext.getBean(DataFlowOperations.class);
70+
assertThat(dataFlowOperations).isNotNull();
71+
AboutResource aboutResource = dataFlowOperations.aboutOperation().get();
72+
assertThat(aboutResource).isNotNull();
73+
assertThat(aboutResource.getSecurityInfo()).isNotNull();
74+
assertThat(aboutResource.getSecurityInfo().isAuthenticated()).isTrue();
75+
assertThat(aboutResource.getSecurityInfo().getUsername()).isEqualTo("joe");
76+
CommandLineApp.completed.set(true);
77+
}
78+
finally {
79+
CommandLineApp.completed.set(true);
80+
}
81+
}
82+
83+
@SpringBootApplication
84+
@ImportAutoConfiguration(DataFlowClientAutoConfiguration.class)
85+
public static class CommandLineApp implements CommandLineRunner {
86+
87+
public static AtomicBoolean completed = new AtomicBoolean(false);
88+
89+
@Override
90+
public void run(String... args) throws Exception {
91+
Awaitility.await().until(() -> completed.get());
92+
}
93+
94+
}
95+
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
spring:
2+
cloud:
3+
dataflow:
4+
security:
5+
authorization:
6+
provider-role-mappings:
7+
keycloak:
8+
map-group-claims: true
9+
role-mappings:
10+
ROLE_VIEW: dataflow_view
11+
ROLE_CREATE: dataflow_create
12+
ROLE_MANAGE: dataflow_manage
13+
ROLE_DEPLOY: dataflow_deploy
14+
ROLE_DESTROY: dataflow_destroy
15+
ROLE_MODIFY: dataflow_modify
16+
ROLE_SCHEDULE: dataflow_schedule
17+
client:
18+
authentication:
19+
client-id: 'dataflow'
20+
client-secret: '090RucamvekrMLyGHMr4lkHX9xhAlsqK'
21+
oauth2:
22+
client-registration-id: keycloak
23+
scope: openid, roles
24+
security:
25+
oauth2:
26+
client:
27+
provider:
28+
keycloak:
29+
issuer-uri: '${keycloak.url}/realms/dataflow'
30+
jwk-set-uri: '${keycloak.url}/realms/dataflow/protocol/openid-connect/certs'
31+
token-uri: '${keycloak.url}/realms/dataflow/protocol/openid-connect/token'
32+
user-info-uri: '${keycloak.url}/realms/dataflow/protocol/openid-connect/userinfo'
33+
user-name-attribute: 'user_name'
34+
authorization-uri: '${keycloak.url}/realms/dataflow/protocol/openid-connect/auth'
35+
registration:
36+
keycloak:
37+
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
38+
client-id: 'dataflow'
39+
client-name: 'dataflow'
40+
client-secret: '090RucamvekrMLyGHMr4lkHX9xhAlsqK'
41+
provider: 'keycloak'
42+
authorization-grant-type: 'authorization_code'
43+
# client-authentication-method: # unsure of value
44+
scope:
45+
- openid
46+
- roles
47+
resourceserver:
48+
opaquetoken:
49+
introspection-uri: ${keycloak.url}/realms/dataflow/protocol/openid-connect/token/introspect
50+
client-id: 'dataflow'
51+
client-secret: '090RucamvekrMLyGHMr4lkHX9xhAlsqK'
52+
authorization:
53+
check-token-access: isAuthenticated()
54+
logging:
55+
level:
56+
org.springframework.security: debug
57+
org.springframework.web: debug
58+
org.springframework.cloud.dataflow: debug
59+
org.springframework.cloud.common: debug
60+
org.apache.hc: debug
61+
org.apache.http: debug
62+
threshold:
63+
console: debug

0 commit comments

Comments
 (0)