org.graylog.events.rest.EventDefinitionsResource Maven / Gradle / Ivy
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* .
*/
package org.graylog.events.rest;
import com.codahale.metrics.annotation.Timed;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.commons.lang.StringUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.graylog.events.audit.EventsAuditEventTypes;
import org.graylog.events.context.EventDefinitionContextService;
import org.graylog.events.processor.DBEventDefinitionService;
import org.graylog.events.processor.EventDefinition;
import org.graylog.events.processor.EventDefinitionConfiguration;
import org.graylog.events.processor.EventDefinitionDto;
import org.graylog.events.processor.EventDefinitionHandler;
import org.graylog.events.processor.EventProcessorConfig;
import org.graylog.events.processor.EventProcessorEngine;
import org.graylog.events.processor.EventProcessorException;
import org.graylog.events.processor.EventProcessorParameters;
import org.graylog.events.processor.EventProcessorParametersWithTimerange;
import org.graylog.events.processor.EventResolver;
import org.graylog.grn.GRNTypes;
import org.graylog.plugins.views.startpage.recentActivities.RecentActivityService;
import org.graylog.security.UserContext;
import org.graylog2.audit.AuditEventSender;
import org.graylog2.audit.jersey.AuditEvent;
import org.graylog2.audit.jersey.NoAuditEvent;
import org.graylog2.database.PaginatedList;
import org.graylog2.plugin.rest.PluginRestResource;
import org.graylog2.plugin.rest.ValidationFailureException;
import org.graylog2.plugin.rest.ValidationResult;
import org.graylog2.rest.bulk.AuditParams;
import org.graylog2.rest.bulk.BulkExecutor;
import org.graylog2.rest.bulk.SequentialBulkExecutor;
import org.graylog2.rest.bulk.model.BulkOperationRequest;
import org.graylog2.rest.bulk.model.BulkOperationResponse;
import org.graylog2.rest.models.PaginatedResponse;
import org.graylog2.rest.models.tools.responses.PageListResponse;
import org.graylog2.rest.resources.entities.EntityAttribute;
import org.graylog2.rest.resources.entities.EntityDefaults;
import org.graylog2.rest.resources.entities.Sorting;
import org.graylog2.search.SearchQuery;
import org.graylog2.search.SearchQueryField;
import org.graylog2.search.SearchQueryParser;
import org.graylog2.shared.rest.resources.RestResource;
import org.graylog2.shared.security.RestPermissions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.InternalServerErrorException;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static org.graylog2.shared.rest.documentation.generator.Generator.CLOUD_VISIBLE;
import static org.graylog2.shared.utilities.StringUtils.f;
@Api(value = "Events/Definitions", description = "Event definition management", tags = {CLOUD_VISIBLE})
@Path("/events/definitions")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@RequiresAuthentication
public class EventDefinitionsResource extends RestResource implements PluginRestResource {
private static final Logger LOG = LoggerFactory.getLogger(EventDefinitionsResource.class);
private static final ImmutableMap SEARCH_FIELD_MAPPING = ImmutableMap.builder()
.put("id", SearchQueryField.create("_id", SearchQueryField.Type.OBJECT_ID))
.put("title", SearchQueryField.create(EventDefinitionDto.FIELD_TITLE))
.put("description", SearchQueryField.create(EventDefinitionDto.FIELD_DESCRIPTION))
.build();
private static final String DEFAULT_SORT_FIELD = "title";
private static final String DEFAULT_SORT_DIRECTION = "asc";
private static final List attributes = List.of(
EntityAttribute.builder().id("title").title("Title").build(),
EntityAttribute.builder().id("description").title("Description").build(),
EntityAttribute.builder().id("priority").title("Priority").type(SearchQueryField.Type.INT).build(),
EntityAttribute.builder().id("status").title("Status").type(SearchQueryField.Type.BOOLEAN).sortable(false).build()
);
private static final EntityDefaults settings = EntityDefaults.builder()
.sort(Sorting.create(DEFAULT_SORT_FIELD, Sorting.Direction.valueOf(DEFAULT_SORT_DIRECTION.toUpperCase(Locale.ROOT))))
.build();
private final DBEventDefinitionService dbService;
private final EventDefinitionHandler eventDefinitionHandler;
private final EventDefinitionContextService contextService;
private final EventProcessorEngine engine;
private final EventDefinitionConfiguration eventDefinitionConfiguration;
private final SearchQueryParser searchQueryParser;
private final RecentActivityService recentActivityService;
private final BulkExecutor bulkDeletionExecutor;
private final BulkExecutor bulkScheduleExecutor;
private final BulkExecutor bulkUnscheduleExecutor;
private final EventResolver eventResolver;
@Inject
public EventDefinitionsResource(DBEventDefinitionService dbService,
EventDefinitionHandler eventDefinitionHandler,
EventDefinitionContextService contextService,
EventProcessorEngine engine,
RecentActivityService recentActivityService,
AuditEventSender auditEventSender,
ObjectMapper objectMapper,
EventResolver eventResolver,
EventDefinitionConfiguration eventDefinitionConfiguration
) {
this.dbService = dbService;
this.eventDefinitionHandler = eventDefinitionHandler;
this.contextService = contextService;
this.engine = engine;
this.eventDefinitionConfiguration = eventDefinitionConfiguration;
this.searchQueryParser = new SearchQueryParser(EventDefinitionDto.FIELD_TITLE, SEARCH_FIELD_MAPPING);
this.recentActivityService = recentActivityService;
this.bulkDeletionExecutor = new SequentialBulkExecutor<>(this::delete, auditEventSender, objectMapper);
this.bulkScheduleExecutor = new SequentialBulkExecutor<>(this::schedule, auditEventSender, objectMapper);
this.bulkUnscheduleExecutor = new SequentialBulkExecutor<>(this::unschedule, auditEventSender, objectMapper);
this.eventResolver = eventResolver;
}
@GET
@Timed
@Path("/paginated")
@ApiOperation(value = "Get a paginated list of event definitions")
@Produces(MediaType.APPLICATION_JSON)
public PageListResponse getPage(@ApiParam(name = "page") @QueryParam("page") @DefaultValue("1") int page,
@ApiParam(name = "per_page") @QueryParam("per_page") @DefaultValue("50") int perPage,
@ApiParam(name = "query") @QueryParam("query") @DefaultValue("") String query,
@ApiParam(name = "sort",
value = "The field to sort the result on",
required = true,
allowableValues = "title,description,priority,status")
@DefaultValue(DEFAULT_SORT_FIELD) @QueryParam("sort") String sort,
@ApiParam(name = "order", value = "The sort direction", allowableValues = "asc, desc")
@DefaultValue(DEFAULT_SORT_DIRECTION) @QueryParam("order") String order) {
SearchQuery searchQuery;
try {
searchQuery = searchQueryParser.parse(query);
} catch (IllegalArgumentException e) {
throw new BadRequestException("Invalid argument in search query: " + e.getMessage());
}
if ("status".equals(sort)) {
sort = "alert";
}
final PaginatedList result = dbService.searchPaginated(searchQuery, event -> {
return isPermitted(RestPermissions.EVENT_DEFINITIONS_READ, event.id());
}, sort, order, page, perPage);
PaginatedList definitionDtos = new PaginatedList<>(
result.delegate(), result.pagination().total(), result.pagination().page(), result.pagination().perPage()
);
final ImmutableMap context = contextService.contextFor(result.delegate());
ImmutableMap schedulerCtx =
(ImmutableMap) context.get(EventDefinitionContextService.SCHEDULER_KEY);
final List eventDefinitionDtos =
result.delegate()
.stream()
.map(eventDefinition -> eventDefinition.toBuilder().schedulerCtx(schedulerCtx.get(eventDefinition.id())).build())
.toList();
return PageListResponse.create(query, definitionDtos.pagination(),
result.grandTotal().orElse(0L), sort, order, eventDefinitionDtos, attributes, settings);
}
@GET
@ApiOperation("List event definitions")
@Deprecated
public PaginatedResponse list(@ApiParam(name = "page") @QueryParam("page") @DefaultValue("1") int page,
@ApiParam(name = "per_page") @QueryParam("per_page") @DefaultValue("50") int perPage,
@ApiParam(name = "query") @QueryParam("query") @DefaultValue("") String query) {
SearchQuery searchQuery;
try {
searchQuery = searchQueryParser.parse(query);
} catch (IllegalArgumentException e) {
throw new BadRequestException("Invalid argument in search query: " + e.getMessage());
}
final PaginatedList result = dbService.searchPaginated(searchQuery, event -> {
return isPermitted(RestPermissions.EVENT_DEFINITIONS_READ, event.id());
}, "title", "asc", page, perPage);
final ImmutableMap context = contextService.contextFor(result.delegate());
return PaginatedResponse.create("event_definitions", result, query, context);
}
@GET
@Path("{definitionId}")
@ApiOperation("Get an event definition")
public EventDefinitionDto get(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId) {
checkPermission(RestPermissions.EVENT_DEFINITIONS_READ, definitionId);
return dbService.get(definitionId)
.orElseThrow(() -> new NotFoundException("Event definition <" + definitionId + "> doesn't exist"));
}
@GET
@Path("{definitionId}/with-context")
@ApiOperation("Get an event definition")
public Map getWithContext(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId) {
checkPermission(RestPermissions.EVENT_DEFINITIONS_READ, definitionId);
return dbService.get(definitionId)
.map(eventDefinition -> ImmutableMap.of(
"event_definition", eventDefinition,
"context", contextService.contextFor(eventDefinition),
"is_mutable", dbService.isMutable(eventDefinition)
))
.orElseThrow(() -> new NotFoundException("Event definition <" + definitionId + "> doesn't exist"));
}
@POST
@Produces(MediaType.APPLICATION_JSON)
@ApiOperation("Create new event definition")
@AuditEvent(type = EventsAuditEventTypes.EVENT_DEFINITION_CREATE)
@RequiresPermissions(RestPermissions.EVENT_DEFINITIONS_CREATE)
public Response create(@ApiParam("schedule") @QueryParam("schedule") @DefaultValue("true") boolean schedule,
@ApiParam(name = "JSON Body") EventDefinitionDto dto, @Context UserContext userContext) {
checkEventDefinitionPermissions(dto, "create");
final ValidationResult result = dto.validate(null, eventDefinitionConfiguration);
if (result.failed()) {
return Response.status(Response.Status.BAD_REQUEST).entity(result).build();
}
final EventDefinitionDto entity = schedule ?
eventDefinitionHandler.create(dto, Optional.of(userContext.getUser())) :
eventDefinitionHandler.createWithoutSchedule(dto.toBuilder().state(EventDefinition.State.DISABLED).build(), Optional.of(userContext.getUser()));
recentActivityService.create(entity.id(), GRNTypes.EVENT_DEFINITION, userContext.getUser());
return Response.ok().entity(entity).build();
}
@PUT
@Path("{definitionId}")
@ApiOperation("Update existing event definition")
@AuditEvent(type = EventsAuditEventTypes.EVENT_DEFINITION_UPDATE)
public Response update(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId,
@ApiParam("schedule") @QueryParam("schedule") @DefaultValue("true") boolean schedule,
@ApiParam(name = "JSON Body") EventDefinitionDto dto,
@Context UserContext userContext) {
checkPermission(RestPermissions.EVENT_DEFINITIONS_EDIT, definitionId);
checkEventDefinitionPermissions(dto, "update");
EventDefinitionDto oldDto = dbService.get(definitionId)
.orElseThrow(() -> new NotFoundException("Event definition <" + definitionId + "> doesn't exist"));
checkProcessorConfig(oldDto, dto);
final ValidationResult result = dto.validate(oldDto, eventDefinitionConfiguration);
if (!definitionId.equals(dto.id())) {
result.addError("id", "Event definition IDs don't match");
}
if (result.failed()) {
return Response.status(Response.Status.BAD_REQUEST).entity(result).build();
}
dto = dto.toBuilder().state(schedule ? EventDefinition.State.ENABLED : EventDefinition.State.DISABLED).build();
recentActivityService.update(definitionId, GRNTypes.EVENT_DEFINITION, userContext.getUser());
return Response.ok().entity(eventDefinitionHandler.update(dto, schedule)).build();
}
@DELETE
@Path("{definitionId}")
@ApiOperation("Delete event definition")
@AuditEvent(type = EventsAuditEventTypes.EVENT_DEFINITION_DELETE)
public EventDefinitionDto delete(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId,
@Context UserContext userContext) {
checkPermission(RestPermissions.EVENT_DEFINITIONS_DELETE, definitionId);
final Optional eventDefinitionDto = dbService.get(definitionId);
final String dependencyTitle = eventDefinitionDto.isPresent() ? eventDefinitionDto.get().title() : definitionId;
final List dependentEventDtoList = eventResolver.dependentEvents(definitionId);
if (!dependentEventDtoList.isEmpty()) {
final List dependenciesTitles = dependentEventDtoList.stream().map(EventDefinitionDto::title).toList();
final List dependenciesIds = dependentEventDtoList.stream().map(EventDefinitionDto::id).toList();
String msg = "Unable to delete event definition <" + dependencyTitle
+ "> - please remove all references from event definitions: " + StringUtils.join(dependenciesTitles, ",");
ValidationResult validationResult = new ValidationResult()
.addError("dependency", msg)
.addContext("dependency_ids", dependenciesIds);
throw new ValidationFailureException(validationResult, msg);
}
eventDefinitionDto.ifPresent(d ->
recentActivityService.delete(d.id(), GRNTypes.EVENT_DEFINITION, d.title(), userContext.getUser())
);
eventDefinitionHandler.delete(definitionId);
return eventDefinitionDto.orElse(null);
}
@POST
@Path("/bulk_delete")
@Consumes(MediaType.APPLICATION_JSON)
@Timed
@ApiOperation(value = "Delete multiple event definitions", response = BulkOperationResponse.class)
@NoAuditEvent("Audit events triggered manually")
public Response bulkDelete(@ApiParam(name = "Entities to remove", required = true) final BulkOperationRequest bulkOperationRequest,
@Context UserContext userContext) {
final BulkOperationResponse response = bulkDeletionExecutor.executeBulkOperation(bulkOperationRequest,
userContext,
new AuditParams(EventsAuditEventTypes.EVENT_DEFINITION_DELETE, "definitionId", EventDefinitionDto.class));
return Response.status(Response.Status.OK)
.entity(response)
.build();
}
@PUT
@Path("{definitionId}/schedule")
@Consumes(MediaType.WILDCARD)
@ApiOperation("Enable event definition")
@AuditEvent(type = EventsAuditEventTypes.EVENT_DEFINITION_UPDATE)
public EventDefinitionDto schedule(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId,
@Context UserContext userContext) {
checkPermission(RestPermissions.EVENT_DEFINITIONS_EDIT, definitionId);
final EventDefinitionDto eventDefinitionDto = dbService.get(definitionId).orElseThrow(() ->
new BadRequestException(f("Unable to find event definition '%s' to enable", definitionId)));
eventDefinitionHandler.schedule(definitionId);
return eventDefinitionDto.toBuilder().state(EventDefinition.State.ENABLED).build();
}
@POST
@Path("/bulk_schedule")
@Consumes(MediaType.APPLICATION_JSON)
@Timed
@ApiOperation(value = "Enable multiple event definitions", response = BulkOperationResponse.class)
@NoAuditEvent("Audit events triggered manually")
public Response bulkSchedule(@ApiParam(name = "Event definitions to enable", required = true) final BulkOperationRequest bulkOperationRequest,
@Context UserContext userContext) {
final BulkOperationResponse response = bulkScheduleExecutor.executeBulkOperation(bulkOperationRequest,
userContext,
new AuditParams(EventsAuditEventTypes.EVENT_DEFINITION_UPDATE, "definitionId", EventDefinitionDto.class));
return Response.status(Response.Status.OK)
.entity(response)
.build();
}
@PUT
@Path("{definitionId}/unschedule")
@Consumes(MediaType.WILDCARD)
@ApiOperation("Disable event definition")
@AuditEvent(type = EventsAuditEventTypes.EVENT_DEFINITION_UPDATE)
public EventDefinitionDto unschedule(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId,
@Context UserContext userContext) {
checkPermission(RestPermissions.EVENT_DEFINITIONS_EDIT, definitionId);
final EventDefinitionDto eventDefinitionDto = dbService.get(definitionId).orElseThrow(() ->
new BadRequestException(f("Unable to find event definition '%s' to disable", definitionId)));
eventDefinitionHandler.unschedule(definitionId);
return eventDefinitionDto.toBuilder().state(EventDefinition.State.DISABLED).build();
}
@POST
@Path("/bulk_unschedule")
@Consumes(MediaType.APPLICATION_JSON)
@Timed
@ApiOperation(value = "Disable multiple event definitions", response = BulkOperationResponse.class)
@NoAuditEvent("Audit events triggered manually")
public Response bulkUnschedule(@ApiParam(name = "Event definitions to disable", required = true) final BulkOperationRequest bulkOperationRequest,
@Context UserContext userContext) {
final BulkOperationResponse response = bulkUnscheduleExecutor.executeBulkOperation(bulkOperationRequest,
userContext,
new AuditParams(EventsAuditEventTypes.EVENT_DEFINITION_UPDATE, "definitionId", EventDefinitionDto.class));
return Response.status(Response.Status.OK)
.entity(response)
.build();
}
@PUT
@Path("{definitionId}/clear-notification-queue")
@Consumes(MediaType.WILDCARD)
@ApiOperation("Clear queued notifications for event")
@AuditEvent(type = EventsAuditEventTypes.EVENT_DEFINITION_CLEAR_NOTIFICATION_QUEUE)
public void clearNotificationQueue(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId) {
checkPermission(RestPermissions.EVENT_DEFINITIONS_EDIT, definitionId);
eventDefinitionHandler.deleteNotificationJobTriggers(definitionId);
}
@POST
@ApiOperation("Execute event definition")
@Path("{definitionId}/execute")
@AuditEvent(type = EventsAuditEventTypes.EVENT_DEFINITION_EXECUTE)
public void execute(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId,
@ApiParam(name = "parameters", required = true) @NotNull EventProcessorParameters parameters) {
checkPermission(RestPermissions.EVENT_DEFINITIONS_EXECUTE, definitionId);
if (parameters instanceof EventProcessorParametersWithTimerange.FallbackParameters) {
throw new BadRequestException("Unknown parameters type");
}
try {
engine.execute(definitionId, parameters);
} catch (EventProcessorException e) {
throw new InternalServerErrorException(e.getMessage(), e);
}
}
@POST
@ApiOperation("Duplicate an event definition")
@Path("{definitionId}/duplicate")
@Consumes(MediaType.WILDCARD)
@AuditEvent(type = EventsAuditEventTypes.EVENT_DEFINITION_CREATE)
@RequiresPermissions(RestPermissions.EVENT_DEFINITIONS_CREATE)
public Response duplicate(@ApiParam(name = "definitionId") @PathParam("definitionId") @NotBlank String definitionId, @Context UserContext userContext) {
final EventDefinitionDto eventDefinitionDto = dbService.get(definitionId).orElseThrow(() ->
new BadRequestException(f("Unable to find event definition '%s' to duplicate", definitionId)));
checkEventDefinitionPermissions(eventDefinitionDto, "create");
final EventDefinitionDto saved = eventDefinitionHandler.duplicate(eventDefinitionDto, Optional.of(userContext.getUser()));
return Response.ok().entity(saved).build();
}
@POST
@Path("/validate")
@NoAuditEvent("Validation only")
@ApiOperation(value = "Validate an event definition")
@RequiresPermissions(RestPermissions.EVENT_DEFINITIONS_CREATE)
public ValidationResult validate(@ApiParam(name = "JSON body", required = true)
@Valid @NotNull EventDefinitionDto toValidate) {
EventProcessorConfig oldConfig = dbService.get(toValidate.id()).map(eventDefinitionDto -> eventDefinitionDto.config()).orElse(null);
ValidationResult validationResult = toValidate.config().validate();
validationResult.addAll(toValidate.config().validate(oldConfig, eventDefinitionConfiguration));
return validationResult;
}
private void checkEventDefinitionPermissions(EventDefinitionDto dto, String action) {
final Set missingPermissions = dto.requiredPermissions().stream()
.filter(permission -> !isPermitted(permission))
.collect(Collectors.toSet());
if (!missingPermissions.isEmpty()) {
LOG.info("Not authorized to {} event definition. User <{}> is missing permissions: {}", action, getSubject().getPrincipal(), missingPermissions);
throw new ForbiddenException("Not authorized");
}
}
/**
* Check that if this Event Definitions Processor Config is being modified, it is allowed to be.
*
* @param oldEventDefinition - The Existing Event Definition
* @param updatedEventDefinition - The Event Definition with pending updates
*/
@VisibleForTesting
void checkProcessorConfig(EventDefinitionDto oldEventDefinition, EventDefinitionDto updatedEventDefinition) {
if (!oldEventDefinition.config().isUserPresentable()
&& !oldEventDefinition.config().type().equals(updatedEventDefinition.config().type())) {
LOG.error("Not allowed to change event definition condition type from <{}> to <{}>.",
oldEventDefinition.config().type(), updatedEventDefinition.config().type());
throw new ForbiddenException("Condition type not changeable");
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy