From 08a5b49d1d3f034a02f7fb54d863de9e377f1727 Mon Sep 17 00:00:00 2001 From: Thomas Georgios Giannos Date: Fri, 8 Dec 2023 11:48:32 +0200 Subject: [PATCH] Supporting client roles for users on Keycloak API service --- .../eudat/service/keycloak/KeycloakRole.java | 7 +++ .../service/keycloak/KeycloakService.java | 2 + .../service/keycloak/KeycloakServiceImpl.java | 36 ++++++++++-- .../keycloak/MyKeycloakAdminRestApi.java | 48 ++++++++++++++++ .../eu/eudat/service/keycloak/MyModules.java | 11 ++++ .../eudat/service/keycloak/MyUsersModule.java | 57 +++++++++++++++++++ .../eudat/service/user/UserServiceImpl.java | 12 +++- .../src/main/resources/config/keycloak.yml | 12 ++-- 8 files changed, 172 insertions(+), 13 deletions(-) create mode 100644 dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakRole.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyKeycloakAdminRestApi.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyModules.java create mode 100644 dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyUsersModule.java diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakRole.java b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakRole.java new file mode 100644 index 000000000..ced1de75b --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakRole.java @@ -0,0 +1,7 @@ +package eu.eudat.service.keycloak; + +public enum KeycloakRole { + + Admin, DatasetTemplateEditor, Manager, User + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakService.java b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakService.java index fd1784e3e..f92f7d579 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakService.java +++ b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakService.java @@ -16,5 +16,7 @@ public interface KeycloakService { void removeUserFromAdministratorsGroup(@NotNull UUID subjectId); void addUserToTenantAuthorityGroup(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); } diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakServiceImpl.java b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakServiceImpl.java index 04ca067e0..6910ad1c4 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakServiceImpl.java +++ b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/KeycloakServiceImpl.java @@ -4,29 +4,31 @@ import com.google.common.collect.Lists; import eu.eudat.configurations.keycloak.KeycloakResourcesConfiguration; import eu.eudat.data.TenantEntity; import gr.cite.commons.web.keycloak.api.KeycloakAdminRestApi; +import gr.cite.commons.web.keycloak.api.configuration.KeycloakClientConfiguration; import gr.cite.tools.logging.LoggerService; import org.jetbrains.annotations.NotNull; import org.keycloak.representations.idm.GroupRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import java.util.HashMap; -import java.util.List; -import java.util.UUID; +import java.util.*; @Service public class KeycloakServiceImpl implements KeycloakService { 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 KeycloakClientConfiguration clientConfiguration; @Autowired - public KeycloakServiceImpl(KeycloakAdminRestApi api, KeycloakResourcesConfiguration configuration) { + public KeycloakServiceImpl(MyKeycloakAdminRestApi api, KeycloakResourcesConfiguration configuration, KeycloakClientConfiguration clientConfiguration) { this.api = api; this.configuration = configuration; //logger.info("Keycloak service initialized. Tenant authorities configured -> {}", configuration.getProperties().getAuthorities().size()); + this.clientConfiguration = clientConfiguration; } @Override @@ -80,6 +82,30 @@ public class KeycloakServiceImpl implements KeycloakService { 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 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 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 getUserGroups(UUID subjectId) { return api.users().getGroups(subjectId.toString()); } diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyKeycloakAdminRestApi.java b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyKeycloakAdminRestApi.java new file mode 100644 index 000000000..179bdf64a --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyKeycloakAdminRestApi.java @@ -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(); + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyModules.java b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyModules.java new file mode 100644 index 000000000..657065304 --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyModules.java @@ -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); + } + +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyUsersModule.java b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyUsersModule.java new file mode 100644 index 000000000..4f3da992a --- /dev/null +++ b/dmp-backend/core/src/main/java/eu/eudat/service/keycloak/MyUsersModule.java @@ -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 getUsers() { + return this.realm.users().list(); + } + + public UserRepresentation findUserById(String id) { + return this.realm.users().get(id).toRepresentation(); + } + + public List 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 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); + } +} diff --git a/dmp-backend/core/src/main/java/eu/eudat/service/user/UserServiceImpl.java b/dmp-backend/core/src/main/java/eu/eudat/service/user/UserServiceImpl.java index 387d4abe8..4fffac9fe 100644 --- a/dmp-backend/core/src/main/java/eu/eudat/service/user/UserServiceImpl.java +++ b/dmp-backend/core/src/main/java/eu/eudat/service/user/UserServiceImpl.java @@ -25,6 +25,8 @@ import eu.eudat.model.persist.UserPersist; import eu.eudat.model.persist.UserRolePatchPersist; import eu.eudat.query.UserQuery; 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.tools.data.builder.BuilderFactory; import gr.cite.tools.data.deleter.DeleterFactory; @@ -80,6 +82,9 @@ public class UserServiceImpl implements UserService { private final JsonHandlingService jsonHandlingService; private final QueryFactory queryFactory; private final UserScope userScope; + + private final KeycloakService keycloakService; + @Autowired public UserServiceImpl( EntityManager entityManager, @@ -91,8 +96,8 @@ public class UserServiceImpl implements UserService { MessageSource messageSource, EventBroker eventBroker, JsonHandlingService jsonHandlingService, - QueryFactory queryFactory, - UserScope userScope) { + QueryFactory queryFactory, + UserScope userScope, KeycloakService keycloakService) { this.entityManager = entityManager; this.authorizationService = authorizationService; this.deleterFactory = deleterFactory; @@ -104,6 +109,7 @@ public class UserServiceImpl implements UserService { this.jsonHandlingService = jsonHandlingService; this.queryFactory = queryFactory; this.userScope = userScope; + this.keycloakService = keycloakService; } //region persist @@ -211,6 +217,7 @@ public class UserServiceImpl implements UserService { item.setRole(roleName); item.setCreatedAt(Instant.now()); this.entityManager.persist(item); + this.keycloakService.assignClientRoleToUser(data.getId(), null, KeycloakRole.valueOf(roleName)); } foundIds.add(item.getId()); } @@ -218,6 +225,7 @@ public class UserServiceImpl implements UserService { this.entityManager.flush(); List 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.entityManager.flush(); diff --git a/dmp-backend/web/src/main/resources/config/keycloak.yml b/dmp-backend/web/src/main/resources/config/keycloak.yml index f62a50749..3916f6454 100644 --- a/dmp-backend/web/src/main/resources/config/keycloak.yml +++ b/dmp-backend/web/src/main/resources/config/keycloak.yml @@ -1,10 +1,10 @@ keycloak-client: - serverUrl: ${KEYCLOAK_API_SERVER_URL} - realm: ${KEYCLOAK_API_REALM} - username: ${KEYCLOAK_API_USERNAME} - password: ${KEYCLOAK_API_PASSWORD} - clientId: ${KEYCLOAK_API_CLIENT_ID} - clientSecret: ${KEYCLOAK_API_CLIENT_SECRET} + serverUrl: ${KEYCLOAK_API_SERVER_URL:} + realm: ${KEYCLOAK_API_REALM:} + username: ${KEYCLOAK_API_USERNAME:} + password: ${KEYCLOAK_API_PASSWORD:} + clientId: ${KEYCLOAK_API_CLIENT_ID:} + clientSecret: ${KEYCLOAK_API_CLIENT_SECRET:} keycloak-resources: authorities: null