Skip to content

Commit

Permalink
Merged in feature/SHIBUI-1031 (pull request #268)
Browse files Browse the repository at this point in the history
SHIBUI-1031 Implemented admin user dashboard

Approved-by: Ryan Mathis <rmathis@unicon.net>
  • Loading branch information
rmathis authored and Jonathan Johnson committed Jan 11, 2019
2 parents 54aa672 + 40463cd commit f1a96cf
Show file tree
Hide file tree
Showing 60 changed files with 1,993 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,29 @@ class DevConfig {

@Transactional
@PostConstruct
void createDevAdminUsers() {
void createDevUsers() {
if (adminUserRepository.count() == 0) {
def user = new User().with {
def users = [new User().with {
username = 'admin'
password = '{noop}adminpass'
firstName = 'Joe'
lastName = 'Doe'
emailAddress = 'joe@institution.edu'
roles.add(new Role(name: 'ROLE_ADMIN'))
it
}, new User().with {
username = 'nonadmin'
password = '{noop}nonadminpass'
firstName = 'Peter'
lastName = 'Vandelay'
emailAddress = 'peter@institution.edu'
roles.add(new Role(name: 'ROLE_USER'))
it
}]
users.each {
adminUserRepository.save(it)
}

adminUserRepository.save(user)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import edu.internet2.tier.shibboleth.admin.ui.repository.MetadataResolversPositionOrderContainerRepository;
import edu.internet2.tier.shibboleth.admin.ui.scheduled.EntityDescriptorFilesScheduledTasks;
import edu.internet2.tier.shibboleth.admin.ui.scheduled.MetadataProvidersScheduledTasks;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository;
import edu.internet2.tier.shibboleth.admin.ui.security.service.UserRoleService;
import edu.internet2.tier.shibboleth.admin.ui.service.DefaultMetadataResolversPositionOrderContainerService;
import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryService;
import edu.internet2.tier.shibboleth.admin.ui.service.DirectoryServiceImpl;
Expand Down Expand Up @@ -194,4 +196,9 @@ public Module stringTrimModule() {
public ModelRepresentationConversions modelRepresentationConversions() {
return new ModelRepresentationConversions(customPropertiesConfiguration());
}

@Bean
public UserRoleService userRoleService(RoleRepository roleRepository) {
return new UserRoleService(roleRepository);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package edu.internet2.tier.shibboleth.admin.ui.controller;

import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration;
import edu.internet2.tier.shibboleth.admin.ui.security.model.Role;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import java.util.stream.Collectors;

/**
* @author Bill Smith (wsmith@unicon.net)
*/
Expand All @@ -17,8 +21,16 @@ public class ConfigurationController {
@Autowired
CustomPropertiesConfiguration customPropertiesConfiguration;

@Autowired
RoleRepository roleRepository;

@GetMapping(value = "/customAttributes")
public ResponseEntity<?> getCustomAttributes() {
return ResponseEntity.ok(customPropertiesConfiguration.getAttributes());
}

@GetMapping(value = "/supportedRoles")
public ResponseEntity<?> getSupportedRoles() {
return ResponseEntity.ok(roleRepository.findAll().stream().map(Role::getName).collect(Collectors.toList()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,9 @@ public MetadataResolver findResolverOrThrowHttp404(String resolverResourceId) {
return resolver;
}

//TODO: Review this handler and update accordingly. Do we still need it?
@ExceptionHandler(HttpClientErrorException.class)
public ResponseEntity<?> notFoundHandler(HttpClientErrorException ex) {
if(ex.getStatusCode() == NOT_FOUND) {
return ResponseEntity.status(NOT_FOUND).body(ex.getStatusText());
}
throw ex;
return ResponseEntity.status(ex.getStatusCode()).body(new ErrorResponse(ex.getStatusCode().toString(), ex.getStatusText()));
}

@ExceptionHandler(ConstraintViolationException.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package edu.internet2.tier.shibboleth.admin.ui.security.controller;

import edu.internet2.tier.shibboleth.admin.ui.controller.ErrorResponse;
import edu.internet2.tier.shibboleth.admin.ui.security.model.User;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository;
import edu.internet2.tier.shibboleth.admin.ui.security.service.UserRoleService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.HttpClientErrorException;

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

import static org.springframework.http.HttpStatus.NOT_FOUND;

/**
* Implementation of the REST resource endpoints exposing system users.
*
* @author Dmitriy Kopylenko
*/
@RestController
@RequestMapping("/api/admin/users")
public class UsersController {

private static final Logger logger = LoggerFactory.getLogger(UsersController.class);

private UserRepository userRepository;
private RoleRepository roleRepository;
private UserRoleService userRoleService;

public UsersController(UserRepository userRepository, RoleRepository roleRepository, UserRoleService userRoleService) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.userRoleService = userRoleService;
}

@Transactional(readOnly = true)
@GetMapping
public List<User> getAll() {
return userRepository.findAll();
}

@Transactional(readOnly = true)
@GetMapping("/{username}")
public ResponseEntity<?> getOne(@PathVariable String username) {
return ResponseEntity.ok(findUserOrThrowHttp404(username));
}

@Transactional
@DeleteMapping("/{username}")
public ResponseEntity<?> deleteOne(@PathVariable String username) {
User user = findUserOrThrowHttp404(username);
userRepository.delete(user);
return ResponseEntity.noContent().build();
}

@Transactional
@PostMapping
ResponseEntity<?> saveOne(@RequestBody User user) {
Optional<User> persistedUser = userRepository.findByUsername(user.getUsername());
if (persistedUser.isPresent()) {
return ResponseEntity
.status(HttpStatus.CONFLICT)
.body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()),
String.format("A user with username [%s] already exists within the system.", user.getUsername())));
}
//TODO: modify this such that additional encoders can be used
user.setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt()));
userRoleService.updateUserRole(user);
User savedUser = userRepository.save(user);
return ResponseEntity.ok(savedUser);
}

@Transactional
@PatchMapping("/{username}")
ResponseEntity<?> updateOne(@PathVariable(value = "username") String username, @RequestBody User user) {
User persistedUser = findUserOrThrowHttp404(username);
if (StringUtils.isNotBlank(user.getFirstName())) {
persistedUser.setFirstName(user.getFirstName());
}
if (StringUtils.isNotBlank(user.getLastName())) {
persistedUser.setLastName(user.getLastName());
}
if (StringUtils.isNotBlank(user.getEmailAddress())) {
persistedUser.setEmailAddress(user.getEmailAddress());
}
if (StringUtils.isNotBlank(user.getPassword())) {
persistedUser.setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt()));
}
if (StringUtils.isNotBlank(user.getRole())) {
persistedUser.setRole(user.getRole());
userRoleService.updateUserRole(persistedUser);
}
User savedUser = userRepository.save(persistedUser);
return ResponseEntity.ok(savedUser);
}

private User findUserOrThrowHttp404(String username) {
return userRepository.findByUsername(username)
.orElseThrow(() -> new HttpClientErrorException(NOT_FOUND, String.format("User with username [%s] not found", username)));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.internet2.tier.shibboleth.admin.ui.security.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable;
import lombok.EqualsAndHashCode;
import lombok.Getter;
Expand Down Expand Up @@ -28,10 +29,23 @@
@ToString(exclude = "users")
public class Role extends AbstractAuditable {

public Role(String name) {
this.name = name;
}

public Role(String name, int rank) {
this.name = name;
this.rank = rank;
}

@Column(unique = true)
private String name;

@ManyToMany(cascade = CascadeType.ALL, mappedBy = "roles", fetch = FetchType.EAGER)
private int rank;

//Ignore properties annotation here is to prevent stack overflow recursive error during JSON serialization
@JsonIgnoreProperties("roles")
@ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER)
private Set<User> users = new HashSet<>();

}
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package edu.internet2.tier.shibboleth.admin.ui.security.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import edu.internet2.tier.shibboleth.admin.ui.domain.AbstractAuditable;
import lombok.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.apache.commons.lang.StringUtils;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.Transient;
import java.util.HashSet;
import java.util.Set;

Expand Down Expand Up @@ -37,8 +44,24 @@ public class User extends AbstractAuditable {

private String lastName;

@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
@ManyToMany(cascade = CascadeType.ALL)
private String emailAddress;

@Transient
private String role;

@JsonIgnore
@ManyToMany(cascade = CascadeType.PERSIST)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();

public String getRole() {
if (StringUtils.isBlank(this.role)) {
Set<Role> roles = this.getRoles();
if (roles.size() != 1) {
throw new RuntimeException(String.format("User with username [%s] does not have exactly one role!", this.getUsername()));
}
this.role = roles.iterator().next().getName();
}
return this.role;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package edu.internet2.tier.shibboleth.admin.ui.security.service;

import edu.internet2.tier.shibboleth.admin.ui.security.model.Role;
import edu.internet2.tier.shibboleth.admin.ui.security.model.User;
import edu.internet2.tier.shibboleth.admin.ui.security.repository.RoleRepository;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

/**
* @author Bill Smith (wsmith@unicon.net)
*/
public class UserRoleService {

private RoleRepository roleRepository;

public UserRoleService(RoleRepository roleRepository) {
this.roleRepository = roleRepository;
}

/**
* Given a user with a defined User.role, update the User.roles collection with that role.
*
* This currently exists because users should only ever have one role in the system at this time. However, user
* roles are persisted as a set of roles (for future-proofing). Once we start allowing a user to have multiple roles,
* this method and User.role can go away.
* @param user
*/
public void updateUserRole(User user) {
if (StringUtils.isNotBlank(user.getRole())) {
Optional<Role> userRole = roleRepository.findByName(user.getRole());
if (userRole.isPresent()) {
Set<Role> userRoles = new HashSet<>();
userRoles.add(userRole.get());
user.setRoles(userRoles);
} else {
throw new RuntimeException(String.format("User with username [%s] is defined with role [%s] which does not exist in the system!", user.getUsername(), user.getRole()));
}
} else {
throw new RuntimeException(String.format("User with username [%s] has no role defined and therefor cannot be updated!", user.getUsername()));
}
}
}
Loading

0 comments on commit f1a96cf

Please sign in to comment.