Skip to content

Commit

Permalink
Merge pull request DependencyTrack#4093 from Gepardgame/feat/add-team…
Browse files Browse the repository at this point in the history
…-selection-in-create-proect-button

Feat/add team selection in create project button
  • Loading branch information
nscuro authored Sep 24, 2024
2 parents 6e6eab1 + d07e96c commit 8c2d933
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
4 changes: 3 additions & 1 deletion 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.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
Expand Down Expand Up @@ -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"))
@JsonIgnore
private List<Team> accessTeams;

@Persistent(defaultFetchGroup = "true")
Expand Down Expand Up @@ -537,10 +537,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 All @@ -39,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;
Expand All @@ -62,9 +65,11 @@
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;

Expand Down Expand Up @@ -279,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 = """
<ul>
<li>An inactive Parent cannot be selected as parent, or</li>
<li>A project with the specified name already exists</li>
</ul>"""),
@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) {
Expand All @@ -299,7 +306,8 @@ 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, "accessTeams")
);
if (jsonProject.getClassifier() == null) {
jsonProject.setClassifier(Classifier.APPLICATION);
Expand All @@ -309,15 +317,47 @@ public Response createProject(Project jsonProject) {
Project parent = qm.getObjectByUuid(Project.class, jsonProject.getParent().getUuid());
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 List<Team> chosenTeams = jsonProject.getAccessTeams() == null ? new ArrayList<Team>()
: 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();
}
Principal principal = getPrincipal();
if (!chosenTeams.isEmpty()) {
List<Team> userTeams = new ArrayList<Team>();
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<Team> visibleTeams = isAdmin ? qm.getTeams() : userTeams;
List<UUID> visibleUuids = visibleTeams.isEmpty() ? new ArrayList<UUID>()
: visibleTeams.stream().map(Team::getUuid).toList();
jsonProject.setAccessTeams(new ArrayList<Team>());
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);
} 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();
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/org/dependencytrack/resources/v1/TeamResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import alpine.common.logging.Logger;
import alpine.model.ApiKey;
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;
Expand Down Expand Up @@ -52,6 +53,8 @@
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;

import static org.datanucleus.PropertyNames.PROPERTY_RETAIN_VALUES;
Expand Down Expand Up @@ -220,6 +223,33 @@ public Response deleteTeam(Team jsonTeam) {
}
}

@GET
@Path("/visible")
@Produces(MediaType.APPLICATION_JSON)
@Operation(summary = "Returns a list of Teams that are visible", description = "<p></p>")
@ApiResponses(value = {
@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();
boolean isAllTeams = qm.hasAccessManagementPermission(user);
List<Team> teams = new ArrayList<Team>();
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(teams).build();
}
}

@PUT
@Path("/{uuid}/key")
@Produces(MediaType.APPLICATION_JSON)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
import alpine.common.util.UuidUtil;
import alpine.event.framework.EventService;
import alpine.model.IConfigProperty.PropertyType;
import alpine.model.ManagedUser;
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;
Expand Down Expand Up @@ -50,6 +54,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;
Expand All @@ -75,6 +80,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(
Expand All @@ -89,6 +96,38 @@ public void after() throws Exception {
super.after();
}

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);
final var generator = new DefaultObjectGenerator();
generator.loadDefaultPermissions();
List<Permission> permissionsList = new ArrayList<Permission>();
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);
}
if (isRequired) {
qm.createConfigProperty(
ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getGroupName(),
ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED.getPropertyName(),
"true",
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
public void getProjectsDefaultRequestTest() {
for (int i=0; i<1000; i++) {
Expand Down Expand Up @@ -478,6 +517,90 @@ public void createProjectEmptyTest() {
Assert.assertEquals(400, response.getStatus(), 0);
}

@Test
public void createProjectWithExistingTeamRequiredTest() {
Team AllowedTeam = qm.createTeam("AllowedTeam", false);
final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithExistingTeamRequired", AllowedTeam);
qm.addUserToTeam(testUser, AllowedTeam);
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);
}

@Test
public void createProjectWithoutExistingTeamRequiredTest() {
final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithoutExistingTeamRequired", null);
Response response = jersey.target(V1_PROJECT)
.request()
.header("Authorization", "Bearer " + jwt)
.put(Entity.json(requestBodyBuilder.build().toString()));
Assert.assertEquals(422, response.getStatus(), 0);
}

@Test
public void createProjectWithNotAllowedExistingTeamTest() {
Team notAllowedTeam = qm.createTeam("NotAllowedTeam", false);
final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithNotAllowedExistingTeam", notAllowedTeam);
Response response = jersey.target(V1_PROJECT)
.request()
.header("Authorization", "Bearer " + jwt)
.put(Entity.json(requestBodyBuilder.build().toString()));
Assert.assertEquals(403, response.getStatus());
}

@Test
public void createProjectWithNotAllowedExistingTeamAdminTest() {
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.json(requestBodyBuilder.build().toString()));
Assert.assertEquals(201, response.getStatus());
JsonObject returnedProject = parseJsonObject(response);
}

@Test
public void createProjectWithNotExistingTeamNoAdminTest() {
Team notAllowedTeam = new Team();
notAllowedTeam.setUuid(new UUID(1, 1));
notAllowedTeam.setName("NotAllowedTeam");
final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(false, true, "ProjectWithNotAllowedExistingTeam", notAllowedTeam);
Response response = jersey.target(V1_PROJECT)
.request()
.header("Authorization", "Bearer " + jwt)
.put(Entity.json(requestBodyBuilder.build().toString()));
Assert.assertEquals(403, response.getStatus());
}

@Test
public void createProjectWithNotExistingTeamTest() {
Team notAllowedTeam = new Team();
notAllowedTeam.setUuid(new UUID(1, 1));
notAllowedTeam.setName("NotAllowedTeam");
final JsonObjectBuilder requestBodyBuilder = setUpEnvironment(true, true, "ProjectWithNotAllowedExistingTeam", notAllowedTeam);
Response response = jersey.target(V1_PROJECT)
.request()
.header("Authorization", "Bearer " + jwt)
.put(Entity.json(requestBodyBuilder.build().toString()));
Assert.assertEquals(404, response.getStatus());
}

@Test
public void createProjectWithApiKeyTest() {
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);
}

@Test
public void updateProjectTest() {
Project project = qm.createProject("ABC", null, "1.0", null, null, null, true, false);
Expand Down
Loading

0 comments on commit 8c2d933

Please sign in to comment.