Skip to content

Commit

Permalink
Added an API for retrieving the REST API audit log entries [comixed#482]
Browse files Browse the repository at this point in the history
 * Added a service and repository method to retrieve the records.
  • Loading branch information
mcpierce committed Aug 22, 2020
1 parent c91f1b4 commit 7ea94e8
Show file tree
Hide file tree
Showing 12 changed files with 328 additions and 21 deletions.
Expand Up @@ -18,10 +18,14 @@

package org.comixedproject.model.auditlog;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonView;
import java.util.Date;
import javax.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.comixedproject.views.View;

/**
* <code>RestAuditLogEntry</code> represents a single entry in the REST API audit log table.
Expand All @@ -34,58 +38,71 @@ public class RestAuditLogEntry {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Getter
@JsonIgnore
private Long id;

@Column(name = "remote_ip", nullable = false, updatable = false)
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
private String remoteIp;

@Column(name = "url", nullable = false, updatable = false)
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
private String url;

@Column(name = "method", nullable = false, updatable = false)
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
private String method;

@Column(name = "request_content", nullable = true, updatable = false)
@Lob
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
private String requestContent;

@Column(name = "response_content", nullable = true, updatable = false)
@Lob
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
private String responseContent;

@Column(name = "email", nullable = true, updatable = false)
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
private String email;

@Column(name = "start_time", nullable = false, updatable = false)
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
private Date startTime = new Date();

@Column(name = "end_time", nullable = false, updatable = false)
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
private Date endTime = new Date();

@Column(name = "successful", nullable = false, updatable = false)
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
private Boolean successful;

@Column(name = "exception", nullable = true, updatable = false)
@Lob
@Getter
@Setter
@JsonView(View.AuditLogEntryList.class)
private String exception;
}
Expand Up @@ -47,37 +47,37 @@ public class TaskAuditLogEntry {
@Setter
@JsonProperty("startTime")
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
@JsonView(View.TaskAuditLogEntryList.class)
@JsonView(View.AuditLogEntryList.class)
private Date startTime = new Date();

@Column(name = "end_time", nullable = false, updatable = false)
@Getter
@Setter
@JsonProperty("endTime")
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
@JsonView(View.TaskAuditLogEntryList.class)
@JsonView(View.AuditLogEntryList.class)
private Date endTime = new Date();

@Column(name = "successful", nullable = false, updatable = false)
@Getter
@Setter
@JsonProperty("successful")
@JsonView(View.TaskAuditLogEntryList.class)
@JsonView(View.AuditLogEntryList.class)
private Boolean successful;

@Column(name = "description", nullable = false, updatable = false, length = 2048)
@Getter
@Setter
@JsonProperty("description")
@JsonView(View.TaskAuditLogEntryList.class)
@JsonView(View.AuditLogEntryList.class)
private String description;

@Column(name = "exception", nullable = true, updatable = false)
@Lob
@Getter
@Setter
@JsonProperty("exception")
@JsonView(View.TaskAuditLogEntryList.class)
@JsonView(View.AuditLogEntryList.class)
private String exception;

@Override
Expand Down
Expand Up @@ -66,6 +66,6 @@ public interface LibraryUpdate {}
/** Used when viewing the list of plugins. */
public interface PluginList {}

/** Uses when viewing the list of task audit log entries. */
public interface TaskAuditLogEntryList extends ApiResponse {}
/** Uses when viewing a list of audit log entries. */
public interface AuditLogEntryList extends ApiResponse {}
}
Expand Up @@ -18,8 +18,12 @@

package org.comixedproject.repositories.auditlog;

import java.util.Date;
import java.util.List;
import org.comixedproject.model.auditlog.RestAuditLogEntry;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

/**
Expand All @@ -28,4 +32,14 @@
* @author Darryl L. Pierce
*/
@Repository
public interface RestAuditLogRepository extends JpaRepository<RestAuditLogEntry, Long> {}
public interface RestAuditLogRepository extends JpaRepository<RestAuditLogEntry, Long> {
/**
* Retrieve a page of entries after the provided cutoff date.
*
* @param cutoff the cutoff date
* @param pageable the page parameters
* @return the list of entries
*/
List<RestAuditLogEntry> findByEndTimeAfterOrderByEndTime(
@Param("cutoff") Date cutoff, Pageable pageable);
}
@@ -0,0 +1,66 @@
/*
* ComiXed - A digital comic book library management application.
* Copyright (C) 2020, The ComiXed Project
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* 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
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses>
*/

package org.comixedproject.repositories.auditlog;

import static junit.framework.TestCase.assertFalse;
import static junit.framework.TestCase.assertNotNull;

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import java.util.Date;
import java.util.List;
import org.comixedproject.model.auditlog.RestAuditLogEntry;
import org.comixedproject.repositories.RepositoryContext;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RepositoryContext.class)
@TestPropertySource(locations = "classpath:application.properties")
@DatabaseSetup("classpath:test-database.xml")
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
TransactionalTestExecutionListener.class,
DbUnitTestExecutionListener.class
})
public class RestAuditLogRepositoryTest {
private static final Date TEST_CUTOFF = new Date();

@Autowired private RestAuditLogRepository auditLogRepository;

@Test
public void testFindByEndDateLaterThanOrderByEndDate() {
final List<RestAuditLogEntry> result =
this.auditLogRepository.findByEndTimeAfterOrderByEndTime(
TEST_CUTOFF, PageRequest.of(0, 100));

assertNotNull(result);
assertFalse(result.isEmpty());
}
}
9 changes: 9 additions & 0 deletions comixed-repositories/src/test/resources/test-database.xml
Expand Up @@ -532,4 +532,13 @@
COMIC_VINE_ID="23173"
COMIC_VINE_URL="http://localhost"
DESCRIPTION="This is a publisher"/>
<rest_audit_log id="1000"
remote_ip="192.168.1.125"
url="/api/comics"
method="GET"
email="reader@comixedproject.org"
start_time="[now+1h]"
end_time="[now+1h+1m]"
successful="true"
response_content="some data"/>
</dataset>
Expand Up @@ -24,17 +24,22 @@
import lombok.extern.log4j.Log4j2;
import org.comixedproject.auditlog.AuditableEndpoint;
import org.comixedproject.controller.ComiXedControllerException;
import org.comixedproject.model.auditlog.RestAuditLogEntry;
import org.comixedproject.model.tasks.TaskAuditLogEntry;
import org.comixedproject.net.ApiResponse;
import org.comixedproject.net.GetRestAuditLogResponse;
import org.comixedproject.net.GetTaskAuditLogResponse;
import org.comixedproject.repositories.tasks.TaskAuditLogRepository;
import org.comixedproject.service.ComiXedServiceException;
import org.comixedproject.service.auditlog.RestAuditLogService;
import org.comixedproject.service.task.TaskService;
import org.comixedproject.views.View;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
* <code>AuditLogController</code> provides REST APIs for interacting with the tasks system.
Expand All @@ -44,8 +49,8 @@
@RestController
@Log4j2
public class AuditLogController {
@Autowired private TaskAuditLogRepository taskAuditLogRepository;
@Autowired private TaskService taskService;
@Autowired private RestAuditLogService restAuditLogService;

/**
* Retrieve the list of log entries after the cutoff time.
Expand All @@ -55,7 +60,7 @@ public class AuditLogController {
*/
@GetMapping(value = "/api/tasks/entries/{cutoff}", produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasRole('ADMIN')")
@JsonView(View.TaskAuditLogEntryList.class)
@JsonView(View.AuditLogEntryList.class)
@AuditableEndpoint
public ApiResponse<GetTaskAuditLogResponse> getAllTaskEntriesAfterDate(
@PathVariable("cutoff") final Long timestamp) throws ComiXedControllerException {
Expand Down Expand Up @@ -106,4 +111,40 @@ public ApiResponse<Void> clearTaskAuditLog() {

return response;
}

/**
* Returns the list of REST audit log entries that ended after the given date.
*
* <p><b>NOTE:</b> this endpoint is <em>NOT</em> audited since it would create an infinite
* feedback loop.
*
* @param cutoff the cutoff timestamp
* @return the list of entries
*/
@GetMapping(
value = "/api/auditing/rest/entries/{cutoff}",
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasRole('ADMIN')")
@JsonView(View.AuditLogEntryList.class)
public ApiResponse<GetRestAuditLogResponse> getAllRestEntriesAfterDate(
@PathVariable("cutoff") final Long cutoff) {
log.info("Getting all REST audit entries since {}", cutoff);
ApiResponse<GetRestAuditLogResponse> response = new ApiResponse<>();

try {
final List<RestAuditLogEntry> result = this.restAuditLogService.getEntriesAfterDate(cutoff);
response.setSuccess(true);
response.setResult(new GetRestAuditLogResponse());
response.getResult().setEntries(result);
response
.getResult()
.setLatest(result.isEmpty() ? new Date() : result.get(result.size() - 1).getEndTime());
} catch (Exception error) {
response.setSuccess(false);
response.setError(error.getMessage());
response.setThrowable(error);
}

return response;
}
}
@@ -0,0 +1,32 @@
package org.comixedproject.net;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonView;
import java.util.Date;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.comixedproject.model.auditlog.RestAuditLogEntry;
import org.comixedproject.views.View;

/**
* <code>GetRestAuditLogResponse</code> represents the content for a reqwuest to get REST audit log
* entries.
*
* @author Darryl L. Pierce
*/
public class GetRestAuditLogResponse {
@Getter
@Setter
@JsonProperty("entries")
@JsonView(View.AuditLogEntryList.class)
private List<RestAuditLogEntry> entries;

@Getter
@Setter
@JsonProperty("latest")
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
@JsonView(View.AuditLogEntryList.class)
private Date latest;
}
Expand Up @@ -21,13 +21,13 @@ public class GetTaskAuditLogResponse {
@Getter
@Setter
@JsonProperty("entries")
@JsonView(View.TaskAuditLogEntryList.class)
@JsonView(View.AuditLogEntryList.class)
private List<TaskAuditLogEntry> entries = new ArrayList<>();

@Getter
@Setter
@JsonProperty("latest")
@JsonView(View.TaskAuditLogEntryList.class)
@JsonView(View.AuditLogEntryList.class)
@JsonFormat(shape = JsonFormat.Shape.NUMBER)
private Date latest;
}

0 comments on commit 7ea94e8

Please sign in to comment.