Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -43,6 +43,11 @@ public Builder entityKey(final String entityKey) {
return this;
}

public Builder username(final String username) {
getInstance().getUsername().add(username);
return this;
}

public Builder who(final String who) {
getInstance().getWho().add(who);
return this;
Expand Down Expand Up @@ -76,6 +81,8 @@ public Builder outcome(final OpEvent.Outcome outcome) {

private String entityKey;

private Set<String> username = new HashSet<>();

private Set<String> who = new HashSet<>();

private OpEvent.CategoryType type;
Expand All @@ -99,6 +106,19 @@ public void setEntityKey(final String entityKey) {
this.entityKey = entityKey;
}

@Parameter(name = "username", description = "username(s) embedded in the audited payload to match "
+ "(the affected entity, as opposed to the 'who' author); "
+ "may be repeated to match any of the given values", array =
@ArraySchema(schema = @Schema(implementation = String.class)))
public Set<String> getUsername() {
return username;
}

@QueryParam("username")
public void setUsername(final Set<String> username) {
this.username = username;
}

@Parameter(name = "who", description = "audit event author(s) (username) to match; "
+ "may be repeated to match any of the given values", array =
@ArraySchema(schema = @Schema(implementation = String.class)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ public List<OpEvent> events() {
@Transactional(readOnly = true)
public Page<AuditEventTO> search(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -285,10 +286,11 @@ public Page<AuditEventTO> search(
final OffsetDateTime after,
final Pageable pageable) {

long count = auditEventDAO.count(entityKey, who, type, category, subcategory, op, result, before, after);
long count = auditEventDAO.count(
entityKey, username, who, type, category, subcategory, op, result, before, after);

List<AuditEventTO> matching = auditEventDAO.search(
entityKey, who, type, category, subcategory, op, result, before, after, pageable);
entityKey, username, who, type, category, subcategory, op, result, before, after, pageable);

return new SyncopePage<>(matching, pageable, count);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public List<OpEvent> events() {
public PagedResult<AuditEventTO> search(final AuditQuery auditQuery) {
Page<AuditEventTO> result = logic.search(
auditQuery.getEntityKey(),
auditQuery.getUsername(),
auditQuery.getWho(),
auditQuery.getType(),
auditQuery.getCategory(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public interface AuditEventDAO {

long count(
String entityKey,
Set<String> username,
Set<String> who,
OpEvent.CategoryType type,
String category,
Expand All @@ -56,6 +57,7 @@ default AuditEventTO toAuditEventTO(final AuditEvent auditEvent) {

List<AuditEventTO> search(
String entityKey,
Set<String> username,
Set<String> who,
OpEvent.CategoryType type,
String category,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,31 @@ protected AuditEventCriteriaBuilder entityKey(final String entityKey) {
return this;
}

// Unlike the entityKey predicate above (a constrained UUID, concatenated), the free-form username
// is bound as a query parameter and its LIKE metacharacters are escaped, so '%'/'_' in a username
// match literally and there is no injection surface.
// '#' is used as the LIKE escape char (instead of '\') and applied uniformly: a single '\' in
// native SQL is mishandled by MySQL/MariaDB and Oracle has no default LIKE escape, whereas '#'
// works across every supported database (this DAO is not subclassed per-database).
protected static String escapeForLike(final String value) {
return value.replace("#", "##").replace("%", "#%").replace("_", "#_");
}

public AuditEventCriteriaBuilder username(final Set<String> username, final List<Object> parameters) {
if (!CollectionUtils.isEmpty(username)) {
query.append(andIfNeeded()).append("(").
append(username.stream().map(value -> {
String pattern = "%\"username\":\"" + escapeForLike(value) + "\"%";
return "(beforeValue LIKE ?" + setParameter(parameters, pattern) + " ESCAPE '#'"
+ " OR inputs LIKE ?" + setParameter(parameters, pattern) + " ESCAPE '#'"
+ " OR output LIKE ?" + setParameter(parameters, pattern) + " ESCAPE '#'"
+ " OR throwable LIKE ?" + setParameter(parameters, pattern) + " ESCAPE '#')";
}).collect(Collectors.joining(" OR "))).
append(")");
}
return this;
}

public AuditEventCriteriaBuilder who(final Set<String> who, final List<Object> parameters) {
if (!CollectionUtils.isEmpty(who)) {
query.append(andIfNeeded()).append("who IN (").
Expand Down Expand Up @@ -140,6 +165,7 @@ protected void fillWithParameters(final Query query, final List<Object> paramete
@Override
public long count(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -153,6 +179,7 @@ public long count(
String queryString = "SELECT COUNT(0)"
+ " FROM " + JPAAuditEvent.TABLE
+ " WHERE" + criteriaBuilder(entityKey).
username(username, parameters).
who(who, parameters).
opEvent(type, category, subcategory, op, outcome).
before(before, parameters).
Expand All @@ -168,6 +195,7 @@ public long count(
@Override
public List<AuditEventTO> search(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -182,6 +210,7 @@ public List<AuditEventTO> search(
String queryString = "SELECT id"
+ " FROM " + JPAAuditEvent.TABLE
+ " WHERE" + criteriaBuilder(entityKey).
username(username, parameters).
who(who, parameters).
opEvent(type, category, subcategory, op, outcome).
before(before, parameters).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ protected AuditEventCriteriaBuilder entityKey(final String entityKey) {
return this;
}

public AuditEventCriteriaBuilder username(final Set<String> username, final Map<String, Object> parameters) {
if (!CollectionUtils.isEmpty(username)) {
parameters.put("usernames", username.stream().map(value -> "\"username\":\"" + value + "\"").toList());
query.append(andIfNeeded()).
append("ANY(u IN $usernames WHERE n.before CONTAINS u OR n.inputs CONTAINS u "
+ "OR n.output CONTAINS u OR n.throwable CONTAINS u)");
}
return this;
}

public AuditEventCriteriaBuilder who(final Set<String> who, final Map<String, Object> parameters) {
if (!CollectionUtils.isEmpty(who)) {
parameters.put("who", List.copyOf(who));
Expand Down Expand Up @@ -135,6 +145,7 @@ protected AuditEventCriteriaBuilder criteriaBuilder(final String entityKey) {
@Override
public long count(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -147,6 +158,7 @@ public long count(
Map<String, Object> parameters = new HashMap<>();
String query = "MATCH (n:" + Neo4jAuditEvent.NODE + ") "
+ " WHERE " + criteriaBuilder(entityKey).
username(username, parameters).
who(who, parameters).
opEvent(type, category, subcategory, op, outcome).
before(before, parameters).
Expand All @@ -160,6 +172,7 @@ public long count(
@Override
public List<AuditEventTO> search(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -174,6 +187,7 @@ public List<AuditEventTO> search(

StringBuilder query = new StringBuilder("MATCH (n:" + Neo4jAuditEvent.NODE + ") "
+ "WHERE " + criteriaBuilder(entityKey).
username(username, parameters).
who(who, parameters).
opEvent(type, category, subcategory, op, outcome).
before(before, parameters).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public AuditEvent save(final AuditEvent auditEvent) {

protected Query getQuery(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -101,6 +102,17 @@ protected Query getQuery(
query("\"key\":\"" + entityKey + "\"").build()).build());
}

if (!CollectionUtils.isEmpty(username)) {
List<Query> usernameQueries = username.stream().map(value -> new Query.Builder().
multiMatch(QueryBuilders.multiMatch().
fields("before", "inputs", "output", "throwable").
type(TextQueryType.Phrase).
query("\"username\":\"" + value + "\"").build()).build()).
toList();
queries.add(new Query.Builder().
bool(QueryBuilders.bool().should(usernameQueries).minimumShouldMatch("1").build()).build());
}

if (!CollectionUtils.isEmpty(who)) {
List<Query> whoQueries = who.stream().map(value -> new Query.Builder().
term(QueryBuilders.term().field("who").value(value).build()).build()).
Expand Down Expand Up @@ -137,6 +149,7 @@ protected Query getQuery(
@Override
public long count(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -148,7 +161,7 @@ public long count(

CountRequest request = new CountRequest.Builder().
index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
query(getQuery(entityKey, who, type, category, subcategory, op, outcome, before, after)).
query(getQuery(entityKey, username, who, type, category, subcategory, op, outcome, before, after)).
build();
LOG.debug("Count request: {}", request);

Expand All @@ -172,6 +185,7 @@ protected List<SortOptions> sortBuilders(final Stream<Sort.Order> orderBy) {
@Override
public List<AuditEventTO> search(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -185,7 +199,7 @@ public List<AuditEventTO> search(
SearchRequest request = new SearchRequest.Builder().
index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
searchType(SearchType.QueryThenFetch).
query(getQuery(entityKey, who, type, category, subcategory, op, outcome, before, after)).
query(getQuery(entityKey, username, who, type, category, subcategory, op, outcome, before, after)).
from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * pageable.getPageNumber()).
size(pageable.isUnpaged() ? indexMaxResultWindow : pageable.getPageSize()).
sort(sortBuilders(pageable.getSort().get())).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public AuditEvent save(final AuditEvent auditEvent) {

protected Query getQuery(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -100,6 +101,17 @@ protected Query getQuery(
query("\"key\":\"" + entityKey + "\"").build()).build());
}

if (!CollectionUtils.isEmpty(username)) {
List<Query> usernameQueries = username.stream().map(value -> new Query.Builder().
multiMatch(QueryBuilders.multiMatch().
fields("before", "inputs", "output", "throwable").
type(TextQueryType.Phrase).
query("\"username\":\"" + value + "\"").build()).build()).
toList();
queries.add(new Query.Builder().
bool(QueryBuilders.bool().should(usernameQueries).minimumShouldMatch("1").build()).build());
}

if (!CollectionUtils.isEmpty(who)) {
List<Query> whoQueries = who.stream().map(value -> new Query.Builder().
term(QueryBuilders.term().field("who").value(v -> v.stringValue(value)).build()).build()).
Expand Down Expand Up @@ -136,6 +148,7 @@ protected Query getQuery(
@Override
public long count(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -147,7 +160,7 @@ public long count(

CountRequest request = new CountRequest.Builder().
index(OpenSearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
query(getQuery(entityKey, who, type, category, subcategory, op, outcome, before, after)).
query(getQuery(entityKey, username, who, type, category, subcategory, op, outcome, before, after)).
build();
LOG.debug("Count request: {}", request);

Expand All @@ -171,6 +184,7 @@ protected List<SortOptions> sortBuilders(final Stream<Sort.Order> orderBy) {
@Override
public List<AuditEventTO> search(
final String entityKey,
final Set<String> username,
final Set<String> who,
final OpEvent.CategoryType type,
final String category,
Expand All @@ -184,7 +198,7 @@ public List<AuditEventTO> search(
SearchRequest request = new SearchRequest.Builder().
index(OpenSearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
searchType(SearchType.QueryThenFetch).
query(getQuery(entityKey, who, type, category, subcategory, op, outcome, before, after)).
query(getQuery(entityKey, username, who, type, category, subcategory, op, outcome, before, after)).
from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * pageable.getPageNumber()).
size(pageable.isUnpaged() ? indexMaxResultWindow : pageable.getPageSize()).
sort(sortBuilders(pageable.getSort().get())).
Expand Down
Loading
Loading