Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to KeycloakUtil #330

Merged
merged 5 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,18 @@
*/
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 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 @@ -50,16 +39,7 @@ public class LoginListener implements ApplicationListener<InteractiveAuthenticat
protected UserRepository userRepository;

@Autowired
protected GroupRepository groupRepository;

@Autowired
private KeycloakUtil keycloakUtil;

@Autowired
private ApplicationEventPublisher eventPublisher;

@Autowired
private UserInstancePermissionService userInstancePermissionService;
protected KeycloakUtil keycloakUtil;

@Override
@Transactional
Expand All @@ -75,29 +55,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);
}
});
keycloakUtil.createOrGetShogunUser(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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ 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;
}

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

Expand Down
178 changes: 153 additions & 25 deletions shogun-lib/src/main/java/de/terrestris/shogun/lib/util/KeycloakUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,30 @@
*/
package de.terrestris.shogun.lib.util;

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 lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.StringUtils;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.admin.client.CreatedResponseUtil;
import org.keycloak.admin.client.resource.*;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
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.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@Log4j2
Expand All @@ -45,11 +49,34 @@ public class KeycloakUtil {
@Autowired
protected RealmResource keycloakRealm;

@Autowired
protected UserRepository userRepository;

@Autowired
protected GroupRepository groupRepository;

@Autowired
private KeycloakUtil keycloakUtil;

@Autowired
private SecurityContextUtil securityContextUtil;

@Autowired
private ApplicationEventPublisher eventPublisher;

@Autowired
private UserInstancePermissionService userInstancePermissionService;

public UserResource getUserResource(User user) {
UsersResource kcUsers = this.keycloakRealm.users();
return kcUsers.get(user.getKeycloakId());
}

public UserResource getUserResource(String id) {
UsersResource kcUsers = this.keycloakRealm.users();
return kcUsers.get(id);
}

public GroupResource getGroupResource(Group group) {
GroupsResource kcGroups = this.keycloakRealm.groups();
return kcGroups.group(group.getKeycloakId());
Expand Down Expand Up @@ -153,33 +180,42 @@ public boolean isUserInGroup(User user, Group group) {
}

/**
* Return keycloak user id from {@link Authentication} object
* - from {@link IDToken}
* - from {@link org.keycloak.Token}
* @param authentication The Spring security authentication
* @return The keycloak user id token
* @deprecated
* This method was moved to the {@link SecurityContextUtil} as it receives an authentication as parameter.
*/
@Deprecated
public String getKeycloakUserIdFromAuthentication(Authentication authentication) {
if (authentication.getPrincipal() instanceof KeycloakPrincipal) {
KeycloakPrincipal keycloakPrincipal = (KeycloakPrincipal) authentication.getPrincipal();
KeycloakSecurityContext keycloakSecurityContext = keycloakPrincipal.getKeycloakSecurityContext();
IDToken idToken = keycloakSecurityContext.getIdToken();
String keycloakUserId;

if (idToken != null) {
keycloakUserId = idToken.getSubject();
} else {
AccessToken accessToken = keycloakSecurityContext.getToken();
keycloakUserId = accessToken.getSubject();
}
return securityContextUtil.getKeycloakUserIdFromAuthentication(authentication);
}

return keycloakUserId;
} else {
return null;
}
/**
* Fetch user name of user from keycloak
* @param user
* @return
*/
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());
}

/**
* @deprecated
* Renamed to `getKeycloakUserGroups`.
*/
@Deprecated
public List<GroupRepresentation> getUserGroups(User user) {
return this.getKeycloakUserGroups(user);
};

/**
* Get the Keycloak GroupRepresentations from a user instance.
*
* @param user
* @return
*/
public List<GroupRepresentation> getKeycloakUserGroups(User user) {
UserResource userResource = this.getUserResource(user);
List<GroupRepresentation> groups = new ArrayList<>();

Expand All @@ -195,4 +231,96 @@ public List<GroupRepresentation> getUserGroups(User user) {
return groups;
}

/**
* Checks if a passed user with the passed keycloak ID exists in the SHOGun DB and creates it if not.
*
* The groups of the user are also checked and created if needed.
*
* @param keycloakUserId
* @return
*/
public User createOrGetShogunUser(String keycloakUserId) {
Optional<User> userOptional = userRepository.findByKeycloakId(keycloakUserId);
User user = userOptional.orElse(null);

// User is not yet it SHOGun DB
if (user == null) {
user = new User(keycloakUserId, null, null, null);
userRepository.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);
return user;
}

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

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

return user;
}

/**
* Delete a user from the SHOGun DB by its keycloak Id.
*
* @param keycloakUserId
*/
public void deleteShogunUser(String keycloakUserId) {
Optional<User> userOptional = userRepository.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);
userRepository.delete(user);
log.info("User with keycloak id {} was deleted in Keycloak and was therefore deleted in SHOGun DB, too.", keycloakUserId);
}

/**
* Checks if a group with the passed keycloak ID exists in the SHOGun DB and creates it if not.
*
* @param keycloakGroupId
* @return
*/
public Group createOrGetShogunGroup(String keycloakGroupId) {
Optional<Group> groupOptional = groupRepository.findByKeycloakId(keycloakGroupId);
Group group = groupOptional.orElse(null);

if (group == null) {
group = new Group(keycloakGroupId, null);
groupRepository.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
*/
public void deleteShogunGroup(String keycloakGroupId) {
Optional<Group> groupOptional = groupRepository.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;
}
groupRepository.delete(group);
log.info("Group with keycloak id {} was deleted in Keycloak and was therefore deleted in SHOGun DB, too.", keycloakGroupId);
}

}