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

Ability to add and update SSP Impl Req #94

Merged
merged 8 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -4,13 +4,29 @@
import gov.nist.secauto.metaschema.binding.IBindingContext;
import gov.nist.secauto.metaschema.binding.io.json.IJsonWritingContext;
import gov.nist.secauto.metaschema.binding.model.DefaultAssemblyClassBinding;
import gov.nist.secauto.oscal.lib.model.ImplementedRequirement;
import java.io.IOException;
import java.util.Map;
import javax.xml.namespace.QName;
import org.apache.commons.collections4.map.HashedMap;
laurelmay marked this conversation as resolved.
Show resolved Hide resolved

/**
* Extends DefaultAssemblyClassBinding to add writing of root items as an array.
*/
public class IterableAssemblyClassBinding extends DefaultAssemblyClassBinding {

/**
* Map of objects that are not declared as root in liboscal-java,
* but we want to treat them as such.
*/
private static final Map<Class<?>, QName> secondaryRootObjects;

static {
secondaryRootObjects = new HashedMap<>();
secondaryRootObjects.put(ImplementedRequirement.class,
new QName("implemented-requirement"));
laurelmay marked this conversation as resolved.
Show resolved Hide resolved
}

laurelmay marked this conversation as resolved.
Show resolved Hide resolved
protected IterableAssemblyClassBinding(Class<?> clazz, IBindingContext bindingContext) {
super(clazz, bindingContext);
}
Expand Down Expand Up @@ -48,4 +64,18 @@ public void writeRootItems(

writer.writeEndArray();
}

@Override
public boolean isRoot() {
return (super.isRoot() || secondaryRootObjects.containsKey(getBoundClass()));
}

@Override
public QName getRootXmlQName() {
if (secondaryRootObjects.containsKey(getBoundClass())) {
return secondaryRootObjects.get(getBoundClass());
}
return super.getRootXmlQName();
}
laurelmay marked this conversation as resolved.
Show resolved Hide resolved

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import gov.nist.secauto.metaschema.binding.IBindingContext;
import gov.nist.secauto.metaschema.binding.io.BindingException;
import gov.nist.secauto.metaschema.binding.io.Feature;
import gov.nist.secauto.metaschema.binding.io.Format;
import gov.nist.secauto.metaschema.binding.io.IDeserializer;
import gov.nist.secauto.metaschema.binding.io.json.DefaultJsonDeserializer;
import gov.nist.secauto.metaschema.binding.model.IAssemblyClassBinding;
import java.io.InputStream;
import java.io.OutputStream;
Expand All @@ -33,7 +33,7 @@ public BaseOscalObjectMarshallerLiboscalImpl(Class<T> clazz) {
IterableAssemblyClassBinding.createInstance(clazz, context);
this.serializer = new IterableJsonSerializer<T>(context, classBinding);
this.serializer.enableFeature(Feature.SERIALIZE_ROOT);
this.deserializer = context.newDeserializer(Format.JSON, clazz);
this.deserializer = new DefaultJsonDeserializer<T>(context, classBinding);
this.deserializer.enableFeature(Feature.DESERIALIZE_ROOT);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.easydynamics.oscal.data.marshalling.impl;

import gov.nist.secauto.oscal.lib.model.ImplementedRequirement;

/**
* OSCAL SSP Implemented Requirement marshaller implementation.
*/
public class OscalSspImplReqMarshallerImpl
extends BaseOscalObjectMarshallerLiboscalImpl<ImplementedRequirement> {

public OscalSspImplReqMarshallerImpl() {
super(ImplementedRequirement.class);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import com.easydynamics.oscal.data.marshalling.impl.OscalCatalogMarshallerImpl;
import com.easydynamics.oscal.data.marshalling.impl.OscalComponentMarshallerImpl;
import com.easydynamics.oscal.data.marshalling.impl.OscalProfileMarshallerImpl;
import com.easydynamics.oscal.data.marshalling.impl.OscalSspImplReqMarshallerImpl;
import com.easydynamics.oscal.data.marshalling.impl.OscalSspMarshallerImpl;
import gov.nist.secauto.oscal.lib.model.Catalog;
import gov.nist.secauto.oscal.lib.model.ComponentDefinition;
import gov.nist.secauto.oscal.lib.model.ImplementedRequirement;
import gov.nist.secauto.oscal.lib.model.Profile;
import gov.nist.secauto.oscal.lib.model.SystemSecurityPlan;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -39,4 +41,9 @@ public OscalObjectMarshaller<Profile> profileMarshaller() {
public OscalObjectMarshaller<SystemSecurityPlan> sspMarshaller() {
return new OscalSspMarshallerImpl();
}

@Bean
public OscalObjectMarshaller<ImplementedRequirement> sspImplReqMarshaller() {
return new OscalSspImplReqMarshallerImpl();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ public abstract class BaseOscalController<T> {

private final Logger logger = LoggerFactory.getLogger(this.getClass());

private final BaseOscalObjectService<T> oscalObjectService;
private final OscalObjectMarshaller<T> oscalObjectMarshaller;
protected final BaseOscalObjectService<T> oscalObjectService;
protected final OscalObjectMarshaller<T> oscalObjectMarshaller;

protected BaseOscalController(
BaseOscalObjectService<T> oscalObjectService,
Expand Down Expand Up @@ -114,21 +114,21 @@ public ResponseEntity<StreamingResponseBody> put(String id, String json) {
return makeObjectResponse(oscalObjectService.save(incomingOscalObject));
}

private ResponseEntity<StreamingResponseBody> makeIterableResponse(
protected ResponseEntity<StreamingResponseBody> makeIterableResponse(
Iterable<T> oscalObjectCollection) {
return makeResponse(
(outputStream) -> oscalObjectMarshaller.toJson(oscalObjectCollection, outputStream),
oscalObjectCollection.getClass());
oscalObjectCollection.getClass());
}

private ResponseEntity<StreamingResponseBody> makeObjectResponse(T oscalObject) {
protected ResponseEntity<StreamingResponseBody> makeObjectResponse(T oscalObject) {
return makeResponse(
(outputStream) -> oscalObjectMarshaller.toJson(oscalObject, outputStream),
oscalObject.getClass());
oscalObject.getClass());
}

private ResponseEntity<StreamingResponseBody> makeResponse(
Consumer<OutputStream> marshallingTask,
protected ResponseEntity<StreamingResponseBody> makeResponse(
Consumer<OutputStream> marshallingTask,
Class<?> clazz) {

StreamingResponseBody responseBody = outputStream -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ String oscalObjectNotFoundHandler(OscalObjectNotFoundException exception) {

@ResponseBody
@ExceptionHandler(OscalObjectConflictException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ResponseStatus(HttpStatus.CONFLICT)
laurelmay marked this conversation as resolved.
Show resolved Hide resolved
String oscalObjectConflictHandler(OscalObjectConflictException exception) {
return exception.getMessage();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@

import com.easydynamics.oscal.data.marshalling.OscalObjectMarshaller;
import com.easydynamics.oscal.service.OscalSspService;
import com.easydynamics.oscal.service.impl.OscalDeepCopyUtils;
import gov.nist.secauto.oscal.lib.model.ControlImplementation;
import gov.nist.secauto.oscal.lib.model.ImplementedRequirement;
import gov.nist.secauto.oscal.lib.model.SystemSecurityPlan;
import io.swagger.v3.oas.annotations.Parameter;
import java.io.ByteArrayInputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -23,13 +35,17 @@
@RequestMapping(path = "/oscal/v1")
@RestController
public class SspController extends BaseOscalController<SystemSecurityPlan> {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final OscalObjectMarshaller<ImplementedRequirement> oscalSspImplReqtMarshaller;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private final OscalObjectMarshaller<ImplementedRequirement> oscalSspImplReqtMarshaller;
private final OscalObjectMarshaller<ImplementedRequirement> oscalSspImplReqMarshaller;

Maybe change this to match the naming convention of "Impl Req" used elsewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed


@Autowired(required = true)
public SspController(
OscalSspService sspService,
OscalObjectMarshaller<SystemSecurityPlan> marshaller
OscalObjectMarshaller<SystemSecurityPlan> marshaller,
OscalObjectMarshaller<ImplementedRequirement> oscalSspImplReqtMarshaller
laurelmay marked this conversation as resolved.
Show resolved Hide resolved
) {
super(sspService, marshaller);
this.oscalSspImplReqtMarshaller = oscalSspImplReqtMarshaller;
}

@GetMapping("/system-security-plans")
Expand Down Expand Up @@ -75,4 +91,122 @@ public ResponseEntity<StreamingResponseBody> put(
@RequestBody String json) {
return super.put(id, json);
}

/**
* Similar to unmarshallAndValidateId, checks that the given id
* matches the UUID in the given json.
*
* @param id the request path id
* @param json the request body json
* @return the unmarshalled object
* @throws OscalObjectConflictException when the path ID does not match the body ID
*/
protected ImplementedRequirement unmarshallImplReqAndValidateId(String id, String json) {
ImplementedRequirement incomingOscalObject = oscalSspImplReqtMarshaller.toObject(
new ByteArrayInputStream(json.getBytes()));

UUID incomingUuid = incomingOscalObject.getUuid();
if (incomingUuid != null && !id.equals(incomingUuid.toString())) {
throw new OscalObjectConflictException(incomingUuid.toString(), id);
}

return incomingOscalObject;
}

/**
* Does the work of finding an existing SSP and updating it with the
* given Implemented Requirement.
*
* @param id the SSP UUID
* @param implementedRequirementId the impl req UUID
* @param json the Implemented Requirement JSON
* @param isCreateOnly requires no impl req with the same UUID exist when true
* @return the response
*/
private ResponseEntity<StreamingResponseBody> updateImplementedRequirement(
String id,
String implementedRequirementId,
String json,
boolean isCreateOnly) {
SystemSecurityPlan existingSsp = oscalObjectService.findById(id)
.orElseThrow(() -> new OscalObjectNotFoundException(id));

ImplementedRequirement incomingImplReq =
unmarshallImplReqAndValidateId(implementedRequirementId, json);

// Find existing ImplementedRequirement if exists and merge or add
ImplementedRequirement existingImplReq = null;
if (existingSsp.getControlImplementation() != null
&& existingSsp.getControlImplementation().getImplementedRequirements() != null) {
laurelmay marked this conversation as resolved.
Show resolved Hide resolved
existingImplReq = existingSsp.getControlImplementation().getImplementedRequirements().stream()
.filter(implReq -> incomingImplReq.getUuid().equals(implReq.getUuid()))
.findAny()
laurelmay marked this conversation as resolved.
Show resolved Hide resolved
.orElse(null);
}
if (existingImplReq != null && isCreateOnly) {
throw new OscalObjectConflictException("Implented Requirement already exists");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new OscalObjectConflictException("Implented Requirement already exists");
throw new OscalObjectConflictException("Implemented Requirement already exists");

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed

}
if (existingImplReq == null) {
ControlImplementation controlImplementation = existingSsp.getControlImplementation();
if (controlImplementation == null) {
controlImplementation = new ControlImplementation();
existingSsp.setControlImplementation(controlImplementation);
}
List<ImplementedRequirement> implReqs = controlImplementation.getImplementedRequirements();
if (implReqs == null) {
implReqs = new ArrayList<>();
controlImplementation.setImplementedRequirements(implReqs);
}
implReqs.add(incomingImplReq);
} else {
try {
OscalDeepCopyUtils.deepCopyProperties(existingImplReq, incomingImplReq);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new InvalidDataAccessResourceUsageException(
"could not deep copy object", e);
}
}

logger.debug("SSP ImplementedRequiremnt updated, saving via service");

return makeObjectResponse(oscalObjectService.save(existingSsp));
}

/**
* Defines a POST request for updating SSPs control implementation
* implemented requirements.
*
* @param id the SSP uuid
* @param implementedRequirementId the Implemented Requirement uuid
* @param json the SSP contents
*/
@PostMapping(value = "/system-security-plans/{id}/control-implementation/"
+ "implemented-requirements/{implementedRequirementId}",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the API spec, the POST request does not include the implementedRequirementID as a parameter.

Kind of annoying because the ID validation cannot happen in the marshaller - the functionality probably needs to be separated between the POST and PUT.

consumes = { MediaType.APPLICATION_JSON_VALUE },
produces = { MediaType.APPLICATION_JSON_VALUE })
public ResponseEntity<StreamingResponseBody> updateImplementedRequirementPost(
@Parameter @PathVariable String id,
@Parameter @PathVariable String implementedRequirementId,
@RequestBody String json) {
return updateImplementedRequirement(id, implementedRequirementId, json, true);
}

/**
* Defines a PUT request for updating SSPs control implementation
* implemented requirements.
*
* @param id the SSP uuid
* @param implementedRequirementId the Implemented Requirement uuid
* @param json the SSP contents
*/
@PutMapping(value = "/system-security-plans/{id}/control-implementation/"
+ "implemented-requirements/{implementedRequirementId}",
consumes = { MediaType.APPLICATION_JSON_VALUE },
produces = { MediaType.APPLICATION_JSON_VALUE })
public ResponseEntity<StreamingResponseBody> updateImplementedRequirementPut(
@Parameter @PathVariable String id,
@Parameter @PathVariable String implementedRequirementId,
@RequestBody String json) {
return updateImplementedRequirement(id, implementedRequirementId, json, false);
laurelmay marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading