diff --git a/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java b/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java index 28f941859..6ac03b719 100644 --- a/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/PolicyConditionResource.java @@ -29,17 +29,6 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Validator; -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.DELETE; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.PUT; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.PathParam; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Policy; @@ -53,8 +42,17 @@ import org.projectnessie.cel.common.CELError; import org.projectnessie.cel.tools.ScriptCreateException; -import javax.jdo.FetchPlan; -import javax.jdo.PersistenceManager; +import jakarta.validation.Validator; +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import java.util.ArrayList; import java.util.Map; @@ -105,7 +103,12 @@ public Response createPolicyCondition( final PolicyCondition pc = qm.createPolicyCondition(policy, jsonPolicyCondition.getSubject(), jsonPolicyCondition.getOperator(), StringUtils.trimToNull(jsonPolicyCondition.getValue()), jsonPolicyCondition.getViolationType()); - return Response.status(Response.Status.CREATED).entity(detachConditions(qm, pc)).build(); + + // Prevent infinite recursion during JSON serialization. + qm.makeTransient(pc); + pc.setPolicy(null); + + return Response.status(Response.Status.CREATED).entity(pc).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the policy could not be found.").build(); } @@ -140,7 +143,12 @@ public Response updatePolicyCondition(PolicyCondition jsonPolicyCondition) { if (pc != null) { maybeValidateExpression(jsonPolicyCondition); pc = qm.updatePolicyCondition(jsonPolicyCondition); - return Response.status(Response.Status.CREATED).entity(pc).build(); + + // Prevent infinite recursion during JSON serialization. + qm.makeTransient(pc); + pc.setPolicy(null); + + return Response.status(Response.Status.OK).entity(pc).build(); } else { return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the policy condition could not be found.").build(); } @@ -196,10 +204,4 @@ private void maybeValidateExpression(final PolicyCondition policyCondition) { } } - private PolicyCondition detachConditions(final QueryManager qm, final PolicyCondition policyCondition) { - final PersistenceManager pm = qm.getPersistenceManager(); - pm.getFetchPlan().setMaxFetchDepth(1); // Ensure policyCondition from policy is not included - pm.getFetchPlan().setDetachmentOptions(FetchPlan.DETACH_LOAD_FIELDS); - return qm.getPersistenceManager().detachCopy(policyCondition); - } } diff --git a/src/test/java/org/dependencytrack/resources/v1/PolicyConditionResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/PolicyConditionResourceTest.java index 822040223..67c3fe3be 100644 --- a/src/test/java/org/dependencytrack/resources/v1/PolicyConditionResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/PolicyConditionResourceTest.java @@ -20,19 +20,28 @@ import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; +import alpine.server.filters.AuthorizationFilter; import org.dependencytrack.JerseyTestRule; import org.dependencytrack.ResourceTest; +import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.Policy; +import org.dependencytrack.model.Policy.Operator; +import org.dependencytrack.model.Policy.ViolationState; import org.dependencytrack.model.PolicyCondition; import org.glassfish.jersey.server.ResourceConfig; import org.junit.ClassRule; import org.junit.Test; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import javax.jdo.JDOObjectNotFoundException; +import java.util.UUID; + import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.hamcrest.CoreMatchers.equalTo; public class PolicyConditionResourceTest extends ResourceTest { @@ -40,16 +49,84 @@ public class PolicyConditionResourceTest extends ResourceTest { public static JerseyTestRule jersey = new JerseyTestRule( new ResourceConfig(PolicyConditionResource.class) .register(ApiFilter.class) - .register(AuthenticationFilter.class)); + .register(AuthenticationFilter.class) + .register(AuthorizationFilter.class)); + + @Test + public void testCreateCondition() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final var policy = new Policy(); + policy.setName("foo"); + policy.setOperator(Operator.ANY); + policy.setViolationState(ViolationState.INFO); + qm.persist(policy); + + final Response response = jersey.target("%s/%s/condition".formatted(V1_POLICY, policy.getUuid())) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "subject": "PACKAGE_URL", + "operator": "MATCHES", + "value": "pkg:maven/foo/bar" + } + """)); + assertThat(response.getStatus()).isEqualTo(201); + assertThatJson(getPlainTextBody(response)).isEqualTo(/* language=JSON */ """ + { + "uuid": "${json-unit.any-string}", + "subject": "PACKAGE_URL", + "operator": "MATCHES", + "value": "pkg:maven/foo/bar", + "violationType": "OPERATIONAL" + } + """); + } + + @Test + public void testCreateConditionWhenPolicyDoesNotExist() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final Response response = jersey.target("%s/cec42e01-62a7-4c86-9b8f-cd6650be2888/condition".formatted(V1_POLICY)) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "subject": "PACKAGE_URL", + "operator": "MATCHES", + "value": "pkg:maven/foo/bar" + } + """)); + assertThat(response.getStatus()).isEqualTo(404); + assertThat(getPlainTextBody(response)).isEqualTo("The UUID of the policy could not be found."); + } + + @Test + public void testCreateConditionWhenUnauthorized() { + final Response response = jersey.target("%s/cec42e01-62a7-4c86-9b8f-cd6650be2888/condition".formatted(V1_POLICY)) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(/* language=JSON */ """ + { + "subject": "PACKAGE_URL", + "operator": "MATCHES", + "value": "pkg:maven/foo/bar" + } + """)); + assertThat(response.getStatus()).isEqualTo(403); + } @Test - public void testCreateExpressionCondition() { - final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.FAIL); + public void testCreateConditionWithExpression() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final Policy policy = qm.createPolicy("policy", Operator.ANY, ViolationState.FAIL); final Response response = jersey.target("%s/%s/condition".formatted(V1_POLICY, policy.getUuid())) .request() .header(X_API_KEY, apiKey) - .put(Entity.entity(""" + .put(Entity.entity(/* language=JSON */ """ { "subject": "EXPRESSION", "value": "component.name == \\"foo\\"", @@ -57,18 +134,9 @@ public void testCreateExpressionCondition() { } """, MediaType.APPLICATION_JSON)); assertThat(response.getStatus()).isEqualTo(201); - var response1 = getPlainTextBody(response); - assertThatJson(response1) - .isEqualTo(""" + assertThatJson(getPlainTextBody(response)) + .isEqualTo(/* language=JSON */ """ { - "policy":{ - "name":"policy", - "operator":"ANY", - "violationState":"FAIL", - "uuid":"${json-unit.any-string}", - "includeChildren":false, - "global":true - }, "uuid": "${json-unit.any-string}", "subject": "EXPRESSION", "operator": "MATCHES", @@ -79,13 +147,15 @@ public void testCreateExpressionCondition() { } @Test - public void testCreateExpressionConditionWithError() { - final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.FAIL); + public void testCreateConditionWithInvalidExpression() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final Policy policy = qm.createPolicy("policy", Operator.ANY, ViolationState.FAIL); final Response response = jersey.target("%s/%s/condition".formatted(V1_POLICY, policy.getUuid())) .request() .header(X_API_KEY, apiKey) - .put(Entity.entity(""" + .put(Entity.entity(/* language=JSON */ """ { "subject": "EXPRESSION", "value": "component.doesNotExist == \\"foo\\"", @@ -95,7 +165,7 @@ public void testCreateExpressionConditionWithError() { assertThat(response.getStatus()).isEqualTo(400); assertThatJson(getPlainTextBody(response)) - .isEqualTo(""" + .isEqualTo(/* language=JSON */ """ { "celErrors": [ { @@ -109,15 +179,94 @@ public void testCreateExpressionConditionWithError() { } @Test - public void testUpdateExpressionCondition() { - final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.FAIL); + public void testUpdateCondition() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final var policy = new Policy(); + policy.setName("foo"); + policy.setOperator(Operator.ANY); + policy.setViolationState(ViolationState.INFO); + qm.persist(policy); + + final var condition = new PolicyCondition(); + condition.setPolicy(policy); + condition.setSubject(PolicyCondition.Subject.PACKAGE_URL); + condition.setOperator(PolicyCondition.Operator.MATCHES); + condition.setValue("pkg:maven/foo/bar"); + qm.persist(condition); + + final Response response = jersey.target("%s/condition".formatted(V1_POLICY)) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "uuid": "%s", + "subject": "SEVERITY", + "operator": "IS", + "value": "HIGH" + } + """.formatted(condition.getUuid()))); + assertThat(response.getStatus()).isEqualTo(200); + assertThatJson(getPlainTextBody(response)) + .withMatcher("conditionUuid", equalTo(condition.getUuid().toString())) + .isEqualTo(/* language=JSON */ """ + { + "uuid": "${json-unit.matches:conditionUuid}", + "subject": "SEVERITY", + "operator": "IS", + "value": "HIGH", + "violationType": "SECURITY" + } + """); + } + + @Test + public void testUpdateConditionWhenConditionDoesNotExist() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final Response response = jersey.target("%s/condition".formatted(V1_POLICY)) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "uuid": "8683b1db-96a3-4014-baf8-03e8cab8c647", + "subject": "SEVERITY", + "operator": "IS", + "value": "HIGH" + } + """)); + assertThat(response.getStatus()).isEqualTo(404); + assertThat(getPlainTextBody(response)).isEqualTo("The UUID of the policy condition could not be found."); + } + + @Test + public void testUpdateConditionWhenUnauthorized() { + final Response response = jersey.target("%s/condition".formatted(V1_POLICY)) + .request() + .header(X_API_KEY, apiKey) + .post(Entity.json(/* language=JSON */ """ + { + "uuid": "8683b1db-96a3-4014-baf8-03e8cab8c647", + "subject": "SEVERITY", + "operator": "IS", + "value": "HIGH" + } + """)); + assertThat(response.getStatus()).isEqualTo(403); + } + + @Test + public void testUpdateConditionWithExpression() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final Policy policy = qm.createPolicy("policy", Operator.ANY, ViolationState.FAIL); final PolicyCondition condition = qm.createPolicyCondition(policy, PolicyCondition.Subject.VULNERABILITY_ID, PolicyCondition.Operator.IS, "foobar"); final Response response = jersey.target("%s/condition".formatted(V1_POLICY)) .request() .header(X_API_KEY, apiKey) - .post(Entity.entity(""" + .post(Entity.entity(/* language=JSON */ """ { "uuid": "%s", "subject": "EXPRESSION", @@ -126,9 +275,9 @@ public void testUpdateExpressionCondition() { } """.formatted(condition.getUuid()), MediaType.APPLICATION_JSON)); - assertThat(response.getStatus()).isEqualTo(201); + assertThat(response.getStatus()).isEqualTo(200); assertThatJson(getPlainTextBody(response)) - .isEqualTo(""" + .isEqualTo(/* language=JSON */ """ { "uuid": "${json-unit.any-string}", "subject": "EXPRESSION", @@ -140,15 +289,17 @@ public void testUpdateExpressionCondition() { } @Test - public void testUpdateExpressionConditionWithError() { - final Policy policy = qm.createPolicy("policy", Policy.Operator.ANY, Policy.ViolationState.FAIL); + public void testUpdateConditionWithInvalidExpression() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final Policy policy = qm.createPolicy("policy", Operator.ANY, ViolationState.FAIL); final PolicyCondition condition = qm.createPolicyCondition(policy, PolicyCondition.Subject.VULNERABILITY_ID, PolicyCondition.Operator.IS, "foobar"); final Response response = jersey.target("%s/condition".formatted(V1_POLICY)) .request() .header(X_API_KEY, apiKey) - .post(Entity.entity(""" + .post(Entity.entity(/* language=JSON */ """ { "uuid": "%s", "subject": "EXPRESSION", @@ -159,7 +310,7 @@ public void testUpdateExpressionConditionWithError() { assertThat(response.getStatus()).isEqualTo(400); assertThatJson(getPlainTextBody(response)) - .isEqualTo(""" + .isEqualTo(/* language=JSON */ """ { "celErrors": [ { @@ -172,4 +323,54 @@ public void testUpdateExpressionConditionWithError() { """); } + @Test + public void testDeleteCondition() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final var policy = new Policy(); + policy.setName("foo"); + policy.setOperator(Operator.ANY); + policy.setViolationState(ViolationState.INFO); + qm.persist(policy); + + final var condition = new PolicyCondition(); + condition.setPolicy(policy); + condition.setSubject(PolicyCondition.Subject.PACKAGE_URL); + condition.setOperator(PolicyCondition.Operator.MATCHES); + condition.setValue("pkg:maven/foo/bar"); + qm.persist(condition); + + final Response response = jersey.target("%s/condition/%s".formatted(V1_POLICY, condition.getUuid())) + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(204); + assertThat(getPlainTextBody(response)).isEmpty(); + + qm.getPersistenceManager().evictAll(); + assertThatExceptionOfType(JDOObjectNotFoundException.class) + .isThrownBy(() -> qm.getObjectById(PolicyCondition.class, condition.getId())); + } + + @Test + public void testDeleteConditionWhenConditionDoesNotExist() { + initializeWithPermissions(Permissions.POLICY_MANAGEMENT_UPDATE); + + final Response response = jersey.target("%s/condition/%s".formatted(V1_POLICY, UUID.randomUUID())) + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(404); + assertThat(getPlainTextBody(response)).isEqualTo("The UUID of the policy condition could not be found."); + } + + @Test + public void testDeleteConditionWhenUnauthorized() { + final Response response = jersey.target("%s/condition/%s".formatted(V1_POLICY, UUID.randomUUID())) + .request() + .header(X_API_KEY, apiKey) + .delete(); + assertThat(response.getStatus()).isEqualTo(403); + } + } \ No newline at end of file