From b0662c3ecca5c1fdf5b04e7b6e5d983c4a1bf7d1 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 1 Mar 2023 18:52:22 +0200 Subject: [PATCH 1/6] Updated documentation for POST /v1/bom Added a sample of autocreating project as a child to some other project/version Signed-off-by: Anton Soroka --- docs/_docs/usage/cicd.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/_docs/usage/cicd.md b/docs/_docs/usage/cicd.md index 006872e44f..173b5a0285 100644 --- a/docs/_docs/usage/cicd.md +++ b/docs/_docs/usage/cicd.md @@ -73,3 +73,17 @@ curl -X "POST" "http://dtrack.example.com/api/v1/bom" \ -F "projectVersion=xxxx" \ -F "bom=@target/bom.xml" ``` + +You can also create a project as a child to some other project if you add `parentUUID` or `parentName` parameters. + +```bash +curl -X "POST" "http://dtrack.example.com/api/v1/bom" \ + -H 'Content-Type: multipart/form-data' \ + -H "X-Api-Key: xxxxxxx" \ + -F "autoCreate=true" \ + -F "projectName=xxxx" \ + -F "projectVersion=xxxx.SNAPSHOT" \ + -F "parentName=xxxx" \ + -F "parentVersion=xxxx" \ + -F "bom=@target/bom.xml" +``` From 3c35c851effa8a2ac3811e5fb2e8832ce6c25a14 Mon Sep 17 00:00:00 2001 From: Anton Date: Wed, 1 Mar 2023 18:23:07 +0200 Subject: [PATCH 2/6] Extend POST /api/v1/bom (uploadBom) to select ParentProject for autocreated projects. Added 3 params: parentName, parentVersion, parentUUID. If parentUUID is specified, it is used to find parent project and attach it to newly created one. If not, parentName and parentVersion are used. If parentUUID or parentName were specified but such project does not exist, 404 is returned. Signed-off-by: Anton Soroka --- .../resources/v1/BomResource.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 636c7a2ce4..33a1dc351a 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -251,6 +251,9 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, @DefaultValue("false") @FormDataParam("autoCreate") boolean autoCreate, @FormDataParam("projectName") String projectName, @FormDataParam("projectVersion") String projectVersion, + @FormDataParam("parentName") String parentName, + @FormDataParam("parentVersion") String parentVersion, + @FormDataParam("parentUUID") String parentUUID, final FormDataMultiPart multiPart) { final List artifactParts = multiPart.getFields("bom"); @@ -266,7 +269,21 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, Project project = qm.getProject(trimmedProjectName, trimmedProjectVersion); if (project == null && autoCreate) { if (hasPermission(Permissions.Constants.PORTFOLIO_MANAGEMENT) || hasPermission(Permissions.Constants.PROJECT_CREATION_UPLOAD)) { - project = qm.createProject(trimmedProjectName, null, trimmedProjectVersion, null, null, null, true, true); + Project parent = null; + if (parentUUID != null || parentName != null) { + if (parentUUID != null) { + parent = qm.getObjectByUuid(Project.class, parentUUID); + } else { + final String trimmedParentName = StringUtils.trimToNull(parentName); + final String trimmedParentVersion = StringUtils.trimToNull(parentVersion); + parent = qm.getProject(trimmedParentName, trimmedParentVersion); + } + + if (parent == null) { // if parent project is specified but not found + return Response.status(Response.Status.NOT_FOUND).entity("The parent component could not be found.").build(); + } + } + project = qm.createProject(trimmedProjectName, null, trimmedProjectVersion, null, parent, null, true, true); Principal principal = getPrincipal(); qm.updateNewProjectACL(project, principal); } else { From 0a461b69da1853ba6577f60446978e84ce2c7d5e Mon Sep 17 00:00:00 2001 From: Anton Soroka Date: Fri, 17 Mar 2023 17:39:08 +0200 Subject: [PATCH 3/6] Added access control check for parent project Signed-off-by: Anton Soroka --- .../java/org/dependencytrack/resources/v1/BomResource.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index 33a1dc351a..cb17dae891 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -272,6 +272,7 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, Project parent = null; if (parentUUID != null || parentName != null) { if (parentUUID != null) { + parent = qm.getObjectByUuid(Project.class, parentUUID); } else { final String trimmedParentName = StringUtils.trimToNull(parentName); @@ -281,6 +282,8 @@ public Response uploadBom(@FormDataParam("project") String projectUuid, if (parent == null) { // if parent project is specified but not found return Response.status(Response.Status.NOT_FOUND).entity("The parent component could not be found.").build(); + } else if (! qm.hasAccess(super.getPrincipal(), parent)) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified parent project is forbidden").build(); } } project = qm.createProject(trimmedProjectName, null, trimmedProjectVersion, null, parent, null, true, true); From 0a0dd4015882706f2dff18905f8c6785c61f5970 Mon Sep 17 00:00:00 2001 From: Anton Soroka Date: Fri, 17 Mar 2023 20:03:37 +0200 Subject: [PATCH 4/6] Added a test for bom upload with autocreation of project as a child. Signed-off-by: Anton Soroka --- .../resources/v1/vo/BomSubmitRequest.java | 25 +++++++++++ .../resources/v1/BomResourceTest.java | 44 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java index 4729e40ce7..8399e6f8a2 100644 --- a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java +++ b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java @@ -50,22 +50,47 @@ public final class BomSubmitRequest { @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The project version may only contain printable characters") private final String projectVersion; + @Pattern(regexp = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", message = "The parent UUID must be a valid 36 character UUID") + private final String parentUUID; + + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The parent name may only contain printable characters") + private final String parentName; + + @JsonDeserialize(using = TrimmedStringDeserializer.class) + @Pattern(regexp = RegexSequence.Definition.PRINTABLE_CHARS, message = "The parent version may only contain printable characters") + private final String parentVersion; + @NotNull @Pattern(regexp = "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$", message = "The BOM must be Base64 encoded") private final String bom; private final boolean autoCreate; + public BomSubmitRequest(String project, + String projectName, + String projectVersion, + boolean autoCreate, + String bom) { + this(project, projectName, projectVersion, autoCreate, null, null, null, bom); + } + @JsonCreator public BomSubmitRequest(@JsonProperty(value = "project", required = false) String project, @JsonProperty(value = "projectName", required = false) String projectName, @JsonProperty(value = "projectVersion", required = false) String projectVersion, @JsonProperty(value = "autoCreate", required = false) boolean autoCreate, + @JsonProperty(value = "parentUUID", required = false) String parentUUID, + @JsonProperty(value = "parentName", required = false) String parentName, + @JsonProperty(value = "parentVersion", required = false) String parentVersion, @JsonProperty(value = "bom", required = true) String bom) { this.project = project; this.projectName = projectName; this.projectVersion = projectVersion; this.autoCreate = autoCreate; + this.parentUUID = parentUUID; + this.parentName = parentName; + this.parentVersion = parentVersion; this.bom = bom; } diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index 58f2377bfd..a4acac02e6 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -561,4 +561,48 @@ public void uploadBomUnauthorizedTest() throws Exception { Assert.assertEquals("The principal does not have permission to create project.", body); } + @Test + public void uploadBomAutoCreateTestWithParent() throws Exception { + initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); + File file = new File(Thread.currentThread().getContextClassLoader().getResource("bom-1.xml").toURI()); + String bomString = Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(file)); + // Upload parent project + BomSubmitRequest request = new BomSubmitRequest(null, "Acme Parent", "1.0", true, bomString); + Response response = target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject json = parseJsonObject(response); + Assert.assertNotNull(json); + String parentUUID = json.getString("token"); + Assert.assertNotNull(parentUUID); + Assert.assertTrue(UuidUtil.isValidUUID(parentUUID)); + + // Upload first child, search parent by UUID + request = new BomSubmitRequest(null, "Acme Example", "1.0", true, parentUUID, null, null, bomString); + response = target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus(), 0); + json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertNotNull(json.getString("token")); + Assert.assertTrue(UuidUtil.isValidUUID(json.getString("token"))); + Project child = qm.getProject("Acme Example", "1.0"); + Assert.assertNotNull(child); + + // Upload second child, search parent by name+ver + request = new BomSubmitRequest(null, "Acme Example", "2.0", true, null, "Acme Parent", "1.0", bomString); + response = target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus(), 0); + json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertNotNull(json.getString("token")); + Assert.assertTrue(UuidUtil.isValidUUID(json.getString("token"))); + child = qm.getProject("Acme Example", "2.0"); + Assert.assertNotNull(child); + } + } From e1990e3abb362e0d604bfadd6b9f5f27d7fddbde Mon Sep 17 00:00:00 2001 From: Anton Soroka Date: Mon, 27 Mar 2023 18:56:06 +0300 Subject: [PATCH 5/6] Fixed tests Also extended PUT method uploadBom with parent. Signed-off-by: Anton Soroka --- .../resources/v1/BomResource.java | 24 ++++++++- .../resources/v1/vo/BomSubmitRequest.java | 9 ++++ .../resources/v1/BomResourceTest.java | 50 +++++++++++++++++-- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/BomResource.java b/src/main/java/org/dependencytrack/resources/v1/BomResource.java index cb17dae891..b770904145 100644 --- a/src/main/java/org/dependencytrack/resources/v1/BomResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/BomResource.java @@ -225,7 +225,29 @@ public Response uploadBom(BomSubmitRequest request) { Project project = qm.getProject(request.getProjectName(), request.getProjectVersion()); if (project == null && request.isAutoCreate()) { if (hasPermission(Permissions.Constants.PORTFOLIO_MANAGEMENT) || hasPermission(Permissions.Constants.PROJECT_CREATION_UPLOAD)) { - project = qm.createProject(StringUtils.trimToNull(request.getProjectName()), null, StringUtils.trimToNull(request.getProjectVersion()), null, null, null, true, true); + Project parent = null; + if (request.getParentUUID() != null || request.getParentName() != null) { + if (request.getParentUUID() != null) { + failOnValidationError(validator.validateProperty(request, "parentUUID")); + parent = qm.getObjectByUuid(Project.class, request.getParentUUID()); + } else { + failOnValidationError( + validator.validateProperty(request, "parentName"), + validator.validateProperty(request, "parentVersion") + ); + final String trimmedParentName = StringUtils.trimToNull(request.getParentName()); + final String trimmedParentVersion = StringUtils.trimToNull(request.getParentVersion()); + parent = qm.getProject(trimmedParentName, trimmedParentVersion); + } + + if (parent == null) { // if parent project is specified but not found + return Response.status(Response.Status.NOT_FOUND).entity("The parent component could not be found.").build(); + } else if (! qm.hasAccess(super.getPrincipal(), parent)) { + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified parent project is forbidden").build(); + } + } + + project = qm.createProject(StringUtils.trimToNull(request.getProjectName()), null, StringUtils.trimToNull(request.getProjectVersion()), null, parent, null, true, true); Principal principal = getPrincipal(); qm.updateNewProjectACL(project, principal); } else { diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java index 8399e6f8a2..5feb40b5c0 100644 --- a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java +++ b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java @@ -105,6 +105,15 @@ public String getProjectName() { public String getProjectVersion() { return projectVersion; } + public String getParentUUID() { + return parentUUID; + } + public String getParentName() { + return parentName; + } + public String getParentVersion() { + return parentVersion; + } public boolean isAutoCreate() { return autoCreate; diff --git a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java index a4acac02e6..a5cb04a9ca 100644 --- a/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/BomResourceTest.java @@ -562,7 +562,7 @@ public void uploadBomUnauthorizedTest() throws Exception { } @Test - public void uploadBomAutoCreateTestWithParent() throws Exception { + public void uploadBomAutoCreateTestWithParentTest() throws Exception { initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); File file = new File(Thread.currentThread().getContextClassLoader().getResource("bom-1.xml").toURI()); String bomString = Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(file)); @@ -574,9 +574,9 @@ public void uploadBomAutoCreateTestWithParent() throws Exception { Assert.assertEquals(200, response.getStatus(), 0); JsonObject json = parseJsonObject(response); Assert.assertNotNull(json); - String parentUUID = json.getString("token"); - Assert.assertNotNull(parentUUID); - Assert.assertTrue(UuidUtil.isValidUUID(parentUUID)); + Project parent = qm.getProject("Acme Parent", "1.0"); + Assert.assertNotNull(parent); + String parentUUID = parent.getUuid().toString(); // Upload first child, search parent by UUID request = new BomSubmitRequest(null, "Acme Example", "1.0", true, parentUUID, null, null, bomString); @@ -590,6 +590,9 @@ public void uploadBomAutoCreateTestWithParent() throws Exception { Assert.assertTrue(UuidUtil.isValidUUID(json.getString("token"))); Project child = qm.getProject("Acme Example", "1.0"); Assert.assertNotNull(child); + Assert.assertNotNull(child.getParent()); + Assert.assertEquals(parentUUID, child.getParent().getUuid().toString()); + // Upload second child, search parent by name+ver request = new BomSubmitRequest(null, "Acme Example", "2.0", true, null, "Acme Parent", "1.0", bomString); @@ -603,6 +606,45 @@ public void uploadBomAutoCreateTestWithParent() throws Exception { Assert.assertTrue(UuidUtil.isValidUUID(json.getString("token"))); child = qm.getProject("Acme Example", "2.0"); Assert.assertNotNull(child); + Assert.assertNotNull(child.getParent()); + Assert.assertEquals(parentUUID, child.getParent().getUuid().toString()); + + // Upload third child, specify parent's UUID, name, ver. Name and ver are ignored when UUID is specified. + request = new BomSubmitRequest(null, "Acme Example", "3.0", true, parentUUID, "Non-existent parent", "1.0", bomString); + response = target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request, MediaType.APPLICATION_JSON)); + Assert.assertEquals(200, response.getStatus(), 0); + json = parseJsonObject(response); + Assert.assertNotNull(json); + Assert.assertNotNull(json.getString("token")); + Assert.assertTrue(UuidUtil.isValidUUID(json.getString("token"))); + child = qm.getProject("Acme Example", "3.0"); + Assert.assertNotNull(child); + Assert.assertNotNull(child.getParent()); + Assert.assertEquals(parentUUID, child.getParent().getUuid().toString()); + } + + @Test + public void uploadBomInvalidParentTest() throws Exception { + initializeWithPermissions(Permissions.BOM_UPLOAD, Permissions.PROJECT_CREATION_UPLOAD); + File file = new File(Thread.currentThread().getContextClassLoader().getResource("bom-1.xml").toURI()); + String bomString = Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(file)); + BomSubmitRequest request = new BomSubmitRequest(null, "Acme Example", "1.0", true, UUID.randomUUID().toString(), null, null, bomString); + Response response = target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request, MediaType.APPLICATION_JSON)); + Assert.assertEquals(404, response.getStatus(), 0); + String body = getPlainTextBody(response); + Assert.assertEquals("The parent component could not be found.", body); + + request = new BomSubmitRequest(null, "Acme Example", "2.0", true, null, "Non-existent parent", null, bomString); + response = target(V1_BOM).request() + .header(X_API_KEY, apiKey) + .put(Entity.entity(request, MediaType.APPLICATION_JSON)); + Assert.assertEquals(404, response.getStatus(), 0); + body = getPlainTextBody(response); + Assert.assertEquals("The parent component could not be found.", body); } } From 9c4c33902f97366345f4bb1e93eb571629ef3d01 Mon Sep 17 00:00:00 2001 From: Anton Soroka Date: Mon, 27 Mar 2023 19:59:36 +0300 Subject: [PATCH 6/6] Codestyle correction Signed-off-by: Anton Soroka --- .../org/dependencytrack/resources/v1/vo/BomSubmitRequest.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java index 5feb40b5c0..7021bca27c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java +++ b/src/main/java/org/dependencytrack/resources/v1/vo/BomSubmitRequest.java @@ -105,12 +105,15 @@ public String getProjectName() { public String getProjectVersion() { return projectVersion; } + public String getParentUUID() { return parentUUID; } + public String getParentName() { return parentName; } + public String getParentVersion() { return parentVersion; }