Skip to content

Commit

Permalink
Merge pull request #2008 from rbt-mm/master-allow-selection-of-teams-…
Browse files Browse the repository at this point in the history
…as-recipients-for-alert-rules

Allow selection of teams as recipients for alert rules
  • Loading branch information
nscuro authored Oct 12, 2022
2 parents b116a0b + c802dcf commit 51e24e3
Show file tree
Hide file tree
Showing 9 changed files with 712 additions and 15 deletions.
15 changes: 15 additions & 0 deletions src/main/java/org/dependencytrack/model/NotificationRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.dependencytrack.model;

import alpine.common.validation.RegexSequence;
import alpine.model.Team;
import alpine.notification.NotificationLevel;
import alpine.server.json.TrimmedStringDeserializer;
import com.fasterxml.jackson.annotation.JsonIgnore;
Expand Down Expand Up @@ -99,6 +100,12 @@ public class NotificationRule implements Serializable {
@Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC, version ASC"))
private List<Project> projects;

@Persistent(table = "NOTIFICATIONRULE_TEAMS", defaultFetchGroup = "true")
@Join(column = "NOTIFICATIONRULE_ID")
@Element(column = "TEAM_ID")
@Order(extensions = @Extension(vendorName = "datanucleus", key = "list-ordering", value = "name ASC"))
private List<Team> teams;

@Persistent
@Column(name = "NOTIFY_ON", length = 1024)
private String notifyOn;
Expand Down Expand Up @@ -175,6 +182,14 @@ public void setProjects(List<Project> projects) {
this.projects = projects;
}

public List<Team> getTeams() {
return teams;
}

public void setTeams(List<Team> teams) {
this.teams = teams;
}

public String getMessage() {
return message;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.dependencytrack.model.NotificationRule;
import org.dependencytrack.model.Project;
import org.dependencytrack.notification.publisher.Publisher;
import org.dependencytrack.notification.publisher.SendMailPublisher;
import org.dependencytrack.notification.vo.AnalysisDecisionChange;
import org.dependencytrack.notification.vo.BomConsumedOrProcessed;
import org.dependencytrack.notification.vo.NewVulnerabilityIdentified;
Expand Down Expand Up @@ -75,7 +76,13 @@ public void inform(final Notification notification) {
.add(Publisher.CONFIG_TEMPLATE_KEY, notificationPublisher.getTemplate())
.addAll(Json.createObjectBuilder(config))
.build();
publisher.inform(restrictNotificationToRuleProjects(notification, rule), notificationPublisherConfig);
if (publisherClass != SendMailPublisher.class || rule.getTeams().isEmpty() || rule.getTeams() == null){
publisher.inform(restrictNotificationToRuleProjects(notification, rule), notificationPublisherConfig);
} else {
((SendMailPublisher)publisher).inform(restrictNotificationToRuleProjects(notification, rule), notificationPublisherConfig, rule.getTeams());
}


} else {
LOGGER.error("The defined notification publisher is not assignable from " + Publisher.class.getCanonicalName());
}
Expand Down Expand Up @@ -145,7 +152,7 @@ List<NotificationRule> resolveRules(final Notification notification) {
sb.append("enabled == true && scope == :scope"); //todo: improve this - this only works for testing
query.setFilter(sb.toString());
final List<NotificationRule> result = (List<NotificationRule>)query.execute(NotificationScope.valueOf(notification.getScope()));

pm.detachCopyAll(result);

if (NotificationScope.PORTFOLIO.name().equals(notification.getScope())
&& notification.getSubject() != null && notification.getSubject() instanceof NewVulnerabilityIdentified) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
import alpine.common.logging.Logger;
import alpine.common.util.BooleanUtil;
import alpine.model.ConfigProperty;
import alpine.model.Team;
import alpine.model.ManagedUser;
import alpine.model.LdapUser;
import alpine.model.OidcUser;
import alpine.notification.Notification;
import alpine.security.crypto.DataEncryption;
import alpine.server.mail.SendMail;
Expand All @@ -33,8 +37,13 @@
import org.dependencytrack.persistence.QueryManager;

import javax.json.JsonObject;
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.function.Predicate;

import static org.dependencytrack.model.ConfigPropertyConstants.*;

Expand All @@ -49,6 +58,19 @@ public void inform(final Notification notification, final JsonObject config) {
return;
}
final String[] destinations = parseDestination(config);
sendNotification(notification, config, destinations);
}

public void inform(final Notification notification, final JsonObject config, List<Team> teams) {
if (config == null) {
LOGGER.warn("No configuration found. Skipping notification.");
return;
}
final String[] destinations = parseDestination(config, teams);
sendNotification(notification, config, destinations);
}

private void sendNotification(Notification notification, JsonObject config, String[] destinations) {
PebbleTemplate template = getTemplate(config);
String mimeType = getTemplateMimeType(config);
final String content = prepareTemplate(notification, template);
Expand Down Expand Up @@ -104,4 +126,19 @@ static String[] parseDestination(final JsonObject config) {
return destinationString.split(",");
}

static String[] parseDestination(final JsonObject config, final List<Team> teams) {
String[] destination = teams.stream().flatMap(
team -> Stream.of(
Arrays.stream(config.getString("destination").split(",")).filter(Predicate.not(String::isEmpty)),
Optional.ofNullable(team.getManagedUsers()).orElseGet(Collections::emptyList).stream().map(ManagedUser::getEmail).filter(Objects::nonNull),
Optional.ofNullable(team.getLdapUsers()).orElseGet(Collections::emptyList).stream().map(LdapUser::getEmail).filter(Objects::nonNull),
Optional.ofNullable(team.getOidcUsers()).orElseGet(Collections::emptyList).stream().map(OidcUser::getEmail).filter(Objects::nonNull)
)
.reduce(Stream::concat)
.orElseGet(Stream::empty)
)
.distinct()
.toArray(String[]::new);
return destination.length == 0 ? null : destination;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package org.dependencytrack.persistence;

import alpine.model.Team;
import alpine.notification.NotificationLevel;
import alpine.persistence.PaginatedResult;
import alpine.resources.AlpineRequest;
Expand Down Expand Up @@ -204,6 +205,18 @@ public void removeProjectFromNotificationRules(final Project project) {
}
}

/**
* Removes teams from NotificationRules
*/
@SuppressWarnings("unchecked")
public void removeTeamFromNotificationRules(final Team team) {
final Query<NotificationRule> query = pm.newQuery(NotificationRule.class, "teams.contains(:team)");
for (final NotificationRule rule: (List<NotificationRule>) query.execute(team)) {
rule.getTeams().remove(team);
persist(rule);
}
}

/**
* Delete a notification publisher and associated rules.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,10 @@ public void removeProjectFromNotificationRules(final Project project) {
getNotificationQueryManager().removeProjectFromNotificationRules(project);
}

public void removeTeamFromNotificationRules(final Team team) {
getNotificationQueryManager().removeTeamFromNotificationRules(team);
}

/**
* Determines if a config property is enabled or not.
* @param configPropertyConstants the property to query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.dependencytrack.resources.v1;

import alpine.common.logging.Logger;
import alpine.model.Team;
import alpine.persistence.PaginatedResult;
import alpine.server.auth.PermissionRequired;
import alpine.server.resources.AlpineResource;
Expand All @@ -35,6 +36,7 @@
import org.dependencytrack.model.NotificationRule;
import org.dependencytrack.model.Project;
import org.dependencytrack.notification.NotificationScope;
import org.dependencytrack.notification.publisher.DefaultNotificationPublishers;
import org.dependencytrack.persistence.QueryManager;

import javax.validation.Validator;
Expand Down Expand Up @@ -255,4 +257,86 @@ public Response removeProjectFromRule(
return Response.status(Response.Status.NOT_MODIFIED).build();
}
}

@POST
@Path("/{ruleUuid}/team/{teamUuid}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(
value = "Adds a team to a notification rule",
response = NotificationRule.class
)
@ApiResponses(value = {
@ApiResponse(code = 304, message = "The rule already has the specified team assigned"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 404, message = "The notification rule or team could not be found")
})
@PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION)
public Response addTeamToRule(
@ApiParam(value = "The UUID of the rule to add a team to", required = true)
@PathParam("ruleUuid") String ruleUuid,
@ApiParam(value = "The UUID of the team to add to the rule", required = true)
@PathParam("teamUuid") String teamUuid) {
try (QueryManager qm = new QueryManager()) {
final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid);
if (rule == null) {
return Response.status(Response.Status.NOT_FOUND).entity("The notification rule could not be found.").build();
}
if (!rule.getPublisher().getName().equals(DefaultNotificationPublishers.EMAIL.getPublisherName())) {
return Response.status(Response.Status.NOT_ACCEPTABLE).entity("Team subscriptions are only possible on notification rules with EMAIL publisher.").build();
}
final Team team = qm.getObjectByUuid(Team.class, teamUuid);
if (team == null) {
return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build();
}
final List<Team> teams = rule.getTeams();
if (teams != null && !teams.contains(team)) {
rule.getTeams().add(team);
qm.persist(rule);
return Response.ok(rule).build();
}
return Response.status(Response.Status.NOT_MODIFIED).build();
}
}

@DELETE
@Path("/{ruleUuid}/team/{teamUuid}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation(
value = "Removes a team from a notification rule",
response = NotificationRule.class
)
@ApiResponses(value = {
@ApiResponse(code = 304, message = "The rule does not have the specified team assigned"),
@ApiResponse(code = 401, message = "Unauthorized"),
@ApiResponse(code = 404, message = "The notification rule or team could not be found")
})
@PermissionRequired(Permissions.Constants.SYSTEM_CONFIGURATION)
public Response removeTeamFromRule(
@ApiParam(value = "The UUID of the rule to remove the project from", required = true)
@PathParam("ruleUuid") String ruleUuid,
@ApiParam(value = "The UUID of the project to remove from the rule", required = true)
@PathParam("teamUuid") String teamUuid) {
try (QueryManager qm = new QueryManager()) {
final NotificationRule rule = qm.getObjectByUuid(NotificationRule.class, ruleUuid);
if (rule == null) {
return Response.status(Response.Status.NOT_FOUND).entity("The notification rule could not be found.").build();
}
if (!rule.getPublisher().getName().equals(DefaultNotificationPublishers.EMAIL.getPublisherName())) {
return Response.status(Response.Status.NOT_ACCEPTABLE).entity("Team subscriptions are only possible on notification rules with EMAIL publisher.").build();
}
final Team team = qm.getObjectByUuid(Team.class, teamUuid);
if (team == null) {
return Response.status(Response.Status.NOT_FOUND).entity("The team could not be found.").build();
}
final List<Team> teams = rule.getTeams();
if (teams != null && teams.contains(team)) {
rule.getTeams().remove(team);
qm.persist(rule);
return Response.ok(rule).build();
}
return Response.status(Response.Status.NOT_MODIFIED).build();
}
}
}
Loading

0 comments on commit 51e24e3

Please sign in to comment.