Skip to content

Commit

Permalink
SHIBUI-2024
Browse files Browse the repository at this point in the history
Added validation when saving entity descriptor - entity ID and ACS locations will validate against the group regex
  • Loading branch information
chasegawa committed Aug 17, 2021
1 parent e911b7d commit 3d32401
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException;
import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException;
import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException;
import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException;
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;
Expand Down Expand Up @@ -73,7 +74,8 @@ public EntityDescriptorController(EntityDescriptorVersionService versionService)

@PostMapping("/EntityDescriptor")
@Transactional
public ResponseEntity<?> create(@RequestBody EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException {
public ResponseEntity<?> create(@RequestBody EntityDescriptorRepresentation edRepresentation)
throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException {
EntityDescriptorRepresentation persistedEd = entityDescriptorService.createNew(edRepresentation);
return ResponseEntity.created(getResourceUriFor(persistedEd.getId())).body(persistedEd);
}
Expand Down Expand Up @@ -145,7 +147,8 @@ public void initRestTemplate() {

@PutMapping("/EntityDescriptor/{resourceId}")
@Transactional
public ResponseEntity<?> update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId) throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException {
public ResponseEntity<?> update(@RequestBody EntityDescriptorRepresentation edRepresentation, @PathVariable String resourceId)
throws ForbiddenException, ConcurrentModificationException, EntityNotFoundException, InvalidUrlMatchException {
edRepresentation.setId(resourceId); // This should be the same already, but just to be safe...
EntityDescriptorRepresentation result = entityDescriptorService.update(edRepresentation);
return ResponseEntity.ok().body(result);
Expand All @@ -171,4 +174,4 @@ public ResponseEntity<?> upload(@RequestParam String metadataUrl, @RequestParam
.body(String.format("Error fetching XML metadata from the provided URL. Error: %s", e.getMessage()));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package edu.internet2.tier.shibboleth.admin.ui.controller;

import java.util.ConcurrentModificationException;

import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException;
import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException;
import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException;
import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -10,9 +12,7 @@
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException;
import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException;
import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException;
import java.util.ConcurrentModificationException;

@ControllerAdvice(assignableTypes = {EntityDescriptorController.class})
public class EntityDescriptorControllerExceptionHandler extends ResponseEntityExceptionHandler {
Expand All @@ -21,24 +21,30 @@ public class EntityDescriptorControllerExceptionHandler extends ResponseEntityEx
public ResponseEntity<?> handleConcurrentModificationException(ConcurrentModificationException e, WebRequest request) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(new ErrorResponse(HttpStatus.CONFLICT, e.getMessage()));
}

@ExceptionHandler({ EntityIdExistsException.class })
public ResponseEntity<?> handleEntityExistsException(EntityIdExistsException e, WebRequest request) {
HttpHeaders headers = new HttpHeaders();
headers.setLocation(EntityDescriptorController.getResourceUriFor(e.getMessage()));
return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers).body(new ErrorResponse(
String.valueOf(HttpStatus.CONFLICT.value()),
String.format("The entity descriptor with entity id [%s] already exists.", e.getMessage())));
return ResponseEntity.status(HttpStatus.CONFLICT).headers(headers)
.body(new ErrorResponse(String.valueOf(HttpStatus.CONFLICT.value()),
String.format("The entity descriptor with entity id [%s] already exists.",
e.getMessage())));

}

@ExceptionHandler({ EntityNotFoundException.class })
public ResponseEntity<?> handleEntityNotFoundException(EntityNotFoundException e, WebRequest request) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(HttpStatus.NOT_FOUND, e.getMessage()));
}

@ExceptionHandler({ ForbiddenException.class })
public ResponseEntity<?> handleForbiddenAccess(ForbiddenException e, WebRequest request) {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(new ErrorResponse(HttpStatus.FORBIDDEN, e.getMessage()));
}
}

@ExceptionHandler({ InvalidUrlMatchException.class })
public ResponseEntity<?> handleInvalidUrlMatchException(InvalidUrlMatchException e, WebRequest request) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(HttpStatus.BAD_REQUEST, e.getMessage()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package edu.internet2.tier.shibboleth.admin.ui.exception;

public class InvalidUrlMatchException extends Exception {
public InvalidUrlMatchException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ public void deleteDefinition(String resourceId) throws EntityNotFoundException,
groupRepository.delete(group);
}

/**
* Though the name URI is used here, any string value that we want to validate against the group's regex is accepted and checked.
* Designed usage is that this would be a URL or an entity Id (which is a URI that does not have to follow the URL conventions)
*/
@Override
public boolean doesUrlMatchGroupPattern(String groupId, String uri) {
Group group = find(groupId);
return Pattern.matches(group.getValidationRegex(), uri);
}

@Override
@Transactional
public void ensureAdminGroupExists() {
Expand All @@ -64,7 +74,7 @@ public void ensureAdminGroupExists() {
g = new Group();
g.setName("ADMIN-GROUP");
g.setResourceId("admingroup");
g.setValidationRegex("/*"); // Everything
g.setValidationRegex("^.+$"); // Just about everything
g = groupRepository.save(g);
}
Group.ADMIN_GROUP = g;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ public interface IGroupService {

Group updateGroup(Group g) throws EntityNotFoundException, InvalidGroupRegexException;

boolean doesUrlMatchGroupPattern(String groupId, String uri);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException;
import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException;
import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException;
import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException;

import java.util.ConcurrentModificationException;
import java.util.List;
Expand All @@ -31,15 +32,17 @@ public interface EntityDescriptorService {
* @throws ForbiddenException If user is unauthorized to perform this operation
* @throws EntityIdExistsException If any EntityDescriptor already exists with the same EntityId
*/
EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException;
EntityDescriptorRepresentation createNew(EntityDescriptor ed)
throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException;

/**
* @param edRepresentation Incoming representation to save
* @return EntityDescriptorRepresentation
* @throws ForbiddenException If user is unauthorized to perform this operation
* @throws EntityIdExistsException If the entity already exists
*/
EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityIdExistsException;
EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRepresentation)
throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException;

/**
* Map from opensaml implementation of entity descriptor model to front-end data representation of entity descriptor
Expand Down Expand Up @@ -93,13 +96,13 @@ public interface EntityDescriptorService {
Map<String, Object> getRelyingPartyOverridesRepresentationFromAttributeList(List<Attribute> attributeList);

/**
* @param edRepresentation Incoming representation to save
* @return EntityDescriptorRepresentation
* @throws ForbiddenException If user is unauthorized to perform this operation
* @throws EntityIdExistsException If the entity already exists
* @throws ConcurrentModificationException If the entity was already modified by another user
* @throws ForbiddenException If the user is not permitted to perform the action
* @throws EntityNotFoundException If the entity doesn't already exist in the database
* @throws ConcurrentModificationException IF the entity is being modified in another session
* @throws InvalidUrlMatchException If the entity id or the ACS location urls don't match the supplied regex
*/
EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation) throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException;
EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRepresentation)
throws ForbiddenException, EntityNotFoundException, ConcurrentModificationException, InvalidUrlMatchException;

/**
* Update an instance of entity descriptor with information from the front-end representation
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
package edu.internet2.tier.shibboleth.admin.ui.service;

import edu.internet2.tier.shibboleth.admin.ui.domain.Attribute;
import edu.internet2.tier.shibboleth.admin.ui.domain.EntityAttributes;
import edu.internet2.tier.shibboleth.admin.ui.domain.EntityDescriptor;
import edu.internet2.tier.shibboleth.admin.ui.domain.KeyDescriptor;
import edu.internet2.tier.shibboleth.admin.ui.domain.IRelyingPartyOverrideProperty;
import edu.internet2.tier.shibboleth.admin.ui.domain.UIInfo;
import edu.internet2.tier.shibboleth.admin.ui.domain.XSBoolean;
import edu.internet2.tier.shibboleth.admin.ui.domain.XSInteger;
import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.AssertionConsumerServiceRepresentation;
import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.ContactRepresentation;
import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.EntityDescriptorRepresentation;
import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.LogoutEndpointRepresentation;
import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.MduiRepresentation;
import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.OrganizationRepresentation;
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.domain.*;
import edu.internet2.tier.shibboleth.admin.ui.domain.frontend.*;
import edu.internet2.tier.shibboleth.admin.ui.exception.EntityIdExistsException;
import edu.internet2.tier.shibboleth.admin.ui.exception.EntityNotFoundException;
import edu.internet2.tier.shibboleth.admin.ui.exception.ForbiddenException;
import edu.internet2.tier.shibboleth.admin.ui.exception.InvalidUrlMatchException;
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.Group;
Expand All @@ -29,23 +16,14 @@
import edu.internet2.tier.shibboleth.admin.util.MDDCConstants;
import edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions;
import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;

import javax.transaction.Transactional;

import static edu.internet2.tier.shibboleth.admin.util.EntityDescriptorConversionUtils.*;
import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.*;
import static edu.internet2.tier.shibboleth.admin.util.ModelRepresentationConversions.getStringListOfAttributeValues;

@Slf4j
@Service
Expand Down Expand Up @@ -94,22 +72,30 @@ public EntityDescriptor createDescriptorFromRepresentation(final EntityDescripto
}

@Override
public EntityDescriptorRepresentation createNew(EntityDescriptor ed) throws ForbiddenException, EntityIdExistsException {
public EntityDescriptorRepresentation createNew(EntityDescriptor ed)
throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException {
return createNew(createRepresentationFromDescriptor(ed));
}

@Override
public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityIdExistsException {
public EntityDescriptorRepresentation createNew(EntityDescriptorRepresentation edRep)
throws ForbiddenException, EntityIdExistsException, InvalidUrlMatchException {
if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) {
throw new ForbiddenException("You do not have the permissions necessary to enable this service.");
}

if (entityDescriptorRepository.findByEntityID(edRep.getEntityId()) != null) {
throw new EntityIdExistsException(edRep.getEntityId());
}


// "Create new" will use the current user's group as the owner
String ownerId = userService.getCurrentUserGroup().getOwnerId();
edRep.setIdOfOwner(ownerId);
validateEntityIdAndACSUrls(edRep);

EntityDescriptor ed = (EntityDescriptor) createDescriptorFromRepresentation(edRep);
ed.setIdOfOwner(userService.getCurrentUserGroup().getOwnerId());
ed.setIdOfOwner(ownerId);

return createRepresentationFromDescriptor(entityDescriptorRepository.save(ed));
}

Expand Down Expand Up @@ -331,14 +317,14 @@ public void delete(String resourceId) throws ForbiddenException, EntityNotFoundE
}
ownershipRepository.deleteEntriesForOwnedObject(ed);
entityDescriptorRepository.delete(ed);

}

@Override
public Iterable<EntityDescriptorRepresentation> getAllDisabledAndNotOwnedByAdmin() throws ForbiddenException {
if (!userService.currentUserIsAdmin()) {
throw new ForbiddenException();
}
}
return entityDescriptorRepository.findAllDisabledAndNotOwnedByAdmin().map(ed -> createRepresentationFromDescriptor(ed)).collect(Collectors.toList());
}

Expand Down Expand Up @@ -371,31 +357,34 @@ public EntityDescriptor getEntityDescriptorByResourceId(String resourceId) throw
}
if (!userService.isAuthorizedFor(ed)) {
throw new ForbiddenException();
}
}
return ed;
}

@Override
public Map<String, Object> getRelyingPartyOverridesRepresentationFromAttributeList(List<Attribute> attributeList) {
return ModelRepresentationConversions.getRelyingPartyOverridesRepresentationFromAttributeList(attributeList);
}

@Override
public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep) throws ForbiddenException, EntityNotFoundException {
public EntityDescriptorRepresentation update(EntityDescriptorRepresentation edRep)
throws ForbiddenException, EntityNotFoundException, InvalidUrlMatchException {
EntityDescriptor existingEd = entityDescriptorRepository.findByResourceId(edRep.getId());
if (existingEd == null) {
throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId()));
throw new EntityNotFoundException(String.format("The entity descriptor with entity id [%s] was not found for update.", edRep.getId()));
}
if (edRep.isServiceEnabled() && !userService.currentUserIsAdmin()) {
throw new ForbiddenException("You do not have the permissions necessary to enable this service.");
}
if (!userService.isAuthorizedFor(existingEd)) {
throw new ForbiddenException();
}
}
// Verify we're the only one attempting to update the EntityDescriptor
if (edRep.getVersion() != existingEd.hashCode()) {
throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", edRep.getId()));
throw new ConcurrentModificationException(String.format("A concurrent modification has occured on entity descriptor with entity id [%s]. Please refresh and try again", edRep.getId()));
}

validateEntityIdAndACSUrls(edRep);
updateDescriptorFromRepresentation(existingEd, edRep);
return createRepresentationFromDescriptor(entityDescriptorRepository.save(existingEd));
}
Expand All @@ -407,4 +396,21 @@ public void updateDescriptorFromRepresentation(org.opensaml.saml.saml2.metadata.
}
buildDescriptorFromRepresentation((EntityDescriptor) entityDescriptor, representation);
}
}

private void validateEntityIdAndACSUrls(EntityDescriptorRepresentation edRep) throws InvalidUrlMatchException {
// Check the entity id first
if (!groupService.doesUrlMatchGroupPattern(edRep.getIdOfOwner(), edRep.getEntityId())) {
throw new InvalidUrlMatchException("EntityId is not a pattern match to the group");
}

// Check the ACS locations
if (edRep.getAssertionConsumerServices() != null && edRep.getAssertionConsumerServices().size() > 0) {
for (AssertionConsumerServiceRepresentation acs : edRep.getAssertionConsumerServices()) {
if (!groupService.doesUrlMatchGroupPattern(edRep.getIdOfOwner(), acs.getLocationUrl())) {
throw new InvalidUrlMatchException(
"ACS location [ " + acs.getLocationUrl() + " ] is not a pattern match to the group");
}
}
}
}
}
Loading

0 comments on commit 3d32401

Please sign in to comment.