Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/main/java/org/dependencytrack/resources/v1/BomResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@

import alpine.common.logging.Logger;
import alpine.event.framework.Event;
import alpine.model.ApiKey;
import alpine.model.ConfigProperty;
import alpine.model.Team;
import alpine.model.UserPrincipal;
import alpine.notification.Notification;
import alpine.notification.NotificationLevel;
import alpine.server.auth.PermissionRequired;
Expand Down Expand Up @@ -60,6 +63,8 @@
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.resources.v1.problems.InvalidBomProblemDetails;
import org.dependencytrack.resources.v1.problems.ProblemDetails;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dependencytrack.resources.v1.vo.BomSubmitRequest;
import org.dependencytrack.resources.v1.vo.BomUploadResponse;
import org.dependencytrack.resources.v1.vo.IsTokenBeingProcessedResponse;
Expand Down Expand Up @@ -92,13 +97,17 @@
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import jakarta.ws.rs.ClientErrorException;
import static java.util.Objects.requireNonNullElseGet;
import static java.util.function.Predicate.not;
import static org.dependencytrack.util.PersistenceUtil.isPersistent;
import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_MODE;
import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_EXCLUSIVE;
import static org.dependencytrack.model.ConfigPropertyConstants.BOM_VALIDATION_TAGS_INCLUSIVE;
Expand Down Expand Up @@ -382,6 +391,7 @@ public Response uploadBom(@Parameter(required = true) BomSubmitRequest request)
StringUtils.trimToNull(request.getProjectVersion()), request.getProjectTags(), parent,
null, true, request.isLatest(), true);
Principal principal = getPrincipal();
applyAccessTeamsToProject(qm, project, requireNonNullElseGet(request.getAccessTeams(), Collections::emptyList), principal);
qm.updateNewProjectACL(project, principal);
} else {
return Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build();
Expand Down Expand Up @@ -444,6 +454,7 @@ public Response uploadBom(
@FormDataParam("projectName") String projectName,
@FormDataParam("projectVersion") String projectVersion,
@FormDataParam("projectTags") String projectTags,
@FormDataParam("accessTeams") String accessTeamsJson,
@FormDataParam("parentName") String parentName,
@FormDataParam("parentVersion") String parentVersion,
@FormDataParam("parentUUID") String parentUUID,
Expand All @@ -453,6 +464,7 @@ public Response uploadBom(
final List<org.dependencytrack.model.Tag> requestTags = (projectTags != null && !projectTags.isBlank())
? Arrays.stream(projectTags.split(",")).map(String::trim).filter(not(String::isEmpty)).map(Tag::new).toList()
: null;
final List<Team> accessTeams = parseAccessTeams(accessTeamsJson);

if (projectUuid != null) { // behavior in v3.0.0
try (QueryManager qm = new QueryManager()) {
Expand Down Expand Up @@ -494,6 +506,7 @@ public Response uploadBom(
}
project = qm.createProject(trimmedProjectName, null, trimmedProjectVersion, requestTags, parent, null, true, isLatest, true);
Principal principal = getPrincipal();
applyAccessTeamsToProject(qm, project, accessTeams, principal);
qm.updateNewProjectACL(project, principal);
} else {
return Response.status(Response.Status.UNAUTHORIZED).entity("The principal does not have permission to create project.").build();
Expand Down Expand Up @@ -694,6 +707,69 @@ private static boolean shouldValidate(final Project project) {
}
}

private static List<Team> parseAccessTeams(final String accessTeamsJson) {
if (accessTeamsJson == null || accessTeamsJson.isBlank()) {
return Collections.emptyList();
}
try {
return new ObjectMapper().readValue(accessTeamsJson, new TypeReference<>() {});
} catch (Exception e) {
throw new ClientErrorException(Response.status(Response.Status.BAD_REQUEST)
.entity("accessTeams must be a valid JSON array of team objects with uuid or name")
.build());
}
}

private void applyAccessTeamsToProject(final QueryManager qm, final Project project,
final List<Team> chosenTeams, final Principal principal) {
if (chosenTeams.isEmpty()) {
return;
}
for (final Team chosenTeam : chosenTeams) {
if (chosenTeam.getUuid() == null && chosenTeam.getName() == null) {
throw new ClientErrorException(Response.status(Response.Status.BAD_REQUEST)
.entity("""
accessTeams must either specify a UUID or a name,\
but the team at index %d has neither.""".formatted(chosenTeams.indexOf(chosenTeam)))
.build());
}
}
final List<Team> userTeams;
if (principal instanceof final UserPrincipal userPrincipal) {
userTeams = userPrincipal.getTeams();
} else if (principal instanceof final ApiKey apiKey) {
userTeams = apiKey.getTeams();
} else {
userTeams = Collections.emptyList();
}
final boolean isAdmin = qm.hasAccessManagementPermission(principal);
final List<Team> visibleTeams = isAdmin ? qm.getTeams() : userTeams;
final var visibleTeamByUuid = new HashMap<UUID, Team>(visibleTeams.size());
final var visibleTeamByName = new HashMap<String, Team>(visibleTeams.size());
for (final Team visibleTeam : visibleTeams) {
visibleTeamByUuid.put(visibleTeam.getUuid(), visibleTeam);
visibleTeamByName.put(visibleTeam.getName(), visibleTeam);
}
for (final Team chosenTeam : chosenTeams) {
Team visibleTeam = visibleTeamByUuid.getOrDefault(
chosenTeam.getUuid(),
visibleTeamByName.get(chosenTeam.getName()));
if (visibleTeam == null) {
throw new ClientErrorException(Response.status(Response.Status.BAD_REQUEST)
.entity("""
The team with %s can not be assigned because it does not exist, \
or is not accessible to the authenticated principal.""".formatted(
chosenTeam.getUuid() != null ? "UUID " + chosenTeam.getUuid() : "name " + chosenTeam.getName()))
.build());
}
if (!isPersistent(visibleTeam)) {
visibleTeam = qm.getObjectById(Team.class, visibleTeam.getId());
}
project.addAccessTeam(visibleTeam);
}
qm.persist(project);
}

private void maybeBindTags(final QueryManager qm, final Project project, final List<Tag> tags) {
if (tags == null) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.swagger.v3.oas.annotations.media.Schema;
import alpine.model.Team;
import org.dependencytrack.model.Tag;

import jakarta.validation.constraints.NotBlank;
Expand Down Expand Up @@ -75,14 +76,16 @@ public final class BomSubmitRequest {

private final boolean isLatest;

private final List<Team> accessTeams;

public BomSubmitRequest(String project,
String projectName,
String projectVersion,
List<Tag> projectTags,
boolean autoCreate,
boolean isLatest,
String bom) {
this(project, projectName, projectVersion, projectTags, autoCreate, null, null, null, isLatest, bom);
this(project, projectName, projectVersion, projectTags, autoCreate, null, null, null, isLatest, null, bom);
}

@JsonCreator
Expand All @@ -96,6 +99,7 @@ public BomSubmitRequest(
@JsonProperty(value = "parentName") String parentName,
@JsonProperty(value = "parentVersion") String parentVersion,
@JsonProperty(value = "isLatest", defaultValue = "false") @JsonAlias("isLatestProjectVersion") boolean isLatest,
@JsonProperty(value = "accessTeams") List<Team> accessTeams,
@JsonProperty(value = "bom", required = true) String bom) {
this.project = project;
this.projectName = projectName;
Expand All @@ -106,6 +110,7 @@ public BomSubmitRequest(
this.parentName = parentName;
this.parentVersion = parentVersion;
this.isLatest = isLatest;
this.accessTeams = accessTeams;
this.bom = bom;
}

Expand Down Expand Up @@ -153,6 +158,14 @@ public boolean isAutoCreate() {
@JsonProperty("isLatest")
public boolean isLatest() { return isLatest; }

@Schema(description = """
Teams to grant access to when auto-creating a project. Either uuid or name of a team must be specified. \
Only teams which the authenticated principal is a member of can be assigned. \
Principals with ACCESS_MANAGEMENT permission can assign any team.""")
public List<Team> getAccessTeams() {
return accessTeams;
}

@Schema(
description = "Base64 encoded BOM",
required = true,
Expand Down
Loading
Loading