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 : Add REST endpoints for bulk tagging & un-tagging of projects #821

Merged
merged 2 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -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 @@
}
}

/**
* 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) {

Check warning on line 858 in src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/main/java/org/dependencytrack/persistence/ProjectQueryManager.java#L858

The method 'preprocessACLs(Query, String, Map, boolean)' has an NPath complexity of 792, current threshold is 200
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 @@
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