diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy index dcf255601..a137526a4 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/configuration/DevConfig.groovy @@ -71,12 +71,12 @@ class DevConfig { emailAddress = 'peter@institution.edu' roles.add(roleRepository.findByName('ROLE_USER').get()) it - }, new User().with { - username = 'admin2' - password = '{noop}anotheradmin' - firstName = 'Rand' - lastName = 'al\'Thor' - emailAddress = 'rand@institution.edu' + }, new User().with { // allow us to auto-login as an admin + username = 'anonymousUser' + password = '{noop}anonymous' + firstName = 'Anon' + lastName = 'Ymous' + emailAddress = 'anon@institution.edu' roles.add(roleRepository.findByName('ROLE_ADMIN').get()) it }] diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy index d138f3a57..6fa503fcc 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/MetadataSourcesUiDefinitionController.groovy @@ -45,6 +45,7 @@ class MetadataSourcesUiDefinitionController { ResponseEntity getUiDefinitionJsonSchema() { try { def parsedJson = jacksonObjectMapper.readValue(this.jsonSchemaLocation.url, Map) + jsonSchemaBuilderService.hideServiceEnabledFromNonAdmins(parsedJson) jsonSchemaBuilderService.addReleaseAttributesToJson(parsedJson['properties']['attributeRelease']['widget']) jsonSchemaBuilderService.addRelyingPartyOverridesToJson(parsedJson['properties']['relyingPartyOverrides']) jsonSchemaBuilderService.addRelyingPartyOverridesCollectionDefinitionsToJson(parsedJson["definitions"]) diff --git a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy index b98bcab26..90e2d12e0 100644 --- a/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy +++ b/backend/src/main/groovy/edu/internet2/tier/shibboleth/admin/ui/service/JsonSchemaBuilderService.groovy @@ -1,6 +1,8 @@ package edu.internet2.tier.shibboleth.admin.ui.service import edu.internet2.tier.shibboleth.admin.ui.configuration.CustomPropertiesConfiguration +import edu.internet2.tier.shibboleth.admin.ui.security.model.User +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService import org.springframework.beans.factory.annotation.Autowired /** @@ -11,6 +13,12 @@ class JsonSchemaBuilderService { @Autowired CustomPropertiesConfiguration customPropertiesConfiguration + UserService userService + + JsonSchemaBuilderService(UserService userService) { + this.userService = userService + } + void addReleaseAttributesToJson(Object json) { json['data'] = customPropertiesConfiguration.getAttributes().collect { [key: it['name'], label: it['displayName']] @@ -62,4 +70,15 @@ class JsonSchemaBuilderService { json[(String) it['name']] = definition } } + + void hideServiceEnabledFromNonAdmins(Map json) { + User currentUser = userService.getCurrentUser() + if (currentUser != null && currentUser.role != 'ROLE_ADMIN') { + // user isn't an admin, so hide 'ServiceEnabled' + Map serviceEnabled = (HashMap) json['properties']['serviceEnabled'] + serviceEnabled['widget'] = 'hidden' + serviceEnabled.remove('title') + serviceEnabled.remove('description') + } + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java index b9679866d..8f964b96a 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/CoreShibUiConfiguration.java @@ -8,7 +8,8 @@ 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.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; 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; @@ -64,8 +65,8 @@ public EntityService jpaEntityService() { } @Bean - public EntityDescriptorService jpaEntityDescriptorService() { - return new JPAEntityDescriptorServiceImpl(openSamlObjects(), jpaEntityService()); + public EntityDescriptorService jpaEntityDescriptorService(UserService userService) { + return new JPAEntityDescriptorServiceImpl(openSamlObjects(), jpaEntityService(), userService); } @Bean @@ -198,7 +199,7 @@ public ModelRepresentationConversions modelRepresentationConversions() { } @Bean - public UserRoleService userRoleService(RoleRepository roleRepository) { - return new UserRoleService(roleRepository); + public UserService userService(RoleRepository roleRepository, UserRepository userRepository) { + return new UserService(roleRepository, userRepository); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java index d70338499..3f507a929 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/configuration/JsonSchemaComponentsConfiguration.java @@ -2,8 +2,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import edu.internet2.tier.shibboleth.admin.ui.jsonschema.JsonSchemaResourceLocationRegistry; +import edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.JsonSchemaBuilderService; import lombok.Setter; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,6 +57,9 @@ public class JsonSchemaComponentsConfiguration { @Setter private String nameIdFormatFilterUiSchemaLocation = "classpath:nameid-filter.schema.json"; + @Autowired + UserRepository userRepository; + @Bean public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(ResourceLoader resourceLoader, ObjectMapper jacksonMapper) { return JsonSchemaResourceLocationRegistry.inMemory() @@ -96,7 +102,7 @@ public JsonSchemaResourceLocationRegistry jsonSchemaResourceLocationRegistry(Res } @Bean - public JsonSchemaBuilderService jsonSchemaBuilderService() { - return new JsonSchemaBuilderService(); + public JsonSchemaBuilderService jsonSchemaBuilderService(UserService userService) { + return new JsonSchemaBuilderService(userService); } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java index c3e76983e..e76749c06 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorController.java @@ -4,6 +4,10 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository; +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.UserService; import edu.internet2.tier.shibboleth.admin.ui.service.EntityDescriptorService; import org.opensaml.core.xml.io.MarshallingException; import org.slf4j.Logger; @@ -45,10 +49,22 @@ public class EntityDescriptorController { @Autowired RestTemplateBuilder restTemplateBuilder; + private UserRepository userRepository; + + private RoleRepository roleRepository; + + private UserService userService; + private RestTemplate restTemplate; private static Logger LOGGER = LoggerFactory.getLogger(EntityDescriptorController.class); + public EntityDescriptorController(UserRepository userRepository, RoleRepository roleRepository, UserService userService) { + this.userRepository = userRepository; + this.roleRepository = roleRepository; + this.userService = userService; + } + @PostConstruct public void initRestTemplate() { this.restTemplate = restTemplateBuilder.build(); @@ -92,56 +108,85 @@ public ResponseEntity upload(@RequestParam String metadataUrl, @RequestParam @PutMapping("/EntityDescriptor/{resourceId}") public ResponseEntity update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) { + User currentUser = userService.getCurrentUser(); EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(resourceId); if (existingEd == null) { return ResponseEntity.notFound().build(); + } else { + if (currentUser.getRole().equals("ROLE_ADMIN") || currentUser.getUsername().equals(existingEd.getCreatedBy())) { + // Verify we're the only one attempting to update the EntityDescriptor + if (edRepresentation.getVersion() != existingEd.hashCode()) { + return new ResponseEntity(HttpStatus.CONFLICT); + } + + EntityDescriptor updatedEd = + EntityDescriptor.class.cast(entityDescriptorService.createDescriptorFromRepresentation(edRepresentation)); + + updatedEd.setAudId(existingEd.getAudId()); + updatedEd.setResourceId(existingEd.getResourceId()); + updatedEd.setCreatedDate(existingEd.getCreatedDate()); + + updatedEd = entityDescriptorRepository.save(updatedEd); + + return ResponseEntity.ok().body(entityDescriptorService.createRepresentationFromDescriptor(updatedEd)); + } else { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, + "You are not authorized to perform the requested operation.")); + } } - - // Verify we're the only one attempting to update the EntityDescriptor - if (edRepresentation.getVersion() != existingEd.hashCode()) { - return new ResponseEntity(HttpStatus.CONFLICT); - } - - EntityDescriptor updatedEd = - EntityDescriptor.class.cast(entityDescriptorService.createDescriptorFromRepresentation(edRepresentation)); - - updatedEd.setAudId(existingEd.getAudId()); - updatedEd.setResourceId(existingEd.getResourceId()); - updatedEd.setCreatedDate(existingEd.getCreatedDate()); - - updatedEd = entityDescriptorRepository.save(updatedEd); - - return ResponseEntity.ok().body(entityDescriptorService.createRepresentationFromDescriptor(updatedEd)); } @GetMapping("/EntityDescriptors") @Transactional(readOnly = true) - public Iterable getAll() { - return entityDescriptorRepository.findAllByCustomQueryAndStream() - .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) - .collect(Collectors.toList()); + public ResponseEntity getAll() { + User currentUser = userService.getCurrentUser(); + if (currentUser != null) { + if (currentUser.getRole().equals("ROLE_ADMIN")) { + return ResponseEntity.ok(entityDescriptorRepository.findAllByCustomQueryAndStream() + .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) + .collect(Collectors.toList())); + } else { + return ResponseEntity.ok(entityDescriptorRepository.findAllByCreatedBy(currentUser.getUsername()) + .map(ed -> entityDescriptorService.createRepresentationFromDescriptor(ed)) + .collect(Collectors.toList())); + } + } else { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, + "You are not authorized to perform the requested operation.")); + } } @GetMapping("/EntityDescriptor/{resourceId}") public ResponseEntity getOne(@PathVariable String resourceId) { + User currentUser = userService.getCurrentUser(); EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); if (ed == null) { return ResponseEntity.notFound().build(); + } else { + if (currentUser != null && (currentUser.getRole().equals("ROLE_ADMIN") || currentUser.getUsername().equals(ed.getCreatedBy()))) { + EntityDescriptorRepresentation edr = entityDescriptorService.createRepresentationFromDescriptor(ed); + return ResponseEntity.ok(edr); + } else { + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, + "You are not authorized to perform the requested operation.")); + } } - EntityDescriptorRepresentation edr = entityDescriptorService.createRepresentationFromDescriptor(ed); - - return ResponseEntity.ok(edr); } @GetMapping(value = "/EntityDescriptor/{resourceId}", produces = "application/xml") public ResponseEntity getOneXml(@PathVariable String resourceId) throws MarshallingException { + User currentUser = userService.getCurrentUser(); EntityDescriptor ed = entityDescriptorRepository.findByResourceId(resourceId); if (ed == null) { return ResponseEntity.notFound().build(); + } else { + if (currentUser != null && (currentUser.getRole().equals("ROLE_ADMIN") || currentUser.getUsername().equals(ed.getCreatedBy()))) { + final String xml = this.openSamlObjects.marshalToXmlString(ed); + return ResponseEntity.ok(xml); + } else { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } } - final String xml = this.openSamlObjects.marshalToXmlString(ed); - - return ResponseEntity.ok(xml); } private static URI getResourceUriFor(EntityDescriptor ed) { @@ -179,4 +224,5 @@ private ResponseEntity handleUploadingEntityDescriptorXml(byte[] rawXmlBytes, return ResponseEntity.created(getResourceUriFor(persistedEd)) .body(entityDescriptorService.createRepresentationFromDescriptor(persistedEd)); } + } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ErrorResponse.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ErrorResponse.java index fa91aa3e6..f3f84169d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ErrorResponse.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/controller/ErrorResponse.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Setter; import lombok.ToString; +import org.springframework.http.HttpStatus; /** * @author Bill Smith (wsmith@unicon.net) @@ -15,4 +16,9 @@ public class ErrorResponse { private String errorCode; private String errorMessage; + + public ErrorResponse(HttpStatus httpStatus, String errorMessage) { + this.errorCode = String.valueOf(httpStatus.value()); + this.errorMessage = errorMessage; + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java index b879e76d5..e378f5fbb 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/domain/frontend/EntityDescriptorRepresentation.java @@ -9,9 +9,6 @@ public class EntityDescriptorRepresentation implements Serializable { - - private int version; - public EntityDescriptorRepresentation() { } @@ -63,6 +60,10 @@ public EntityDescriptorRepresentation(String id, private List attributeRelease; + private int version; + + private String createdBy; + public String getId() { return id; } @@ -204,4 +205,12 @@ public int getVersion() { public void setVersion(int version) { this.version = version; } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java index be729d489..2ba4f419d 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/repository/EntityDescriptorRepository.java @@ -21,4 +21,5 @@ public interface EntityDescriptorRepository extends CrudRepository findAllByCustomQueryAndStream(); + Stream findAllByCreatedBy(String createdBy); } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java index 0953d528c..8532d3d26 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/controller/UsersController.java @@ -4,7 +4,7 @@ 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 edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,12 +40,12 @@ public class UsersController { private UserRepository userRepository; private RoleRepository roleRepository; - private UserRoleService userRoleService; + private UserService userService; - public UsersController(UserRepository userRepository, RoleRepository roleRepository, UserRoleService userRoleService) { + public UsersController(UserRepository userRepository, RoleRepository roleRepository, UserService userService) { this.userRepository = userRepository; this.roleRepository = roleRepository; - this.userRoleService = userRoleService; + this.userService = userService; } @Transactional(readOnly = true) @@ -54,6 +54,17 @@ public List getAll() { return userRepository.findAll(); } + @Transactional(readOnly = true) + @GetMapping("/current") + public ResponseEntity getCurrentUser() { + User user = userService.getCurrentUser(); + if (user != null) { + return ResponseEntity.ok(user); + } else { + return ResponseEntity.notFound().build(); + } + } + @Transactional(readOnly = true) @GetMapping("/{username}") public ResponseEntity getOne(@PathVariable String username) { @@ -80,7 +91,7 @@ ResponseEntity saveOne(@RequestBody User user) { } //TODO: modify this such that additional encoders can be used user.setPassword(BCrypt.hashpw(user.getPassword(), BCrypt.gensalt())); - userRoleService.updateUserRole(user); + userService.updateUserRole(user); User savedUser = userRepository.save(user); return ResponseEntity.ok(savedUser); } @@ -103,7 +114,7 @@ ResponseEntity updateOne(@PathVariable(value = "username") String username, @ } if (StringUtils.isNotBlank(user.getRole())) { persistedUser.setRole(user.getRole()); - userRoleService.updateUserRole(persistedUser); + userService.updateUserRole(persistedUser); } User savedUser = userRepository.save(persistedUser); return ResponseEntity.ok(savedUser); diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserRoleService.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java similarity index 64% rename from backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserRoleService.java rename to backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java index 87a6431d0..ced3fe133 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserRoleService.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/security/service/UserService.java @@ -3,8 +3,9 @@ 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 edu.internet2.tier.shibboleth.admin.ui.security.repository.UserRepository; import org.apache.commons.lang.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.context.SecurityContextHolder; import java.util.HashSet; import java.util.Optional; @@ -13,12 +14,14 @@ /** * @author Bill Smith (wsmith@unicon.net) */ -public class UserRoleService { +public class UserService { private RoleRepository roleRepository; + private UserRepository userRepository; - public UserRoleService(RoleRepository roleRepository) { + public UserService(RoleRepository roleRepository, UserRepository userRepository) { this.roleRepository = roleRepository; + this.userRepository = userRepository; } /** @@ -43,4 +46,18 @@ public void updateUserRole(User user) { throw new RuntimeException(String.format("User with username [%s] has no role defined and therefor cannot be updated!", user.getUsername())); } } + + public User getCurrentUser() { + User user = null; + if (SecurityContextHolder.getContext() != null && SecurityContextHolder.getContext().getAuthentication() != null) { + String principal = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (StringUtils.isNotBlank(principal)) { + Optional persistedUser = userRepository.findByUsername(principal); + if (persistedUser.isPresent()) { + user = persistedUser.get(); + } + } + } + return user; + } } diff --git a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java index adb8e4cb8..e21be8a29 100644 --- a/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java +++ b/backend/src/main/java/edu/internet2/tier/shibboleth/admin/ui/service/JPAEntityDescriptorServiceImpl.java @@ -43,6 +43,8 @@ import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.SecurityInfoRepresentation; import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ServiceProviderSsoDescriptorRepresentation; import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects; +import edu.internet2.tier.shibboleth.admin.ui.security.model.User; +import edu.internet2.tier.shibboleth.admin.ui.security.service.UserService; import edu.internet2.tier.shibboleth.admin.util.MDDCConstants; import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions; @@ -80,15 +82,24 @@ public class JPAEntityDescriptorServiceImpl implements EntityDescriptorService { @Autowired private EntityService entityService; - public JPAEntityDescriptorServiceImpl(OpenSamlObjects openSamlObjects, EntityService entityService) { + private UserService userService; + + public JPAEntityDescriptorServiceImpl(OpenSamlObjects openSamlObjects, EntityService entityService, UserService userService) { this.openSamlObjects = openSamlObjects; this.entityService = entityService; + this.userService = userService; } @Override public EntityDescriptor createDescriptorFromRepresentation(final EntityDescriptorRepresentation representation) { EntityDescriptor ed = openSamlObjects.buildDefaultInstanceOfType(EntityDescriptor.class); ed.setEntityID(representation.getEntityId()); + User user = userService.getCurrentUser(); + if (user != null) { + ed.setCreatedBy(user.getUsername()); + } else { + LOGGER.warn("Current user was null! Who is logged in?"); + } // setup SPSSODescriptor if (representation.getServiceProviderSsoDescriptor() != null) { @@ -357,6 +368,7 @@ public EntityDescriptorRepresentation createRepresentationFromDescriptor(org.ope representation.setCreatedDate(ed.getCreatedDate()); representation.setModifiedDate(ed.getModifiedDate()); representation.setVersion(ed.hashCode()); + representation.setCreatedBy(ed.getCreatedBy()); if (ed.getSPSSODescriptor("") != null && ed.getSPSSODescriptor("").getSupportedProtocols().size() > 0) { ServiceProviderSsoDescriptorRepresentation serviceProviderSsoDescriptorRepresentation = representation.getServiceProviderSsoDescriptor(true); diff --git a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy index b38941b74..58173bd33 100644 --- a/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy +++ b/backend/src/test/groovy/edu/internet2/tier/shibboleth/admin/ui/controller/EntityDescriptorControllerTests.groovy @@ -1,20 +1,39 @@ package edu.internet2.tier.shibboleth.admin.ui.controller import com.fasterxml.jackson.databind.ObjectMapper +import edu.internet2.tier.shibboleth.admin.ui.configuration.CoreShibUiConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.InternationalizationConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.SearchConfiguration +import edu.internet2.tier.shibboleth.admin.ui.configuration.TestConfiguration import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor import edu.internet2.tier.shibboleth.admin.ui.opensaml.OpenSamlObjects import edu.internet2.tier.shibboleth.admin.ui.repository.EntityDescriptorRepository +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.UserService import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityDescriptorServiceImpl import edu.internet2.tier.shibboleth.admin.ui.service.JPAEntityServiceImpl import edu.internet2.tier.shibboleth.admin.ui.util.RandomGenerator import edu.internet2.tier.shibboleth.admin.ui.util.TestObjectGenerator import groovy.json.JsonOutput import groovy.json.JsonSlurper +import org.springframework.boot.autoconfigure.domain.EntityScan +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest +import org.springframework.data.jpa.repository.config.EnableJpaRepositories +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.security.web.context.HttpSessionSecurityContextRepository +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.web.servlet.result.MockMvcResultHandlers import org.springframework.test.web.servlet.setup.MockMvcBuilders import org.springframework.web.client.RestTemplate import spock.lang.Specification import spock.lang.Subject +import javax.servlet.http.HttpSession +import java.security.Principal import java.time.LocalDateTime import static org.hamcrest.CoreMatchers.containsString @@ -22,6 +41,10 @@ import static org.springframework.http.MediaType.* import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +@DataJpaTest +@ContextConfiguration(classes=[CoreShibUiConfiguration, SearchConfiguration, TestConfiguration, InternationalizationConfiguration]) +@EnableJpaRepositories(basePackages = ["edu.internet2.tier.shibboleth.admin.ui"]) +@EntityScan("edu.internet2.tier.shibboleth.admin.ui") class EntityDescriptorControllerTests extends Specification { RandomGenerator randomGenerator @@ -43,23 +66,31 @@ class EntityDescriptorControllerTests extends Specification { @Subject def controller + Authentication authentication = Mock() + SecurityContext securityContext = Mock() + UserRepository userRepository = Mock() + RoleRepository roleRepository = Mock() + def setup() { generator = new TestObjectGenerator() randomGenerator = new RandomGenerator() mapper = new ObjectMapper() service = new JPAEntityDescriptorServiceImpl(openSamlObjects, new JPAEntityServiceImpl(openSamlObjects)) - controller = new EntityDescriptorController( - entityDescriptorRepository: entityDescriptorRepository, - openSamlObjects: openSamlObjects, - entityDescriptorService: service - ) + controller = new EntityDescriptorController(userRepository, roleRepository, new UserService(roleRepository, userRepository)) + controller.entityDescriptorRepository = entityDescriptorRepository + controller.openSamlObjects = openSamlObjects + controller.entityDescriptorService = service + controller.restTemplate = mockRestTemplate mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + + securityContext.getAuthentication() >> authentication } - def 'GET /EntityDescriptors with empty repository'() { + def 'GET /EntityDescriptors with empty repository as admin'() { given: + prepareAdminUser() def emptyRecordsFromRepository = [].stream() def expectedEmptyListResponseBody = '[]' def expectedResponseContentType = APPLICATION_JSON_UTF8 @@ -77,8 +108,9 @@ class EntityDescriptorControllerTests extends Specification { } - def 'GET /EntityDescriptors with 1 record in repository'() { + def 'GET /EntityDescriptors with 1 record in repository as admin'() { given: + prepareAdminUser() def expectedCreationDate = '2017-10-23T11:11:11' def entityDescriptor = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, createdDate: LocalDateTime.parse(expectedCreationDate)) @@ -102,7 +134,8 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null, - "version": $version + "version": $version, + "createdBy": null } ] """ @@ -122,8 +155,9 @@ class EntityDescriptorControllerTests extends Specification { } - def 'GET /EntityDescriptors with 2 records in repository'() { + def 'GET /EntityDescriptors with 2 records in repository as admin'() { given: + prepareAdminUser() def expectedCreationDate = '2017-10-23T11:11:11' def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', serviceEnabled: true, @@ -152,7 +186,8 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null, - "version": $versionOne + "version": $versionOne, + "createdBy": null }, { "id": "uuid-2", @@ -170,7 +205,8 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null, - "version": $versionTwo + "version": $versionTwo, + "createdBy": null } ] """ @@ -190,6 +226,54 @@ class EntityDescriptorControllerTests extends Specification { } + def 'GET /EntityDescriptors with 1 record in repository as user returns only that user\'s records'() { + given: + prepareUser('someUser', 'ROLE_USER') + def expectedCreationDate = '2017-10-23T11:11:11' + def entityDescriptorOne = new EntityDescriptor(resourceId: 'uuid-1', entityID: 'eid1', serviceProviderName: 'sp1', + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate), + createdBy: 'someUser') + def versionOne = entityDescriptorOne.hashCode() + def oneRecordFromRepository = [entityDescriptorOne].stream() + def expectedOneRecordListResponseBody = """ + [ + { + "id": "uuid-1", + "serviceProviderName": "sp1", + "entityId": "eid1", + "serviceEnabled": true, + "createdDate": "$expectedCreationDate", + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null, + "version": $versionOne, + "createdBy": "someUser" + } + ] + """ + + def expectedResponseContentType = APPLICATION_JSON_UTF8 + def expectedHttpResponseStatus = status().isOk() + + when: + def result = mockMvc.perform(get('/api/EntityDescriptors')) + + then: + //One call to the repo expected + 1 * entityDescriptorRepository.findAllByCreatedBy('someUser') >> oneRecordFromRepository + result.andExpect(expectedHttpResponseStatus) + .andExpect(content().contentType(expectedResponseContentType)) + .andExpect(content().json(expectedOneRecordListResponseBody, true)) + } + def 'POST /EntityDescriptor and successfully create new record'() { given: def expectedCreationDate = '2017-10-23T11:11:11' @@ -241,7 +325,8 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null, - "version": $version + "version": $version, + "createdBy": null } """ @@ -318,6 +403,7 @@ class EntityDescriptorControllerTests extends Specification { def 'GET /EntityDescriptor/{resourceId} existing'() { given: + prepareAdminUser() def expectedCreationDate = '2017-10-23T11:11:11' def providedResourceId = 'uuid-1' def expectedSpName = 'sp1' @@ -346,7 +432,8 @@ class EntityDescriptorControllerTests extends Specification { "assertionConsumerServices": null, "relyingPartyOverrides": null, "attributeRelease": null, - "version": $version + "version": $version, + "createdBy": null } """ @@ -362,8 +449,81 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(content().json(expectedJsonBody, true)) } + def 'GET /EntityDescriptor/{resourceId} existing, owned by non-admin'() { + given: + prepareUser('someUser', 'ROLE_USER') + def expectedCreationDate = '2017-10-23T11:11:11' + def providedResourceId = 'uuid-1' + def expectedSpName = 'sp1' + def expectedEntityId = 'eid1' + + def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate), + createdBy: 'someUser') + def version = entityDescriptor.hashCode() + + def expectedJsonBody = """ + { + "id": "${providedResourceId}", + "serviceProviderName": "$expectedSpName", + "entityId": "$expectedEntityId", + "organization": null, + "serviceEnabled": true, + "createdDate": "$expectedCreationDate", + "modifiedDate": null, + "organization": null, + "contacts": null, + "mdui": null, + "serviceProviderSsoDescriptor": null, + "logoutEndpoints": null, + "securityInfo": null, + "assertionConsumerServices": null, + "relyingPartyOverrides": null, + "attributeRelease": null, + "version": $version, + "createdBy": "someUser" + } + """ + + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId")) + + then: + //EntityDescriptor found + 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor + + + result.andExpect(status().isOk()) + .andExpect(content().json(expectedJsonBody, true)) + } + + def 'GET /EntityDescriptor/{resourceId} existing, owned by some other user'() { + given: + prepareUser('someUser', 'ROLE_USER') + def expectedCreationDate = '2017-10-23T11:11:11' + def providedResourceId = 'uuid-1' + def expectedSpName = 'sp1' + def expectedEntityId = 'eid1' + + def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate), + createdBy: 'someOtherUser') + + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId")) + + then: + //EntityDescriptor found + 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor + + result.andExpect(status().is(403)) + } + def 'GET /EntityDescriptor/{resourceId} existing (xml)'() { given: + prepareAdminUser() def expectedCreationDate = '2017-10-23T11:11:11' def providedResourceId = 'uuid-1' def expectedSpName = 'sp1' @@ -393,8 +553,69 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(content().xml(expectedXML)) } + def 'GET /EntityDescriptor/{resourceId} existing (xml), user-owned'() { + given: + prepareUser('someUser', 'ROLE_USER') + def expectedCreationDate = '2017-10-23T11:11:11' + def providedResourceId = 'uuid-1' + def expectedSpName = 'sp1' + def expectedEntityId = 'eid1' + + def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate), + createdBy: 'someUser') + entityDescriptor.setElementLocalName("EntityDescriptor") + entityDescriptor.setNamespacePrefix("md") + entityDescriptor.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + + def expectedXML = """ +""" + + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId") + .accept(APPLICATION_XML)) + + then: + //EntityDescriptor found + 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor + + + result.andExpect(status().isOk()) + .andExpect(content().xml(expectedXML)) + } + + def 'GET /EntityDescriptor/{resourceId} existing (xml), other user-owned'() { + given: + prepareUser('someUser', 'ROLE_USER') + def expectedCreationDate = '2017-10-23T11:11:11' + def providedResourceId = 'uuid-1' + def expectedSpName = 'sp1' + def expectedEntityId = 'eid1' + + def entityDescriptor = new EntityDescriptor(resourceId: providedResourceId, entityID: expectedEntityId, serviceProviderName: expectedSpName, + serviceEnabled: true, + createdDate: LocalDateTime.parse(expectedCreationDate), + createdBy: 'someOtherUser') + entityDescriptor.setElementLocalName("EntityDescriptor") + entityDescriptor.setNamespacePrefix("md") + entityDescriptor.setNamespaceURI("urn:oasis:names:tc:SAML:2.0:metadata") + + when: + def result = mockMvc.perform(get("/api/EntityDescriptor/$providedResourceId") + .accept(APPLICATION_XML)) + + then: + //EntityDescriptor found + 1 * entityDescriptorRepository.findByResourceId(providedResourceId) >> entityDescriptor + + result.andExpect(status().is(403)) + } + def "POST /EntityDescriptor handles XML happily"() { given: + prepareAdminUser() def postedBody = ''' @@ -452,7 +673,8 @@ class EntityDescriptorControllerTests extends Specification { "attributeRelease": [ "givenName", "employeeNumber" - ] + ], + "createdBy": null } """ @@ -510,6 +732,7 @@ class EntityDescriptorControllerTests extends Specification { def "POST /EntityDescriptor handles x-www-form-urlencoded happily"() { given: + prepareAdminUser() def postedMetadataUrl = "http://test.scaldingspoon.org/test1" def restXml = ''' @@ -570,7 +793,8 @@ class EntityDescriptorControllerTests extends Specification { "attributeRelease": [ "givenName", "employeeNumber" - ] + ], + "createdBy": null } """ @@ -586,8 +810,9 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(content().json(expectedJson, true)) } - def "PUT /EntityDescriptor updates entity descriptors properly"() { + def "PUT /EntityDescriptor updates entity descriptors properly as admin"() { given: + prepareAdminUser() def entityDescriptor = generator.buildEntityDescriptor() def updatedEntityDescriptor = generator.buildEntityDescriptor() updatedEntityDescriptor.resourceId = entityDescriptor.resourceId @@ -613,8 +838,32 @@ class EntityDescriptorControllerTests extends Specification { .andExpect(content().json(JsonOutput.toJson(expectedJson), true)) } + def "PUT /EntityDescriptor denies the request if the PUTing user is not an ADMIN and not the createdBy user"() { + given: + prepareUser('randomUser', 'ROLE_USER') + def entityDescriptor = generator.buildEntityDescriptor() + entityDescriptor.createdBy = 'someoneElse' + def updatedEntityDescriptor = generator.buildEntityDescriptor() + updatedEntityDescriptor.createdBy = 'someoneElse' + updatedEntityDescriptor.resourceId = entityDescriptor.resourceId + def updatedEntityDescriptorRepresentation = service.createRepresentationFromDescriptor(updatedEntityDescriptor) + def postedJsonBody = mapper.writeValueAsString(updatedEntityDescriptorRepresentation) + def resourceId = entityDescriptor.resourceId + 1 * entityDescriptorRepository.findByResourceId(resourceId) >> entityDescriptor + + when: + def result = mockMvc.perform( + put("/api/EntityDescriptor/$resourceId") + .contentType(APPLICATION_JSON_UTF8) + .content(postedJsonBody)) + + then: + result.andExpect(status().is(403)) + } + def "PUT /EntityDescriptor 409's if the version numbers don't match"() { given: + prepareAdminUser() def entityDescriptor = generator.buildEntityDescriptor() def updatedEntityDescriptor = generator.buildEntityDescriptor() updatedEntityDescriptor.resourceId = entityDescriptor.resourceId @@ -634,4 +883,16 @@ class EntityDescriptorControllerTests extends Specification { then: result.andExpect(status().is(409)) } + + def prepareAdminUser() { + prepareUser('foo', 'ROLE_ADMIN') + } + + def prepareUser(String username, String rolename) { + authentication.getPrincipal() >> username + SecurityContextHolder.setContext(securityContext) + def user = new User(username: username, role: rolename) + Optional currentUser = Optional.of(user) + userRepository.findByUsername(username) >> currentUser + } } diff --git a/ui/src/app/app.component.html b/ui/src/app/app.component.html index fe988c62f..f29cfd620 100644 --- a/ui/src/app/app.component.html +++ b/ui/src/app/app.component.html @@ -34,7 +34,8 @@ routerLink="/metadata/provider/wizard" routerLinkActive="active" [attr.aria-label]="'action.add-new-provider' | translate" - role="button"> + role="button" + *ngIf="isAdmin$ | async">   Metadata Provider diff --git a/ui/src/app/app.component.spec.ts b/ui/src/app/app.component.spec.ts index b76cbccde..754afd64f 100644 --- a/ui/src/app/app.component.spec.ts +++ b/ui/src/app/app.component.spec.ts @@ -58,7 +58,7 @@ describe('AppComponent', () => { it('should create the app', async(() => { expect(app).toBeTruthy(); - expect(store.dispatch).toHaveBeenCalledTimes(2); + expect(store.dispatch).toHaveBeenCalledTimes(3); })); it(`should have as title 'Shib-UI'`, async(() => { diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts index f0ac72720..57119404d 100644 --- a/ui/src/app/app.component.ts +++ b/ui/src/app/app.component.ts @@ -11,6 +11,7 @@ import { I18nService } from './i18n/service/i18n.service'; import { SetLocale } from './i18n/action/message.action'; import { brand } from './app.brand'; import { Brand } from './core/model/brand'; +import { UserLoadRequestAction } from './core/action/user.action'; @Component({ selector: 'app-root', @@ -24,6 +25,7 @@ export class AppComponent implements OnInit { formatted$: Observable; today = new Date(); year = new Date().getFullYear(); + isAdmin$: Observable; brand: Brand = brand; @@ -35,9 +37,11 @@ export class AppComponent implements OnInit { ) { this.version$ = this.store.select(fromRoot.getVersionInfo); this.formatted$ = this.version$.pipe(map(this.formatter)); + this.isAdmin$ = this.store.select(fromRoot.isCurrentUserAdmin); } ngOnInit(): void { + this.store.dispatch(new UserLoadRequestAction()); this.store.dispatch(new VersionInfoLoadRequestAction()); this.store.dispatch(new SetLocale(this.i18nService.getCurrentLocale())); } diff --git a/ui/src/app/core/action/user.action.ts b/ui/src/app/core/action/user.action.ts index 2aa273f9a..8ab396e12 100644 --- a/ui/src/app/core/action/user.action.ts +++ b/ui/src/app/core/action/user.action.ts @@ -1,39 +1,42 @@ import { Action } from '@ngrx/store'; import { User } from '../model/user'; -export const USER_LOAD_REQUEST = '[Auth] User REQUEST'; -export const USER_LOAD_SUCCESS = '[Auth] User SUCCESS'; -export const USER_LOAD_ERROR = '[Auth] User ERROR'; -export const REDIRECT = '[Auth] User Redirect'; + +export enum CurrentUserActionTypes { + USER_LOAD_REQUEST = '[Current User] Load User Request', + USER_LOAD_SUCCESS = '[Current User] Load User Success', + USER_LOAD_ERROR = '[Current User] Load User Fail', + REDIRECT = '[Current User] Redirect' +} /** * Add User to Collection Actions */ export class UserLoadRequestAction implements Action { - readonly type = USER_LOAD_REQUEST; + readonly type = CurrentUserActionTypes.USER_LOAD_REQUEST; constructor() { } } export class UserLoadSuccessAction implements Action { - readonly type = USER_LOAD_SUCCESS; + readonly type = CurrentUserActionTypes.USER_LOAD_SUCCESS; constructor(public payload: User) { } } export class UserLoadErrorAction implements Action { - readonly type = USER_LOAD_ERROR; + readonly type = CurrentUserActionTypes.USER_LOAD_ERROR; constructor(public payload: { message: string }) { } } export class UserRedirect implements Action { - readonly type = REDIRECT; + readonly type = CurrentUserActionTypes.REDIRECT; constructor(public payload: string) { } } -export type Actions = +export type CurrentUserActionsUnion = | UserLoadRequestAction | UserLoadSuccessAction | UserLoadErrorAction diff --git a/ui/src/app/core/effect/user.effect.ts b/ui/src/app/core/effect/user.effect.ts index 095267bff..5417fa999 100644 --- a/ui/src/app/core/effect/user.effect.ts +++ b/ui/src/app/core/effect/user.effect.ts @@ -4,8 +4,19 @@ import { Effect, Actions, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; import { map, tap, catchError, switchMap } from 'rxjs/operators'; -import * as user from '../action/user.action'; -import { LoadRoleRequest, LoadRoleFail, LoadRoleSuccess, ConfigurationActionTypes } from '../action/configuration.action'; +import { + LoadRoleRequest, + LoadRoleFail, + LoadRoleSuccess, + ConfigurationActionTypes +} from '../action/configuration.action'; +import { + UserLoadRequestAction, + CurrentUserActionTypes, + UserLoadSuccessAction, + UserLoadErrorAction, + UserRedirect +} from '../action/user.action'; import { UserService } from '../service/user.service'; @Injectable() @@ -23,10 +34,22 @@ export class UserEffects { ) ); + @Effect() + loadCurrentUser$ = this.actions$.pipe( + ofType(CurrentUserActionTypes.USER_LOAD_REQUEST), + switchMap(() => + this.userService.getCurrentUser() + .pipe( + map(user => new UserLoadSuccessAction(user)), + catchError(error => of(new UserLoadErrorAction(error))) + ) + ) + ); + @Effect({dispatch: false}) redirect$ = this.actions$.pipe( - ofType(user.REDIRECT), - map((action: user.UserRedirect) => action.payload), + ofType(CurrentUserActionTypes.REDIRECT), + map((action: UserRedirect) => action.payload), tap(path => { window.location.href = path; }) diff --git a/ui/src/app/core/model/user.ts b/ui/src/app/core/model/user.ts index 37d05212e..27375c23e 100644 --- a/ui/src/app/core/model/user.ts +++ b/ui/src/app/core/model/user.ts @@ -1,8 +1,7 @@ export interface User { - id: string; + username: string; role: string; - name: { - first: string, - last: string - }; + firstName: string; + lastName: string; + emailAddress: string; } diff --git a/ui/src/app/core/reducer/index.ts b/ui/src/app/core/reducer/index.ts index 9d9f91dec..7d420742b 100644 --- a/ui/src/app/core/reducer/index.ts +++ b/ui/src/app/core/reducer/index.ts @@ -41,3 +41,5 @@ export const getVersionError = createSelector(getVersionState, fromVersion.getVe export const getConfigState = createSelector(getCoreFeature, getConfigStateFn); export const getRoles = createSelector(getConfigState, fromConfig.getRoles); + +export const isCurrentUserAdmin = createSelector(getUser, user => user ? user.role === 'ROLE_ADMIN' : null); diff --git a/ui/src/app/core/reducer/user.reducer.spec.ts b/ui/src/app/core/reducer/user.reducer.spec.ts index 6dc2bed01..d7cbaacdf 100644 --- a/ui/src/app/core/reducer/user.reducer.spec.ts +++ b/ui/src/app/core/reducer/user.reducer.spec.ts @@ -11,12 +11,11 @@ describe('User Reducer', () => { }; const user: User = { - id: '1', + username: 'foo', role: 'admin', - name: { - first: 'foo', - last: 'bar' - } + firstName: 'somebody', + lastName: 'nobody', + emailAddress: 'email@edu.edu' }; describe('undefined action', () => { @@ -59,14 +58,7 @@ describe('User Reducer', () => { describe('User Selectors', () => { const state = { - user: { - id: '1', - role: 'admin', - name: { - first: 'foo', - last: 'bar' - } - }, + user: { ...user }, fetching: true, error: { message: 'foo', type: 'bar' } } as fromUser.UserState; diff --git a/ui/src/app/core/reducer/user.reducer.ts b/ui/src/app/core/reducer/user.reducer.ts index b18810b2c..2510d5c5e 100644 --- a/ui/src/app/core/reducer/user.reducer.ts +++ b/ui/src/app/core/reducer/user.reducer.ts @@ -1,7 +1,8 @@ -import { createSelector, createFeatureSelector } from '@ngrx/store'; import { User } from '../model/user'; -import * as user from '../action/user.action'; -import * as fromRoot from '../../core/reducer'; +import { + CurrentUserActionTypes, + CurrentUserActionsUnion +} from '../action/user.action'; export interface UserState { fetching: boolean; @@ -18,20 +19,20 @@ export const initialState: UserState = { error: null }; -export function reducer(state = initialState, action: user.Actions): UserState { +export function reducer(state = initialState, action: CurrentUserActionsUnion): UserState { switch (action.type) { - case user.USER_LOAD_REQUEST: { + case CurrentUserActionTypes.USER_LOAD_REQUEST: { return Object.assign({}, state, { fetching: true }); } - case user.USER_LOAD_SUCCESS: { + case CurrentUserActionTypes.USER_LOAD_SUCCESS: { return Object.assign({}, state, { fetching: false, user: action.payload }); } - case user.USER_LOAD_ERROR: { + case CurrentUserActionTypes.USER_LOAD_ERROR: { return Object.assign({}, state, { fetching: false, user: null, diff --git a/ui/src/app/core/service/admin.guard.ts b/ui/src/app/core/service/admin.guard.ts new file mode 100644 index 000000000..c602082c8 --- /dev/null +++ b/ui/src/app/core/service/admin.guard.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; +import { Store } from '@ngrx/store'; + +import * as fromCore from '../reducer'; +import { Observable } from 'rxjs'; +import { filter, catchError, take } from 'rxjs/operators'; + + +@Injectable({ + providedIn: 'root', +}) +export class AdminGuard implements CanActivate { + + constructor( + private store: Store, + private router: Router + ) {} + + canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + const isAdminObs = this.checkIsAdmin().pipe(take(1)); + isAdminObs.subscribe(authed => { + if (!authed) { + this.router.navigate(['/']); + } + }); + return isAdminObs; + } + + checkIsAdmin(): Observable { + return this.store + .select(fromCore.isCurrentUserAdmin) + .pipe( + filter(isAdmin => isAdmin !== null), + ); + } +} diff --git a/ui/src/app/core/service/user.service.ts b/ui/src/app/core/service/user.service.ts index e44e16bce..af8907160 100644 --- a/ui/src/app/core/service/user.service.ts +++ b/ui/src/app/core/service/user.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { User } from '../model/user'; import { HttpClient } from '@angular/common/http'; +import { catchError, map } from 'rxjs/operators'; @Injectable() export class UserService { @@ -17,4 +18,10 @@ export class UserService { `${this.base}/supportedRoles` ); } + + getCurrentUser(): Observable { + return this.http.get( + `${this.base}/admin/users/current` + ); + } } /* istanbul ignore next */ diff --git a/ui/src/app/dashboard/container/dashboard.component.html b/ui/src/app/dashboard/container/dashboard.component.html index 0a9e4319f..09c647146 100644 --- a/ui/src/app/dashboard/container/dashboard.component.html +++ b/ui/src/app/dashboard/container/dashboard.component.html @@ -8,7 +8,7 @@ Metadata Sources - -