Skip to content

Commit

Permalink
Updates to KeycloakUtil (#330)
Browse files Browse the repository at this point in the history
* refactors Keycloak- and SecurityContextUtil
* introduces findOrCreateByKeycloakId and deleteByKeycloakId methods
  • Loading branch information
KaiVolland authored Jun 1, 2021
1 parent 4a1ce04 commit dcb7a09
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 98 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,18 @@
import de.terrestris.shogun.lib.model.User;
import de.terrestris.shogun.lib.service.GroupService;
import de.terrestris.shogun.lib.service.UserService;
import java.util.List;
import java.util.Optional;
import de.terrestris.shogun.lib.util.KeycloakUtil;
import org.keycloak.representations.idm.GroupRepresentation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/groups")
@ConditionalOnExpression("${controller.groups.enabled:true}")
Expand All @@ -42,6 +40,9 @@ public class GroupController extends BaseController<GroupService, Group> {
@Autowired
private UserService userService;

@Autowired
private KeycloakUtil keycloakUtil;

@GetMapping("/user/{id}")
@ResponseStatus(HttpStatus.OK)
public List<Group> findByUser(@PathVariable("id") Long userId) {
Expand Down Expand Up @@ -74,7 +75,7 @@ public List<Group> findByUser(@PathVariable("id") Long userId) {
@ResponseStatus(HttpStatus.OK)
public GroupRepresentation findByKeycloakId(@PathVariable("id") String keycloakId) {
try {
return service.findByKeycloakId(keycloakId);
return keycloakUtil.getGroupResource(keycloakId).toRepresentation();
} catch (Exception e) {
LOG.error("Error while requesting keycloak group with ID {}: \n {}",
keycloakId, e.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,17 @@
*/
package de.terrestris.shogun.lib.listener;

import de.terrestris.shogun.lib.enumeration.PermissionCollectionType;
import de.terrestris.shogun.lib.event.OnRegistrationConfirmedEvent;
import de.terrestris.shogun.lib.model.Group;
import de.terrestris.shogun.lib.model.User;
import de.terrestris.shogun.lib.repository.GroupRepository;
import de.terrestris.shogun.lib.repository.UserRepository;
import de.terrestris.shogun.lib.security.SecurityContextUtil;
import de.terrestris.shogun.lib.service.security.permission.UserInstancePermissionService;
import de.terrestris.shogun.lib.util.KeycloakUtil;
import de.terrestris.shogun.lib.service.UserService;
import lombok.extern.log4j.Log4j2;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.representations.idm.GroupRepresentation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Optional;

@Component
@Log4j2
public class LoginListener implements ApplicationListener<InteractiveAuthenticationSuccessEvent> {
Expand All @@ -47,19 +35,7 @@ public class LoginListener implements ApplicationListener<InteractiveAuthenticat
protected SecurityContextUtil securityContextUtil;

@Autowired
protected UserRepository userRepository;

@Autowired
protected GroupRepository groupRepository;

@Autowired
private KeycloakUtil keycloakUtil;

@Autowired
private ApplicationEventPublisher eventPublisher;

@Autowired
private UserInstancePermissionService userInstancePermissionService;
protected UserService userService;

@Override
@Transactional
Expand All @@ -75,29 +51,6 @@ public synchronized void onApplicationEvent(InteractiveAuthenticationSuccessEven
String keycloakUserId = SecurityContextUtil.getKeycloakUserIdFromAuthentication(authentication);

// Add missing user to shogun db
Optional<User> userOptional = userRepository.findByKeycloakId(keycloakUserId);
User user = userOptional.orElse(null);
if (user == null) {
user = new User(keycloakUserId, null, null, null);
userRepository.save(user);

// Add admin instance permissions for the user.
userInstancePermissionService.setPermission(user, user, PermissionCollectionType.ADMIN);

// If the user doesn't exist, we assume it's the first login after registration.
eventPublisher.publishEvent(new OnRegistrationConfirmedEvent(user));
}

List<GroupRepresentation> userGroups = securityContextUtil.getKeycloakGroupsForUser(user);

// Add missing groups to shogun db
userGroups.stream().map(GroupRepresentation::getId).forEach(keycloakGroupId -> {
Optional<Group> group = groupRepository.findByKeycloakId(keycloakGroupId);
if (group.isEmpty()) {
Group newGroup = new Group();
newGroup.setKeycloakId(keycloakGroupId);
groupRepository.save(newGroup);
}
});
userService.findOrCreateByKeyCloakId(keycloakUserId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.admin.client.resource.UsersResource;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.idm.GroupRepresentation;
Expand Down Expand Up @@ -184,26 +183,21 @@ public List<Group> getGroupsForUser() {
}

/**
* Return keycloak GroupRepresentaions (groups) for user
* @param user
* @return
* @deprecated
* This method was moved to the {@link KeycloakUtil} as its not related to security or authentication.
*/
@Deprecated
public List<GroupRepresentation> getKeycloakGroupsForUser(User user) {
UsersResource users = this.keycloakRealm.users();
UserResource kcUser = users.get(user.getKeycloakId());
return kcUser.groups();
return keycloakUtil.getKeycloakUserGroups(user);
}

/**
* Fetch user name of user from keycloak
* @param user
* @return
* @deprecated
* This method was moved to the {@link KeycloakUtil} as its not related to security or authentication.
*/
@Deprecated
public String getUserNameFromKeycloak(User user) {
UsersResource users = this.keycloakRealm.users();
UserResource kcUser = users.get(user.getKeycloakId());
UserRepresentation kcUserRepresentation = kcUser.toRepresentation();
return String.format("%s %s", kcUserRepresentation.getFirstName(), kcUserRepresentation.getLastName());
return keycloakUtil.getUserNameFromKeycloak(user);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ public Optional<Group> findOne(Long id) {
public List<Group> findByUser(User user) {
List<Group> groups = new ArrayList<>();

List<GroupRepresentation> keycloakGroups = keycloakUtil.getUserGroups(user);
List<GroupRepresentation> keycloakGroups = keycloakUtil.getKeycloakUserGroups(user);

for (GroupRepresentation keycloakGroup : keycloakGroups) {
Optional<Group> group = repository.findByKeycloakId(keycloakGroup.getId());
Expand All @@ -101,6 +101,10 @@ public List<Group> findByUser(User user) {
return groups;
}

/**
* @deprecated Use KeycloakUtil instead
*/
@Deprecated
public GroupRepresentation findByKeycloakId(String keycloakId) {
GroupResource groupResource = keycloakUtil.getGroupResource(keycloakId);

Expand All @@ -123,6 +127,47 @@ public List<User> getGroupMembers(String id) {
return users;
}

/**
* Finds a Group by the passed keycloak ID. If it does not exists in the SHOGun DB it gets created.
*
* @param keycloakGroupId
* @return
*/
@Transactional
// @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#keycloakGroupId, 'CREATE')")
public Group findOrCreateByKeycloakId(String keycloakGroupId) {
Optional<Group> groupOptional = repository.findByKeycloakId(keycloakGroupId);
Group group = groupOptional.orElse(null);

if (group == null) {
group = new Group(keycloakGroupId, null);
repository.save(group);

LOG.info("Group with keycloak id {} did not yet exist in the SHOGun DB and was therefore created.", keycloakGroupId);
return group;
}

return group;
}

/**
* Delete a group from the SHOGun DB by its keycloak Id.
*
* @param keycloakGroupId
*/
@Transactional
// @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#keycloakGroupId, 'DELETE')")
public void deleteByKeycloakId(String keycloakGroupId) {
Optional<Group> groupOptional = repository.findByKeycloakId(keycloakGroupId);
Group group = groupOptional.orElse(null);
if (group == null) {
LOG.debug("Group with keycloak id {} was deleted in Keycloak. It did not exists in SHOGun DB. No action needed.", keycloakGroupId);
return;
}
repository.delete(group);
LOG.info("Group with keycloak id {} was deleted in Keycloak and was therefore deleted in SHOGun DB, too.", keycloakGroupId);
}

private Group setTransientKeycloakRepresentations(Group group) {
GroupResource groupResource = keycloakUtil.getGroupResource(group);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@
*/
package de.terrestris.shogun.lib.service;

import de.terrestris.shogun.lib.enumeration.PermissionCollectionType;
import de.terrestris.shogun.lib.event.OnRegistrationConfirmedEvent;
import de.terrestris.shogun.lib.model.User;
import de.terrestris.shogun.lib.repository.UserRepository;
import de.terrestris.shogun.lib.util.KeycloakUtil;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
Expand All @@ -37,6 +41,12 @@ public class UserService extends BaseService<UserRepository, User> {
@Autowired
KeycloakUtil keycloakUtil;

@Autowired
GroupService groupService;

@Autowired
ApplicationEventPublisher eventPublisher;

@PostFilter("hasRole('ROLE_ADMIN') or hasPermission(filterObject, 'READ')")
@Transactional(readOnly = true)
@Override
Expand Down Expand Up @@ -76,6 +86,79 @@ public Optional<User> findOne(Long id) {
return user;
}

@PostAuthorize("hasRole('ROLE_ADMIN') or hasPermission(returnObject.orElse(null), 'READ')")
@Transactional(readOnly = true)
public Optional<User> findByKeyCloakId(String keycloakId) {
Optional<User> user = repository.findByKeycloakId(keycloakId);

if (user.isPresent()) {
this.setTransientKeycloakRepresentations(user.get());
}

return user;
}

/**
* Finds a User by the passed keycloak ID. If it does not exists in the SHOGun DB it gets created.
*
* The groups of the user are also checked and created if needed.
*
* @param keycloakUserId
* @return
*/
// @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#keycloakUserId, 'CREATE')")
@Transactional
public User findOrCreateByKeyCloakId(String keycloakUserId) {
Optional<User> userOptional = repository.findByKeycloakId(keycloakUserId);
User user = userOptional.orElse(null);

// User is not yet it SHOGun DB
if (user == null) {
user = new User(keycloakUserId, null, null, null);
repository.save(user);

// If the user doesn't exist, we assume it's the first login after registration.
eventPublisher.publishEvent(new OnRegistrationConfirmedEvent(user));

// Add admin instance permissions for the user.
userInstancePermissionService.setPermission(user, user, PermissionCollectionType.ADMIN);

LOG.info("User with keycloak id {} did not yet exist in the SHOGun DB and was therefore created.", keycloakUserId);
this.setTransientKeycloakRepresentations(user);
return user;
}

List<GroupRepresentation> keycloakUserGroups = keycloakUtil.getKeycloakUserGroups(user);

// Add missing groups to shogun db
keycloakUserGroups
.stream()
.map(GroupRepresentation::getId)
.forEach(groupService::findOrCreateByKeycloakId);

this.setTransientKeycloakRepresentations(user);
return user;
}

/**
* Delete a user from the SHOGun DB by its keycloak Id.
*
* @param keycloakUserId
*/
@Transactional
// @PreAuthorize("hasRole('ROLE_ADMIN') or hasPermission(#keycloakUserId, 'DELETE')")
public void deleteByKeycloakId(String keycloakUserId) {
Optional<User> userOptional = repository.findByKeycloakId(keycloakUserId);
User user = userOptional.orElse(null);
if (user == null) {
LOG.debug("User with keycloak id {} was deleted in Keycloak. It did not exists in SHOGun DB. No action needed.", keycloakUserId);
return;
}
userInstancePermissionService.deleteAllForEntity(user);
repository.delete(user);
LOG.info("User with keycloak id {} was deleted in Keycloak and was therefore deleted in SHOGun DB, too.", keycloakUserId);
}

private User setTransientKeycloakRepresentations(User user) {
UserResource userResource = keycloakUtil.getUserResource(user);

Expand Down
Loading

0 comments on commit dcb7a09

Please sign in to comment.