Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port : Enhance "Create Project" dialog to include team selection #948

Merged
merged 2 commits into from
Oct 9, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public enum ConfigPropertyConstants {
KENNA_SYNC_CADENCE("integrations", "kenna.sync.cadence", "60", PropertyType.INTEGER, "The cadence (in minutes) to upload to Kenna Security", ConfigPropertyAccessMode.READ_WRITE),
KENNA_TOKEN("integrations", "kenna.token", null, PropertyType.ENCRYPTEDSTRING, "The token to use when authenticating to Kenna Security", ConfigPropertyAccessMode.READ_WRITE),
KENNA_CONNECTOR_ID("integrations", "kenna.connector.id", null, PropertyType.STRING, "The Kenna Security connector identifier to upload to", ConfigPropertyAccessMode.READ_WRITE),
ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio", ConfigPropertyAccessMode.READ_WRITE),
ACCESS_MANAGEMENT_ACL_ENABLED("access-management", "acl.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable access control to projects in the portfolio", ConfigPropertyAccessMode.READ_WRITE, 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", ConfigPropertyAccessMode.READ_WRITE),
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", ConfigPropertyAccessMode.READ_WRITE),
TASK_SCHEDULER_LDAP_SYNC_CADENCE("task-scheduler", "ldap.sync.cadence", "6", PropertyType.INTEGER, "Sync cadence (in hours) for LDAP", ConfigPropertyAccessMode.READ_WRITE),
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/org/dependencytrack/model/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.annotation.JsonView;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
Expand All @@ -37,7 +38,6 @@
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;

import org.dependencytrack.persistence.converter.OrganizationalContactsJsonConverter;
import org.dependencytrack.persistence.converter.OrganizationalEntityJsonConverter;
import org.dependencytrack.resources.v1.serializers.CustomPackageURLSerializer;
Expand Down Expand Up @@ -297,7 +297,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<Team> accessTeams;

@Persistent(defaultFetchGroup = "true")
Expand Down Expand Up @@ -548,10 +547,12 @@ public void setVersions(List<ProjectVersion> versions) {
this.versions = versions;
}

@JsonIgnore
public List<Team> getAccessTeams() {
return accessTeams;
}

@JsonSetter
public void setAccessTeams(List<Team> accessTeams) {
this.accessTeams = accessTeams;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +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.persistence.PaginatedResult;
import alpine.server.auth.PermissionRequired;
import alpine.server.resources.AlpineResource;
Expand Down Expand Up @@ -72,6 +74,7 @@
import java.security.Principal;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand All @@ -80,7 +83,9 @@
import java.util.function.Function;

import static alpine.event.framework.Event.isEventBeingProcessed;
import static java.util.Objects.requireNonNullElseGet;
import static org.dependencytrack.persistence.jdbi.JdbiFactory.withJdbiHandle;
import static org.dependencytrack.util.PersistenceUtil.isPersistent;
import static org.dependencytrack.util.PersistenceUtil.isUniqueConstraintViolation;

/**
Expand Down Expand Up @@ -369,6 +374,13 @@
summary = "Creates a new project",
description = """
<p>If a parent project exists, <code>parent.uuid</code> is required</p>
<p>
When portfolio access control is enabled, one or more teams to grant access
to can be provided via <code>accessTeams</code>. Either <code>uuid</code> or
<code>name</code> of a team must be specified. Only teams which the authenticated
principal is a member of can be assigned. Principals with <strong>ACCESS_MANAGEMENT</strong>
permission can assign <em>any</em> team.
</p>
<p>Requires permission <strong>PORTFOLIO_MANAGEMENT</strong> or <strong>PORTFOLIO_MANAGEMENT_CREATE</strong></p>"""
)
@ApiResponses(value = {
Expand All @@ -377,15 +389,16 @@
description = "The created project",
content = @Content(schema = @Schema(implementation = Project.class))
),
@ApiResponse(responseCode = "400", description = "Bad Request"),
@ApiResponse(responseCode = "401", description = "Unauthorized"),
@ApiResponse(responseCode = "409", description = """
<ul>
<li>An inactive Parent cannot be selected as parent, or</li>
<li>A project with the specified name already exists</li>
</ul>"""),
</ul>""")
})
@PermissionRequired({Permissions.Constants.PORTFOLIO_MANAGEMENT, Permissions.Constants.PORTFOLIO_MANAGEMENT_CREATE})
public Response createProject(Project jsonProject) {
public Response createProject(final Project jsonProject) {

Check warning on line 401 in src/main/java/org/dependencytrack/resources/v1/ProjectResource.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/java/org/dependencytrack/resources/v1/ProjectResource.java#L401

The method 'createProject(Project)' has an NPath complexity of 8162, current threshold is 200
final Validator validator = super.getValidator();
failOnValidationError(
validator.validateProperty(jsonProject, "authors"),
Expand All @@ -397,7 +410,8 @@
validator.validateProperty(jsonProject, "classifier"),
validator.validateProperty(jsonProject, "cpe"),
validator.validateProperty(jsonProject, "purl"),
validator.validateProperty(jsonProject, "swidTagId")
validator.validateProperty(jsonProject, "swidTagId"),
validator.validateProperty(jsonProject, "accessTeams")
);
if (jsonProject.getClassifier() == null) {
jsonProject.setClassifier(Classifier.APPLICATION);
Expand All @@ -409,6 +423,67 @@
jsonProject.setParent(parent);
}

Principal principal = getPrincipal();

final List<Team> chosenTeams = requireNonNullElseGet(
jsonProject.getAccessTeams(), Collections::emptyList);
jsonProject.setAccessTeams(null);

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());
}
}

if (!chosenTeams.isEmpty()) {
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();
}

boolean isAdmin = qm.hasAccessManagementPermission(principal);
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 (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)) {
// Teams sourced from the principal will not be in persistent state
// and need to be attached to the persistence context.
visibleTeam = qm.getObjectById(Team.class, visibleTeam.getId());
}
jsonProject.addAccessTeam(visibleTeam);
}
}

final Project project;
try {
project = qm.createProject(jsonProject, jsonProject.getTags(), true);
Expand All @@ -429,8 +504,6 @@
LOGGER.error("Failed to create project %s".formatted(jsonProject), e);
throw new ServerErrorException(Response.Status.INTERNAL_SERVER_ERROR);
}

Principal principal = getPrincipal();
qm.updateNewProjectACL(project, principal);
return project;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ public static void assertNonPersistent(final Object object, final String message
}
}

private static boolean isPersistent(final Object object) {
public static boolean isPersistent(final Object object) {
final ObjectState objectState = JDOHelper.getObjectState(object);
return objectState == PERSISTENT_CLEAN
|| objectState == PERSISTENT_DIRTY
Expand Down
Loading
Loading