Skip to content

Commit

Permalink
Setup the keycloak standalone server for testing.
Browse files Browse the repository at this point in the history
Handle keycloak user roles update, delete interactions through saga.
Fix database connection closed issue and functional tests
  • Loading branch information
ali-everest authored and nitinks-ee committed Oct 29, 2021
1 parent a15a3c6 commit de0dc51
Show file tree
Hide file tree
Showing 32 changed files with 717 additions and 140 deletions.
8 changes: 5 additions & 3 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ steps:
agents:
java: 11
commands:
- ./gradlew build --console plain
- pkill java || echo 'No Java processes to kill.'
- ./gradlew startServer && ./gradlew build --console plain

- label: ':sonarqube: Quality reporting'
<<: *if-our-repo
<<: *if-main-branch
agents:
java: 11
commands:
- ./gradlew clean codeCoverageReport sonarqube --console plain
- pkill java || echo 'No Java processes to kill.'
- ./gradlew clean startServer && ./gradlew codeCoverageReport sonarqube --console plain

- wait:
<<: *if-release
Expand All @@ -29,7 +31,7 @@ steps:
<<: *if-release
key: bintray-publish
agents:
java: 11
java: 11
commands:
- git fetch --tags
- ./gradlew bintrayPublish bintrayUpload --console plain
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ subprojects {
h2Version = '1.4.200'
hamcrestVersion = '2.2'
junitVersion = '5.7.2'
keycloakTestServerVersion = '15.0.2'
mockitoVersion = '3.11.2'
undercouchVersion = '4.1.2'
}

dependencyManagement {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ public UserResponse getUser(@ApiIgnore Principal principal) {
@PutMapping
@ApiOperation("Update currently authenticated user information")
public void updateUser(@ApiIgnore Principal principal, @RequestBody UpdateUserRequest updateUserRequest) {
var uId = UUID.fromString(principal.getName());
usersService.updateUser(uId, uId, updateUserRequest.getEmail(),
var userId = UUID.fromString(principal.getName());
usersService.updateUser(userId, userId, updateUserRequest.getEmail(),
updateUserRequest.getDisplayName());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public boolean belongsToOrg(UUID organizationId) {

@Override
public void setFilterObject(Object o) {
// Todo
// Do nothing
}

@Override
Expand All @@ -135,7 +135,7 @@ public Object getFilterObject() {

@Override
public void setReturnObject(Object returnObject) {
// Todo
// Do nothing
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,16 +200,6 @@ void creatingOrganizationUser_WillFail_WhenDisplayNameIsBlank() throws Exception
verifyNoInteractions(usersService);
}

@Test
void creatingOrganizationUser_WillFail_WhenPasswordIsBlank() throws Exception {
mockMvc.perform(post("/api/organizations/{organizationId}/users", ORGANIZATION_2.getId())
.contentType(APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new NewUserRequest(ORG_1_USER_1.getUsername(), ORG_1_USER_1.getDisplayName()))))
.andExpect(status().isBadRequest());

verifyNoInteractions(usersService);
}

@Test
@WithMockKeycloakAuth(authorities = ROLE_ADMIN)
void creatingOrganizationUserWillDelegate_WhenRequestingUserIsAdmin() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public CommandValidatingMessageHandlerInterceptor(List<Validates> validators, Va
Map<Class<?>, Validates<?>> m = new ConcurrentHashMap<>();
for (Validates<?> validator : validators) {
Type validatableCommandType = Arrays.stream(validator.getClass().getGenericInterfaces())
.map(e -> (ParameterizedType) e)
.map(ParameterizedType.class::cast)
.filter(e -> Validates.class == e.getRawType())
.map(e -> e.getActualTypeArguments()[0])
.findFirst().orElseThrow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ private boolean isReplaying() {

private List<ReplayableEventProcessor> getReplayableEventProcessors() {
return axonConfiguration.eventProcessingConfiguration().eventProcessors().values().stream()
.filter(e -> e instanceof ReplayableEventProcessor)
.map(e -> (ReplayableEventProcessor) e)
.filter(ReplayableEventProcessor.class::isInstance)
.map(ReplayableEventProcessor.class::cast)
.collect(toList());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package engineering.everest.lhotse.axon.common.exceptions;

public class KeycloakSynchronizationException extends RuntimeException {
public KeycloakSynchronizationException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
package engineering.everest.lhotse.axon.common.services;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

import engineering.everest.lhotse.axon.common.domain.Role;
import engineering.everest.lhotse.axon.common.domain.UserAttribute;
import engineering.everest.lhotse.axon.common.exceptions.KeycloakSynchronizationException;
import lombok.extern.slf4j.Slf4j;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.json.JSONArray;
import org.json.JSONObject;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
Expand All @@ -14,10 +23,12 @@

import reactor.core.publisher.Mono;

@Slf4j
@Component
public class KeycloakSynchronizationService {
private static final String BEARER = "Bearer ";
private static final String AUTHORIZATION = "Authorization";
private static final String VALUE_KEY = "value";

@Value("${keycloak.auth-server-url}")
private String keycloakServerAuthUrl;
Expand All @@ -26,13 +37,15 @@ public class KeycloakSynchronizationService {
@Value("${kc.server.admin-password}")
private String keycloakAdminPassword;
@Value("${kc.server.master-realm.default.client-id}")
private String keycloakAdminClientId;
private String keycloakMasterRealmAdminClientId;
@Value("${keycloak.resource}")
private String keycloakDefaultRealmDefaultClientId;
@Value("${kc.server.connection.pool-size}")
private int keycloakServerConnectionPoolSize;

private Keycloak getAdminKeycloakClientInstance() {
return KeycloakBuilder.builder().serverUrl(keycloakServerAuthUrl).grantType(OAuth2Constants.PASSWORD)
.realm("master").clientId(keycloakAdminClientId).username(keycloakAdminUser)
.realm("master").clientId(keycloakMasterRealmAdminClientId).username(keycloakAdminUser)
.password(keycloakAdminPassword)
.resteasyClient(new ResteasyClientBuilder().connectionPoolSize(keycloakServerConnectionPoolSize).build()).build();
}
Expand Down Expand Up @@ -67,6 +80,49 @@ public String getUsers(Map<String, Object> queryFilters) {
var usersUri = String.format("%s/admin/realms/default/users", keycloakServerAuthUrl);
var accessToken = BEARER + getAdminKeycloakClientInstance().tokenManager().getAccessToken().getToken();

usersUri += getFilters(queryFilters);
return WebClient.create(usersUri).get().header(AUTHORIZATION, accessToken).retrieve().bodyToMono(String.class)
.block();
}

public String getClientDetails(Map<String, Object> queryFilters) {
var clientsUri = String.format("%s/admin/realms/default/clients", keycloakServerAuthUrl);
var accessToken = BEARER + getAdminKeycloakClientInstance().tokenManager().getAccessToken().getToken();

clientsUri += getFilters(queryFilters);
return WebClient.create(clientsUri).get().header(AUTHORIZATION, accessToken).retrieve().bodyToMono(String.class)
.block();
}

public String getClientLevelRoleMappings(UUID userId, UUID clientId) {
var clientLevelAvailableRolesUri =
String.format("%s/admin/realms/default/users/%s/role-mappings/clients/%s/available",
keycloakServerAuthUrl, userId, clientId);
var accessToken = BEARER + getAdminKeycloakClientInstance().tokenManager().getAccessToken().getToken();

return WebClient.create(clientLevelAvailableRolesUri).get().header(AUTHORIZATION, accessToken).retrieve().bodyToMono(String.class)
.block();
}

public void updateUserRoles(UUID userId, UUID clientId, List<Map<String, Object>> roles) {
var userClientRolesMappingUri =
String.format("%s/admin/realms/default/users/%s/role-mappings/clients/%s", keycloakServerAuthUrl, userId, clientId);
var accessToken = BEARER + getAdminKeycloakClientInstance().tokenManager().getAccessToken().getToken();

WebClient.create(userClientRolesMappingUri).post().header(AUTHORIZATION, accessToken).contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON).bodyValue(roles)
.exchangeToMono(res -> Mono.just(res.statusCode())).block();
}

public String getClientSecret(UUID clientId) {
var clientSecretUri = String.format("%s/admin/realms/default/clients/%s/client-secret", keycloakServerAuthUrl, clientId);
var accessToken = BEARER + getAdminKeycloakClientInstance().tokenManager().getAccessToken().getToken();

return WebClient.create(clientSecretUri).get().header(AUTHORIZATION, accessToken).retrieve().bodyToMono(String.class)
.block();
}

private StringBuilder getFilters(Map<String, Object> queryFilters) {
var filters = new StringBuilder("?");
if (!queryFilters.isEmpty()) {
for (var filter : queryFilters.entrySet()) {
Expand All @@ -75,11 +131,51 @@ public String getUsers(Map<String, Object> queryFilters) {
filters.append(filter.getValue());
filters.append('&');
}
usersUri += filters;
}

return WebClient.create(usersUri).get().header(AUTHORIZATION, accessToken).retrieve().bodyToMono(String.class)
.block();
return filters;
}

public Map<String, Object> setupKeycloakUser(String username, String email, boolean enabled, UUID organizationId,
Set<Role> roles, String displayName, String password, boolean passwordTemporary) {
var userDetails = new HashMap<String, Object>();
try {
createUser(Map.of("username", username,
"email", email,
"enabled", enabled,
"attributes", new UserAttribute(organizationId, roles, displayName),
"credentials", List.of(Map.of("type", "password",
VALUE_KEY, password,
"temporary", passwordTemporary))));

var userId = UUID.fromString(new JSONArray(getUsers(Map.of("username", username)))
.getJSONObject(0).getString("id"));
userDetails.put("userId", userId);

var clientId = UUID.fromString(new JSONArray(getClientDetails(Map.of("clientId", keycloakDefaultRealmDefaultClientId)))
.getJSONObject(0).getString("id"));
userDetails.put("clientId", clientId);

var clientSecret = getClientSecret(clientId);
if (clientSecret.contains(VALUE_KEY)) {
userDetails.put("clientSecret", new JSONObject(clientSecret).getString(VALUE_KEY));
}

var clientLevelMappingArray = new JSONArray(getClientLevelRoleMappings(userId, clientId));
if (clientLevelMappingArray.length() > 0) {
var clientLevelMappingDetails = clientLevelMappingArray.getJSONObject(0);
updateUserRoles(userId, clientId, List.of(Map.of(
"id", clientLevelMappingDetails.getString("id"),
"name", clientLevelMappingDetails.getString("name"),
"description", clientLevelMappingDetails.getString("description"),
"composite", clientLevelMappingDetails.getBoolean("composite"),
"clientRole", clientLevelMappingDetails.getBoolean("clientRole"),
"containerId", clientLevelMappingDetails.getString("containerId"))));
} else {
LOGGER.warn("Roles are already mapped or no role mappings found.");
}
} catch (Exception e) {
throw (KeycloakSynchronizationException)new KeycloakSynchronizationException(e.getMessage()).initCause(e);
}
return userDetails;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
public class TranslatableException extends RuntimeException {

private final String i18nMessageKey;
private final Object[] args;
private final transient Object[] args;

public TranslatableException(String i18nMessageKey) {
super(i18nMessageKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public boolean belongsToOrg(UUID organizationId) {

@Override
public void setFilterObject(Object o) {
// Todo
// Do nothing
}

@Override
Expand All @@ -59,7 +59,7 @@ public Object getFilterObject() {

@Override
public void setReturnObject(Object returnObject) {
// Todo
// Do nothing
}

@Override
Expand Down
36 changes: 35 additions & 1 deletion src/launcher/build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
buildscript {
repositories {
jcenter()
mavenCentral()
}
}

plugins {
id 'org.springframework.boot'
id "de.undercouch.download" version "${undercouchVersion}"
}

apply plugin: 'jacoco'
Expand Down Expand Up @@ -85,3 +86,36 @@ dependencyManagement {
mavenBom "org.keycloak.bom:keycloak-adapter-bom:${keycloakVersion}"
}
}

task downloadKeycloakZipFile(type: Download) {
src "https://github.com/keycloak/keycloak/releases/download/${keycloakTestServerVersion}/keycloak-${keycloakTestServerVersion}.zip"
dest new File(buildDir, "keycloak-${keycloakTestServerVersion}.zip")
onlyIfModified true
}

task downloadAndUnzipKeycloakFile(dependsOn: downloadKeycloakZipFile, type: Copy) {
from zipTree(downloadKeycloakZipFile.dest)
into buildDir
}

task startServer(dependsOn: 'downloadAndUnzipKeycloakFile') {
doLast {
def keycloakDir = "$buildDir/keycloak-${keycloakTestServerVersion}"
def port = 8180
def waitTime = 10000
def testUser = "[email protected]"
def testPass = "ac0n3x72"

def createUser = "$keycloakDir/bin/add-user-keycloak.sh -r master -u $testUser -p $testPass".execute()
createUser.consumeProcessOutput(System.out, System.err)
createUser.waitForOrKill(waitTime)

def startServer = "$keycloakDir/bin/standalone.sh -Djboss.http.port=$port".execute()
startServer.consumeProcessOutput(System.out, System.err)
startServer.waitForOrKill(3 * waitTime)

def createRealm = "$keycloakDir/bin/kcadm.sh create realms -f $rootDir/imports/realm-export.json --no-config --server http://localhost:$port/auth --realm master --user $testUser --password $testPass".execute()
createRealm.consumeProcessOutput(System.out, System.err)
createRealm.waitForOrKill(waitTime)
}
}
Loading

0 comments on commit de0dc51

Please sign in to comment.