Skip to content

Commit

Permalink
feat: add service account realm role mapping (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcScheib committed Jan 27, 2024
1 parent 72a1a66 commit 68d2413
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.cycrilabs.keycloak.configurator.commands.configure.boundary;

import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

import jakarta.enterprise.context.ApplicationScoped;

import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;

import com.cycrilabs.keycloak.configurator.commands.configure.entity.ServiceUserRealmRoleMappingDTO;
import com.cycrilabs.keycloak.configurator.shared.control.JsonUtil;
import com.cycrilabs.keycloak.configurator.shared.entity.EntityType;

import io.quarkus.logging.Log;

@ApplicationScoped
public class ServiceAccountRealmRoleImporter extends AbstractImporter {
@Override
public EntityType getType() {
return EntityType.SERVICE_ACCOUNT_REALM_ROLE;
}

@Override
protected Object importFile(final Path file) {
final String[] fileNameParts = file.toString().split(PATH_SEPARATOR);
final String realmName = fileNameParts[fileNameParts.length - 4];
final String serviceUsername = fileNameParts[fileNameParts.length - 2];

Log.debugf(
"Importing service account realm roles '%s' for service user '%s' of realm '%s'.",
file.getFileName(), serviceUsername, realmName);

final UserRepresentation user = loadUserByUsername(realmName, serviceUsername);
if (user == null) {
return null;
}

Log.debugf("Found service user '%s' of realm '%s'.", user.getUsername(), realmName);

importServiceUserRealmRoleMappings(file, realmName, user);

return null;
}

private UserRepresentation loadUserByUsername(final String realmName, final String username) {
try {
final List<UserRepresentation> userRepresentations = keycloak.realm(realmName)
.users()
.searchByUsername(username, Boolean.TRUE);
if (userRepresentations.size() == 1) {
return userRepresentations.getFirst();
}

Log.warnf("Found %d users '%s' of realm '%s'. Skipping import.",
Integer.valueOf(userRepresentations.size()), username, realmName);
} catch (final Exception e) {
Log.errorf("Could not find user '%s' of realm '%s': %s", username, realmName,
e.getMessage());
}
return null;
}

private void importServiceUserRealmRoleMappings(final Path file, final String realmName,
final UserRepresentation serviceUser) {
final ServiceUserRealmRoleMappingDTO serviceUserRealmRoleMappings =
JsonUtil.loadEntity(file, ServiceUserRealmRoleMappingDTO.class);
final List<String> roles = serviceUserRealmRoleMappings.getRoles();

Log.debugf("Importing realm roles '%s' for service user '%s' of realm '%s'.",
roles.toString(), serviceUser.getUsername(), realmName);
final Map<String, RoleRepresentation> availableRealmRoles = keycloak.realm(realmName)
.roles()
.list()
.stream()
.collect(Collectors.toMap(RoleRepresentation::getName, Function.identity()));
Log.debugf("Found %d roles of realm '%s'.", Integer.valueOf(availableRealmRoles.size()),
realmName);

keycloak.realm(realmName)
.users()
.get(serviceUser.getId())
.roles()
.realmLevel()
.add(roles.stream()
.filter(availableRealmRoles::containsKey)
.map(availableRealmRoles::get)
.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private void mapEntityTypes(final Map<String, Path> typeDirectoryLookup) {
for (final Map.Entry<String, Path> configurationEntry : typeDirectoryLookup.entrySet()) {
final String potentialEntityTypeDirectory = configurationEntry.getKey();
final Path fullEntityTypePath = configurationEntry.getValue();
if (potentialEntityTypeDirectory.contains(entityType.getDirectory())) {
if (compareDirectoryNames(potentialEntityTypeDirectory, entityType)) {
Log.debugf("Reading files from '%s' for type '%s'.", fullEntityTypePath,
entityType);
configurationFiles.computeIfAbsent(entityType,
Expand All @@ -113,6 +113,27 @@ private void mapEntityTypes(final Map<String, Path> typeDirectoryLookup) {
}
}

/**
* Compare the given directory name with the directory name of the given entity type.
*
* @param potentialEntityTypeDirectory
* directory name to compare
* @param entityType
* entity type to compare
* @return true if the directory name contains the directory name of the entity type
*/
private boolean compareDirectoryNames(final String potentialEntityTypeDirectory,
final EntityType entityType) {
// some simple algorithm to compare if a directory matches an entity type,
// e.g. if it is prefixed by number: 1_realms and realms
// the overall length difference should not be that huge to avoid embedded naming
// e.g. client-roles and service-account-client-roles
final int maxNameLengthDiff = 5;
return potentialEntityTypeDirectory.contains(entityType.getDirectory())
&& potentialEntityTypeDirectory.length() - entityType.getDirectory().length()
< maxNameLengthDiff;
}

/**
* List all files in the given directory and its subdirectories that end with '.json'.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.cycrilabs.keycloak.configurator.commands.configure.entity;

import java.util.List;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ServiceUserRealmRoleMappingDTO {
private List<String> roles;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public enum EntityType {
REALM_ROLE(4, "realm-role", "realm-roles"),
SERVICE_ACCOUNT_CLIENT_ROLE(5, "service-account-client-role", "service-account-client-roles"),
GROUP(6, "group", "groups"),
USER(7, "user", "users");
USER(7, "user", "users"),
SERVICE_ACCOUNT_REALM_ROLE(8, "service-account-realm-role", "service-account-realm-roles");

private final int priority;
private final String name;
Expand Down

0 comments on commit 68d2413

Please sign in to comment.