Skip to content

Commit

Permalink
Port : Add REST endpoints for bulk tagging & un-tagging of projects (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
sahibamittal authored Aug 2, 2024
1 parent b9d83dc commit 946b2a6
Show file tree
Hide file tree
Showing 8 changed files with 497 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
/**
* @since 4.11.0
*/
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE_USE})
@Constraint(validatedBy = {})
@Retention(RUNTIME)
@ReportAsSingleViolation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,19 @@
*/
package org.dependencytrack.persistence;

import alpine.model.ApiKey;
import alpine.model.IConfigProperty.PropertyType;
import alpine.model.Team;
import alpine.model.UserPrincipal;
import alpine.persistence.OrderDirection;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
import com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonValue;
import org.apache.commons.lang3.tuple.Pair;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.ComponentIdentity;
import org.dependencytrack.model.ComponentProperty;
import org.dependencytrack.model.ConfigPropertyConstants;
import org.dependencytrack.model.Project;
import org.dependencytrack.model.RepositoryMetaComponent;
import org.dependencytrack.model.RepositoryType;
Expand All @@ -42,9 +41,6 @@
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import jakarta.json.Json;
import jakarta.json.JsonArray;
import jakarta.json.JsonValue;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
Expand Down Expand Up @@ -880,48 +876,6 @@ public void reconcileComponents(Project project, List<Component> existingProject
}
}

/**
* A similar method exists in ProjectQueryManager
*/
private void preprocessACLs(final Query<Component> query, final String inputFilter, final Map<String, Object> params, final boolean bypass) {
if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && !bypass) {
final List<Team> teams;
if (super.principal instanceof UserPrincipal) {
final UserPrincipal userPrincipal = ((UserPrincipal) super.principal);
teams = userPrincipal.getTeams();
if (super.hasAccessManagementPermission(userPrincipal)) {
query.setFilter(inputFilter);
return;
}
} else {
final ApiKey apiKey = ((ApiKey) super.principal);
teams = apiKey.getTeams();
if (super.hasAccessManagementPermission(apiKey)) {
query.setFilter(inputFilter);
return;
}
}
if (teams != null && teams.size() > 0) {
final StringBuilder sb = new StringBuilder();
for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) {
final Team team = super.getObjectById(Team.class, teams.get(i).getId());
sb.append(" project.accessTeams.contains(:team").append(i).append(") ");
params.put("team" + i, team);
if (i < teamsSize - 1) {
sb.append(" || ");
}
}
if (inputFilter != null) {
query.setFilter(inputFilter + " && (" + sb.toString() + ")");
} else {
query.setFilter(sb.toString());
}
}
} else {
query.setFilter(inputFilter);
}
}

public Map<String, Component> getDependencyGraphForComponents(Project project, List<Component> components) {
Map<String, Component> dependencyGraph = new HashMap<>();
if (project.getDirectDependencies() == null || project.getDirectDependencies().isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.github.packageurl.PackageURL;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.datanucleus.api.jdo.JDOQuery;
import org.dependencytrack.auth.Permissions;
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
import org.dependencytrack.model.Analysis;
Expand All @@ -53,6 +54,8 @@
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;
import javax.jdo.metadata.MemberMetadata;
import javax.jdo.metadata.TypeMetadata;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Date;
Expand Down Expand Up @@ -851,10 +854,35 @@ public boolean hasAccess(final Principal principal, final Project project) {
}
}

/**
* A similar method exists in ComponentQueryManager
*/
private void preprocessACLs(final Query<Project> query, final String inputFilter, final Map<String, Object> params, final boolean bypass) {
@Override
void preprocessACLs(final Query<?> query, final String inputFilter, final Map<String, Object> params, final boolean bypass) {
String projectMemberFieldName = null;
final org.datanucleus.store.query.Query<?> internalQuery = ((JDOQuery<?>)query).getInternalQuery();
if (!Project.class.equals(internalQuery.getCandidateClass())) {
// NB: The query does not directly target Project, but if it has a relationship
// with Project we can still make the ACL check work. If the query candidate
// has EXACTLY one persistent field of type Project, we'll use that.
// If there are more than one, or none at all, we fail to avoid unintentional behavior.
final TypeMetadata candidateTypeMetadata = pm.getPersistenceManagerFactory().getMetadata(internalQuery.getCandidateClassName());

for (final MemberMetadata memberMetadata : candidateTypeMetadata.getMembers()) {
if (!Project.class.getName().equals(memberMetadata.getFieldType())) {
continue;
}

if (projectMemberFieldName != null) {
throw new IllegalArgumentException("Query candidate class %s has multiple members of type %s"
.formatted(internalQuery.getCandidateClassName(), Project.class.getName()));
}

projectMemberFieldName = memberMetadata.getName();
}

if (projectMemberFieldName == null) {
throw new IllegalArgumentException("Query candidate class %s has no member of type %s"
.formatted(internalQuery.getCandidateClassName(), Project.class.getName()));
}
}
if (super.principal != null && isEnabled(ConfigPropertyConstants.ACCESS_MANAGEMENT_ACL_ENABLED) && !bypass) {
final List<Team> teams;
if (super.principal instanceof UserPrincipal userPrincipal) {
Expand All @@ -871,18 +899,22 @@ private void preprocessACLs(final Query<Project> query, final String inputFilter
return;
}
}
if (teams != null && teams.size() > 0) {
if (teams != null && !teams.isEmpty()) {
final StringBuilder sb = new StringBuilder();
for (int i = 0, teamsSize = teams.size(); i < teamsSize; i++) {
final Team team = super.getObjectById(Team.class, teams.get(i).getId());
sb.append(" ");
if (projectMemberFieldName != null) {
sb.append(projectMemberFieldName).append(".");
}
sb.append(" accessTeams.contains(:team").append(i).append(") ");
params.put("team" + i, team);
if (i < teamsSize - 1) {
sb.append(" || ");
}
}
if (inputFilter != null && !inputFilter.isBlank()) {
query.setFilter(inputFilter + " && (" + sb.toString() + ")");
query.setFilter(inputFilter + " && (" + sb + ")");
} else {
query.setFilter(sb.toString());
}
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/dependencytrack/persistence/QueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,10 @@ public boolean hasAccess(final Principal principal, final Project project) {
return getProjectQueryManager().hasAccess(principal, project);
}

void preprocessACLs(final Query<?> query, final String inputFilter, final Map<String, Object> params, final boolean bypass) {
getProjectQueryManager().preprocessACLs(query, inputFilter, params, bypass);
}

public PaginatedResult getProjects(final Tag tag, final boolean includeMetrics, final boolean excludeInactive, final boolean onlyRoot) {
return getProjectQueryManager().getProjects(tag, includeMetrics, excludeInactive, onlyRoot);
}
Expand Down Expand Up @@ -1403,6 +1407,14 @@ public List<TagQueryManager.TaggedProjectRow> getTaggedProjects(final String tag
return getTagQueryManager().getTaggedProjects(tagName);
}

public void tagProjects(final String tagName, final Collection<String> projectUuids) {
getTagQueryManager().tagProjects(tagName, projectUuids);
}

public void untagProjects(final String tagName, final Collection<String> projectUuids) {
getTagQueryManager().untagProjects(tagName, projectUuids);
}

public List<TagQueryManager.TaggedPolicyRow> getTaggedPolicies(final String tagName) {
return getTagQueryManager().getTaggedPolicies(tagName);
}
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/org/dependencytrack/persistence/TagQueryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.stream.Stream;

public class TagQueryManager extends QueryManager implements IQueryManager {
Expand Down Expand Up @@ -192,6 +194,63 @@ public List<TaggedProjectRow> getTaggedProjects(final String tagName) {
}
}

/**
* @since 4.12.0
*/
@Override
public void tagProjects(final String tagName, final Collection<String> projectUuids) {
runInTransaction(() -> {
final Tag tag = getTagByName(tagName);
if (tag == null) {
throw new NoSuchElementException("A tag with name %s does not exist".formatted(tagName));
}

final Query<Project> projectsQuery = pm.newQuery(Project.class);
final var params = new HashMap<String, Object>(Map.of("uuids", projectUuids));
preprocessACLs(projectsQuery, ":uuids.contains(uuid)", params, /* bypass */ false);
projectsQuery.setNamedParameters(params);
final List<Project> projects = executeAndCloseList(projectsQuery);

for (final Project project : projects) {
if (project.getTags() == null || project.getTags().isEmpty()) {
project.setTags(List.of(tag));
continue;
}

if (!project.getTags().contains(tag)) {
project.getTags().add(tag);
}
}
});
}

/**
* @since 4.12.0
*/
@Override
public void untagProjects(final String tagName, final Collection<String> projectUuids) {
runInTransaction(() -> {
final Tag tag = getTagByName(tagName);
if (tag == null) {
throw new NoSuchElementException("A tag with name %s does not exist".formatted(tagName));
}

final Query<Project> projectsQuery = pm.newQuery(Project.class);
final var params = new HashMap<String, Object>(Map.of("uuids", projectUuids));
preprocessACLs(projectsQuery, ":uuids.contains(uuid)", params, /* bypass */ false);
projectsQuery.setNamedParameters(params);
final List<Project> projects = executeAndCloseList(projectsQuery);

for (final Project project : projects) {
if (project.getTags() == null || project.getTags().isEmpty()) {
continue;
}

project.getTags().remove(tag);
}
});
}

/**
* @since 4.12.0
*/
Expand Down
Loading

0 comments on commit 946b2a6

Please sign in to comment.