From 1737c455a29300cddcf971ffd531fe4c5ef3617a Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Fri, 23 Aug 2024 14:01:06 +0200 Subject: [PATCH 01/13] feat: Team Selection Added additional API point to get teams Initial Team is given to the project Signed-off-by: Thomas Schauer-Koeckeis --- .../dependencytrack/model/AvailableTeams.java | 56 +++ .../org/dependencytrack/model/LittleTeam.java | 53 +++ .../org/dependencytrack/model/Project.java | 39 +- .../resources/v1/ProjectResource.java | 448 ++++++++---------- .../resources/v1/TeamResource.java | 197 ++++---- 5 files changed, 444 insertions(+), 349 deletions(-) create mode 100644 src/main/java/org/dependencytrack/model/AvailableTeams.java create mode 100644 src/main/java/org/dependencytrack/model/LittleTeam.java diff --git a/src/main/java/org/dependencytrack/model/AvailableTeams.java b/src/main/java/org/dependencytrack/model/AvailableTeams.java new file mode 100644 index 0000000000..1a4522166f --- /dev/null +++ b/src/main/java/org/dependencytrack/model/AvailableTeams.java @@ -0,0 +1,56 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AvailableTeams implements Serializable { + private boolean required; + private List teams; + + public boolean isRequired() { + return required; + } + + public void setRequired(final boolean required) { + this.required = required; + } + + public List getTeams() { + return teams; + } + + public void setTeams(final List teams) { + this.teams = teams; + } + + @Override + public String toString() { + List strlistTeams = teams.stream() + .map(Object::toString) + .toList(); + String strTeams = String.join(",", strlistTeams); + return String.format("required: %s, teams: [ %s ]", required, strTeams); + } + +} diff --git a/src/main/java/org/dependencytrack/model/LittleTeam.java b/src/main/java/org/dependencytrack/model/LittleTeam.java new file mode 100644 index 0000000000..5355b5a0e0 --- /dev/null +++ b/src/main/java/org/dependencytrack/model/LittleTeam.java @@ -0,0 +1,53 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.model; + +import java.io.Serializable; + +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class LittleTeam implements Serializable { + + private UUID value; + private String text; + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public UUID getValue() { + return value; + } + + public void setValue(UUID value) { + this.value = value; + } + + @Override + public String toString() { + return String.format("{value: %s, text: %s}", value.toString(), text); + } +} diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 74fdef9446..74b8480b3c 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -190,7 +190,8 @@ public enum FetchGroup { @Index(name = "PROJECT_CPE_IDX") @Size(max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) - //Patterns obtained from https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd + // Patterns obtained from + // https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd @Pattern(regexp = "(cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){4})|([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6})", message = "The CPE must conform to the CPE v2.2 or v2.3 specification defined by NIST") private String cpe; @@ -223,7 +224,7 @@ public enum FetchGroup { @Persistent @Column(name = "PARENT_PROJECT_ID") - @JsonIncludeProperties(value = {"name", "version", "uuid"}) + @JsonIncludeProperties(value = { "name", "version", "uuid" }) private Project parent; @Persistent(mappedBy = "parent") @@ -240,7 +241,8 @@ public enum FetchGroup { private List tags; /** - * Convenience field which will contain the date of the last entry in the {@link Bom} table + * Convenience field which will contain the date of the last entry in the + * {@link Bom} table */ @Persistent @Index(name = "PROJECT_LASTBOMIMPORT_IDX") @@ -249,7 +251,8 @@ public enum FetchGroup { private Date lastBomImport; /** - * Convenience field which will contain the format of the last entry in the {@link Bom} table + * Convenience field which will contain the format of the last entry in the + * {@link Bom} table */ @Persistent @Index(name = "PROJECT_LASTBOMIMPORT_FORMAT_IDX") @@ -257,7 +260,8 @@ public enum FetchGroup { private String lastBomImportFormat; /** - * Convenience field which stores the Inherited Risk Score (IRS) of the last metric in the {@link ProjectMetrics} table + * Convenience field which stores the Inherited Risk Score (IRS) of the last + * metric in the {@link ProjectMetrics} table */ @Persistent @Index(name = "PROJECT_LAST_RISKSCORE_IDX") @@ -289,6 +293,7 @@ public enum FetchGroup { private transient ProjectMetrics metrics; private transient List versions; private transient List dependencyGraph; + private UUID initialTeam; public long getId() { return id; @@ -308,20 +313,22 @@ public void setAuthors(List authors) { @Deprecated @JsonInclude(JsonInclude.Include.NON_EMPTY) - public String getAuthor(){ + public String getAuthor() { return ModelConverter.convertContactsToString(this.authors); } @Deprecated - public void setAuthor(String author){ - if(this.authors==null){ + public void setAuthor(String author) { + if (this.authors == null) { this.authors = new ArrayList<>(); - } else{ + } else { this.authors.clear(); } - this.authors.add(new OrganizationalContact() {{ - setName(author); - }}); + this.authors.add(new OrganizationalContact() { + { + setName(author); + } + }); } public String getPublisher() { @@ -560,6 +567,14 @@ public void setMetadata(final ProjectMetadata metadata) { this.metadata = metadata; } + public UUID getInitialTeam() { + return this.initialTeam; + } + + public void setInitialTeam(UUID initialTeam) { + this.initialTeam = initialTeam; + } + @JsonIgnore public List getDependencyGraph() { return dependencyGraph; diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index c9cd063908..ef8ed26d49 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -21,6 +21,9 @@ import alpine.common.logging.Logger; import alpine.event.framework.Event; import alpine.model.Team; +import alpine.model.UserPrincipal; +import alpine.model.ConfigProperty; +import alpine.model.Permission; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -65,6 +68,7 @@ import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Function; @@ -86,39 +90,31 @@ public class ProjectResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a list of all projects", - description = "

Requires permission VIEW_PORTFOLIO

" - ) + @Operation(summary = "Returns a list of all projects", description = "

Requires permission VIEW_PORTFOLIO

") @PaginatedApi @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A list of all projects", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) - ), + @ApiResponse(responseCode = "200", description = "A list of all projects", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getProjects(@Parameter(description = "The optional name of the project to query on", required = false) - @QueryParam("name") String name, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) - @QueryParam("excludeInactive") boolean excludeInactive, - @Parameter(description = "Optionally excludes children projects from being returned", required = false) - @QueryParam("onlyRoot") boolean onlyRoot, - @Parameter(description = "The UUID of the team which projects shall be excluded", schema = @Schema(type = "string", format = "uuid"), required = false) - @QueryParam("notAssignedToTeamWithUuid") @ValidUuid String notAssignedToTeamWithUuid) { + public Response getProjects( + @Parameter(description = "The optional name of the project to query on", required = false) @QueryParam("name") String name, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, + @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot, + @Parameter(description = "The UUID of the team which projects shall be excluded", schema = @Schema(type = "string", format = "uuid"), required = false) @QueryParam("notAssignedToTeamWithUuid") @ValidUuid String notAssignedToTeamWithUuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { Team notAssignedToTeam = null; if (StringUtils.isNotEmpty(notAssignedToTeamWithUuid)) { notAssignedToTeam = qm.getObjectByUuid(Team.class, notAssignedToTeamWithUuid); if (notAssignedToTeam == null) { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the team could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the team could not be found.") + .build(); } } - final PaginatedResult result = (name != null) ? qm.getProjects(name, excludeInactive, onlyRoot, notAssignedToTeam) : qm.getProjects(true, excludeInactive, onlyRoot, notAssignedToTeam); + final PaginatedResult result = (name != null) + ? qm.getProjects(name, excludeInactive, onlyRoot, notAssignedToTeam) + : qm.getProjects(true, excludeInactive, onlyRoot, notAssignedToTeam); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } } @@ -126,31 +122,24 @@ public Response getProjects(@Parameter(description = "The optional name of the p @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a specific project", - description = "

Requires permission VIEW_PORTFOLIO

" - ) + @Operation(summary = "Returns a specific project", description = "

Requires permission VIEW_PORTFOLIO

") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A specific project", - content = @Content(schema = @Schema(implementation = Project.class)) - ), + @ApiResponse(responseCode = "200", description = "A specific project", content = @Content(schema = @Schema(implementation = Project.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProject( - @Parameter(description = "The UUID of the project to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid) { + @Parameter(description = "The UUID of the project to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(uuid); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(project).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -161,34 +150,25 @@ public Response getProject( @GET @Path("/lookup") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a specific project by its name and version", - operationId = "getProjectByNameAndVersion", - description = "

Requires permission VIEW_PORTFOLIO

" - ) + @Operation(summary = "Returns a specific project by its name and version", operationId = "getProjectByNameAndVersion", description = "

Requires permission VIEW_PORTFOLIO

") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A specific project by its name and version", - content = @Content(schema = @Schema(implementation = Project.class)) - ), + @ApiResponse(responseCode = "200", description = "A specific project by its name and version", content = @Content(schema = @Schema(implementation = Project.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProject( - @Parameter(description = "The name of the project to query on", required = true) - @QueryParam("name") String name, - @Parameter(description = "The version of the project to query on", required = true) - @QueryParam("version") String version) { + @Parameter(description = "The name of the project to query on", required = true) @QueryParam("name") String name, + @Parameter(description = "The version of the project to query on", required = true) @QueryParam("version") String version) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(name, version); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(project).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -199,28 +179,17 @@ public Response getProject( @GET @Path("/tag/{tag}") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a list of all projects by tag", - description = "

Requires permission VIEW_PORTFOLIO

" - ) + @Operation(summary = "Returns a list of all projects by tag", description = "

Requires permission VIEW_PORTFOLIO

") @PaginatedApi @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A list of all projects by tag", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) - ), + @ApiResponse(responseCode = "200", description = "A list of all projects by tag", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsByTag( - @Parameter(description = "The tag to query on", required = true) - @PathParam("tag") String tagString, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) - @QueryParam("excludeInactive") boolean excludeInactive, - @Parameter(description = "Optionally excludes children projects from being returned", required = false) - @QueryParam("onlyRoot") boolean onlyRoot) { + @Parameter(description = "The tag to query on", required = true) @PathParam("tag") String tagString, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, + @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Tag tag = qm.getTagByName(tagString); final PaginatedResult result = qm.getProjects(tag, true, excludeInactive, onlyRoot); @@ -231,53 +200,36 @@ public Response getProjectsByTag( @GET @Path("/classifier/{classifier}") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a list of all projects by classifier", - description = "

Requires permission VIEW_PORTFOLIO

" - ) + @Operation(summary = "Returns a list of all projects by classifier", description = "

Requires permission VIEW_PORTFOLIO

") @PaginatedApi @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A list of all projects by classifier", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) - ), + @ApiResponse(responseCode = "200", description = "A list of all projects by classifier", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsByClassifier( - @Parameter(description = "The classifier to query on", required = true) - @PathParam("classifier") String classifierString, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) - @QueryParam("excludeInactive") boolean excludeInactive, - @Parameter(description = "Optionally excludes children projects from being returned", required = false) - @QueryParam("onlyRoot") boolean onlyRoot) { + @Parameter(description = "The classifier to query on", required = true) @PathParam("classifier") String classifierString, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, + @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Classifier classifier = Classifier.valueOf(classifierString); final PaginatedResult result = qm.getProjects(classifier, true, excludeInactive, onlyRoot); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } catch (IllegalArgumentException e) { - return Response.status(Response.Status.BAD_REQUEST).entity("The classifier type specified is not valid.").build(); + return Response.status(Response.Status.BAD_REQUEST).entity("The classifier type specified is not valid.") + .build(); } } @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Creates a new project", - description = """ -

If a parent project exists, parent.uuid is required

-

Requires permission PORTFOLIO_MANAGEMENT

- """ - ) + @Operation(summary = "Creates a new project", description = """ +

If a parent project exists, parent.uuid is required

+

Requires permission PORTFOLIO_MANAGEMENT

+ """) @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "The created project", - content = @Content(schema = @Schema(implementation = Project.class)) - ), + @ApiResponse(responseCode = "201", description = "The created project", content = @Content(schema = @Schema(implementation = Project.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "409", description = """
    @@ -299,30 +251,86 @@ public Response createProject(Project jsonProject) { validator.validateProperty(jsonProject, "classifier"), validator.validateProperty(jsonProject, "cpe"), validator.validateProperty(jsonProject, "purl"), - validator.validateProperty(jsonProject, "swidTagId") - ); + validator.validateProperty(jsonProject, "swidTagId"), + validator.validateProperty(jsonProject, "initialTeam")); if (jsonProject.getClassifier() == null) { jsonProject.setClassifier(Classifier.APPLICATION); } try (QueryManager qm = new QueryManager()) { if (jsonProject.getParent() != null && jsonProject.getParent().getUuid() != null) { Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); - jsonProject.setParent(parent); + jsonProject.setParent(parent); } - if (!qm.doesProjectExist(StringUtils.trimToNull(jsonProject.getName()), StringUtils.trimToNull(jsonProject.getVersion()))) { + if (!qm.doesProjectExist(StringUtils.trimToNull(jsonProject.getName()), + StringUtils.trimToNull(jsonProject.getVersion()))) { final Project project; try { project = qm.createProject(jsonProject, jsonProject.getTags(), true); - } catch (IllegalArgumentException e){ + } catch (IllegalArgumentException e) { LOGGER.debug(e.getMessage()); - return Response.status(Response.Status.CONFLICT).entity("An inactive Parent cannot be selected as parent").build(); + return Response.status(Response.Status.CONFLICT) + .entity("An inactive Parent cannot be selected as parent") + .build(); + } + UserPrincipal user; + if (super.isLdapUser()) { + user = qm.getLdapUser(getPrincipal().getName()); + } else if (super.isManagedUser()) { + user = qm.getManagedUser(getPrincipal().getName()); + } else if (super.isOidcUser()) { + user = qm.getOidcUser(getPrincipal().getName()); + } else { + return Response.status(401).build(); + } + boolean isAdmin = false; + boolean required = false; + final List configProperties = qm.getConfigProperties(); + for (final ConfigProperty configProperty : configProperties) { + // Checks if User needs to supply a Team + if (configProperty.getGroupName().equals("access-management") + && configProperty.getPropertyName().equals("acl.enabled")) { + required = configProperty.getPropertyValue().equals("true"); + break; + } + } + List permissions = user.getPermissions(); + for (Permission permission : permissions) { + // Checks if user has Right to submit any team to the project + if (permission.getName().equals("ACCESS_MANAGEMENT")) { + isAdmin = true; + break; + } + } + if (required && jsonProject.getInitialTeam() == null) { + return Response.status(422).build(); + } + final UUID teamUuid = jsonProject.getInitialTeam(); + if (!isAdmin) { + boolean hasTeam = false; + List teams = user.getTeams(); + for (Team team : teams) { + if (team.getUuid().equals(teamUuid)) { + hasTeam = true; + break; + } + } + if (!hasTeam) { + return Response.status(403).build(); + } + + } + if (jsonProject.getInitialTeam() != null) { + final Team team = qm.getObjectByUuid(Team.class, teamUuid); + project.addAccessTeam(team); } Principal principal = getPrincipal(); qm.updateNewProjectACL(project, principal); LOGGER.info("Project " + project.toString() + " created by " + super.getPrincipal().getName()); return Response.status(Response.Status.CREATED).entity(project).build(); } else { - return Response.status(Response.Status.CONFLICT).entity("A project with the specified name already exists.").build(); + return Response.status(Response.Status.CONFLICT) + .entity("A project with the specified name already exists.") + .build(); } } } @@ -330,16 +338,9 @@ public Response createProject(Project jsonProject) { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Updates a project", - description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " - ) + @Operation(summary = "Updates a project", description = "

    Requires permission PORTFOLIO_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "The updated project", - content = @Content(schema = @Schema(implementation = Project.class)) - ), + @ApiResponse(responseCode = "200", description = "The updated project", content = @Content(schema = @Schema(implementation = Project.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found"), @ApiResponse(responseCode = "409", description = """ @@ -363,8 +364,7 @@ public Response updateProject(Project jsonProject) { validator.validateProperty(jsonProject, "classifier"), validator.validateProperty(jsonProject, "cpe"), validator.validateProperty(jsonProject, "purl"), - validator.validateProperty(jsonProject, "swidTagId") - ); + validator.validateProperty(jsonProject, "swidTagId")); if (jsonProject.getClassifier() == null) { jsonProject.setClassifier(Classifier.APPLICATION); } @@ -372,7 +372,8 @@ public Response updateProject(Project jsonProject) { Project project = qm.getObjectByUuid(Project.class, jsonProject.getUuid()); if (project != null) { if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden").build(); } final String name = StringUtils.trimToNull(jsonProject.getName()); final String version = StringUtils.trimToNull(jsonProject.getVersion()); @@ -384,17 +385,19 @@ public Response updateProject(Project jsonProject) { } try { project = qm.updateProject(jsonProject, true); - } catch (IllegalArgumentException e){ + } catch (IllegalArgumentException e) { LOGGER.debug(e.getMessage()); return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build(); } LOGGER.info("Project " + project.toString() + " updated by " + super.getPrincipal().getName()); return Response.ok(project).build(); } else { - return Response.status(Response.Status.CONFLICT).entity("A project with the specified name and version already exists.").build(); + return Response.status(Response.Status.CONFLICT) + .entity("A project with the specified name and version already exists.").build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") + .build(); } } } @@ -403,16 +406,9 @@ public Response updateProject(Project jsonProject) { @Path("/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Partially updates a project", - description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " - ) + @Operation(summary = "Partially updates a project", description = "

    Requires permission PORTFOLIO_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "The updated project", - content = @Content(schema = @Schema(implementation = Project.class)) - ), + @ApiResponse(responseCode = "200", description = "The updated project", content = @Content(schema = @Schema(implementation = Project.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found"), @ApiResponse(responseCode = "409", description = """ @@ -425,8 +421,7 @@ public Response updateProject(Project jsonProject) { }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response patchProject( - @Parameter(description = "The UUID of the project to modify", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "The UUID of the project to modify", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, Project jsonProject) { final Validator validator = getValidator(); failOnValidationError( @@ -440,22 +435,24 @@ public Response patchProject( validator.validateProperty(jsonProject, "classifier"), validator.validateProperty(jsonProject, "cpe"), validator.validateProperty(jsonProject, "purl"), - validator.validateProperty(jsonProject, "swidTagId") - ); + validator.validateProperty(jsonProject, "swidTagId")); try (QueryManager qm = new QueryManager()) { Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden").build(); } var modified = false; project = qm.detachWithGroups(project, List.of(FetchGroup.DEFAULT, Project.FetchGroup.PARENT.name())); modified |= setIfDifferent(jsonProject, project, Project::getName, Project::setName); modified |= setIfDifferent(jsonProject, project, Project::getVersion, Project::setVersion); - // if either name or version has been changed, verify that this new combination does not already exist + // if either name or version has been changed, verify that this new combination + // does not already exist if (modified && qm.doesProjectExist(project.getName(), project.getVersion())) { - return Response.status(Response.Status.CONFLICT).entity("A project with the specified name and version already exists.").build(); + return Response.status(Response.Status.CONFLICT) + .entity("A project with the specified name and version already exists.").build(); } modified |= setIfDifferent(jsonProject, project, Project::getAuthors, Project::setAuthors); modified |= setIfDifferent(jsonProject, project, Project::getPublisher, Project::setPublisher); @@ -471,10 +468,12 @@ public Response patchProject( if (jsonProject.getParent() != null && jsonProject.getParent().getUuid() != null) { final Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); if (parent == null) { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the parent project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND) + .entity("The UUID of the parent project could not be found.").build(); } if (!qm.hasAccess(getPrincipal(), parent)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified parent project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified parent project is forbidden").build(); } modified |= project.getParent() == null || !parent.getUuid().equals(project.getParent().getUuid()); project.setParent(parent); @@ -484,13 +483,13 @@ public Response patchProject( project.setTags(jsonProject.getTags()); } if (isCollectionModified(jsonProject.getExternalReferences(), project.getExternalReferences())) { - modified = true; - project.setExternalReferences(jsonProject.getExternalReferences()); + modified = true; + project.setExternalReferences(jsonProject.getExternalReferences()); } if (modified) { try { project = qm.updateProject(project, true); - } catch (IllegalArgumentException e){ + } catch (IllegalArgumentException e) { LOGGER.debug(e.getMessage()); return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build(); } @@ -500,16 +499,18 @@ public Response patchProject( return Response.notModified().build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") + .build(); } } } /** - * returns `true` if the given [updated] collection should be considered an update of the [original] collection. + * returns `true` if the given [updated] collection should be considered an + * update of the [original] collection. */ private static boolean isCollectionModified(Collection updated, Collection original) { - return updated != null && (!Collections.isEmpty(updated) || !Collections.isEmpty(original)); + return updated != null && (!Collections.isEmpty(updated) || !Collections.isEmpty(original)); } /** @@ -518,16 +519,17 @@ private static boolean isCollectionModified(Collection updated, Collectio * only if the new value is not {@code null} and it is not * {@link Object#equals(java.lang.Object) equal to} the old value. * - * @param the type of the old and new value + * @param the type of the old and new value * @param source the source object that contains the new value * @param target the target object that should be updated * @param getter the method to retrieve the new value from {@code source} - * and the old value from {@code target} + * and the old value from {@code target} * @param setter the method to set the new value on {@code target} * @return {@code true} if {@code target} has been changed, else - * {@code false} + * {@code false} */ - private boolean setIfDifferent(final Project source, final Project target, final Function getter, final BiConsumer setter) { + private boolean setIfDifferent(final Project source, final Project target, final Function getter, + final BiConsumer setter) { final T newValue = getter.apply(source); if (newValue != null && !newValue.equals(getter.apply(target))) { setter.accept(target, newValue); @@ -541,10 +543,7 @@ private boolean setIfDifferent(final Project source, final Project target, f @Path("/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Deletes a project", - description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " - ) + @Operation(summary = "Deletes a project", description = "

    Requires permission PORTFOLIO_MANAGEMENT

    ") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Project removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -553,8 +552,7 @@ private boolean setIfDifferent(final Project source, final Project target, f }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteProject( - @Parameter(description = "The UUID of the project to delete", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid) { + @Parameter(description = "The UUID of the project to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); if (project != null) { @@ -563,10 +561,13 @@ public Response deleteProject( qm.recursivelyDelete(project, true); return Response.status(Response.Status.NO_CONTENT).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden") + .build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") + .build(); } } } @@ -575,16 +576,9 @@ public Response deleteProject( @Path("/clone") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Clones a project", - description = "

    Requires permission PORTFOLIO_MANAGEMENT

    " - ) + @Operation(summary = "Clones a project", description = "

    Requires permission PORTFOLIO_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "Token to be used for checking cloning progress", - content = @Content(schema = @Schema(implementation = BomUploadResponse.class)) - ), + @ApiResponse(responseCode = "200", description = "Token to be used for checking cloning progress", content = @Content(schema = @Schema(implementation = BomUploadResponse.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @@ -593,16 +587,19 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { final Validator validator = super.getValidator(); failOnValidationError( validator.validateProperty(jsonRequest, "project"), - validator.validateProperty(jsonRequest, "version") - ); + validator.validateProperty(jsonRequest, "version")); try (QueryManager qm = new QueryManager()) { - final Project sourceProject = qm.getObjectByUuid(Project.class, jsonRequest.getProject(), Project.FetchGroup.ALL.name()); + final Project sourceProject = qm.getObjectByUuid(Project.class, jsonRequest.getProject(), + Project.FetchGroup.ALL.name()); if (sourceProject != null) { if (!qm.hasAccess(super.getPrincipal(), sourceProject)) { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden") + .build(); } if (qm.doesProjectExist(sourceProject.getName(), StringUtils.trimToNull(jsonRequest.getVersion()))) { - return Response.status(Response.Status.CONFLICT).entity("A project with the specified name and version already exists.").build(); + return Response.status(Response.Status.CONFLICT) + .entity("A project with the specified name and version already exists.").build(); } LOGGER.info("Project " + sourceProject + " is being cloned by " + super.getPrincipal().getName()); @@ -610,36 +607,27 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { Event.dispatch(event); return Response.ok(java.util.Collections.singletonMap("token", event.getChainIdentifier())).build(); } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") + .build(); } } } - @GET @Path("/{uuid}/children") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a list of all children for a project", - description = "

    Requires permission VIEW_PORTFOLIO

    " - ) + @Operation(summary = "Returns a list of all children for a project", description = "

    Requires permission VIEW_PORTFOLIO

    ") @PaginatedApi @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A list of all children for a project", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) - ), + @ApiResponse(responseCode = "200", description = "A list of all children for a project", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getChildrenProjects(@Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) - @QueryParam("excludeInactive") boolean excludeInactive) { + public Response getChildrenProjects( + @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -647,10 +635,13 @@ public Response getChildrenProjects(@Parameter(description = "The UUID of the pr if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden") + .build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") + .build(); } } } @@ -658,42 +649,35 @@ public Response getChildrenProjects(@Parameter(description = "The UUID of the pr @GET @Path("/{uuid}/children/classifier/{classifier}") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a list of all children for a project by classifier", - description = "

    Requires permission VIEW_PORTFOLIO

    " - ) + @Operation(summary = "Returns a list of all children for a project by classifier", description = "

    Requires permission VIEW_PORTFOLIO

    ") @PaginatedApi @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A list of all children for a project by classifier", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) - ), + @ApiResponse(responseCode = "200", description = "A list of all children for a project by classifier", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getChildrenProjectsByClassifier( - @Parameter(description = "The classifier to query on", required = true) - @PathParam("classifier") String classifierString, - @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) - @QueryParam("excludeInactive") boolean excludeInactive) { + @Parameter(description = "The classifier to query on", required = true) @PathParam("classifier") String classifierString, + @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { final Classifier classifier = Classifier.valueOf(classifierString); - final PaginatedResult result = qm.getChildrenProjects(classifier, project.getUuid(), true, excludeInactive); + final PaginatedResult result = qm.getChildrenProjects(classifier, project.getUuid(), true, + excludeInactive); if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden") + .build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") + .build(); } } } @@ -701,30 +685,19 @@ public Response getChildrenProjectsByClassifier( @GET @Path("/{uuid}/children/tag/{tag}") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a list of all children for a project by tag", - description = "

    Requires permission VIEW_PORTFOLIO

    " - ) + @Operation(summary = "Returns a list of all children for a project by tag", description = "

    Requires permission VIEW_PORTFOLIO

    ") @PaginatedApi @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A list of all children for a project by tag", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) - ), + @ApiResponse(responseCode = "200", description = "A list of all children for a project by tag", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getChildrenProjectsByTag( - @Parameter(description = "The tag to query on", required = true) - @PathParam("tag") String tagString, - @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) - @QueryParam("excludeInactive") boolean excludeInactive) { + @Parameter(description = "The tag to query on", required = true) @PathParam("tag") String tagString, + @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -733,10 +706,13 @@ public Response getChildrenProjectsByTag( if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden") + .build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") + .build(); } } } @@ -744,41 +720,35 @@ public Response getChildrenProjectsByTag( @GET @Path("/withoutDescendantsOf/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a list of all projects without the descendants of the selected project", - description = "

    Requires permission VIEW_PORTFOLIO

    " - ) + @Operation(summary = "Returns a list of all projects without the descendants of the selected project", description = "

    Requires permission VIEW_PORTFOLIO

    ") @PaginatedApi @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A list of all projects without the descendants of the selected project", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) - ), + @ApiResponse(responseCode = "200", description = "A list of all projects without the descendants of the selected project", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsWithoutDescendantsOf( - @Parameter(description = "The UUID of the project which descendants will be excluded", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid, - @Parameter(description = "The optional name of the project to query on", required = false) - @QueryParam("name") String name, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) - @QueryParam("excludeInactive") boolean excludeInactive) { + @Parameter(description = "The UUID of the project which descendants will be excluded", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "The optional name of the project to query on", required = false) @QueryParam("name") String name, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { - final PaginatedResult result = (name != null) ? qm.getProjectsWithoutDescendantsOf(name, excludeInactive, project) : qm.getProjectsWithoutDescendantsOf(excludeInactive, project); + final PaginatedResult result = (name != null) + ? qm.getProjectsWithoutDescendantsOf(name, excludeInactive, project) + : qm.getProjectsWithoutDescendantsOf(excludeInactive, project); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); - } else{ - return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); + } else { + return Response.status(Response.Status.FORBIDDEN) + .entity("Access to the specified project is forbidden") + .build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") + .build(); } } } diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index b55b421988..a065858ea4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -20,8 +20,11 @@ import alpine.Config; import alpine.common.logging.Logger; +import alpine.model.ConfigProperty; import alpine.model.ApiKey; +import alpine.model.Permission; import alpine.model.Team; +import alpine.model.UserPrincipal; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; import io.swagger.v3.oas.annotations.Operation; @@ -37,6 +40,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.validation.ValidUuid; +import org.dependencytrack.model.AvailableTeams; +import org.dependencytrack.model.LittleTeam; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.TeamSelfResponse; import org.owasp.security.logging.SecurityMarkers; @@ -52,6 +57,8 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; + +import java.util.ArrayList; import java.util.List; import static org.datanucleus.PropertyNames.PROPERTY_RETAIN_VALUES; @@ -74,17 +81,9 @@ public class TeamResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a list of all teams", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Returns a list of all teams", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A list of all teams", - headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of teams", schema = @Schema(format = "integer")), - content = @Content(array = @ArraySchema(schema = @Schema(implementation = Team.class))) - ), + @ApiResponse(responseCode = "200", description = "A list of all teams", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of teams", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Team.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) @@ -99,23 +98,15 @@ public Response getTeams() { @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns a specific team", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Returns a specific team", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "A specific team", - content = @Content(schema = @Schema(implementation = Team.class)) - ), + @ApiResponse(responseCode = "200", description = "A specific team", content = @Content(schema = @Schema(implementation = Team.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The team could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response getTeam( - @Parameter(description = "The UUID of the team to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid) { + @Parameter(description = "The UUID of the team to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, uuid); if (team != null) { @@ -129,26 +120,18 @@ public Response getTeam( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Creates a new team", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Creates a new team", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "The created team", - content = @Content(schema = @Schema(implementation = Team.class)) - ), + @ApiResponse(responseCode = "201", description = "The created team", content = @Content(schema = @Schema(implementation = Team.class))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - //public Response createTeam(String jsonRequest) { + // public Response createTeam(String jsonRequest) { public Response createTeam(Team jsonTeam) { - //Team team = MapperUtil.readAsObjectOf(Team.class, jsonRequest); + // Team team = MapperUtil.readAsObjectOf(Team.class, jsonRequest); final Validator validator = super.getValidator(); failOnValidationError( - validator.validateProperty(jsonTeam, "name") - ); + validator.validateProperty(jsonTeam, "name")); try (QueryManager qm = new QueryManager()) { final Team team = qm.createTeam(jsonTeam.getName(), false); @@ -160,16 +143,9 @@ public Response createTeam(Team jsonTeam) { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Updates a team's fields", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Updates a team's fields", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "The updated team", - content = @Content(schema = @Schema(implementation = Team.class)) - ), + @ApiResponse(responseCode = "200", description = "The updated team", content = @Content(schema = @Schema(implementation = Team.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The team could not be found") }) @@ -177,13 +153,12 @@ public Response createTeam(Team jsonTeam) { public Response updateTeam(Team jsonTeam) { final Validator validator = super.getValidator(); failOnValidationError( - validator.validateProperty(jsonTeam, "name") - ); + validator.validateProperty(jsonTeam, "name")); try (QueryManager qm = new QueryManager()) { Team team = qm.getObjectByUuid(Team.class, jsonTeam.getUuid()); if (team != null) { team.setName(jsonTeam.getName()); - //todo: set permissions + // todo: set permissions team = qm.updateTeam(jsonTeam); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Team updated: " + team.getName()); return Response.ok(team).build(); @@ -196,10 +171,7 @@ public Response updateTeam(Team jsonTeam) { @DELETE @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Deletes a team", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Deletes a team", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Team removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -220,26 +192,77 @@ public Response deleteTeam(Team jsonTeam) { } } + @GET + @Path("/available-teams") + @Produces(MediaType.APPLICATION_JSON) + @Operation(summary = "Returns a list of Teams what are available as selection", description = "Requires permission PORTFOLIO_MANAGEMENT

    ") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "The Available Teams", content = @Content(schema = @Schema(implementation = AvailableTeams.class))), + @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "404", description = "Teams could not be found") + }) + @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) + public Response availableTeams() { + UserPrincipal user; + boolean isAllTeams = false; + boolean required = false; + try (QueryManager qm = new QueryManager()) { + if (super.isLdapUser()) { + user = qm.getLdapUser(getPrincipal().getName()); + } else if (super.isManagedUser()) { + user = qm.getManagedUser(getPrincipal().getName()); + } else if (super.isOidcUser()) { + user = qm.getOidcUser(getPrincipal().getName()); + } else { + return Response.status(401).build(); + } + final List allTeams = qm.getTeams(); + final List configProperties = qm.getConfigProperties(); + for (final ConfigProperty configProperty : configProperties) { + // Replace the value of encrypted strings with the pre-defined placeholder + if (configProperty.getGroupName().equals("access-management") + && configProperty.getPropertyName().equals("acl.enabled")) { + required = configProperty.getPropertyValue().equals("true"); + break; + } + } + qm.getPersistenceManager().detachCopyAll(configProperties); + qm.close(); + List permissions = user.getPermissions(); + for (Permission permission : permissions) { + if (permission.getName().equals("ACCESS_MANAGEMENT")) { + isAllTeams = true; + break; + } + } + AvailableTeams response = new AvailableTeams(); + response.setRequired(required); + List availableTeams = new ArrayList(); + List teams = isAllTeams ? allTeams : user.getTeams(); + for (Team team : teams) { + LittleTeam newTeam = new LittleTeam(); + newTeam.setValue(team.getUuid()); + newTeam.setText(team.getName()); + availableTeams.add(newTeam); + + } + response.setTeams(availableTeams); + return Response.ok(response).build(); + } + } + @PUT @Path("/{uuid}/key") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Generates an API key and returns its value", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Generates an API key and returns its value", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "201", - description = "The created API key", - content = @Content(schema = @Schema(implementation = ApiKey.class)) - ), + @ApiResponse(responseCode = "201", description = "The created API key", content = @Content(schema = @Schema(implementation = ApiKey.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The team could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response generateApiKey( - @Parameter(description = "The UUID of the team to generate a key for", schema = @Schema(type = "string", format = "uuid"), required = true) - @PathParam("uuid") @ValidUuid String uuid) { + @Parameter(description = "The UUID of the team to generate a key for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, uuid); if (team != null) { @@ -254,23 +277,15 @@ public Response generateApiKey( @POST @Path("/key/{apikey}") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Regenerates an API key by removing the specified key, generating a new one and returning its value", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Regenerates an API key by removing the specified key, generating a new one and returning its value", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "The re-generated API key", - content = @Content(schema = @Schema(implementation = ApiKey.class)) - ), + @ApiResponse(responseCode = "200", description = "The re-generated API key", content = @Content(schema = @Schema(implementation = ApiKey.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The API key could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response regenerateApiKey( - @Parameter(description = "The API key to regenerate", required = true) - @PathParam("apikey") String apikey) { + @Parameter(description = "The API key to regenerate", required = true) @PathParam("apikey") String apikey) { try (QueryManager qm = new QueryManager()) { ApiKey apiKey = qm.getApiKey(apikey); if (apiKey != null) { @@ -286,22 +301,15 @@ public Response regenerateApiKey( @Path("/key/{key}/comment") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Updates an API key's comment", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Updates an API key's comment", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "The updated API key", - content = @Content(schema = @Schema(implementation = ApiKey.class)) - ), + @ApiResponse(responseCode = "200", description = "The updated API key", content = @Content(schema = @Schema(implementation = ApiKey.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The API key could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response updateApiKeyComment(@PathParam("key") final String key, - final String comment) { + final String comment) { try (final var qm = new QueryManager()) { qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true"); @@ -322,10 +330,7 @@ public Response updateApiKeyComment(@PathParam("key") final String key, @DELETE @Path("/key/{apikey}") - @Operation( - summary = "Deletes the specified API key", - description = "

    Requires permission ACCESS_MANAGEMENT

    " - ) + @Operation(summary = "Deletes the specified API key", description = "

    Requires permission ACCESS_MANAGEMENT

    ") @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "API key removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -333,8 +338,7 @@ public Response updateApiKeyComment(@PathParam("key") final String key, }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response deleteApiKey( - @Parameter(description = "The API key to delete", required = true) - @PathParam("apikey") String apikey) { + @Parameter(description = "The API key to delete", required = true) @PathParam("apikey") String apikey) { try (QueryManager qm = new QueryManager()) { final ApiKey apiKey = qm.getApiKey(apikey); if (apiKey != null) { @@ -349,14 +353,9 @@ public Response deleteApiKey( @GET @Path("self") @Produces(MediaType.APPLICATION_JSON) - @Operation( - summary = "Returns information about the current team.") + @Operation(summary = "Returns information about the current team.") @ApiResponses(value = { - @ApiResponse( - responseCode = "200", - description = "Information about the current team", - content = @Content(schema = @Schema(implementation = TeamSelfResponse.class)) - ), + @ApiResponse(responseCode = "200", description = "Information about the current team", content = @Content(schema = @Schema(implementation = TeamSelfResponse.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "400", description = "Invalid API key supplied"), @ApiResponse(responseCode = "404", description = "No Team for the given API key found") @@ -365,19 +364,21 @@ public Response getSelf() { if (Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.ENFORCE_AUTHENTICATION)) { try (var qm = new QueryManager()) { if (isApiKey()) { - final var apiKey = qm.getApiKey(((ApiKey)getPrincipal()).getKey()); + final var apiKey = qm.getApiKey(((ApiKey) getPrincipal()).getKey()); final var team = apiKey.getTeams().stream().findFirst(); if (team.isPresent()) { return Response.ok(new TeamSelfResponse(team.get())).build(); } else { - return Response.status(Response.Status.NOT_FOUND).entity("No Team for the given API key found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("No Team for the given API key found.") + .build(); } } else { return Response.status(Response.Status.BAD_REQUEST).entity("Invalid API key supplied.").build(); } } } - // Authentication is not enabled, but we need to return a positive response without any principal data. + // Authentication is not enabled, but we need to return a positive response + // without any principal data. return Response.ok().build(); } } From 4af6f943eabf59183c164554fd00ffd2a833096e Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Tue, 27 Aug 2024 10:02:06 +0200 Subject: [PATCH 02/13] Fixed most stuff from review Signed-off-by: Thomas Schauer-Koeckeis --- .../org/dependencytrack/model/Project.java | 41 +- .../resources/v1/ProjectResource.java | 479 ++++++++++-------- .../resources/v1/TeamResource.java | 197 ++++--- .../resources/v1/vo/VisibleTeams.java | 27 + 4 files changed, 425 insertions(+), 319 deletions(-) create mode 100644 src/main/java/org/dependencytrack/resources/v1/vo/VisibleTeams.java diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 74b8480b3c..6fc9dae62d 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -190,8 +190,7 @@ public enum FetchGroup { @Index(name = "PROJECT_CPE_IDX") @Size(max = 255) @JsonDeserialize(using = TrimmedStringDeserializer.class) - // Patterns obtained from - // https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd + //Patterns obtained from https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd @Pattern(regexp = "(cpe:2\\.3:[aho\\*\\-](:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){5}(:(([a-zA-Z]{2,3}(-([a-zA-Z]{2}|[0-9]{3}))?)|[\\*\\-]))(:(((\\?*|\\*?)([a-zA-Z0-9\\-\\._]|(\\\\[\\\\\\*\\?!\"#$$%&'\\(\\)\\+,/:;<=>@\\[\\]\\^`\\{\\|}~]))+(\\?*|\\*?))|[\\*\\-])){4})|([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\\._\\-~%]*){0,6})", message = "The CPE must conform to the CPE v2.2 or v2.3 specification defined by NIST") private String cpe; @@ -224,7 +223,7 @@ public enum FetchGroup { @Persistent @Column(name = "PARENT_PROJECT_ID") - @JsonIncludeProperties(value = { "name", "version", "uuid" }) + @JsonIncludeProperties(value = {"name", "version", "uuid"}) private Project parent; @Persistent(mappedBy = "parent") @@ -241,8 +240,7 @@ public enum FetchGroup { private List tags; /** - * Convenience field which will contain the date of the last entry in the - * {@link Bom} table + * Convenience field which will contain the date of the last entry in the {@link Bom} table */ @Persistent @Index(name = "PROJECT_LASTBOMIMPORT_IDX") @@ -251,8 +249,7 @@ public enum FetchGroup { private Date lastBomImport; /** - * Convenience field which will contain the format of the last entry in the - * {@link Bom} table + * Convenience field which will contain the format of the last entry in the {@link Bom} table */ @Persistent @Index(name = "PROJECT_LASTBOMIMPORT_FORMAT_IDX") @@ -260,8 +257,7 @@ public enum FetchGroup { private String lastBomImportFormat; /** - * Convenience field which stores the Inherited Risk Score (IRS) of the last - * metric in the {@link ProjectMetrics} table + * Convenience field which stores the Inherited Risk Score (IRS) of the last metric in the {@link ProjectMetrics} table */ @Persistent @Index(name = "PROJECT_LAST_RISKSCORE_IDX") @@ -277,7 +273,7 @@ public enum FetchGroup { @Join(column = "PROJECT_ID") @Element(column = "TEAM_ID") @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) - @JsonIgnore + //@JsonIgnore private List accessTeams; @Persistent(defaultFetchGroup = "true") @@ -293,7 +289,6 @@ public enum FetchGroup { private transient ProjectMetrics metrics; private transient List versions; private transient List dependencyGraph; - private UUID initialTeam; public long getId() { return id; @@ -313,22 +308,20 @@ public void setAuthors(List authors) { @Deprecated @JsonInclude(JsonInclude.Include.NON_EMPTY) - public String getAuthor() { + public String getAuthor(){ return ModelConverter.convertContactsToString(this.authors); } @Deprecated - public void setAuthor(String author) { - if (this.authors == null) { + public void setAuthor(String author){ + if(this.authors==null){ this.authors = new ArrayList<>(); - } else { + } else{ this.authors.clear(); } - this.authors.add(new OrganizationalContact() { - { - setName(author); - } - }); + this.authors.add(new OrganizationalContact() {{ + setName(author); + }}); } public String getPublisher() { @@ -567,14 +560,6 @@ public void setMetadata(final ProjectMetadata metadata) { this.metadata = metadata; } - public UUID getInitialTeam() { - return this.initialTeam; - } - - public void setInitialTeam(UUID initialTeam) { - this.initialTeam = initialTeam; - } - @JsonIgnore public List getDependencyGraph() { return dependencyGraph; diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index ef8ed26d49..31c84c83e4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -20,10 +20,9 @@ import alpine.common.logging.Logger; import alpine.event.framework.Event; +import alpine.model.ApiKey; import alpine.model.Team; import alpine.model.UserPrincipal; -import alpine.model.ConfigProperty; -import alpine.model.Permission; import alpine.persistence.PaginatedResult; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; @@ -42,6 +41,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.event.CloneProjectEvent; import org.dependencytrack.model.Classifier; +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; import org.dependencytrack.model.Tag; import org.dependencytrack.model.validation.ValidUuid; @@ -65,10 +65,10 @@ import jakarta.ws.rs.core.Response; import javax.jdo.FetchGroup; import java.security.Principal; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Set; -import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Function; @@ -90,31 +90,39 @@ public class ProjectResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of all projects", description = "

    Requires permission VIEW_PORTFOLIO

    ") + @Operation( + summary = "Returns a list of all projects", + description = "

    Requires permission VIEW_PORTFOLIO

    " + ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A list of all projects", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse( + responseCode = "200", + description = "A list of all projects", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getProjects( - @Parameter(description = "The optional name of the project to query on", required = false) @QueryParam("name") String name, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot, - @Parameter(description = "The UUID of the team which projects shall be excluded", schema = @Schema(type = "string", format = "uuid"), required = false) @QueryParam("notAssignedToTeamWithUuid") @ValidUuid String notAssignedToTeamWithUuid) { + public Response getProjects(@Parameter(description = "The optional name of the project to query on", required = false) + @QueryParam("name") String name, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) + @QueryParam("excludeInactive") boolean excludeInactive, + @Parameter(description = "Optionally excludes children projects from being returned", required = false) + @QueryParam("onlyRoot") boolean onlyRoot, + @Parameter(description = "The UUID of the team which projects shall be excluded", schema = @Schema(type = "string", format = "uuid"), required = false) + @QueryParam("notAssignedToTeamWithUuid") @ValidUuid String notAssignedToTeamWithUuid) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { Team notAssignedToTeam = null; if (StringUtils.isNotEmpty(notAssignedToTeamWithUuid)) { notAssignedToTeam = qm.getObjectByUuid(Team.class, notAssignedToTeamWithUuid); if (notAssignedToTeam == null) { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the team could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the team could not be found.").build(); } } - final PaginatedResult result = (name != null) - ? qm.getProjects(name, excludeInactive, onlyRoot, notAssignedToTeam) - : qm.getProjects(true, excludeInactive, onlyRoot, notAssignedToTeam); + final PaginatedResult result = (name != null) ? qm.getProjects(name, excludeInactive, onlyRoot, notAssignedToTeam) : qm.getProjects(true, excludeInactive, onlyRoot, notAssignedToTeam); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } } @@ -122,24 +130,31 @@ public Response getProjects( @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a specific project", description = "

    Requires permission VIEW_PORTFOLIO

    ") + @Operation( + summary = "Returns a specific project", + description = "

    Requires permission VIEW_PORTFOLIO

    " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A specific project", content = @Content(schema = @Schema(implementation = Project.class))), + @ApiResponse( + responseCode = "200", + description = "A specific project", + content = @Content(schema = @Schema(implementation = Project.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProject( - @Parameter(description = "The UUID of the project to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { + @Parameter(description = "The UUID of the project to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(uuid); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(project).build(); } else { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -150,25 +165,34 @@ public Response getProject( @GET @Path("/lookup") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a specific project by its name and version", operationId = "getProjectByNameAndVersion", description = "

    Requires permission VIEW_PORTFOLIO

    ") + @Operation( + summary = "Returns a specific project by its name and version", + operationId = "getProjectByNameAndVersion", + description = "

    Requires permission VIEW_PORTFOLIO

    " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A specific project by its name and version", content = @Content(schema = @Schema(implementation = Project.class))), + @ApiResponse( + responseCode = "200", + description = "A specific project by its name and version", + content = @Content(schema = @Schema(implementation = Project.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProject( - @Parameter(description = "The name of the project to query on", required = true) @QueryParam("name") String name, - @Parameter(description = "The version of the project to query on", required = true) @QueryParam("version") String version) { + @Parameter(description = "The name of the project to query on", required = true) + @QueryParam("name") String name, + @Parameter(description = "The version of the project to query on", required = true) + @QueryParam("version") String version) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getProject(name, version); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(project).build(); } else { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } } else { return Response.status(Response.Status.NOT_FOUND).entity("The project could not be found.").build(); @@ -179,17 +203,28 @@ public Response getProject( @GET @Path("/tag/{tag}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of all projects by tag", description = "

    Requires permission VIEW_PORTFOLIO

    ") + @Operation( + summary = "Returns a list of all projects by tag", + description = "

    Requires permission VIEW_PORTFOLIO

    " + ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A list of all projects by tag", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse( + responseCode = "200", + description = "A list of all projects by tag", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsByTag( - @Parameter(description = "The tag to query on", required = true) @PathParam("tag") String tagString, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot) { + @Parameter(description = "The tag to query on", required = true) + @PathParam("tag") String tagString, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) + @QueryParam("excludeInactive") boolean excludeInactive, + @Parameter(description = "Optionally excludes children projects from being returned", required = false) + @QueryParam("onlyRoot") boolean onlyRoot) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Tag tag = qm.getTagByName(tagString); final PaginatedResult result = qm.getProjects(tag, true, excludeInactive, onlyRoot); @@ -200,36 +235,53 @@ public Response getProjectsByTag( @GET @Path("/classifier/{classifier}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of all projects by classifier", description = "

    Requires permission VIEW_PORTFOLIO

    ") + @Operation( + summary = "Returns a list of all projects by classifier", + description = "

    Requires permission VIEW_PORTFOLIO

    " + ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A list of all projects by classifier", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse( + responseCode = "200", + description = "A list of all projects by classifier", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsByClassifier( - @Parameter(description = "The classifier to query on", required = true) @PathParam("classifier") String classifierString, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive, - @Parameter(description = "Optionally excludes children projects from being returned", required = false) @QueryParam("onlyRoot") boolean onlyRoot) { + @Parameter(description = "The classifier to query on", required = true) + @PathParam("classifier") String classifierString, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) + @QueryParam("excludeInactive") boolean excludeInactive, + @Parameter(description = "Optionally excludes children projects from being returned", required = false) + @QueryParam("onlyRoot") boolean onlyRoot) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Classifier classifier = Classifier.valueOf(classifierString); final PaginatedResult result = qm.getProjects(classifier, true, excludeInactive, onlyRoot); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } catch (IllegalArgumentException e) { - return Response.status(Response.Status.BAD_REQUEST).entity("The classifier type specified is not valid.") - .build(); + return Response.status(Response.Status.BAD_REQUEST).entity("The classifier type specified is not valid.").build(); } } @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Creates a new project", description = """ -

    If a parent project exists, parent.uuid is required

    -

    Requires permission PORTFOLIO_MANAGEMENT

    - """) + @Operation( + summary = "Creates a new project", + description = """ +

    If a parent project exists, parent.uuid is required

    +

    Requires permission PORTFOLIO_MANAGEMENT

    + """ + ) @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "The created project", content = @Content(schema = @Schema(implementation = Project.class))), + @ApiResponse( + responseCode = "201", + description = "The created project", + content = @Content(schema = @Schema(implementation = Project.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "409", description = """
      @@ -252,85 +304,58 @@ public Response createProject(Project jsonProject) { validator.validateProperty(jsonProject, "cpe"), validator.validateProperty(jsonProject, "purl"), validator.validateProperty(jsonProject, "swidTagId"), - validator.validateProperty(jsonProject, "initialTeam")); + validator.validateProperty(jsonProject, "accessTeams") + ); if (jsonProject.getClassifier() == null) { jsonProject.setClassifier(Classifier.APPLICATION); } try (QueryManager qm = new QueryManager()) { if (jsonProject.getParent() != null && jsonProject.getParent().getUuid() != null) { Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); - jsonProject.setParent(parent); + jsonProject.setParent(parent); } - if (!qm.doesProjectExist(StringUtils.trimToNull(jsonProject.getName()), - StringUtils.trimToNull(jsonProject.getVersion()))) { - final Project project; - try { - project = qm.createProject(jsonProject, jsonProject.getTags(), true); - } catch (IllegalArgumentException e) { - LOGGER.debug(e.getMessage()); - return Response.status(Response.Status.CONFLICT) - .entity("An inactive Parent cannot be selected as parent") - .build(); - } - UserPrincipal user; - if (super.isLdapUser()) { - user = qm.getLdapUser(getPrincipal().getName()); - } else if (super.isManagedUser()) { - user = qm.getManagedUser(getPrincipal().getName()); - } else if (super.isOidcUser()) { - user = qm.getOidcUser(getPrincipal().getName()); - } else { - return Response.status(401).build(); - } - boolean isAdmin = false; - boolean required = false; - final List configProperties = qm.getConfigProperties(); - for (final ConfigProperty configProperty : configProperties) { - // Checks if User needs to supply a Team - if (configProperty.getGroupName().equals("access-management") - && configProperty.getPropertyName().equals("acl.enabled")) { - required = configProperty.getPropertyValue().equals("true"); - break; - } - } - List permissions = user.getPermissions(); - for (Permission permission : permissions) { - // Checks if user has Right to submit any team to the project - if (permission.getName().equals("ACCESS_MANAGEMENT")) { - isAdmin = true; + final List choosenTeams = jsonProject.getAccessTeams(); + LOGGER.info(choosenTeams.toString()); + Principal principal = getPrincipal(); + List userTeams = new ArrayList(); + if (principal instanceof final UserPrincipal userPrincipal) { + userTeams = userPrincipal.getTeams(); + } else if (principal instanceof final ApiKey apiKey) { + userTeams = apiKey.getTeams(); + } + boolean required = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED); + boolean isAdmin = qm.hasAccessManagementPermission(principal); + if (required && choosenTeams.size() == 0) { + return Response.status(422).build(); + } + List visibleTeams = isAdmin ? qm.getTeams() : userTeams; + boolean hasTeam; + for (Team choosenTeam : choosenTeams) { + hasTeam = false; + LOGGER.info(Boolean.toString(visibleTeams.contains(choosenTeam)) + choosenTeam.getName()); + for (Team team : visibleTeams) { + if (team.getUuid().equals(choosenTeam.getUuid())) { + hasTeam = true; break; } } - if (required && jsonProject.getInitialTeam() == null) { - return Response.status(422).build(); + if (!hasTeam) { + return Response.status(403).build(); } - final UUID teamUuid = jsonProject.getInitialTeam(); - if (!isAdmin) { - boolean hasTeam = false; - List teams = user.getTeams(); - for (Team team : teams) { - if (team.getUuid().equals(teamUuid)) { - hasTeam = true; - break; - } - } - if (!hasTeam) { - return Response.status(403).build(); - } - - } - if (jsonProject.getInitialTeam() != null) { - final Team team = qm.getObjectByUuid(Team.class, teamUuid); - project.addAccessTeam(team); + } + if (!qm.doesProjectExist(StringUtils.trimToNull(jsonProject.getName()), StringUtils.trimToNull(jsonProject.getVersion()))) { + final Project project; + try { + project = qm.createProject(jsonProject, jsonProject.getTags(), true); + } catch (IllegalArgumentException e){ + LOGGER.debug(e.getMessage()); + return Response.status(Response.Status.CONFLICT).entity("An inactive Parent cannot be selected as parent").build(); } - Principal principal = getPrincipal(); qm.updateNewProjectACL(project, principal); LOGGER.info("Project " + project.toString() + " created by " + super.getPrincipal().getName()); return Response.status(Response.Status.CREATED).entity(project).build(); } else { - return Response.status(Response.Status.CONFLICT) - .entity("A project with the specified name already exists.") - .build(); + return Response.status(Response.Status.CONFLICT).entity("A project with the specified name already exists.").build(); } } } @@ -338,9 +363,16 @@ public Response createProject(Project jsonProject) { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Updates a project", description = "

      Requires permission PORTFOLIO_MANAGEMENT

      ") + @Operation( + summary = "Updates a project", + description = "

      Requires permission PORTFOLIO_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "The updated project", content = @Content(schema = @Schema(implementation = Project.class))), + @ApiResponse( + responseCode = "200", + description = "The updated project", + content = @Content(schema = @Schema(implementation = Project.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found"), @ApiResponse(responseCode = "409", description = """ @@ -364,7 +396,8 @@ public Response updateProject(Project jsonProject) { validator.validateProperty(jsonProject, "classifier"), validator.validateProperty(jsonProject, "cpe"), validator.validateProperty(jsonProject, "purl"), - validator.validateProperty(jsonProject, "swidTagId")); + validator.validateProperty(jsonProject, "swidTagId") + ); if (jsonProject.getClassifier() == null) { jsonProject.setClassifier(Classifier.APPLICATION); } @@ -372,8 +405,7 @@ public Response updateProject(Project jsonProject) { Project project = qm.getObjectByUuid(Project.class, jsonProject.getUuid()); if (project != null) { if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } final String name = StringUtils.trimToNull(jsonProject.getName()); final String version = StringUtils.trimToNull(jsonProject.getVersion()); @@ -385,19 +417,17 @@ public Response updateProject(Project jsonProject) { } try { project = qm.updateProject(jsonProject, true); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e){ LOGGER.debug(e.getMessage()); return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build(); } LOGGER.info("Project " + project.toString() + " updated by " + super.getPrincipal().getName()); return Response.ok(project).build(); } else { - return Response.status(Response.Status.CONFLICT) - .entity("A project with the specified name and version already exists.").build(); + return Response.status(Response.Status.CONFLICT).entity("A project with the specified name and version already exists.").build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } } } @@ -406,9 +436,16 @@ public Response updateProject(Project jsonProject) { @Path("/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Partially updates a project", description = "

      Requires permission PORTFOLIO_MANAGEMENT

      ") + @Operation( + summary = "Partially updates a project", + description = "

      Requires permission PORTFOLIO_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "The updated project", content = @Content(schema = @Schema(implementation = Project.class))), + @ApiResponse( + responseCode = "200", + description = "The updated project", + content = @Content(schema = @Schema(implementation = Project.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found"), @ApiResponse(responseCode = "409", description = """ @@ -421,7 +458,8 @@ public Response updateProject(Project jsonProject) { }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response patchProject( - @Parameter(description = "The UUID of the project to modify", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "The UUID of the project to modify", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, Project jsonProject) { final Validator validator = getValidator(); failOnValidationError( @@ -435,24 +473,22 @@ public Response patchProject( validator.validateProperty(jsonProject, "classifier"), validator.validateProperty(jsonProject, "cpe"), validator.validateProperty(jsonProject, "purl"), - validator.validateProperty(jsonProject, "swidTagId")); + validator.validateProperty(jsonProject, "swidTagId") + ); try (QueryManager qm = new QueryManager()) { Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { if (!qm.hasAccess(super.getPrincipal(), project)) { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } var modified = false; project = qm.detachWithGroups(project, List.of(FetchGroup.DEFAULT, Project.FetchGroup.PARENT.name())); modified |= setIfDifferent(jsonProject, project, Project::getName, Project::setName); modified |= setIfDifferent(jsonProject, project, Project::getVersion, Project::setVersion); - // if either name or version has been changed, verify that this new combination - // does not already exist + // if either name or version has been changed, verify that this new combination does not already exist if (modified && qm.doesProjectExist(project.getName(), project.getVersion())) { - return Response.status(Response.Status.CONFLICT) - .entity("A project with the specified name and version already exists.").build(); + return Response.status(Response.Status.CONFLICT).entity("A project with the specified name and version already exists.").build(); } modified |= setIfDifferent(jsonProject, project, Project::getAuthors, Project::setAuthors); modified |= setIfDifferent(jsonProject, project, Project::getPublisher, Project::setPublisher); @@ -468,12 +504,10 @@ public Response patchProject( if (jsonProject.getParent() != null && jsonProject.getParent().getUuid() != null) { final Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); if (parent == null) { - return Response.status(Response.Status.NOT_FOUND) - .entity("The UUID of the parent project could not be found.").build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the parent project could not be found.").build(); } if (!qm.hasAccess(getPrincipal(), parent)) { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified parent project is forbidden").build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified parent project is forbidden").build(); } modified |= project.getParent() == null || !parent.getUuid().equals(project.getParent().getUuid()); project.setParent(parent); @@ -483,13 +517,13 @@ public Response patchProject( project.setTags(jsonProject.getTags()); } if (isCollectionModified(jsonProject.getExternalReferences(), project.getExternalReferences())) { - modified = true; - project.setExternalReferences(jsonProject.getExternalReferences()); + modified = true; + project.setExternalReferences(jsonProject.getExternalReferences()); } if (modified) { try { project = qm.updateProject(project, true); - } catch (IllegalArgumentException e) { + } catch (IllegalArgumentException e){ LOGGER.debug(e.getMessage()); return Response.status(Response.Status.CONFLICT).entity(e.getMessage()).build(); } @@ -499,18 +533,16 @@ public Response patchProject( return Response.notModified().build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } } } /** - * returns `true` if the given [updated] collection should be considered an - * update of the [original] collection. + * returns `true` if the given [updated] collection should be considered an update of the [original] collection. */ private static boolean isCollectionModified(Collection updated, Collection original) { - return updated != null && (!Collections.isEmpty(updated) || !Collections.isEmpty(original)); + return updated != null && (!Collections.isEmpty(updated) || !Collections.isEmpty(original)); } /** @@ -519,17 +551,16 @@ private static boolean isCollectionModified(Collection updated, Collectio * only if the new value is not {@code null} and it is not * {@link Object#equals(java.lang.Object) equal to} the old value. * - * @param the type of the old and new value + * @param the type of the old and new value * @param source the source object that contains the new value * @param target the target object that should be updated * @param getter the method to retrieve the new value from {@code source} - * and the old value from {@code target} + * and the old value from {@code target} * @param setter the method to set the new value on {@code target} * @return {@code true} if {@code target} has been changed, else - * {@code false} + * {@code false} */ - private boolean setIfDifferent(final Project source, final Project target, final Function getter, - final BiConsumer setter) { + private boolean setIfDifferent(final Project source, final Project target, final Function getter, final BiConsumer setter) { final T newValue = getter.apply(source); if (newValue != null && !newValue.equals(getter.apply(target))) { setter.accept(target, newValue); @@ -543,7 +574,10 @@ private boolean setIfDifferent(final Project source, final Project target, f @Path("/{uuid}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Deletes a project", description = "

      Requires permission PORTFOLIO_MANAGEMENT

      ") + @Operation( + summary = "Deletes a project", + description = "

      Requires permission PORTFOLIO_MANAGEMENT

      " + ) @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Project removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -552,7 +586,8 @@ private boolean setIfDifferent(final Project source, final Project target, f }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response deleteProject( - @Parameter(description = "The UUID of the project to delete", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { + @Parameter(description = "The UUID of the project to delete", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Project project = qm.getObjectByUuid(Project.class, uuid, Project.FetchGroup.ALL.name()); if (project != null) { @@ -561,13 +596,10 @@ public Response deleteProject( qm.recursivelyDelete(project, true); return Response.status(Response.Status.NO_CONTENT).build(); } else { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } } } @@ -576,9 +608,16 @@ public Response deleteProject( @Path("/clone") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Clones a project", description = "

      Requires permission PORTFOLIO_MANAGEMENT

      ") + @Operation( + summary = "Clones a project", + description = "

      Requires permission PORTFOLIO_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Token to be used for checking cloning progress", content = @Content(schema = @Schema(implementation = BomUploadResponse.class))), + @ApiResponse( + responseCode = "200", + description = "Token to be used for checking cloning progress", + content = @Content(schema = @Schema(implementation = BomUploadResponse.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @@ -587,19 +626,16 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { final Validator validator = super.getValidator(); failOnValidationError( validator.validateProperty(jsonRequest, "project"), - validator.validateProperty(jsonRequest, "version")); + validator.validateProperty(jsonRequest, "version") + ); try (QueryManager qm = new QueryManager()) { - final Project sourceProject = qm.getObjectByUuid(Project.class, jsonRequest.getProject(), - Project.FetchGroup.ALL.name()); + final Project sourceProject = qm.getObjectByUuid(Project.class, jsonRequest.getProject(), Project.FetchGroup.ALL.name()); if (sourceProject != null) { if (!qm.hasAccess(super.getPrincipal(), sourceProject)) { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } if (qm.doesProjectExist(sourceProject.getName(), StringUtils.trimToNull(jsonRequest.getVersion()))) { - return Response.status(Response.Status.CONFLICT) - .entity("A project with the specified name and version already exists.").build(); + return Response.status(Response.Status.CONFLICT).entity("A project with the specified name and version already exists.").build(); } LOGGER.info("Project " + sourceProject + " is being cloned by " + super.getPrincipal().getName()); @@ -607,27 +643,36 @@ public Response cloneProject(CloneProjectRequest jsonRequest) { Event.dispatch(event); return Response.ok(java.util.Collections.singletonMap("token", event.getChainIdentifier())).build(); } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } } } + @GET @Path("/{uuid}/children") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of all children for a project", description = "

      Requires permission VIEW_PORTFOLIO

      ") + @Operation( + summary = "Returns a list of all children for a project", + description = "

      Requires permission VIEW_PORTFOLIO

      " + ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A list of all children for a project", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse( + responseCode = "200", + description = "A list of all children for a project", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) - public Response getChildrenProjects( - @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { + public Response getChildrenProjects(@Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) + @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -635,13 +680,10 @@ public Response getChildrenProjects( if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } } } @@ -649,35 +691,42 @@ public Response getChildrenProjects( @GET @Path("/{uuid}/children/classifier/{classifier}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of all children for a project by classifier", description = "

      Requires permission VIEW_PORTFOLIO

      ") + @Operation( + summary = "Returns a list of all children for a project by classifier", + description = "

      Requires permission VIEW_PORTFOLIO

      " + ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A list of all children for a project by classifier", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse( + responseCode = "200", + description = "A list of all children for a project by classifier", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getChildrenProjectsByClassifier( - @Parameter(description = "The classifier to query on", required = true) @PathParam("classifier") String classifierString, - @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { + @Parameter(description = "The classifier to query on", required = true) + @PathParam("classifier") String classifierString, + @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) + @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { final Classifier classifier = Classifier.valueOf(classifierString); - final PaginatedResult result = qm.getChildrenProjects(classifier, project.getUuid(), true, - excludeInactive); + final PaginatedResult result = qm.getChildrenProjects(classifier, project.getUuid(), true, excludeInactive); if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } } } @@ -685,19 +734,30 @@ public Response getChildrenProjectsByClassifier( @GET @Path("/{uuid}/children/tag/{tag}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of all children for a project by tag", description = "

      Requires permission VIEW_PORTFOLIO

      ") + @Operation( + summary = "Returns a list of all children for a project by tag", + description = "

      Requires permission VIEW_PORTFOLIO

      " + ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A list of all children for a project by tag", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse( + responseCode = "200", + description = "A list of all children for a project by tag", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getChildrenProjectsByTag( - @Parameter(description = "The tag to query on", required = true) @PathParam("tag") String tagString, - @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { + @Parameter(description = "The tag to query on", required = true) + @PathParam("tag") String tagString, + @Parameter(description = "The UUID of the project to get the children from", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) + @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { @@ -706,13 +766,10 @@ public Response getChildrenProjectsByTag( if (qm.hasAccess(super.getPrincipal(), project)) { return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); } else { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build(); + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } } } @@ -720,35 +777,41 @@ public Response getChildrenProjectsByTag( @GET @Path("/withoutDescendantsOf/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of all projects without the descendants of the selected project", description = "

      Requires permission VIEW_PORTFOLIO

      ") + @Operation( + summary = "Returns a list of all projects without the descendants of the selected project", + description = "

      Requires permission VIEW_PORTFOLIO

      " + ) @PaginatedApi @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A list of all projects without the descendants of the selected project", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class)))), + @ApiResponse( + responseCode = "200", + description = "A list of all projects without the descendants of the selected project", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of projects", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Project.class))) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "403", description = "Access to the specified project is forbidden"), @ApiResponse(responseCode = "404", description = "The UUID of the project could not be found") }) @PermissionRequired(Permissions.Constants.VIEW_PORTFOLIO) public Response getProjectsWithoutDescendantsOf( - @Parameter(description = "The UUID of the project which descendants will be excluded", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid, - @Parameter(description = "The optional name of the project to query on", required = false) @QueryParam("name") String name, - @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) @QueryParam("excludeInactive") boolean excludeInactive) { + @Parameter(description = "The UUID of the project which descendants will be excluded", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid, + @Parameter(description = "The optional name of the project to query on", required = false) + @QueryParam("name") String name, + @Parameter(description = "Optionally excludes inactive projects from being returned", required = false) + @QueryParam("excludeInactive") boolean excludeInactive) { try (QueryManager qm = new QueryManager(getAlpineRequest())) { final Project project = qm.getObjectByUuid(Project.class, uuid); if (project != null) { if (qm.hasAccess(super.getPrincipal(), project)) { - final PaginatedResult result = (name != null) - ? qm.getProjectsWithoutDescendantsOf(name, excludeInactive, project) - : qm.getProjectsWithoutDescendantsOf(excludeInactive, project); + final PaginatedResult result = (name != null) ? qm.getProjectsWithoutDescendantsOf(name, excludeInactive, project) : qm.getProjectsWithoutDescendantsOf(excludeInactive, project); return Response.ok(result.getObjects()).header(TOTAL_COUNT_HEADER, result.getTotal()).build(); - } else { - return Response.status(Response.Status.FORBIDDEN) - .entity("Access to the specified project is forbidden") - .build(); + } else{ + return Response.status(Response.Status.FORBIDDEN).entity("Access to the specified project is forbidden").build(); } } else { - return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("The UUID of the project could not be found.").build(); } } } diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index a065858ea4..23817c545c 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -20,9 +20,7 @@ import alpine.Config; import alpine.common.logging.Logger; -import alpine.model.ConfigProperty; import alpine.model.ApiKey; -import alpine.model.Permission; import alpine.model.Team; import alpine.model.UserPrincipal; import alpine.server.auth.PermissionRequired; @@ -39,11 +37,11 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; +import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.validation.ValidUuid; -import org.dependencytrack.model.AvailableTeams; -import org.dependencytrack.model.LittleTeam; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.TeamSelfResponse; +import org.dependencytrack.resources.v1.vo.VisibleTeams; import org.owasp.security.logging.SecurityMarkers; import jakarta.validation.Validator; @@ -57,7 +55,7 @@ import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; - +import java.security.Principal; import java.util.ArrayList; import java.util.List; @@ -81,9 +79,17 @@ public class TeamResource extends AlpineResource { @GET @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of all teams", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Returns a list of all teams", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A list of all teams", headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of teams", schema = @Schema(format = "integer")), content = @Content(array = @ArraySchema(schema = @Schema(implementation = Team.class)))), + @ApiResponse( + responseCode = "200", + description = "A list of all teams", + headers = @Header(name = TOTAL_COUNT_HEADER, description = "The total number of teams", schema = @Schema(format = "integer")), + content = @Content(array = @ArraySchema(schema = @Schema(implementation = Team.class))) + ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) @@ -98,15 +104,23 @@ public Response getTeams() { @GET @Path("/{uuid}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a specific team", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Returns a specific team", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "A specific team", content = @Content(schema = @Schema(implementation = Team.class))), + @ApiResponse( + responseCode = "200", + description = "A specific team", + content = @Content(schema = @Schema(implementation = Team.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The team could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response getTeam( - @Parameter(description = "The UUID of the team to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { + @Parameter(description = "The UUID of the team to retrieve", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, uuid); if (team != null) { @@ -120,18 +134,26 @@ public Response getTeam( @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Creates a new team", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Creates a new team", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "The created team", content = @Content(schema = @Schema(implementation = Team.class))), + @ApiResponse( + responseCode = "201", + description = "The created team", + content = @Content(schema = @Schema(implementation = Team.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) - // public Response createTeam(String jsonRequest) { + //public Response createTeam(String jsonRequest) { public Response createTeam(Team jsonTeam) { - // Team team = MapperUtil.readAsObjectOf(Team.class, jsonRequest); + //Team team = MapperUtil.readAsObjectOf(Team.class, jsonRequest); final Validator validator = super.getValidator(); failOnValidationError( - validator.validateProperty(jsonTeam, "name")); + validator.validateProperty(jsonTeam, "name") + ); try (QueryManager qm = new QueryManager()) { final Team team = qm.createTeam(jsonTeam.getName(), false); @@ -143,9 +165,16 @@ public Response createTeam(Team jsonTeam) { @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Updates a team's fields", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Updates a team's fields", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "The updated team", content = @Content(schema = @Schema(implementation = Team.class))), + @ApiResponse( + responseCode = "200", + description = "The updated team", + content = @Content(schema = @Schema(implementation = Team.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The team could not be found") }) @@ -153,12 +182,13 @@ public Response createTeam(Team jsonTeam) { public Response updateTeam(Team jsonTeam) { final Validator validator = super.getValidator(); failOnValidationError( - validator.validateProperty(jsonTeam, "name")); + validator.validateProperty(jsonTeam, "name") + ); try (QueryManager qm = new QueryManager()) { Team team = qm.getObjectByUuid(Team.class, jsonTeam.getUuid()); if (team != null) { team.setName(jsonTeam.getName()); - // todo: set permissions + //todo: set permissions team = qm.updateTeam(jsonTeam); super.logSecurityEvent(LOGGER, SecurityMarkers.SECURITY_AUDIT, "Team updated: " + team.getName()); return Response.ok(team).build(); @@ -171,7 +201,10 @@ public Response updateTeam(Team jsonTeam) { @DELETE @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Deletes a team", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Deletes a team", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "Team removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -193,60 +226,28 @@ public Response deleteTeam(Team jsonTeam) { } @GET - @Path("/available-teams") + @Path("/visible") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of Teams what are available as selection", description = "Requires permission PORTFOLIO_MANAGEMENT

      ") + @Operation(summary = "Returns a list of Teams what are visible", description = "Requires permission PORTFOLIO_MANAGEMENT

      ") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "The Available Teams", content = @Content(schema = @Schema(implementation = AvailableTeams.class))), + @ApiResponse(responseCode = "200", description = "The Visible Teams", content = @Content(schema = @Schema(implementation = VisibleTeams.class))), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "Teams could not be found") }) - @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response availableTeams() { - UserPrincipal user; - boolean isAllTeams = false; - boolean required = false; try (QueryManager qm = new QueryManager()) { - if (super.isLdapUser()) { - user = qm.getLdapUser(getPrincipal().getName()); - } else if (super.isManagedUser()) { - user = qm.getManagedUser(getPrincipal().getName()); - } else if (super.isOidcUser()) { - user = qm.getOidcUser(getPrincipal().getName()); - } else { - return Response.status(401).build(); - } - final List allTeams = qm.getTeams(); - final List configProperties = qm.getConfigProperties(); - for (final ConfigProperty configProperty : configProperties) { - // Replace the value of encrypted strings with the pre-defined placeholder - if (configProperty.getGroupName().equals("access-management") - && configProperty.getPropertyName().equals("acl.enabled")) { - required = configProperty.getPropertyValue().equals("true"); - break; - } + Principal user = getPrincipal(); + List userTeams = new ArrayList(); + if (user instanceof final UserPrincipal userPrincipal) { + userTeams = userPrincipal.getTeams(); + } else if (user instanceof final ApiKey apiKey) { + userTeams = apiKey.getTeams(); } - qm.getPersistenceManager().detachCopyAll(configProperties); - qm.close(); - List permissions = user.getPermissions(); - for (Permission permission : permissions) { - if (permission.getName().equals("ACCESS_MANAGEMENT")) { - isAllTeams = true; - break; - } - } - AvailableTeams response = new AvailableTeams(); - response.setRequired(required); - List availableTeams = new ArrayList(); - List teams = isAllTeams ? allTeams : user.getTeams(); - for (Team team : teams) { - LittleTeam newTeam = new LittleTeam(); - newTeam.setValue(team.getUuid()); - newTeam.setText(team.getName()); - availableTeams.add(newTeam); + boolean required = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED); + boolean isAllTeams = qm.hasAccessManagementPermission(user); + List teams = isAllTeams ? qm.getTeams() : userTeams; + VisibleTeams response = new VisibleTeams(required, teams); - } - response.setTeams(availableTeams); return Response.ok(response).build(); } } @@ -254,15 +255,23 @@ public Response availableTeams() { @PUT @Path("/{uuid}/key") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Generates an API key and returns its value", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Generates an API key and returns its value", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "201", description = "The created API key", content = @Content(schema = @Schema(implementation = ApiKey.class))), + @ApiResponse( + responseCode = "201", + description = "The created API key", + content = @Content(schema = @Schema(implementation = ApiKey.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The team could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response generateApiKey( - @Parameter(description = "The UUID of the team to generate a key for", schema = @Schema(type = "string", format = "uuid"), required = true) @PathParam("uuid") @ValidUuid String uuid) { + @Parameter(description = "The UUID of the team to generate a key for", schema = @Schema(type = "string", format = "uuid"), required = true) + @PathParam("uuid") @ValidUuid String uuid) { try (QueryManager qm = new QueryManager()) { final Team team = qm.getObjectByUuid(Team.class, uuid); if (team != null) { @@ -277,15 +286,23 @@ public Response generateApiKey( @POST @Path("/key/{apikey}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Regenerates an API key by removing the specified key, generating a new one and returning its value", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Regenerates an API key by removing the specified key, generating a new one and returning its value", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "The re-generated API key", content = @Content(schema = @Schema(implementation = ApiKey.class))), + @ApiResponse( + responseCode = "200", + description = "The re-generated API key", + content = @Content(schema = @Schema(implementation = ApiKey.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The API key could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response regenerateApiKey( - @Parameter(description = "The API key to regenerate", required = true) @PathParam("apikey") String apikey) { + @Parameter(description = "The API key to regenerate", required = true) + @PathParam("apikey") String apikey) { try (QueryManager qm = new QueryManager()) { ApiKey apiKey = qm.getApiKey(apikey); if (apiKey != null) { @@ -301,15 +318,22 @@ public Response regenerateApiKey( @Path("/key/{key}/comment") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Updates an API key's comment", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Updates an API key's comment", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "The updated API key", content = @Content(schema = @Schema(implementation = ApiKey.class))), + @ApiResponse( + responseCode = "200", + description = "The updated API key", + content = @Content(schema = @Schema(implementation = ApiKey.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "404", description = "The API key could not be found") }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response updateApiKeyComment(@PathParam("key") final String key, - final String comment) { + final String comment) { try (final var qm = new QueryManager()) { qm.getPersistenceManager().setProperty(PROPERTY_RETAIN_VALUES, "true"); @@ -330,7 +354,10 @@ public Response updateApiKeyComment(@PathParam("key") final String key, @DELETE @Path("/key/{apikey}") - @Operation(summary = "Deletes the specified API key", description = "

      Requires permission ACCESS_MANAGEMENT

      ") + @Operation( + summary = "Deletes the specified API key", + description = "

      Requires permission ACCESS_MANAGEMENT

      " + ) @ApiResponses(value = { @ApiResponse(responseCode = "204", description = "API key removed successfully"), @ApiResponse(responseCode = "401", description = "Unauthorized"), @@ -338,7 +365,8 @@ public Response updateApiKeyComment(@PathParam("key") final String key, }) @PermissionRequired(Permissions.Constants.ACCESS_MANAGEMENT) public Response deleteApiKey( - @Parameter(description = "The API key to delete", required = true) @PathParam("apikey") String apikey) { + @Parameter(description = "The API key to delete", required = true) + @PathParam("apikey") String apikey) { try (QueryManager qm = new QueryManager()) { final ApiKey apiKey = qm.getApiKey(apikey); if (apiKey != null) { @@ -353,9 +381,14 @@ public Response deleteApiKey( @GET @Path("self") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns information about the current team.") + @Operation( + summary = "Returns information about the current team.") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "Information about the current team", content = @Content(schema = @Schema(implementation = TeamSelfResponse.class))), + @ApiResponse( + responseCode = "200", + description = "Information about the current team", + content = @Content(schema = @Schema(implementation = TeamSelfResponse.class)) + ), @ApiResponse(responseCode = "401", description = "Unauthorized"), @ApiResponse(responseCode = "400", description = "Invalid API key supplied"), @ApiResponse(responseCode = "404", description = "No Team for the given API key found") @@ -364,21 +397,19 @@ public Response getSelf() { if (Config.getInstance().getPropertyAsBoolean(Config.AlpineKey.ENFORCE_AUTHENTICATION)) { try (var qm = new QueryManager()) { if (isApiKey()) { - final var apiKey = qm.getApiKey(((ApiKey) getPrincipal()).getKey()); + final var apiKey = qm.getApiKey(((ApiKey)getPrincipal()).getKey()); final var team = apiKey.getTeams().stream().findFirst(); if (team.isPresent()) { return Response.ok(new TeamSelfResponse(team.get())).build(); } else { - return Response.status(Response.Status.NOT_FOUND).entity("No Team for the given API key found.") - .build(); + return Response.status(Response.Status.NOT_FOUND).entity("No Team for the given API key found.").build(); } } else { return Response.status(Response.Status.BAD_REQUEST).entity("Invalid API key supplied.").build(); } } } - // Authentication is not enabled, but we need to return a positive response - // without any principal data. + // Authentication is not enabled, but we need to return a positive response without any principal data. return Response.ok().build(); } } diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/VisibleTeams.java b/src/main/java/org/dependencytrack/resources/v1/vo/VisibleTeams.java new file mode 100644 index 0000000000..ba9f186262 --- /dev/null +++ b/src/main/java/org/dependencytrack/resources/v1/vo/VisibleTeams.java @@ -0,0 +1,27 @@ +/* + * This file is part of Dependency-Track. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.dependencytrack.resources.v1.vo; + +import java.util.List; + +import alpine.model.Team; + +public record VisibleTeams(boolean required, + List teams) { +} From d5e095605d915637777123cfa64cf1b828a1628b Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Tue, 27 Aug 2024 13:00:00 +0200 Subject: [PATCH 03/13] Now able to add accessTeam to the API Query and handles right Signed-off-by: Thomas Schauer-Koeckeis --- .../org/dependencytrack/model/Project.java | 1 - .../resources/v1/ProjectResource.java | 21 ++++++++----------- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 6fc9dae62d..df8c5873b4 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -273,7 +273,6 @@ public enum FetchGroup { @Join(column = "PROJECT_ID") @Element(column = "TEAM_ID") @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) - //@JsonIgnore private List accessTeams; @Persistent(defaultFetchGroup = "true") diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 31c84c83e4..e51db7ffbb 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -326,22 +326,19 @@ public Response createProject(Project jsonProject) { boolean required = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED); boolean isAdmin = qm.hasAccessManagementPermission(principal); if (required && choosenTeams.size() == 0) { - return Response.status(422).build(); + return Response.status(422) + .entity("You need to specify at least one team to which the project should belong").build(); } List visibleTeams = isAdmin ? qm.getTeams() : userTeams; - boolean hasTeam; + jsonProject.setAccessTeams(new ArrayList()); for (Team choosenTeam : choosenTeams) { - hasTeam = false; - LOGGER.info(Boolean.toString(visibleTeams.contains(choosenTeam)) + choosenTeam.getName()); - for (Team team : visibleTeams) { - if (team.getUuid().equals(choosenTeam.getUuid())) { - hasTeam = true; - break; - } - } - if (!hasTeam) { - return Response.status(403).build(); + Team ormTeam = qm.getObjectByUuid(Team.class, choosenTeam.getUuid()); + if (!visibleTeams.contains(ormTeam)) { + return isAdmin ? Response.status(404).entity("This team does not exist!").build() + : Response.status(403) + .entity("You don't have the permission to assign this team to a project.").build(); } + jsonProject.addAccessTeam(ormTeam); } if (!qm.doesProjectExist(StringUtils.trimToNull(jsonProject.getName()), StringUtils.trimToNull(jsonProject.getVersion()))) { final Project project; From 13ee8f4341a4b09d1f00840345fe6277e9f3922e Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Tue, 27 Aug 2024 13:12:30 +0200 Subject: [PATCH 04/13] Deleted unused files Signed-off-by: Thomas Schauer-Koeckeis --- .../dependencytrack/model/AvailableTeams.java | 56 ------------------- .../org/dependencytrack/model/LittleTeam.java | 53 ------------------ 2 files changed, 109 deletions(-) delete mode 100644 src/main/java/org/dependencytrack/model/AvailableTeams.java delete mode 100644 src/main/java/org/dependencytrack/model/LittleTeam.java diff --git a/src/main/java/org/dependencytrack/model/AvailableTeams.java b/src/main/java/org/dependencytrack/model/AvailableTeams.java deleted file mode 100644 index 1a4522166f..0000000000 --- a/src/main/java/org/dependencytrack/model/AvailableTeams.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.model; - -import java.io.Serializable; -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class AvailableTeams implements Serializable { - private boolean required; - private List teams; - - public boolean isRequired() { - return required; - } - - public void setRequired(final boolean required) { - this.required = required; - } - - public List getTeams() { - return teams; - } - - public void setTeams(final List teams) { - this.teams = teams; - } - - @Override - public String toString() { - List strlistTeams = teams.stream() - .map(Object::toString) - .toList(); - String strTeams = String.join(",", strlistTeams); - return String.format("required: %s, teams: [ %s ]", required, strTeams); - } - -} diff --git a/src/main/java/org/dependencytrack/model/LittleTeam.java b/src/main/java/org/dependencytrack/model/LittleTeam.java deleted file mode 100644 index 5355b5a0e0..0000000000 --- a/src/main/java/org/dependencytrack/model/LittleTeam.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.model; - -import java.io.Serializable; - -import java.util.UUID; - -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -public class LittleTeam implements Serializable { - - private UUID value; - private String text; - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public UUID getValue() { - return value; - } - - public void setValue(UUID value) { - this.value = value; - } - - @Override - public String toString() { - return String.format("{value: %s, text: %s}", value.toString(), text); - } -} From 639a4a58c394b40114afa9e32da574d7ac626151 Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Tue, 27 Aug 2024 13:14:35 +0200 Subject: [PATCH 05/13] Removed Debug info Signed-off-by: Thomas Schauer-Koeckeis --- .../java/org/dependencytrack/resources/v1/ProjectResource.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index e51db7ffbb..5edaf6bea5 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -315,7 +315,6 @@ public Response createProject(Project jsonProject) { jsonProject.setParent(parent); } final List choosenTeams = jsonProject.getAccessTeams(); - LOGGER.info(choosenTeams.toString()); Principal principal = getPrincipal(); List userTeams = new ArrayList(); if (principal instanceof final UserPrincipal userPrincipal) { From 78aa74b987372351eebdb3d8f8e692adc2323b6c Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Tue, 27 Aug 2024 16:56:02 +0200 Subject: [PATCH 06/13] Fixed most tests Signed-off-by: Thomas Schauer-Koeckeis --- .../resources/v1/ProjectResourceTest.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index acce124c10..adeabf8579 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -21,6 +21,7 @@ import alpine.common.util.UuidUtil; import alpine.event.framework.EventService; import alpine.model.IConfigProperty.PropertyType; +import alpine.model.Team; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import jakarta.json.Json; @@ -284,6 +285,7 @@ public void getProjectByUuidTest() { .withMatcher("childUuid", equalTo(childProject.getUuid().toString())) .isEqualTo(""" { + "accessTeams": [], "name": "acme-app", "version": "1.0.0", "uuid": "${json-unit.matches:projectUuid}", @@ -413,6 +415,7 @@ public void getProjectByUnknownTagTest() { @Test public void createProjectTest(){ Project project = new Project(); + project.setAccessTeams(new ArrayList()); project.setName("Acme Example"); project.setVersion("1.0"); project.setDescription("Test project"); @@ -433,6 +436,7 @@ public void createProjectTest(){ @Test public void createProjectDuplicateTest() { Project project = new Project(); + project.setAccessTeams(new ArrayList()); project.setName("Acme Example"); project.setVersion("1.0"); Response response = jersey.target(V1_PROJECT) @@ -452,6 +456,7 @@ public void createProjectDuplicateTest() { @Test public void createProjectWithoutVersionDuplicateTest() { Project project = new Project(); + project.setAccessTeams(new ArrayList()); project.setName("Acme Example"); Response response = jersey.target(V1_PROJECT) .request() @@ -708,6 +713,7 @@ public void patchProjectSuccessfullyPatchedTest() { .withMatcher("projectUuid", equalTo(p1.getUuid().toString())) .isEqualTo(""" { + "accessTeams": [], "publisher": "new publisher", "manufacturer": { "name": "manufacturerName", @@ -804,6 +810,7 @@ public void patchProjectParentTest() { .withMatcher("parentProjectUuid", CoreMatchers.equalTo(newParent.getUuid().toString())) .isEqualTo(""" { + "accessTeams": [], "name": "DEF", "version": "2.0", "uuid": "${json-unit.matches:projectUuid}", @@ -1162,6 +1169,7 @@ public void issue3883RegressionTest() { .header(X_API_KEY, apiKey) .put(Entity.json(""" { + "accessTeams": [], "name": "acme-app-parent", "version": "1.0.0" } @@ -1174,6 +1182,7 @@ public void issue3883RegressionTest() { .header(X_API_KEY, apiKey) .put(Entity.json(""" { + "accessTeams": [], "name": "acme-app", "version": "1.0.0", "parent": { @@ -1191,6 +1200,7 @@ public void issue3883RegressionTest() { assertThat(response.getStatus()).isEqualTo(200); assertThatJson(getPlainTextBody(response)).isEqualTo(""" { + "accessTeams": [], "name": "acme-app-parent", "version": "1.0.0", "classifier": "APPLICATION", @@ -1224,6 +1234,7 @@ public void issue3883RegressionTest() { assertThat(response.getStatus()).isEqualTo(200); assertThatJson(getPlainTextBody(response)).isEqualTo(""" { + "accessTeams": [], "name": "acme-app", "version": "1.0.0", "classifier": "APPLICATION", @@ -1264,7 +1275,8 @@ public void issue4048RegressionTest() { final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() .add("name", "project-%d-%d".formatted(i, j)) - .add("version", "%d.%d".formatted(i, j)); + .add("version", "%d.%d".formatted(i, j)) + .add("accessTeams", "[]"); if (parentUuid != null) { requestBodyBuilder.add("parent", Json.createObjectBuilder() .add("uuid", parentUuid.toString())); From b24d6f90820cba80edcca40d98f7bc0e00998db5 Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Wed, 28 Aug 2024 11:20:51 +0200 Subject: [PATCH 07/13] Fixed last failing test Signed-off-by: Thomas Schauer-Koeckeis --- .../org/dependencytrack/resources/v1/ProjectResourceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index adeabf8579..e3f5c7ea8d 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -1276,7 +1276,7 @@ public void issue4048RegressionTest() { final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() .add("name", "project-%d-%d".formatted(i, j)) .add("version", "%d.%d".formatted(i, j)) - .add("accessTeams", "[]"); + .add("accessTeams",Json.createArrayBuilder().build()); if (parentUuid != null) { requestBodyBuilder.add("parent", Json.createObjectBuilder() .add("uuid", parentUuid.toString())); From e663e7e70969f206a288ccd441a663d5c5a9e0f2 Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Thu, 29 Aug 2024 11:10:26 +0200 Subject: [PATCH 08/13] Fixed Tests and bug for no Admin Users Signed-off-by: Thomas Schauer-Koeckeis --- .../resources/v1/ProjectResource.java | 8 +- .../resources/v1/ProjectResourceTest.java | 120 ++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index 5edaf6bea5..b85446aee7 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -69,6 +69,7 @@ import java.util.Collection; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Function; @@ -324,19 +325,20 @@ public Response createProject(Project jsonProject) { } boolean required = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED); boolean isAdmin = qm.hasAccessManagementPermission(principal); - if (required && choosenTeams.size() == 0) { + if (required && choosenTeams.isEmpty()) { return Response.status(422) .entity("You need to specify at least one team to which the project should belong").build(); } List visibleTeams = isAdmin ? qm.getTeams() : userTeams; + List visibleUuids = visibleTeams.isEmpty() ? new ArrayList(): visibleTeams.stream().map(Team::getUuid).toList(); jsonProject.setAccessTeams(new ArrayList()); for (Team choosenTeam : choosenTeams) { - Team ormTeam = qm.getObjectByUuid(Team.class, choosenTeam.getUuid()); - if (!visibleTeams.contains(ormTeam)) { + if (!visibleUuids.contains(choosenTeam.getUuid())) { return isAdmin ? Response.status(404).entity("This team does not exist!").build() : Response.status(403) .entity("You don't have the permission to assign this team to a project.").build(); } + Team ormTeam = qm.getObjectByUuid(Team.class, choosenTeam.getUuid()); jsonProject.addAccessTeam(ormTeam); } if (!qm.doesProjectExist(StringUtils.trimToNull(jsonProject.getName()), StringUtils.trimToNull(jsonProject.getVersion()))) { diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index e3f5c7ea8d..1418ab36e1 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -20,8 +20,12 @@ import alpine.common.util.UuidUtil; import alpine.event.framework.EventService; +import alpine.model.IConfigProperty; +import alpine.model.ManagedUser; import alpine.model.IConfigProperty.PropertyType; import alpine.model.Team; +import alpine.model.Permission; +import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; import alpine.server.filters.AuthenticationFilter; import jakarta.json.Json; @@ -51,6 +55,7 @@ import org.dependencytrack.model.ServiceComponent; import org.dependencytrack.model.Tag; import org.dependencytrack.model.Vulnerability; +import org.dependencytrack.persistence.DefaultObjectGenerator; import org.dependencytrack.tasks.CloneProjectTask; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.glassfish.jersey.client.HttpUrlConnectorProvider; @@ -76,6 +81,8 @@ import static org.hamcrest.Matchers.equalTo; public class ProjectResourceTest extends ResourceTest { + private ManagedUser testUser; + private String jwt; @ClassRule public static JerseyTestRule jersey = new JerseyTestRule( @@ -90,6 +97,23 @@ public void after() throws Exception { super.after(); } + public void getUserToken(boolean isAdmin) { + testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); + jwt = new JsonWebToken().createToken(testUser); + qm.addUserToTeam(testUser, team); + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultPermissions(); + List permissionsList = new ArrayList(); + final Permission permission = qm.getPermission("PORTFOLIO_MANAGEMENT"); + permissionsList.add(permission); + testUser.setPermissions(permissionsList); + if (isAdmin) { + final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT"); + permissionsList.add(adminPermission); + testUser.setPermissions(permissionsList); + } + } + @Test public void getProjectsDefaultRequestTest() { for (int i=0; i<1000; i++) { @@ -483,6 +507,102 @@ public void createProjectEmptyTest() { Assert.assertEquals(400, response.getStatus(), 0); } + @Test + public void createProjectWithExistingTeamRequiredTest() { + getUserToken(false); + Team AllowedTeam = qm.createTeam("AllowedTeam", false); + Project project = new Project(); + project.setName("ProjectWithExistingTeamRequired"); + qm.addUserToTeam(testUser, AllowedTeam); + qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + final JsonObject jsonTeam = Json.createObjectBuilder().add("uuid", AllowedTeam.getUuid().toString()).build(); + final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() + .add("name", project.getName()).add("classifier", "CONTAINER").addNull("parent").add("active", true) + .add("accessTeams", Json.createArrayBuilder().add(jsonTeam).build()); + Response response = jersey.target(V1_PROJECT) + .request() + .header("Authorization", "Bearer " + jwt) + .put(Entity.json(requestBodyBuilder.build().toString())); + Assert.assertEquals(201, response.getStatus(), 0); + } + + @Test + public void createProjectWithoutExistingTeamRequiredTest() { + getUserToken(false); + Project project = new Project(); + project.setName("ProjectWithoutExistingTeamRequired"); + project.setAccessTeams(new ArrayList()); + qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + Response response = jersey.target(V1_PROJECT) + .request() + .header("Authorization", "Bearer " + jwt) + .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + Assert.assertEquals(422, response.getStatus(), 0); + } + + @Test + public void createProjectWithNotAllowedExistingTeamTest() { + getUserToken(false); + Team notAllowedTeam = qm.createTeam("NotAllowedTeam", false); + Project project = new Project(); + project.setName("ProjectWithNotAllowedExistingTeam"); + project.addAccessTeam(notAllowedTeam); + qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + Response response = jersey.target(V1_PROJECT) + .request() + .header("Authorization", "Bearer " + jwt) + .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + Assert.assertEquals(403, response.getStatus(), 0); + } + + @Test + public void createProjectWithNotAllowedExistingTeamAdminTest() { + getUserToken(true); + Team notAllowedTeam = qm.createTeam("NotAllowedTeam", false); + Project project = new Project(); + project.setName("ProjectWithNotAllowedExistingTeam"); + project.addAccessTeam(notAllowedTeam); + qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + Response response = jersey.target(V1_PROJECT) + .request() + .header("Authorization", "Bearer " + jwt) + .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + Assert.assertEquals(201, response.getStatus(), 0); + } + + @Test + public void createProjectWithNotExistingTeamNoAdminTest() { + getUserToken(false); + Team notAllowedTeam = new Team(); + notAllowedTeam.setUuid(new UUID(1, 1)); + notAllowedTeam.setName("NotAllowedTeam"); + Project project = new Project(); + project.addAccessTeam(notAllowedTeam); + project.setName("ProjectWithNotAllowedExistingTeam"); + qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + Response response = jersey.target(V1_PROJECT) + .request() + .header("Authorization", "Bearer " + jwt) + .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + Assert.assertEquals(403, response.getStatus(), 0); + } + + @Test + public void createProjectWithNotExistingTeamTest() { + getUserToken(true); + Team notAllowedTeam = new Team(); + notAllowedTeam.setUuid(new UUID(1, 1)); + notAllowedTeam.setName("NotAllowedTeam"); + Project project = new Project(); + project.addAccessTeam(notAllowedTeam); + project.setName("ProjectWithNotExistingTeam"); + Response response = jersey.target(V1_PROJECT) + .request() + .header("Authorization", "Bearer " + jwt) + .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + Assert.assertEquals(404, response.getStatus(), 0); + } + @Test public void updateProjectTest() { Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false); From d8f24999fe2f565ba675b8cba660427c771c1e09 Mon Sep 17 00:00:00 2001 From: Thomas Schauer-Koeckeis Date: Fri, 30 Aug 2024 09:39:31 +0200 Subject: [PATCH 09/13] Added Tests for the visible API endpoint Signed-off-by: Thomas Schauer-Koeckeis --- .../resources/v1/TeamResourceTest.java | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index b1662d8d36..be052f347e 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -21,7 +21,9 @@ import alpine.common.util.UuidUtil; import alpine.model.ApiKey; import alpine.model.ConfigProperty; +import alpine.model.IConfigProperty; import alpine.model.ManagedUser; +import alpine.model.Permission; import alpine.model.Team; import alpine.server.auth.JsonWebToken; import alpine.server.filters.ApiFilter; @@ -31,6 +33,7 @@ import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Project; +import org.dependencytrack.persistence.DefaultObjectGenerator; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.server.ResourceConfig; import org.junit.Assert; @@ -42,6 +45,9 @@ import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; + +import java.util.ArrayList; +import java.util.List; import java.util.UUID; import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; @@ -49,6 +55,9 @@ import static org.hamcrest.CoreMatchers.equalTo; public class TeamResourceTest extends ResourceTest { + private ManagedUser testUser; + private String jwt; + private Team userNotPartof; @ClassRule public static JerseyTestRule jersey = new JerseyTestRule( @@ -56,6 +65,21 @@ public class TeamResourceTest extends ResourceTest { .register(ApiFilter.class) .register(AuthenticationFilter.class)); + public void getUserToken(boolean isAdmin) { + testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); + jwt = new JsonWebToken().createToken(testUser); + qm.addUserToTeam(testUser, team); + userNotPartof = qm.createTeam("UserNotPartof", false); + if (isAdmin) { + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultPermissions(); + List permissionsList = new ArrayList(); + final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT"); + permissionsList.add(adminPermission); + testUser.setPermissions(permissionsList); + } + } + @Test public void getTeamsTest() { for (int i=0; i<1000; i++) { @@ -206,6 +230,70 @@ public void deleteTeamWithAclTest() { Assert.assertEquals(204, response.getStatus(), 0); } + @Test + public void getVisibleAdminRequiredTeams() { + getUserToken(true); + qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + Response response = jersey.target(V1_TEAM + "/visible") + .request() + .header("Authorization", "Bearer " + jwt) + .get(); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject body = parseJsonObject(response); + Assert.assertTrue(body.getBoolean("required")); + JsonArray teams = body.getJsonArray("teams"); + Assert.assertEquals(teams.size(), 2); + Assert.assertEquals(teams.getFirst().asJsonObject().getString("uuid"), this.team.getUuid().toString()); + Assert.assertEquals(teams.get(1).asJsonObject().getString("uuid"), userNotPartof.getUuid().toString()); + } + + @Test + public void getVisibleAdminNotRequiredTeams() { + getUserToken(true); + Response response = jersey.target(V1_TEAM + "/visible") + .request() + .header("Authorization", "Bearer " + jwt) + .get(); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject body = parseJsonObject(response); + Assert.assertFalse(body.getBoolean("required")); + JsonArray teams = body.getJsonArray("teams"); + Assert.assertEquals(teams.size(), 2); + Assert.assertEquals(teams.getFirst().asJsonObject().getString("uuid"), this.team.getUuid().toString()); + Assert.assertEquals(teams.get(1).asJsonObject().getString("uuid"), userNotPartof.getUuid().toString()); + } + + @Test + public void getVisibleNotAdminRequiredTeams() { + getUserToken(false); + qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + Response response = jersey.target(V1_TEAM + "/visible") + .request() + .header("Authorization", "Bearer " + jwt) + .get(); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject body = parseJsonObject(response); + Assert.assertTrue(body.getBoolean("required")); + JsonArray teams = body.getJsonArray("teams"); + Assert.assertEquals(teams.size(), 1); + Assert.assertEquals(teams.getFirst().asJsonObject().getString("uuid"), this.team.getUuid().toString()); + } + + @Test + public void getVisibleNotAdminNotRequiredTeams() { + getUserToken(false); + Response response = jersey.target(V1_TEAM + "/visible") + .request() + .header("Authorization", "Bearer " + jwt) + .get(); + Assert.assertEquals(200, response.getStatus(), 0); + JsonObject body = parseJsonObject(response); + Assert.assertFalse(body.getBoolean("required")); + JsonArray teams = body.getJsonArray("teams"); + Assert.assertEquals(teams.size(), 1); + Assert.assertEquals(teams.getFirst().asJsonObject().getString("uuid"), this.team.getUuid().toString()); + } + @Test public void generateApiKeyTest() { Team team = qm.createTeam("My Team", false); From f45225ed9d03b564a330c7fc99f22e53c6c1346f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Schauer-K=C3=B6ckeis=20Thomas?= Date: Tue, 10 Sep 2024 07:53:40 +0200 Subject: [PATCH 10/13] Fixed some things MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Schauer-Köckeis --- .../java/org/dependencytrack/resources/v1/TeamResource.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index 23817c545c..fb447ac635 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -228,11 +228,10 @@ public Response deleteTeam(Team jsonTeam) { @GET @Path("/visible") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns a list of Teams what are visible", description = "Requires permission PORTFOLIO_MANAGEMENT

      ") + @Operation(summary = "Returns a list of Teams that are visible", description = "

      ") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "The Visible Teams", content = @Content(schema = @Schema(implementation = VisibleTeams.class))), - @ApiResponse(responseCode = "401", description = "Unauthorized"), - @ApiResponse(responseCode = "404", description = "Teams could not be found") + @ApiResponse(responseCode = "401", description = "Unauthorized") }) public Response availableTeams() { try (QueryManager qm = new QueryManager()) { From 3822462c82a2bc876a634ff4d80bc13a70d98636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Schauer-K=C3=B6ckeis=20Thomas?= Date: Tue, 10 Sep 2024 07:52:20 +0200 Subject: [PATCH 11/13] Switched params for tests, so expected is the real expected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Schauer-Köckeis --- .../resources/v1/TeamResourceTest.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index be052f347e..0e463ba7df 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -242,9 +242,9 @@ public void getVisibleAdminRequiredTeams() { JsonObject body = parseJsonObject(response); Assert.assertTrue(body.getBoolean("required")); JsonArray teams = body.getJsonArray("teams"); - Assert.assertEquals(teams.size(), 2); - Assert.assertEquals(teams.getFirst().asJsonObject().getString("uuid"), this.team.getUuid().toString()); - Assert.assertEquals(teams.get(1).asJsonObject().getString("uuid"), userNotPartof.getUuid().toString()); + Assert.assertEquals(2, teams.size()); + Assert.assertEquals(this.team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); + Assert.assertEquals(userNotPartof.getUuid().toString(), teams.get(1).asJsonObject().getString("uuid")); } @Test @@ -258,9 +258,9 @@ public void getVisibleAdminNotRequiredTeams() { JsonObject body = parseJsonObject(response); Assert.assertFalse(body.getBoolean("required")); JsonArray teams = body.getJsonArray("teams"); - Assert.assertEquals(teams.size(), 2); - Assert.assertEquals(teams.getFirst().asJsonObject().getString("uuid"), this.team.getUuid().toString()); - Assert.assertEquals(teams.get(1).asJsonObject().getString("uuid"), userNotPartof.getUuid().toString()); + Assert.assertEquals(2, teams.size()); + Assert.assertEquals(this.team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); + Assert.assertEquals(userNotPartof.getUuid().toString(), teams.get(1).asJsonObject().getString("uuid")); } @Test @@ -275,8 +275,8 @@ public void getVisibleNotAdminRequiredTeams() { JsonObject body = parseJsonObject(response); Assert.assertTrue(body.getBoolean("required")); JsonArray teams = body.getJsonArray("teams"); - Assert.assertEquals(teams.size(), 1); - Assert.assertEquals(teams.getFirst().asJsonObject().getString("uuid"), this.team.getUuid().toString()); + Assert.assertEquals(1, teams.size()); + Assert.assertEquals(this.team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test @@ -290,8 +290,8 @@ public void getVisibleNotAdminNotRequiredTeams() { JsonObject body = parseJsonObject(response); Assert.assertFalse(body.getBoolean("required")); JsonArray teams = body.getJsonArray("teams"); - Assert.assertEquals(teams.size(), 1); - Assert.assertEquals(teams.getFirst().asJsonObject().getString("uuid"), this.team.getUuid().toString()); + Assert.assertEquals(1, teams.size()); + Assert.assertEquals(this.team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test From 0cb20df002ab4d119900dd2158d4a0045a4ef698 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Schauer-K=C3=B6ckeis?= Date: Fri, 20 Sep 2024 13:09:00 +0200 Subject: [PATCH 12/13] Implemented things from the review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Schauer-Köckeis --- .../model/ConfigPropertyConstants.java | 2 +- .../org/dependencytrack/model/Project.java | 1 + .../resources/v1/ProjectResource.java | 61 ++++++++------ .../resources/v1/TeamResource.java | 25 +++--- .../resources/v1/vo/VisibleTeams.java | 27 ------ .../resources/v1/ProjectResourceTest.java | 83 +++++++++++-------- .../resources/v1/TeamResourceTest.java | 56 ++++++------- 7 files changed, 124 insertions(+), 131 deletions(-) delete mode 100644 src/main/java/org/dependencytrack/resources/v1/vo/VisibleTeams.java diff --git a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java index 7515206c91..fdc9ecb4f4 100644 --- a/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java +++ b/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java @@ -95,7 +95,7 @@ public enum ConfigPropertyConstants { KENNA_SYNC_CADENCE("integrations", "kenna.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to Kenna Security"), KENNA_TOKEN("integrations", "kenna.token", null, PropertyType.ENCRYPTEDSTRING, "The token to use when authenticating to Kenna Security"), KENNA_CONNECTOR_ID("integrations", "kenna.connector.id", null, PropertyType.STRING, "The Kenna Security connector identifier to upload to"), - ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio"), + ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio", true), NOTIFICATION_TEMPLATE_BASE_DIR("notification", "template.baseDir", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_BASE_DIRECTORY", System.getProperty("user.home")), PropertyType.STRING, "The base directory to use when searching for notification templates"), NOTIFICATION_TEMPLATE_DEFAULT_OVERRIDE_ENABLED("notification", "template.default.override.enabled", SystemUtils.getEnvironmentVariable("DEFAULT_TEMPLATES_OVERRIDE_ENABLED", "false"), PropertyType.BOOLEAN, "Flag to enable/disable override of default notification templates"), TASK_SCHEDULER_LDAP_SYNC_CADENCE("task-scheduler", "ldap.sync.cadence", "6", PropertyType.INTEGER, "Sync cadence (in hours) for LDAP"), diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index df8c5873b4..5d10072ee4 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -273,6 +273,7 @@ public enum FetchGroup { @Join(column = "PROJECT_ID") @Element(column = "TEAM_ID") @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) + @JsonInclude(value = JsonInclude.Include.NON_EMPTY) private List accessTeams; @Persistent(defaultFetchGroup = "true") diff --git a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java index b85446aee7..b209b331e4 100644 --- a/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/ProjectResource.java @@ -284,11 +284,13 @@ public Response getProjectsByClassifier( content = @Content(schema = @Schema(implementation = Project.class)) ), @ApiResponse(responseCode = "401", description = "Unauthorized"), + @ApiResponse(responseCode = "403", description = "You don't have the permission to assign this team to a project."), @ApiResponse(responseCode = "409", description = """
      • An inactive Parent cannot be selected as parent, or
      • A project with the specified name already exists
      """), + @ApiResponse(responseCode = "422", description = "You need to specify at least one team to which the project should belong"), }) @PermissionRequired(Permissions.Constants.PORTFOLIO_MANAGEMENT) public Response createProject(Project jsonProject) { @@ -315,33 +317,40 @@ public Response createProject(Project jsonProject) { Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid()); jsonProject.setParent(parent); } - final List choosenTeams = jsonProject.getAccessTeams(); - Principal principal = getPrincipal(); - List userTeams = new ArrayList(); - if (principal instanceof final UserPrincipal userPrincipal) { - userTeams = userPrincipal.getTeams(); - } else if (principal instanceof final ApiKey apiKey) { - userTeams = apiKey.getTeams(); - } - boolean required = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED); - boolean isAdmin = qm.hasAccessManagementPermission(principal); - if (required && choosenTeams.isEmpty()) { - return Response.status(422) - .entity("You need to specify at least one team to which the project should belong").build(); - } - List visibleTeams = isAdmin ? qm.getTeams() : userTeams; - List visibleUuids = visibleTeams.isEmpty() ? new ArrayList(): visibleTeams.stream().map(Team::getUuid).toList(); - jsonProject.setAccessTeams(new ArrayList()); - for (Team choosenTeam : choosenTeams) { - if (!visibleUuids.contains(choosenTeam.getUuid())) { - return isAdmin ? Response.status(404).entity("This team does not exist!").build() - : Response.status(403) - .entity("You don't have the permission to assign this team to a project.").build(); + if (!qm.doesProjectExist(StringUtils.trimToNull(jsonProject.getName()), + StringUtils.trimToNull(jsonProject.getVersion()))) { + final List chosenTeams = jsonProject.getAccessTeams() == null ? new ArrayList() + : jsonProject.getAccessTeams(); + boolean required = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED); + if (required && chosenTeams.isEmpty()) { + return Response.status(422) + .entity("You need to specify at least one team to which the project should belong").build(); } - Team ormTeam = qm.getObjectByUuid(Team.class, choosenTeam.getUuid()); - jsonProject.addAccessTeam(ormTeam); - } - if (!qm.doesProjectExist(StringUtils.trimToNull(jsonProject.getName()), StringUtils.trimToNull(jsonProject.getVersion()))) { + Principal principal = getPrincipal(); + if (!chosenTeams.isEmpty()) { + List userTeams = new ArrayList(); + if (principal instanceof final UserPrincipal userPrincipal) { + userTeams = userPrincipal.getTeams(); + } else if (principal instanceof final ApiKey apiKey) { + userTeams = apiKey.getTeams(); + } + boolean isAdmin = qm.hasAccessManagementPermission(principal); + List visibleTeams = isAdmin ? qm.getTeams() : userTeams; + List visibleUuids = visibleTeams.isEmpty() ? new ArrayList() + : visibleTeams.stream().map(Team::getUuid).toList(); + jsonProject.setAccessTeams(new ArrayList()); + for (Team choosenTeam : chosenTeams) { + if (!visibleUuids.contains(choosenTeam.getUuid())) { + return isAdmin ? Response.status(404).entity("This team does not exist!").build() + : Response.status(403) + .entity("You don't have the permission to assign this team to a project.") + .build(); + } + Team ormTeam = qm.getObjectByUuid(Team.class, choosenTeam.getUuid()); + jsonProject.addAccessTeam(ormTeam); + } + } + final Project project; try { project = qm.createProject(jsonProject, jsonProject.getTags(), true); diff --git a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java index fb447ac635..469a55a258 100644 --- a/src/main/java/org/dependencytrack/resources/v1/TeamResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/TeamResource.java @@ -37,11 +37,9 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirements; import io.swagger.v3.oas.annotations.tags.Tag; import org.dependencytrack.auth.Permissions; -import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.validation.ValidUuid; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.resources.v1.vo.TeamSelfResponse; -import org.dependencytrack.resources.v1.vo.VisibleTeams; import org.owasp.security.logging.SecurityMarkers; import jakarta.validation.Validator; @@ -230,24 +228,25 @@ public Response deleteTeam(Team jsonTeam) { @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "Returns a list of Teams that are visible", description = "

      ") @ApiResponses(value = { - @ApiResponse(responseCode = "200", description = "The Visible Teams", content = @Content(schema = @Schema(implementation = VisibleTeams.class))), + @ApiResponse(responseCode = "200", description = "The Visible Teams", content = @Content(array = @ArraySchema(schema = @Schema(implementation = Team.class)))), @ApiResponse(responseCode = "401", description = "Unauthorized") }) public Response availableTeams() { try (QueryManager qm = new QueryManager()) { Principal user = getPrincipal(); - List userTeams = new ArrayList(); - if (user instanceof final UserPrincipal userPrincipal) { - userTeams = userPrincipal.getTeams(); - } else if (user instanceof final ApiKey apiKey) { - userTeams = apiKey.getTeams(); - } - boolean required = qm.isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED); boolean isAllTeams = qm.hasAccessManagementPermission(user); - List teams = isAllTeams ? qm.getTeams() : userTeams; - VisibleTeams response = new VisibleTeams(required, teams); + List teams = new ArrayList(); + if (isAllTeams) { + teams = qm.getTeams(); + } else { + if (user instanceof final UserPrincipal userPrincipal) { + teams = userPrincipal.getTeams(); + } else if (user instanceof final ApiKey apiKey) { + teams = apiKey.getTeams(); + } + } - return Response.ok(response).build(); + return Response.ok(teams).build(); } } diff --git a/src/main/java/org/dependencytrack/resources/v1/vo/VisibleTeams.java b/src/main/java/org/dependencytrack/resources/v1/vo/VisibleTeams.java deleted file mode 100644 index ba9f186262..0000000000 --- a/src/main/java/org/dependencytrack/resources/v1/vo/VisibleTeams.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright (c) OWASP Foundation. All Rights Reserved. - */ -package org.dependencytrack.resources.v1.vo; - -import java.util.List; - -import alpine.model.Team; - -public record VisibleTeams(boolean required, - List teams) { -} diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 1418ab36e1..039f4ec4fc 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -20,9 +20,8 @@ import alpine.common.util.UuidUtil; import alpine.event.framework.EventService; -import alpine.model.IConfigProperty; -import alpine.model.ManagedUser; import alpine.model.IConfigProperty.PropertyType; +import alpine.model.ManagedUser; import alpine.model.Team; import alpine.model.Permission; import alpine.server.auth.JsonWebToken; @@ -97,7 +96,7 @@ public void after() throws Exception { super.after(); } - public void getUserToken(boolean isAdmin) { + public void setUpUser(boolean isAdmin, boolean isRequired) { testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); jwt = new JsonWebToken().createToken(testUser); qm.addUserToTeam(testUser, team); @@ -112,6 +111,14 @@ public void getUserToken(boolean isAdmin) { permissionsList.add(adminPermission); testUser.setPermissions(permissionsList); } + if (isRequired) { + qm.createConfigProperty( + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(), + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(), + "true", + ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), + null); + } } @Test @@ -309,7 +316,6 @@ public void getProjectByUuidTest() { .withMatcher("childUuid", equalTo(childProject.getUuid().toString())) .isEqualTo(""" { - "accessTeams": [], "name": "acme-app", "version": "1.0.0", "uuid": "${json-unit.matches:projectUuid}", @@ -439,7 +445,6 @@ public void getProjectByUnknownTagTest() { @Test public void createProjectTest(){ Project project = new Project(); - project.setAccessTeams(new ArrayList()); project.setName("Acme Example"); project.setVersion("1.0"); project.setDescription("Test project"); @@ -460,7 +465,6 @@ public void createProjectTest(){ @Test public void createProjectDuplicateTest() { Project project = new Project(); - project.setAccessTeams(new ArrayList()); project.setName("Acme Example"); project.setVersion("1.0"); Response response = jersey.target(V1_PROJECT) @@ -480,7 +484,6 @@ public void createProjectDuplicateTest() { @Test public void createProjectWithoutVersionDuplicateTest() { Project project = new Project(); - project.setAccessTeams(new ArrayList()); project.setName("Acme Example"); Response response = jersey.target(V1_PROJECT) .request() @@ -509,30 +512,31 @@ public void createProjectEmptyTest() { @Test public void createProjectWithExistingTeamRequiredTest() { - getUserToken(false); + setUpUser(false, true); Team AllowedTeam = qm.createTeam("AllowedTeam", false); Project project = new Project(); project.setName("ProjectWithExistingTeamRequired"); qm.addUserToTeam(testUser, AllowedTeam); - qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); final JsonObject jsonTeam = Json.createObjectBuilder().add("uuid", AllowedTeam.getUuid().toString()).build(); final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() - .add("name", project.getName()).add("classifier", "CONTAINER").addNull("parent").add("active", true) + .add("name", project.getName()).add("classifier", "CONTAINER").addNull("parent").add("active", true).add("tags", Json.createArrayBuilder()) .add("accessTeams", Json.createArrayBuilder().add(jsonTeam).build()); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) .put(Entity.json(requestBodyBuilder.build().toString())); - Assert.assertEquals(201, response.getStatus(), 0); + Assert.assertEquals(201, response.getStatus()); + JsonObject returnedProject = parseJsonObject(response); + JsonArray teams = returnedProject.getJsonArray("accessTeams"); + Assert.assertEquals(teams.size(), 1); + Assert.assertEquals(AllowedTeam.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test public void createProjectWithoutExistingTeamRequiredTest() { - getUserToken(false); + setUpUser(false, true); Project project = new Project(); project.setName("ProjectWithoutExistingTeamRequired"); - project.setAccessTeams(new ArrayList()); - qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) @@ -542,54 +546,55 @@ public void createProjectWithoutExistingTeamRequiredTest() { @Test public void createProjectWithNotAllowedExistingTeamTest() { - getUserToken(false); + setUpUser(false, true); Team notAllowedTeam = qm.createTeam("NotAllowedTeam", false); Project project = new Project(); project.setName("ProjectWithNotAllowedExistingTeam"); project.addAccessTeam(notAllowedTeam); - qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); - Assert.assertEquals(403, response.getStatus(), 0); + Assert.assertEquals(403, response.getStatus()); } @Test public void createProjectWithNotAllowedExistingTeamAdminTest() { - getUserToken(true); + setUpUser(true, true); Team notAllowedTeam = qm.createTeam("NotAllowedTeam", false); Project project = new Project(); project.setName("ProjectWithNotAllowedExistingTeam"); project.addAccessTeam(notAllowedTeam); - qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); - Assert.assertEquals(201, response.getStatus(), 0); + Assert.assertEquals(201, response.getStatus()); + JsonObject returnedProject = parseJsonObject(response); + JsonArray teams = returnedProject.getJsonArray("accessTeams"); + Assert.assertEquals(teams.size(), 1); + Assert.assertEquals(notAllowedTeam.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test public void createProjectWithNotExistingTeamNoAdminTest() { - getUserToken(false); + setUpUser(false, true); Team notAllowedTeam = new Team(); notAllowedTeam.setUuid(new UUID(1, 1)); notAllowedTeam.setName("NotAllowedTeam"); Project project = new Project(); project.addAccessTeam(notAllowedTeam); project.setName("ProjectWithNotAllowedExistingTeam"); - qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); - Assert.assertEquals(403, response.getStatus(), 0); + Assert.assertEquals(403, response.getStatus()); } @Test public void createProjectWithNotExistingTeamTest() { - getUserToken(true); + setUpUser(true, false); Team notAllowedTeam = new Team(); notAllowedTeam.setUuid(new UUID(1, 1)); notAllowedTeam.setName("NotAllowedTeam"); @@ -600,7 +605,26 @@ public void createProjectWithNotExistingTeamTest() { .request() .header("Authorization", "Bearer " + jwt) .put(Entity.entity(project, MediaType.APPLICATION_JSON)); - Assert.assertEquals(404, response.getStatus(), 0); + Assert.assertEquals(404, response.getStatus()); + } + + @Test + public void createProjectWithApiKeyTest() { + Project project = new Project(); + project.setName("ProjectWithNotExistingTeam"); + final JsonObject jsonTeam = Json.createObjectBuilder().add("uuid", team.getUuid().toString()).build(); + final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() + .add("name", project.getName()).add("classifier", "CONTAINER").addNull("parent").add("active", true).add("tags", Json.createArrayBuilder()) + .add("accessTeams", Json.createArrayBuilder().add(jsonTeam).build()); + Response response = jersey.target(V1_PROJECT) + .request() + .header(X_API_KEY, apiKey) + .put(Entity.json(requestBodyBuilder.build().toString())); + Assert.assertEquals(201, response.getStatus()); + JsonObject returnedProject = parseJsonObject(response); + JsonArray teams = returnedProject.getJsonArray("accessTeams"); + Assert.assertEquals(teams.size(), 1); + Assert.assertEquals(team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test @@ -833,7 +857,6 @@ public void patchProjectSuccessfullyPatchedTest() { .withMatcher("projectUuid", equalTo(p1.getUuid().toString())) .isEqualTo(""" { - "accessTeams": [], "publisher": "new publisher", "manufacturer": { "name": "manufacturerName", @@ -930,7 +953,6 @@ public void patchProjectParentTest() { .withMatcher("parentProjectUuid", CoreMatchers.equalTo(newParent.getUuid().toString())) .isEqualTo(""" { - "accessTeams": [], "name": "DEF", "version": "2.0", "uuid": "${json-unit.matches:projectUuid}", @@ -1289,7 +1311,6 @@ public void issue3883RegressionTest() { .header(X_API_KEY, apiKey) .put(Entity.json(""" { - "accessTeams": [], "name": "acme-app-parent", "version": "1.0.0" } @@ -1302,7 +1323,6 @@ public void issue3883RegressionTest() { .header(X_API_KEY, apiKey) .put(Entity.json(""" { - "accessTeams": [], "name": "acme-app", "version": "1.0.0", "parent": { @@ -1320,7 +1340,6 @@ public void issue3883RegressionTest() { assertThat(response.getStatus()).isEqualTo(200); assertThatJson(getPlainTextBody(response)).isEqualTo(""" { - "accessTeams": [], "name": "acme-app-parent", "version": "1.0.0", "classifier": "APPLICATION", @@ -1354,7 +1373,6 @@ public void issue3883RegressionTest() { assertThat(response.getStatus()).isEqualTo(200); assertThatJson(getPlainTextBody(response)).isEqualTo(""" { - "accessTeams": [], "name": "acme-app", "version": "1.0.0", "classifier": "APPLICATION", @@ -1395,8 +1413,7 @@ public void issue4048RegressionTest() { final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() .add("name", "project-%d-%d".formatted(i, j)) - .add("version", "%d.%d".formatted(i, j)) - .add("accessTeams",Json.createArrayBuilder().build()); + .add("version", "%d.%d".formatted(i, j)); if (parentUuid != null) { requestBodyBuilder.add("parent", Json.createObjectBuilder() .add("uuid", parentUuid.toString())); diff --git a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java index 0e463ba7df..2d55542cb2 100644 --- a/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/TeamResourceTest.java @@ -21,7 +21,6 @@ import alpine.common.util.UuidUtil; import alpine.model.ApiKey; import alpine.model.ConfigProperty; -import alpine.model.IConfigProperty; import alpine.model.ManagedUser; import alpine.model.Permission; import alpine.model.Team; @@ -55,7 +54,6 @@ import static org.hamcrest.CoreMatchers.equalTo; public class TeamResourceTest extends ResourceTest { - private ManagedUser testUser; private String jwt; private Team userNotPartof; @@ -65,8 +63,8 @@ public class TeamResourceTest extends ResourceTest { .register(ApiFilter.class) .register(AuthenticationFilter.class)); - public void getUserToken(boolean isAdmin) { - testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); + public void setUpUser(boolean isAdmin) { + ManagedUser testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); jwt = new JsonWebToken().createToken(testUser); qm.addUserToTeam(testUser, team); userNotPartof = qm.createTeam("UserNotPartof", false); @@ -231,67 +229,63 @@ public void deleteTeamWithAclTest() { } @Test - public void getVisibleAdminRequiredTeams() { - getUserToken(true); - qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + public void getVisibleAdminTeams() { + setUpUser(true); Response response = jersey.target(V1_TEAM + "/visible") .request() .header("Authorization", "Bearer " + jwt) .get(); Assert.assertEquals(200, response.getStatus(), 0); - JsonObject body = parseJsonObject(response); - Assert.assertTrue(body.getBoolean("required")); - JsonArray teams = body.getJsonArray("teams"); + JsonArray teams = parseJsonArray(response); Assert.assertEquals(2, teams.size()); Assert.assertEquals(this.team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); Assert.assertEquals(userNotPartof.getUuid().toString(), teams.get(1).asJsonObject().getString("uuid")); } @Test - public void getVisibleAdminNotRequiredTeams() { - getUserToken(true); + public void getVisibleNotAdminTeams() { + setUpUser(false); Response response = jersey.target(V1_TEAM + "/visible") .request() .header("Authorization", "Bearer " + jwt) .get(); Assert.assertEquals(200, response.getStatus(), 0); - JsonObject body = parseJsonObject(response); - Assert.assertFalse(body.getBoolean("required")); - JsonArray teams = body.getJsonArray("teams"); - Assert.assertEquals(2, teams.size()); + JsonArray teams = parseJsonArray(response); + Assert.assertEquals(1, teams.size()); Assert.assertEquals(this.team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); - Assert.assertEquals(userNotPartof.getUuid().toString(), teams.get(1).asJsonObject().getString("uuid")); } @Test - public void getVisibleNotAdminRequiredTeams() { - getUserToken(false); - qm.createConfigProperty("access-management", "acl.enabled", "true", IConfigProperty.PropertyType.BOOLEAN, ""); + public void getVisibleNotAdminApiKeyTeams() { Response response = jersey.target(V1_TEAM + "/visible") .request() - .header("Authorization", "Bearer " + jwt) + .header(X_API_KEY, apiKey) .get(); Assert.assertEquals(200, response.getStatus(), 0); - JsonObject body = parseJsonObject(response); - Assert.assertTrue(body.getBoolean("required")); - JsonArray teams = body.getJsonArray("teams"); + JsonArray teams = parseJsonArray(response); Assert.assertEquals(1, teams.size()); Assert.assertEquals(this.team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test - public void getVisibleNotAdminNotRequiredTeams() { - getUserToken(false); + public void getVisibleAdminApiKeyTeams() { + userNotPartof = qm.createTeam("UserNotPartof", false); + final var generator = new DefaultObjectGenerator(); + generator.loadDefaultPermissions(); + List permissionsList = new ArrayList(); + final Permission adminPermission = qm.getPermission("ACCESS_MANAGEMENT"); + permissionsList.add(adminPermission); + this.team.setPermissions(permissionsList); + Response response = jersey.target(V1_TEAM + "/visible") .request() - .header("Authorization", "Bearer " + jwt) + .header(X_API_KEY, apiKey) .get(); Assert.assertEquals(200, response.getStatus(), 0); - JsonObject body = parseJsonObject(response); - Assert.assertFalse(body.getBoolean("required")); - JsonArray teams = body.getJsonArray("teams"); - Assert.assertEquals(1, teams.size()); + JsonArray teams = parseJsonArray(response); + Assert.assertEquals(2, teams.size()); Assert.assertEquals(this.team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); + Assert.assertEquals(userNotPartof.getUuid().toString(), teams.get(1).asJsonObject().getString("uuid")); } @Test From 11b005d7d98aa46159e5cb0b850d471bc4c0f1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Schauer-K=C3=B6ckeis?= Date: Tue, 24 Sep 2024 16:54:03 +0200 Subject: [PATCH 13/13] Excluded accessTeams for json encoding and updated tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Thomas Schauer-Köckeis --- .../org/dependencytrack/model/Project.java | 4 +- .../resources/v1/ProjectResourceTest.java | 70 ++++++------------- 2 files changed, 25 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/dependencytrack/model/Project.java b/src/main/java/org/dependencytrack/model/Project.java index 5d10072ee4..01148e4e5d 100644 --- a/src/main/java/org/dependencytrack/model/Project.java +++ b/src/main/java/org/dependencytrack/model/Project.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonIncludeProperties; +import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; @@ -273,7 +274,6 @@ public enum FetchGroup { @Join(column = "PROJECT_ID") @Element(column = "TEAM_ID") @Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC")) - @JsonInclude(value = JsonInclude.Include.NON_EMPTY) private List accessTeams; @Persistent(defaultFetchGroup = "true") @@ -537,10 +537,12 @@ public void setVersions(List versions) { this.versions = versions; } + @JsonIgnore public List getAccessTeams() { return accessTeams; } + @JsonSetter public void setAccessTeams(List accessTeams) { this.accessTeams = accessTeams; } diff --git a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java index 039f4ec4fc..76b3fe5ac7 100644 --- a/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java +++ b/src/test/java/org/dependencytrack/resources/v1/ProjectResourceTest.java @@ -96,7 +96,7 @@ public void after() throws Exception { super.after(); } - public void setUpUser(boolean isAdmin, boolean isRequired) { + public JsonObjectBuilder setUpEnvironment(boolean isAdmin, boolean isRequired, String name, Team team1) { testUser = qm.createManagedUser("testuser", TEST_USER_PASSWORD_HASH); jwt = new JsonWebToken().createToken(testUser); qm.addUserToTeam(testUser, team); @@ -119,6 +119,13 @@ public void setUpUser(boolean isAdmin, boolean isRequired) { ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyType(), null); } + final JsonObjectBuilder jsonProject = Json.createObjectBuilder() + .add("name", name).add("classifier", "CONTAINER").addNull("parent").add("active", true).add("tags", Json.createArrayBuilder()); + if (team1 != null) { + final JsonObject jsonTeam = Json.createObjectBuilder().add("uuid", team1.getUuid().toString()).build(); + jsonProject.add("accessTeams", Json.createArrayBuilder().add(jsonTeam).build()); + } + return jsonProject; } @Test @@ -512,119 +519,86 @@ public void createProjectEmptyTest() { @Test public void createProjectWithExistingTeamRequiredTest() { - setUpUser(false, true); Team AllowedTeam = qm.createTeam("AllowedTeam", false); - Project project = new Project(); - project.setName("ProjectWithExistingTeamRequired"); + final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithExistingTeamRequired", AllowedTeam); qm.addUserToTeam(testUser, AllowedTeam); - final JsonObject jsonTeam = Json.createObjectBuilder().add("uuid", AllowedTeam.getUuid().toString()).build(); - final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() - .add("name", project.getName()).add("classifier", "CONTAINER").addNull("parent").add("active", true).add("tags", Json.createArrayBuilder()) - .add("accessTeams", Json.createArrayBuilder().add(jsonTeam).build()); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) .put(Entity.json(requestBodyBuilder.build().toString())); Assert.assertEquals(201, response.getStatus()); JsonObject returnedProject = parseJsonObject(response); - JsonArray teams = returnedProject.getJsonArray("accessTeams"); - Assert.assertEquals(teams.size(), 1); - Assert.assertEquals(AllowedTeam.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test public void createProjectWithoutExistingTeamRequiredTest() { - setUpUser(false, true); - Project project = new Project(); - project.setName("ProjectWithoutExistingTeamRequired"); + final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithoutExistingTeamRequired", null); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) - .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + .put(Entity.json(requestBodyBuilder.build().toString())); Assert.assertEquals(422, response.getStatus(), 0); } @Test public void createProjectWithNotAllowedExistingTeamTest() { - setUpUser(false, true); Team notAllowedTeam = qm.createTeam("NotAllowedTeam", false); - Project project = new Project(); - project.setName("ProjectWithNotAllowedExistingTeam"); - project.addAccessTeam(notAllowedTeam); + final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithNotAllowedExistingTeam", notAllowedTeam); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) - .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + .put(Entity.json(requestBodyBuilder.build().toString())); Assert.assertEquals(403, response.getStatus()); } @Test public void createProjectWithNotAllowedExistingTeamAdminTest() { - setUpUser(true, true); - Team notAllowedTeam = qm.createTeam("NotAllowedTeam", false); - Project project = new Project(); - project.setName("ProjectWithNotAllowedExistingTeam"); - project.addAccessTeam(notAllowedTeam); + Team AllowedTeam = qm.createTeam("NotAllowedTeam", false); + final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithNotAllowedExistingTeam", AllowedTeam); + qm.addUserToTeam(testUser, AllowedTeam); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) - .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + .put(Entity.json(requestBodyBuilder.build().toString())); Assert.assertEquals(201, response.getStatus()); JsonObject returnedProject = parseJsonObject(response); - JsonArray teams = returnedProject.getJsonArray("accessTeams"); - Assert.assertEquals(teams.size(), 1); - Assert.assertEquals(notAllowedTeam.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test public void createProjectWithNotExistingTeamNoAdminTest() { - setUpUser(false, true); Team notAllowedTeam = new Team(); notAllowedTeam.setUuid(new UUID(1, 1)); notAllowedTeam.setName("NotAllowedTeam"); - Project project = new Project(); - project.addAccessTeam(notAllowedTeam); - project.setName("ProjectWithNotAllowedExistingTeam"); + final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithNotAllowedExistingTeam", notAllowedTeam); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) - .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + .put(Entity.json(requestBodyBuilder.build().toString())); Assert.assertEquals(403, response.getStatus()); } @Test public void createProjectWithNotExistingTeamTest() { - setUpUser(true, false); Team notAllowedTeam = new Team(); notAllowedTeam.setUuid(new UUID(1, 1)); notAllowedTeam.setName("NotAllowedTeam"); - Project project = new Project(); - project.addAccessTeam(notAllowedTeam); - project.setName("ProjectWithNotExistingTeam"); + final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(true, true, "ProjectWithNotAllowedExistingTeam", notAllowedTeam); Response response = jersey.target(V1_PROJECT) .request() .header("Authorization", "Bearer " + jwt) - .put(Entity.entity(project, MediaType.APPLICATION_JSON)); + .put(Entity.json(requestBodyBuilder.build().toString())); Assert.assertEquals(404, response.getStatus()); } @Test public void createProjectWithApiKeyTest() { - Project project = new Project(); - project.setName("ProjectWithNotExistingTeam"); - final JsonObject jsonTeam = Json.createObjectBuilder().add("uuid", team.getUuid().toString()).build(); - final JsonObjectBuilder requestBodyBuilder = Json.createObjectBuilder() - .add("name", project.getName()).add("classifier", "CONTAINER").addNull("parent").add("active", true).add("tags", Json.createArrayBuilder()) - .add("accessTeams", Json.createArrayBuilder().add(jsonTeam).build()); + final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithNotAllowedExistingTeam", team); Response response = jersey.target(V1_PROJECT) .request() .header(X_API_KEY, apiKey) .put(Entity.json(requestBodyBuilder.build().toString())); Assert.assertEquals(201, response.getStatus()); JsonObject returnedProject = parseJsonObject(response); - JsonArray teams = returnedProject.getJsonArray("accessTeams"); - Assert.assertEquals(teams.size(), 1); - Assert.assertEquals(team.getUuid().toString(), teams.getFirst().asJsonObject().getString("uuid")); } @Test