Skip to content

Commit

Permalink
Merge pull request #498 from pjgg/feat/upgrade_keycloak
Browse files Browse the repository at this point in the history
Upgrade keycloak to version 18.0.0
  • Loading branch information
mjurc authored Jun 27, 2022
2 parents d7278df + d183c63 commit 5488216
Show file tree
Hide file tree
Showing 29 changed files with 657 additions and 81 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@

import io.quarkus.test.bootstrap.KeycloakService;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.services.Container;
import io.quarkus.test.services.KeycloakContainer;

public abstract class BaseSecurityResourceIT {

static final String REALM_BASE_PATH = "realms";
static final String REALM_DEFAULT = "test-realm";
static final String CLIENT_ID_DEFAULT = "test-application-client";
static final String CLIENT_SECRET_DEFAULT = "test-application-client-secret";
static final String NORMAL_USER = "test-normal-user";

@Container(image = "quay.io/keycloak/keycloak:16.1.0", expectedLog = "Admin console listening", port = 8080)
static final KeycloakService keycloak = new KeycloakService("/keycloak-realm.json", REALM_DEFAULT);
@KeycloakContainer(command = { "start-dev --import-realm" })
static KeycloakService keycloak = new KeycloakService("/keycloak-realm.json", REALM_DEFAULT, REALM_BASE_PATH);

private AuthzClient authzClient;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.qe;

import static org.hamcrest.Matchers.equalTo;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.keycloak.authorization.client.AuthzClient;

import io.quarkus.test.bootstrap.KeycloakService;
import io.quarkus.test.bootstrap.RestService;
import io.quarkus.test.scenarios.QuarkusScenario;
import io.quarkus.test.services.Container;
import io.quarkus.test.services.QuarkusApplication;

@QuarkusScenario
public class LegacyKeycloakIT {

static final String REALM_DEFAULT = "test-realm";
static final String CLIENT_ID_DEFAULT = "test-application-client";
static final String CLIENT_SECRET_DEFAULT = "test-application-client-secret";
static final String NORMAL_USER = "test-normal-user";

@Container(image = "quay.io/keycloak/keycloak:14.0.0", expectedLog = "Admin console listening", port = 8080)
static KeycloakService keycloak = new KeycloakService("/keycloak-realm.json", REALM_DEFAULT, "/auth/realms/");

private AuthzClient authzClient;

@QuarkusApplication
static final RestService app = new RestService()
.withProperty("quarkus.oidc.auth-server-url", () -> keycloak.getRealmUrl())
.withProperty("quarkus.oidc.client-id", CLIENT_ID_DEFAULT)
.withProperty("quarkus.oidc.credentials.secret", CLIENT_SECRET_DEFAULT);

@BeforeEach
public void setup() {
authzClient = keycloak.createAuthzClient(CLIENT_ID_DEFAULT, CLIENT_SECRET_DEFAULT);
}

@Test
public void checkUserResourceByNormalUser() {
app.given()
.auth().oauth2(getTokenByTestNormalUser())
.get("/user")
.then()
.statusCode(200)
.body(equalTo("Hello, user test-normal-user"));
}

private String getTokenByTestNormalUser() {
return authzClient.obtainAccessToken(NORMAL_USER, NORMAL_USER).getToken();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class OpenShiftUsingCustomTemplateResourceIT {
private static final String CLIENT_ID_DEFAULT = "test-application-client";
private static final String CLIENT_SECRET_DEFAULT = "test-application-client-secret";

@Container(image = "quay.io/keycloak/keycloak:16.1.0", expectedLog = "Admin console listening", port = 8080)
@Container(image = "quay.io/keycloak/keycloak:18.0.0", expectedLog = "started", port = 8080)
static final KeycloakService customkeycloak = new KeycloakService("/keycloak-realm.json", REALM_DEFAULT);

@QuarkusApplication
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.quarkus.qe;

import io.quarkus.test.scenarios.OpenShiftDeploymentStrategy;
import io.quarkus.test.scenarios.OpenShiftScenario;

@OpenShiftScenario(deployment = OpenShiftDeploymentStrategy.UsingOpenShiftExtension)
public class OpenShiftUsingExtensionLegacyKeycloakIT extends LegacyKeycloakIT {
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

@QuarkusScenario
public class SecurityResourceIT extends BaseSecurityResourceIT {

@QuarkusApplication
static final RestService app = new RestService()
.withProperty("quarkus.oidc.auth-server-url", () -> keycloak.getRealmUrl())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ items:
- name: latest
from:
kind: DockerImage
name: quay.io/keycloak/keycloak:16.1.0
name: quay.io/keycloak/keycloak:18.0.0
- apiVersion: apps.openshift.io/v1
kind: DeploymentConfig
metadata:
Expand All @@ -31,6 +31,7 @@ items:
spec:
containers:
- name: keycloak
args: [ "start-dev", "--import-realm"]
env:
- name: X509_CA_BUNDLE
value: /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt
Expand Down
33 changes: 32 additions & 1 deletion examples/keycloak/src/test/resources/keycloak-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,38 @@
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"clientAuthenticatorType": "client-secret",
"secret": "test-application-client-secret"
"secret": "test-application-client-secret",
"protocolMappers": [
{
"id": "f17f8d5f-2327-4e0b-8001-48a7706be252",
"name": "realm roles",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-realm-role-mapper",
"consentRequired": false,
"config": {
"user.attribute": "foo",
"access.token.claim": "true",
"claim.name": "realm_access.roles",
"jsonType.label": "String",
"multivalued": "true"
}
},
{
"id": "0e0f1e8d-60f9-4435-b753-136d70e56af8",
"name": "username",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-property-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"user.attribute": "username",
"id.token.claim": "true",
"access.token.claim": "true",
"claim.name": "preferred_username",
"jsonType.label": "String"
}
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.test.services.containers;

import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.ServiceLoader;

import io.quarkus.test.bootstrap.ManagedResource;
Expand Down Expand Up @@ -29,7 +30,7 @@ protected String getExpectedLog() {
}

protected String[] getCommand() {
return command;
return Optional.ofNullable(command).orElse(new String[] {});
}

protected Integer getPort() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import static io.quarkus.test.bootstrap.BaseService.SERVICE_STARTUP_TIMEOUT;
import static io.quarkus.test.bootstrap.BaseService.SERVICE_STARTUP_TIMEOUT_DEFAULT;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_PREFIX;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_PREFIX;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_PREFIX_MATCHER;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_SPLIT_CHAR;
import static io.quarkus.test.utils.PropertiesUtils.SECRET_PREFIX;

import java.nio.file.Files;
Expand Down Expand Up @@ -119,6 +122,17 @@ private Map<String, String> resolveProperties() {
if (isResource(entry.getValue())) {
value = entry.getValue().replace(RESOURCE_PREFIX, StringUtils.EMPTY);
addFileToContainer(value);
} else if (isResourceWithDestinationPath(entry.getValue())) {
value = entry.getValue().replace(RESOURCE_WITH_DESTINATION_PREFIX, StringUtils.EMPTY);
if (!value.matches(RESOURCE_WITH_DESTINATION_PREFIX_MATCHER)) {
String errorMsg = String.format("Unexpected %s format. Expected destinationPath|fileName but found %s",
RESOURCE_WITH_DESTINATION_PREFIX, value);
throw new RuntimeException(errorMsg);
}

String destinationPath = value.split(RESOURCE_WITH_DESTINATION_SPLIT_CHAR)[0];
String fileName = value.split(RESOURCE_WITH_DESTINATION_SPLIT_CHAR)[1];
addFileToContainer(destinationPath, fileName);
} else if (isSecret(entry.getValue())) {
value = entry.getValue().replace(SECRET_PREFIX, StringUtils.EMPTY);
addFileToContainer(value);
Expand All @@ -139,6 +153,15 @@ private void addFileToContainer(String filePath) {
}
}

private void addFileToContainer(String destinationPath, String hostFilePath) {
String containerFullPath = destinationPath + hostFilePath;
innerContainer.withClasspathResourceMapping(hostFilePath, containerFullPath, BindMode.READ_ONLY);
}

private boolean isResourceWithDestinationPath(String key) {
return key.startsWith(RESOURCE_WITH_DESTINATION_PREFIX);
}

private boolean isResource(String key) {
return key.startsWith(RESOURCE_PREFIX);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
public final class PropertiesUtils {

public static final String RESOURCE_PREFIX = "resource::/";
public static final String RESOURCE_WITH_DESTINATION_PREFIX = "resource_with_destination::";
public static final String RESOURCE_WITH_DESTINATION_SPLIT_CHAR = "\\|";
public static final String RESOURCE_WITH_DESTINATION_PREFIX_MATCHER = ".*" + RESOURCE_WITH_DESTINATION_SPLIT_CHAR + ".*";
public static final String SECRET_PREFIX = "secret::/";
public static final Path TARGET = Path.of("target");
public static final String SLASH = "/";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package io.quarkus.test.bootstrap.inject;

import static io.quarkus.test.model.CustomVolume.VolumeType.CONFIG_MAP;
import static io.quarkus.test.model.CustomVolume.VolumeType.SECRET;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_PREFIX;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_PREFIX;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_PREFIX_MATCHER;
import static io.quarkus.test.utils.PropertiesUtils.RESOURCE_WITH_DESTINATION_SPLIT_CHAR;
import static io.quarkus.test.utils.PropertiesUtils.SECRET_PREFIX;
import static io.quarkus.test.utils.PropertiesUtils.SLASH;
import static io.quarkus.test.utils.PropertiesUtils.TARGET;
Expand All @@ -26,16 +31,13 @@
import org.apache.commons.lang3.StringUtils;

import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.ConfigMapVolumeSourceBuilder;
import io.fabric8.kubernetes.api.model.Container;
import io.fabric8.kubernetes.api.model.EnvVar;
import io.fabric8.kubernetes.api.model.HasMetadata;
import io.fabric8.kubernetes.api.model.KubernetesList;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.SecretVolumeSourceBuilder;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.Volume;
import io.fabric8.kubernetes.api.model.VolumeBuilder;
import io.fabric8.kubernetes.api.model.VolumeMount;
import io.fabric8.kubernetes.api.model.VolumeMountBuilder;
import io.fabric8.kubernetes.api.model.apps.Deployment;
import io.fabric8.kubernetes.client.utils.Serialization;
Expand All @@ -46,6 +48,7 @@
import io.quarkus.test.bootstrap.Service;
import io.quarkus.test.configuration.PropertyLookup;
import io.quarkus.test.logging.Log;
import io.quarkus.test.model.CustomVolume;
import io.quarkus.test.utils.Command;
import io.quarkus.test.utils.FileUtils;

Expand Down Expand Up @@ -365,67 +368,88 @@ private EnvVar getEnvVarByKey(String key, Container container) {

private Map<String, String> enrichProperties(Map<String, String> properties, Deployment deployment) {
// mount path x volume
Map<String, Volume> volumes = new HashMap<>();
Map<String, CustomVolume> volumes = new HashMap<>();

Map<String, String> output = new HashMap<>();
for (Entry<String, String> entry : properties.entrySet()) {
String value = entry.getValue();
String propertyValue = entry.getValue();
if (isResource(entry.getValue())) {
String path = entry.getValue().replace(RESOURCE_PREFIX, StringUtils.EMPTY);
String mountPath = getMountPath(path);
String filename = getFileName(path);
String configMapName = normalizeName(mountPath);
String configMapName = normalizeConfigMapName(mountPath);

// Update config map
createOrUpdateConfigMap(configMapName, filename, getFileContent(path));

// Add the volume
if (!volumes.containsKey(mountPath)) {
Volume volume = new VolumeBuilder()
.withName(configMapName)
.withConfigMap(new ConfigMapVolumeSourceBuilder().withName(configMapName).build())
.build();
volumes.put(mountPath, volume);
volumes.put(mountPath, new CustomVolume(configMapName, "", CONFIG_MAP));
}

value = mountPath + SLASH + filename;
propertyValue = mountPath + SLASH + filename;
} else if (isResourceWithDestinationPath(propertyValue)) {
String path = propertyValue.replace(RESOURCE_WITH_DESTINATION_PREFIX, StringUtils.EMPTY);
if (!propertyValue.matches(RESOURCE_WITH_DESTINATION_PREFIX_MATCHER)) {
String errorMsg = String.format("Unexpected %s format. Expected destinationPath|fileName but found %s",
RESOURCE_WITH_DESTINATION_PREFIX, propertyValue);
throw new RuntimeException(errorMsg);
}

String mountPath = path.split(RESOURCE_WITH_DESTINATION_SPLIT_CHAR)[0];
String fileName = path.split(RESOURCE_WITH_DESTINATION_SPLIT_CHAR)[1];
String fileNameNormalized = getFileName(fileName);
String configMapName = normalizeConfigMapName(mountPath);

// Update config map
createOrUpdateConfigMap(configMapName, fileNameNormalized, getFileContent(fileName));
propertyValue = mountPath + SLASH + fileNameNormalized;
// Add the volume
if (!volumes.containsKey(mountPath)) {
volumes.put(propertyValue, new CustomVolume(configMapName, fileNameNormalized, CONFIG_MAP));
}
} else if (isSecret(entry.getValue())) {
String path = entry.getValue().replace(SECRET_PREFIX, StringUtils.EMPTY);
String mountPath = getMountPath(path);
String filename = getFileName(path);
String secretName = normalizeName(path);
String secretName = normalizeConfigMapName(path);

// Push secret file
doCreateSecretFromFile(secretName, getFilePath(path));

// Add the volume
Volume volume = new VolumeBuilder()
.withName(secretName)
.withSecret(new SecretVolumeSourceBuilder()
.withSecretName(secretName)
.build())
.build();
volumes.put(mountPath, volume);

value = mountPath + SLASH + filename;
volumes.put(mountPath, volumes.put(mountPath, new CustomVolume(secretName, "", SECRET)));
propertyValue = mountPath + SLASH + filename;
}

output.put(entry.getKey(), value);
output.put(entry.getKey(), propertyValue);
}

for (Entry<String, Volume> volume : volumes.entrySet()) {
deployment.getSpec().getTemplate().getSpec().getVolumes().add(volume.getValue());
for (Entry<String, CustomVolume> volume : volumes.entrySet()) {
deployment.getSpec().getTemplate().getSpec().getVolumes().add(volume.getValue().getVolume());

// Configure all the containers to map the volume
deployment.getSpec().getTemplate().getSpec().getContainers()
.forEach(container -> container.getVolumeMounts()
.add(new VolumeMountBuilder().withName(volume.getValue().getName())
.withReadOnly(true).withMountPath(volume.getKey()).build()));
.add(createVolumeMount(volume)));
}

return output;
}

private VolumeMount createVolumeMount(Entry<String, CustomVolume> volume) {
VolumeMountBuilder volumeMountBuilder = new VolumeMountBuilder().withName(volume.getValue().getName())
.withReadOnly(true).withMountPath(volume.getKey());

if (!volume.getValue().getSubFolderRegExp().isEmpty()) {
volumeMountBuilder.withSubPathExpr(volume.getValue().getSubFolderRegExp());
}

return volumeMountBuilder.build();
}

private boolean isResourceWithDestinationPath(String key) {
return key.startsWith(RESOURCE_WITH_DESTINATION_PREFIX);
}

private void createOrUpdateConfigMap(String configMapName, String key, String value) {
if (client.configMaps().withName(configMapName).get() != null) {
// update existing config map by adding new file
Expand Down Expand Up @@ -498,7 +522,7 @@ private String getFilePath(String path) {
return path;
}

private String normalizeName(String name) {
private String normalizeConfigMapName(String name) {
return StringUtils.removeStart(name, SLASH)
.replaceAll(Pattern.quote("."), "-")
.replaceAll(SLASH, "-");
Expand Down
Loading

0 comments on commit 5488216

Please sign in to comment.