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

Support assigning of teams for portfolio ACL when creating a project #4093

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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 @@ -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
Gepardgame marked this conversation as resolved.
Show resolved Hide resolved
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
Gepardgame marked this conversation as resolved.
Show resolved Hide resolved
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
Gepardgame marked this conversation as resolved.
Show resolved Hide resolved
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
Loading