Supporting client roles for users on Keycloak API service

This commit is contained in:
Thomas Georgios Giannos 2023-12-08 11:48:32 +02:00
parent 0c6e800118
commit 08a5b49d1d
8 changed files with 172 additions and 13 deletions

View File

@ -0,0 +1,7 @@
package eu.eudat.service.keycloak;
public enum KeycloakRole {
Admin, DatasetTemplateEditor, Manager, User
}

View File

@ -16,5 +16,7 @@ public interface KeycloakService {
void removeUserFromAdministratorsGroup(@NotNull UUID subjectId); void removeUserFromAdministratorsGroup(@NotNull UUID subjectId);
void addUserToTenantAuthorityGroup(UUID subjectId, TenantEntity tenant, String key); void addUserToTenantAuthorityGroup(UUID subjectId, TenantEntity tenant, String key);
void removeUserFromTenantAuthorityGroup(UUID subjectId, TenantEntity tenant, String key); void removeUserFromTenantAuthorityGroup(UUID subjectId, TenantEntity tenant, String key);
void assignClientRoleToUser(UUID subjectId, String clientId, KeycloakRole role);
void removeClientRoleFromUser(UUID subjectId, String clientId, KeycloakRole role);
} }

View File

@ -4,29 +4,31 @@ import com.google.common.collect.Lists;
import eu.eudat.configurations.keycloak.KeycloakResourcesConfiguration; import eu.eudat.configurations.keycloak.KeycloakResourcesConfiguration;
import eu.eudat.data.TenantEntity; import eu.eudat.data.TenantEntity;
import gr.cite.commons.web.keycloak.api.KeycloakAdminRestApi; import gr.cite.commons.web.keycloak.api.KeycloakAdminRestApi;
import gr.cite.commons.web.keycloak.api.configuration.KeycloakClientConfiguration;
import gr.cite.tools.logging.LoggerService; import gr.cite.tools.logging.LoggerService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.keycloak.representations.idm.GroupRepresentation; import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.HashMap; import java.util.*;
import java.util.List;
import java.util.UUID;
@Service @Service
public class KeycloakServiceImpl implements KeycloakService { public class KeycloakServiceImpl implements KeycloakService {
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(KeycloakServiceImpl.class)); private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(KeycloakServiceImpl.class));
private final KeycloakAdminRestApi api; private final MyKeycloakAdminRestApi api;
private final KeycloakResourcesConfiguration configuration; private final KeycloakResourcesConfiguration configuration;
private final KeycloakClientConfiguration clientConfiguration;
@Autowired @Autowired
public KeycloakServiceImpl(KeycloakAdminRestApi api, KeycloakResourcesConfiguration configuration) { public KeycloakServiceImpl(MyKeycloakAdminRestApi api, KeycloakResourcesConfiguration configuration, KeycloakClientConfiguration clientConfiguration) {
this.api = api; this.api = api;
this.configuration = configuration; this.configuration = configuration;
//logger.info("Keycloak service initialized. Tenant authorities configured -> {}", configuration.getProperties().getAuthorities().size()); //logger.info("Keycloak service initialized. Tenant authorities configured -> {}", configuration.getProperties().getAuthorities().size());
this.clientConfiguration = clientConfiguration;
} }
@Override @Override
@ -80,6 +82,30 @@ public class KeycloakServiceImpl implements KeycloakService {
removeUserFromGroup(subjectId, group.getId()); removeUserFromGroup(subjectId, group.getId());
} }
@Override
public void assignClientRoleToUser(UUID subjectId, String clientId, KeycloakRole role) {
if (clientId == null) clientId = clientConfiguration.getProperties().getClientId();
UserRepresentation user = api.users().findUserById(subjectId.toString());
user.getClientRoles().computeIfAbsent(clientId, k -> Lists.newArrayList());
Set<String> clientRoles = new HashSet<>(Set.copyOf(user.getClientRoles().get(clientId)));
clientRoles.add(role.name());
user.getClientRoles().get(clientId).clear();
user.getClientRoles().get(clientId).addAll(clientRoles);
api.users().updateUser(subjectId.toString(), user);
}
@Override
public void removeClientRoleFromUser(UUID subjectId, String clientId, KeycloakRole role) {
if (clientId == null) clientId = clientConfiguration.getProperties().getClientId();
UserRepresentation user = api.users().findUserById(subjectId.toString());
user.getClientRoles().computeIfAbsent(clientId, k -> Lists.newArrayList());
Set<String> clientRoles = new HashSet<>(Set.copyOf(user.getClientRoles().get(clientId)));
clientRoles.remove(role.name());
user.getClientRoles().get(clientId).clear();
user.getClientRoles().get(clientId).addAll(clientRoles);
api.users().updateUser(subjectId.toString(), user);
}
public List<GroupRepresentation> getUserGroups(UUID subjectId) { public List<GroupRepresentation> getUserGroups(UUID subjectId) {
return api.users().getGroups(subjectId.toString()); return api.users().getGroups(subjectId.toString());
} }

View File

@ -0,0 +1,48 @@
package eu.eudat.service.keycloak;
import gr.cite.commons.web.keycloak.api.configuration.KeycloakClientConfiguration;
import gr.cite.commons.web.keycloak.api.modules.ClientsModule;
import gr.cite.commons.web.keycloak.api.modules.GroupsModule;
import gr.cite.commons.web.keycloak.api.modules.Modules;
import gr.cite.tools.logging.LoggerService;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.admin.client.resource.RealmResource;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class MyKeycloakAdminRestApi {
private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(MyKeycloakAdminRestApi.class));
private final KeycloakClientConfiguration configuration;
protected RealmResource realm;
@Autowired
public MyKeycloakAdminRestApi(KeycloakClientConfiguration configuration) {
this.configuration = configuration;
try (Keycloak client = this.initClient()) {
this.realm = client.realm(configuration.getProperties().getRealm());
}
logger.info("Custom Keycloak client initialized. Keycloak serving from {}", configuration.getProperties().getServerUrl().replace("/auth", "").replaceAll("https?://", ""));
}
public MyUsersModule users() {
return MyModules.getUsersModule(this.realm);
}
public GroupsModule groups() {
return Modules.getGroupsModule(this.realm);
}
public ClientsModule clients() {
return Modules.getClientsModule(this.realm);
}
private Keycloak initClient() {
return KeycloakBuilder.builder().serverUrl(this.configuration.getProperties().getServerUrl()).realm(this.configuration.getProperties().getRealm()).username(this.configuration.getProperties().getUsername()).password(this.configuration.getProperties().getPassword()).clientId(this.configuration.getProperties().getClientId()).clientSecret(this.configuration.getProperties().getClientSecret()).grantType("password").build();
}
}

View File

@ -0,0 +1,11 @@
package eu.eudat.service.keycloak;
import org.keycloak.admin.client.resource.RealmResource;
public class MyModules {
public static MyUsersModule getUsersModule(RealmResource realm) {
return new MyUsersModule(realm);
}
}

View File

@ -0,0 +1,57 @@
package eu.eudat.service.keycloak;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.List;
public class MyUsersModule {
private final RealmResource realm;
MyUsersModule(RealmResource realm) {
this.realm = realm;
}
public List<UserRepresentation> getUsers() {
return this.realm.users().list();
}
public UserRepresentation findUserById(String id) {
return this.realm.users().get(id).toRepresentation();
}
public List<UserRepresentation> findUsersByUsername(String username) {
return this.realm.users().search(username);
}
public UserRepresentation findUserByUsername(String username) {
return this.realm.users().search(username, true).stream().findFirst().orElse(null);
}
public UserRepresentation addUser(UserRepresentation user) {
return this.realm.users().create(user).readEntity(UserRepresentation.class);
}
public void addUserToGroup(String userId, String groupId) {
this.realm.users().get(userId).joinGroup(groupId);
}
public void removeUserFromGroup(String userId, String groupId) {
this.realm.users().get(userId).leaveGroup(groupId);
}
public List<GroupRepresentation> getGroups(String userId) {
return this.realm.users().get(userId).groups();
}
public void updateUser(String userId, UserRepresentation user) {
UserRepresentation existing = this.realm.users().get(userId).toRepresentation();
existing.setFirstName(user.getFirstName());
existing.setLastName(user.getLastName());
existing.setEnabled(user.isEnabled());
existing.setAttributes(user.getAttributes());
existing.setClientRoles(user.getClientRoles());
this.realm.users().get(userId).update(existing);
}
}

View File

@ -25,6 +25,8 @@ import eu.eudat.model.persist.UserPersist;
import eu.eudat.model.persist.UserRolePatchPersist; import eu.eudat.model.persist.UserRolePatchPersist;
import eu.eudat.query.UserQuery; import eu.eudat.query.UserQuery;
import eu.eudat.query.UserRoleQuery; import eu.eudat.query.UserRoleQuery;
import eu.eudat.service.keycloak.KeycloakRole;
import eu.eudat.service.keycloak.KeycloakService;
import gr.cite.commons.web.authz.service.AuthorizationService; import gr.cite.commons.web.authz.service.AuthorizationService;
import gr.cite.tools.data.builder.BuilderFactory; import gr.cite.tools.data.builder.BuilderFactory;
import gr.cite.tools.data.deleter.DeleterFactory; import gr.cite.tools.data.deleter.DeleterFactory;
@ -80,6 +82,9 @@ public class UserServiceImpl implements UserService {
private final JsonHandlingService jsonHandlingService; private final JsonHandlingService jsonHandlingService;
private final QueryFactory queryFactory; private final QueryFactory queryFactory;
private final UserScope userScope; private final UserScope userScope;
private final KeycloakService keycloakService;
@Autowired @Autowired
public UserServiceImpl( public UserServiceImpl(
EntityManager entityManager, EntityManager entityManager,
@ -92,7 +97,7 @@ public class UserServiceImpl implements UserService {
EventBroker eventBroker, EventBroker eventBroker,
JsonHandlingService jsonHandlingService, JsonHandlingService jsonHandlingService,
QueryFactory queryFactory, QueryFactory queryFactory,
UserScope userScope) { UserScope userScope, KeycloakService keycloakService) {
this.entityManager = entityManager; this.entityManager = entityManager;
this.authorizationService = authorizationService; this.authorizationService = authorizationService;
this.deleterFactory = deleterFactory; this.deleterFactory = deleterFactory;
@ -104,6 +109,7 @@ public class UserServiceImpl implements UserService {
this.jsonHandlingService = jsonHandlingService; this.jsonHandlingService = jsonHandlingService;
this.queryFactory = queryFactory; this.queryFactory = queryFactory;
this.userScope = userScope; this.userScope = userScope;
this.keycloakService = keycloakService;
} }
//region persist //region persist
@ -211,6 +217,7 @@ public class UserServiceImpl implements UserService {
item.setRole(roleName); item.setRole(roleName);
item.setCreatedAt(Instant.now()); item.setCreatedAt(Instant.now());
this.entityManager.persist(item); this.entityManager.persist(item);
this.keycloakService.assignClientRoleToUser(data.getId(), null, KeycloakRole.valueOf(roleName));
} }
foundIds.add(item.getId()); foundIds.add(item.getId());
} }
@ -218,6 +225,7 @@ public class UserServiceImpl implements UserService {
this.entityManager.flush(); this.entityManager.flush();
List<UserRoleEntity> toDelete = existingItems.stream().filter(x-> foundIds.stream().noneMatch(y-> y.equals(x.getId()))).collect(Collectors.toList()); List<UserRoleEntity> toDelete = existingItems.stream().filter(x-> foundIds.stream().noneMatch(y-> y.equals(x.getId()))).collect(Collectors.toList());
toDelete.forEach(x -> this.keycloakService.removeClientRoleFromUser(data.getId(), null, KeycloakRole.valueOf(x.getRole())));
this.deleterFactory.deleter(UserRoleDeleter.class).deleteAndSave(toDelete); this.deleterFactory.deleter(UserRoleDeleter.class).deleteAndSave(toDelete);
this.entityManager.flush(); this.entityManager.flush();

View File

@ -1,10 +1,10 @@
keycloak-client: keycloak-client:
serverUrl: ${KEYCLOAK_API_SERVER_URL} serverUrl: ${KEYCLOAK_API_SERVER_URL:}
realm: ${KEYCLOAK_API_REALM} realm: ${KEYCLOAK_API_REALM:}
username: ${KEYCLOAK_API_USERNAME} username: ${KEYCLOAK_API_USERNAME:}
password: ${KEYCLOAK_API_PASSWORD} password: ${KEYCLOAK_API_PASSWORD:}
clientId: ${KEYCLOAK_API_CLIENT_ID} clientId: ${KEYCLOAK_API_CLIENT_ID:}
clientSecret: ${KEYCLOAK_API_CLIENT_SECRET} clientSecret: ${KEYCLOAK_API_CLIENT_SECRET:}
keycloak-resources: keycloak-resources:
authorities: null authorities: null