Skip to content

Commit 3da46df

Browse files
ozimakovilgrosso
authored andcommitted
[SYNCOPE-1978] Search audit events by username (#1443)
1 parent 4f5a8cb commit 3da46df

9 files changed

Lines changed: 183 additions & 6 deletions

File tree

‎common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AuditQuery.java‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ public Builder entityKey(final String entityKey) {
4343
return this;
4444
}
4545

46+
public Builder username(final String username) {
47+
getInstance().getUsername().add(username);
48+
return this;
49+
}
50+
4651
public Builder who(final String who) {
4752
getInstance().getWho().add(who);
4853
return this;
@@ -76,6 +81,8 @@ public Builder outcome(final OpEvent.Outcome outcome) {
7681

7782
private String entityKey;
7883

84+
private Set<String> username = new HashSet<>();
85+
7986
private Set<String> who = new HashSet<>();
8087

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

109+
@Parameter(name = "username", description = "username(s) embedded in the audited payload to match "
110+
+ "(the affected entity, as opposed to the 'who' author); "
111+
+ "may be repeated to match any of the given values", array =
112+
@ArraySchema(schema = @Schema(implementation = String.class)))
113+
public Set<String> getUsername() {
114+
return username;
115+
}
116+
117+
@QueryParam("username")
118+
public void setUsername(final Set<String> username) {
119+
this.username = username;
120+
}
121+
102122
@Parameter(name = "who", description = "audit event author(s) (username) to match; "
103123
+ "may be repeated to match any of the given values", array =
104124
@ArraySchema(schema = @Schema(implementation = String.class)))

‎core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AuditLogic.java‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ public List<OpEvent> events() {
275275
@Transactional(readOnly = true)
276276
public Page<AuditEventTO> search(
277277
final String entityKey,
278+
final Set<String> username,
278279
final Set<String> who,
279280
final OpEvent.CategoryType type,
280281
final String category,
@@ -285,10 +286,11 @@ public Page<AuditEventTO> search(
285286
final OffsetDateTime after,
286287
final Pageable pageable) {
287288

288-
long count = auditEventDAO.count(entityKey, who, type, category, subcategory, op, result, before, after);
289+
long count = auditEventDAO.count(
290+
entityKey, username, who, type, category, subcategory, op, result, before, after);
289291

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

293295
return new SyncopePage<>(matching, pageable, count);
294296
}

‎core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/AuditServiceImpl.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ public List<OpEvent> events() {
7070
public PagedResult<AuditEventTO> search(final AuditQuery auditQuery) {
7171
Page<AuditEventTO> result = logic.search(
7272
auditQuery.getEntityKey(),
73+
auditQuery.getUsername(),
7374
auditQuery.getWho(),
7475
auditQuery.getType(),
7576
auditQuery.getCategory(),

‎core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AuditEventDAO.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public interface AuditEventDAO {
3232

3333
long count(
3434
String entityKey,
35+
Set<String> username,
3536
Set<String> who,
3637
OpEvent.CategoryType type,
3738
String category,
@@ -56,6 +57,7 @@ default AuditEventTO toAuditEventTO(final AuditEvent auditEvent) {
5657

5758
List<AuditEventTO> search(
5859
String entityKey,
60+
Set<String> username,
5961
Set<String> who,
6062
OpEvent.CategoryType type,
6163
String category,

‎core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,31 @@ protected AuditEventCriteriaBuilder entityKey(final String entityKey) {
6363
return this;
6464
}
6565

66+
// Unlike the entityKey predicate above (a constrained UUID, concatenated), the free-form username
67+
// is bound as a query parameter and its LIKE metacharacters are escaped, so '%'/'_' in a username
68+
// match literally and there is no injection surface.
69+
// '#' is used as the LIKE escape char (instead of '\') and applied uniformly: a single '\' in
70+
// native SQL is mishandled by MySQL/MariaDB and Oracle has no default LIKE escape, whereas '#'
71+
// works across every supported database (this DAO is not subclassed per-database).
72+
protected static String escapeForLike(final String value) {
73+
return value.replace("#", "##").replace("%", "#%").replace("_", "#_");
74+
}
75+
76+
public AuditEventCriteriaBuilder username(final Set<String> username, final List<Object> parameters) {
77+
if (!CollectionUtils.isEmpty(username)) {
78+
query.append(andIfNeeded()).append("(").
79+
append(username.stream().map(value -> {
80+
String pattern = "%\"username\":\"" + escapeForLike(value) + "\"%";
81+
return "(beforeValue LIKE ?" + setParameter(parameters, pattern) + " ESCAPE '#'"
82+
+ " OR inputs LIKE ?" + setParameter(parameters, pattern) + " ESCAPE '#'"
83+
+ " OR output LIKE ?" + setParameter(parameters, pattern) + " ESCAPE '#'"
84+
+ " OR throwable LIKE ?" + setParameter(parameters, pattern) + " ESCAPE '#')";
85+
}).collect(Collectors.joining(" OR "))).
86+
append(")");
87+
}
88+
return this;
89+
}
90+
6691
public AuditEventCriteriaBuilder who(final Set<String> who, final List<Object> parameters) {
6792
if (!CollectionUtils.isEmpty(who)) {
6893
query.append(andIfNeeded()).append("who IN (").
@@ -140,6 +165,7 @@ protected void fillWithParameters(final Query query, final List<Object> paramete
140165
@Override
141166
public long count(
142167
final String entityKey,
168+
final Set<String> username,
143169
final Set<String> who,
144170
final OpEvent.CategoryType type,
145171
final String category,
@@ -153,6 +179,7 @@ public long count(
153179
String queryString = "SELECT COUNT(0)"
154180
+ " FROM " + JPAAuditEvent.TABLE
155181
+ " WHERE" + criteriaBuilder(entityKey).
182+
username(username, parameters).
156183
who(who, parameters).
157184
opEvent(type, category, subcategory, op, outcome).
158185
before(before, parameters).
@@ -168,6 +195,7 @@ public long count(
168195
@Override
169196
public List<AuditEventTO> search(
170197
final String entityKey,
198+
final Set<String> username,
171199
final Set<String> who,
172200
final OpEvent.CategoryType type,
173201
final String category,
@@ -182,6 +210,7 @@ public List<AuditEventTO> search(
182210
String queryString = "SELECT id"
183211
+ " FROM " + JPAAuditEvent.TABLE
184212
+ " WHERE" + criteriaBuilder(entityKey).
213+
username(username, parameters).
185214
who(who, parameters).
186215
opEvent(type, category, subcategory, op, outcome).
187216
before(before, parameters).

‎core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ protected AuditEventCriteriaBuilder entityKey(final String entityKey) {
6060
return this;
6161
}
6262

63+
public AuditEventCriteriaBuilder username(final Set<String> username, final Map<String, Object> parameters) {
64+
if (!CollectionUtils.isEmpty(username)) {
65+
parameters.put("usernames", username.stream().map(value -> "\"username\":\"" + value + "\"").toList());
66+
query.append(andIfNeeded()).
67+
append("ANY(u IN $usernames WHERE n.before CONTAINS u OR n.inputs CONTAINS u "
68+
+ "OR n.output CONTAINS u OR n.throwable CONTAINS u)");
69+
}
70+
return this;
71+
}
72+
6373
public AuditEventCriteriaBuilder who(final Set<String> who, final Map<String, Object> parameters) {
6474
if (!CollectionUtils.isEmpty(who)) {
6575
parameters.put("who", List.copyOf(who));
@@ -135,6 +145,7 @@ protected AuditEventCriteriaBuilder criteriaBuilder(final String entityKey) {
135145
@Override
136146
public long count(
137147
final String entityKey,
148+
final Set<String> username,
138149
final Set<String> who,
139150
final OpEvent.CategoryType type,
140151
final String category,
@@ -147,6 +158,7 @@ public long count(
147158
Map<String, Object> parameters = new HashMap<>();
148159
String query = "MATCH (n:" + Neo4jAuditEvent.NODE + ") "
149160
+ " WHERE " + criteriaBuilder(entityKey).
161+
username(username, parameters).
150162
who(who, parameters).
151163
opEvent(type, category, subcategory, op, outcome).
152164
before(before, parameters).
@@ -160,6 +172,7 @@ public long count(
160172
@Override
161173
public List<AuditEventTO> search(
162174
final String entityKey,
175+
final Set<String> username,
163176
final Set<String> who,
164177
final OpEvent.CategoryType type,
165178
final String category,
@@ -174,6 +187,7 @@ public List<AuditEventTO> search(
174187

175188
StringBuilder query = new StringBuilder("MATCH (n:" + Neo4jAuditEvent.NODE + ") "
176189
+ "WHERE " + criteriaBuilder(entityKey).
190+
username(username, parameters).
177191
who(who, parameters).
178192
opEvent(type, category, subcategory, op, outcome).
179193
before(before, parameters).

‎ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchAuditEventDAO.java‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public AuditEvent save(final AuditEvent auditEvent) {
8585

8686
protected Query getQuery(
8787
final String entityKey,
88+
final Set<String> username,
8889
final Set<String> who,
8990
final OpEvent.CategoryType type,
9091
final String category,
@@ -104,6 +105,17 @@ protected Query getQuery(
104105
query("\"key\":\"" + entityKey + "\"").build()).build());
105106
}
106107

108+
if (!CollectionUtils.isEmpty(username)) {
109+
List<Query> usernameQueries = username.stream().map(value -> new Query.Builder().
110+
multiMatch(QueryBuilders.multiMatch().
111+
fields("before", "inputs", "output", "throwable").
112+
type(TextQueryType.Phrase).
113+
query("\"username\":\"" + value + "\"").build()).build()).
114+
toList();
115+
queries.add(new Query.Builder().
116+
bool(QueryBuilders.bool().should(usernameQueries).minimumShouldMatch("1").build()).build());
117+
}
118+
107119
if (!CollectionUtils.isEmpty(who)) {
108120
List<Query> whoQueries = who.stream().map(value -> new Query.Builder().
109121
term(QueryBuilders.term().field("who").value(value).build()).build()).
@@ -140,6 +152,7 @@ protected Query getQuery(
140152
@Override
141153
public long count(
142154
final String entityKey,
155+
final Set<String> username,
143156
final Set<String> who,
144157
final OpEvent.CategoryType type,
145158
final String category,
@@ -151,7 +164,7 @@ public long count(
151164

152165
CountRequest request = new CountRequest.Builder().
153166
index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
154-
query(getQuery(entityKey, who, type, category, subcategory, op, outcome, before, after)).
167+
query(getQuery(entityKey, username, who, type, category, subcategory, op, outcome, before, after)).
155168
build();
156169
LOG.debug("Count request: {}", request);
157170

@@ -175,6 +188,7 @@ protected List<SortOptions> sortBuilders(final Stream<Sort.Order> orderBy) {
175188
@Override
176189
public List<AuditEventTO> search(
177190
final String entityKey,
191+
final Set<String> username,
178192
final Set<String> who,
179193
final OpEvent.CategoryType type,
180194
final String category,
@@ -188,7 +202,7 @@ public List<AuditEventTO> search(
188202
SearchRequest request = new SearchRequest.Builder().
189203
index(ElasticsearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
190204
searchType(SearchType.QueryThenFetch).
191-
query(getQuery(entityKey, who, type, category, subcategory, op, outcome, before, after)).
205+
query(getQuery(entityKey, username, who, type, category, subcategory, op, outcome, before, after)).
192206
from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * pageable.getPageNumber()).
193207
size(pageable.isUnpaged() ? indexMaxResultWindow : pageable.getPageSize()).
194208
sort(sortBuilders(pageable.getSort().get())).

‎ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchAuditEventDAO.java‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public AuditEvent save(final AuditEvent auditEvent) {
8484

8585
protected Query getQuery(
8686
final String entityKey,
87+
final Set<String> username,
8788
final Set<String> who,
8889
final OpEvent.CategoryType type,
8990
final String category,
@@ -103,6 +104,17 @@ protected Query getQuery(
103104
query("\"key\":\"" + entityKey + "\"").build()).build());
104105
}
105106

107+
if (!CollectionUtils.isEmpty(username)) {
108+
List<Query> usernameQueries = username.stream().map(value -> new Query.Builder().
109+
multiMatch(QueryBuilders.multiMatch().
110+
fields("before", "inputs", "output", "throwable").
111+
type(TextQueryType.Phrase).
112+
query("\"username\":\"" + value + "\"").build()).build()).
113+
toList();
114+
queries.add(new Query.Builder().
115+
bool(QueryBuilders.bool().should(usernameQueries).minimumShouldMatch("1").build()).build());
116+
}
117+
106118
if (!CollectionUtils.isEmpty(who)) {
107119
List<Query> whoQueries = who.stream().map(value -> new Query.Builder().
108120
term(QueryBuilders.term().field("who").value(v -> v.stringValue(value)).build()).build()).
@@ -139,6 +151,7 @@ protected Query getQuery(
139151
@Override
140152
public long count(
141153
final String entityKey,
154+
final Set<String> username,
142155
final Set<String> who,
143156
final OpEvent.CategoryType type,
144157
final String category,
@@ -150,7 +163,7 @@ public long count(
150163

151164
CountRequest request = new CountRequest.Builder().
152165
index(OpenSearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
153-
query(getQuery(entityKey, who, type, category, subcategory, op, outcome, before, after)).
166+
query(getQuery(entityKey, username, who, type, category, subcategory, op, outcome, before, after)).
154167
build();
155168
LOG.debug("Count request: {}", request);
156169

@@ -174,6 +187,7 @@ protected List<SortOptions> sortBuilders(final Stream<Sort.Order> orderBy) {
174187
@Override
175188
public List<AuditEventTO> search(
176189
final String entityKey,
190+
final Set<String> username,
177191
final Set<String> who,
178192
final OpEvent.CategoryType type,
179193
final String category,
@@ -187,7 +201,7 @@ public List<AuditEventTO> search(
187201
SearchRequest request = new SearchRequest.Builder().
188202
index(OpenSearchUtils.getAuditIndex(AuthContextUtils.getDomain())).
189203
searchType(SearchType.QueryThenFetch).
190-
query(getQuery(entityKey, who, type, category, subcategory, op, outcome, before, after)).
204+
query(getQuery(entityKey, username, who, type, category, subcategory, op, outcome, before, after)).
191205
from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * pageable.getPageNumber()).
192206
size(pageable.isUnpaged() ? indexMaxResultWindow : pageable.getPageSize()).
193207
sort(sortBuilders(pageable.getSort().get())).

0 commit comments

Comments
 (0)