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 all 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 @@ -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