Skip to content

Commit 24a6623

Browse files
authored
[RORDEV-1567] ECS serializer and improved configurable serializer (#1165)
1 parent 5b1786c commit 24a6623

File tree

9 files changed

+530
-92
lines changed

9 files changed

+530
-92
lines changed

audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala

Lines changed: 104 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,23 @@ private[ror] object AuditSerializationHelper {
4545
}
4646

4747
def serialize(responseContext: AuditResponseContext,
48-
fields: Map[AuditFieldName, AuditFieldValueDescriptor],
48+
fields: Map[AuditFieldPath, AuditFieldValueDescriptor],
4949
allowedEventMode: AllowedEventMode): Option[JSONObject] = {
5050
responseContext match {
5151
case Allowed(requestContext, verbosity, reason) =>
5252
allowedEvent(
5353
allowedEventMode,
5454
verbosity,
55-
createEntry(fields, EventData(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None))
55+
createEntry(fields, EventData(matched = true, FinalState.Allowed, reason, responseContext.duration, requestContext, None))
5656
)
5757
case ForbiddenBy(requestContext, _, reason) =>
58-
Some(createEntry(fields, EventData(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None)))
58+
Some(createEntry(fields, EventData(matched = true, FinalState.Forbidden, reason, responseContext.duration, requestContext, None)))
5959
case Forbidden(requestContext) =>
60-
Some(createEntry(fields, EventData(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None)))
60+
Some(createEntry(fields, EventData(matched = false, FinalState.Forbidden, "default", responseContext.duration, requestContext, None)))
6161
case RequestedIndexNotExist(requestContext) =>
62-
Some(createEntry(fields, EventData(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None)))
62+
Some(createEntry(fields, EventData(matched = false, FinalState.IndexNotExist, "Requested index doesn't exist", responseContext.duration, requestContext, None)))
6363
case Errored(requestContext, cause) =>
64-
Some(createEntry(fields, EventData(matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))))
64+
Some(createEntry(fields, EventData(matched = false, FinalState.Errored, "error", responseContext.duration, requestContext, Some(cause))))
6565
}
6666
}
6767

@@ -76,23 +76,48 @@ private[ror] object AuditSerializationHelper {
7676
}
7777
}
7878

79-
private def createEntry(fields: Map[AuditFieldName, AuditFieldValueDescriptor],
79+
private def createEntry(fields: Map[AuditFieldPath, AuditFieldValueDescriptor],
8080
eventData: EventData) = {
8181
val resolveAuditFieldValue = resolver(eventData)
82-
val resolvedFields: Map[String, Any] =
83-
Map("@timestamp" -> timestampFormatter.format(eventData.requestContext.timestamp)) ++
84-
fields.map { case (name, valueDescriptor) => name.value -> resolveAuditFieldValue(valueDescriptor) }
82+
val resolvedFields: Map[AuditFieldPath, Any] =
83+
Map(AuditFieldPath("@timestamp") -> timestampFormatter.format(eventData.requestContext.timestamp)) ++
84+
fields.map { case (name, valueDescriptor) => name -> resolveAuditFieldValue(valueDescriptor) }
8585

86-
resolvedFields
87-
.foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) }
88-
.mergeWith(eventData.requestContext.generalAuditEvents)
86+
resolvedFields.foldLeft(new JSONObject()) { case (soFar, (path, value)) =>
87+
putNested(soFar, path.path.toList, value)
88+
}.mergeWith(eventData.requestContext.generalAuditEvents)
89+
}
90+
91+
private def putNested(json: JSONObject, path: List[String], value: Any): JSONObject = {
92+
path match {
93+
case Nil =>
94+
json
95+
case key :: Nil =>
96+
json.put(key, value)
97+
json
98+
case key :: tail =>
99+
val child = Option(json.optJSONObject(key)).getOrElse(new JSONObject())
100+
json.put(key, putNested(child, tail, value))
101+
json
102+
}
89103
}
90104

91105
private def resolver(eventData: EventData): AuditFieldValueDescriptor => Any = auditValue => {
92106
val requestContext = eventData.requestContext
93107
auditValue match {
94108
case AuditFieldValueDescriptor.IsMatched => eventData.matched
95-
case AuditFieldValueDescriptor.FinalState => eventData.finalState
109+
case AuditFieldValueDescriptor.FinalState => eventData.finalState match {
110+
case FinalState.Allowed => "ALLOWED"
111+
case FinalState.Forbidden => "FORBIDDEN"
112+
case FinalState.Errored => "ERRORED"
113+
case FinalState.IndexNotExist => "INDEX NOT EXIST"
114+
}
115+
case AuditFieldValueDescriptor.EcsEventOutcome => eventData.finalState match {
116+
case FinalState.Allowed => "success"
117+
case FinalState.Forbidden => "failure"
118+
case FinalState.Errored => "unknown"
119+
case FinalState.IndexNotExist => "unknown"
120+
}
96121
case AuditFieldValueDescriptor.Reason => eventData.reason
97122
case AuditFieldValueDescriptor.User => SerializeUser.serialize(requestContext).orNull
98123
case AuditFieldValueDescriptor.LoggedUser => requestContext.loggedInUserName.orNull
@@ -102,6 +127,7 @@ private[ror] object AuditSerializationHelper {
102127
case AuditFieldValueDescriptor.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava
103128
case AuditFieldValueDescriptor.AclHistory => requestContext.history
104129
case AuditFieldValueDescriptor.ProcessingDurationMillis => eventData.duration.toMillis
130+
case AuditFieldValueDescriptor.ProcessingDurationNanos => eventData.duration.toNanos
105131
case AuditFieldValueDescriptor.Timestamp => timestampFormatter.format(requestContext.timestamp)
106132
case AuditFieldValueDescriptor.Id => requestContext.id
107133
case AuditFieldValueDescriptor.CorrelationId => requestContext.correlationId
@@ -121,6 +147,8 @@ private[ror] object AuditSerializationHelper {
121147
case AuditFieldValueDescriptor.EsNodeName => eventData.requestContext.auditEnvironmentContext.esNodeName
122148
case AuditFieldValueDescriptor.EsClusterName => eventData.requestContext.auditEnvironmentContext.esClusterName
123149
case AuditFieldValueDescriptor.StaticText(text) => text
150+
case AuditFieldValueDescriptor.NumericValue(value) => value.bigDecimal
151+
case AuditFieldValueDescriptor.BooleanValue(value) => value
124152
case AuditFieldValueDescriptor.Combined(values) => values.map(resolver(eventData)).mkString
125153
}
126154
}
@@ -141,12 +169,24 @@ private[ror] object AuditSerializationHelper {
141169
}
142170

143171
private final case class EventData(matched: Boolean,
144-
finalState: String,
172+
finalState: FinalState,
145173
reason: String,
146174
duration: FiniteDuration,
147175
requestContext: AuditRequestContext,
148176
error: Option[Throwable])
149177

178+
private sealed trait FinalState
179+
180+
private object FinalState {
181+
case object Allowed extends FinalState
182+
183+
case object Forbidden extends FinalState
184+
185+
case object Errored extends FinalState
186+
187+
case object IndexNotExist extends FinalState
188+
}
189+
150190
sealed trait AllowedEventMode
151191

152192
object AllowedEventMode {
@@ -155,7 +195,15 @@ private[ror] object AuditSerializationHelper {
155195
final case class Include(types: Set[Verbosity]) extends AllowedEventMode
156196
}
157197

158-
final case class AuditFieldName(value: String)
198+
final case class AuditFieldPath private(path: List[String])
199+
200+
object AuditFieldPath {
201+
def apply(name: String): AuditFieldPath =
202+
AuditFieldPath(List(name))
203+
204+
def apply(head: String, tail: List[String]): AuditFieldPath =
205+
AuditFieldPath(head :: tail)
206+
}
159207

160208
sealed trait AuditFieldValueDescriptor
161209

@@ -166,6 +214,8 @@ private[ror] object AuditSerializationHelper {
166214

167215
case object FinalState extends AuditFieldValueDescriptor
168216

217+
case object EcsEventOutcome extends AuditFieldValueDescriptor
218+
169219
case object Reason extends AuditFieldValueDescriptor
170220

171221
@deprecated("[ROR] The User audit field value descriptor should not be used. Use LoggedUser or PresentedIdentity instead", "1.68.0")
@@ -185,6 +235,8 @@ private[ror] object AuditSerializationHelper {
185235

186236
case object ProcessingDurationMillis extends AuditFieldValueDescriptor
187237

238+
case object ProcessingDurationNanos extends AuditFieldValueDescriptor
239+
188240
// Identifiers
189241
case object Timestamp extends AuditFieldValueDescriptor
190242

@@ -230,6 +282,10 @@ private[ror] object AuditSerializationHelper {
230282

231283
final case class StaticText(value: String) extends AuditFieldValueDescriptor
232284

285+
final case class BooleanValue(value: Boolean) extends AuditFieldValueDescriptor
286+
287+
final case class NumericValue(value: BigDecimal) extends AuditFieldValueDescriptor
288+
233289
final case class Combined(values: List[AuditFieldValueDescriptor]) extends AuditFieldValueDescriptor
234290

235291
}
@@ -244,42 +300,42 @@ private[ror] object AuditSerializationHelper {
244300
case object FullRequestContentFields extends AuditFieldGroup
245301
}
246302

247-
private val commonFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map(
248-
AuditFieldName("match") -> AuditFieldValueDescriptor.IsMatched,
249-
AuditFieldName("block") -> AuditFieldValueDescriptor.Reason,
250-
AuditFieldName("id") -> AuditFieldValueDescriptor.Id,
251-
AuditFieldName("final_state") -> AuditFieldValueDescriptor.FinalState,
252-
AuditFieldName("@timestamp") -> AuditFieldValueDescriptor.Timestamp,
253-
AuditFieldName("correlation_id") -> AuditFieldValueDescriptor.CorrelationId,
254-
AuditFieldName("processingMillis") -> AuditFieldValueDescriptor.ProcessingDurationMillis,
255-
AuditFieldName("error_type") -> AuditFieldValueDescriptor.ErrorType,
256-
AuditFieldName("error_message") -> AuditFieldValueDescriptor.ErrorMessage,
257-
AuditFieldName("content_len") -> AuditFieldValueDescriptor.ContentLengthInBytes,
258-
AuditFieldName("content_len_kb") -> AuditFieldValueDescriptor.ContentLengthInKb,
259-
AuditFieldName("type") -> AuditFieldValueDescriptor.Type,
260-
AuditFieldName("origin") -> AuditFieldValueDescriptor.RemoteAddress,
261-
AuditFieldName("destination") -> AuditFieldValueDescriptor.LocalAddress,
262-
AuditFieldName("xff") -> AuditFieldValueDescriptor.XForwardedForHttpHeader,
263-
AuditFieldName("task_id") -> AuditFieldValueDescriptor.TaskId,
264-
AuditFieldName("req_method") -> AuditFieldValueDescriptor.HttpMethod,
265-
AuditFieldName("headers") -> AuditFieldValueDescriptor.HttpHeaderNames,
266-
AuditFieldName("path") -> AuditFieldValueDescriptor.HttpPath,
267-
AuditFieldName("user") -> AuditFieldValueDescriptor.User,
268-
AuditFieldName("logged_user") -> AuditFieldValueDescriptor.LoggedUser,
269-
AuditFieldName("presented_identity") -> AuditFieldValueDescriptor.PresentedIdentity,
270-
AuditFieldName("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser,
271-
AuditFieldName("action") -> AuditFieldValueDescriptor.Action,
272-
AuditFieldName("indices") -> AuditFieldValueDescriptor.InvolvedIndices,
273-
AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory
303+
private val commonFields: Map[AuditFieldPath, AuditFieldValueDescriptor] = Map(
304+
AuditFieldPath("match") -> AuditFieldValueDescriptor.IsMatched,
305+
AuditFieldPath("block") -> AuditFieldValueDescriptor.Reason,
306+
AuditFieldPath("id") -> AuditFieldValueDescriptor.Id,
307+
AuditFieldPath("final_state") -> AuditFieldValueDescriptor.FinalState,
308+
AuditFieldPath("@timestamp") -> AuditFieldValueDescriptor.Timestamp,
309+
AuditFieldPath("correlation_id") -> AuditFieldValueDescriptor.CorrelationId,
310+
AuditFieldPath("processingMillis") -> AuditFieldValueDescriptor.ProcessingDurationMillis,
311+
AuditFieldPath("error_type") -> AuditFieldValueDescriptor.ErrorType,
312+
AuditFieldPath("error_message") -> AuditFieldValueDescriptor.ErrorMessage,
313+
AuditFieldPath("content_len") -> AuditFieldValueDescriptor.ContentLengthInBytes,
314+
AuditFieldPath("content_len_kb") -> AuditFieldValueDescriptor.ContentLengthInKb,
315+
AuditFieldPath("type") -> AuditFieldValueDescriptor.Type,
316+
AuditFieldPath("origin") -> AuditFieldValueDescriptor.RemoteAddress,
317+
AuditFieldPath("destination") -> AuditFieldValueDescriptor.LocalAddress,
318+
AuditFieldPath("xff") -> AuditFieldValueDescriptor.XForwardedForHttpHeader,
319+
AuditFieldPath("task_id") -> AuditFieldValueDescriptor.TaskId,
320+
AuditFieldPath("req_method") -> AuditFieldValueDescriptor.HttpMethod,
321+
AuditFieldPath("headers") -> AuditFieldValueDescriptor.HttpHeaderNames,
322+
AuditFieldPath("path") -> AuditFieldValueDescriptor.HttpPath,
323+
AuditFieldPath("user") -> AuditFieldValueDescriptor.User,
324+
AuditFieldPath("logged_user") -> AuditFieldValueDescriptor.LoggedUser,
325+
AuditFieldPath("presented_identity") -> AuditFieldValueDescriptor.PresentedIdentity,
326+
AuditFieldPath("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser,
327+
AuditFieldPath("action") -> AuditFieldValueDescriptor.Action,
328+
AuditFieldPath("indices") -> AuditFieldValueDescriptor.InvolvedIndices,
329+
AuditFieldPath("acl_history") -> AuditFieldValueDescriptor.AclHistory
274330
)
275331

276-
private val esEnvironmentFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map(
277-
AuditFieldName("es_node_name") -> AuditFieldValueDescriptor.EsNodeName,
278-
AuditFieldName("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName
332+
private val esEnvironmentFields: Map[AuditFieldPath, AuditFieldValueDescriptor] = Map(
333+
AuditFieldPath("es_node_name") -> AuditFieldValueDescriptor.EsNodeName,
334+
AuditFieldPath("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName
279335
)
280336

281-
private val requestContentFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map(
282-
AuditFieldName("content") -> AuditFieldValueDescriptor.Content
337+
private val requestContentFields: Map[AuditFieldPath, AuditFieldValueDescriptor] = Map(
338+
AuditFieldPath("content") -> AuditFieldValueDescriptor.Content
283339
)
284340

285341
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* This file is part of ReadonlyREST.
3+
*
4+
* ReadonlyREST is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* ReadonlyREST is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with ReadonlyREST. If not, see http://www.gnu.org/licenses/
16+
*/
17+
package tech.beshu.ror.accesscontrol.audit
18+
19+
import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldPath, AuditFieldValueDescriptor}
20+
21+
object AuditFieldUtils {
22+
def fields(values: ((AuditFieldPath, AuditFieldValueDescriptor) | Map[AuditFieldPath, AuditFieldValueDescriptor])*): Map[AuditFieldPath, AuditFieldValueDescriptor] =
23+
values.flatMap(toMap).toMap
24+
25+
def withPrefix(prefix: String)(
26+
values: ((AuditFieldPath, AuditFieldValueDescriptor) | Map[AuditFieldPath, AuditFieldValueDescriptor])*
27+
): Map[AuditFieldPath, AuditFieldValueDescriptor] =
28+
withPrefix(prefix, values.flatMap(toMap).toMap)
29+
30+
private def withPrefix(prefix: String,
31+
values: Map[AuditFieldPath, AuditFieldValueDescriptor]): Map[AuditFieldPath, AuditFieldValueDescriptor] = {
32+
values.map { case (path, desc) =>
33+
val newPath = AuditFieldPath(prefix, path.path)
34+
newPath -> desc
35+
}
36+
}
37+
38+
private def toMap(value: (AuditFieldPath, AuditFieldValueDescriptor) | Map[AuditFieldPath, AuditFieldValueDescriptor]): Map[AuditFieldPath, AuditFieldValueDescriptor] = {
39+
value match {
40+
case (path: AuditFieldPath, value: AuditFieldValueDescriptor) =>
41+
Map(path -> value)
42+
case values: Map[AuditFieldPath, AuditFieldValueDescriptor] =>
43+
values
44+
}
45+
}
46+
}

core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDescriptorParser.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ object AuditFieldValueDescriptorParser extends Logging {
2828
private val key = P.charsWhile(c => c != '{' && c != '}')
2929

3030
private val placeholder: P[Either[String, AuditFieldValueDescriptor]] =
31-
(lbrace *> key <* rbrace).map(k => deserializerAuditFieldValueDescriptor(k.trim.toUpperCase).toRight(k))
31+
(lbrace *> key <* rbrace).map(k => deserializerAuditFieldValueDescriptor(k.trim).toRight(k))
3232

3333
private val text: P[AuditFieldValueDescriptor] =
3434
P.charsWhile(_ != '{').map(AuditFieldValueDescriptor.StaticText.apply)
@@ -59,6 +59,7 @@ object AuditFieldValueDescriptorParser extends Logging {
5959
str.toUpperCase match {
6060
case "IS_MATCHED" => Some(AuditFieldValueDescriptor.IsMatched)
6161
case "FINAL_STATE" => Some(AuditFieldValueDescriptor.FinalState)
62+
case "ECS_EVENT_OUTCOME" => Some(AuditFieldValueDescriptor.EcsEventOutcome)
6263
case "REASON" => Some(AuditFieldValueDescriptor.Reason)
6364
case "USER" =>
6465
logger.warn(
@@ -75,6 +76,7 @@ object AuditFieldValueDescriptorParser extends Logging {
7576
case "INVOLVED_INDICES" => Some(AuditFieldValueDescriptor.InvolvedIndices)
7677
case "ACL_HISTORY" => Some(AuditFieldValueDescriptor.AclHistory)
7778
case "PROCESSING_DURATION_MILLIS" => Some(AuditFieldValueDescriptor.ProcessingDurationMillis)
79+
case "PROCESSING_DURATION_NANOS" => Some(AuditFieldValueDescriptor.ProcessingDurationNanos)
7880
case "TIMESTAMP" => Some(AuditFieldValueDescriptor.Timestamp)
7981
case "ID" => Some(AuditFieldValueDescriptor.Id)
8082
case "CORRELATION_ID" => Some(AuditFieldValueDescriptor.CorrelationId)

core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ package tech.beshu.ror.accesscontrol.audit.configurable
1818

1919
import org.json.JSONObject
2020
import tech.beshu.ror.audit.utils.AuditSerializationHelper
21-
import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor}
21+
import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldPath, AuditFieldValueDescriptor}
2222
import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext}
2323

2424
class ConfigurableAuditLogSerializer(val allowedEventMode: AllowedEventMode,
25-
val fields: Map[AuditFieldName, AuditFieldValueDescriptor]) extends AuditLogSerializer {
25+
val fields: Map[AuditFieldPath, AuditFieldValueDescriptor]) extends AuditLogSerializer {
2626

2727
override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] =
2828
AuditSerializationHelper.serialize(responseContext, fields, allowedEventMode)

0 commit comments

Comments
 (0)