From 549a1b46b9748fe38d7023e51c988a823a4ad832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 8 Jul 2025 19:56:01 +0200 Subject: [PATCH 01/27] qs --- .../instances/BaseAuditLogSerializer.scala | 176 ++++++++++++++++++ .../DefaultAuditLogSerializerV1.scala | 108 ++++------- .../DefaultAuditLogSerializerV2.scala | 23 ++- .../instances/QueryAuditLogSerializerV1.scala | 20 +- .../instances/QueryAuditLogSerializerV2.scala | 20 +- ...ReportingAllEventsAuditLogSerializer.scala | 29 +++ 6 files changed, 279 insertions(+), 97 deletions(-) create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala new file mode 100644 index 0000000000..f3d02853c4 --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala @@ -0,0 +1,176 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit.instances + +import org.json.JSONObject +import tech.beshu.ror.audit.AuditResponseContext.* +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditRequestContext, AuditResponseContext} + +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import scala.collection.JavaConverters.* +import scala.concurrent.duration.FiniteDuration + +object BaseAuditLogSerializer { + + private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("GMT")) + + def serialize(responseContext: AuditResponseContext, + environmentContext: AuditEnvironmentContext, + fields: Map[String, AuditValue], + allowedEventSerializationMode: AllowedEventSerializationMode): Option[JSONObject] = responseContext match { + case Allowed(requestContext, verbosity, reason) => + (verbosity, allowedEventSerializationMode) match { + case (Verbosity.Error, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) => + None + case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) | + (_, AllowedEventSerializationMode.AlwaysSerialize) => + Some(createEntry(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) + } + case ForbiddenBy(requestContext, _, reason) => + Some(createEntry(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None)) + case Forbidden(requestContext) => + Some(createEntry(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None)) + case RequestedIndexNotExist(requestContext) => + Some(createEntry(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None)) + case Errored(requestContext, cause) => + Some(createEntry(matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))) + } + + private def createEntry(matched: Boolean, + finalState: String, + reason: String, + duration: FiniteDuration, + requestContext: AuditRequestContext, + error: Option[Throwable]) = { + new JSONObject() + .put("match", matched) + .put("block", reason) + .put("id", requestContext.id) + .put("final_state", finalState) + .put("@timestamp", timestampFormatter.format(requestContext.timestamp)) + .put("correlation_id", requestContext.correlationId) + .put("processingMillis", duration.toMillis) + .put("error_type", error.map(_.getClass.getSimpleName).orNull) + .put("error_message", error.map(_.getMessage).orNull) + .put("content_len", requestContext.contentLength) + .put("content_len_kb", requestContext.contentLength / 1024) + .put("type", requestContext.`type`) + .put("origin", requestContext.remoteAddress) + .put("destination", requestContext.localAddress) + .put("xff", requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull) + .put("task_id", requestContext.taskId) + .put("req_method", requestContext.httpMethod) + .put("headers", requestContext.requestHeaders.names.asJava) + .put("path", requestContext.uriPath) + .put("user", SerializeUser.serialize(requestContext).orNull) + .put("impersonated_by", requestContext.impersonatedByUserName.orNull) + .put("action", requestContext.action) + .put("indices", if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava) + .put("acl_history", requestContext.history) + .mergeWith(requestContext.generalAuditEvents) + } + + private implicit class JsonObjectOps(val mainJson: JSONObject) { + def mergeWith(secondaryJson: JSONObject): JSONObject = { + jsonKeys(secondaryJson).foldLeft(mainJson) { + case (json, name) if !json.has(name) => + json.put(name, secondaryJson.get(name)) + case (json, _) => + json + } + } + + private def jsonKeys(json: JSONObject) = { + Option(JSONObject.getNames(json)).toList.flatten + } + } + + sealed trait AuditValue + + object AuditValue { + + // Rule + case object IsMatched extends AuditValue + + case object FinalState extends AuditValue + + case object Reason extends AuditValue + + case object User extends AuditValue + + case object ImpersonatedByUser extends AuditValue + + case object Action extends AuditValue + + case object InvolvedIndices extends AuditValue + + case object AclHistory extends AuditValue + + case object ProcessingDurationMillis extends AuditValue + + // Identifiers + case object Timestamp extends AuditValue + + case object Id extends AuditValue + + case object CorrelationId extends AuditValue + + case object TaskId extends AuditValue + + // Error details + case object ErrorType extends AuditValue + + case object ErrorMessage extends AuditValue + + case object Type extends AuditValue + + // HTTP protocol values + case object HttpMethod extends AuditValue + + case object HttpHeaderNames extends AuditValue + + case object HttpPath extends AuditValue + + case object XForwardedForHttpHeader extends AuditValue + + case object RemoteAddress extends AuditValue + + case object LocalAddress extends AuditValue + + case object Content extends AuditValue + + case object ContentLengthInBytes extends AuditValue + + case object ContentLengthInKb extends AuditValue + + // Environment + case object EsNodeName extends AuditValue + + case object EsClusterName extends AuditValue + + } + + sealed trait AllowedEventSerializationMode + + object AllowedEventSerializationMode { + case object SerializeOnlyEventsWithInfoLevelVerbose extends AllowedEventSerializationMode + + case object AlwaysSerialize extends AllowedEventSerializationMode + } + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index fabe3440ea..ebe0e39090 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -17,82 +17,42 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.AuditResponseContext._ -import tech.beshu.ror.audit.{AuditLogSerializer, AuditRequestContext, AuditResponseContext} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import scala.collection.JavaConverters._ -import scala.concurrent.duration.FiniteDuration +class DefaultAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { -class DefaultAuditLogSerializerV1 extends AuditLogSerializer { + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV1AuditFields, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) - private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("GMT")) - - override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = responseContext match { - case Allowed(requestContext, verbosity, reason) => - verbosity match { - case Verbosity.Info => - Some(createEntry(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) - case Verbosity.Error => - None - } - case ForbiddenBy(requestContext, _, reason) => - Some(createEntry(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None)) - case Forbidden(requestContext) => - Some(createEntry(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None)) - case RequestedIndexNotExist(requestContext) => - Some(createEntry(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None)) - case Errored(requestContext, cause) => - Some(createEntry(matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))) - } - - private def createEntry(matched: Boolean, - finalState: String, - reason: String, - duration: FiniteDuration, - requestContext: AuditRequestContext, - error: Option[Throwable]) = { - new JSONObject() - .put("match", matched) - .put("block", reason) - .put("id", requestContext.id) - .put("final_state", finalState) - .put("@timestamp", timestampFormatter.format(requestContext.timestamp)) - .put("correlation_id", requestContext.correlationId) - .put("processingMillis", duration.toMillis) - .put("error_type", error.map(_.getClass.getSimpleName).orNull) - .put("error_message", error.map(_.getMessage).orNull) - .put("content_len", requestContext.contentLength) - .put("content_len_kb", requestContext.contentLength / 1024) - .put("type", requestContext.`type`) - .put("origin", requestContext.remoteAddress) - .put("destination", requestContext.localAddress) - .put("xff", requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull) - .put("task_id", requestContext.taskId) - .put("req_method", requestContext.httpMethod) - .put("headers", requestContext.requestHeaders.names.asJava) - .put("path", requestContext.uriPath) - .put("user", SerializeUser.serialize(requestContext).orNull) - .put("impersonated_by", requestContext.impersonatedByUserName.orNull) - .put("action", requestContext.action) - .put("indices", if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava) - .put("acl_history", requestContext.history) - .mergeWith(requestContext.generalAuditEvents) - } - - private implicit class JsonObjectOps(val mainJson: JSONObject) { - def mergeWith(secondaryJson: JSONObject): JSONObject = { - jsonKeys(secondaryJson).foldLeft(mainJson) { - case (json, name) if !json.has(name) => - json.put(name, secondaryJson.get(name)) - case (json, _) => - json - } - } +} - private def jsonKeys(json: JSONObject) = { - Option(JSONObject.getNames(json)).toList.flatten - } - } +object DefaultAuditLogSerializerV1 { + val defaultV1AuditFields: Map[String, AuditValue] = Map( + "match" -> AuditValue.IsMatched, + "block" -> AuditValue.Reason, + "id" -> AuditValue.Id, + "final_state" -> AuditValue.FinalState, + "@timestamp" -> AuditValue.Timestamp, + "correlation_id" -> AuditValue.CorrelationId, + "processingMillis" -> AuditValue.ProcessingDurationMillis, + "error_type" -> AuditValue.ErrorType, + "error_message" -> AuditValue.ErrorMessage, + "content_len" -> AuditValue.ContentLengthInBytes, + "content_len_kb" -> AuditValue.ContentLengthInKb, + "type" -> AuditValue.Type, + "origin" -> AuditValue.RemoteAddress, + "destination" -> AuditValue.LocalAddress, + "xff" -> AuditValue.XForwardedForHttpHeader, + "task_id" -> AuditValue.TaskId, + "req_method" -> AuditValue.HttpMethod, + "headers" -> AuditValue.HttpHeaderNames, + "path" -> AuditValue.HttpPath, + "user" -> AuditValue.User, + "impersonated_by" -> AuditValue.ImpersonatedByUser, + "action" -> AuditValue.Action, + "indices" -> AuditValue.InvolvedIndices, + "acl_history" -> AuditValue.AclHistory, + ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index 9772b5e6ee..c9226b9d12 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -17,16 +17,21 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} -class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializerV1 { +class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { - override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = { - lazy val additionalFields = Map( - "es_node_name" -> environmentContext.esNodeName, - "es_cluster_name" -> environmentContext.esClusterName + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV2AuditFields, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) + +} + +object DefaultAuditLogSerializerV2 { + val defaultV2AuditFields: Map[String, AuditValue] = + DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map( + "es_node_name" -> AuditValue.EsNodeName, + "es_cluster_name" -> AuditValue.EsClusterName, ) - super.onResponse(responseContext) - .map(additionalFields.foldLeft(_) { case (soFar, (key, value)) => soFar.put(key, value) }) - } } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index 4937c4413e..66f454adf0 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -17,12 +17,18 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.AuditResponseContext +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1.queryV1AuditFields +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} -class QueryAuditLogSerializerV1 extends DefaultAuditLogSerializerV1 { +class QueryAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { - override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = { - super.onResponse(responseContext) - .map(_.put("content", responseContext.requestContext.content)) - } -} \ No newline at end of file + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV1AuditFields, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) + +} + +object QueryAuditLogSerializerV1 { + val queryV1AuditFields: Map[String, AuditValue] = + DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map("content" -> AuditValue.Content) +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index 28d47a69da..c1e212c18c 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -17,12 +17,18 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} -class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializerV2(environmentContext) { +class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { - override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = { - super.onResponse(responseContext) - .map(_.put("content", responseContext.requestContext.content)) - } -} \ No newline at end of file + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV2AuditFields, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) + +} + +object QueryAuditLogSerializerV2 { + val queryV2AuditFields: Map[String, AuditValue] = + DefaultAuditLogSerializerV2.defaultV2AuditFields ++ Map("content" -> AuditValue.Content) +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala new file mode 100644 index 0000000000..1191e7c587 --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala @@ -0,0 +1,29 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit.instances + +import org.json.JSONObject +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} + +class ReportingAllEventsAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { + + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV2AuditFields, AllowedEventSerializationMode.AlwaysSerialize) + +} From 905499228e39d829d6d12be96459a27a38802980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 12 Jul 2025 21:12:48 +0200 Subject: [PATCH 02/27] qs --- .../instances/BaseAuditLogSerializer.scala | 70 +++++++++------- .../ConfigurableQueryAuditLogSerializer.scala | 30 +++++++ ...AllEventsWithQueryAuditLogSerializer.scala | 29 +++++++ .../decoders/AuditingSettingsDecoder.scala | 19 +++++ .../LocalClusterAuditingToolsSuite.scala | 81 ++++++++++++++++++- 5 files changed, 195 insertions(+), 34 deletions(-) create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala index f3d02853c4..93a041ccf2 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala @@ -39,49 +39,57 @@ object BaseAuditLogSerializer { None case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) | (_, AllowedEventSerializationMode.AlwaysSerialize) => - Some(createEntry(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) + Some(createEntry(fields, environmentContext, matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) } case ForbiddenBy(requestContext, _, reason) => - Some(createEntry(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None)) + Some(createEntry(fields, environmentContext, matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None)) case Forbidden(requestContext) => - Some(createEntry(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None)) + Some(createEntry(fields, environmentContext, matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None)) case RequestedIndexNotExist(requestContext) => - Some(createEntry(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None)) + Some(createEntry(fields, environmentContext, matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None)) case Errored(requestContext, cause) => - Some(createEntry(matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))) + Some(createEntry(fields, environmentContext, matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))) } - private def createEntry(matched: Boolean, + private def createEntry(fields: Map[String, AuditValue], + environmentContext: AuditEnvironmentContext, + matched: Boolean, finalState: String, reason: String, duration: FiniteDuration, requestContext: AuditRequestContext, error: Option[Throwable]) = { - new JSONObject() - .put("match", matched) - .put("block", reason) - .put("id", requestContext.id) - .put("final_state", finalState) - .put("@timestamp", timestampFormatter.format(requestContext.timestamp)) - .put("correlation_id", requestContext.correlationId) - .put("processingMillis", duration.toMillis) - .put("error_type", error.map(_.getClass.getSimpleName).orNull) - .put("error_message", error.map(_.getMessage).orNull) - .put("content_len", requestContext.contentLength) - .put("content_len_kb", requestContext.contentLength / 1024) - .put("type", requestContext.`type`) - .put("origin", requestContext.remoteAddress) - .put("destination", requestContext.localAddress) - .put("xff", requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull) - .put("task_id", requestContext.taskId) - .put("req_method", requestContext.httpMethod) - .put("headers", requestContext.requestHeaders.names.asJava) - .put("path", requestContext.uriPath) - .put("user", SerializeUser.serialize(requestContext).orNull) - .put("impersonated_by", requestContext.impersonatedByUserName.orNull) - .put("action", requestContext.action) - .put("indices", if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava) - .put("acl_history", requestContext.history) + val resolvedFields: Map[String, Any] = fields.view.mapValues { + case AuditValue.IsMatched => matched + case AuditValue.FinalState => finalState + case AuditValue.Reason => reason + case AuditValue.User => SerializeUser.serialize(requestContext).orNull + case AuditValue.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull + case AuditValue.Action => requestContext.action + case AuditValue.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava + case AuditValue.AclHistory => requestContext.history + case AuditValue.ProcessingDurationMillis => duration.toMillis + case AuditValue.Timestamp => timestampFormatter.format(requestContext.timestamp) + case AuditValue.Id => requestContext.id + case AuditValue.CorrelationId => requestContext.correlationId + case AuditValue.TaskId => requestContext.taskId + case AuditValue.ErrorType => error.map(_.getClass.getSimpleName).orNull + case AuditValue.ErrorMessage => error.map(_.getMessage).orNull + case AuditValue.Type => requestContext.`type` + case AuditValue.HttpMethod => requestContext.httpMethod + case AuditValue.HttpHeaderNames => requestContext.requestHeaders.names.asJava + case AuditValue.HttpPath => requestContext.uriPath + case AuditValue.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull + case AuditValue.RemoteAddress => requestContext.remoteAddress + case AuditValue.LocalAddress => requestContext.localAddress + case AuditValue.Content => requestContext.content + case AuditValue.ContentLengthInBytes => requestContext.contentLength + case AuditValue.ContentLengthInKb => requestContext.contentLength / 1024 + case AuditValue.EsNodeName => environmentContext.esNodeName + case AuditValue.EsClusterName => environmentContext.esClusterName + }.toMap + resolvedFields + .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } .mergeWith(requestContext.generalAuditEvents) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala new file mode 100644 index 0000000000..589051a2a3 --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala @@ -0,0 +1,30 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit.instances + +import org.json.JSONObject +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} + +class ConfigurableQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext, + allowedEventSerializationMode: AllowedEventSerializationMode, + fields: Map[String, AuditValue]) extends DefaultAuditLogSerializer(environmentContext) { + + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + BaseAuditLogSerializer.serialize(responseContext, environmentContext, fields, allowedEventSerializationMode) + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala new file mode 100644 index 0000000000..d5eb3a12d5 --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala @@ -0,0 +1,29 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit.instances + +import org.json.JSONObject +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} + +class ReportingAllEventsWithQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { + + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV2AuditFields, AllowedEventSerializationMode.AlwaysSerialize) + +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 4c06893350..0393181214 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -32,6 +32,7 @@ import tech.beshu.ror.accesscontrol.factory.decoders.common.{lemonLabsUriDecoder import tech.beshu.ror.accesscontrol.utils.CirceOps.DecodingFailureOps import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator import tech.beshu.ror.audit.adapters.* +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* @@ -214,6 +215,10 @@ object AuditingSettingsDecoder extends Logging { val serializer = Try(clazz.getConstructor(classOf[AuditEnvironmentContext])) .map(_.newInstance(summon[AuditEnvironmentContext])) + .orElse( + Try(clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[Map[String, AuditValue]])) + .map(_.newInstance(summon[AuditEnvironmentContext], AllowedEventSerializationMode.AlwaysSerialize, Map.empty)) + ) .orElse(Try(clazz.getDeclaredConstructor()).map(_.newInstance())) .getOrElse( throw new IllegalStateException( @@ -241,6 +246,20 @@ object AuditingSettingsDecoder extends Logging { } .decoder + given allowedEventSerializationMode: Decoder[AllowedEventSerializationMode] = + SyncDecoderCreator + .from(Decoder.decodeString) + .map(_.toUpperCase) + .emapE[AllowedEventSerializationMode] { + case "SERIALIZE_ONLY_EVENTS_WITH_INFO_LEVEL_VERBOSE" => + Right(AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) + case "ALWAYS_SERIALIZE" => + Right(AllowedEventSerializationMode.AlwaysSerialize) + case other => + Left(AuditingSettingsCreationError(Message(s"Not supported AllowedEventSerializationMode $other"))) + } + .decoder + private given Decoder[AuditCluster.RemoteAuditCluster] = SyncDecoderCreator .from(Decoder.decodeNonEmptyList[Uri]) diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index 91f909e07c..83f5e6e66b 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -92,6 +92,81 @@ class LocalClusterAuditingToolsSuite } } } + "using ReportingAllEventsAuditLogSerializer" in { + val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) + + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ReportingAllEventsAuditLogSerializer") + performAndAssertExampleSearchRequest(indexManager) + + forEachAuditManager { adminAuditManager => + eventually { + val auditEntries = adminAuditManager.getEntries.force().jsons + assert(auditEntries.size >= 3) + + auditEntries.exists(entry => + entry("final_state").str == "ALLOWED" && + entry("user").str == "username" && + entry("block").str.contains("name: 'Rule 1'") && + Try(entry("es_node_name")).map(_.str) == Success("ROR_SINGLE_1") && + Try(entry("es_cluster_name")).map(_.str) == Success("ROR_SINGLE") && + entry.obj.get("content").isEmpty + ) shouldBe true + + auditEntries.exists(entry => entry("path").str == "/_readonlyrest/admin/refreshconfig/") shouldBe true + auditEntries.exists(entry => entry("path").str == "/audit_index/_search/") shouldBe true + } + } + } + "using ReportingAllEventsWithQueryAuditLogSerializer" in { + val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) + + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ReportingAllEventsWithQueryAuditLogSerializer") + performAndAssertExampleSearchRequest(indexManager) + + forEachAuditManager { adminAuditManager => + eventually { + val auditEntries = adminAuditManager.getEntries.force().jsons + assert(auditEntries.size >= 3) + + auditEntries.exists(entry => + entry("final_state").str == "ALLOWED" && + entry("user").str == "username" && + entry("block").str.contains("name: 'Rule 1'") && + Try(entry("es_node_name")).map(_.str) == Success("ROR_SINGLE_1") && + Try(entry("es_cluster_name")).map(_.str) == Success("ROR_SINGLE") && + Try(entry("content")).map(_.str) == Success("") + ) shouldBe true + + auditEntries.exists(entry => entry("path").str == "/_readonlyrest/admin/refreshconfig/") shouldBe true + auditEntries.exists(entry => entry("path").str == "/audit_index/_search/") shouldBe true + } + } + } + "using ConfigurableQueryAuditLogSerializer" in { + val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) + + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ConfigurableQueryAuditLogSerializer") + performAndAssertExampleSearchRequest(indexManager) + + forEachAuditManager { adminAuditManager => + eventually { + val auditEntries = adminAuditManager.getEntries.force().jsons + assert(auditEntries.size >= 3) + + auditEntries.exists(entry => + entry("final_state").str == "ALLOWED" && + entry("user").str == "username" && + entry("block").str.contains("name: 'Rule 1'") && + Try(entry("es_node_name")).map(_.str) == Success("ROR_SINGLE_1") && + Try(entry("es_cluster_name")).map(_.str) == Success("ROR_SINGLE") && + Try(entry("content")).map(_.str) == Success("") + ) shouldBe true + + auditEntries.exists(entry => entry("path").str == "/_readonlyrest/admin/refreshconfig/") shouldBe true + auditEntries.exists(entry => entry("path").str == "/audit_index/_search/") shouldBe true + } + } + } } } @@ -102,9 +177,9 @@ class LocalClusterAuditingToolsSuite private def updateRorConfigToUseSerializer(serializer: String) = { val initialConfig = getResourceContent(rorConfigFileName) - val serializerUsedInOriginalConfigFile = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" - val firstModifiedConfig = initialConfig.replace(serializerUsedInOriginalConfigFile, serializer) - rorApiManager.updateRorInIndexConfig(firstModifiedConfig).forceOKStatusOrConfigAlreadyLoaded() + val serializerUsedInOriginalConfigFile = """ serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""" + val modifiedConfig = initialConfig.replace(serializerUsedInOriginalConfigFile, s""" serializer: "$serializer"""") + rorApiManager.updateRorInIndexConfig(modifiedConfig).forceOKStatusOrConfigAlreadyLoaded() rorApiManager.reloadRorConfig().force() } } From 2fa284c7ddccb9c5ef0d94f92208b74919f0bb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 22 Jul 2025 00:29:45 +0200 Subject: [PATCH 03/27] qs --- audit/build.gradle | 1 + .../instances/BaseAuditLogSerializer.scala | 13 +++- .../ConfigurableQueryAuditLogSerializer.scala | 6 +- .../decoders/AuditingSettingsDecoder.scala | 61 +++++++++++++------ .../enabled_auditing_tools/readonlyrest.yml | 8 +++ .../readonlyrest_audit_index.yml | 4 ++ .../LocalClusterAuditingToolsSuite.scala | 32 +++++----- .../suites/base/BaseAuditingToolsSuite.scala | 3 + 8 files changed, 87 insertions(+), 41 deletions(-) diff --git a/audit/build.gradle b/audit/build.gradle index 1d6be5a20f..bb662e1e62 100644 --- a/audit/build.gradle +++ b/audit/build.gradle @@ -63,6 +63,7 @@ crossBuild { dependencies { implementation group: 'org.json', name: 'json', version: '20231013' + implementation group: 'com.beachape', name: 'enumeratum_3', version: '1.7.5' crossBuildV211Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.11.12' crossBuildV212Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.12.19' crossBuildV213Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.13.13' diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala index 93a041ccf2..fb4d575e44 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala @@ -16,6 +16,7 @@ */ package tech.beshu.ror.audit.instances +import enumeratum.* import org.json.JSONObject import tech.beshu.ror.audit.AuditResponseContext.* import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditRequestContext, AuditResponseContext} @@ -87,7 +88,9 @@ object BaseAuditLogSerializer { case AuditValue.ContentLengthInKb => requestContext.contentLength / 1024 case AuditValue.EsNodeName => environmentContext.esNodeName case AuditValue.EsClusterName => environmentContext.esClusterName - }.toMap + }.toMap ++ Map( + "@timestamp" -> timestampFormatter.format(requestContext.timestamp), + ) resolvedFields .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } .mergeWith(requestContext.generalAuditEvents) @@ -108,9 +111,11 @@ object BaseAuditLogSerializer { } } - sealed trait AuditValue + final case class Fields(value: Map[String, AuditValue]) - object AuditValue { + sealed trait AuditValue extends EnumEntry.UpperSnakecase + + object AuditValue extends Enum[AuditValue] { // Rule case object IsMatched extends AuditValue @@ -171,6 +176,8 @@ object BaseAuditLogSerializer { case object EsClusterName extends AuditValue + override def values: IndexedSeq[AuditValue] = findValues + } sealed trait AllowedEventSerializationMode diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala index 589051a2a3..bda38fa05b 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala @@ -17,14 +17,14 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue, Fields} import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} class ConfigurableQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext, allowedEventSerializationMode: AllowedEventSerializationMode, - fields: Map[String, AuditValue]) extends DefaultAuditLogSerializer(environmentContext) { + fields: Fields) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, fields, allowedEventSerializationMode) + BaseAuditLogSerializer.serialize(responseContext, environmentContext, fields.value, allowedEventSerializationMode) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 0393181214..6ce3e3908b 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -32,7 +32,8 @@ import tech.beshu.ror.accesscontrol.factory.decoders.common.{lemonLabsUriDecoder import tech.beshu.ror.accesscontrol.utils.CirceOps.DecodingFailureOps import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator import tech.beshu.ror.audit.adapters.* -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.AllowedEventSerializationMode.* +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue, Fields} import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* @@ -121,6 +122,10 @@ object AuditingSettingsDecoder extends Logging { given Decoder[LogBasedSink] = Decoder.instance { c => for { + serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] + given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) AlwaysSerialize else SerializeOnlyEventsWithInfoLevelVerbose) + fields <- c.downField("fields").as[Option[Map[String, AuditValue]]] + given Option[Fields] = fields.map(Fields.apply) logSerializerCreator <- c.downField("serializer").as[Option[AuditLogSerializer]] loggerName <- c.downField("logger_name").as[Option[RorAuditLoggerName]] } yield LogBasedSink( @@ -132,6 +137,10 @@ object AuditingSettingsDecoder extends Logging { given Decoder[EsIndexBasedSink] = Decoder.instance { c => for { auditIndexTemplate <- c.downField("index_template").as[Option[RorAuditIndexTemplate]] + serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] + given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) AlwaysSerialize else SerializeOnlyEventsWithInfoLevelVerbose) + fields <- c.downField("fields").as[Option[Map[String, AuditValue]]] + given Option[Fields] = fields.map(Fields.apply) customAuditSerializer <- c.downField("serializer").as[Option[AuditLogSerializer]] remoteAuditCluster <- c.downField("cluster").as[Option[AuditCluster.RemoteAuditCluster]] } yield EsIndexBasedSink( @@ -144,6 +153,10 @@ object AuditingSettingsDecoder extends Logging { given Decoder[EsDataStreamBasedSink] = Decoder.instance { c => for { rorAuditDataStream <- c.downField("data_stream").as[Option[RorAuditDataStream]] + serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] + given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) AlwaysSerialize else SerializeOnlyEventsWithInfoLevelVerbose) + fields <- c.downField("fields").as[Option[Map[String, AuditValue]]] + given Option[Fields] = fields.map(Fields.apply) customAuditSerializer <- c.downField("serializer").as[Option[AuditLogSerializer]] remoteAuditCluster <- c.downField("cluster").as[Option[AuditCluster.RemoteAuditCluster]] } yield EsDataStreamBasedSink( @@ -207,19 +220,34 @@ object AuditingSettingsDecoder extends Logging { .decoder @nowarn("cat=deprecation") - private given customAuditLogSerializer(using AuditEnvironmentContext): Decoder[AuditLogSerializer] = + private given customAuditLogSerializer(using + context: AuditEnvironmentContext, + allowedEventSerializationMode: Option[AllowedEventSerializationMode], + fields: Option[Fields]): Decoder[AuditLogSerializer] = SyncDecoderCreator .from(Decoder.decodeString) .emapE { fullClassName => val clazz = Class.forName(fullClassName) - val serializer = + + def createInstanceOfEnvironmentAwareSerializer() = { Try(clazz.getConstructor(classOf[AuditEnvironmentContext])) .map(_.newInstance(summon[AuditEnvironmentContext])) - .orElse( - Try(clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[Map[String, AuditValue]])) - .map(_.newInstance(summon[AuditEnvironmentContext], AllowedEventSerializationMode.AlwaysSerialize, Map.empty)) - ) - .orElse(Try(clazz.getDeclaredConstructor()).map(_.newInstance())) + } + + def createInstanceOfCustomizableSerializer() = { + Try(clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[Fields])) + .map(_.newInstance(context, allowedEventSerializationMode.get, fields.get)) + } + + def createInstanceOfSimpleSerializer() = { + Try(clazz.getDeclaredConstructor()) + .map(_.newInstance()) + } + + val serializer = + createInstanceOfSimpleSerializer() + .orElse(createInstanceOfEnvironmentAwareSerializer()) + .orElse(createInstanceOfCustomizableSerializer()) .getOrElse( throw new IllegalStateException( s"Class ${Class.forName(fullClassName).getName} is required to have either one (AuditEnvironmentContext) parameter constructor or constructor without parameters" @@ -246,19 +274,12 @@ object AuditingSettingsDecoder extends Logging { } .decoder - given allowedEventSerializationMode: Decoder[AllowedEventSerializationMode] = + given auditValueDecoder: Decoder[AuditValue] = { SyncDecoderCreator .from(Decoder.decodeString) - .map(_.toUpperCase) - .emapE[AllowedEventSerializationMode] { - case "SERIALIZE_ONLY_EVENTS_WITH_INFO_LEVEL_VERBOSE" => - Right(AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) - case "ALWAYS_SERIALIZE" => - Right(AllowedEventSerializationMode.AlwaysSerialize) - case other => - Left(AuditingSettingsCreationError(Message(s"Not supported AllowedEventSerializationMode $other"))) - } + .emap(AuditValue.withNameEither(_).left.map(_.getMessage)) .decoder + } private given Decoder[AuditCluster.RemoteAuditCluster] = SyncDecoderCreator @@ -323,6 +344,10 @@ object AuditingSettingsDecoder extends Logging { whenEnabled(c) { for { auditIndexTemplate <- decodeOptionalSetting[RorAuditIndexTemplate](c)("index_template", fallbackKey = "audit_index_template") + serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] + given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) AlwaysSerialize else SerializeOnlyEventsWithInfoLevelVerbose) + fields <- c.downField("fields").as[Option[Map[String, AuditValue]]] + given Option[Fields] = fields.map(Fields.apply) customAuditSerializer <- decodeOptionalSetting[AuditLogSerializer](c)("serializer", fallbackKey = "audit_serializer") remoteAuditCluster <- decodeOptionalSetting[AuditCluster.RemoteAuditCluster](c)("cluster", fallbackKey = "audit_cluster") } yield AuditingTool.AuditSettings( diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml index e413c7174d..d690d7eae9 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml @@ -6,9 +6,17 @@ readonlyrest: - type: index index_template: "'audit_index'" serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" + serialize_all_allowed_events: false + fields: + abrakadabra: ES_NODE_NAME + another_field: ES_CLUSTER_NAME - type: data_stream data_stream: "audit_data_stream" serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" + serialize_all_allowed_events: false + fields: + abrakadabra: ES_NODE_NAME + another_field: ES_CLUSTER_NAME access_control_rules: diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml index b9dd7d0084..94d3ce8bf7 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml @@ -6,6 +6,10 @@ readonlyrest: - type: index index_template: "'audit_index'" serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" + serialize_all_allowed_events: false + fields: + abrakadabra: ES_NODE_NAME + another_field: ES_CLUSTER_NAME access_control_rules: diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index 83f5e6e66b..fd689d1683 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -65,15 +65,10 @@ class LocalClusterAuditingToolsSuite updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") performAndAssertExampleSearchRequest(indexManager) - - updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2") - performAndAssertExampleSearchRequest(indexManager) - forEachAuditManager { adminAuditManager => eventually { val auditEntries = adminAuditManager.getEntries.force().jsons - auditEntries.size shouldBe 2 - + auditEntries.size shouldBe 1 auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && @@ -81,7 +76,16 @@ class LocalClusterAuditingToolsSuite entry.obj.get("es_node_name").isEmpty && entry.obj.get("es_cluster_name").isEmpty ) shouldBe true + } + } + + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2") + performAndAssertExampleSearchRequest(indexManager) + forEachAuditManager { adminAuditManager => + eventually { + val auditEntries = adminAuditManager.getEntries.force().jsons + auditEntries.size shouldBe 1 auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && @@ -127,7 +131,6 @@ class LocalClusterAuditingToolsSuite eventually { val auditEntries = adminAuditManager.getEntries.force().jsons assert(auditEntries.size >= 3) - auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && @@ -151,19 +154,12 @@ class LocalClusterAuditingToolsSuite forEachAuditManager { adminAuditManager => eventually { val auditEntries = adminAuditManager.getEntries.force().jsons - assert(auditEntries.size >= 3) + auditEntries.size shouldBe 1 auditEntries.exists(entry => - entry("final_state").str == "ALLOWED" && - entry("user").str == "username" && - entry("block").str.contains("name: 'Rule 1'") && - Try(entry("es_node_name")).map(_.str) == Success("ROR_SINGLE_1") && - Try(entry("es_cluster_name")).map(_.str) == Success("ROR_SINGLE") && - Try(entry("content")).map(_.str) == Success("") + entry("abrakadabra").str == "ROR_SINGLE_1" && + entry("another_field").str == "ROR_SINGLE" ) shouldBe true - - auditEntries.exists(entry => entry("path").str == "/_readonlyrest/admin/refreshconfig/") shouldBe true - auditEntries.exists(entry => entry("path").str == "/audit_index/_search/") shouldBe true } } } @@ -181,5 +177,7 @@ class LocalClusterAuditingToolsSuite val modifiedConfig = initialConfig.replace(serializerUsedInOriginalConfigFile, s""" serializer: "$serializer"""") rorApiManager.updateRorInIndexConfig(modifiedConfig).forceOKStatusOrConfigAlreadyLoaded() rorApiManager.reloadRorConfig().force() + Thread.sleep(1000) // We need to wait a little while (set to 1 second) after changing the serializers, because some last events could have been serialized using previous serializer + truncateAllAuditManagers() } } diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/base/BaseAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/base/BaseAuditingToolsSuite.scala index bd9bed414b..f3e1e1b157 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/base/BaseAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/base/BaseAuditingToolsSuite.scala @@ -74,6 +74,9 @@ trait BaseAuditingToolsSuite } } + protected def truncateAllAuditManagers(): Unit = + adminAuditManagers.values.foreach(_.truncate()) + implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = scaled(Span(15, Seconds)), interval = scaled(Span(100, Millis))) From 4ef563bb541bcaeb2d958b6d46c85e1412f2f6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 25 Jul 2025 20:45:55 +0200 Subject: [PATCH 04/27] qs --- .../instances/BaseAuditLogSerializer.scala | 197 +++++++++++------- .../ConfigurableQueryAuditLogSerializer.scala | 4 +- .../DefaultAuditLogSerializerV1.scala | 54 ++--- .../DefaultAuditLogSerializerV2.scala | 10 +- .../instances/QueryAuditLogSerializerV1.scala | 8 +- .../instances/QueryAuditLogSerializerV2.scala | 8 +- ...ReportingAllEventsAuditLogSerializer.scala | 2 +- ...AllEventsWithQueryAuditLogSerializer.scala | 2 +- .../decoders/AuditingSettingsDecoder.scala | 58 +++--- .../enabled_auditing_tools/readonlyrest.yml | 8 +- .../readonlyrest_audit_index.yml | 4 +- .../LocalClusterAuditingToolsSuite.scala | 13 +- 12 files changed, 209 insertions(+), 159 deletions(-) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala index fb4d575e44..dfcade2b90 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala @@ -32,14 +32,14 @@ object BaseAuditLogSerializer { def serialize(responseContext: AuditResponseContext, environmentContext: AuditEnvironmentContext, - fields: Map[String, AuditValue], + fields: Map[String, AuditFieldValue], allowedEventSerializationMode: AllowedEventSerializationMode): Option[JSONObject] = responseContext match { case Allowed(requestContext, verbosity, reason) => (verbosity, allowedEventSerializationMode) match { - case (Verbosity.Error, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) => + case (Verbosity.Error, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) => None - case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) | - (_, AllowedEventSerializationMode.AlwaysSerialize) => + case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) | + (_, AllowedEventSerializationMode.SerializeAllAllowedEvents) => Some(createEntry(fields, environmentContext, matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) } case ForbiddenBy(requestContext, _, reason) => @@ -52,7 +52,7 @@ object BaseAuditLogSerializer { Some(createEntry(fields, environmentContext, matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))) } - private def createEntry(fields: Map[String, AuditValue], + private def createEntry(fields: Map[String, AuditFieldValue], environmentContext: AuditEnvironmentContext, matched: Boolean, finalState: String, @@ -60,42 +60,54 @@ object BaseAuditLogSerializer { duration: FiniteDuration, requestContext: AuditRequestContext, error: Option[Throwable]) = { - val resolvedFields: Map[String, Any] = fields.view.mapValues { - case AuditValue.IsMatched => matched - case AuditValue.FinalState => finalState - case AuditValue.Reason => reason - case AuditValue.User => SerializeUser.serialize(requestContext).orNull - case AuditValue.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull - case AuditValue.Action => requestContext.action - case AuditValue.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava - case AuditValue.AclHistory => requestContext.history - case AuditValue.ProcessingDurationMillis => duration.toMillis - case AuditValue.Timestamp => timestampFormatter.format(requestContext.timestamp) - case AuditValue.Id => requestContext.id - case AuditValue.CorrelationId => requestContext.correlationId - case AuditValue.TaskId => requestContext.taskId - case AuditValue.ErrorType => error.map(_.getClass.getSimpleName).orNull - case AuditValue.ErrorMessage => error.map(_.getMessage).orNull - case AuditValue.Type => requestContext.`type` - case AuditValue.HttpMethod => requestContext.httpMethod - case AuditValue.HttpHeaderNames => requestContext.requestHeaders.names.asJava - case AuditValue.HttpPath => requestContext.uriPath - case AuditValue.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull - case AuditValue.RemoteAddress => requestContext.remoteAddress - case AuditValue.LocalAddress => requestContext.localAddress - case AuditValue.Content => requestContext.content - case AuditValue.ContentLengthInBytes => requestContext.contentLength - case AuditValue.ContentLengthInKb => requestContext.contentLength / 1024 - case AuditValue.EsNodeName => environmentContext.esNodeName - case AuditValue.EsClusterName => environmentContext.esClusterName - }.toMap ++ Map( + val resolvedFields: Map[String, Any] = Map( "@timestamp" -> timestampFormatter.format(requestContext.timestamp), - ) + ) ++ fields.view.mapValues(_.value.map(resolvePlaceholder(_, environmentContext, matched, finalState, reason, duration, requestContext, error)).mkString).toMap resolvedFields .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } .mergeWith(requestContext.generalAuditEvents) } + private def resolvePlaceholder(auditValue: AuditValuePlaceholder | String, + environmentContext: AuditEnvironmentContext, + matched: Boolean, + finalState: String, + reason: String, + duration: FiniteDuration, + requestContext: AuditRequestContext, + error: Option[Throwable]): Any = { + auditValue match { + case stringValue: String => stringValue + case AuditValuePlaceholder.IsMatched => matched + case AuditValuePlaceholder.FinalState => finalState + case AuditValuePlaceholder.Reason => reason + case AuditValuePlaceholder.User => SerializeUser.serialize(requestContext).orNull + case AuditValuePlaceholder.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull + case AuditValuePlaceholder.Action => requestContext.action + case AuditValuePlaceholder.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava + case AuditValuePlaceholder.AclHistory => requestContext.history + case AuditValuePlaceholder.ProcessingDurationMillis => duration.toMillis + case AuditValuePlaceholder.Timestamp => timestampFormatter.format(requestContext.timestamp) + case AuditValuePlaceholder.Id => requestContext.id + case AuditValuePlaceholder.CorrelationId => requestContext.correlationId + case AuditValuePlaceholder.TaskId => requestContext.taskId + case AuditValuePlaceholder.ErrorType => error.map(_.getClass.getSimpleName).orNull + case AuditValuePlaceholder.ErrorMessage => error.map(_.getMessage).orNull + case AuditValuePlaceholder.Type => requestContext.`type` + case AuditValuePlaceholder.HttpMethod => requestContext.httpMethod + case AuditValuePlaceholder.HttpHeaderNames => requestContext.requestHeaders.names.asJava + case AuditValuePlaceholder.HttpPath => requestContext.uriPath + case AuditValuePlaceholder.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull + case AuditValuePlaceholder.RemoteAddress => requestContext.remoteAddress + case AuditValuePlaceholder.LocalAddress => requestContext.localAddress + case AuditValuePlaceholder.Content => requestContext.content + case AuditValuePlaceholder.ContentLengthInBytes => requestContext.contentLength + case AuditValuePlaceholder.ContentLengthInKb => requestContext.contentLength / 1024 + case AuditValuePlaceholder.EsNodeName => environmentContext.esNodeName + case AuditValuePlaceholder.EsClusterName => environmentContext.esClusterName + } + } + private implicit class JsonObjectOps(val mainJson: JSONObject) { def mergeWith(secondaryJson: JSONObject): JSONObject = { jsonKeys(secondaryJson).foldLeft(mainJson) { @@ -111,81 +123,118 @@ object BaseAuditLogSerializer { } } - final case class Fields(value: Map[String, AuditValue]) + sealed trait AllowedEventSerializationMode + + object AllowedEventSerializationMode { + case object SerializeOnlyAllowedEventsWithInfoLevelVerbose extends AllowedEventSerializationMode + + case object SerializeAllAllowedEvents extends AllowedEventSerializationMode + } + + final case class AuditFields(value: Map[String, AuditFieldValue]) + + final case class AuditFieldValue private(value: List[AuditValuePlaceholder | String]) + + object AuditFieldValue { + + private val pattern = "\\{([^}]+)\\}".r + + def apply(placeholder: AuditValuePlaceholder): AuditFieldValue = AuditFieldValue(List(placeholder)) + + def fromString(str: String): Either[String, AuditFieldValue] = { + val matches = pattern.findAllMatchIn(str).toList + + val (parts, missing, lastIndex) = + matches.foldLeft((List.empty[AuditValuePlaceholder | String], List.empty[String], 0)) { + case ((partsAcc, missingAcc, lastEnd), m) => + val key = m.group(1) + val before = str.substring(lastEnd, m.start) + val partBefore = Option.when(before.nonEmpty)(before).toList - sealed trait AuditValue extends EnumEntry.UpperSnakecase + val (partAfter, newMissing) = AuditValuePlaceholder.withNameOption(key) match { + case Some(placeholder) => (List(placeholder), Nil) + case None => (Nil, List(key)) + } - object AuditValue extends Enum[AuditValue] { + (partsAcc ++ partBefore ++ partAfter, missingAcc ++ newMissing, m.end) + } + + val trailing = Option.when(lastIndex < str.length)(str.substring(lastIndex)).toList + val allParts = parts ++ trailing + + missing match { + case Nil => Right(AuditFieldValue(allParts)) + case missingList => Left(s"There are invalid placeholder values: ${missingList.mkString(", ")}") + } + } + + } + + sealed trait AuditValuePlaceholder extends EnumEntry.UpperSnakecase + + object AuditValuePlaceholder extends Enum[AuditValuePlaceholder] { // Rule - case object IsMatched extends AuditValue + case object IsMatched extends AuditValuePlaceholder - case object FinalState extends AuditValue + case object FinalState extends AuditValuePlaceholder - case object Reason extends AuditValue + case object Reason extends AuditValuePlaceholder - case object User extends AuditValue + case object User extends AuditValuePlaceholder - case object ImpersonatedByUser extends AuditValue + case object ImpersonatedByUser extends AuditValuePlaceholder - case object Action extends AuditValue + case object Action extends AuditValuePlaceholder - case object InvolvedIndices extends AuditValue + case object InvolvedIndices extends AuditValuePlaceholder - case object AclHistory extends AuditValue + case object AclHistory extends AuditValuePlaceholder - case object ProcessingDurationMillis extends AuditValue + case object ProcessingDurationMillis extends AuditValuePlaceholder // Identifiers - case object Timestamp extends AuditValue + case object Timestamp extends AuditValuePlaceholder - case object Id extends AuditValue + case object Id extends AuditValuePlaceholder - case object CorrelationId extends AuditValue + case object CorrelationId extends AuditValuePlaceholder - case object TaskId extends AuditValue + case object TaskId extends AuditValuePlaceholder // Error details - case object ErrorType extends AuditValue + case object ErrorType extends AuditValuePlaceholder - case object ErrorMessage extends AuditValue + case object ErrorMessage extends AuditValuePlaceholder - case object Type extends AuditValue + case object Type extends AuditValuePlaceholder // HTTP protocol values - case object HttpMethod extends AuditValue + case object HttpMethod extends AuditValuePlaceholder - case object HttpHeaderNames extends AuditValue + case object HttpHeaderNames extends AuditValuePlaceholder - case object HttpPath extends AuditValue + case object HttpPath extends AuditValuePlaceholder - case object XForwardedForHttpHeader extends AuditValue + case object XForwardedForHttpHeader extends AuditValuePlaceholder - case object RemoteAddress extends AuditValue + case object RemoteAddress extends AuditValuePlaceholder - case object LocalAddress extends AuditValue + case object LocalAddress extends AuditValuePlaceholder - case object Content extends AuditValue + case object Content extends AuditValuePlaceholder - case object ContentLengthInBytes extends AuditValue + case object ContentLengthInBytes extends AuditValuePlaceholder - case object ContentLengthInKb extends AuditValue + case object ContentLengthInKb extends AuditValuePlaceholder // Environment - case object EsNodeName extends AuditValue - - case object EsClusterName extends AuditValue + case object EsNodeName extends AuditValuePlaceholder - override def values: IndexedSeq[AuditValue] = findValues - - } + case object EsClusterName extends AuditValuePlaceholder - sealed trait AllowedEventSerializationMode - - object AllowedEventSerializationMode { - case object SerializeOnlyEventsWithInfoLevelVerbose extends AllowedEventSerializationMode + override def values: IndexedSeq[AuditValuePlaceholder] = findValues - case object AlwaysSerialize extends AllowedEventSerializationMode } } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala index bda38fa05b..596bb182f9 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala @@ -17,12 +17,12 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue, Fields} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditFields} import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} class ConfigurableQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext, allowedEventSerializationMode: AllowedEventSerializationMode, - fields: Fields) extends DefaultAuditLogSerializer(environmentContext) { + fields: AuditFields) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = BaseAuditLogSerializer.serialize(responseContext, environmentContext, fields.value, allowedEventSerializationMode) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index ebe0e39090..47aff2eecc 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -17,42 +17,42 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditValuePlaceholder} import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} class DefaultAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV1AuditFields, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) + BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV1AuditFields, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) } object DefaultAuditLogSerializerV1 { - val defaultV1AuditFields: Map[String, AuditValue] = Map( - "match" -> AuditValue.IsMatched, - "block" -> AuditValue.Reason, - "id" -> AuditValue.Id, - "final_state" -> AuditValue.FinalState, - "@timestamp" -> AuditValue.Timestamp, - "correlation_id" -> AuditValue.CorrelationId, - "processingMillis" -> AuditValue.ProcessingDurationMillis, - "error_type" -> AuditValue.ErrorType, - "error_message" -> AuditValue.ErrorMessage, - "content_len" -> AuditValue.ContentLengthInBytes, - "content_len_kb" -> AuditValue.ContentLengthInKb, - "type" -> AuditValue.Type, - "origin" -> AuditValue.RemoteAddress, - "destination" -> AuditValue.LocalAddress, - "xff" -> AuditValue.XForwardedForHttpHeader, - "task_id" -> AuditValue.TaskId, - "req_method" -> AuditValue.HttpMethod, - "headers" -> AuditValue.HttpHeaderNames, - "path" -> AuditValue.HttpPath, - "user" -> AuditValue.User, - "impersonated_by" -> AuditValue.ImpersonatedByUser, - "action" -> AuditValue.Action, - "indices" -> AuditValue.InvolvedIndices, - "acl_history" -> AuditValue.AclHistory, + val defaultV1AuditFields: Map[String, AuditFieldValue] = Map( + "match" -> AuditFieldValue(AuditValuePlaceholder.IsMatched), + "block" -> AuditFieldValue(AuditValuePlaceholder.Reason), + "id" -> AuditFieldValue(AuditValuePlaceholder.Id), + "final_state" -> AuditFieldValue(AuditValuePlaceholder.FinalState), + "@timestamp" -> AuditFieldValue(AuditValuePlaceholder.Timestamp), + "correlation_id" -> AuditFieldValue(AuditValuePlaceholder.CorrelationId), + "processingMillis" -> AuditFieldValue(AuditValuePlaceholder.ProcessingDurationMillis), + "error_type" -> AuditFieldValue(AuditValuePlaceholder.ErrorType), + "error_message" -> AuditFieldValue(AuditValuePlaceholder.ErrorMessage), + "content_len" -> AuditFieldValue(AuditValuePlaceholder.ContentLengthInBytes), + "content_len_kb" -> AuditFieldValue(AuditValuePlaceholder.ContentLengthInKb), + "type" -> AuditFieldValue(AuditValuePlaceholder.Type), + "origin" -> AuditFieldValue(AuditValuePlaceholder.RemoteAddress), + "destination" -> AuditFieldValue(AuditValuePlaceholder.LocalAddress), + "xff" -> AuditFieldValue(AuditValuePlaceholder.XForwardedForHttpHeader), + "task_id" -> AuditFieldValue(AuditValuePlaceholder.TaskId), + "req_method" -> AuditFieldValue(AuditValuePlaceholder.HttpMethod), + "headers" -> AuditFieldValue(AuditValuePlaceholder.HttpHeaderNames), + "path" -> AuditFieldValue(AuditValuePlaceholder.HttpPath), + "user" -> AuditFieldValue(AuditValuePlaceholder.User), + "impersonated_by" -> AuditFieldValue(AuditValuePlaceholder.ImpersonatedByUser), + "action" -> AuditFieldValue(AuditValuePlaceholder.Action), + "indices" -> AuditFieldValue(AuditValuePlaceholder.InvolvedIndices), + "acl_history" -> AuditFieldValue(AuditValuePlaceholder.AclHistory), ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index c9226b9d12..58ac1b1508 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -17,21 +17,21 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditValuePlaceholder} import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV2AuditFields, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) + BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV2AuditFields, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) } object DefaultAuditLogSerializerV2 { - val defaultV2AuditFields: Map[String, AuditValue] = + val defaultV2AuditFields: Map[String, AuditFieldValue] = DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map( - "es_node_name" -> AuditValue.EsNodeName, - "es_cluster_name" -> AuditValue.EsClusterName, + "es_node_name" -> AuditFieldValue(AuditValuePlaceholder.EsNodeName), + "es_cluster_name" -> AuditFieldValue(AuditValuePlaceholder.EsClusterName), ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index 66f454adf0..b4848af028 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -17,18 +17,18 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditValuePlaceholder} import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1.queryV1AuditFields import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} class QueryAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV1AuditFields, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) + BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV1AuditFields, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) } object QueryAuditLogSerializerV1 { - val queryV1AuditFields: Map[String, AuditValue] = - DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map("content" -> AuditValue.Content) + val queryV1AuditFields: Map[String, AuditFieldValue] = + DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map("content" -> AuditFieldValue(AuditValuePlaceholder.Content)) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index c1e212c18c..a9070e09d8 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -17,18 +17,18 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditValuePlaceholder} import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV2AuditFields, AllowedEventSerializationMode.SerializeOnlyEventsWithInfoLevelVerbose) + BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV2AuditFields, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) } object QueryAuditLogSerializerV2 { - val queryV2AuditFields: Map[String, AuditValue] = - DefaultAuditLogSerializerV2.defaultV2AuditFields ++ Map("content" -> AuditValue.Content) + val queryV2AuditFields: Map[String, AuditFieldValue] = + DefaultAuditLogSerializerV2.defaultV2AuditFields ++ Map("content" -> AuditFieldValue(AuditValuePlaceholder.Content)) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala index 1191e7c587..9349b92fb8 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala @@ -24,6 +24,6 @@ import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} class ReportingAllEventsAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV2AuditFields, AllowedEventSerializationMode.AlwaysSerialize) + BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV2AuditFields, AllowedEventSerializationMode.SerializeAllAllowedEvents) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala index d5eb3a12d5..dace3e1400 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala @@ -24,6 +24,6 @@ import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} class ReportingAllEventsWithQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV2AuditFields, AllowedEventSerializationMode.AlwaysSerialize) + BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV2AuditFields, AllowedEventSerializationMode.SerializeAllAllowedEvents) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 6ce3e3908b..31c015cf84 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -18,6 +18,7 @@ package tech.beshu.ror.accesscontrol.factory.decoders import cats.data.NonEmptyList import io.circe.{Decoder, HCursor} +import io.circe.Decoder.* import io.lemonlabs.uri.Uri import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.audit.AuditingTool @@ -33,7 +34,7 @@ import tech.beshu.ror.accesscontrol.utils.CirceOps.DecodingFailureOps import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator import tech.beshu.ror.audit.adapters.* import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.AllowedEventSerializationMode.* -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditValue, Fields} +import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditFields} import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* @@ -122,14 +123,10 @@ object AuditingSettingsDecoder extends Logging { given Decoder[LogBasedSink] = Decoder.instance { c => for { - serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] - given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) AlwaysSerialize else SerializeOnlyEventsWithInfoLevelVerbose) - fields <- c.downField("fields").as[Option[Map[String, AuditValue]]] - given Option[Fields] = fields.map(Fields.apply) - logSerializerCreator <- c.downField("serializer").as[Option[AuditLogSerializer]] + logSerializer <- c.as[Option[AuditLogSerializer]] loggerName <- c.downField("logger_name").as[Option[RorAuditLoggerName]] } yield LogBasedSink( - logSerializer = logSerializerCreator.getOrElse(LogBasedSink.default.logSerializer), + logSerializer = logSerializer.getOrElse(LogBasedSink.default.logSerializer), loggerName = loggerName.getOrElse(LogBasedSink.default.loggerName) ) } @@ -137,14 +134,10 @@ object AuditingSettingsDecoder extends Logging { given Decoder[EsIndexBasedSink] = Decoder.instance { c => for { auditIndexTemplate <- c.downField("index_template").as[Option[RorAuditIndexTemplate]] - serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] - given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) AlwaysSerialize else SerializeOnlyEventsWithInfoLevelVerbose) - fields <- c.downField("fields").as[Option[Map[String, AuditValue]]] - given Option[Fields] = fields.map(Fields.apply) - customAuditSerializer <- c.downField("serializer").as[Option[AuditLogSerializer]] + logSerializer <- c.as[Option[AuditLogSerializer]] remoteAuditCluster <- c.downField("cluster").as[Option[AuditCluster.RemoteAuditCluster]] } yield EsIndexBasedSink( - customAuditSerializer.getOrElse(EsIndexBasedSink.default.logSerializer), + logSerializer.getOrElse(EsIndexBasedSink.default.logSerializer), auditIndexTemplate.getOrElse(EsIndexBasedSink.default.rorAuditIndexTemplate), remoteAuditCluster.getOrElse(EsIndexBasedSink.default.auditCluster), ) @@ -153,14 +146,10 @@ object AuditingSettingsDecoder extends Logging { given Decoder[EsDataStreamBasedSink] = Decoder.instance { c => for { rorAuditDataStream <- c.downField("data_stream").as[Option[RorAuditDataStream]] - serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] - given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) AlwaysSerialize else SerializeOnlyEventsWithInfoLevelVerbose) - fields <- c.downField("fields").as[Option[Map[String, AuditValue]]] - given Option[Fields] = fields.map(Fields.apply) - customAuditSerializer <- c.downField("serializer").as[Option[AuditLogSerializer]] + logSerializer <- c.as[Option[AuditLogSerializer]] remoteAuditCluster <- c.downField("cluster").as[Option[AuditCluster.RemoteAuditCluster]] } yield EsDataStreamBasedSink( - customAuditSerializer.getOrElse(EsDataStreamBasedSink.default.logSerializer), + logSerializer.getOrElse(EsDataStreamBasedSink.default.logSerializer), rorAuditDataStream.getOrElse(EsDataStreamBasedSink.default.rorAuditDataStream), remoteAuditCluster.getOrElse(EsDataStreamBasedSink.default.auditCluster), ) @@ -219,11 +208,20 @@ object AuditingSettingsDecoder extends Logging { } .decoder + given auditLogSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + for { + serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] + given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) SerializeAllAllowedEvents else SerializeOnlyAllowedEventsWithInfoLevelVerbose) + fields <- c.downField("fields").as[Option[Map[String, AuditFieldValue]]] + given Option[AuditFields] = fields.map(AuditFields.apply) + serializer <- c.downField("serializer").as[Option[AuditLogSerializer]](decodeOption(auditLogSerializerInstanceDecoder)) + } yield serializer + } + @nowarn("cat=deprecation") - private given customAuditLogSerializer(using - context: AuditEnvironmentContext, - allowedEventSerializationMode: Option[AllowedEventSerializationMode], - fields: Option[Fields]): Decoder[AuditLogSerializer] = + given auditLogSerializerInstanceDecoder(using context: AuditEnvironmentContext, + allowedEventSerializationMode: Option[AllowedEventSerializationMode], + fields: Option[AuditFields]): Decoder[AuditLogSerializer] = SyncDecoderCreator .from(Decoder.decodeString) .emapE { fullClassName => @@ -235,7 +233,7 @@ object AuditingSettingsDecoder extends Logging { } def createInstanceOfCustomizableSerializer() = { - Try(clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[Fields])) + Try(clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[AuditFields])) .map(_.newInstance(context, allowedEventSerializationMode.get, fields.get)) } @@ -274,10 +272,10 @@ object AuditingSettingsDecoder extends Logging { } .decoder - given auditValueDecoder: Decoder[AuditValue] = { + given auditValueDecoder: Decoder[AuditFieldValue] = { SyncDecoderCreator .from(Decoder.decodeString) - .emap(AuditValue.withNameEither(_).left.map(_.getMessage)) + .emap(AuditFieldValue.fromString(_)) .decoder } @@ -344,17 +342,13 @@ object AuditingSettingsDecoder extends Logging { whenEnabled(c) { for { auditIndexTemplate <- decodeOptionalSetting[RorAuditIndexTemplate](c)("index_template", fallbackKey = "audit_index_template") - serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] - given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) AlwaysSerialize else SerializeOnlyEventsWithInfoLevelVerbose) - fields <- c.downField("fields").as[Option[Map[String, AuditValue]]] - given Option[Fields] = fields.map(Fields.apply) - customAuditSerializer <- decodeOptionalSetting[AuditLogSerializer](c)("serializer", fallbackKey = "audit_serializer") + logSerializer <- c.as[Option[AuditLogSerializer]] remoteAuditCluster <- decodeOptionalSetting[AuditCluster.RemoteAuditCluster](c)("cluster", fallbackKey = "audit_cluster") } yield AuditingTool.AuditSettings( auditSinks = NonEmptyList.one( AuditSink.Enabled( EsIndexBasedSink( - logSerializer = customAuditSerializer.getOrElse(EsIndexBasedSink.default.logSerializer), + logSerializer = logSerializer.getOrElse(EsIndexBasedSink.default.logSerializer), rorAuditIndexTemplate = auditIndexTemplate.getOrElse(EsIndexBasedSink.default.rorAuditIndexTemplate), auditCluster = remoteAuditCluster.getOrElse(EsIndexBasedSink.default.auditCluster), ) diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml index d690d7eae9..f57da33aad 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml @@ -8,15 +8,15 @@ readonlyrest: serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" serialize_all_allowed_events: false fields: - abrakadabra: ES_NODE_NAME - another_field: ES_CLUSTER_NAME + abrakadabra: "{ES_NODE_NAME} with suffix" + another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" - type: data_stream data_stream: "audit_data_stream" serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" serialize_all_allowed_events: false fields: - abrakadabra: ES_NODE_NAME - another_field: ES_CLUSTER_NAME + abrakadabra: "{ES_NODE_NAME} with suffix" + another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" access_control_rules: diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml index 94d3ce8bf7..d8847cd60a 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml @@ -8,8 +8,8 @@ readonlyrest: serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" serialize_all_allowed_events: false fields: - abrakadabra: ES_NODE_NAME - another_field: ES_CLUSTER_NAME + abrakadabra: "{ES_NODE_NAME} with suffix" + another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" access_control_rules: diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index fd689d1683..786eaff9dc 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -95,6 +95,7 @@ class LocalClusterAuditingToolsSuite ) shouldBe true } } + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") } "using ReportingAllEventsAuditLogSerializer" in { val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) @@ -120,6 +121,9 @@ class LocalClusterAuditingToolsSuite auditEntries.exists(entry => entry("path").str == "/audit_index/_search/") shouldBe true } } + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") + // This test uses serializer, that reports all events. We need to wait a moment, to ensure that there will be no more events using that serializer + Thread.sleep(3000) } "using ReportingAllEventsWithQueryAuditLogSerializer" in { val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) @@ -144,6 +148,9 @@ class LocalClusterAuditingToolsSuite auditEntries.exists(entry => entry("path").str == "/audit_index/_search/") shouldBe true } } + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") + // This test uses serializer, that reports all events. We need to wait a moment, to ensure that there will be no more events using that serializer + Thread.sleep(3000) } "using ConfigurableQueryAuditLogSerializer" in { val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) @@ -157,11 +164,12 @@ class LocalClusterAuditingToolsSuite auditEntries.size shouldBe 1 auditEntries.exists(entry => - entry("abrakadabra").str == "ROR_SINGLE_1" && - entry("another_field").str == "ROR_SINGLE" + entry("abrakadabra").str == "ROR_SINGLE_1 with suffix" && + entry("another_field").str == "ROR_SINGLE GET" ) shouldBe true } } + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") } } } @@ -177,7 +185,6 @@ class LocalClusterAuditingToolsSuite val modifiedConfig = initialConfig.replace(serializerUsedInOriginalConfigFile, s""" serializer: "$serializer"""") rorApiManager.updateRorInIndexConfig(modifiedConfig).forceOKStatusOrConfigAlreadyLoaded() rorApiManager.reloadRorConfig().force() - Thread.sleep(1000) // We need to wait a little while (set to 1 second) after changing the serializers, because some last events could have been serialized using previous serializer truncateAllAuditManagers() } } From ab6a55c317b953497da7c658be7a06c88abd9782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 25 Jul 2025 20:51:56 +0200 Subject: [PATCH 05/27] qs --- .../ror_audit/enabled_auditing_tools/readonlyrest.yml | 4 ++-- .../enabled_auditing_tools/readonlyrest_audit_index.yml | 2 +- .../suites/audit/LocalClusterAuditingToolsSuite.scala | 7 +++---- .../integration/suites/base/BaseAuditingToolsSuite.scala | 3 --- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml index f57da33aad..a9c9b636cc 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml @@ -8,14 +8,14 @@ readonlyrest: serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" serialize_all_allowed_events: false fields: - abrakadabra: "{ES_NODE_NAME} with suffix" + node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" - type: data_stream data_stream: "audit_data_stream" serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" serialize_all_allowed_events: false fields: - abrakadabra: "{ES_NODE_NAME} with suffix" + node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" access_control_rules: diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml index d8847cd60a..24f635b90d 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml @@ -8,7 +8,7 @@ readonlyrest: serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" serialize_all_allowed_events: false fields: - abrakadabra: "{ES_NODE_NAME} with suffix" + node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" access_control_rules: diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index 786eaff9dc..4461fead9f 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -164,7 +164,7 @@ class LocalClusterAuditingToolsSuite auditEntries.size shouldBe 1 auditEntries.exists(entry => - entry("abrakadabra").str == "ROR_SINGLE_1 with suffix" && + entry("node_name_with_static_suffix").str == "ROR_SINGLE_1 with suffix" && entry("another_field").str == "ROR_SINGLE GET" ) shouldBe true } @@ -181,10 +181,9 @@ class LocalClusterAuditingToolsSuite private def updateRorConfigToUseSerializer(serializer: String) = { val initialConfig = getResourceContent(rorConfigFileName) - val serializerUsedInOriginalConfigFile = """ serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""" - val modifiedConfig = initialConfig.replace(serializerUsedInOriginalConfigFile, s""" serializer: "$serializer"""") + val serializerUsedInOriginalConfigFile = """serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""" + val modifiedConfig = initialConfig.replace(serializerUsedInOriginalConfigFile, s"""serializer: "$serializer"""") rorApiManager.updateRorInIndexConfig(modifiedConfig).forceOKStatusOrConfigAlreadyLoaded() rorApiManager.reloadRorConfig().force() - truncateAllAuditManagers() } } diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/base/BaseAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/base/BaseAuditingToolsSuite.scala index f3e1e1b157..bd9bed414b 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/base/BaseAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/base/BaseAuditingToolsSuite.scala @@ -74,9 +74,6 @@ trait BaseAuditingToolsSuite } } - protected def truncateAllAuditManagers(): Unit = - adminAuditManagers.values.foreach(_.truncate()) - implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = scaled(Span(15, Seconds)), interval = scaled(Span(100, Millis))) From ac12fea5343a8af4f1559d8bf5c8b2eace0ffcc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 25 Jul 2025 21:23:45 +0200 Subject: [PATCH 06/27] qs --- .../instances/BaseAuditLogSerializer.scala | 10 +++++++++- .../enabled_auditing_tools/readonlyrest.yml | 4 ++++ .../readonlyrest_audit_index.yml | 2 ++ .../LocalClusterAuditingToolsSuite.scala | 20 ++++++++----------- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala index dfcade2b90..0d30c22ba8 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala @@ -62,7 +62,15 @@ object BaseAuditLogSerializer { error: Option[Throwable]) = { val resolvedFields: Map[String, Any] = Map( "@timestamp" -> timestampFormatter.format(requestContext.timestamp), - ) ++ fields.view.mapValues(_.value.map(resolvePlaceholder(_, environmentContext, matched, finalState, reason, duration, requestContext, error)).mkString).toMap + ) ++ fields.view.mapValues( + _.value.map( + resolvePlaceholder(_, environmentContext, matched, finalState, reason, duration, requestContext, error) + ) match { + case Nil => "" + case singleElement :: Nil => singleElement + case multipleElements => multipleElements.mkString + } + ).toMap resolvedFields .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } .mergeWith(requestContext.generalAuditEvents) diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml index a9c9b636cc..09539ff27a 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml @@ -10,6 +10,8 @@ readonlyrest: fields: node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" + tid: "{TASK_ID}" + bytes: "{CONTENT_LENGTH_IN_BYTES}" - type: data_stream data_stream: "audit_data_stream" serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" @@ -17,6 +19,8 @@ readonlyrest: fields: node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" + tid: "{TASK_ID}" + bytes: "{CONTENT_LENGTH_IN_BYTES}" access_control_rules: diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml index 24f635b90d..25d08f8456 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml @@ -10,6 +10,8 @@ readonlyrest: fields: node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" + tid: "{TASK_ID}" + bytes: "{CONTENT_LENGTH_IN_BYTES}" access_control_rules: diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index 4461fead9f..a2da40e3fb 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -65,10 +65,14 @@ class LocalClusterAuditingToolsSuite updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") performAndAssertExampleSearchRequest(indexManager) + + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2") + performAndAssertExampleSearchRequest(indexManager) + forEachAuditManager { adminAuditManager => eventually { val auditEntries = adminAuditManager.getEntries.force().jsons - auditEntries.size shouldBe 1 + auditEntries.size shouldBe 2 auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && @@ -76,16 +80,6 @@ class LocalClusterAuditingToolsSuite entry.obj.get("es_node_name").isEmpty && entry.obj.get("es_cluster_name").isEmpty ) shouldBe true - } - } - - updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2") - performAndAssertExampleSearchRequest(indexManager) - - forEachAuditManager { adminAuditManager => - eventually { - val auditEntries = adminAuditManager.getEntries.force().jsons - auditEntries.size shouldBe 1 auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && @@ -165,7 +159,9 @@ class LocalClusterAuditingToolsSuite auditEntries.exists(entry => entry("node_name_with_static_suffix").str == "ROR_SINGLE_1 with suffix" && - entry("another_field").str == "ROR_SINGLE GET" + entry("another_field").str == "ROR_SINGLE GET" && + entry("tid").numOpt.isDefined && + entry("bytes").num == 0 ) shouldBe true } } From 003c334a1e898cfac75372fff21b6d5e7019676a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 25 Jul 2025 21:36:22 +0200 Subject: [PATCH 07/27] qs --- .../decoders/AuditingSettingsDecoder.scala | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 31c015cf84..993ba16aef 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -17,8 +17,8 @@ package tech.beshu.ror.accesscontrol.factory.decoders import cats.data.NonEmptyList -import io.circe.{Decoder, HCursor} import io.circe.Decoder.* +import io.circe.{Decoder, HCursor} import io.lemonlabs.uri.Uri import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.audit.AuditingTool @@ -220,8 +220,8 @@ object AuditingSettingsDecoder extends Logging { @nowarn("cat=deprecation") given auditLogSerializerInstanceDecoder(using context: AuditEnvironmentContext, - allowedEventSerializationMode: Option[AllowedEventSerializationMode], - fields: Option[AuditFields]): Decoder[AuditLogSerializer] = + allowedEventSerializationModeOpt: Option[AllowedEventSerializationMode], + fieldsOpt: Option[AuditFields]): Decoder[AuditLogSerializer] = SyncDecoderCreator .from(Decoder.decodeString) .emapE { fullClassName => @@ -233,8 +233,15 @@ object AuditingSettingsDecoder extends Logging { } def createInstanceOfCustomizableSerializer() = { - Try(clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[AuditFields])) - .map(_.newInstance(context, allowedEventSerializationMode.get, fields.get)) + for { + constructor <- Try(clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[AuditFields])) + allowedEventSerializationMode <- Try(allowedEventSerializationModeOpt.getOrElse( + throw new IllegalStateException(s"Configurable serializer is used, but the serialize_all_allowed_events setting is missing in configuration") + )) + fields <- Try(fieldsOpt.getOrElse( + throw new IllegalStateException(s"Configurable serializer is used, but the fields setting is missing in configuration") + )) + } yield constructor.newInstance(context, allowedEventSerializationMode, fields) } def createInstanceOfSimpleSerializer() = { From b39f21793d5f4f738d0123f4e972d3eb68d7037d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 25 Jul 2025 21:38:26 +0200 Subject: [PATCH 08/27] qs --- .../factory/decoders/AuditingSettingsDecoder.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 993ba16aef..6c4ae899a4 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -250,9 +250,9 @@ object AuditingSettingsDecoder extends Logging { } val serializer = - createInstanceOfSimpleSerializer() + createInstanceOfCustomizableSerializer() .orElse(createInstanceOfEnvironmentAwareSerializer()) - .orElse(createInstanceOfCustomizableSerializer()) + .orElse(createInstanceOfSimpleSerializer()) .getOrElse( throw new IllegalStateException( s"Class ${Class.forName(fullClassName).getName} is required to have either one (AuditEnvironmentContext) parameter constructor or constructor without parameters" From 1c65e81b1f93c14f1652bc91488986ff5e2b37ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 25 Jul 2025 22:05:49 +0200 Subject: [PATCH 09/27] Cross compile from 2.11 to 3 --- audit/build.gradle | 5 ++- .../instances/BaseAuditLogSerializer.scala | 45 +++++++++++-------- .../DefaultAuditLogSerializerV1.scala | 2 +- .../DefaultAuditLogSerializerV2.scala | 2 +- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/audit/build.gradle b/audit/build.gradle index bb662e1e62..81eafaaa61 100644 --- a/audit/build.gradle +++ b/audit/build.gradle @@ -63,7 +63,10 @@ crossBuild { dependencies { implementation group: 'org.json', name: 'json', version: '20231013' - implementation group: 'com.beachape', name: 'enumeratum_3', version: '1.7.5' + crossBuildV211Implementation group: 'com.beachape', name: 'enumeratum_2.11', version: '1.7.2' + crossBuildV212Implementation group: 'com.beachape', name: 'enumeratum_2.12', version: '1.7.5' + crossBuildV213Implementation group: 'com.beachape', name: 'enumeratum_2.13', version: '1.7.5' + crossBuildV3Implementation group: 'com.beachape', name: 'enumeratum_3', version: '1.7.5' crossBuildV211Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.11.12' crossBuildV212Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.12.19' crossBuildV213Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.13.13' diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala index 0d30c22ba8..2f649f25ec 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala @@ -16,14 +16,15 @@ */ package tech.beshu.ror.audit.instances -import enumeratum.* +import enumeratum._ import org.json.JSONObject -import tech.beshu.ror.audit.AuditResponseContext.* +import tech.beshu.ror.audit.AuditResponseContext._ import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditRequestContext, AuditResponseContext} import java.time.ZoneId import java.time.format.DateTimeFormatter -import scala.collection.JavaConverters.* +import scala.collection.immutable.IndexedSeq +import scala.collection.JavaConverters._ import scala.concurrent.duration.FiniteDuration object BaseAuditLogSerializer { @@ -60,23 +61,27 @@ object BaseAuditLogSerializer { duration: FiniteDuration, requestContext: AuditRequestContext, error: Option[Throwable]) = { - val resolvedFields: Map[String, Any] = Map( - "@timestamp" -> timestampFormatter.format(requestContext.timestamp), - ) ++ fields.view.mapValues( - _.value.map( - resolvePlaceholder(_, environmentContext, matched, finalState, reason, duration, requestContext, error) - ) match { - case Nil => "" - case singleElement :: Nil => singleElement - case multipleElements => multipleElements.mkString + val resolvedFields: Map[String, Any] = { + Map( + "@timestamp" -> timestampFormatter.format(requestContext.timestamp) + ) ++ fields.map { + case (key, field) => + val resolvedValue = field.value.map { + resolvePlaceholder(_, environmentContext, matched, finalState, reason, duration, requestContext, error) + } match { + case Nil => "" + case singleElement :: Nil => singleElement + case multipleElements => multipleElements.mkString + } + key -> resolvedValue } - ).toMap + } resolvedFields .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } .mergeWith(requestContext.generalAuditEvents) } - private def resolvePlaceholder(auditValue: AuditValuePlaceholder | String, + private def resolvePlaceholder(auditValue: AuditValuePlaceholder, environmentContext: AuditEnvironmentContext, matched: Boolean, finalState: String, @@ -85,7 +90,7 @@ object BaseAuditLogSerializer { requestContext: AuditRequestContext, error: Option[Throwable]): Any = { auditValue match { - case stringValue: String => stringValue + case AuditValuePlaceholder.StaticText(text) => text case AuditValuePlaceholder.IsMatched => matched case AuditValuePlaceholder.FinalState => finalState case AuditValuePlaceholder.Reason => reason @@ -141,7 +146,7 @@ object BaseAuditLogSerializer { final case class AuditFields(value: Map[String, AuditFieldValue]) - final case class AuditFieldValue private(value: List[AuditValuePlaceholder | String]) + final case class AuditFieldValue private(value: List[AuditValuePlaceholder]) object AuditFieldValue { @@ -153,11 +158,11 @@ object BaseAuditLogSerializer { val matches = pattern.findAllMatchIn(str).toList val (parts, missing, lastIndex) = - matches.foldLeft((List.empty[AuditValuePlaceholder | String], List.empty[String], 0)) { + matches.foldLeft((List.empty[AuditValuePlaceholder], List.empty[String], 0)) { case ((partsAcc, missingAcc, lastEnd), m) => val key = m.group(1) val before = str.substring(lastEnd, m.start) - val partBefore = Option.when(before.nonEmpty)(before).toList + val partBefore = if (before.nonEmpty) List(AuditValuePlaceholder.StaticText(before)) else Nil val (partAfter, newMissing) = AuditValuePlaceholder.withNameOption(key) match { case Some(placeholder) => (List(placeholder), Nil) @@ -167,7 +172,7 @@ object BaseAuditLogSerializer { (partsAcc ++ partBefore ++ partAfter, missingAcc ++ newMissing, m.end) } - val trailing = Option.when(lastIndex < str.length)(str.substring(lastIndex)).toList + val trailing = if (lastIndex < str.length) List(AuditValuePlaceholder.StaticText(str.substring(lastIndex))) else Nil val allParts = parts ++ trailing missing match { @@ -241,6 +246,8 @@ object BaseAuditLogSerializer { case object EsClusterName extends AuditValuePlaceholder + final case class StaticText(value: String) extends AuditValuePlaceholder + override def values: IndexedSeq[AuditValuePlaceholder] = findValues } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index 47aff2eecc..dc1812ff1d 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -53,6 +53,6 @@ object DefaultAuditLogSerializerV1 { "impersonated_by" -> AuditFieldValue(AuditValuePlaceholder.ImpersonatedByUser), "action" -> AuditFieldValue(AuditValuePlaceholder.Action), "indices" -> AuditFieldValue(AuditValuePlaceholder.InvolvedIndices), - "acl_history" -> AuditFieldValue(AuditValuePlaceholder.AclHistory), + "acl_history" -> AuditFieldValue(AuditValuePlaceholder.AclHistory) ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index 58ac1b1508..24c33c0be3 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -32,6 +32,6 @@ object DefaultAuditLogSerializerV2 { val defaultV2AuditFields: Map[String, AuditFieldValue] = DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map( "es_node_name" -> AuditFieldValue(AuditValuePlaceholder.EsNodeName), - "es_cluster_name" -> AuditFieldValue(AuditValuePlaceholder.EsClusterName), + "es_cluster_name" -> AuditFieldValue(AuditValuePlaceholder.EsClusterName) ) } From 5cc3848853dc0622af5e0a265cd01e99a20dabdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 25 Jul 2025 23:01:08 +0200 Subject: [PATCH 10/27] qs --- audit/build.gradle | 1 + .../factory/decoders/AuditingSettingsDecoder.scala | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/audit/build.gradle b/audit/build.gradle index 81eafaaa61..280de2919e 100644 --- a/audit/build.gradle +++ b/audit/build.gradle @@ -67,6 +67,7 @@ dependencies { crossBuildV212Implementation group: 'com.beachape', name: 'enumeratum_2.12', version: '1.7.5' crossBuildV213Implementation group: 'com.beachape', name: 'enumeratum_2.13', version: '1.7.5' crossBuildV3Implementation group: 'com.beachape', name: 'enumeratum_3', version: '1.7.5' + compileOnly group: 'com.beachape', name: 'enumeratum_3', version: '1.7.5' crossBuildV211Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.11.12' crossBuildV212Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.12.19' crossBuildV213Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.13.13' diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 6c4ae899a4..7a0d1b897c 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -215,7 +215,8 @@ object AuditingSettingsDecoder extends Logging { fields <- c.downField("fields").as[Option[Map[String, AuditFieldValue]]] given Option[AuditFields] = fields.map(AuditFields.apply) serializer <- c.downField("serializer").as[Option[AuditLogSerializer]](decodeOption(auditLogSerializerInstanceDecoder)) - } yield serializer + legacyFieldNameSerializer <- c.downField("audit_serializer").as[Option[AuditLogSerializer]](decodeOption(auditLogSerializerInstanceDecoder)) + } yield serializer.orElse(legacyFieldNameSerializer) } @nowarn("cat=deprecation") @@ -349,7 +350,9 @@ object AuditingSettingsDecoder extends Logging { whenEnabled(c) { for { auditIndexTemplate <- decodeOptionalSetting[RorAuditIndexTemplate](c)("index_template", fallbackKey = "audit_index_template") - logSerializer <- c.as[Option[AuditLogSerializer]] + logSerializerOutsideAuditSection <- c.as[Option[AuditLogSerializer]] + logSerializerInAuditSection <- c.downField("audit").success.map(_.as[Option[AuditLogSerializer]]).getOrElse(Right(None)) + logSerializer = logSerializerOutsideAuditSection.orElse(logSerializerInAuditSection) remoteAuditCluster <- decodeOptionalSetting[AuditCluster.RemoteAuditCluster](c)("cluster", fallbackKey = "audit_cluster") } yield AuditingTool.AuditSettings( auditSinks = NonEmptyList.one( From f063e8de622242ded00889db9c605afb615c338c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 26 Jul 2025 17:44:54 +0200 Subject: [PATCH 11/27] refactor --- audit/build.gradle | 18 ++ .../beshu/ror/audit/AuditFieldValue.scala | 54 ++++ .../audit/AuditFieldValuePlaceholder.scala | 90 +++++++ .../ror/audit/BaseAuditLogSerializer.scala | 147 ++++++++++ .../instances/BaseAuditLogSerializer.scala | 255 ------------------ .../ConfigurableQueryAuditLogSerializer.scala | 4 +- .../DefaultAuditLogSerializerV1.scala | 52 ++-- .../DefaultAuditLogSerializerV2.scala | 8 +- .../instances/QueryAuditLogSerializerV1.scala | 6 +- .../instances/QueryAuditLogSerializerV2.scala | 6 +- ...ReportingAllEventsAuditLogSerializer.scala | 4 +- ...AllEventsWithQueryAuditLogSerializer.scala | 4 +- .../decoders/AuditingSettingsDecoder.scala | 5 +- 13 files changed, 354 insertions(+), 299 deletions(-) create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValuePlaceholder.scala create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/BaseAuditLogSerializer.scala delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala diff --git a/audit/build.gradle b/audit/build.gradle index 280de2919e..8a543ba18d 100644 --- a/audit/build.gradle +++ b/audit/build.gradle @@ -75,6 +75,24 @@ dependencies { compileOnly group: 'org.scala-lang', name: 'scala3-library_3', version: '3.3.3' } +// The Scala library is automatically used only in the correct version in cross build. +// But other libraries also need to be imported as `compileOnly` - otherwise the IntelliJ does not see them. +// Those other libraries are not automatically excluded from cross build and it needs to be done manually. +configurations { + crossBuildV211CompileClasspath { + exclude group: 'com.beachape', module: 'enumeratum_3' + exclude group: 'org.scala-lang', module: 'scala3-library_3' + } + crossBuildV212CompileClasspath { + exclude group: 'com.beachape', module: 'enumeratum_3' + exclude group: 'org.scala-lang', module: 'scala3-library_3' + } + crossBuildV213CompileClasspath { + exclude group: 'com.beachape', module: 'enumeratum_3' + exclude group: 'org.scala-lang', module: 'scala3-library_3' + } +} + test { testLogging { showStandardStreams = true diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala new file mode 100644 index 0000000000..053dd13315 --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala @@ -0,0 +1,54 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit + +final case class AuditFieldValue private(value: List[AuditFieldValuePlaceholder]) + +object AuditFieldValue { + + private val pattern = "\\{([^}]+)\\}".r + + def apply(placeholder: AuditFieldValuePlaceholder): AuditFieldValue = AuditFieldValue(List(placeholder)) + + def fromString(str: String): Either[String, AuditFieldValue] = { + val matches = pattern.findAllMatchIn(str).toList + + val (parts, missing, lastIndex) = + matches.foldLeft((List.empty[AuditFieldValuePlaceholder], List.empty[String], 0)) { + case ((partsAcc, missingAcc, lastEnd), m) => + val key = m.group(1) + val before = str.substring(lastEnd, m.start) + val partBefore = if (before.nonEmpty) List(AuditFieldValuePlaceholder.StaticText(before)) else Nil + + val (partAfter, newMissing) = AuditFieldValuePlaceholder.withNameOption(key) match { + case Some(placeholder) => (List(placeholder), Nil) + case None => (Nil, List(key)) + } + + (partsAcc ++ partBefore ++ partAfter, missingAcc ++ newMissing, m.end) + } + + val trailing = if (lastIndex < str.length) List(AuditFieldValuePlaceholder.StaticText(str.substring(lastIndex))) else Nil + val allParts = parts ++ trailing + + missing match { + case Nil => Right(AuditFieldValue(allParts)) + case missingList => Left(s"There are invalid placeholder values: ${missingList.mkString(", ")}") + } + } + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValuePlaceholder.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValuePlaceholder.scala new file mode 100644 index 0000000000..f29b918a97 --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValuePlaceholder.scala @@ -0,0 +1,90 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit + +import enumeratum._ + +import scala.collection.immutable.IndexedSeq + +sealed trait AuditFieldValuePlaceholder extends EnumEntry.UpperSnakecase + +object AuditFieldValuePlaceholder extends Enum[AuditFieldValuePlaceholder] { + + // Rule + case object IsMatched extends AuditFieldValuePlaceholder + + case object FinalState extends AuditFieldValuePlaceholder + + case object Reason extends AuditFieldValuePlaceholder + + case object User extends AuditFieldValuePlaceholder + + case object ImpersonatedByUser extends AuditFieldValuePlaceholder + + case object Action extends AuditFieldValuePlaceholder + + case object InvolvedIndices extends AuditFieldValuePlaceholder + + case object AclHistory extends AuditFieldValuePlaceholder + + case object ProcessingDurationMillis extends AuditFieldValuePlaceholder + + // Identifiers + case object Timestamp extends AuditFieldValuePlaceholder + + case object Id extends AuditFieldValuePlaceholder + + case object CorrelationId extends AuditFieldValuePlaceholder + + case object TaskId extends AuditFieldValuePlaceholder + + // Error details + case object ErrorType extends AuditFieldValuePlaceholder + + case object ErrorMessage extends AuditFieldValuePlaceholder + + case object Type extends AuditFieldValuePlaceholder + + // HTTP protocol values + case object HttpMethod extends AuditFieldValuePlaceholder + + case object HttpHeaderNames extends AuditFieldValuePlaceholder + + case object HttpPath extends AuditFieldValuePlaceholder + + case object XForwardedForHttpHeader extends AuditFieldValuePlaceholder + + case object RemoteAddress extends AuditFieldValuePlaceholder + + case object LocalAddress extends AuditFieldValuePlaceholder + + case object Content extends AuditFieldValuePlaceholder + + case object ContentLengthInBytes extends AuditFieldValuePlaceholder + + case object ContentLengthInKb extends AuditFieldValuePlaceholder + + // Environment + case object EsNodeName extends AuditFieldValuePlaceholder + + case object EsClusterName extends AuditFieldValuePlaceholder + + final case class StaticText(value: String) extends AuditFieldValuePlaceholder + + override def values: IndexedSeq[AuditFieldValuePlaceholder] = findValues + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/BaseAuditLogSerializer.scala new file mode 100644 index 0000000000..0a6cf1fc04 --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/BaseAuditLogSerializer.scala @@ -0,0 +1,147 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit + +import org.json.JSONObject +import tech.beshu.ror.audit.AuditResponseContext._ +import tech.beshu.ror.audit.instances.SerializeUser + +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import scala.collection.JavaConverters._ +import scala.concurrent.duration.FiniteDuration + +object BaseAuditLogSerializer { + + private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("GMT")) + + def serialize(responseContext: AuditResponseContext, + environmentContext: AuditEnvironmentContext, + fields: Map[String, AuditFieldValue], + allowedEventSerializationMode: AllowedEventSerializationMode): Option[JSONObject] = responseContext match { + case Allowed(requestContext, verbosity, reason) => + (verbosity, allowedEventSerializationMode) match { + case (Verbosity.Error, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) => + None + case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) | + (_, AllowedEventSerializationMode.SerializeAllAllowedEvents) => + Some(createEntry(fields, environmentContext, matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) + } + case ForbiddenBy(requestContext, _, reason) => + Some(createEntry(fields, environmentContext, matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None)) + case Forbidden(requestContext) => + Some(createEntry(fields, environmentContext, matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None)) + case RequestedIndexNotExist(requestContext) => + Some(createEntry(fields, environmentContext, matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None)) + case Errored(requestContext, cause) => + Some(createEntry(fields, environmentContext, matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))) + } + + private def createEntry(fields: Map[String, AuditFieldValue], + environmentContext: AuditEnvironmentContext, + matched: Boolean, + finalState: String, + reason: String, + duration: FiniteDuration, + requestContext: AuditRequestContext, + error: Option[Throwable]) = { + val resolvedFields: Map[String, Any] = { + Map( + "@timestamp" -> timestampFormatter.format(requestContext.timestamp) + ) ++ fields.map { + case (key, field) => + val resolvedValue = field.value.map { + resolvePlaceholder(_, environmentContext, matched, finalState, reason, duration, requestContext, error) + } match { + case Nil => "" + case singleElement :: Nil => singleElement + case multipleElements => multipleElements.mkString + } + key -> resolvedValue + } + } + resolvedFields + .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } + .mergeWith(requestContext.generalAuditEvents) + } + + private def resolvePlaceholder(auditValue: AuditFieldValuePlaceholder, + environmentContext: AuditEnvironmentContext, + matched: Boolean, + finalState: String, + reason: String, + duration: FiniteDuration, + requestContext: AuditRequestContext, + error: Option[Throwable]): Any = { + auditValue match { + case AuditFieldValuePlaceholder.StaticText(text) => text + case AuditFieldValuePlaceholder.IsMatched => matched + case AuditFieldValuePlaceholder.FinalState => finalState + case AuditFieldValuePlaceholder.Reason => reason + case AuditFieldValuePlaceholder.User => SerializeUser.serialize(requestContext).orNull + case AuditFieldValuePlaceholder.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull + case AuditFieldValuePlaceholder.Action => requestContext.action + case AuditFieldValuePlaceholder.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava + case AuditFieldValuePlaceholder.AclHistory => requestContext.history + case AuditFieldValuePlaceholder.ProcessingDurationMillis => duration.toMillis + case AuditFieldValuePlaceholder.Timestamp => timestampFormatter.format(requestContext.timestamp) + case AuditFieldValuePlaceholder.Id => requestContext.id + case AuditFieldValuePlaceholder.CorrelationId => requestContext.correlationId + case AuditFieldValuePlaceholder.TaskId => requestContext.taskId + case AuditFieldValuePlaceholder.ErrorType => error.map(_.getClass.getSimpleName).orNull + case AuditFieldValuePlaceholder.ErrorMessage => error.map(_.getMessage).orNull + case AuditFieldValuePlaceholder.Type => requestContext.`type` + case AuditFieldValuePlaceholder.HttpMethod => requestContext.httpMethod + case AuditFieldValuePlaceholder.HttpHeaderNames => requestContext.requestHeaders.names.asJava + case AuditFieldValuePlaceholder.HttpPath => requestContext.uriPath + case AuditFieldValuePlaceholder.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull + case AuditFieldValuePlaceholder.RemoteAddress => requestContext.remoteAddress + case AuditFieldValuePlaceholder.LocalAddress => requestContext.localAddress + case AuditFieldValuePlaceholder.Content => requestContext.content + case AuditFieldValuePlaceholder.ContentLengthInBytes => requestContext.contentLength + case AuditFieldValuePlaceholder.ContentLengthInKb => requestContext.contentLength / 1024 + case AuditFieldValuePlaceholder.EsNodeName => environmentContext.esNodeName + case AuditFieldValuePlaceholder.EsClusterName => environmentContext.esClusterName + } + } + + private implicit class JsonObjectOps(val mainJson: JSONObject) { + def mergeWith(secondaryJson: JSONObject): JSONObject = { + jsonKeys(secondaryJson).foldLeft(mainJson) { + case (json, name) if !json.has(name) => + json.put(name, secondaryJson.get(name)) + case (json, _) => + json + } + } + + private def jsonKeys(json: JSONObject) = { + Option(JSONObject.getNames(json)).toList.flatten + } + } + + sealed trait AllowedEventSerializationMode + + object AllowedEventSerializationMode { + case object SerializeOnlyAllowedEventsWithInfoLevelVerbose extends AllowedEventSerializationMode + + case object SerializeAllAllowedEvents extends AllowedEventSerializationMode + } + + final case class AuditFields(value: Map[String, AuditFieldValue]) + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala deleted file mode 100644 index 2f649f25ec..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/BaseAuditLogSerializer.scala +++ /dev/null @@ -1,255 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit.instances - -import enumeratum._ -import org.json.JSONObject -import tech.beshu.ror.audit.AuditResponseContext._ -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditRequestContext, AuditResponseContext} - -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import scala.collection.immutable.IndexedSeq -import scala.collection.JavaConverters._ -import scala.concurrent.duration.FiniteDuration - -object BaseAuditLogSerializer { - - private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("GMT")) - - def serialize(responseContext: AuditResponseContext, - environmentContext: AuditEnvironmentContext, - fields: Map[String, AuditFieldValue], - allowedEventSerializationMode: AllowedEventSerializationMode): Option[JSONObject] = responseContext match { - case Allowed(requestContext, verbosity, reason) => - (verbosity, allowedEventSerializationMode) match { - case (Verbosity.Error, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) => - None - case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) | - (_, AllowedEventSerializationMode.SerializeAllAllowedEvents) => - Some(createEntry(fields, environmentContext, matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) - } - case ForbiddenBy(requestContext, _, reason) => - Some(createEntry(fields, environmentContext, matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None)) - case Forbidden(requestContext) => - Some(createEntry(fields, environmentContext, matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None)) - case RequestedIndexNotExist(requestContext) => - Some(createEntry(fields, environmentContext, matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None)) - case Errored(requestContext, cause) => - Some(createEntry(fields, environmentContext, matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))) - } - - private def createEntry(fields: Map[String, AuditFieldValue], - environmentContext: AuditEnvironmentContext, - matched: Boolean, - finalState: String, - reason: String, - duration: FiniteDuration, - requestContext: AuditRequestContext, - error: Option[Throwable]) = { - val resolvedFields: Map[String, Any] = { - Map( - "@timestamp" -> timestampFormatter.format(requestContext.timestamp) - ) ++ fields.map { - case (key, field) => - val resolvedValue = field.value.map { - resolvePlaceholder(_, environmentContext, matched, finalState, reason, duration, requestContext, error) - } match { - case Nil => "" - case singleElement :: Nil => singleElement - case multipleElements => multipleElements.mkString - } - key -> resolvedValue - } - } - resolvedFields - .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } - .mergeWith(requestContext.generalAuditEvents) - } - - private def resolvePlaceholder(auditValue: AuditValuePlaceholder, - environmentContext: AuditEnvironmentContext, - matched: Boolean, - finalState: String, - reason: String, - duration: FiniteDuration, - requestContext: AuditRequestContext, - error: Option[Throwable]): Any = { - auditValue match { - case AuditValuePlaceholder.StaticText(text) => text - case AuditValuePlaceholder.IsMatched => matched - case AuditValuePlaceholder.FinalState => finalState - case AuditValuePlaceholder.Reason => reason - case AuditValuePlaceholder.User => SerializeUser.serialize(requestContext).orNull - case AuditValuePlaceholder.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull - case AuditValuePlaceholder.Action => requestContext.action - case AuditValuePlaceholder.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava - case AuditValuePlaceholder.AclHistory => requestContext.history - case AuditValuePlaceholder.ProcessingDurationMillis => duration.toMillis - case AuditValuePlaceholder.Timestamp => timestampFormatter.format(requestContext.timestamp) - case AuditValuePlaceholder.Id => requestContext.id - case AuditValuePlaceholder.CorrelationId => requestContext.correlationId - case AuditValuePlaceholder.TaskId => requestContext.taskId - case AuditValuePlaceholder.ErrorType => error.map(_.getClass.getSimpleName).orNull - case AuditValuePlaceholder.ErrorMessage => error.map(_.getMessage).orNull - case AuditValuePlaceholder.Type => requestContext.`type` - case AuditValuePlaceholder.HttpMethod => requestContext.httpMethod - case AuditValuePlaceholder.HttpHeaderNames => requestContext.requestHeaders.names.asJava - case AuditValuePlaceholder.HttpPath => requestContext.uriPath - case AuditValuePlaceholder.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull - case AuditValuePlaceholder.RemoteAddress => requestContext.remoteAddress - case AuditValuePlaceholder.LocalAddress => requestContext.localAddress - case AuditValuePlaceholder.Content => requestContext.content - case AuditValuePlaceholder.ContentLengthInBytes => requestContext.contentLength - case AuditValuePlaceholder.ContentLengthInKb => requestContext.contentLength / 1024 - case AuditValuePlaceholder.EsNodeName => environmentContext.esNodeName - case AuditValuePlaceholder.EsClusterName => environmentContext.esClusterName - } - } - - private implicit class JsonObjectOps(val mainJson: JSONObject) { - def mergeWith(secondaryJson: JSONObject): JSONObject = { - jsonKeys(secondaryJson).foldLeft(mainJson) { - case (json, name) if !json.has(name) => - json.put(name, secondaryJson.get(name)) - case (json, _) => - json - } - } - - private def jsonKeys(json: JSONObject) = { - Option(JSONObject.getNames(json)).toList.flatten - } - } - - sealed trait AllowedEventSerializationMode - - object AllowedEventSerializationMode { - case object SerializeOnlyAllowedEventsWithInfoLevelVerbose extends AllowedEventSerializationMode - - case object SerializeAllAllowedEvents extends AllowedEventSerializationMode - } - - final case class AuditFields(value: Map[String, AuditFieldValue]) - - final case class AuditFieldValue private(value: List[AuditValuePlaceholder]) - - object AuditFieldValue { - - private val pattern = "\\{([^}]+)\\}".r - - def apply(placeholder: AuditValuePlaceholder): AuditFieldValue = AuditFieldValue(List(placeholder)) - - def fromString(str: String): Either[String, AuditFieldValue] = { - val matches = pattern.findAllMatchIn(str).toList - - val (parts, missing, lastIndex) = - matches.foldLeft((List.empty[AuditValuePlaceholder], List.empty[String], 0)) { - case ((partsAcc, missingAcc, lastEnd), m) => - val key = m.group(1) - val before = str.substring(lastEnd, m.start) - val partBefore = if (before.nonEmpty) List(AuditValuePlaceholder.StaticText(before)) else Nil - - val (partAfter, newMissing) = AuditValuePlaceholder.withNameOption(key) match { - case Some(placeholder) => (List(placeholder), Nil) - case None => (Nil, List(key)) - } - - (partsAcc ++ partBefore ++ partAfter, missingAcc ++ newMissing, m.end) - } - - val trailing = if (lastIndex < str.length) List(AuditValuePlaceholder.StaticText(str.substring(lastIndex))) else Nil - val allParts = parts ++ trailing - - missing match { - case Nil => Right(AuditFieldValue(allParts)) - case missingList => Left(s"There are invalid placeholder values: ${missingList.mkString(", ")}") - } - } - - } - - sealed trait AuditValuePlaceholder extends EnumEntry.UpperSnakecase - - object AuditValuePlaceholder extends Enum[AuditValuePlaceholder] { - - // Rule - case object IsMatched extends AuditValuePlaceholder - - case object FinalState extends AuditValuePlaceholder - - case object Reason extends AuditValuePlaceholder - - case object User extends AuditValuePlaceholder - - case object ImpersonatedByUser extends AuditValuePlaceholder - - case object Action extends AuditValuePlaceholder - - case object InvolvedIndices extends AuditValuePlaceholder - - case object AclHistory extends AuditValuePlaceholder - - case object ProcessingDurationMillis extends AuditValuePlaceholder - - // Identifiers - case object Timestamp extends AuditValuePlaceholder - - case object Id extends AuditValuePlaceholder - - case object CorrelationId extends AuditValuePlaceholder - - case object TaskId extends AuditValuePlaceholder - - // Error details - case object ErrorType extends AuditValuePlaceholder - - case object ErrorMessage extends AuditValuePlaceholder - - case object Type extends AuditValuePlaceholder - - // HTTP protocol values - case object HttpMethod extends AuditValuePlaceholder - - case object HttpHeaderNames extends AuditValuePlaceholder - - case object HttpPath extends AuditValuePlaceholder - - case object XForwardedForHttpHeader extends AuditValuePlaceholder - - case object RemoteAddress extends AuditValuePlaceholder - - case object LocalAddress extends AuditValuePlaceholder - - case object Content extends AuditValuePlaceholder - - case object ContentLengthInBytes extends AuditValuePlaceholder - - case object ContentLengthInKb extends AuditValuePlaceholder - - // Environment - case object EsNodeName extends AuditValuePlaceholder - - case object EsClusterName extends AuditValuePlaceholder - - final case class StaticText(value: String) extends AuditValuePlaceholder - - override def values: IndexedSeq[AuditValuePlaceholder] = findValues - - } - -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala index 596bb182f9..5e38c58cab 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala @@ -17,8 +17,8 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditFields} -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} +import tech.beshu.ror.audit.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFields} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, BaseAuditLogSerializer} class ConfigurableQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext, allowedEventSerializationMode: AllowedEventSerializationMode, diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index dc1812ff1d..ce89b91a22 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -17,9 +17,9 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditValuePlaceholder} +import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} +import tech.beshu.ror.audit._ class DefaultAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { @@ -30,29 +30,29 @@ class DefaultAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) e object DefaultAuditLogSerializerV1 { val defaultV1AuditFields: Map[String, AuditFieldValue] = Map( - "match" -> AuditFieldValue(AuditValuePlaceholder.IsMatched), - "block" -> AuditFieldValue(AuditValuePlaceholder.Reason), - "id" -> AuditFieldValue(AuditValuePlaceholder.Id), - "final_state" -> AuditFieldValue(AuditValuePlaceholder.FinalState), - "@timestamp" -> AuditFieldValue(AuditValuePlaceholder.Timestamp), - "correlation_id" -> AuditFieldValue(AuditValuePlaceholder.CorrelationId), - "processingMillis" -> AuditFieldValue(AuditValuePlaceholder.ProcessingDurationMillis), - "error_type" -> AuditFieldValue(AuditValuePlaceholder.ErrorType), - "error_message" -> AuditFieldValue(AuditValuePlaceholder.ErrorMessage), - "content_len" -> AuditFieldValue(AuditValuePlaceholder.ContentLengthInBytes), - "content_len_kb" -> AuditFieldValue(AuditValuePlaceholder.ContentLengthInKb), - "type" -> AuditFieldValue(AuditValuePlaceholder.Type), - "origin" -> AuditFieldValue(AuditValuePlaceholder.RemoteAddress), - "destination" -> AuditFieldValue(AuditValuePlaceholder.LocalAddress), - "xff" -> AuditFieldValue(AuditValuePlaceholder.XForwardedForHttpHeader), - "task_id" -> AuditFieldValue(AuditValuePlaceholder.TaskId), - "req_method" -> AuditFieldValue(AuditValuePlaceholder.HttpMethod), - "headers" -> AuditFieldValue(AuditValuePlaceholder.HttpHeaderNames), - "path" -> AuditFieldValue(AuditValuePlaceholder.HttpPath), - "user" -> AuditFieldValue(AuditValuePlaceholder.User), - "impersonated_by" -> AuditFieldValue(AuditValuePlaceholder.ImpersonatedByUser), - "action" -> AuditFieldValue(AuditValuePlaceholder.Action), - "indices" -> AuditFieldValue(AuditValuePlaceholder.InvolvedIndices), - "acl_history" -> AuditFieldValue(AuditValuePlaceholder.AclHistory) + "match" -> AuditFieldValue(AuditFieldValuePlaceholder.IsMatched), + "block" -> AuditFieldValue(AuditFieldValuePlaceholder.Reason), + "id" -> AuditFieldValue(AuditFieldValuePlaceholder.Id), + "final_state" -> AuditFieldValue(AuditFieldValuePlaceholder.FinalState), + "@timestamp" -> AuditFieldValue(AuditFieldValuePlaceholder.Timestamp), + "correlation_id" -> AuditFieldValue(AuditFieldValuePlaceholder.CorrelationId), + "processingMillis" -> AuditFieldValue(AuditFieldValuePlaceholder.ProcessingDurationMillis), + "error_type" -> AuditFieldValue(AuditFieldValuePlaceholder.ErrorType), + "error_message" -> AuditFieldValue(AuditFieldValuePlaceholder.ErrorMessage), + "content_len" -> AuditFieldValue(AuditFieldValuePlaceholder.ContentLengthInBytes), + "content_len_kb" -> AuditFieldValue(AuditFieldValuePlaceholder.ContentLengthInKb), + "type" -> AuditFieldValue(AuditFieldValuePlaceholder.Type), + "origin" -> AuditFieldValue(AuditFieldValuePlaceholder.RemoteAddress), + "destination" -> AuditFieldValue(AuditFieldValuePlaceholder.LocalAddress), + "xff" -> AuditFieldValue(AuditFieldValuePlaceholder.XForwardedForHttpHeader), + "task_id" -> AuditFieldValue(AuditFieldValuePlaceholder.TaskId), + "req_method" -> AuditFieldValue(AuditFieldValuePlaceholder.HttpMethod), + "headers" -> AuditFieldValue(AuditFieldValuePlaceholder.HttpHeaderNames), + "path" -> AuditFieldValue(AuditFieldValuePlaceholder.HttpPath), + "user" -> AuditFieldValue(AuditFieldValuePlaceholder.User), + "impersonated_by" -> AuditFieldValue(AuditFieldValuePlaceholder.ImpersonatedByUser), + "action" -> AuditFieldValue(AuditFieldValuePlaceholder.Action), + "indices" -> AuditFieldValue(AuditFieldValuePlaceholder.InvolvedIndices), + "acl_history" -> AuditFieldValue(AuditFieldValuePlaceholder.AclHistory) ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index 24c33c0be3..be2334fd80 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -17,9 +17,9 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditValuePlaceholder} +import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} +import tech.beshu.ror.audit._ class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { @@ -31,7 +31,7 @@ class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) e object DefaultAuditLogSerializerV2 { val defaultV2AuditFields: Map[String, AuditFieldValue] = DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map( - "es_node_name" -> AuditFieldValue(AuditValuePlaceholder.EsNodeName), - "es_cluster_name" -> AuditFieldValue(AuditValuePlaceholder.EsClusterName) + "es_node_name" -> AuditFieldValue(AuditFieldValuePlaceholder.EsNodeName), + "es_cluster_name" -> AuditFieldValue(AuditFieldValuePlaceholder.EsClusterName) ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index b4848af028..340e7837ad 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -17,9 +17,9 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditValuePlaceholder} +import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1.queryV1AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} +import tech.beshu.ror.audit._ class QueryAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { @@ -30,5 +30,5 @@ class QueryAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) ext object QueryAuditLogSerializerV1 { val queryV1AuditFields: Map[String, AuditFieldValue] = - DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map("content" -> AuditFieldValue(AuditValuePlaceholder.Content)) + DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map("content" -> AuditFieldValue(AuditFieldValuePlaceholder.Content)) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index a9070e09d8..3613e594f8 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -17,9 +17,9 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditValuePlaceholder} +import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} +import tech.beshu.ror.audit._ class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { @@ -30,5 +30,5 @@ class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) ext object QueryAuditLogSerializerV2 { val queryV2AuditFields: Map[String, AuditFieldValue] = - DefaultAuditLogSerializerV2.defaultV2AuditFields ++ Map("content" -> AuditFieldValue(AuditValuePlaceholder.Content)) + DefaultAuditLogSerializerV2.defaultV2AuditFields ++ Map("content" -> AuditFieldValue(AuditFieldValuePlaceholder.Content)) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala index 9349b92fb8..b496e27a01 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala @@ -17,9 +17,9 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, BaseAuditLogSerializer} class ReportingAllEventsAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala index dace3e1400..30db4d135a 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala @@ -17,9 +17,9 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, BaseAuditLogSerializer} class ReportingAllEventsWithQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 7a0d1b897c..17895cbe23 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -33,8 +33,9 @@ import tech.beshu.ror.accesscontrol.factory.decoders.common.{lemonLabsUriDecoder import tech.beshu.ror.accesscontrol.utils.CirceOps.DecodingFailureOps import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator import tech.beshu.ror.audit.adapters.* -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.AllowedEventSerializationMode.* -import tech.beshu.ror.audit.instances.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFieldValue, AuditFields} +import tech.beshu.ror.audit.AuditFieldValue +import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode.* +import tech.beshu.ror.audit.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFields} import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* From 083050f0ca713f91acd3e7e093a248aa5410b5a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 26 Jul 2025 21:05:33 +0200 Subject: [PATCH 12/27] tests --- .../beshu/ror/audit/AuditFieldValue.scala | 2 +- .../ConfigurableQueryAuditLogSerializer.scala | 6 +- .../decoders/AuditingSettingsDecoder.scala | 37 ++++---- .../unit/acl/factory/AuditSettingsTests.scala | 91 ++++++++++++++++++- 4 files changed, 113 insertions(+), 23 deletions(-) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala index 053dd13315..4d4f956794 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala @@ -16,7 +16,7 @@ */ package tech.beshu.ror.audit -final case class AuditFieldValue private(value: List[AuditFieldValuePlaceholder]) +final case class AuditFieldValue(value: List[AuditFieldValuePlaceholder]) object AuditFieldValue { diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala index 5e38c58cab..acf8ecb02e 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala @@ -20,9 +20,9 @@ import org.json.JSONObject import tech.beshu.ror.audit.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFields} import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, BaseAuditLogSerializer} -class ConfigurableQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext, - allowedEventSerializationMode: AllowedEventSerializationMode, - fields: AuditFields) extends DefaultAuditLogSerializer(environmentContext) { +class ConfigurableQueryAuditLogSerializer(val environmentContext: AuditEnvironmentContext, + val allowedEventSerializationMode: AllowedEventSerializationMode, + val fields: AuditFields) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = BaseAuditLogSerializer.serialize(responseContext, environmentContext, fields.value, allowedEventSerializationMode) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 17895cbe23..949223aeef 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -32,11 +32,10 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.decoders.common.{lemonLabsUriDecoder, nonEmptyStringDecoder} import tech.beshu.ror.accesscontrol.utils.CirceOps.DecodingFailureOps import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator -import tech.beshu.ror.audit.adapters.* -import tech.beshu.ror.audit.AuditFieldValue import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode.* import tech.beshu.ror.audit.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFields} -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer} +import tech.beshu.ror.audit.adapters.* +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditFieldValue, AuditLogSerializer} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* import tech.beshu.ror.utils.yaml.YamlKeyDecoder @@ -229,26 +228,27 @@ object AuditingSettingsDecoder extends Logging { .emapE { fullClassName => val clazz = Class.forName(fullClassName) - def createInstanceOfEnvironmentAwareSerializer() = { + def createInstanceOfEnvironmentAwareSerializer(): Option[Either[String, Any]] = { Try(clazz.getConstructor(classOf[AuditEnvironmentContext])) .map(_.newInstance(summon[AuditEnvironmentContext])) + .toOption.map(_.asRight) } - def createInstanceOfCustomizableSerializer() = { - for { - constructor <- Try(clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[AuditFields])) - allowedEventSerializationMode <- Try(allowedEventSerializationModeOpt.getOrElse( - throw new IllegalStateException(s"Configurable serializer is used, but the serialize_all_allowed_events setting is missing in configuration") - )) - fields <- Try(fieldsOpt.getOrElse( - throw new IllegalStateException(s"Configurable serializer is used, but the fields setting is missing in configuration") - )) - } yield constructor.newInstance(context, allowedEventSerializationMode, fields) + def createInstanceOfCustomizableSerializer(): Option[Either[String, Any]] = { + Try( + clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[AuditFields]) + ).toOption.map { constructor => + for { + allowedEventSerializationMode <- allowedEventSerializationModeOpt.toRight(s"Configurable serializer is used, but the serialize_all_allowed_events setting is missing in configuration") + fields <- fieldsOpt.toRight(s"Configurable serializer is used, but the fields setting is missing in configuration") + } yield constructor.newInstance(context, allowedEventSerializationMode, fields) + } } - def createInstanceOfSimpleSerializer() = { + def createInstanceOfSimpleSerializer(): Option[Either[String, Any]] = { Try(clazz.getDeclaredConstructor()) .map(_.newInstance()) + .toOption.map(_.asRight) } val serializer = @@ -262,7 +262,7 @@ object AuditingSettingsDecoder extends Logging { ) Try { - serializer match { + serializer.map { case serializer: tech.beshu.ror.audit.AuditLogSerializer => Some(serializer) case serializer: tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer => @@ -272,10 +272,11 @@ object AuditingSettingsDecoder extends Logging { case _ => None } } match { - case Success(Some(customSerializer)) => + case Success(Right(Some(customSerializer))) => logger.info(s"Using custom serializer: ${customSerializer.getClass.getName}") Right(customSerializer) - case Success(None) => Left(AuditingSettingsCreationError(Message(s"Class ${fullClassName.show} is not a subclass of ${classOf[AuditLogSerializer].getName.show} or ${classOf[tech.beshu.ror.requestcontext.AuditLogSerializer[_]].getName.show}"))) + case Success(Right(None)) => Left(AuditingSettingsCreationError(Message(s"Class ${fullClassName.show} is not a subclass of ${classOf[AuditLogSerializer].getName.show} or ${classOf[tech.beshu.ror.requestcontext.AuditLogSerializer[_]].getName.show}"))) + case Success(Left(message)) => Left(AuditingSettingsCreationError(Message(message))) case Failure(ex) => Left(AuditingSettingsCreationError(Message(s"Cannot create instance of class '${fullClassName.show}', error: ${ex.getMessage.show}"))) } } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index 6b50ef3b6f..621d3c36d9 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -33,8 +33,10 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.{Core, RawRorConfigBasedCoreFactory} import tech.beshu.ror.audit.* +import tech.beshu.ror.audit.{AuditFieldValuePlaceholder => Placeholder} +import tech.beshu.ror.audit.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFields} import tech.beshu.ror.audit.adapters.{DeprecatedAuditLogSerializerAdapter, EnvironmentAwareAuditLogSerializerAdapter} -import tech.beshu.ror.audit.instances.{DefaultAuditLogSerializer, QueryAuditLogSerializer} +import tech.beshu.ror.audit.instances.{ConfigurableQueryAuditLogSerializer, DefaultAuditLogSerializer, QueryAuditLogSerializer} import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.mocks.{MockHttpClientsFactory, MockLdapConnectionPoolProvider} @@ -373,6 +375,47 @@ class AuditSettingsTests extends AnyWordSpec with Inside { expectedLoggerName = "custom_logger" ) } + "configurable serializer is set" in { + val config = rorConfigFromUnsafe( + """ + |readonlyrest: + | audit: + | enabled: true + | outputs: + | - type: log + | serializer: "tech.beshu.ror.audit.instances.ConfigurableQueryAuditLogSerializer" + | serialize_all_allowed_events: false + | fields: + | node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" + | another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" + | tid: "{TASK_ID}" + | bytes: "{CONTENT_LENGTH_IN_BYTES}" + + | access_control_rules: + | + | - name: test_block + | type: allow + | auth_key: admin:container + | + """.stripMargin) + + assertLogBasedAuditSinkSettingsPresent[ConfigurableQueryAuditLogSerializer]( + config, + expectedLoggerName = "readonlyrest_audit" + ) + + val configuredSerializer = serializer(config).asInstanceOf[ConfigurableQueryAuditLogSerializer] + + configuredSerializer.environmentContext.esClusterName shouldBe testAuditEnvironmentContext.esClusterName + configuredSerializer.environmentContext.esNodeName shouldBe testAuditEnvironmentContext.esNodeName + configuredSerializer.allowedEventSerializationMode shouldBe AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + configuredSerializer.fields shouldBe AuditFields(Map( + "node_name_with_static_suffix" -> AuditFieldValue(List(Placeholder.EsNodeName, Placeholder.StaticText(" with suffix"))), + "another_field" -> AuditFieldValue(List(Placeholder.EsClusterName, Placeholder.StaticText(" "), Placeholder.HttpMethod)), + "tid" -> AuditFieldValue(List(Placeholder.TaskId)), + "bytes" -> AuditFieldValue(List(Placeholder.ContentLengthInBytes)) + )) + } } "'index' output type defined" when { "only type is set" in { @@ -1242,6 +1285,52 @@ class AuditSettingsTests extends AnyWordSpec with Inside { expectedErrorMessage = "The audit 'outputs' array cannot be empty" ) } + "configurable serializer is set, but without serialize_all_allowed_events setting" in { + val config = rorConfigFromUnsafe( + """ + |readonlyrest: + | audit: + | enabled: true + | outputs: + | - type: log + | serializer: "tech.beshu.ror.audit.instances.ConfigurableQueryAuditLogSerializer" + | + | access_control_rules: + | + | - name: test_block + | type: allow + | auth_key: admin:container + | + """.stripMargin) + + assertInvalidSettings( + config, + expectedErrorMessage = "Configurable serializer is used, but the serialize_all_allowed_events setting is missing in configuration" + ) + } + "configurable serializer is set, but without fields setting" in { + val config = rorConfigFromUnsafe( + """ + |readonlyrest: + | audit: + | enabled: true + | outputs: + | - type: log + | serializer: "tech.beshu.ror.audit.instances.ConfigurableQueryAuditLogSerializer" + | serialize_all_allowed_events: false + | access_control_rules: + | + | - name: test_block + | type: allow + | auth_key: admin:container + | + """.stripMargin) + + assertInvalidSettings( + config, + expectedErrorMessage = "Configurable serializer is used, but the fields setting is missing in configuration" + ) + } } } "deprecated format is used" should { From 5b5cbc89650d10850128b0e7c1523669b024be7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 17 Aug 2025 18:23:15 +0200 Subject: [PATCH 13/27] qs --- audit/build.gradle | 23 --- .../beshu/ror/audit/AuditFieldValue.scala | 81 +++++++--- .../audit/AuditFieldValuePlaceholder.scala | 90 ----------- .../ror/audit/AuditSerializationHelper.scala | 141 +++++++++++++++++ .../ror/audit/BaseAuditLogSerializer.scala | 147 ------------------ .../EnvironmentAwareAuditLogSerializer.scala | 8 + .../DefaultAuditLogSerializerV1.scala | 63 ++++---- .../DefaultAuditLogSerializerV2.scala | 19 ++- .../instances/QueryAuditLogSerializerV1.scala | 19 ++- .../instances/QueryAuditLogSerializerV2.scala | 16 +- ...gAllTypesOfEventsAuditLogSerializer.scala} | 13 +- ...OfEventsWithQueryAuditLogSerializer.scala} | 13 +- .../AuditFieldValueDeserializer.scala | 91 +++++++++++ .../ConfigurableAuditLogSerializer.scala | 15 +- .../RawRorConfigBasedCoreFactory.scala | 1 + .../decoders/AuditingSettingsDecoder.scala | 136 ++++++++-------- .../unit/acl/factory/AuditSettingsTests.scala | 28 ++-- .../LocalClusterAuditingToolsSuite.scala | 19 ++- 18 files changed, 487 insertions(+), 436 deletions(-) delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValuePlaceholder.scala create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/BaseAuditLogSerializer.scala rename audit/src/main/scala/tech/beshu/ror/audit/instances/{ReportingAllEventsAuditLogSerializer.scala => ReportingAllTypesOfEventsAuditLogSerializer.scala} (65%) rename audit/src/main/scala/tech/beshu/ror/audit/instances/{ReportingAllEventsWithQueryAuditLogSerializer.scala => ReportingAllTypesOfEventsWithQueryAuditLogSerializer.scala} (65%) create mode 100644 core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala rename audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala => core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala (51%) diff --git a/audit/build.gradle b/audit/build.gradle index 8a543ba18d..1d6be5a20f 100644 --- a/audit/build.gradle +++ b/audit/build.gradle @@ -63,11 +63,6 @@ crossBuild { dependencies { implementation group: 'org.json', name: 'json', version: '20231013' - crossBuildV211Implementation group: 'com.beachape', name: 'enumeratum_2.11', version: '1.7.2' - crossBuildV212Implementation group: 'com.beachape', name: 'enumeratum_2.12', version: '1.7.5' - crossBuildV213Implementation group: 'com.beachape', name: 'enumeratum_2.13', version: '1.7.5' - crossBuildV3Implementation group: 'com.beachape', name: 'enumeratum_3', version: '1.7.5' - compileOnly group: 'com.beachape', name: 'enumeratum_3', version: '1.7.5' crossBuildV211Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.11.12' crossBuildV212Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.12.19' crossBuildV213Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.13.13' @@ -75,24 +70,6 @@ dependencies { compileOnly group: 'org.scala-lang', name: 'scala3-library_3', version: '3.3.3' } -// The Scala library is automatically used only in the correct version in cross build. -// But other libraries also need to be imported as `compileOnly` - otherwise the IntelliJ does not see them. -// Those other libraries are not automatically excluded from cross build and it needs to be done manually. -configurations { - crossBuildV211CompileClasspath { - exclude group: 'com.beachape', module: 'enumeratum_3' - exclude group: 'org.scala-lang', module: 'scala3-library_3' - } - crossBuildV212CompileClasspath { - exclude group: 'com.beachape', module: 'enumeratum_3' - exclude group: 'org.scala-lang', module: 'scala3-library_3' - } - crossBuildV213CompileClasspath { - exclude group: 'com.beachape', module: 'enumeratum_3' - exclude group: 'org.scala-lang', module: 'scala3-library_3' - } -} - test { testLogging { showStandardStreams = true diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala index 4d4f956794..3da8571a40 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala @@ -16,39 +16,70 @@ */ package tech.beshu.ror.audit -final case class AuditFieldValue(value: List[AuditFieldValuePlaceholder]) +private[ror] sealed trait AuditFieldValue -object AuditFieldValue { +private[ror] object AuditFieldValue { - private val pattern = "\\{([^}]+)\\}".r + // Rule + case object IsMatched extends AuditFieldValue - def apply(placeholder: AuditFieldValuePlaceholder): AuditFieldValue = AuditFieldValue(List(placeholder)) + case object FinalState extends AuditFieldValue - def fromString(str: String): Either[String, AuditFieldValue] = { - val matches = pattern.findAllMatchIn(str).toList + case object Reason extends AuditFieldValue - val (parts, missing, lastIndex) = - matches.foldLeft((List.empty[AuditFieldValuePlaceholder], List.empty[String], 0)) { - case ((partsAcc, missingAcc, lastEnd), m) => - val key = m.group(1) - val before = str.substring(lastEnd, m.start) - val partBefore = if (before.nonEmpty) List(AuditFieldValuePlaceholder.StaticText(before)) else Nil + case object User extends AuditFieldValue - val (partAfter, newMissing) = AuditFieldValuePlaceholder.withNameOption(key) match { - case Some(placeholder) => (List(placeholder), Nil) - case None => (Nil, List(key)) - } + case object ImpersonatedByUser extends AuditFieldValue - (partsAcc ++ partBefore ++ partAfter, missingAcc ++ newMissing, m.end) - } + case object Action extends AuditFieldValue - val trailing = if (lastIndex < str.length) List(AuditFieldValuePlaceholder.StaticText(str.substring(lastIndex))) else Nil - val allParts = parts ++ trailing + case object InvolvedIndices extends AuditFieldValue - missing match { - case Nil => Right(AuditFieldValue(allParts)) - case missingList => Left(s"There are invalid placeholder values: ${missingList.mkString(", ")}") - } - } + case object AclHistory extends AuditFieldValue + + case object ProcessingDurationMillis extends AuditFieldValue + + // Identifiers + case object Timestamp extends AuditFieldValue + + case object Id extends AuditFieldValue + + case object CorrelationId extends AuditFieldValue + + case object TaskId extends AuditFieldValue + + // Error details + case object ErrorType extends AuditFieldValue + + case object ErrorMessage extends AuditFieldValue + + case object Type extends AuditFieldValue + + // HTTP protocol values + case object HttpMethod extends AuditFieldValue + + case object HttpHeaderNames extends AuditFieldValue + + case object HttpPath extends AuditFieldValue + + case object XForwardedForHttpHeader extends AuditFieldValue + + case object RemoteAddress extends AuditFieldValue + + case object LocalAddress extends AuditFieldValue + + case object Content extends AuditFieldValue + + case object ContentLengthInBytes extends AuditFieldValue + + case object ContentLengthInKb extends AuditFieldValue + + case object EsNodeName extends AuditFieldValue + + case object EsClusterName extends AuditFieldValue + + final case class StaticText(value: String) extends AuditFieldValue + + final case class Combined(values: List[AuditFieldValue]) extends AuditFieldValue } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValuePlaceholder.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValuePlaceholder.scala deleted file mode 100644 index f29b918a97..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValuePlaceholder.scala +++ /dev/null @@ -1,90 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit - -import enumeratum._ - -import scala.collection.immutable.IndexedSeq - -sealed trait AuditFieldValuePlaceholder extends EnumEntry.UpperSnakecase - -object AuditFieldValuePlaceholder extends Enum[AuditFieldValuePlaceholder] { - - // Rule - case object IsMatched extends AuditFieldValuePlaceholder - - case object FinalState extends AuditFieldValuePlaceholder - - case object Reason extends AuditFieldValuePlaceholder - - case object User extends AuditFieldValuePlaceholder - - case object ImpersonatedByUser extends AuditFieldValuePlaceholder - - case object Action extends AuditFieldValuePlaceholder - - case object InvolvedIndices extends AuditFieldValuePlaceholder - - case object AclHistory extends AuditFieldValuePlaceholder - - case object ProcessingDurationMillis extends AuditFieldValuePlaceholder - - // Identifiers - case object Timestamp extends AuditFieldValuePlaceholder - - case object Id extends AuditFieldValuePlaceholder - - case object CorrelationId extends AuditFieldValuePlaceholder - - case object TaskId extends AuditFieldValuePlaceholder - - // Error details - case object ErrorType extends AuditFieldValuePlaceholder - - case object ErrorMessage extends AuditFieldValuePlaceholder - - case object Type extends AuditFieldValuePlaceholder - - // HTTP protocol values - case object HttpMethod extends AuditFieldValuePlaceholder - - case object HttpHeaderNames extends AuditFieldValuePlaceholder - - case object HttpPath extends AuditFieldValuePlaceholder - - case object XForwardedForHttpHeader extends AuditFieldValuePlaceholder - - case object RemoteAddress extends AuditFieldValuePlaceholder - - case object LocalAddress extends AuditFieldValuePlaceholder - - case object Content extends AuditFieldValuePlaceholder - - case object ContentLengthInBytes extends AuditFieldValuePlaceholder - - case object ContentLengthInKb extends AuditFieldValuePlaceholder - - // Environment - case object EsNodeName extends AuditFieldValuePlaceholder - - case object EsClusterName extends AuditFieldValuePlaceholder - - final case class StaticText(value: String) extends AuditFieldValuePlaceholder - - override def values: IndexedSeq[AuditFieldValuePlaceholder] = findValues - -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala new file mode 100644 index 0000000000..43970814fd --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala @@ -0,0 +1,141 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit + +import org.json.JSONObject +import tech.beshu.ror.audit.AuditResponseContext.* +import tech.beshu.ror.audit.instances.SerializeUser + +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import scala.collection.JavaConverters.* +import scala.concurrent.duration.FiniteDuration + +private[ror] object AuditSerializationHelper { + + private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("GMT")) + + def serialize(responseContext: AuditResponseContext, + environmentContext: Option[AuditEnvironmentContext], + fields: Map[AuditFieldName, AuditFieldValue], + allowedEventSerializationMode: AllowedEventSerializationMode): Option[JSONObject] = responseContext match { + case Allowed(requestContext, verbosity, reason) => + (verbosity, allowedEventSerializationMode) match { + case (Verbosity.Error, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) => + None + case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) | + (_, AllowedEventSerializationMode.SerializeAllAllowedEvents) => + Some(createEntry(fields, matched = true, "ALLOWED", reason, responseContext.duration, requestContext, environmentContext, None)) + } + case ForbiddenBy(requestContext, _, reason) => + Some(createEntry(fields, matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, environmentContext, None)) + case Forbidden(requestContext) => + Some(createEntry(fields, matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, environmentContext, None)) + case RequestedIndexNotExist(requestContext) => + Some(createEntry(fields, matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, environmentContext, None)) + case Errored(requestContext, cause) => + Some(createEntry(fields, matched = false, "ERRORED", "error", responseContext.duration, requestContext, environmentContext, Some(cause))) + } + + private def createEntry(fields: Map[AuditFieldName, AuditFieldValue], + matched: Boolean, + finalState: String, + reason: String, + duration: FiniteDuration, + requestContext: AuditRequestContext, + environmentContext: Option[AuditEnvironmentContext], + error: Option[Throwable]) = { + val resolvedFields: Map[String, Any] = { + Map( + "@timestamp" -> timestampFormatter.format(requestContext.timestamp) + ) ++ fields.map { + case (fieldName, fieldValue) => + fieldName.value -> resolvePlaceholder(fieldValue, matched, finalState, reason, duration, requestContext, environmentContext, error) + } + } + resolvedFields + .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } + .mergeWith(requestContext.generalAuditEvents) + } + + private def resolvePlaceholder(auditValue: AuditFieldValue, + matched: Boolean, + finalState: String, + reason: String, + duration: FiniteDuration, + requestContext: AuditRequestContext, + environmentContext: Option[AuditEnvironmentContext], + error: Option[Throwable]): Any = { + auditValue match { + case AuditFieldValue.IsMatched => matched + case AuditFieldValue.FinalState => finalState + case AuditFieldValue.Reason => reason + case AuditFieldValue.User => SerializeUser.serialize(requestContext).orNull + case AuditFieldValue.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull + case AuditFieldValue.Action => requestContext.action + case AuditFieldValue.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava + case AuditFieldValue.AclHistory => requestContext.history + case AuditFieldValue.ProcessingDurationMillis => duration.toMillis + case AuditFieldValue.Timestamp => timestampFormatter.format(requestContext.timestamp) + case AuditFieldValue.Id => requestContext.id + case AuditFieldValue.CorrelationId => requestContext.correlationId + case AuditFieldValue.TaskId => requestContext.taskId + case AuditFieldValue.ErrorType => error.map(_.getClass.getSimpleName).orNull + case AuditFieldValue.ErrorMessage => error.map(_.getMessage).orNull + case AuditFieldValue.Type => requestContext.`type` + case AuditFieldValue.HttpMethod => requestContext.httpMethod + case AuditFieldValue.HttpHeaderNames => requestContext.requestHeaders.names.asJava + case AuditFieldValue.HttpPath => requestContext.uriPath + case AuditFieldValue.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull + case AuditFieldValue.RemoteAddress => requestContext.remoteAddress + case AuditFieldValue.LocalAddress => requestContext.localAddress + case AuditFieldValue.Content => requestContext.content + case AuditFieldValue.ContentLengthInBytes => requestContext.contentLength + case AuditFieldValue.ContentLengthInKb => requestContext.contentLength / 1024 + case AuditFieldValue.EsNodeName => environmentContext.map(_.esNodeName).getOrElse("") + case AuditFieldValue.EsClusterName => environmentContext.map(_.esClusterName).getOrElse("") + case AuditFieldValue.StaticText(text) => text + case AuditFieldValue.Combined(values) => values.map(resolvePlaceholder(_, matched, finalState, reason, duration, requestContext, environmentContext, error)).mkString + } + } + + private implicit class JsonObjectOps(val mainJson: JSONObject) { + def mergeWith(secondaryJson: JSONObject): JSONObject = { + jsonKeys(secondaryJson).foldLeft(mainJson) { + case (json, name) if !json.has(name) => + json.put(name, secondaryJson.get(name)) + case (json, _) => + json + } + } + + private def jsonKeys(json: JSONObject) = { + Option(JSONObject.getNames(json)).toList.flatten + } + } + + sealed trait AllowedEventSerializationMode + + object AllowedEventSerializationMode { + case object SerializeOnlyAllowedEventsWithInfoLevelVerbose extends AllowedEventSerializationMode + + case object SerializeAllAllowedEvents extends AllowedEventSerializationMode + } + + final case class AuditFieldName(value: String) + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/BaseAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/BaseAuditLogSerializer.scala deleted file mode 100644 index 0a6cf1fc04..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/BaseAuditLogSerializer.scala +++ /dev/null @@ -1,147 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit - -import org.json.JSONObject -import tech.beshu.ror.audit.AuditResponseContext._ -import tech.beshu.ror.audit.instances.SerializeUser - -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import scala.collection.JavaConverters._ -import scala.concurrent.duration.FiniteDuration - -object BaseAuditLogSerializer { - - private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("GMT")) - - def serialize(responseContext: AuditResponseContext, - environmentContext: AuditEnvironmentContext, - fields: Map[String, AuditFieldValue], - allowedEventSerializationMode: AllowedEventSerializationMode): Option[JSONObject] = responseContext match { - case Allowed(requestContext, verbosity, reason) => - (verbosity, allowedEventSerializationMode) match { - case (Verbosity.Error, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) => - None - case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) | - (_, AllowedEventSerializationMode.SerializeAllAllowedEvents) => - Some(createEntry(fields, environmentContext, matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) - } - case ForbiddenBy(requestContext, _, reason) => - Some(createEntry(fields, environmentContext, matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None)) - case Forbidden(requestContext) => - Some(createEntry(fields, environmentContext, matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None)) - case RequestedIndexNotExist(requestContext) => - Some(createEntry(fields, environmentContext, matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None)) - case Errored(requestContext, cause) => - Some(createEntry(fields, environmentContext, matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause))) - } - - private def createEntry(fields: Map[String, AuditFieldValue], - environmentContext: AuditEnvironmentContext, - matched: Boolean, - finalState: String, - reason: String, - duration: FiniteDuration, - requestContext: AuditRequestContext, - error: Option[Throwable]) = { - val resolvedFields: Map[String, Any] = { - Map( - "@timestamp" -> timestampFormatter.format(requestContext.timestamp) - ) ++ fields.map { - case (key, field) => - val resolvedValue = field.value.map { - resolvePlaceholder(_, environmentContext, matched, finalState, reason, duration, requestContext, error) - } match { - case Nil => "" - case singleElement :: Nil => singleElement - case multipleElements => multipleElements.mkString - } - key -> resolvedValue - } - } - resolvedFields - .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } - .mergeWith(requestContext.generalAuditEvents) - } - - private def resolvePlaceholder(auditValue: AuditFieldValuePlaceholder, - environmentContext: AuditEnvironmentContext, - matched: Boolean, - finalState: String, - reason: String, - duration: FiniteDuration, - requestContext: AuditRequestContext, - error: Option[Throwable]): Any = { - auditValue match { - case AuditFieldValuePlaceholder.StaticText(text) => text - case AuditFieldValuePlaceholder.IsMatched => matched - case AuditFieldValuePlaceholder.FinalState => finalState - case AuditFieldValuePlaceholder.Reason => reason - case AuditFieldValuePlaceholder.User => SerializeUser.serialize(requestContext).orNull - case AuditFieldValuePlaceholder.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull - case AuditFieldValuePlaceholder.Action => requestContext.action - case AuditFieldValuePlaceholder.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava - case AuditFieldValuePlaceholder.AclHistory => requestContext.history - case AuditFieldValuePlaceholder.ProcessingDurationMillis => duration.toMillis - case AuditFieldValuePlaceholder.Timestamp => timestampFormatter.format(requestContext.timestamp) - case AuditFieldValuePlaceholder.Id => requestContext.id - case AuditFieldValuePlaceholder.CorrelationId => requestContext.correlationId - case AuditFieldValuePlaceholder.TaskId => requestContext.taskId - case AuditFieldValuePlaceholder.ErrorType => error.map(_.getClass.getSimpleName).orNull - case AuditFieldValuePlaceholder.ErrorMessage => error.map(_.getMessage).orNull - case AuditFieldValuePlaceholder.Type => requestContext.`type` - case AuditFieldValuePlaceholder.HttpMethod => requestContext.httpMethod - case AuditFieldValuePlaceholder.HttpHeaderNames => requestContext.requestHeaders.names.asJava - case AuditFieldValuePlaceholder.HttpPath => requestContext.uriPath - case AuditFieldValuePlaceholder.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull - case AuditFieldValuePlaceholder.RemoteAddress => requestContext.remoteAddress - case AuditFieldValuePlaceholder.LocalAddress => requestContext.localAddress - case AuditFieldValuePlaceholder.Content => requestContext.content - case AuditFieldValuePlaceholder.ContentLengthInBytes => requestContext.contentLength - case AuditFieldValuePlaceholder.ContentLengthInKb => requestContext.contentLength / 1024 - case AuditFieldValuePlaceholder.EsNodeName => environmentContext.esNodeName - case AuditFieldValuePlaceholder.EsClusterName => environmentContext.esClusterName - } - } - - private implicit class JsonObjectOps(val mainJson: JSONObject) { - def mergeWith(secondaryJson: JSONObject): JSONObject = { - jsonKeys(secondaryJson).foldLeft(mainJson) { - case (json, name) if !json.has(name) => - json.put(name, secondaryJson.get(name)) - case (json, _) => - json - } - } - - private def jsonKeys(json: JSONObject) = { - Option(JSONObject.getNames(json)).toList.flatten - } - } - - sealed trait AllowedEventSerializationMode - - object AllowedEventSerializationMode { - case object SerializeOnlyAllowedEventsWithInfoLevelVerbose extends AllowedEventSerializationMode - - case object SerializeAllAllowedEvents extends AllowedEventSerializationMode - } - - final case class AuditFields(value: Map[String, AuditFieldValue]) - -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala index c27f9f3671..b69f8a4380 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala @@ -17,8 +17,16 @@ package tech.beshu.ror.audit import org.json.JSONObject +import tech.beshu.ror.audit.AuditSerializationHelper.AuditFieldName trait EnvironmentAwareAuditLogSerializer { def onResponse(responseContext: AuditResponseContext, environmentContext: AuditEnvironmentContext): Option[JSONObject] } + +object EnvironmentAwareAuditLogSerializer { + val environmentRelatedAuditFields = Map( + AuditFieldName("es_node_name") -> AuditFieldValue.EsNodeName, + AuditFieldName("es_cluster_name") -> AuditFieldValue.EsClusterName + ) +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index ce89b91a22..385467fa03 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -17,42 +17,47 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.* +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields -import tech.beshu.ror.audit._ -class DefaultAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { +class DefaultAuditLogSerializerV1 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV1AuditFields, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) + AuditSerializationHelper.serialize( + responseContext = responseContext, + environmentContext = None, + fields = defaultV1AuditFields, + allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + ) } object DefaultAuditLogSerializerV1 { - val defaultV1AuditFields: Map[String, AuditFieldValue] = Map( - "match" -> AuditFieldValue(AuditFieldValuePlaceholder.IsMatched), - "block" -> AuditFieldValue(AuditFieldValuePlaceholder.Reason), - "id" -> AuditFieldValue(AuditFieldValuePlaceholder.Id), - "final_state" -> AuditFieldValue(AuditFieldValuePlaceholder.FinalState), - "@timestamp" -> AuditFieldValue(AuditFieldValuePlaceholder.Timestamp), - "correlation_id" -> AuditFieldValue(AuditFieldValuePlaceholder.CorrelationId), - "processingMillis" -> AuditFieldValue(AuditFieldValuePlaceholder.ProcessingDurationMillis), - "error_type" -> AuditFieldValue(AuditFieldValuePlaceholder.ErrorType), - "error_message" -> AuditFieldValue(AuditFieldValuePlaceholder.ErrorMessage), - "content_len" -> AuditFieldValue(AuditFieldValuePlaceholder.ContentLengthInBytes), - "content_len_kb" -> AuditFieldValue(AuditFieldValuePlaceholder.ContentLengthInKb), - "type" -> AuditFieldValue(AuditFieldValuePlaceholder.Type), - "origin" -> AuditFieldValue(AuditFieldValuePlaceholder.RemoteAddress), - "destination" -> AuditFieldValue(AuditFieldValuePlaceholder.LocalAddress), - "xff" -> AuditFieldValue(AuditFieldValuePlaceholder.XForwardedForHttpHeader), - "task_id" -> AuditFieldValue(AuditFieldValuePlaceholder.TaskId), - "req_method" -> AuditFieldValue(AuditFieldValuePlaceholder.HttpMethod), - "headers" -> AuditFieldValue(AuditFieldValuePlaceholder.HttpHeaderNames), - "path" -> AuditFieldValue(AuditFieldValuePlaceholder.HttpPath), - "user" -> AuditFieldValue(AuditFieldValuePlaceholder.User), - "impersonated_by" -> AuditFieldValue(AuditFieldValuePlaceholder.ImpersonatedByUser), - "action" -> AuditFieldValue(AuditFieldValuePlaceholder.Action), - "indices" -> AuditFieldValue(AuditFieldValuePlaceholder.InvolvedIndices), - "acl_history" -> AuditFieldValue(AuditFieldValuePlaceholder.AclHistory) + val defaultV1AuditFields: Map[AuditFieldName, AuditFieldValue] = Map( + AuditFieldName("match") -> AuditFieldValue.IsMatched, + AuditFieldName("block") -> AuditFieldValue.Reason, + AuditFieldName("id") -> AuditFieldValue.Id, + AuditFieldName("final_state") -> AuditFieldValue.FinalState, + AuditFieldName("@timestamp") -> AuditFieldValue.Timestamp, + AuditFieldName("correlation_id") -> AuditFieldValue.CorrelationId, + AuditFieldName("processingMillis") -> AuditFieldValue.ProcessingDurationMillis, + AuditFieldName("error_type") -> AuditFieldValue.ErrorType, + AuditFieldName("error_message") -> AuditFieldValue.ErrorMessage, + AuditFieldName("content_len") -> AuditFieldValue.ContentLengthInBytes, + AuditFieldName("content_len_kb") -> AuditFieldValue.ContentLengthInKb, + AuditFieldName("type") -> AuditFieldValue.Type, + AuditFieldName("origin") -> AuditFieldValue.RemoteAddress, + AuditFieldName("destination") -> AuditFieldValue.LocalAddress, + AuditFieldName("xff") -> AuditFieldValue.XForwardedForHttpHeader, + AuditFieldName("task_id") -> AuditFieldValue.TaskId, + AuditFieldName("req_method") -> AuditFieldValue.HttpMethod, + AuditFieldName("headers") -> AuditFieldValue.HttpHeaderNames, + AuditFieldName("path") -> AuditFieldValue.HttpPath, + AuditFieldName("user") -> AuditFieldValue.User, + AuditFieldName("impersonated_by") -> AuditFieldValue.ImpersonatedByUser, + AuditFieldName("action") -> AuditFieldValue.Action, + AuditFieldName("indices") -> AuditFieldValue.InvolvedIndices, + AuditFieldName("acl_history") -> AuditFieldValue.AclHistory ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index be2334fd80..263c53fe12 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -17,21 +17,24 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.* +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer.environmentRelatedAuditFields import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields -import tech.beshu.ror.audit._ class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV2AuditFields, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) + AuditSerializationHelper.serialize( + responseContext = responseContext, + environmentContext = Some(environmentContext), + fields = defaultV2AuditFields, + allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + ) } object DefaultAuditLogSerializerV2 { - val defaultV2AuditFields: Map[String, AuditFieldValue] = - DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map( - "es_node_name" -> AuditFieldValue(AuditFieldValuePlaceholder.EsNodeName), - "es_cluster_name" -> AuditFieldValue(AuditFieldValuePlaceholder.EsClusterName) - ) + val defaultV2AuditFields: Map[AuditFieldName, AuditFieldValue] = + DefaultAuditLogSerializerV1.defaultV1AuditFields ++ environmentRelatedAuditFields } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index 340e7837ad..8216d1fe76 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -17,18 +17,25 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.* +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1.queryV1AuditFields -import tech.beshu.ror.audit._ -class QueryAuditLogSerializerV1(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { +class QueryAuditLogSerializerV1 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV1AuditFields, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) + AuditSerializationHelper.serialize( + responseContext = responseContext, + environmentContext = None, + fields = queryV1AuditFields, + allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + ) } object QueryAuditLogSerializerV1 { - val queryV1AuditFields: Map[String, AuditFieldValue] = - DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map("content" -> AuditFieldValue(AuditFieldValuePlaceholder.Content)) + val queryV1AuditFields: Map[AuditFieldName, AuditFieldValue] = + DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map( + AuditFieldName("content") -> AuditFieldValue.Content + ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index 3613e594f8..de5e41ea02 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -17,18 +17,24 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.* +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer.environmentRelatedAuditFields import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields -import tech.beshu.ror.audit._ class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV2AuditFields, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) + AuditSerializationHelper.serialize( + responseContext = responseContext, + environmentContext = Some(environmentContext), + fields = queryV2AuditFields, + allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + ) } object QueryAuditLogSerializerV2 { - val queryV2AuditFields: Map[String, AuditFieldValue] = - DefaultAuditLogSerializerV2.defaultV2AuditFields ++ Map("content" -> AuditFieldValue(AuditFieldValuePlaceholder.Content)) + val queryV2AuditFields: Map[AuditFieldName, AuditFieldValue] = + QueryAuditLogSerializerV1.queryV1AuditFields ++ environmentRelatedAuditFields } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsAuditLogSerializer.scala similarity index 65% rename from audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala rename to audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsAuditLogSerializer.scala index b496e27a01..35173642d3 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsAuditLogSerializer.scala @@ -17,13 +17,18 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, BaseAuditLogSerializer} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, AuditSerializationHelper} -class ReportingAllEventsAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { +class ReportingAllTypesOfEventsAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, defaultV2AuditFields, AllowedEventSerializationMode.SerializeAllAllowedEvents) + AuditSerializationHelper.serialize( + responseContext = responseContext, + environmentContext = Some(environmentContext), + fields = defaultV2AuditFields, + allowedEventSerializationMode = AllowedEventSerializationMode.SerializeAllAllowedEvents + ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsWithQueryAuditLogSerializer.scala similarity index 65% rename from audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala rename to audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsWithQueryAuditLogSerializer.scala index 30db4d135a..ee59595879 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllEventsWithQueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsWithQueryAuditLogSerializer.scala @@ -17,13 +17,18 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode +import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, BaseAuditLogSerializer} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, AuditSerializationHelper} -class ReportingAllEventsWithQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { +class ReportingAllTypesOfEventsWithQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, queryV2AuditFields, AllowedEventSerializationMode.SerializeAllAllowedEvents) + AuditSerializationHelper.serialize( + responseContext = responseContext, + environmentContext = Some(environmentContext), + fields = queryV2AuditFields, + allowedEventSerializationMode = AllowedEventSerializationMode.SerializeAllAllowedEvents + ) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala new file mode 100644 index 0000000000..51297f8932 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala @@ -0,0 +1,91 @@ + + +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.accesscontrol.audit.configurable + +import tech.beshu.ror.audit.AuditFieldValue + +object AuditFieldValueDeserializer { + + private val pattern = "\\{([^}]+)\\}".r + + def deserialize(str: String): Either[String, AuditFieldValue] = { + val matches = pattern.findAllMatchIn(str).toList + + val (parts, missing, lastIndex) = + matches.foldLeft((List.empty[AuditFieldValue], List.empty[String], 0)) { + case ((partsAcc, missingAcc, lastEnd), m) => + val key = m.group(1) + val before = str.substring(lastEnd, m.start) + val partBefore = if (before.nonEmpty) List(AuditFieldValue.StaticText(before)) else Nil + + val (partAfter, newMissing) = deserializerAuditFieldValue(key) match { + case Some(placeholder) => (List(placeholder), Nil) + case None => (Nil, List(key)) + } + + (partsAcc ++ partBefore ++ partAfter, missingAcc ++ newMissing, m.end) + } + + val trailing = if (lastIndex < str.length) List(AuditFieldValue.StaticText(str.substring(lastIndex))) else Nil + val allParts = parts ++ trailing + + missing match { + case Nil => allParts match { + case Nil => Right(AuditFieldValue.StaticText("")) + case singleElement :: Nil => Right(singleElement) + case multipleElements => Right(AuditFieldValue.Combined(multipleElements)) + } + case missingList => Left(s"There are invalid placeholder values: ${missingList.mkString(", ")}") + } + } + + private def deserializerAuditFieldValue(str: String): Option[AuditFieldValue] = { + str match { + case "IS_MATCHED" => Some(AuditFieldValue.IsMatched) + case "FINAL_STATE" => Some(AuditFieldValue.FinalState) + case "REASON" => Some(AuditFieldValue.Reason) + case "USER" => Some(AuditFieldValue.User) + case "IMPERSONATED_BY_USER" => Some(AuditFieldValue.ImpersonatedByUser) + case "ACTION" => Some(AuditFieldValue.Action) + case "INVOLVED_INDICES" => Some(AuditFieldValue.InvolvedIndices) + case "ACL_HISTORY" => Some(AuditFieldValue.AclHistory) + case "PROCESSING_DURATION_MILLIS" => Some(AuditFieldValue.ProcessingDurationMillis) + case "TIMESTAMP" => Some(AuditFieldValue.Timestamp) + case "ID" => Some(AuditFieldValue.Id) + case "CORRELATION_ID" => Some(AuditFieldValue.CorrelationId) + case "TASK_ID" => Some(AuditFieldValue.TaskId) + case "ERROR_TYPE" => Some(AuditFieldValue.ErrorType) + case "ERROR_MESSAGE" => Some(AuditFieldValue.ErrorMessage) + case "TYPE" => Some(AuditFieldValue.Type) + case "HTTP_METHOD" => Some(AuditFieldValue.HttpMethod) + case "HTTP_HEADER_NAMES" => Some(AuditFieldValue.HttpHeaderNames) + case "HTTP_PATH" => Some(AuditFieldValue.HttpPath) + case "X_FORWARDED_FOR_HTTP_HEADER" => Some(AuditFieldValue.XForwardedForHttpHeader) + case "REMOTE_ADDRESS" => Some(AuditFieldValue.RemoteAddress) + case "LOCAL_ADDRESS" => Some(AuditFieldValue.LocalAddress) + case "CONTENT" => Some(AuditFieldValue.Content) + case "CONTENT_LENGTH_IN_BYTES" => Some(AuditFieldValue.ContentLengthInBytes) + case "CONTENT_LENGTH_IN_KB" => Some(AuditFieldValue.ContentLengthInKb) + case "ES_NODE_NAME" => Some(AuditFieldValue.EsNodeName) + case "ES_CLUSTER_NAME" => Some(AuditFieldValue.EsClusterName) + } + + } + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala similarity index 51% rename from audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala rename to core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala index acf8ecb02e..13e7a7dbee 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ConfigurableQueryAuditLogSerializer.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala @@ -14,17 +14,18 @@ * You should have received a copy of the GNU General Public License * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ */ -package tech.beshu.ror.audit.instances +package tech.beshu.ror.accesscontrol.audit.configurable import org.json.JSONObject -import tech.beshu.ror.audit.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFields} -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, BaseAuditLogSerializer} +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.instances.DefaultAuditLogSerializer +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditFieldValue, AuditResponseContext, AuditSerializationHelper} -class ConfigurableQueryAuditLogSerializer(val environmentContext: AuditEnvironmentContext, - val allowedEventSerializationMode: AllowedEventSerializationMode, - val fields: AuditFields) extends DefaultAuditLogSerializer(environmentContext) { +class ConfigurableAuditLogSerializer(val environmentContext: AuditEnvironmentContext, + val allowedEventSerializationMode: AllowedEventSerializationMode, + val fields: Map[AuditFieldName, AuditFieldValue]) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - BaseAuditLogSerializer.serialize(responseContext, environmentContext, fields.value, allowedEventSerializationMode) + AuditSerializationHelper.serialize(responseContext, Some(environmentContext), fields, allowedEventSerializationMode) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala index 7bf1a6d24a..1164f7f7fd 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala @@ -111,6 +111,7 @@ class RawRorConfigBasedCoreFactory(esVersion: EsVersion, case Right(settings) => Right(settings) case Left(failure) => + println(failure) Left(NonEmptyList.one(failure.aclCreationError.getOrElse(GeneralReadonlyrestSettingsError(Message(s"Malformed settings"))))) } case Left(errors) => diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 949223aeef..2ccf1893d6 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -18,22 +18,23 @@ package tech.beshu.ror.accesscontrol.factory.decoders import cats.data.NonEmptyList import io.circe.Decoder.* -import io.circe.{Decoder, HCursor} +import io.circe.{Decoder, DecodingFailure, HCursor, KeyDecoder} import io.lemonlabs.uri.Uri import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.audit.AuditingTool import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink.Config import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink.Config.{EsDataStreamBasedSink, EsIndexBasedSink, LogBasedSink} +import tech.beshu.ror.accesscontrol.audit.configurable.{AuditFieldValueDeserializer, ConfigurableAuditLogSerializer} import tech.beshu.ror.accesscontrol.domain.RorAuditIndexTemplate.CreationError import tech.beshu.ror.accesscontrol.domain.{AuditCluster, RorAuditDataStream, RorAuditIndexTemplate, RorAuditLoggerName} import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.{AuditingSettingsCreationError, Reason} import tech.beshu.ror.accesscontrol.factory.decoders.common.{lemonLabsUriDecoder, nonEmptyStringDecoder} -import tech.beshu.ror.accesscontrol.utils.CirceOps.DecodingFailureOps +import tech.beshu.ror.accesscontrol.utils.CirceOps.{AclCreationErrorCoders, DecodingFailureOps} import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator -import tech.beshu.ror.audit.BaseAuditLogSerializer.AllowedEventSerializationMode.* -import tech.beshu.ror.audit.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFields} +import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode.* +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} import tech.beshu.ror.audit.adapters.* import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditFieldValue, AuditLogSerializer} import tech.beshu.ror.es.EsVersion @@ -210,82 +211,81 @@ object AuditingSettingsDecoder extends Logging { given auditLogSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => for { - serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Option[Boolean]] - given Option[AllowedEventSerializationMode] = serializeAllAllowedEvents.map(c => if (c) SerializeAllAllowedEvents else SerializeOnlyAllowedEventsWithInfoLevelVerbose) - fields <- c.downField("fields").as[Option[Map[String, AuditFieldValue]]] - given Option[AuditFields] = fields.map(AuditFields.apply) - serializer <- c.downField("serializer").as[Option[AuditLogSerializer]](decodeOption(auditLogSerializerInstanceDecoder)) + serializer <- c.as[Option[AuditLogSerializer]](decodeOption(auditLogSerializerInstanceDecoder)) legacyFieldNameSerializer <- c.downField("audit_serializer").as[Option[AuditLogSerializer]](decodeOption(auditLogSerializerInstanceDecoder)) } yield serializer.orElse(legacyFieldNameSerializer) } - @nowarn("cat=deprecation") - given auditLogSerializerInstanceDecoder(using context: AuditEnvironmentContext, - allowedEventSerializationModeOpt: Option[AllowedEventSerializationMode], - fieldsOpt: Option[AuditFields]): Decoder[AuditLogSerializer] = - SyncDecoderCreator - .from(Decoder.decodeString) - .emapE { fullClassName => - val clazz = Class.forName(fullClassName) - - def createInstanceOfEnvironmentAwareSerializer(): Option[Either[String, Any]] = { - Try(clazz.getConstructor(classOf[AuditEnvironmentContext])) - .map(_.newInstance(summon[AuditEnvironmentContext])) - .toOption.map(_.asRight) - } - - def createInstanceOfCustomizableSerializer(): Option[Either[String, Any]] = { - Try( - clazz.getConstructor(classOf[AuditEnvironmentContext], classOf[AllowedEventSerializationMode], classOf[AuditFields]) - ).toOption.map { constructor => + private def auditLogSerializerInstanceDecoder(using context: AuditEnvironmentContext): Decoder[AuditLogSerializer] = SyncDecoderCreator.from( + Decoder.instance[AuditLogSerializer] { c => + for { + configurable <- c.downField("configurable").as[Option[Boolean]] + serializer <- if (configurable.contains(true)) { for { - allowedEventSerializationMode <- allowedEventSerializationModeOpt.toRight(s"Configurable serializer is used, but the serialize_all_allowed_events setting is missing in configuration") - fields <- fieldsOpt.toRight(s"Configurable serializer is used, but the fields setting is missing in configuration") - } yield constructor.newInstance(context, allowedEventSerializationMode, fields) + serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Boolean] + allowedEventSerializationMode = if (serializeAllAllowedEvents) SerializeAllAllowedEvents else SerializeOnlyAllowedEventsWithInfoLevelVerbose + fields <- c.downField("fields").as[Map[AuditFieldName, AuditFieldValue]] + serializer = new ConfigurableAuditLogSerializer(context, allowedEventSerializationMode, fields) + } yield serializer + } else { + for { + fullClassName <- c.downField("serializer").as[String] + serializer <- createSerializerInstanceFromClassName(fullClassName) + .left.map(error => DecodingFailure(AclCreationErrorCoders.stringify(error), Nil)) + } yield serializer } - } - - def createInstanceOfSimpleSerializer(): Option[Either[String, Any]] = { - Try(clazz.getDeclaredConstructor()) - .map(_.newInstance()) - .toOption.map(_.asRight) - } + } yield serializer + } + ) + .decoder - val serializer = - createInstanceOfCustomizableSerializer() - .orElse(createInstanceOfEnvironmentAwareSerializer()) - .orElse(createInstanceOfSimpleSerializer()) - .getOrElse( - throw new IllegalStateException( - s"Class ${Class.forName(fullClassName).getName} is required to have either one (AuditEnvironmentContext) parameter constructor or constructor without parameters" - ) - ) + @nowarn("cat=deprecation") + private def createSerializerInstanceFromClassName(fullClassName: String) + (using context: AuditEnvironmentContext): Either[AuditingSettingsCreationError, AuditLogSerializer] = { + val clazz = Class.forName(fullClassName) + + def createInstanceOfEnvironmentAwareSerializer(): Try[Any] = + Try(clazz.getConstructor(classOf[AuditEnvironmentContext])).map(_.newInstance(context)) + + def createInstanceOfSimpleSerializer(): Try[Any] = + Try(clazz.getDeclaredConstructor()).map(_.newInstance()) + + val serializer = + createInstanceOfEnvironmentAwareSerializer() + .orElse(createInstanceOfSimpleSerializer()) + .getOrElse( + throw new IllegalStateException( + s"Class ${Class.forName(fullClassName).getName} is required to have either one (AuditEnvironmentContext) parameter constructor or constructor without parameters" + ) + ) - Try { - serializer.map { - case serializer: tech.beshu.ror.audit.AuditLogSerializer => - Some(serializer) - case serializer: tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer => - Some(new EnvironmentAwareAuditLogSerializerAdapter(serializer, summon[AuditEnvironmentContext])) - case serializer: tech.beshu.ror.requestcontext.AuditLogSerializer[_] => - Some(new DeprecatedAuditLogSerializerAdapter(serializer)) - case _ => None - } - } match { - case Success(Right(Some(customSerializer))) => - logger.info(s"Using custom serializer: ${customSerializer.getClass.getName}") - Right(customSerializer) - case Success(Right(None)) => Left(AuditingSettingsCreationError(Message(s"Class ${fullClassName.show} is not a subclass of ${classOf[AuditLogSerializer].getName.show} or ${classOf[tech.beshu.ror.requestcontext.AuditLogSerializer[_]].getName.show}"))) - case Success(Left(message)) => Left(AuditingSettingsCreationError(Message(message))) - case Failure(ex) => Left(AuditingSettingsCreationError(Message(s"Cannot create instance of class '${fullClassName.show}', error: ${ex.getMessage.show}"))) - } + Try { + serializer match { + case serializer: tech.beshu.ror.audit.AuditLogSerializer => + Some(serializer) + case serializer: tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer => + Some(new EnvironmentAwareAuditLogSerializerAdapter(serializer, summon[AuditEnvironmentContext])) + case serializer: tech.beshu.ror.requestcontext.AuditLogSerializer[_] => + Some(new DeprecatedAuditLogSerializerAdapter(serializer)) + case _ => None } - .decoder + } match { + case Success(Some(customSerializer)) => + logger.info(s"Using custom serializer: ${customSerializer.getClass.getName}") + Right(customSerializer) + case Success(None) => Left(AuditingSettingsCreationError(Message(s"Class ${fullClassName.show} is not a subclass of ${classOf[AuditLogSerializer].getName.show} or ${classOf[tech.beshu.ror.requestcontext.AuditLogSerializer[_]].getName.show}"))) + case Failure(ex) => Left(AuditingSettingsCreationError(Message(s"Cannot create instance of class '${fullClassName.show}', error: ${ex.getMessage.show}"))) + } + } + + given auditFieldNameDecoder: KeyDecoder[AuditFieldName] = { + KeyDecoder.decodeKeyString.map(AuditFieldName.apply) + } - given auditValueDecoder: Decoder[AuditFieldValue] = { + given auditFieldValueDecoder: Decoder[AuditFieldValue] = { SyncDecoderCreator .from(Decoder.decodeString) - .emap(AuditFieldValue.fromString(_)) + .emap(AuditFieldValueDeserializer.deserialize) .decoder } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index 621d3c36d9..a500a35143 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -26,6 +26,7 @@ import org.scalatest.matchers.should.Matchers.* import org.scalatest.wordspec.AnyWordSpec import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink.Config +import tech.beshu.ror.accesscontrol.audit.configurable.ConfigurableAuditLogSerializer import tech.beshu.ror.accesscontrol.blocks.mocks.NoOpMocksProvider import tech.beshu.ror.accesscontrol.domain.AuditCluster.{LocalAuditCluster, RemoteAuditCluster} import tech.beshu.ror.accesscontrol.domain.{AuditCluster, IndexName, RorAuditLoggerName, RorConfigurationIndex} @@ -33,10 +34,9 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.{Core, RawRorConfigBasedCoreFactory} import tech.beshu.ror.audit.* -import tech.beshu.ror.audit.{AuditFieldValuePlaceholder => Placeholder} -import tech.beshu.ror.audit.BaseAuditLogSerializer.{AllowedEventSerializationMode, AuditFields} +import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode import tech.beshu.ror.audit.adapters.{DeprecatedAuditLogSerializerAdapter, EnvironmentAwareAuditLogSerializerAdapter} -import tech.beshu.ror.audit.instances.{ConfigurableQueryAuditLogSerializer, DefaultAuditLogSerializer, QueryAuditLogSerializer} +import tech.beshu.ror.audit.instances.{DefaultAuditLogSerializer, QueryAuditLogSerializer} import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.mocks.{MockHttpClientsFactory, MockLdapConnectionPoolProvider} @@ -383,7 +383,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | serializer: "tech.beshu.ror.audit.instances.ConfigurableQueryAuditLogSerializer" + | serializer: "tech.beshu.ror.audit.instances.ConfigurableAuditLogSerializer" | serialize_all_allowed_events: false | fields: | node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" @@ -399,22 +399,22 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertLogBasedAuditSinkSettingsPresent[ConfigurableQueryAuditLogSerializer]( + assertLogBasedAuditSinkSettingsPresent[ConfigurableAuditLogSerializer]( config, expectedLoggerName = "readonlyrest_audit" ) - val configuredSerializer = serializer(config).asInstanceOf[ConfigurableQueryAuditLogSerializer] + val configuredSerializer = serializer(config).asInstanceOf[ConfigurableAuditLogSerializer] configuredSerializer.environmentContext.esClusterName shouldBe testAuditEnvironmentContext.esClusterName configuredSerializer.environmentContext.esNodeName shouldBe testAuditEnvironmentContext.esNodeName configuredSerializer.allowedEventSerializationMode shouldBe AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose - configuredSerializer.fields shouldBe AuditFields(Map( - "node_name_with_static_suffix" -> AuditFieldValue(List(Placeholder.EsNodeName, Placeholder.StaticText(" with suffix"))), - "another_field" -> AuditFieldValue(List(Placeholder.EsClusterName, Placeholder.StaticText(" "), Placeholder.HttpMethod)), - "tid" -> AuditFieldValue(List(Placeholder.TaskId)), - "bytes" -> AuditFieldValue(List(Placeholder.ContentLengthInBytes)) - )) + configuredSerializer.fields shouldBe Map( + "node_name_with_static_suffix" -> AuditFieldValue.Combined(List(AuditFieldValue.EsNodeName, AuditFieldValue.StaticText(" with suffix"))), + "another_field" -> AuditFieldValue.Combined(List(AuditFieldValue.EsClusterName, AuditFieldValue.StaticText(" "), AuditFieldValue.HttpMethod)), + "tid" -> AuditFieldValue.Combined(List(AuditFieldValue.TaskId)), + "bytes" -> AuditFieldValue.Combined(List(AuditFieldValue.ContentLengthInBytes)) + ) } } "'index' output type defined" when { @@ -1293,7 +1293,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | serializer: "tech.beshu.ror.audit.instances.ConfigurableQueryAuditLogSerializer" + | serializer: "tech.beshu.ror.audit.instances.ConfigurableAuditLogSerializer" | | access_control_rules: | @@ -1316,7 +1316,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | serializer: "tech.beshu.ror.audit.instances.ConfigurableQueryAuditLogSerializer" + | serializer: "tech.beshu.ror.audit.instances.ConfigurableAuditLogSerializer" | serialize_all_allowed_events: false | access_control_rules: | diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index a2da40e3fb..af96612223 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -94,7 +94,7 @@ class LocalClusterAuditingToolsSuite "using ReportingAllEventsAuditLogSerializer" in { val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) - updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ReportingAllEventsAuditLogSerializer") + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ReportingAllTypesOfEventsAuditLogSerializer") performAndAssertExampleSearchRequest(indexManager) forEachAuditManager { adminAuditManager => @@ -122,7 +122,7 @@ class LocalClusterAuditingToolsSuite "using ReportingAllEventsWithQueryAuditLogSerializer" in { val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) - updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ReportingAllEventsWithQueryAuditLogSerializer") + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ReportingAllTypesOfEventsWithQueryAuditLogSerializer") performAndAssertExampleSearchRequest(indexManager) forEachAuditManager { adminAuditManager => @@ -149,7 +149,10 @@ class LocalClusterAuditingToolsSuite "using ConfigurableQueryAuditLogSerializer" in { val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) - updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ConfigurableQueryAuditLogSerializer") + updateRorConfig( + originalString = """serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""", + newString = "configurable: true", + ) performAndAssertExampleSearchRequest(indexManager) forEachAuditManager { adminAuditManager => @@ -175,10 +178,14 @@ class LocalClusterAuditingToolsSuite response should have statusCode 200 } - private def updateRorConfigToUseSerializer(serializer: String) = { + private def updateRorConfigToUseSerializer(serializer: String) = updateRorConfig( + originalString = """serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""", + newString = s"""serializer: "$serializer"""" + ) + + private def updateRorConfig(originalString: String, newString: String) = { val initialConfig = getResourceContent(rorConfigFileName) - val serializerUsedInOriginalConfigFile = """serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""" - val modifiedConfig = initialConfig.replace(serializerUsedInOriginalConfigFile, s"""serializer: "$serializer"""") + val modifiedConfig = initialConfig.replace(originalString, newString) rorApiManager.updateRorInIndexConfig(modifiedConfig).forceOKStatusOrConfigAlreadyLoaded() rorApiManager.reloadRorConfig().force() } From 0e598b043ade743ea3c194066f6867cadaebe187 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 17 Aug 2025 20:41:30 +0200 Subject: [PATCH 14/27] adjustments --- .../RawRorConfigBasedCoreFactory.scala | 1 - .../decoders/AuditingSettingsDecoder.scala | 35 +++++++++++-------- .../unit/acl/factory/AuditSettingsTests.scala | 16 ++++----- 3 files changed, 28 insertions(+), 24 deletions(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala index 1164f7f7fd..7bf1a6d24a 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala @@ -111,7 +111,6 @@ class RawRorConfigBasedCoreFactory(esVersion: EsVersion, case Right(settings) => Right(settings) case Left(failure) => - println(failure) Left(NonEmptyList.one(failure.aclCreationError.getOrElse(GeneralReadonlyrestSettingsError(Message(s"Malformed settings"))))) } case Left(errors) => diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 2ccf1893d6..6008bc8559 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -34,7 +34,7 @@ import tech.beshu.ror.accesscontrol.factory.decoders.common.{lemonLabsUriDecoder import tech.beshu.ror.accesscontrol.utils.CirceOps.{AclCreationErrorCoders, DecodingFailureOps} import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode.* -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.AuditSerializationHelper.AuditFieldName import tech.beshu.ror.audit.adapters.* import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditFieldValue, AuditLogSerializer} import tech.beshu.ror.es.EsVersion @@ -209,36 +209,41 @@ object AuditingSettingsDecoder extends Logging { } .decoder - given auditLogSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => - for { - serializer <- c.as[Option[AuditLogSerializer]](decodeOption(auditLogSerializerInstanceDecoder)) - legacyFieldNameSerializer <- c.downField("audit_serializer").as[Option[AuditLogSerializer]](decodeOption(auditLogSerializerInstanceDecoder)) - } yield serializer.orElse(legacyFieldNameSerializer) - } - - private def auditLogSerializerInstanceDecoder(using context: AuditEnvironmentContext): Decoder[AuditLogSerializer] = SyncDecoderCreator.from( - Decoder.instance[AuditLogSerializer] { c => + given auditLogSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = SyncDecoderCreator.from( + Decoder.instance[Option[AuditLogSerializer]] { c => for { configurable <- c.downField("configurable").as[Option[Boolean]] serializer <- if (configurable.contains(true)) { for { serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Boolean] + .left.map(withAuditingSettingsCreationErrorMessage("Configurable serializer is used, but the serialize_all_allowed_events setting is missing in configuration")) allowedEventSerializationMode = if (serializeAllAllowedEvents) SerializeAllAllowedEvents else SerializeOnlyAllowedEventsWithInfoLevelVerbose fields <- c.downField("fields").as[Map[AuditFieldName, AuditFieldValue]] + .left.map(withAuditingSettingsCreationErrorMessage("Configurable serializer is used, but the fields setting is missing in configuration")) serializer = new ConfigurableAuditLogSerializer(context, allowedEventSerializationMode, fields) - } yield serializer + } yield Some(serializer) } else { for { - fullClassName <- c.downField("serializer").as[String] - serializer <- createSerializerInstanceFromClassName(fullClassName) - .left.map(error => DecodingFailure(AclCreationErrorCoders.stringify(error), Nil)) - } yield serializer + fullClassNameOpt <- c.downField("serializer").as[Option[String]] + legacyFullClassNameOpt <- c.downField("audit_serializer").as[Option[String]] + serializerOpt <- fullClassNameOpt.orElse(legacyFullClassNameOpt) match { + case Some(fullClassName) => + createSerializerInstanceFromClassName(fullClassName).map(Some(_)) + .left.map(error => DecodingFailure(AclCreationErrorCoders.stringify(error), Nil)) + case None => + Right(None) + } + } yield serializerOpt } } yield serializer } ) .decoder + private def withAuditingSettingsCreationErrorMessage(message: String)(decodingFailure: DecodingFailure) = { + decodingFailure.withMessage(AclCreationErrorCoders.stringify(AuditingSettingsCreationError(Message(message)))) + } + @nowarn("cat=deprecation") private def createSerializerInstanceFromClassName(fullClassName: String) (using context: AuditEnvironmentContext): Either[AuditingSettingsCreationError, AuditLogSerializer] = { diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index a500a35143..8e4ce66060 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -34,7 +34,7 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.{Core, RawRorConfigBasedCoreFactory} import tech.beshu.ror.audit.* -import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} import tech.beshu.ror.audit.adapters.{DeprecatedAuditLogSerializerAdapter, EnvironmentAwareAuditLogSerializerAdapter} import tech.beshu.ror.audit.instances.{DefaultAuditLogSerializer, QueryAuditLogSerializer} import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} @@ -383,7 +383,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | serializer: "tech.beshu.ror.audit.instances.ConfigurableAuditLogSerializer" + | configurable: true | serialize_all_allowed_events: false | fields: | node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" @@ -410,10 +410,10 @@ class AuditSettingsTests extends AnyWordSpec with Inside { configuredSerializer.environmentContext.esNodeName shouldBe testAuditEnvironmentContext.esNodeName configuredSerializer.allowedEventSerializationMode shouldBe AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose configuredSerializer.fields shouldBe Map( - "node_name_with_static_suffix" -> AuditFieldValue.Combined(List(AuditFieldValue.EsNodeName, AuditFieldValue.StaticText(" with suffix"))), - "another_field" -> AuditFieldValue.Combined(List(AuditFieldValue.EsClusterName, AuditFieldValue.StaticText(" "), AuditFieldValue.HttpMethod)), - "tid" -> AuditFieldValue.Combined(List(AuditFieldValue.TaskId)), - "bytes" -> AuditFieldValue.Combined(List(AuditFieldValue.ContentLengthInBytes)) + AuditFieldName("node_name_with_static_suffix") -> AuditFieldValue.Combined(List(AuditFieldValue.EsNodeName, AuditFieldValue.StaticText(" with suffix"))), + AuditFieldName("another_field") -> AuditFieldValue.Combined(List(AuditFieldValue.EsClusterName, AuditFieldValue.StaticText(" "), AuditFieldValue.HttpMethod)), + AuditFieldName("tid") -> AuditFieldValue.TaskId, + AuditFieldName("bytes") -> AuditFieldValue.ContentLengthInBytes, ) } } @@ -1293,7 +1293,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | serializer: "tech.beshu.ror.audit.instances.ConfigurableAuditLogSerializer" + | configurable: true | | access_control_rules: | @@ -1316,7 +1316,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | serializer: "tech.beshu.ror.audit.instances.ConfigurableAuditLogSerializer" + | configurable: true | serialize_all_allowed_events: false | access_control_rules: | From ff9347c68d88be47fecac2982ab3fab816c40dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 17 Aug 2025 21:50:42 +0200 Subject: [PATCH 15/27] fix audit cross compile --- .../scala/tech/beshu/ror/audit/AuditSerializationHelper.scala | 4 ++-- .../ror/audit/instances/DefaultAuditLogSerializerV1.scala | 2 +- .../ror/audit/instances/DefaultAuditLogSerializerV2.scala | 2 +- .../beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala | 2 +- .../beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala index 43970814fd..17e5daad77 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala @@ -17,12 +17,12 @@ package tech.beshu.ror.audit import org.json.JSONObject -import tech.beshu.ror.audit.AuditResponseContext.* +import tech.beshu.ror.audit.AuditResponseContext._ import tech.beshu.ror.audit.instances.SerializeUser import java.time.ZoneId import java.time.format.DateTimeFormatter -import scala.collection.JavaConverters.* +import scala.collection.JavaConverters._ import scala.concurrent.duration.FiniteDuration private[ror] object AuditSerializationHelper { diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index 385467fa03..6cdb473a8c 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -17,7 +17,7 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.* +import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index 263c53fe12..dda193aa31 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -17,7 +17,7 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.* +import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} import tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer.environmentRelatedAuditFields import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index 8216d1fe76..c266aed64e 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -17,7 +17,7 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.* +import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1.queryV1AuditFields diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index de5e41ea02..9aec8d1100 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -17,7 +17,7 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.* +import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} import tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer.environmentRelatedAuditFields import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields From 0ab38e6997c917cf7f211d7cc818fa3cd57474a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Tue, 19 Aug 2025 20:28:22 +0200 Subject: [PATCH 16/27] review changes --- .../scala/tech/beshu/ror/audit/AuditFieldValue.scala | 4 ++++ .../configurable/AuditFieldValueDeserializer.scala | 1 + .../factory/decoders/AuditingSettingsDecoder.scala | 10 ++++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala index 3da8571a40..33fe1ec7ee 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala @@ -74,10 +74,14 @@ private[ror] object AuditFieldValue { case object ContentLengthInKb extends AuditFieldValue + // ES environment + case object EsNodeName extends AuditFieldValue case object EsClusterName extends AuditFieldValue + // Technical + final case class StaticText(value: String) extends AuditFieldValue final case class Combined(values: List[AuditFieldValue]) extends AuditFieldValue diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala index 51297f8932..580a53b825 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala @@ -84,6 +84,7 @@ object AuditFieldValueDeserializer { case "CONTENT_LENGTH_IN_KB" => Some(AuditFieldValue.ContentLengthInKb) case "ES_NODE_NAME" => Some(AuditFieldValue.EsNodeName) case "ES_CLUSTER_NAME" => Some(AuditFieldValue.EsClusterName) + case _ => None } } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 6008bc8559..542acf5b87 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -247,7 +247,13 @@ object AuditingSettingsDecoder extends Logging { @nowarn("cat=deprecation") private def createSerializerInstanceFromClassName(fullClassName: String) (using context: AuditEnvironmentContext): Either[AuditingSettingsCreationError, AuditLogSerializer] = { - val clazz = Class.forName(fullClassName) + val clazz = Try(Class.forName(fullClassName)).fold( + { + case _: ClassNotFoundException => throw new IllegalStateException(s"Serializer with class name $fullClassName not found.") + case other => throw other + }, + identity + ) def createInstanceOfEnvironmentAwareSerializer(): Try[Any] = Try(clazz.getConstructor(classOf[AuditEnvironmentContext])).map(_.newInstance(context)) @@ -260,7 +266,7 @@ object AuditingSettingsDecoder extends Logging { .orElse(createInstanceOfSimpleSerializer()) .getOrElse( throw new IllegalStateException( - s"Class ${Class.forName(fullClassName).getName} is required to have either one (AuditEnvironmentContext) parameter constructor or constructor without parameters" + s"Class ${clazz.getName} is required to have either one (AuditEnvironmentContext) parameter constructor or constructor without parameters" ) ) From 38b84496b12ee0da08b1432eaa20f4a822f3fde2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Wed, 27 Aug 2025 23:38:57 +0200 Subject: [PATCH 17/27] review changes --- .../beshu/ror/audit/AuditFieldValue.scala | 89 -------- .../ror/audit/AuditSerializationHelper.scala | 211 ++++++++++++------ .../EnvironmentAwareAuditLogSerializer.scala | 8 +- .../DefaultAuditLogSerializerV1.scala | 55 ++--- .../DefaultAuditLogSerializerV2.scala | 33 ++- ...zer.scala => FullAuditLogSerializer.scala} | 6 +- ... => FullAuditLogWithQuerySerializer.scala} | 6 +- .../instances/QueryAuditLogSerializerV1.scala | 12 +- .../instances/QueryAuditLogSerializerV2.scala | 11 +- .../AuditFieldValueDeserializer.scala | 74 +++--- .../ConfigurableAuditLogSerializer.scala | 10 +- .../decoders/AuditingSettingsDecoder.scala | 37 ++- .../unit/acl/factory/AuditSettingsTests.scala | 23 +- 13 files changed, 306 insertions(+), 269 deletions(-) delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala rename audit/src/main/scala/tech/beshu/ror/audit/instances/{ReportingAllTypesOfEventsAuditLogSerializer.scala => FullAuditLogSerializer.scala} (83%) rename audit/src/main/scala/tech/beshu/ror/audit/instances/{ReportingAllTypesOfEventsWithQueryAuditLogSerializer.scala => FullAuditLogWithQuerySerializer.scala} (82%) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala deleted file mode 100644 index 33fe1ec7ee..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala +++ /dev/null @@ -1,89 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit - -private[ror] sealed trait AuditFieldValue - -private[ror] object AuditFieldValue { - - // Rule - case object IsMatched extends AuditFieldValue - - case object FinalState extends AuditFieldValue - - case object Reason extends AuditFieldValue - - case object User extends AuditFieldValue - - case object ImpersonatedByUser extends AuditFieldValue - - case object Action extends AuditFieldValue - - case object InvolvedIndices extends AuditFieldValue - - case object AclHistory extends AuditFieldValue - - case object ProcessingDurationMillis extends AuditFieldValue - - // Identifiers - case object Timestamp extends AuditFieldValue - - case object Id extends AuditFieldValue - - case object CorrelationId extends AuditFieldValue - - case object TaskId extends AuditFieldValue - - // Error details - case object ErrorType extends AuditFieldValue - - case object ErrorMessage extends AuditFieldValue - - case object Type extends AuditFieldValue - - // HTTP protocol values - case object HttpMethod extends AuditFieldValue - - case object HttpHeaderNames extends AuditFieldValue - - case object HttpPath extends AuditFieldValue - - case object XForwardedForHttpHeader extends AuditFieldValue - - case object RemoteAddress extends AuditFieldValue - - case object LocalAddress extends AuditFieldValue - - case object Content extends AuditFieldValue - - case object ContentLengthInBytes extends AuditFieldValue - - case object ContentLengthInKb extends AuditFieldValue - - // ES environment - - case object EsNodeName extends AuditFieldValue - - case object EsClusterName extends AuditFieldValue - - // Technical - - final case class StaticText(value: String) extends AuditFieldValue - - final case class Combined(values: List[AuditFieldValue]) extends AuditFieldValue - -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala index 17e5daad77..ef52b70652 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala @@ -17,12 +17,12 @@ package tech.beshu.ror.audit import org.json.JSONObject -import tech.beshu.ror.audit.AuditResponseContext._ +import tech.beshu.ror.audit.AuditResponseContext.* import tech.beshu.ror.audit.instances.SerializeUser import java.time.ZoneId import java.time.format.DateTimeFormatter -import scala.collection.JavaConverters._ +import scala.collection.JavaConverters.* import scala.concurrent.duration.FiniteDuration private[ror] object AuditSerializationHelper { @@ -31,85 +31,82 @@ private[ror] object AuditSerializationHelper { def serialize(responseContext: AuditResponseContext, environmentContext: Option[AuditEnvironmentContext], - fields: Map[AuditFieldName, AuditFieldValue], - allowedEventSerializationMode: AllowedEventSerializationMode): Option[JSONObject] = responseContext match { + fields: Map[AuditFieldName, AuditFieldValueDescriptor], + allowedEventMode: AllowedEventMode): Option[JSONObject] = responseContext match { case Allowed(requestContext, verbosity, reason) => - (verbosity, allowedEventSerializationMode) match { - case (Verbosity.Error, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) => - None - case (Verbosity.Info, AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose) | - (_, AllowedEventSerializationMode.SerializeAllAllowedEvents) => - Some(createEntry(fields, matched = true, "ALLOWED", reason, responseContext.duration, requestContext, environmentContext, None)) - } + allowedEvent( + allowedEventMode, + verbosity, + createEntry(fields, EventData(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, environmentContext, None)), + ) case ForbiddenBy(requestContext, _, reason) => - Some(createEntry(fields, matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, environmentContext, None)) + Some(createEntry(fields, EventData(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, environmentContext, None))) case Forbidden(requestContext) => - Some(createEntry(fields, matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, environmentContext, None)) + Some(createEntry(fields, EventData(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, environmentContext, None))) case RequestedIndexNotExist(requestContext) => - Some(createEntry(fields, matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, environmentContext, None)) + Some(createEntry(fields, EventData(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, environmentContext, None))) case Errored(requestContext, cause) => - Some(createEntry(fields, matched = false, "ERRORED", "error", responseContext.duration, requestContext, environmentContext, Some(cause))) + Some(createEntry(fields, EventData(matched = false, "ERRORED", "error", responseContext.duration, requestContext, environmentContext, Some(cause)))) } - private def createEntry(fields: Map[AuditFieldName, AuditFieldValue], - matched: Boolean, - finalState: String, - reason: String, - duration: FiniteDuration, - requestContext: AuditRequestContext, - environmentContext: Option[AuditEnvironmentContext], - error: Option[Throwable]) = { + private def allowedEvent(allowedEventMode: AllowedEventMode, verbosity: Verbosity, entry: JSONObject) = { + allowedEventMode match { + case AllowedEventMode.IncludeAll => + Some(entry) + case AllowedEventMode.Include(types) if types.contains(verbosity) => + Some(entry) + case _ => + None + } + } + + private def createEntry(fields: Map[AuditFieldName, AuditFieldValueDescriptor], + eventData: EventData) = { val resolvedFields: Map[String, Any] = { Map( - "@timestamp" -> timestampFormatter.format(requestContext.timestamp) + "@timestamp" -> timestampFormatter.format(eventData.requestContext.timestamp) ) ++ fields.map { case (fieldName, fieldValue) => - fieldName.value -> resolvePlaceholder(fieldValue, matched, finalState, reason, duration, requestContext, environmentContext, error) + fieldName.value -> resolver(eventData)(fieldValue) } } resolvedFields .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } - .mergeWith(requestContext.generalAuditEvents) + .mergeWith(eventData.requestContext.generalAuditEvents) } - private def resolvePlaceholder(auditValue: AuditFieldValue, - matched: Boolean, - finalState: String, - reason: String, - duration: FiniteDuration, - requestContext: AuditRequestContext, - environmentContext: Option[AuditEnvironmentContext], - error: Option[Throwable]): Any = { + private def resolver(eventData: EventData): AuditFieldValueDescriptor => Any = auditValue => { + val requestContext = eventData.requestContext auditValue match { - case AuditFieldValue.IsMatched => matched - case AuditFieldValue.FinalState => finalState - case AuditFieldValue.Reason => reason - case AuditFieldValue.User => SerializeUser.serialize(requestContext).orNull - case AuditFieldValue.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull - case AuditFieldValue.Action => requestContext.action - case AuditFieldValue.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava - case AuditFieldValue.AclHistory => requestContext.history - case AuditFieldValue.ProcessingDurationMillis => duration.toMillis - case AuditFieldValue.Timestamp => timestampFormatter.format(requestContext.timestamp) - case AuditFieldValue.Id => requestContext.id - case AuditFieldValue.CorrelationId => requestContext.correlationId - case AuditFieldValue.TaskId => requestContext.taskId - case AuditFieldValue.ErrorType => error.map(_.getClass.getSimpleName).orNull - case AuditFieldValue.ErrorMessage => error.map(_.getMessage).orNull - case AuditFieldValue.Type => requestContext.`type` - case AuditFieldValue.HttpMethod => requestContext.httpMethod - case AuditFieldValue.HttpHeaderNames => requestContext.requestHeaders.names.asJava - case AuditFieldValue.HttpPath => requestContext.uriPath - case AuditFieldValue.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull - case AuditFieldValue.RemoteAddress => requestContext.remoteAddress - case AuditFieldValue.LocalAddress => requestContext.localAddress - case AuditFieldValue.Content => requestContext.content - case AuditFieldValue.ContentLengthInBytes => requestContext.contentLength - case AuditFieldValue.ContentLengthInKb => requestContext.contentLength / 1024 - case AuditFieldValue.EsNodeName => environmentContext.map(_.esNodeName).getOrElse("") - case AuditFieldValue.EsClusterName => environmentContext.map(_.esClusterName).getOrElse("") - case AuditFieldValue.StaticText(text) => text - case AuditFieldValue.Combined(values) => values.map(resolvePlaceholder(_, matched, finalState, reason, duration, requestContext, environmentContext, error)).mkString + case AuditFieldValueDescriptor.IsMatched => eventData.matched + case AuditFieldValueDescriptor.FinalState => eventData.finalState + case AuditFieldValueDescriptor.Reason => eventData.reason + case AuditFieldValueDescriptor.User => SerializeUser.serialize(requestContext).orNull + case AuditFieldValueDescriptor.ImpersonatedByUser => requestContext.impersonatedByUserName.orNull + case AuditFieldValueDescriptor.Action => requestContext.action + case AuditFieldValueDescriptor.InvolvedIndices => if (requestContext.involvesIndices) requestContext.indices.toList.asJava else List.empty.asJava + case AuditFieldValueDescriptor.AclHistory => requestContext.history + case AuditFieldValueDescriptor.ProcessingDurationMillis => eventData.duration.toMillis + case AuditFieldValueDescriptor.Timestamp => timestampFormatter.format(requestContext.timestamp) + case AuditFieldValueDescriptor.Id => requestContext.id + case AuditFieldValueDescriptor.CorrelationId => requestContext.correlationId + case AuditFieldValueDescriptor.TaskId => requestContext.taskId + case AuditFieldValueDescriptor.ErrorType => eventData.error.map(_.getClass.getSimpleName).orNull + case AuditFieldValueDescriptor.ErrorMessage => eventData.error.map(_.getMessage).orNull + case AuditFieldValueDescriptor.Type => requestContext.`type` + case AuditFieldValueDescriptor.HttpMethod => requestContext.httpMethod + case AuditFieldValueDescriptor.HttpHeaderNames => requestContext.requestHeaders.names.asJava + case AuditFieldValueDescriptor.HttpPath => requestContext.uriPath + case AuditFieldValueDescriptor.XForwardedForHttpHeader => requestContext.requestHeaders.getValue("X-Forwarded-For").flatMap(_.headOption).orNull + case AuditFieldValueDescriptor.RemoteAddress => requestContext.remoteAddress + case AuditFieldValueDescriptor.LocalAddress => requestContext.localAddress + case AuditFieldValueDescriptor.Content => requestContext.content + case AuditFieldValueDescriptor.ContentLengthInBytes => requestContext.contentLength + case AuditFieldValueDescriptor.ContentLengthInKb => requestContext.contentLength / 1024 + case AuditFieldValueDescriptor.EsNodeName => eventData.environmentContext.map(_.esNodeName).getOrElse("") + case AuditFieldValueDescriptor.EsClusterName => eventData.environmentContext.map(_.esClusterName).getOrElse("") + case AuditFieldValueDescriptor.StaticText(text) => text + case AuditFieldValueDescriptor.Combined(values) => values.map(resolver(eventData)).mkString } } @@ -128,14 +125,94 @@ private[ror] object AuditSerializationHelper { } } - sealed trait AllowedEventSerializationMode + private final case class EventData(matched: Boolean, + finalState: String, + reason: String, + duration: FiniteDuration, + requestContext: AuditRequestContext, + environmentContext: Option[AuditEnvironmentContext], + error: Option[Throwable]) + + sealed trait AllowedEventMode - object AllowedEventSerializationMode { - case object SerializeOnlyAllowedEventsWithInfoLevelVerbose extends AllowedEventSerializationMode + object AllowedEventMode { + case object IncludeAll extends AllowedEventMode - case object SerializeAllAllowedEvents extends AllowedEventSerializationMode + final case class Include(types: Set[Verbosity]) extends AllowedEventMode } final case class AuditFieldName(value: String) + sealed trait AuditFieldValueDescriptor + + object AuditFieldValueDescriptor { + + // Rule + case object IsMatched extends AuditFieldValueDescriptor + + case object FinalState extends AuditFieldValueDescriptor + + case object Reason extends AuditFieldValueDescriptor + + case object User extends AuditFieldValueDescriptor + + case object ImpersonatedByUser extends AuditFieldValueDescriptor + + case object Action extends AuditFieldValueDescriptor + + case object InvolvedIndices extends AuditFieldValueDescriptor + + case object AclHistory extends AuditFieldValueDescriptor + + case object ProcessingDurationMillis extends AuditFieldValueDescriptor + + // Identifiers + case object Timestamp extends AuditFieldValueDescriptor + + case object Id extends AuditFieldValueDescriptor + + case object CorrelationId extends AuditFieldValueDescriptor + + case object TaskId extends AuditFieldValueDescriptor + + // Error details + case object ErrorType extends AuditFieldValueDescriptor + + case object ErrorMessage extends AuditFieldValueDescriptor + + case object Type extends AuditFieldValueDescriptor + + // HTTP protocol values + case object HttpMethod extends AuditFieldValueDescriptor + + case object HttpHeaderNames extends AuditFieldValueDescriptor + + case object HttpPath extends AuditFieldValueDescriptor + + case object XForwardedForHttpHeader extends AuditFieldValueDescriptor + + case object RemoteAddress extends AuditFieldValueDescriptor + + case object LocalAddress extends AuditFieldValueDescriptor + + case object Content extends AuditFieldValueDescriptor + + case object ContentLengthInBytes extends AuditFieldValueDescriptor + + case object ContentLengthInKb extends AuditFieldValueDescriptor + + // ES environment + + case object EsNodeName extends AuditFieldValueDescriptor + + case object EsClusterName extends AuditFieldValueDescriptor + + // Technical + + final case class StaticText(value: String) extends AuditFieldValueDescriptor + + final case class Combined(values: List[AuditFieldValueDescriptor]) extends AuditFieldValueDescriptor + + } + } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala index b69f8a4380..1262f4a192 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala @@ -17,7 +17,7 @@ package tech.beshu.ror.audit import org.json.JSONObject -import tech.beshu.ror.audit.AuditSerializationHelper.AuditFieldName +import tech.beshu.ror.audit.AuditSerializationHelper.{AuditFieldName, AuditFieldValueDescriptor} trait EnvironmentAwareAuditLogSerializer { def onResponse(responseContext: AuditResponseContext, @@ -25,8 +25,8 @@ trait EnvironmentAwareAuditLogSerializer { } object EnvironmentAwareAuditLogSerializer { - val environmentRelatedAuditFields = Map( - AuditFieldName("es_node_name") -> AuditFieldValue.EsNodeName, - AuditFieldName("es_cluster_name") -> AuditFieldValue.EsClusterName + val environmentRelatedAuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( + AuditFieldName("es_node_name") -> AuditFieldValueDescriptor.EsNodeName, + AuditFieldName("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index 6cdb473a8c..bf0846c420 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -18,7 +18,8 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.AuditResponseContext.Verbosity +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields class DefaultAuditLogSerializerV1 extends AuditLogSerializer { @@ -28,36 +29,36 @@ class DefaultAuditLogSerializerV1 extends AuditLogSerializer { responseContext = responseContext, environmentContext = None, fields = defaultV1AuditFields, - allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + allowedEventMode = AllowedEventMode.Include(Set(Verbosity.Info)), ) } object DefaultAuditLogSerializerV1 { - val defaultV1AuditFields: Map[AuditFieldName, AuditFieldValue] = Map( - AuditFieldName("match") -> AuditFieldValue.IsMatched, - AuditFieldName("block") -> AuditFieldValue.Reason, - AuditFieldName("id") -> AuditFieldValue.Id, - AuditFieldName("final_state") -> AuditFieldValue.FinalState, - AuditFieldName("@timestamp") -> AuditFieldValue.Timestamp, - AuditFieldName("correlation_id") -> AuditFieldValue.CorrelationId, - AuditFieldName("processingMillis") -> AuditFieldValue.ProcessingDurationMillis, - AuditFieldName("error_type") -> AuditFieldValue.ErrorType, - AuditFieldName("error_message") -> AuditFieldValue.ErrorMessage, - AuditFieldName("content_len") -> AuditFieldValue.ContentLengthInBytes, - AuditFieldName("content_len_kb") -> AuditFieldValue.ContentLengthInKb, - AuditFieldName("type") -> AuditFieldValue.Type, - AuditFieldName("origin") -> AuditFieldValue.RemoteAddress, - AuditFieldName("destination") -> AuditFieldValue.LocalAddress, - AuditFieldName("xff") -> AuditFieldValue.XForwardedForHttpHeader, - AuditFieldName("task_id") -> AuditFieldValue.TaskId, - AuditFieldName("req_method") -> AuditFieldValue.HttpMethod, - AuditFieldName("headers") -> AuditFieldValue.HttpHeaderNames, - AuditFieldName("path") -> AuditFieldValue.HttpPath, - AuditFieldName("user") -> AuditFieldValue.User, - AuditFieldName("impersonated_by") -> AuditFieldValue.ImpersonatedByUser, - AuditFieldName("action") -> AuditFieldValue.Action, - AuditFieldName("indices") -> AuditFieldValue.InvolvedIndices, - AuditFieldName("acl_history") -> AuditFieldValue.AclHistory + val defaultV1AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( + AuditFieldName("match") -> AuditFieldValueDescriptor.IsMatched, + AuditFieldName("block") -> AuditFieldValueDescriptor.Reason, + AuditFieldName("id") -> AuditFieldValueDescriptor.Id, + AuditFieldName("final_state") -> AuditFieldValueDescriptor.FinalState, + AuditFieldName("@timestamp") -> AuditFieldValueDescriptor.Timestamp, + AuditFieldName("correlation_id") -> AuditFieldValueDescriptor.CorrelationId, + AuditFieldName("processingMillis") -> AuditFieldValueDescriptor.ProcessingDurationMillis, + AuditFieldName("error_type") -> AuditFieldValueDescriptor.ErrorType, + AuditFieldName("error_message") -> AuditFieldValueDescriptor.ErrorMessage, + AuditFieldName("content_len") -> AuditFieldValueDescriptor.ContentLengthInBytes, + AuditFieldName("content_len_kb") -> AuditFieldValueDescriptor.ContentLengthInKb, + AuditFieldName("type") -> AuditFieldValueDescriptor.Type, + AuditFieldName("origin") -> AuditFieldValueDescriptor.RemoteAddress, + AuditFieldName("destination") -> AuditFieldValueDescriptor.LocalAddress, + AuditFieldName("xff") -> AuditFieldValueDescriptor.XForwardedForHttpHeader, + AuditFieldName("task_id") -> AuditFieldValueDescriptor.TaskId, + AuditFieldName("req_method") -> AuditFieldValueDescriptor.HttpMethod, + AuditFieldName("headers") -> AuditFieldValueDescriptor.HttpHeaderNames, + AuditFieldName("path") -> AuditFieldValueDescriptor.HttpPath, + AuditFieldName("user") -> AuditFieldValueDescriptor.User, + AuditFieldName("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser, + AuditFieldName("action") -> AuditFieldValueDescriptor.Action, + AuditFieldName("indices") -> AuditFieldValueDescriptor.InvolvedIndices, + AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index dda193aa31..2b230d6de5 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -18,7 +18,8 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.AuditResponseContext.Verbosity +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer.environmentRelatedAuditFields import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields @@ -29,12 +30,36 @@ class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) e responseContext = responseContext, environmentContext = Some(environmentContext), fields = defaultV2AuditFields, - allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + allowedEventMode = AllowedEventMode.Include(Set(Verbosity.Info)), ) } object DefaultAuditLogSerializerV2 { - val defaultV2AuditFields: Map[AuditFieldName, AuditFieldValue] = - DefaultAuditLogSerializerV1.defaultV1AuditFields ++ environmentRelatedAuditFields + val defaultV2AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( + AuditFieldName("match") -> AuditFieldValueDescriptor.IsMatched, + AuditFieldName("block") -> AuditFieldValueDescriptor.Reason, + AuditFieldName("id") -> AuditFieldValueDescriptor.Id, + AuditFieldName("final_state") -> AuditFieldValueDescriptor.FinalState, + AuditFieldName("@timestamp") -> AuditFieldValueDescriptor.Timestamp, + AuditFieldName("correlation_id") -> AuditFieldValueDescriptor.CorrelationId, + AuditFieldName("processingMillis") -> AuditFieldValueDescriptor.ProcessingDurationMillis, + AuditFieldName("error_type") -> AuditFieldValueDescriptor.ErrorType, + AuditFieldName("error_message") -> AuditFieldValueDescriptor.ErrorMessage, + AuditFieldName("content_len") -> AuditFieldValueDescriptor.ContentLengthInBytes, + AuditFieldName("content_len_kb") -> AuditFieldValueDescriptor.ContentLengthInKb, + AuditFieldName("type") -> AuditFieldValueDescriptor.Type, + AuditFieldName("origin") -> AuditFieldValueDescriptor.RemoteAddress, + AuditFieldName("destination") -> AuditFieldValueDescriptor.LocalAddress, + AuditFieldName("xff") -> AuditFieldValueDescriptor.XForwardedForHttpHeader, + AuditFieldName("task_id") -> AuditFieldValueDescriptor.TaskId, + AuditFieldName("req_method") -> AuditFieldValueDescriptor.HttpMethod, + AuditFieldName("headers") -> AuditFieldValueDescriptor.HttpHeaderNames, + AuditFieldName("path") -> AuditFieldValueDescriptor.HttpPath, + AuditFieldName("user") -> AuditFieldValueDescriptor.User, + AuditFieldName("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser, + AuditFieldName("action") -> AuditFieldValueDescriptor.Action, + AuditFieldName("indices") -> AuditFieldValueDescriptor.InvolvedIndices, + AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory + ) ++ environmentRelatedAuditFields } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala similarity index 83% rename from audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsAuditLogSerializer.scala rename to audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala index 35173642d3..6867f4d4b9 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala @@ -17,18 +17,18 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode +import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventMode import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, AuditSerializationHelper} -class ReportingAllTypesOfEventsAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { +class FullAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, environmentContext = Some(environmentContext), fields = defaultV2AuditFields, - allowedEventSerializationMode = AllowedEventSerializationMode.SerializeAllAllowedEvents + allowedEventMode = AllowedEventMode.IncludeAll, ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsWithQueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala similarity index 82% rename from audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsWithQueryAuditLogSerializer.scala rename to audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala index ee59595879..9b4e239938 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/ReportingAllTypesOfEventsWithQueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala @@ -17,18 +17,18 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode +import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventMode import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, AuditSerializationHelper} -class ReportingAllTypesOfEventsWithQueryAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { +class FullAuditLogWithQuerySerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, environmentContext = Some(environmentContext), fields = queryV2AuditFields, - allowedEventSerializationMode = AllowedEventSerializationMode.SerializeAllAllowedEvents + allowedEventMode = AllowedEventMode.IncludeAll, ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index c266aed64e..7deb5dc4b6 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -18,7 +18,8 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.AuditResponseContext.Verbosity +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1.queryV1AuditFields class QueryAuditLogSerializerV1 extends AuditLogSerializer { @@ -28,14 +29,13 @@ class QueryAuditLogSerializerV1 extends AuditLogSerializer { responseContext = responseContext, environmentContext = None, fields = queryV1AuditFields, - allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + allowedEventMode = AllowedEventMode.Include(Set(Verbosity.Info)), ) } object QueryAuditLogSerializerV1 { - val queryV1AuditFields: Map[AuditFieldName, AuditFieldValue] = - DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map( - AuditFieldName("content") -> AuditFieldValue.Content - ) + val queryV1AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = + DefaultAuditLogSerializerV1.defaultV1AuditFields ++ + Map(AuditFieldName("content") -> AuditFieldValueDescriptor.Content) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index 9aec8d1100..443a63bf54 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -18,7 +18,8 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.AuditResponseContext.Verbosity +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer.environmentRelatedAuditFields import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields @@ -29,12 +30,14 @@ class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) ext responseContext = responseContext, environmentContext = Some(environmentContext), fields = queryV2AuditFields, - allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + allowedEventMode = AllowedEventMode.Include(Set(Verbosity.Info)), ) } object QueryAuditLogSerializerV2 { - val queryV2AuditFields: Map[AuditFieldName, AuditFieldValue] = - QueryAuditLogSerializerV1.queryV1AuditFields ++ environmentRelatedAuditFields + val queryV2AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = + DefaultAuditLogSerializerV1.defaultV1AuditFields ++ + Map(AuditFieldName("content") -> AuditFieldValueDescriptor.Content) ++ + environmentRelatedAuditFields } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala index 580a53b825..b5d89f69f5 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala @@ -18,23 +18,23 @@ */ package tech.beshu.ror.accesscontrol.audit.configurable -import tech.beshu.ror.audit.AuditFieldValue +import tech.beshu.ror.audit.AuditSerializationHelper.AuditFieldValueDescriptor -object AuditFieldValueDeserializer { +object AuditFieldValueDescriptorDeserializer { private val pattern = "\\{([^}]+)\\}".r - def deserialize(str: String): Either[String, AuditFieldValue] = { + def deserialize(str: String): Either[String, AuditFieldValueDescriptor] = { val matches = pattern.findAllMatchIn(str).toList val (parts, missing, lastIndex) = - matches.foldLeft((List.empty[AuditFieldValue], List.empty[String], 0)) { + matches.foldLeft((List.empty[AuditFieldValueDescriptor], List.empty[String], 0)) { case ((partsAcc, missingAcc, lastEnd), m) => val key = m.group(1) val before = str.substring(lastEnd, m.start) - val partBefore = if (before.nonEmpty) List(AuditFieldValue.StaticText(before)) else Nil + val partBefore = if (before.nonEmpty) List(AuditFieldValueDescriptor.StaticText(before)) else Nil - val (partAfter, newMissing) = deserializerAuditFieldValue(key) match { + val (partAfter, newMissing) = deserializerAuditFieldValueDescriptor(key) match { case Some(placeholder) => (List(placeholder), Nil) case None => (Nil, List(key)) } @@ -42,48 +42,48 @@ object AuditFieldValueDeserializer { (partsAcc ++ partBefore ++ partAfter, missingAcc ++ newMissing, m.end) } - val trailing = if (lastIndex < str.length) List(AuditFieldValue.StaticText(str.substring(lastIndex))) else Nil + val trailing = if (lastIndex < str.length) List(AuditFieldValueDescriptor.StaticText(str.substring(lastIndex))) else Nil val allParts = parts ++ trailing missing match { case Nil => allParts match { - case Nil => Right(AuditFieldValue.StaticText("")) + case Nil => Right(AuditFieldValueDescriptor.StaticText("")) case singleElement :: Nil => Right(singleElement) - case multipleElements => Right(AuditFieldValue.Combined(multipleElements)) + case multipleElements => Right(AuditFieldValueDescriptor.Combined(multipleElements)) } case missingList => Left(s"There are invalid placeholder values: ${missingList.mkString(", ")}") } } - private def deserializerAuditFieldValue(str: String): Option[AuditFieldValue] = { + private def deserializerAuditFieldValueDescriptor(str: String): Option[AuditFieldValueDescriptor] = { str match { - case "IS_MATCHED" => Some(AuditFieldValue.IsMatched) - case "FINAL_STATE" => Some(AuditFieldValue.FinalState) - case "REASON" => Some(AuditFieldValue.Reason) - case "USER" => Some(AuditFieldValue.User) - case "IMPERSONATED_BY_USER" => Some(AuditFieldValue.ImpersonatedByUser) - case "ACTION" => Some(AuditFieldValue.Action) - case "INVOLVED_INDICES" => Some(AuditFieldValue.InvolvedIndices) - case "ACL_HISTORY" => Some(AuditFieldValue.AclHistory) - case "PROCESSING_DURATION_MILLIS" => Some(AuditFieldValue.ProcessingDurationMillis) - case "TIMESTAMP" => Some(AuditFieldValue.Timestamp) - case "ID" => Some(AuditFieldValue.Id) - case "CORRELATION_ID" => Some(AuditFieldValue.CorrelationId) - case "TASK_ID" => Some(AuditFieldValue.TaskId) - case "ERROR_TYPE" => Some(AuditFieldValue.ErrorType) - case "ERROR_MESSAGE" => Some(AuditFieldValue.ErrorMessage) - case "TYPE" => Some(AuditFieldValue.Type) - case "HTTP_METHOD" => Some(AuditFieldValue.HttpMethod) - case "HTTP_HEADER_NAMES" => Some(AuditFieldValue.HttpHeaderNames) - case "HTTP_PATH" => Some(AuditFieldValue.HttpPath) - case "X_FORWARDED_FOR_HTTP_HEADER" => Some(AuditFieldValue.XForwardedForHttpHeader) - case "REMOTE_ADDRESS" => Some(AuditFieldValue.RemoteAddress) - case "LOCAL_ADDRESS" => Some(AuditFieldValue.LocalAddress) - case "CONTENT" => Some(AuditFieldValue.Content) - case "CONTENT_LENGTH_IN_BYTES" => Some(AuditFieldValue.ContentLengthInBytes) - case "CONTENT_LENGTH_IN_KB" => Some(AuditFieldValue.ContentLengthInKb) - case "ES_NODE_NAME" => Some(AuditFieldValue.EsNodeName) - case "ES_CLUSTER_NAME" => Some(AuditFieldValue.EsClusterName) + case "IS_MATCHED" => Some(AuditFieldValueDescriptor.IsMatched) + case "FINAL_STATE" => Some(AuditFieldValueDescriptor.FinalState) + case "REASON" => Some(AuditFieldValueDescriptor.Reason) + case "USER" => Some(AuditFieldValueDescriptor.User) + case "IMPERSONATED_BY_USER" => Some(AuditFieldValueDescriptor.ImpersonatedByUser) + case "ACTION" => Some(AuditFieldValueDescriptor.Action) + case "INVOLVED_INDICES" => Some(AuditFieldValueDescriptor.InvolvedIndices) + case "ACL_HISTORY" => Some(AuditFieldValueDescriptor.AclHistory) + case "PROCESSING_DURATION_MILLIS" => Some(AuditFieldValueDescriptor.ProcessingDurationMillis) + case "TIMESTAMP" => Some(AuditFieldValueDescriptor.Timestamp) + case "ID" => Some(AuditFieldValueDescriptor.Id) + case "CORRELATION_ID" => Some(AuditFieldValueDescriptor.CorrelationId) + case "TASK_ID" => Some(AuditFieldValueDescriptor.TaskId) + case "ERROR_TYPE" => Some(AuditFieldValueDescriptor.ErrorType) + case "ERROR_MESSAGE" => Some(AuditFieldValueDescriptor.ErrorMessage) + case "TYPE" => Some(AuditFieldValueDescriptor.Type) + case "HTTP_METHOD" => Some(AuditFieldValueDescriptor.HttpMethod) + case "HTTP_HEADER_NAMES" => Some(AuditFieldValueDescriptor.HttpHeaderNames) + case "HTTP_PATH" => Some(AuditFieldValueDescriptor.HttpPath) + case "X_FORWARDED_FOR_HTTP_HEADER" => Some(AuditFieldValueDescriptor.XForwardedForHttpHeader) + case "REMOTE_ADDRESS" => Some(AuditFieldValueDescriptor.RemoteAddress) + case "LOCAL_ADDRESS" => Some(AuditFieldValueDescriptor.LocalAddress) + case "CONTENT" => Some(AuditFieldValueDescriptor.Content) + case "CONTENT_LENGTH_IN_BYTES" => Some(AuditFieldValueDescriptor.ContentLengthInBytes) + case "CONTENT_LENGTH_IN_KB" => Some(AuditFieldValueDescriptor.ContentLengthInKb) + case "ES_NODE_NAME" => Some(AuditFieldValueDescriptor.EsNodeName) + case "ES_CLUSTER_NAME" => Some(AuditFieldValueDescriptor.EsClusterName) case _ => None } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala index 13e7a7dbee..a56c3e3c28 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala @@ -17,15 +17,15 @@ package tech.beshu.ror.accesscontrol.audit.configurable import org.json.JSONObject -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.instances.DefaultAuditLogSerializer -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditFieldValue, AuditResponseContext, AuditSerializationHelper} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, AuditSerializationHelper} class ConfigurableAuditLogSerializer(val environmentContext: AuditEnvironmentContext, - val allowedEventSerializationMode: AllowedEventSerializationMode, - val fields: Map[AuditFieldName, AuditFieldValue]) extends DefaultAuditLogSerializer(environmentContext) { + val allowedEventMode: AllowedEventMode, + val fields: Map[AuditFieldName, AuditFieldValueDescriptor]) extends DefaultAuditLogSerializer(environmentContext) { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - AuditSerializationHelper.serialize(responseContext, Some(environmentContext), fields, allowedEventSerializationMode) + AuditSerializationHelper.serialize(responseContext, Some(environmentContext), fields, allowedEventMode) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 542acf5b87..fcbac767af 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -25,7 +25,7 @@ import tech.beshu.ror.accesscontrol.audit.AuditingTool import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink.Config import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink.Config.{EsDataStreamBasedSink, EsIndexBasedSink, LogBasedSink} -import tech.beshu.ror.accesscontrol.audit.configurable.{AuditFieldValueDeserializer, ConfigurableAuditLogSerializer} +import tech.beshu.ror.accesscontrol.audit.configurable.{AuditFieldValueDescriptorDeserializer, ConfigurableAuditLogSerializer} import tech.beshu.ror.accesscontrol.domain.RorAuditIndexTemplate.CreationError import tech.beshu.ror.accesscontrol.domain.{AuditCluster, RorAuditDataStream, RorAuditIndexTemplate, RorAuditLoggerName} import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message @@ -33,10 +33,10 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.decoders.common.{lemonLabsUriDecoder, nonEmptyStringDecoder} import tech.beshu.ror.accesscontrol.utils.CirceOps.{AclCreationErrorCoders, DecodingFailureOps} import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator -import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventSerializationMode.* -import tech.beshu.ror.audit.AuditSerializationHelper.AuditFieldName +import tech.beshu.ror.audit.AuditResponseContext.Verbosity +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.adapters.* -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditFieldValue, AuditLogSerializer} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* import tech.beshu.ror.utils.yaml.YamlKeyDecoder @@ -215,12 +215,16 @@ object AuditingSettingsDecoder extends Logging { configurable <- c.downField("configurable").as[Option[Boolean]] serializer <- if (configurable.contains(true)) { for { - serializeAllAllowedEvents <- c.downField("serialize_all_allowed_events").as[Boolean] - .left.map(withAuditingSettingsCreationErrorMessage("Configurable serializer is used, but the serialize_all_allowed_events setting is missing in configuration")) - allowedEventSerializationMode = if (serializeAllAllowedEvents) SerializeAllAllowedEvents else SerializeOnlyAllowedEventsWithInfoLevelVerbose - fields <- c.downField("fields").as[Map[AuditFieldName, AuditFieldValue]] + allowedEventModeStr <- c.downField("allowed_event_serialization").downField("mode").as[String] + .left.map(withAuditingSettingsCreationErrorMessage("Configurable serializer is used, but the allowed_event_serialization.mode setting is missing in configuration")) + allowedEventMode <- allowedEventModeStr match { + case "INCLUDE_ALL" => Right(AllowedEventMode.IncludeAll) + case "INCLUDE_WITH_VERBOSITY_LEVELS" => c.downField("allowed_event_serialization").downField("verbosity_levels").as[Set[Verbosity]].map(AllowedEventMode.Include.apply) + case other => Left(DecodingFailure(s"Invalid allowed_event_serialization.mode $other. Allowed values: [INCLUDE_ALL, INCLUDE_WITH_VERBOSITY_LEVELS]", Nil)) + } + fields <- c.downField("fields").as[Map[AuditFieldName, AuditFieldValueDescriptor]] .left.map(withAuditingSettingsCreationErrorMessage("Configurable serializer is used, but the fields setting is missing in configuration")) - serializer = new ConfigurableAuditLogSerializer(context, allowedEventSerializationMode, fields) + serializer = new ConfigurableAuditLogSerializer(context, allowedEventMode, fields) } yield Some(serializer) } else { for { @@ -293,10 +297,21 @@ object AuditingSettingsDecoder extends Logging { KeyDecoder.decodeKeyString.map(AuditFieldName.apply) } - given auditFieldValueDecoder: Decoder[AuditFieldValue] = { + given auditFieldValueDecoder: Decoder[AuditFieldValueDescriptor] = { SyncDecoderCreator .from(Decoder.decodeString) - .emap(AuditFieldValueDeserializer.deserialize) + .emap(AuditFieldValueDescriptorDeserializer.deserialize) + .decoder + } + + given verbosityDecoder: Decoder[Verbosity] = { + SyncDecoderCreator + .from(Decoder.decodeString) + .emap { + case "ERROR" => Right(Verbosity.Error: Verbosity) + case "INFO" => Right(Verbosity.Info: Verbosity) + case other => Left(s"Unknown verbosity level [$other], allowed values are: [ERROR, INFO]") + } .decoder } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index 8e4ce66060..13ac548001 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -34,7 +34,8 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.{Core, RawRorConfigBasedCoreFactory} import tech.beshu.ror.audit.* -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName} +import tech.beshu.ror.audit.AuditResponseContext.Verbosity +import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.adapters.{DeprecatedAuditLogSerializerAdapter, EnvironmentAwareAuditLogSerializerAdapter} import tech.beshu.ror.audit.instances.{DefaultAuditLogSerializer, QueryAuditLogSerializer} import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} @@ -384,7 +385,9 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | outputs: | - type: log | configurable: true - | serialize_all_allowed_events: false + | allowed_event_serialization: + | mode: INCLUDE_WITH_VERBOSITY_LEVELS + | verbosity_levels: [INFO] | fields: | node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" | another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" @@ -408,12 +411,12 @@ class AuditSettingsTests extends AnyWordSpec with Inside { configuredSerializer.environmentContext.esClusterName shouldBe testAuditEnvironmentContext.esClusterName configuredSerializer.environmentContext.esNodeName shouldBe testAuditEnvironmentContext.esNodeName - configuredSerializer.allowedEventSerializationMode shouldBe AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose + configuredSerializer.allowedEventMode shouldBe AllowedEventMode.Include(Set(Verbosity.Info)) configuredSerializer.fields shouldBe Map( - AuditFieldName("node_name_with_static_suffix") -> AuditFieldValue.Combined(List(AuditFieldValue.EsNodeName, AuditFieldValue.StaticText(" with suffix"))), - AuditFieldName("another_field") -> AuditFieldValue.Combined(List(AuditFieldValue.EsClusterName, AuditFieldValue.StaticText(" "), AuditFieldValue.HttpMethod)), - AuditFieldName("tid") -> AuditFieldValue.TaskId, - AuditFieldName("bytes") -> AuditFieldValue.ContentLengthInBytes, + AuditFieldName("node_name_with_static_suffix") -> AuditFieldValueDescriptor.Combined(List(AuditFieldValueDescriptor.EsNodeName, AuditFieldValueDescriptor.StaticText(" with suffix"))), + AuditFieldName("another_field") -> AuditFieldValueDescriptor.Combined(List(AuditFieldValueDescriptor.EsClusterName, AuditFieldValueDescriptor.StaticText(" "), AuditFieldValueDescriptor.HttpMethod)), + AuditFieldName("tid") -> AuditFieldValueDescriptor.TaskId, + AuditFieldName("bytes") -> AuditFieldValueDescriptor.ContentLengthInBytes, ) } } @@ -1305,7 +1308,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { assertInvalidSettings( config, - expectedErrorMessage = "Configurable serializer is used, but the serialize_all_allowed_events setting is missing in configuration" + expectedErrorMessage = "Configurable serializer is used, but the allowed_event_serialization.mode setting is missing in configuration" ) } "configurable serializer is set, but without fields setting" in { @@ -1317,7 +1320,9 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | outputs: | - type: log | configurable: true - | serialize_all_allowed_events: false + | allowed_event_serialization: + | mode: INCLUDE_WITH_VERBOSITY_LEVELS + | verbosity_levels: [INFO] | access_control_rules: | | - name: test_block From edb8373c8b85d9b580be7ae5bb14100c9c46a4f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 29 Aug 2025 00:26:09 +0200 Subject: [PATCH 18/27] review changes --- .../EnvironmentAwareAuditLogSerializer.scala | 8 -- .../DefaultAuditLogSerializerV1.scala | 6 +- .../DefaultAuditLogSerializerV2.scala | 13 ++- .../instances/FullAuditLogSerializer.scala | 9 +- .../FullAuditLogWithQuerySerializer.scala | 9 +- .../instances/QueryAuditLogSerializerV1.scala | 6 +- .../instances/QueryAuditLogSerializerV2.scala | 12 +- .../AuditSerializationHelper.scala | 16 ++- ... => AuditFieldValueDescriptorParser.scala} | 61 +++++----- .../ConfigurableAuditLogSerializer.scala | 8 +- .../decoders/AuditingSettingsDecoder.scala | 107 ++++++++++++------ .../unit/acl/factory/AuditSettingsTests.scala | 42 +++---- .../enabled_auditing_tools/readonlyrest.yml | 32 +++--- .../readonlyrest_audit_index.yml | 16 +-- .../LocalClusterAuditingToolsSuite.scala | 12 +- 15 files changed, 204 insertions(+), 153 deletions(-) rename audit/src/main/scala/tech/beshu/ror/audit/{ => utils}/AuditSerializationHelper.scala (95%) rename core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/{AuditFieldValueDeserializer.scala => AuditFieldValueDescriptorParser.scala} (65%) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala index 1262f4a192..c27f9f3671 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala @@ -17,16 +17,8 @@ package tech.beshu.ror.audit import org.json.JSONObject -import tech.beshu.ror.audit.AuditSerializationHelper.{AuditFieldName, AuditFieldValueDescriptor} trait EnvironmentAwareAuditLogSerializer { def onResponse(responseContext: AuditResponseContext, environmentContext: AuditEnvironmentContext): Option[JSONObject] } - -object EnvironmentAwareAuditLogSerializer { - val environmentRelatedAuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( - AuditFieldName("es_node_name") -> AuditFieldValueDescriptor.EsNodeName, - AuditFieldName("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName - ) -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index bf0846c420..029b4857a7 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -19,8 +19,10 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields +import tech.beshu.ror.audit.utils.AuditSerializationHelper class DefaultAuditLogSerializerV1 extends AuditLogSerializer { @@ -29,7 +31,7 @@ class DefaultAuditLogSerializerV1 extends AuditLogSerializer { responseContext = responseContext, environmentContext = None, fields = defaultV1AuditFields, - allowedEventMode = AllowedEventMode.Include(Set(Verbosity.Info)), + allowedEventMode = Include(Set(Verbosity.Info)), ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index 2b230d6de5..8e170d451f 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -19,9 +19,10 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} -import tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer.environmentRelatedAuditFields +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields +import tech.beshu.ror.audit.utils.AuditSerializationHelper class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { @@ -30,7 +31,7 @@ class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) e responseContext = responseContext, environmentContext = Some(environmentContext), fields = defaultV2AuditFields, - allowedEventMode = AllowedEventMode.Include(Set(Verbosity.Info)), + allowedEventMode = Include(Set(Verbosity.Info)), ) } @@ -60,6 +61,8 @@ object DefaultAuditLogSerializerV2 { AuditFieldName("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser, AuditFieldName("action") -> AuditFieldValueDescriptor.Action, AuditFieldName("indices") -> AuditFieldValueDescriptor.InvolvedIndices, - AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory - ) ++ environmentRelatedAuditFields + AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory, + AuditFieldName("es_node_name") -> AuditFieldValueDescriptor.EsNodeName, + AuditFieldName("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName + ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala index 6867f4d4b9..abdf976056 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala @@ -17,18 +17,19 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventMode +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.IncludeAll import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, AuditSerializationHelper} +import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} -class FullAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { +class FullAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, environmentContext = Some(environmentContext), fields = defaultV2AuditFields, - allowedEventMode = AllowedEventMode.IncludeAll, + allowedEventMode = IncludeAll, ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala index 9b4e239938..1f1652d25a 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala @@ -17,18 +17,19 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.AuditSerializationHelper.AllowedEventMode +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.IncludeAll import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, AuditSerializationHelper} +import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} -class FullAuditLogWithQuerySerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializer(environmentContext) { +class FullAuditLogWithQuerySerializer(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, environmentContext = Some(environmentContext), fields = queryV2AuditFields, - allowedEventMode = AllowedEventMode.IncludeAll, + allowedEventMode = IncludeAll, ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index 7deb5dc4b6..bf49edec8c 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -19,8 +19,10 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1.queryV1AuditFields +import tech.beshu.ror.audit.utils.AuditSerializationHelper class QueryAuditLogSerializerV1 extends AuditLogSerializer { @@ -29,7 +31,7 @@ class QueryAuditLogSerializerV1 extends AuditLogSerializer { responseContext = responseContext, environmentContext = None, fields = queryV1AuditFields, - allowedEventMode = AllowedEventMode.Include(Set(Verbosity.Info)), + allowedEventMode = Include(Set(Verbosity.Info)), ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index 443a63bf54..ad11c3ae3c 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -19,9 +19,10 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} -import tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer.environmentRelatedAuditFields +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields +import tech.beshu.ror.audit.utils.AuditSerializationHelper class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { @@ -30,14 +31,13 @@ class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) ext responseContext = responseContext, environmentContext = Some(environmentContext), fields = queryV2AuditFields, - allowedEventMode = AllowedEventMode.Include(Set(Verbosity.Info)), + allowedEventMode = Include(Set(Verbosity.Info)), ) } object QueryAuditLogSerializerV2 { val queryV2AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = - DefaultAuditLogSerializerV1.defaultV1AuditFields ++ - Map(AuditFieldName("content") -> AuditFieldValueDescriptor.Content) ++ - environmentRelatedAuditFields + DefaultAuditLogSerializerV2.defaultV2AuditFields ++ + Map(AuditFieldName("content") -> AuditFieldValueDescriptor.Content) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala b/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala similarity index 95% rename from audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala rename to audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala index ef52b70652..edabcef19d 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditSerializationHelper.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala @@ -14,11 +14,12 @@ * You should have received a copy of the GNU General Public License * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ */ -package tech.beshu.ror.audit +package tech.beshu.ror.audit.utils import org.json.JSONObject import tech.beshu.ror.audit.AuditResponseContext.* import tech.beshu.ror.audit.instances.SerializeUser +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditRequestContext, AuditResponseContext} import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -62,14 +63,11 @@ private[ror] object AuditSerializationHelper { private def createEntry(fields: Map[AuditFieldName, AuditFieldValueDescriptor], eventData: EventData) = { - val resolvedFields: Map[String, Any] = { - Map( - "@timestamp" -> timestampFormatter.format(eventData.requestContext.timestamp) - ) ++ fields.map { - case (fieldName, fieldValue) => - fieldName.value -> resolver(eventData)(fieldValue) - } - } + val resolveAuditFieldValue = resolver(eventData) + val resolvedFields: Map[String, Any] = + Map("@timestamp" -> timestampFormatter.format(eventData.requestContext.timestamp)) ++ + fields.map { case (name, valueDescriptor) => name.value -> resolveAuditFieldValue(valueDescriptor) } + resolvedFields .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } .mergeWith(eventData.requestContext.generalAuditEvents) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDescriptorParser.scala similarity index 65% rename from core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala rename to core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDescriptorParser.scala index b5d89f69f5..e07f1bb7a3 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDeserializer.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDescriptorParser.scala @@ -1,5 +1,3 @@ - - /* * This file is part of ReadonlyREST. * @@ -18,45 +16,46 @@ */ package tech.beshu.ror.accesscontrol.audit.configurable -import tech.beshu.ror.audit.AuditSerializationHelper.AuditFieldValueDescriptor - -object AuditFieldValueDescriptorDeserializer { +import cats.parse.{Parser0, Parser as P} +import cats.syntax.list.* +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldValueDescriptor - private val pattern = "\\{([^}]+)\\}".r +object AuditFieldValueDescriptorParser { - def deserialize(str: String): Either[String, AuditFieldValueDescriptor] = { - val matches = pattern.findAllMatchIn(str).toList + private val lbrace = P.char('{') + private val rbrace = P.char('}') + private val key = P.charsWhile(c => c != '{' && c != '}') - val (parts, missing, lastIndex) = - matches.foldLeft((List.empty[AuditFieldValueDescriptor], List.empty[String], 0)) { - case ((partsAcc, missingAcc, lastEnd), m) => - val key = m.group(1) - val before = str.substring(lastEnd, m.start) - val partBefore = if (before.nonEmpty) List(AuditFieldValueDescriptor.StaticText(before)) else Nil + private val placeholder: P[Either[String, AuditFieldValueDescriptor]] = + (lbrace *> key <* rbrace).map(k => deserializerAuditFieldValueDescriptor(k.trim.toUpperCase).toRight(k)) - val (partAfter, newMissing) = deserializerAuditFieldValueDescriptor(key) match { - case Some(placeholder) => (List(placeholder), Nil) - case None => (Nil, List(key)) - } + private val text: P[AuditFieldValueDescriptor] = + P.charsWhile(_ != '{').map(AuditFieldValueDescriptor.StaticText.apply) - (partsAcc ++ partBefore ++ partAfter, missingAcc ++ newMissing, m.end) - } + private val segment: P[Either[String, AuditFieldValueDescriptor]] = + placeholder.orElse(text.map(Right(_))) - val trailing = if (lastIndex < str.length) List(AuditFieldValueDescriptor.StaticText(str.substring(lastIndex))) else Nil - val allParts = parts ++ trailing + private val parser: Parser0[List[Either[String, AuditFieldValueDescriptor]]] = + segment.rep0 <* P.end - missing match { - case Nil => allParts match { - case Nil => Right(AuditFieldValueDescriptor.StaticText("")) - case singleElement :: Nil => Right(singleElement) - case multipleElements => Right(AuditFieldValueDescriptor.Combined(multipleElements)) - } - case missingList => Left(s"There are invalid placeholder values: ${missingList.mkString(", ")}") + def parse(s: String): Either[String, AuditFieldValueDescriptor] = + parser.parseAll(s) match { + case Left(err) => + Left(err.toString) + case Right(segments) => + val (missing, ok) = segments.partitionMap(identity) + missing.toNel match { + case Some(missing) => Left(s"There are invalid placeholder values: ${missing.toList.distinct.mkString(", ")}") + case None => ok match { + case Nil => Right(AuditFieldValueDescriptor.StaticText("")) + case single :: Nil => Right(single) + case many => Right(AuditFieldValueDescriptor.Combined(many)) + } + } } - } private def deserializerAuditFieldValueDescriptor(str: String): Option[AuditFieldValueDescriptor] = { - str match { + str.toUpperCase match { case "IS_MATCHED" => Some(AuditFieldValueDescriptor.IsMatched) case "FINAL_STATE" => Some(AuditFieldValueDescriptor.FinalState) case "REASON" => Some(AuditFieldValueDescriptor.Reason) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala index a56c3e3c28..12a77f86cd 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala @@ -17,13 +17,13 @@ package tech.beshu.ror.accesscontrol.audit.configurable import org.json.JSONObject -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} -import tech.beshu.ror.audit.instances.DefaultAuditLogSerializer -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditResponseContext, AuditSerializationHelper} +import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} class ConfigurableAuditLogSerializer(val environmentContext: AuditEnvironmentContext, val allowedEventMode: AllowedEventMode, - val fields: Map[AuditFieldName, AuditFieldValueDescriptor]) extends DefaultAuditLogSerializer(environmentContext) { + val fields: Map[AuditFieldName, AuditFieldValueDescriptor]) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize(responseContext, Some(environmentContext), fields, allowedEventMode) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index fcbac767af..f3647f530f 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -18,14 +18,14 @@ package tech.beshu.ror.accesscontrol.factory.decoders import cats.data.NonEmptyList import io.circe.Decoder.* -import io.circe.{Decoder, DecodingFailure, HCursor, KeyDecoder} +import io.circe.{Decoder, DecodingFailure, HCursor, Json, KeyDecoder} import io.lemonlabs.uri.Uri import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.audit.AuditingTool import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink.Config import tech.beshu.ror.accesscontrol.audit.AuditingTool.AuditSettings.AuditSink.Config.{EsDataStreamBasedSink, EsIndexBasedSink, LogBasedSink} -import tech.beshu.ror.accesscontrol.audit.configurable.{AuditFieldValueDescriptorDeserializer, ConfigurableAuditLogSerializer} +import tech.beshu.ror.accesscontrol.audit.configurable.{AuditFieldValueDescriptorParser, ConfigurableAuditLogSerializer} import tech.beshu.ror.accesscontrol.domain.RorAuditIndexTemplate.CreationError import tech.beshu.ror.accesscontrol.domain.{AuditCluster, RorAuditDataStream, RorAuditIndexTemplate, RorAuditLoggerName} import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message @@ -34,7 +34,7 @@ import tech.beshu.ror.accesscontrol.factory.decoders.common.{lemonLabsUriDecoder import tech.beshu.ror.accesscontrol.utils.CirceOps.{AclCreationErrorCoders, DecodingFailureOps} import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.adapters.* import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer} import tech.beshu.ror.es.EsVersion @@ -212,40 +212,47 @@ object AuditingSettingsDecoder extends Logging { given auditLogSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = SyncDecoderCreator.from( Decoder.instance[Option[AuditLogSerializer]] { c => for { - configurable <- c.downField("configurable").as[Option[Boolean]] - serializer <- if (configurable.contains(true)) { - for { - allowedEventModeStr <- c.downField("allowed_event_serialization").downField("mode").as[String] - .left.map(withAuditingSettingsCreationErrorMessage("Configurable serializer is used, but the allowed_event_serialization.mode setting is missing in configuration")) - allowedEventMode <- allowedEventModeStr match { - case "INCLUDE_ALL" => Right(AllowedEventMode.IncludeAll) - case "INCLUDE_WITH_VERBOSITY_LEVELS" => c.downField("allowed_event_serialization").downField("verbosity_levels").as[Set[Verbosity]].map(AllowedEventMode.Include.apply) - case other => Left(DecodingFailure(s"Invalid allowed_event_serialization.mode $other. Allowed values: [INCLUDE_ALL, INCLUDE_WITH_VERBOSITY_LEVELS]", Nil)) - } - fields <- c.downField("fields").as[Map[AuditFieldName, AuditFieldValueDescriptor]] - .left.map(withAuditingSettingsCreationErrorMessage("Configurable serializer is used, but the fields setting is missing in configuration")) - serializer = new ConfigurableAuditLogSerializer(context, allowedEventMode, fields) - } yield Some(serializer) - } else { - for { - fullClassNameOpt <- c.downField("serializer").as[Option[String]] - legacyFullClassNameOpt <- c.downField("audit_serializer").as[Option[String]] - serializerOpt <- fullClassNameOpt.orElse(legacyFullClassNameOpt) match { - case Some(fullClassName) => - createSerializerInstanceFromClassName(fullClassName).map(Some(_)) - .left.map(error => DecodingFailure(AclCreationErrorCoders.stringify(error), Nil)) - case None => - Right(None) - } - } yield serializerOpt + serializerType <- c.as[SerializerType] + serializer <- serializerType match { + case SerializerType.StaticSerializerInOutputSection => + c.as[Option[AuditLogSerializer]](classNameBasedSerializerDecoder) + case SerializerType.StaticSerializerInSerializerSection => + c.downField("serializer").as[Option[AuditLogSerializer]](classNameBasedSerializerDecoder) + case SerializerType.ConfigurableSerializer => + c.downField("serializer").as[Option[AuditLogSerializer]](configurableSerializerDecoder) } } yield serializer } ) .decoder - private def withAuditingSettingsCreationErrorMessage(message: String)(decodingFailure: DecodingFailure) = { - decodingFailure.withMessage(AclCreationErrorCoders.stringify(AuditingSettingsCreationError(Message(message)))) + private def configurableSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + for { + allowedEventMode <- c.downField("verbosity_level_serialization_mode").as[AllowedEventMode] + .left.map(withAuditingSettingsCreationErrorMessage(msg => s"Configurable serializer is used, but the 'verbosity_level_serialization_mode' setting is invalid: $msg")) + fields <- c.downField("fields").as[Map[AuditFieldName, AuditFieldValueDescriptor]] + .left.map(withAuditingSettingsCreationErrorMessage(msg => s"Configurable serializer is used, but the 'fields' setting is missing or invalid: $msg")) + serializer = new ConfigurableAuditLogSerializer(context, allowedEventMode, fields) + } yield Some(serializer) + } + + private def classNameBasedSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + for { + classNameOpt <- c.downField("class_name").as[Option[String]] + fullClassNameOpt <- c.downField("serializer").as[Option[String]] + legacyFullClassNameOpt <- c.downField("audit_serializer").as[Option[String]] + serializerOpt <- classNameOpt.orElse(fullClassNameOpt).orElse(legacyFullClassNameOpt) match { + case Some(fullClassName) => + createSerializerInstanceFromClassName(fullClassName).map(Some(_)) + .left.map(error => DecodingFailure(AclCreationErrorCoders.stringify(error), Nil)) + case None => + Right(None) + } + } yield serializerOpt + } + + private def withAuditingSettingsCreationErrorMessage(message: String => String)(decodingFailure: DecodingFailure) = { + decodingFailure.withMessage(AclCreationErrorCoders.stringify(AuditingSettingsCreationError(Message(message(decodingFailure.message))))) } @nowarn("cat=deprecation") @@ -293,6 +300,16 @@ object AuditingSettingsDecoder extends Logging { } } + given allowedEventModeDecoder: Decoder[AllowedEventMode] = { + SyncDecoderCreator + .from(Decoder[Option[Set[Verbosity]]]) + .map[AllowedEventMode] { + case Some(verbosityLevels) => AllowedEventMode.Include(verbosityLevels) + case None => AllowedEventMode.IncludeAll + } + .decoder + } + given auditFieldNameDecoder: KeyDecoder[AuditFieldName] = { KeyDecoder.decodeKeyString.map(AuditFieldName.apply) } @@ -300,7 +317,7 @@ object AuditingSettingsDecoder extends Logging { given auditFieldValueDecoder: Decoder[AuditFieldValueDescriptor] = { SyncDecoderCreator .from(Decoder.decodeString) - .emap(AuditFieldValueDescriptorDeserializer.deserialize) + .emap(AuditFieldValueDescriptorParser.parse) .decoder } @@ -411,4 +428,30 @@ object AuditingSettingsDecoder extends Logging { } } } + + private given serializerTypeDecoder: Decoder[SerializerType] = Decoder.instance { c => + c.downField("serializer").as[Option[Json]].flatMap { + case Some(json) if json.isObject => + json.hcursor.downField("type").as[String].map(_.toLowerCase).flatMap { + case "static" => + Right(SerializerType.StaticSerializerInSerializerSection) + case "configurable" => + Right(SerializerType.ConfigurableSerializer) + case other => + Left(DecodingFailure(AclCreationErrorCoders.stringify( + AuditingSettingsCreationError(Message(s"Invalid serializer type '$other', allowed values [static, configurable]")) + ), Nil)) + } + case Some(_) | None => + Right(SerializerType.StaticSerializerInOutputSection) + } + } + + private sealed trait SerializerType + + private object SerializerType { + case object StaticSerializerInOutputSection extends SerializerType + case object StaticSerializerInSerializerSection extends SerializerType + case object ConfigurableSerializer extends SerializerType + } } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index 13ac548001..d2008a72b1 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -35,7 +35,7 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.{Core, RawRorConfigBasedCoreFactory} import tech.beshu.ror.audit.* import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.adapters.{DeprecatedAuditLogSerializerAdapter, EnvironmentAwareAuditLogSerializerAdapter} import tech.beshu.ror.audit.instances.{DefaultAuditLogSerializer, QueryAuditLogSerializer} import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} @@ -384,15 +384,14 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | configurable: true - | allowed_event_serialization: - | mode: INCLUDE_WITH_VERBOSITY_LEVELS - | verbosity_levels: [INFO] - | fields: - | node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" - | another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" - | tid: "{TASK_ID}" - | bytes: "{CONTENT_LENGTH_IN_BYTES}" + | serializer: + | type: configurable + | verbosity_level_serialization_mode: [INFO] + | fields: + | node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" + | another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" + | tid: "{TASK_ID}" + | bytes: "{CONTENT_LENGTH_IN_BYTES}" | access_control_rules: | @@ -1288,7 +1287,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { expectedErrorMessage = "The audit 'outputs' array cannot be empty" ) } - "configurable serializer is set, but without serialize_all_allowed_events setting" in { + "configurable serializer is set with invalid value descriptor" in { val config = rorConfigFromUnsafe( """ |readonlyrest: @@ -1296,8 +1295,14 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | configurable: true - | + | serializer: + | type: configurable + | verbosity_level_serialization_mode: [INFO] + | fields: + | node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" + | another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD2}" + | tid: "{TASK_ID}" + | bytes: "{CONTENT_LENGTH_IN_BYTES}" | access_control_rules: | | - name: test_block @@ -1308,7 +1313,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { assertInvalidSettings( config, - expectedErrorMessage = "Configurable serializer is used, but the allowed_event_serialization.mode setting is missing in configuration" + expectedErrorMessage = "Configurable serializer is used, but the 'fields' setting is missing or invalid: There are invalid placeholder values: HTTP_METHOD2" ) } "configurable serializer is set, but without fields setting" in { @@ -1319,10 +1324,9 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | enabled: true | outputs: | - type: log - | configurable: true - | allowed_event_serialization: - | mode: INCLUDE_WITH_VERBOSITY_LEVELS - | verbosity_levels: [INFO] + | serializer: + | type: configurable + | verbosity_level_serialization_mode: [INFO] | access_control_rules: | | - name: test_block @@ -1333,7 +1337,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { assertInvalidSettings( config, - expectedErrorMessage = "Configurable serializer is used, but the fields setting is missing in configuration" + expectedErrorMessage = "Configurable serializer is used, but the 'fields' setting is missing or invalid: Missing required field" ) } } diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml index 09539ff27a..0e8e383748 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest.yml @@ -5,22 +5,26 @@ readonlyrest: outputs: - type: index index_template: "'audit_index'" - serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" - serialize_all_allowed_events: false - fields: - node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" - another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" - tid: "{TASK_ID}" - bytes: "{CONTENT_LENGTH_IN_BYTES}" + serializer: + type: "static" + class_name: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" + verbosity_level_serialization_mode: [INFO] + fields: + node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" + another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" + tid: "{TASK_ID}" + bytes: "{CONTENT_LENGTH_IN_BYTES}" - type: data_stream data_stream: "audit_data_stream" - serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" - serialize_all_allowed_events: false - fields: - node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" - another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" - tid: "{TASK_ID}" - bytes: "{CONTENT_LENGTH_IN_BYTES}" + serializer: + type: "static" + class_name: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" + verbosity_level_serialization_mode: [INFO] + fields: + node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" + another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" + tid: "{TASK_ID}" + bytes: "{CONTENT_LENGTH_IN_BYTES}" access_control_rules: diff --git a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml index 25d08f8456..a6b93beda9 100644 --- a/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml +++ b/integration-tests/src/test/resources/ror_audit/enabled_auditing_tools/readonlyrest_audit_index.yml @@ -5,13 +5,15 @@ readonlyrest: outputs: - type: index index_template: "'audit_index'" - serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" - serialize_all_allowed_events: false - fields: - node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" - another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" - tid: "{TASK_ID}" - bytes: "{CONTENT_LENGTH_IN_BYTES}" + serializer: + type: "static" + class_name: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1" + verbosity_level_serialization_mode: [INFO] + fields: + node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" + another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" + tid: "{TASK_ID}" + bytes: "{CONTENT_LENGTH_IN_BYTES}" access_control_rules: diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index af96612223..185f8f1baa 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -94,7 +94,7 @@ class LocalClusterAuditingToolsSuite "using ReportingAllEventsAuditLogSerializer" in { val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) - updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ReportingAllTypesOfEventsAuditLogSerializer") + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.FullAuditLogSerializer") performAndAssertExampleSearchRequest(indexManager) forEachAuditManager { adminAuditManager => @@ -122,7 +122,7 @@ class LocalClusterAuditingToolsSuite "using ReportingAllEventsWithQueryAuditLogSerializer" in { val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) - updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.ReportingAllTypesOfEventsWithQueryAuditLogSerializer") + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.FullAuditLogWithQuerySerializer") performAndAssertExampleSearchRequest(indexManager) forEachAuditManager { adminAuditManager => @@ -150,8 +150,8 @@ class LocalClusterAuditingToolsSuite val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) updateRorConfig( - originalString = """serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""", - newString = "configurable: true", + originalString = """type: "static"""", + newString = """type: "configurable"""", ) performAndAssertExampleSearchRequest(indexManager) @@ -179,8 +179,8 @@ class LocalClusterAuditingToolsSuite } private def updateRorConfigToUseSerializer(serializer: String) = updateRorConfig( - originalString = """serializer: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""", - newString = s"""serializer: "$serializer"""" + originalString = """class_name: "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1"""", + newString = s"""class_name: "$serializer"""" ) private def updateRorConfig(originalString: String, newString: String) = { From 2146ba65fcafdb2afdf4ef339ad50e43a14d3cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 29 Aug 2025 14:01:39 +0200 Subject: [PATCH 19/27] review changes --- .../ror/audit/instances/DefaultAuditLogSerializerV1.scala | 4 ++-- .../ror/audit/instances/DefaultAuditLogSerializerV2.scala | 4 ++-- .../beshu/ror/audit/instances/FullAuditLogSerializer.scala | 2 +- .../audit/instances/FullAuditLogWithQuerySerializer.scala | 2 +- .../ror/audit/instances/QueryAuditLogSerializerV1.scala | 4 ++-- .../ror/audit/instances/QueryAuditLogSerializerV2.scala | 4 ++-- .../beshu/ror/audit/utils/AuditSerializationHelper.scala | 6 +++--- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index 029b4857a7..e32ff9664a 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -31,12 +31,12 @@ class DefaultAuditLogSerializerV1 extends AuditLogSerializer { responseContext = responseContext, environmentContext = None, fields = defaultV1AuditFields, - allowedEventMode = Include(Set(Verbosity.Info)), + allowedEventMode = Include(Set(Verbosity.Info)) ) } -object DefaultAuditLogSerializerV1 { +private[ror] object DefaultAuditLogSerializerV1 { val defaultV1AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( AuditFieldName("match") -> AuditFieldValueDescriptor.IsMatched, AuditFieldName("block") -> AuditFieldValueDescriptor.Reason, diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index 8e170d451f..e1955df94f 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -31,12 +31,12 @@ class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) e responseContext = responseContext, environmentContext = Some(environmentContext), fields = defaultV2AuditFields, - allowedEventMode = Include(Set(Verbosity.Info)), + allowedEventMode = Include(Set(Verbosity.Info)) ) } -object DefaultAuditLogSerializerV2 { +private[ror] object DefaultAuditLogSerializerV2 { val defaultV2AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( AuditFieldName("match") -> AuditFieldValueDescriptor.IsMatched, AuditFieldName("block") -> AuditFieldValueDescriptor.Reason, diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala index abdf976056..b2d7894316 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala @@ -29,7 +29,7 @@ class FullAuditLogSerializer(environmentContext: AuditEnvironmentContext) extend responseContext = responseContext, environmentContext = Some(environmentContext), fields = defaultV2AuditFields, - allowedEventMode = IncludeAll, + allowedEventMode = IncludeAll ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala index 1f1652d25a..89d5cf3c7a 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala @@ -29,7 +29,7 @@ class FullAuditLogWithQuerySerializer(environmentContext: AuditEnvironmentContex responseContext = responseContext, environmentContext = Some(environmentContext), fields = queryV2AuditFields, - allowedEventMode = IncludeAll, + allowedEventMode = IncludeAll ) } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index bf49edec8c..71b21d35fa 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -31,12 +31,12 @@ class QueryAuditLogSerializerV1 extends AuditLogSerializer { responseContext = responseContext, environmentContext = None, fields = queryV1AuditFields, - allowedEventMode = Include(Set(Verbosity.Info)), + allowedEventMode = Include(Set(Verbosity.Info)) ) } -object QueryAuditLogSerializerV1 { +private[ror] object QueryAuditLogSerializerV1 { val queryV1AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = DefaultAuditLogSerializerV1.defaultV1AuditFields ++ Map(AuditFieldName("content") -> AuditFieldValueDescriptor.Content) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index ad11c3ae3c..acbc74e6db 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -31,12 +31,12 @@ class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) ext responseContext = responseContext, environmentContext = Some(environmentContext), fields = queryV2AuditFields, - allowedEventMode = Include(Set(Verbosity.Info)), + allowedEventMode = Include(Set(Verbosity.Info)) ) } -object QueryAuditLogSerializerV2 { +private[ror] object QueryAuditLogSerializerV2 { val queryV2AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = DefaultAuditLogSerializerV2.defaultV2AuditFields ++ Map(AuditFieldName("content") -> AuditFieldValueDescriptor.Content) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala b/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala index edabcef19d..dc9f91482a 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala @@ -17,13 +17,13 @@ package tech.beshu.ror.audit.utils import org.json.JSONObject -import tech.beshu.ror.audit.AuditResponseContext.* +import tech.beshu.ror.audit.AuditResponseContext._ import tech.beshu.ror.audit.instances.SerializeUser import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditRequestContext, AuditResponseContext} import java.time.ZoneId import java.time.format.DateTimeFormatter -import scala.collection.JavaConverters.* +import scala.collection.JavaConverters._ import scala.concurrent.duration.FiniteDuration private[ror] object AuditSerializationHelper { @@ -38,7 +38,7 @@ private[ror] object AuditSerializationHelper { allowedEvent( allowedEventMode, verbosity, - createEntry(fields, EventData(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, environmentContext, None)), + createEntry(fields, EventData(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, environmentContext, None)) ) case ForbiddenBy(requestContext, _, reason) => Some(createEntry(fields, EventData(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, environmentContext, None))) From 2bb638afa267d936bff319f3910947c47f48ed9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Fri, 29 Aug 2025 13:59:41 +0200 Subject: [PATCH 20/27] AuditEnvironmentContext refactor and fix --- .../beshu/ror/audit/AuditRequestContext.scala | 2 +- .../EnvironmentAwareAuditLogSerializer.scala | 2 + ...onmentAwareAuditLogSerializerAdapter.scala | 5 +- .../instances/DefaultAuditLogSerializer.scala | 4 +- .../DefaultAuditLogSerializerV1.scala | 1 - .../DefaultAuditLogSerializerV2.scala | 3 +- .../instances/FullAuditLogSerializer.scala | 5 +- .../FullAuditLogWithQuerySerializer.scala | 3 +- .../instances/QueryAuditLogSerializer.scala | 4 +- .../instances/QueryAuditLogSerializerV1.scala | 1 - .../instances/QueryAuditLogSerializerV2.scala | 3 +- .../utils/AuditSerializationHelper.scala | 16 +++--- .../AuditRequestContextBasedOnAclResult.scala | 3 +- .../accesscontrol/audit/AuditingTool.scala | 26 +++++---- .../ConfigurableAuditLogSerializer.scala | 7 ++- .../RawRorConfigBasedCoreFactory.scala | 10 ++-- .../decoders/AuditingSettingsDecoder.scala | 53 ++++++++----------- .../AccessControlListLoggingDecorator.scala | 4 +- .../tech/beshu/ror/boot/ReadonlyRest.scala | 6 ++- .../integration/AuditOutputFormatTests.scala | 6 ++- .../BaseYamlLoadedAccessControlTest.scala | 4 +- .../unit/acl/factory/AuditSettingsTests.scala | 6 +-- .../unit/acl/factory/CoreFactoryTests.scala | 2 +- .../factory/ImpersonationWarningsTests.scala | 2 +- .../ror/unit/acl/factory/LocalUsersTest.scala | 2 +- .../rules/BaseRuleSettingsDecoderTest.scala | 2 +- .../unit/acl/logging/AuditingToolTests.scala | 26 ++++----- .../unit/boot/ReadonlyRestStartingTests.scala | 2 +- 28 files changed, 101 insertions(+), 109 deletions(-) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/AuditRequestContext.scala b/audit/src/main/scala/tech/beshu/ror/audit/AuditRequestContext.scala index 1b6c8b38c8..3343ba5f8b 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/AuditRequestContext.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/AuditRequestContext.scala @@ -21,7 +21,6 @@ import java.time.Instant import org.json.JSONObject trait AuditRequestContext { - def timestamp: Instant def id: String def correlationId: String @@ -45,4 +44,5 @@ trait AuditRequestContext { def attemptedUserName: Option[String] def rawAuthHeader: Option[String] def generalAuditEvents: JSONObject + def auditEnvironmentContext: AuditEnvironmentContext } \ No newline at end of file diff --git a/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala index c27f9f3671..81e9887249 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/EnvironmentAwareAuditLogSerializer.scala @@ -18,6 +18,8 @@ package tech.beshu.ror.audit import org.json.JSONObject +// The `AuditResponseContext` now contains the `AuditEnvironmentContext` and there is no need to use this trait. +// This trait is preserved and supported for compatibility reasons, but we should not include it in our docs and use `AuditLogSerializer` instead trait EnvironmentAwareAuditLogSerializer { def onResponse(responseContext: AuditResponseContext, environmentContext: AuditEnvironmentContext): Option[JSONObject] diff --git a/audit/src/main/scala/tech/beshu/ror/audit/adapters/EnvironmentAwareAuditLogSerializerAdapter.scala b/audit/src/main/scala/tech/beshu/ror/audit/adapters/EnvironmentAwareAuditLogSerializerAdapter.scala index 3038b83c90..7c0a876e1b 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/adapters/EnvironmentAwareAuditLogSerializerAdapter.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/adapters/EnvironmentAwareAuditLogSerializerAdapter.scala @@ -19,11 +19,10 @@ package tech.beshu.ror.audit.adapters import org.json.JSONObject import tech.beshu.ror.audit._ -class EnvironmentAwareAuditLogSerializerAdapter(underlying: EnvironmentAwareAuditLogSerializer, - environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { +class EnvironmentAwareAuditLogSerializerAdapter(underlying: EnvironmentAwareAuditLogSerializer) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = { - underlying.onResponse(responseContext, environmentContext) + underlying.onResponse(responseContext, responseContext.requestContext.auditEnvironmentContext) } } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala index 7a1b070f3d..47d6ffbd22 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala @@ -16,6 +16,4 @@ */ package tech.beshu.ror.audit.instances -import tech.beshu.ror.audit.AuditEnvironmentContext - -class DefaultAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends DefaultAuditLogSerializerV2(environmentContext) +class DefaultAuditLogSerializer extends DefaultAuditLogSerializerV2 diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index e32ff9664a..0798e80d04 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -29,7 +29,6 @@ class DefaultAuditLogSerializerV1 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - environmentContext = None, fields = defaultV1AuditFields, allowedEventMode = Include(Set(Verbosity.Info)) ) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index e1955df94f..54b7df5035 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -24,12 +24,11 @@ import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldName, Audi import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper -class DefaultAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { +class DefaultAuditLogSerializerV2 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - environmentContext = Some(environmentContext), fields = defaultV2AuditFields, allowedEventMode = Include(Set(Verbosity.Info)) ) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala index b2d7894316..d1d239d2f3 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala @@ -20,14 +20,13 @@ import org.json.JSONObject import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.IncludeAll import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} +import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} -class FullAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { +class FullAuditLogSerializer extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - environmentContext = Some(environmentContext), fields = defaultV2AuditFields, allowedEventMode = IncludeAll ) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala index 89d5cf3c7a..2de22f7081 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala @@ -22,12 +22,11 @@ import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFiel import tech.beshu.ror.audit.utils.AuditSerializationHelper import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} -class FullAuditLogWithQuerySerializer(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { +class FullAuditLogWithQuerySerializer extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - environmentContext = Some(environmentContext), fields = queryV2AuditFields, allowedEventMode = IncludeAll ) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala index 86dc1e4dea..151eb580e9 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala @@ -16,6 +16,4 @@ */ package tech.beshu.ror.audit.instances -import tech.beshu.ror.audit.AuditEnvironmentContext - -class QueryAuditLogSerializer(environmentContext: AuditEnvironmentContext) extends QueryAuditLogSerializerV2(environmentContext) +class QueryAuditLogSerializer extends QueryAuditLogSerializerV2 \ No newline at end of file diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index 71b21d35fa..23b21730c1 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -29,7 +29,6 @@ class QueryAuditLogSerializerV1 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - environmentContext = None, fields = queryV1AuditFields, allowedEventMode = Include(Set(Verbosity.Info)) ) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index acbc74e6db..76f2e22e48 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -24,12 +24,11 @@ import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldName, Audi import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper -class QueryAuditLogSerializerV2(environmentContext: AuditEnvironmentContext) extends AuditLogSerializer { +class QueryAuditLogSerializerV2 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - environmentContext = Some(environmentContext), fields = queryV2AuditFields, allowedEventMode = Include(Set(Verbosity.Info)) ) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala b/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala index dc9f91482a..5c16dbc120 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala @@ -31,23 +31,22 @@ private[ror] object AuditSerializationHelper { private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("GMT")) def serialize(responseContext: AuditResponseContext, - environmentContext: Option[AuditEnvironmentContext], fields: Map[AuditFieldName, AuditFieldValueDescriptor], allowedEventMode: AllowedEventMode): Option[JSONObject] = responseContext match { case Allowed(requestContext, verbosity, reason) => allowedEvent( allowedEventMode, verbosity, - createEntry(fields, EventData(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, environmentContext, None)) + createEntry(fields, EventData(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) ) case ForbiddenBy(requestContext, _, reason) => - Some(createEntry(fields, EventData(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, environmentContext, None))) + Some(createEntry(fields, EventData(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None))) case Forbidden(requestContext) => - Some(createEntry(fields, EventData(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, environmentContext, None))) + Some(createEntry(fields, EventData(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None))) case RequestedIndexNotExist(requestContext) => - Some(createEntry(fields, EventData(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, environmentContext, None))) + Some(createEntry(fields, EventData(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None))) case Errored(requestContext, cause) => - Some(createEntry(fields, EventData(matched = false, "ERRORED", "error", responseContext.duration, requestContext, environmentContext, Some(cause)))) + Some(createEntry(fields, EventData(matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause)))) } private def allowedEvent(allowedEventMode: AllowedEventMode, verbosity: Verbosity, entry: JSONObject) = { @@ -101,8 +100,8 @@ private[ror] object AuditSerializationHelper { case AuditFieldValueDescriptor.Content => requestContext.content case AuditFieldValueDescriptor.ContentLengthInBytes => requestContext.contentLength case AuditFieldValueDescriptor.ContentLengthInKb => requestContext.contentLength / 1024 - case AuditFieldValueDescriptor.EsNodeName => eventData.environmentContext.map(_.esNodeName).getOrElse("") - case AuditFieldValueDescriptor.EsClusterName => eventData.environmentContext.map(_.esClusterName).getOrElse("") + case AuditFieldValueDescriptor.EsNodeName => eventData.requestContext.auditEnvironmentContext.esNodeName + case AuditFieldValueDescriptor.EsClusterName => eventData.requestContext.auditEnvironmentContext.esClusterName case AuditFieldValueDescriptor.StaticText(text) => text case AuditFieldValueDescriptor.Combined(values) => values.map(resolver(eventData)).mkString } @@ -128,7 +127,6 @@ private[ror] object AuditSerializationHelper { reason: String, duration: FiniteDuration, requestContext: AuditRequestContext, - environmentContext: Option[AuditEnvironmentContext], error: Option[Throwable]) sealed trait AllowedEventMode diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditRequestContextBasedOnAclResult.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditRequestContextBasedOnAclResult.scala index fe88dd7725..005283c492 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditRequestContextBasedOnAclResult.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditRequestContextBasedOnAclResult.scala @@ -26,7 +26,7 @@ import tech.beshu.ror.accesscontrol.domain.LoggedUser.{DirectlyLoggedUser, Imper import tech.beshu.ror.accesscontrol.domain.{Address, Header} import tech.beshu.ror.accesscontrol.request.RequestContext import tech.beshu.ror.accesscontrol.request.RequestContextOps.* -import tech.beshu.ror.audit.{AuditRequestContext, Headers} +import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditRequestContext, Headers} import tech.beshu.ror.implicits.* import java.time.Instant @@ -36,6 +36,7 @@ private[audit] class AuditRequestContextBasedOnAclResult[B <: BlockContext](requ userMetadata: Option[UserMetadata], historyEntries: Vector[History[B]], loggingContext: LoggingContext, + override val auditEnvironmentContext: AuditEnvironmentContext, override val generalAuditEvents: JSONObject, override val involvesIndices: Boolean) extends AuditRequestContext { diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditingTool.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditingTool.scala index 80dcd2ccfa..1121ef86eb 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditingTool.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditingTool.scala @@ -40,8 +40,8 @@ import java.time.Clock final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) (implicit loggingContext: LoggingContext) { - def audit[B <: BlockContext](response: ResponseContext[B]): Task[Unit] = { - val auditResponseContext = toAuditResponse(response) + def audit[B <: BlockContext](response: ResponseContext[B], auditEnvironmentContext: AuditEnvironmentContext): Task[Unit] = { + val auditResponseContext = toAuditResponse(response, auditEnvironmentContext) auditSinks .parTraverse(_.submit(auditResponseContext)) .map((_: NonEmptyList[Unit]) => ()) @@ -53,12 +53,13 @@ final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) .map((_: NonEmptyList[Unit]) => ()) } - private def toAuditResponse[B <: BlockContext](responseContext: ResponseContext[B]): AuditResponseContext = { + private def toAuditResponse[B <: BlockContext](responseContext: ResponseContext[B], auditEnvironmentContext: AuditEnvironmentContext): AuditResponseContext = { responseContext match { case allowedBy: ResponseContext.AllowedBy[B] => AuditResponseContext.Allowed( requestContext = toAuditRequestContext( requestContext = allowedBy.requestContext, + auditEnvironmentContext = auditEnvironmentContext, blockContext = Some(allowedBy.blockContext), userMetadata = Some(allowedBy.blockContext.userMetadata), historyEntries = allowedBy.history, @@ -71,6 +72,7 @@ final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) AuditResponseContext.Allowed( requestContext = toAuditRequestContext( requestContext = allow.requestContext, + auditEnvironmentContext = auditEnvironmentContext, blockContext = None, userMetadata = Some(allow.userMetadata), historyEntries = allow.history, @@ -83,6 +85,7 @@ final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) AuditResponseContext.ForbiddenBy( requestContext = toAuditRequestContext( requestContext = forbiddenBy.requestContext, + auditEnvironmentContext = auditEnvironmentContext, blockContext = Some(forbiddenBy.blockContext), userMetadata = Some(forbiddenBy.blockContext.userMetadata), historyEntries = forbiddenBy.history), @@ -92,6 +95,7 @@ final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) case forbidden: ResponseContext.Forbidden[B] => AuditResponseContext.Forbidden(toAuditRequestContext( requestContext = forbidden.requestContext, + auditEnvironmentContext = auditEnvironmentContext, blockContext = None, userMetadata = None, historyEntries = forbidden.history)) @@ -99,6 +103,7 @@ final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) AuditResponseContext.RequestedIndexNotExist( toAuditRequestContext( requestContext = requestedIndexNotExist.requestContext, + auditEnvironmentContext = auditEnvironmentContext, blockContext = None, userMetadata = None, historyEntries = requestedIndexNotExist.history) @@ -107,6 +112,7 @@ final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) AuditResponseContext.Errored( requestContext = toAuditRequestContext( requestContext = errored.requestContext, + auditEnvironmentContext = auditEnvironmentContext, blockContext = None, userMetadata = None, historyEntries = Vector.empty), @@ -120,6 +126,7 @@ final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) } private def toAuditRequestContext[B <: BlockContext](requestContext: RequestContext.Aux[B], + auditEnvironmentContext: AuditEnvironmentContext, blockContext: Option[B], userMetadata: Option[UserMetadata], historyEntries: Vector[History[B]], @@ -129,6 +136,7 @@ final class AuditingTool private(auditSinks: NonEmptyList[BaseAuditSink]) userMetadata, historyEntries, loggingContext, + auditEnvironmentContext, generalAuditEvents, involvesIndices(blockContext) ) @@ -162,8 +170,8 @@ object AuditingTool extends Logging { auditCluster: AuditCluster) extends Config object EsIndexBasedSink { - def default(implicit auditEnvironmentContext: AuditEnvironmentContext): EsIndexBasedSink = EsIndexBasedSink( - logSerializer = new DefaultAuditLogSerializer(auditEnvironmentContext), + val default: EsIndexBasedSink = EsIndexBasedSink( + logSerializer = new DefaultAuditLogSerializer, rorAuditIndexTemplate = RorAuditIndexTemplate.default, auditCluster = AuditCluster.LocalAuditCluster, ) @@ -174,8 +182,8 @@ object AuditingTool extends Logging { auditCluster: AuditCluster) extends Config object EsDataStreamBasedSink { - def default(implicit auditEnvironmentContext: AuditEnvironmentContext): EsDataStreamBasedSink = EsDataStreamBasedSink( - logSerializer = new DefaultAuditLogSerializer(auditEnvironmentContext), + val default: EsDataStreamBasedSink = EsDataStreamBasedSink( + logSerializer = new DefaultAuditLogSerializer, rorAuditDataStream = RorAuditDataStream.default, auditCluster = AuditCluster.LocalAuditCluster, ) @@ -185,8 +193,8 @@ object AuditingTool extends Logging { loggerName: RorAuditLoggerName) extends Config object LogBasedSink { - def default(implicit auditEnvironmentContext: AuditEnvironmentContext): LogBasedSink = LogBasedSink( - logSerializer = new DefaultAuditLogSerializer(auditEnvironmentContext), + val default: LogBasedSink = LogBasedSink( + logSerializer = new DefaultAuditLogSerializer, loggerName = RorAuditLoggerName.default ) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala index 12a77f86cd..3e2d6a76b8 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/ConfigurableAuditLogSerializer.scala @@ -19,13 +19,12 @@ package tech.beshu.ror.accesscontrol.audit.configurable import org.json.JSONObject import tech.beshu.ror.audit.utils.AuditSerializationHelper import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} +import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} -class ConfigurableAuditLogSerializer(val environmentContext: AuditEnvironmentContext, - val allowedEventMode: AllowedEventMode, +class ConfigurableAuditLogSerializer(val allowedEventMode: AllowedEventMode, val fields: Map[AuditFieldName, AuditFieldValueDescriptor]) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - AuditSerializationHelper.serialize(responseContext, Some(environmentContext), fields, allowedEventMode) + AuditSerializationHelper.serialize(responseContext, fields, allowedEventMode) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala index 7bf1a6d24a..98b63fc471 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/RawRorConfigBasedCoreFactory.scala @@ -23,7 +23,7 @@ import monix.eval.Task import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.* import tech.beshu.ror.accesscontrol.EnabledAccessControlList.AccessControlListStaticContext -import tech.beshu.ror.accesscontrol.audit.{AuditEnvironmentContextBasedOnEsNodeSettings, LoggingContext} +import tech.beshu.ror.accesscontrol.audit.LoggingContext import tech.beshu.ror.accesscontrol.blocks.Block.{RuleDefinition, Verbosity} import tech.beshu.ror.accesscontrol.blocks.ImpersonationWarning.ImpersonationWarningSupport import tech.beshu.ror.accesscontrol.blocks.definitions.UserDef.Mode @@ -50,7 +50,7 @@ import tech.beshu.ror.accesscontrol.utils.CirceOps.* import tech.beshu.ror.accesscontrol.utils.CirceOps.DecoderHelpers.FieldListResult.{FieldListValue, NoField} import tech.beshu.ror.configuration.RorConfig.ImpersonationWarningsReader import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} -import tech.beshu.ror.es.{EsNodeSettings, EsVersion} +import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* import tech.beshu.ror.syntax.* import tech.beshu.ror.utils.ScalaOps.* @@ -67,8 +67,7 @@ trait CoreFactory { mocksProvider: MocksProvider): Task[Either[NonEmptyList[CoreCreationError], Core]] } -class RawRorConfigBasedCoreFactory(esVersion: EsVersion, - esNodeSettings: EsNodeSettings) +class RawRorConfigBasedCoreFactory(esVersion: EsVersion) (implicit environmentConfig: EnvironmentConfig) extends CoreFactory with Logging { @@ -318,8 +317,7 @@ class RawRorConfigBasedCoreFactory(esVersion: EsVersion, dynamicVariableTransformationAliases.items.map(_.alias) ) ) - auditEnvironmentContext = new AuditEnvironmentContextBasedOnEsNodeSettings(esNodeSettings) - auditingTools <- AsyncDecoderCreator.from(AuditingSettingsDecoder.instance(esVersion)(auditEnvironmentContext)) + auditingTools <- AsyncDecoderCreator.from(AuditingSettingsDecoder.instance(esVersion)) authProxies <- AsyncDecoderCreator.from(ProxyAuthDefinitionsDecoder.instance) authenticationServices <- AsyncDecoderCreator.from(ExternalAuthenticationServicesDecoder.instance(httpClientFactory)) authorizationServices <- AsyncDecoderCreator.from(ExternalAuthorizationServicesDecoder.instance(httpClientFactory)) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index f3647f530f..e936bbc4e4 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -36,7 +36,7 @@ import tech.beshu.ror.accesscontrol.utils.SyncDecoderCreator import tech.beshu.ror.audit.AuditResponseContext.Verbosity import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.adapters.* -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer} +import tech.beshu.ror.audit.AuditLogSerializer import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* import tech.beshu.ror.utils.yaml.YamlKeyDecoder @@ -46,42 +46,40 @@ import scala.util.{Failure, Success, Try} object AuditingSettingsDecoder extends Logging { - def instance(esVersion: EsVersion) - (implicit context: AuditEnvironmentContext): Decoder[Option[AuditingTool.AuditSettings]] = { + def instance(esVersion: EsVersion): Decoder[Option[AuditingTool.AuditSettings]] = { for { auditSettings <- auditSettingsDecoder(esVersion) deprecatedAuditSettings <- DeprecatedAuditSettingsDecoder.instance } yield auditSettings.orElse(deprecatedAuditSettings) } - private def auditSettingsDecoder(esVersion: EsVersion) - (using AuditEnvironmentContext): Decoder[Option[AuditingTool.AuditSettings]] = Decoder.instance { c => + private def auditSettingsDecoder(esVersion: EsVersion): Decoder[Option[AuditingTool.AuditSettings]] = Decoder.instance { c => for { isAuditEnabled <- YamlKeyDecoder[Boolean]( segments = NonEmptyList.of("audit", "enabled"), default = false ).apply(c) result <- if (isAuditEnabled) { - decodeAuditSettings(using esVersion, summon[AuditEnvironmentContext])(c).map(Some.apply) + decodeAuditSettings(using esVersion)(c).map(Some.apply) } else { Right(None) } } yield result } - private def decodeAuditSettings(using EsVersion, AuditEnvironmentContext) = { - decodeAuditSettingsWith(using auditSinkConfigSimpleDecoder, summon[AuditEnvironmentContext]) + private def decodeAuditSettings(using EsVersion) = { + decodeAuditSettingsWith(using auditSinkConfigSimpleDecoder) .handleErrorWith { error => if (error.aclCreationError.isDefined) { // the schema was valid, but the config not Decoder.failed(error) } else { - decodeAuditSettingsWith(using auditSinkConfigExtendedDecoder, summon[AuditEnvironmentContext]) + decodeAuditSettingsWith(using auditSinkConfigExtendedDecoder) } } } - private def decodeAuditSettingsWith(using Decoder[AuditSink], AuditEnvironmentContext) = { + private def decodeAuditSettingsWith(using Decoder[AuditSink]) = { SyncDecoderCreator .instance { _.downField("audit").downField("outputs").as[Option[List[AuditSink]]] @@ -100,7 +98,7 @@ object AuditingSettingsDecoder extends Logging { .decoder } - private def auditSinkConfigSimpleDecoder(using EsVersion, AuditEnvironmentContext): Decoder[AuditSink] = { + private def auditSinkConfigSimpleDecoder(using EsVersion): Decoder[AuditSink] = { Decoder[AuditSinkType] .emap[AuditSink.Config] { case AuditSinkType.DataStream => @@ -113,7 +111,7 @@ object AuditingSettingsDecoder extends Logging { .map(AuditSink.Enabled.apply) } - private def auditSinkConfigExtendedDecoder(using EsVersion, AuditEnvironmentContext): Decoder[AuditSink] = { + private def auditSinkConfigExtendedDecoder(using EsVersion): Decoder[AuditSink] = { given Decoder[RorAuditLoggerName] = { SyncDecoderCreator .from(nonEmptyStringDecoder) @@ -209,7 +207,7 @@ object AuditingSettingsDecoder extends Logging { } .decoder - given auditLogSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = SyncDecoderCreator.from( + given auditLogSerializerDecoder: Decoder[Option[AuditLogSerializer]] = SyncDecoderCreator.from( Decoder.instance[Option[AuditLogSerializer]] { c => for { serializerType <- c.as[SerializerType] @@ -226,17 +224,17 @@ object AuditingSettingsDecoder extends Logging { ) .decoder - private def configurableSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + private def configurableSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => for { allowedEventMode <- c.downField("verbosity_level_serialization_mode").as[AllowedEventMode] .left.map(withAuditingSettingsCreationErrorMessage(msg => s"Configurable serializer is used, but the 'verbosity_level_serialization_mode' setting is invalid: $msg")) fields <- c.downField("fields").as[Map[AuditFieldName, AuditFieldValueDescriptor]] .left.map(withAuditingSettingsCreationErrorMessage(msg => s"Configurable serializer is used, but the 'fields' setting is missing or invalid: $msg")) - serializer = new ConfigurableAuditLogSerializer(context, allowedEventMode, fields) + serializer = new ConfigurableAuditLogSerializer(allowedEventMode, fields) } yield Some(serializer) } - private def classNameBasedSerializerDecoder(using context: AuditEnvironmentContext): Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + private def classNameBasedSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => for { classNameOpt <- c.downField("class_name").as[Option[String]] fullClassNameOpt <- c.downField("serializer").as[Option[String]] @@ -256,8 +254,7 @@ object AuditingSettingsDecoder extends Logging { } @nowarn("cat=deprecation") - private def createSerializerInstanceFromClassName(fullClassName: String) - (using context: AuditEnvironmentContext): Either[AuditingSettingsCreationError, AuditLogSerializer] = { + private def createSerializerInstanceFromClassName(fullClassName: String): Either[AuditingSettingsCreationError, AuditLogSerializer] = { val clazz = Try(Class.forName(fullClassName)).fold( { case _: ClassNotFoundException => throw new IllegalStateException(s"Serializer with class name $fullClassName not found.") @@ -266,27 +263,21 @@ object AuditingSettingsDecoder extends Logging { identity ) - def createInstanceOfEnvironmentAwareSerializer(): Try[Any] = - Try(clazz.getConstructor(classOf[AuditEnvironmentContext])).map(_.newInstance(context)) - def createInstanceOfSimpleSerializer(): Try[Any] = Try(clazz.getDeclaredConstructor()).map(_.newInstance()) - val serializer = - createInstanceOfEnvironmentAwareSerializer() - .orElse(createInstanceOfSimpleSerializer()) - .getOrElse( - throw new IllegalStateException( - s"Class ${clazz.getName} is required to have either one (AuditEnvironmentContext) parameter constructor or constructor without parameters" - ) - ) + val serializer = createInstanceOfSimpleSerializer().getOrElse( + throw new IllegalStateException( + s"Class ${clazz.getName} is required to have either one (AuditEnvironmentContext) parameter constructor or constructor without parameters" + ) + ) Try { serializer match { case serializer: tech.beshu.ror.audit.AuditLogSerializer => Some(serializer) case serializer: tech.beshu.ror.audit.EnvironmentAwareAuditLogSerializer => - Some(new EnvironmentAwareAuditLogSerializerAdapter(serializer, summon[AuditEnvironmentContext])) + Some(new EnvironmentAwareAuditLogSerializerAdapter(serializer)) case serializer: tech.beshu.ror.requestcontext.AuditLogSerializer[_] => Some(new DeprecatedAuditLogSerializerAdapter(serializer)) case _ => None @@ -391,7 +382,7 @@ object AuditingSettingsDecoder extends Logging { } private object DeprecatedAuditSettingsDecoder { - def instance(using AuditEnvironmentContext): Decoder[Option[AuditingTool.AuditSettings]] = Decoder.instance { c => + def instance: Decoder[Option[AuditingTool.AuditSettings]] = Decoder.instance { c => whenEnabled(c) { for { auditIndexTemplate <- decodeOptionalSetting[RorAuditIndexTemplate](c)("index_template", fallbackKey = "audit_index_template") diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/logging/AccessControlListLoggingDecorator.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/logging/AccessControlListLoggingDecorator.scala index 4d8c8442f4..57d9c89c2f 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/logging/AccessControlListLoggingDecorator.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/logging/AccessControlListLoggingDecorator.scala @@ -30,6 +30,7 @@ import tech.beshu.ror.accesscontrol.blocks.{Block, BlockContext, BlockContextUpd import tech.beshu.ror.accesscontrol.domain.Header import tech.beshu.ror.accesscontrol.logging.ResponseContext.* import tech.beshu.ror.accesscontrol.request.RequestContext +import tech.beshu.ror.audit.AuditEnvironmentContext import tech.beshu.ror.constants import tech.beshu.ror.implicits.* import tech.beshu.ror.utils.TaskOps.* @@ -39,6 +40,7 @@ import scala.util.{Failure, Success} class AccessControlListLoggingDecorator(val underlying: AccessControlList, auditingTool: Option[AuditingTool]) (implicit loggingContext: LoggingContext, + auditEnvironmentContext: AuditEnvironmentContext, scheduler: Scheduler) extends AccessControlList with Logging { @@ -105,7 +107,7 @@ class AccessControlListLoggingDecorator(val underlying: AccessControlList, } auditingTool.foreach { _ - .audit(responseContext) + .audit(responseContext, auditEnvironmentContext) .runAsync { case Right(_) => case Left(ex) => diff --git a/core/src/main/scala/tech/beshu/ror/boot/ReadonlyRest.scala b/core/src/main/scala/tech/beshu/ror/boot/ReadonlyRest.scala index db757d790b..4032538787 100644 --- a/core/src/main/scala/tech/beshu/ror/boot/ReadonlyRest.scala +++ b/core/src/main/scala/tech/beshu/ror/boot/ReadonlyRest.scala @@ -21,7 +21,7 @@ import monix.eval.Task import monix.execution.Scheduler import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.audit.sink.AuditSinkServiceCreator -import tech.beshu.ror.accesscontrol.audit.{AuditingTool, LoggingContext} +import tech.beshu.ror.accesscontrol.audit.{AuditEnvironmentContextBasedOnEsNodeSettings, AuditingTool, LoggingContext} import tech.beshu.ror.accesscontrol.blocks.definitions.ldap.implementations.UnboundidLdapConnectionPoolProvider import tech.beshu.ror.accesscontrol.blocks.mocks.{AuthServicesMocks, MutableMocksProviderWithCachePerRequest} import tech.beshu.ror.accesscontrol.domain.RorConfigurationIndex @@ -31,6 +31,7 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCreationError.Reason.Message import tech.beshu.ror.accesscontrol.factory.{AsyncHttpClientsFactory, Core, CoreFactory, RawRorConfigBasedCoreFactory} import tech.beshu.ror.accesscontrol.logging.AccessControlListLoggingDecorator +import tech.beshu.ror.audit.AuditEnvironmentContext import tech.beshu.ror.boot.ReadonlyRest.* import tech.beshu.ror.configuration.* import tech.beshu.ror.configuration.ConfigLoading.{ErrorOr, LoadRorConfig} @@ -233,6 +234,7 @@ class ReadonlyRest(coreFactory: CoreFactory, ldapConnectionPoolProvider: UnboundidLdapConnectionPoolProvider, core: Core): EitherT[Task, NonEmptyList[CoreCreationError], Engine] = { implicit val loggingContext: LoggingContext = LoggingContext(core.accessControl.staticContext.obfuscatedHeaders) + implicit val auditEnvironmentContext: AuditEnvironmentContext = new AuditEnvironmentContextBasedOnEsNodeSettings(esEnv.esNodeSettings) EitherT(createAuditingTool(core)) .map { auditingTool => val decoratedCore = Core( @@ -329,7 +331,7 @@ object ReadonlyRest { env: EsEnv) (implicit scheduler: Scheduler, environmentConfig: EnvironmentConfig): ReadonlyRest = { - val coreFactory: CoreFactory = new RawRorConfigBasedCoreFactory(env.esVersion, env.esNodeSettings) + val coreFactory: CoreFactory = new RawRorConfigBasedCoreFactory(env.esVersion) create(coreFactory, indexContentService, auditSinkServiceCreator, env) } diff --git a/core/src/test/scala/tech/beshu/ror/integration/AuditOutputFormatTests.scala b/core/src/test/scala/tech/beshu/ror/integration/AuditOutputFormatTests.scala index 0e01a0ac98..7237d56cd9 100644 --- a/core/src/test/scala/tech/beshu/ror/integration/AuditOutputFormatTests.scala +++ b/core/src/test/scala/tech/beshu/ror/integration/AuditOutputFormatTests.scala @@ -29,6 +29,7 @@ import tech.beshu.ror.accesscontrol.audit.sink.{AuditDataStreamCreator, DataStre import tech.beshu.ror.accesscontrol.audit.{AuditingTool, LoggingContext} import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.logging.AccessControlListLoggingDecorator +import tech.beshu.ror.audit.AuditEnvironmentContext import tech.beshu.ror.audit.instances.DefaultAuditLogSerializer import tech.beshu.ror.es.{DataStreamBasedAuditSinkService, DataStreamService, IndexBasedAuditSinkService} import tech.beshu.ror.mocks.MockRequestContext @@ -157,15 +158,16 @@ class AuditOutputFormatTests extends AnyWordSpec with BaseYamlLoadedAccessContro private def auditedAcl(indexBasedAuditSinkService: IndexBasedAuditSinkService, dataStreamBasedAuditSinkService: DataStreamBasedAuditSinkService) = { implicit val loggingContext: LoggingContext = LoggingContext(Set.empty) + implicit val auditEnvironmentContext: AuditEnvironmentContext = testAuditEnvironmentContext val settings = AuditingTool.AuditSettings( NonEmptyList.of( AuditSink.Enabled(Config.EsIndexBasedSink( - new DefaultAuditLogSerializer(testAuditEnvironmentContext), + new DefaultAuditLogSerializer, RorAuditIndexTemplate.default, AuditCluster.LocalAuditCluster )), AuditSink.Enabled(Config.EsDataStreamBasedSink( - new DefaultAuditLogSerializer(testAuditEnvironmentContext), + new DefaultAuditLogSerializer, RorAuditDataStream.default, AuditCluster.LocalAuditCluster )) diff --git a/core/src/test/scala/tech/beshu/ror/integration/BaseYamlLoadedAccessControlTest.scala b/core/src/test/scala/tech/beshu/ror/integration/BaseYamlLoadedAccessControlTest.scala index d72436a940..73b770df4a 100644 --- a/core/src/test/scala/tech/beshu/ror/integration/BaseYamlLoadedAccessControlTest.scala +++ b/core/src/test/scala/tech/beshu/ror/integration/BaseYamlLoadedAccessControlTest.scala @@ -27,7 +27,7 @@ import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig} import tech.beshu.ror.mocks.{MockHttpClientsFactory, MockLdapConnectionPoolProvider} import tech.beshu.ror.providers.* import tech.beshu.ror.utils.TestsPropertiesProvider -import tech.beshu.ror.utils.TestsUtils.{BlockContextAssertion, defaultEsVersionForTests, testEsNodeSettings, unsafeNes} +import tech.beshu.ror.utils.TestsUtils.{BlockContextAssertion, defaultEsVersionForTests, unsafeNes} trait BaseYamlLoadedAccessControlTest extends BlockContextAssertion { @@ -41,7 +41,7 @@ trait BaseYamlLoadedAccessControlTest extends BlockContextAssertion { envVarsProvider = envVarsProvider, propertiesProvider = propertiesProvider ) - private val factory = new RawRorConfigBasedCoreFactory(defaultEsVersionForTests, testEsNodeSettings) + private val factory = new RawRorConfigBasedCoreFactory(defaultEsVersionForTests) protected val ldapConnectionPoolProvider: UnboundidLdapConnectionPoolProvider = MockLdapConnectionPoolProvider protected val httpClientsFactory: HttpClientsFactory = MockHttpClientsFactory protected val mockProvider: MocksProvider = NoOpMocksProvider diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index d2008a72b1..7c9e01d552 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -50,7 +50,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { private def factory(esVersion: EsVersion = defaultEsVersionForTests) = { implicit val environmentConfig: EnvironmentConfig = EnvironmentConfig.default - new RawRorConfigBasedCoreFactory(esVersion, testEsNodeSettings) + new RawRorConfigBasedCoreFactory(esVersion) } private val zonedDateTime = ZonedDateTime.of(2019, 1, 1, 0, 1, 59, 0, ZoneId.of("+1")) @@ -408,8 +408,6 @@ class AuditSettingsTests extends AnyWordSpec with Inside { val configuredSerializer = serializer(config).asInstanceOf[ConfigurableAuditLogSerializer] - configuredSerializer.environmentContext.esClusterName shouldBe testAuditEnvironmentContext.esClusterName - configuredSerializer.environmentContext.esNodeName shouldBe testAuditEnvironmentContext.esNodeName configuredSerializer.allowedEventMode shouldBe AllowedEventMode.Include(Set(Verbosity.Info)) configuredSerializer.fields shouldBe Map( AuditFieldName("node_name_with_static_suffix") -> AuditFieldValueDescriptor.Combined(List(AuditFieldValueDescriptor.EsNodeName, AuditFieldValueDescriptor.StaticText(" with suffix"))), @@ -2015,4 +2013,6 @@ private object DummyAuditRequestContext extends AuditRequestContext { override def rawAuthHeader: Option[String] = None override def generalAuditEvents: JSONObject = new JSONObject + + override def auditEnvironmentContext: AuditEnvironmentContext = testAuditEnvironmentContext } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/CoreFactoryTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/CoreFactoryTests.scala index 1bfd2c5869..2982cc6393 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/CoreFactoryTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/CoreFactoryTests.scala @@ -40,7 +40,7 @@ class CoreFactoryTests extends AnyWordSpec with Inside with MockFactory { private val factory: CoreFactory = { implicit val environmentConfig: EnvironmentConfig = EnvironmentConfig.default - new RawRorConfigBasedCoreFactory(defaultEsVersionForTests, testEsNodeSettings) + new RawRorConfigBasedCoreFactory(defaultEsVersionForTests) } "A RorAclFactory" should { diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/ImpersonationWarningsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/ImpersonationWarningsTests.scala index bce32288eb..0c5d7bc7b9 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/ImpersonationWarningsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/ImpersonationWarningsTests.scala @@ -387,6 +387,6 @@ class ImpersonationWarningsTests extends AnyWordSpec with Inside { private val factory: CoreFactory = { implicit val environmentConfig: EnvironmentConfig = EnvironmentConfig.default - new RawRorConfigBasedCoreFactory(defaultEsVersionForTests, testEsNodeSettings) + new RawRorConfigBasedCoreFactory(defaultEsVersionForTests) } } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/LocalUsersTest.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/LocalUsersTest.scala index c44813f69f..a02b11634b 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/LocalUsersTest.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/LocalUsersTest.scala @@ -319,7 +319,7 @@ class LocalUsersTest extends AnyWordSpec with Inside { private val factory = { implicit val environmentConfig: EnvironmentConfig = EnvironmentConfig.default - new RawRorConfigBasedCoreFactory(defaultEsVersionForTests, testEsNodeSettings) + new RawRorConfigBasedCoreFactory(defaultEsVersionForTests) } } diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/BaseRuleSettingsDecoderTest.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/BaseRuleSettingsDecoderTest.scala index 4751f42ce2..c2332d1965 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/BaseRuleSettingsDecoderTest.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/decoders/rules/BaseRuleSettingsDecoderTest.scala @@ -50,7 +50,7 @@ abstract class BaseRuleSettingsDecoderTest[T <: Rule : ClassTag] extends AnyWord protected def factory: RawRorConfigBasedCoreFactory = { implicit val environmentConfig: EnvironmentConfig = new EnvironmentConfig(envVarsProvider = envVarsProvider) - new RawRorConfigBasedCoreFactory(defaultEsVersionForTests, testEsNodeSettings) + new RawRorConfigBasedCoreFactory(defaultEsVersionForTests) } def assertDecodingSuccess(yaml: String, diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala index 43bd90d959..f0a1d28266 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala @@ -62,7 +62,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter "not submit any audit entry" when { "request was allowed and verbosity level was ERROR" in { val auditingTool = AuditingTool.create( - settings = auditSettings(new DefaultAuditLogSerializer(testAuditEnvironmentContext)), + settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { override def dataStream(cluster: AuditCluster): DataStreamBasedAuditSinkService = mockedDataStreamBasedAuditSinkService @@ -70,7 +70,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter override def index(cluster: AuditCluster): IndexBasedAuditSinkService = mock[IndexBasedAuditSinkService] } ).runSyncUnsafe().toOption.flatten.get - auditingTool.audit(createAllowedResponseContext(Policy.Allow, Verbosity.Error)).runSyncUnsafe() + auditingTool.audit(createAllowedResponseContext(Policy.Allow, Verbosity.Error), testAuditEnvironmentContext).runSyncUnsafe() } "custom serializer throws exception" in { val auditingTool = AuditingTool.create( @@ -83,7 +83,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter } ).runSyncUnsafe().toOption.flatten.get an[IllegalArgumentException] should be thrownBy { - auditingTool.audit(createAllowedResponseContext(Policy.Allow, Verbosity.Info)).runSyncUnsafe() + auditingTool.audit(createAllowedResponseContext(Policy.Allow, Verbosity.Info), testAuditEnvironmentContext).runSyncUnsafe() } } } @@ -95,14 +95,14 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter (dataStreamAuditSink.submit _).expects(fullDataStreamName("test_ds"), "mock-1", *).returning(()) val auditingTool = AuditingTool.create( - settings = auditSettings(new DefaultAuditLogSerializer(testAuditEnvironmentContext)), + settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { override def dataStream(cluster: AuditCluster): DataStreamBasedAuditSinkService = dataStreamAuditSink override def index(cluster: AuditCluster): IndexBasedAuditSinkService = indexAuditSink } ).runSyncUnsafe().toOption.flatten.get - auditingTool.audit(createAllowedResponseContext(Policy.Allow, Verbosity.Info)).runSyncUnsafe() + auditingTool.audit(createAllowedResponseContext(Policy.Allow, Verbosity.Info), testAuditEnvironmentContext).runSyncUnsafe() } "request was matched by forbidden rule" in { val indexAuditSink = mock[IndexBasedAuditSinkService] @@ -111,7 +111,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter (dataStreamAuditSink.submit _).expects(fullDataStreamName("test_ds"), "mock-1", *).returning(()) val auditingTool = AuditingTool.create( - settings = auditSettings(new DefaultAuditLogSerializer(testAuditEnvironmentContext)), + settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { override def dataStream(cluster: AuditCluster): DataStreamBasedAuditSinkService = dataStreamAuditSink @@ -132,7 +132,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter Vector.empty ) - auditingTool.audit(responseContext).runSyncUnsafe() + auditingTool.audit(responseContext, testAuditEnvironmentContext).runSyncUnsafe() } "request was forbidden" in { val indexAuditSink = mock[IndexBasedAuditSinkService] @@ -141,7 +141,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter (dataStreamAuditSink.submit _).expects(fullDataStreamName("test_ds"), "mock-1", *).returning(()) val auditingTool = AuditingTool.create( - settings = auditSettings(new DefaultAuditLogSerializer(testAuditEnvironmentContext)), + settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { override def dataStream(cluster: AuditCluster): DataStreamBasedAuditSinkService = dataStreamAuditSink @@ -152,7 +152,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter val requestContext = MockRequestContext.indices.copy(timestamp = someday.toInstant, id = RequestContext.Id.fromString("mock-1")) val responseContext = Forbidden(requestContext, Vector.empty) - auditingTool.audit(responseContext).runSyncUnsafe() + auditingTool.audit(responseContext, testAuditEnvironmentContext).runSyncUnsafe() } "request was finished with error" in { val indexAuditSink = mock[IndexBasedAuditSinkService] @@ -161,7 +161,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter (dataStreamAuditSink.submit _).expects(fullDataStreamName("test_ds"), "mock-1", *).returning(()) val auditingTool = AuditingTool.create( - settings = auditSettings(new DefaultAuditLogSerializer(testAuditEnvironmentContext)), + settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { override def dataStream(cluster: AuditCluster): DataStreamBasedAuditSinkService = dataStreamAuditSink @@ -172,7 +172,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter val requestContext = MockRequestContext.indices.copy(timestamp = someday.toInstant, id = RequestContext.Id.fromString("mock-1")) val responseContext = Errored(requestContext, new Exception("error")) - auditingTool.audit(responseContext).runSyncUnsafe() + auditingTool.audit(responseContext, testAuditEnvironmentContext).runSyncUnsafe() } } } @@ -182,7 +182,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter settings = AuditSettings( NonEmptyList.of( AuditSink.Enabled(Config.LogBasedSink( - new DefaultAuditLogSerializer(testAuditEnvironmentContext), + new DefaultAuditLogSerializer, RorAuditLoggerName.default )) ) @@ -200,7 +200,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter auditLogFile.overwrite("") - auditingTool.audit(responseContext).runSyncUnsafe() + auditingTool.audit(responseContext, testAuditEnvironmentContext).runSyncUnsafe() val logFileContent = auditLogFile.contentAsString logFileContent should include(requestContextId.value) diff --git a/core/src/test/scala/tech/beshu/ror/unit/boot/ReadonlyRestStartingTests.scala b/core/src/test/scala/tech/beshu/ror/unit/boot/ReadonlyRestStartingTests.scala index 2b588070f2..8a84192d6e 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/boot/ReadonlyRestStartingTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/boot/ReadonlyRestStartingTests.scala @@ -1315,7 +1315,7 @@ class ReadonlyRestStartingTests val mockedIndexJsonContentManager = mock[IndexJsonContentService] mockIndexJsonContentManagerSourceOfCallTestConfig(mockedIndexJsonContentManager) - val dataStreamSinkConfig1 = AuditSink.Config.EsDataStreamBasedSink.default(testAuditEnvironmentContext) + val dataStreamSinkConfig1 = AuditSink.Config.EsDataStreamBasedSink.default val dataStreamSinkConfig2 = dataStreamSinkConfig1.copy( auditCluster = AuditCluster.RemoteAuditCluster(NonEmptyList.one(Uri.parse("0.0.0.0"))) ) From 9536e4a766e30a96fa66dd74b97474daec3179ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 4 Sep 2025 22:05:26 +0200 Subject: [PATCH 21/27] review changes --- .../DeprecatedAuditLogSerializerAdapter.scala | 2 +- .../instances/DefaultAuditLogSerializer.scala | 18 +++ .../DefaultAuditLogSerializerV1.scala | 52 +++----- .../DefaultAuditLogSerializerV2.scala | 53 +++------ .../instances/FullAuditLogSerializer.scala | 15 ++- .../FullAuditLogWithQuerySerializer.scala | 17 ++- .../instances/QueryAuditLogSerializer.scala | 9 ++ .../instances/QueryAuditLogSerializerV1.scala | 25 ++-- .../instances/QueryAuditLogSerializerV2.scala | 23 ++-- .../utils/AuditSerializationHelper.scala | 96 ++++++++++++--- .../{instances => utils}/SerializeUser.scala | 4 +- .../decoders/AuditingSettingsDecoder.scala | 111 ++++++++++-------- 12 files changed, 262 insertions(+), 163 deletions(-) rename audit/src/main/scala/tech/beshu/ror/audit/{instances => utils}/SerializeUser.scala (93%) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/adapters/DeprecatedAuditLogSerializerAdapter.scala b/audit/src/main/scala/tech/beshu/ror/audit/adapters/DeprecatedAuditLogSerializerAdapter.scala index b2802b7ee8..102e44cc33 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/adapters/DeprecatedAuditLogSerializerAdapter.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/adapters/DeprecatedAuditLogSerializerAdapter.scala @@ -18,7 +18,7 @@ package tech.beshu.ror.audit.adapters import org.json.JSONObject import tech.beshu.ror.audit.AuditResponseContext.{Allowed, Verbosity} -import tech.beshu.ror.audit.instances.SerializeUser +import tech.beshu.ror.audit.utils.SerializeUser import tech.beshu.ror.audit.{AuditLogSerializer, AuditRequestContext, AuditResponseContext} import tech.beshu.ror.commons.ResponseContext.FinalState import tech.beshu.ror.commons.shims.request.RequestContextShim diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala index 47d6ffbd22..0922a8b997 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala @@ -16,4 +16,22 @@ */ package tech.beshu.ror.audit.instances +/** + * Serializer for audit events that is aware of **rule-defined verbosity**. + * + * - Includes `CommonFields` and `EsEnvironmentFields`. + * - Serializes all non-Allowed events. + * - Serializes `Allowed` events only if the corresponding rule + * specifies that they should be logged at `Verbosity.Info`. + * + * This is the recommended serializer for standard audit logging. + */ +class BlockVerbosityAwareAuditLogSerializer extends DefaultAuditLogSerializer + +/** + * Base implementation delegating to [[DefaultAuditLogSerializerV2]]. + * + * - Not intended for direct external use. + * - Provides the underlying logic for [[BlockVerbosityAwareAuditLogSerializer]]. + */ class DefaultAuditLogSerializer extends DefaultAuditLogSerializerV2 diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala index 0798e80d04..8719eb89cd 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala @@ -19,47 +19,33 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include -import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldName, AuditFieldValueDescriptor} -import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.CommonFields +/** + * Serializer for audit events (V1) that is aware of **rule-defined verbosity**. + * + * - Includes only `CommonFields` (request metadata, user, action, etc.). + * - Serializes all non-Allowed events. + * - Serializes `Allowed` events only if the corresponding rule + * specifies that they should be logged at `Verbosity.Info`. + * + * Recommended when a minimal, compact audit log is sufficient. + */ +class BlockVerbosityAwareAuditLogSerializerV1 extends DefaultAuditLogSerializerV1 + +/** + * Base implementation for [[BlockVerbosityAwareAuditLogSerializerV1]]. + * Not intended for direct external use. + */ class DefaultAuditLogSerializerV1 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - fields = defaultV1AuditFields, + fieldGroups = Set(CommonFields), allowedEventMode = Include(Set(Verbosity.Info)) ) } - -private[ror] object DefaultAuditLogSerializerV1 { - val defaultV1AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( - AuditFieldName("match") -> AuditFieldValueDescriptor.IsMatched, - AuditFieldName("block") -> AuditFieldValueDescriptor.Reason, - AuditFieldName("id") -> AuditFieldValueDescriptor.Id, - AuditFieldName("final_state") -> AuditFieldValueDescriptor.FinalState, - AuditFieldName("@timestamp") -> AuditFieldValueDescriptor.Timestamp, - AuditFieldName("correlation_id") -> AuditFieldValueDescriptor.CorrelationId, - AuditFieldName("processingMillis") -> AuditFieldValueDescriptor.ProcessingDurationMillis, - AuditFieldName("error_type") -> AuditFieldValueDescriptor.ErrorType, - AuditFieldName("error_message") -> AuditFieldValueDescriptor.ErrorMessage, - AuditFieldName("content_len") -> AuditFieldValueDescriptor.ContentLengthInBytes, - AuditFieldName("content_len_kb") -> AuditFieldValueDescriptor.ContentLengthInKb, - AuditFieldName("type") -> AuditFieldValueDescriptor.Type, - AuditFieldName("origin") -> AuditFieldValueDescriptor.RemoteAddress, - AuditFieldName("destination") -> AuditFieldValueDescriptor.LocalAddress, - AuditFieldName("xff") -> AuditFieldValueDescriptor.XForwardedForHttpHeader, - AuditFieldName("task_id") -> AuditFieldValueDescriptor.TaskId, - AuditFieldName("req_method") -> AuditFieldValueDescriptor.HttpMethod, - AuditFieldName("headers") -> AuditFieldValueDescriptor.HttpHeaderNames, - AuditFieldName("path") -> AuditFieldValueDescriptor.HttpPath, - AuditFieldName("user") -> AuditFieldValueDescriptor.User, - AuditFieldName("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser, - AuditFieldName("action") -> AuditFieldValueDescriptor.Action, - AuditFieldName("indices") -> AuditFieldValueDescriptor.InvolvedIndices, - AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory - ) -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala index 54b7df5035..3f10e8d276 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala @@ -20,48 +20,33 @@ import org.json.JSONObject import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditResponseContext.Verbosity import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include -import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldName, AuditFieldValueDescriptor} -import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.{CommonFields, EsEnvironmentFields} +/** + * Serializer for audit events (V2) that is aware of **rule-defined verbosity**. + * + * - Includes `CommonFields` plus `EsEnvironmentFields` + * (cluster and node identifiers). + * - Serializes all non-Allowed events. + * - Serializes `Allowed` events only if the corresponding rule + * specifies that they should be logged at `Verbosity.Info`. + * + * Recommended when node and cluster context are also required. + */ +class BlockVerbosityAwareAuditLogSerializerV2 extends DefaultAuditLogSerializerV2 + +/** + * Base implementation for [[BlockVerbosityAwareAuditLogSerializerV2]]. + * Not intended for direct external use. + */ class DefaultAuditLogSerializerV2 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - fields = defaultV2AuditFields, + fieldGroups = Set(CommonFields, EsEnvironmentFields), allowedEventMode = Include(Set(Verbosity.Info)) ) } - -private[ror] object DefaultAuditLogSerializerV2 { - val defaultV2AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( - AuditFieldName("match") -> AuditFieldValueDescriptor.IsMatched, - AuditFieldName("block") -> AuditFieldValueDescriptor.Reason, - AuditFieldName("id") -> AuditFieldValueDescriptor.Id, - AuditFieldName("final_state") -> AuditFieldValueDescriptor.FinalState, - AuditFieldName("@timestamp") -> AuditFieldValueDescriptor.Timestamp, - AuditFieldName("correlation_id") -> AuditFieldValueDescriptor.CorrelationId, - AuditFieldName("processingMillis") -> AuditFieldValueDescriptor.ProcessingDurationMillis, - AuditFieldName("error_type") -> AuditFieldValueDescriptor.ErrorType, - AuditFieldName("error_message") -> AuditFieldValueDescriptor.ErrorMessage, - AuditFieldName("content_len") -> AuditFieldValueDescriptor.ContentLengthInBytes, - AuditFieldName("content_len_kb") -> AuditFieldValueDescriptor.ContentLengthInKb, - AuditFieldName("type") -> AuditFieldValueDescriptor.Type, - AuditFieldName("origin") -> AuditFieldValueDescriptor.RemoteAddress, - AuditFieldName("destination") -> AuditFieldValueDescriptor.LocalAddress, - AuditFieldName("xff") -> AuditFieldValueDescriptor.XForwardedForHttpHeader, - AuditFieldName("task_id") -> AuditFieldValueDescriptor.TaskId, - AuditFieldName("req_method") -> AuditFieldValueDescriptor.HttpMethod, - AuditFieldName("headers") -> AuditFieldValueDescriptor.HttpHeaderNames, - AuditFieldName("path") -> AuditFieldValueDescriptor.HttpPath, - AuditFieldName("user") -> AuditFieldValueDescriptor.User, - AuditFieldName("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser, - AuditFieldName("action") -> AuditFieldValueDescriptor.Action, - AuditFieldName("indices") -> AuditFieldValueDescriptor.InvolvedIndices, - AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory, - AuditFieldName("es_node_name") -> AuditFieldValueDescriptor.EsNodeName, - AuditFieldName("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName - ) -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala index d1d239d2f3..593c148e4f 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala @@ -17,17 +17,26 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.IncludeAll -import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2.defaultV2AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.IncludeAll +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup._ import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} +/** + * Serializer for **full audit events**. + * + * - Includes `CommonFields` and `EsEnvironmentFields`. + * - Serializes all events, including every `Allowed` request, + * regardless of rule verbosity. + * + * Use this when you need complete coverage of all audit events. + */ class FullAuditLogSerializer extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - fields = defaultV2AuditFields, + fieldGroups = Set(CommonFields, EsEnvironmentFields), allowedEventMode = IncludeAll ) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala index 2de22f7081..4b7cae9030 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala @@ -17,17 +17,26 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.IncludeAll -import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditResponseContext} +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.IncludeAll +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup._ +import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} +/** + * Serializer for **full audit events including request content**. + * + * - Includes `CommonFields`, `EsEnvironmentFields`, and `FullRequestContentFields`. + * - Serializes all events, including every `Allowed` request, + * regardless of rule verbosity. + * + * Use this when request body capture is required. + */ class FullAuditLogWithQuerySerializer extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - fields = queryV2AuditFields, + fieldGroups = Set(CommonFields, EsEnvironmentFields, FullRequestContentFields), allowedEventMode = IncludeAll ) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala index 151eb580e9..6c1c2a622d 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala @@ -16,4 +16,13 @@ */ package tech.beshu.ror.audit.instances +/** + * Public alias for [[QueryAuditLogSerializerV2]]. + * + * - Captures full request content along with common and ES environment fields. + * - Respects rule-defined verbosity for `Allowed` events: + * only serializes them if the corresponding rule allows logging at `Verbosity.Info`. + * + * Prefer this class name in configurations and client code for full-content auditing. + */ class QueryAuditLogSerializer extends QueryAuditLogSerializerV2 \ No newline at end of file diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala index 23b21730c1..a67a43ab3d 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala @@ -19,24 +19,29 @@ package tech.beshu.ror.audit.instances import org.json.JSONObject import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include -import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} -import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1.queryV1AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.{CommonFields, FullRequestContentFields} +/** + * Serializer for audit events (V1) that is aware of **rule-defined verbosity** + * and includes **full request content**. + * + * - Includes `CommonFields` and `FullRequestContentFields`. + * - Serializes all non-Allowed events. + * - Serializes `Allowed` events only if the corresponding rule + * specifies that they should be logged at `Verbosity.Info`. + * + * Recommended when capturing the full request content is important, + * without cluster context. + */ class QueryAuditLogSerializerV1 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - fields = queryV1AuditFields, + fieldGroups = Set(CommonFields, FullRequestContentFields), allowedEventMode = Include(Set(Verbosity.Info)) ) } - -private[ror] object QueryAuditLogSerializerV1 { - val queryV1AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = - DefaultAuditLogSerializerV1.defaultV1AuditFields ++ - Map(AuditFieldName("content") -> AuditFieldValueDescriptor.Content) -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala index 76f2e22e48..8900c687aa 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala @@ -20,23 +20,28 @@ import org.json.JSONObject import tech.beshu.ror.audit._ import tech.beshu.ror.audit.AuditResponseContext.Verbosity import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include -import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldName, AuditFieldValueDescriptor} -import tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2.queryV2AuditFields import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup._ +/** + * Serializer for audit events (V2) that is aware of **rule-defined verbosity** + * and includes **full request content**. + * + * - Includes `CommonFields`, `EsEnvironmentFields`, and `FullRequestContentFields`. + * - Serializes all non-Allowed events. + * - Serializes `Allowed` events only if the corresponding rule + * specifies that they should be logged at `Verbosity.Info`. + * + * Recommended when capturing the full request content along with + * cluster and node context is needed. + */ class QueryAuditLogSerializerV2 extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize( responseContext = responseContext, - fields = queryV2AuditFields, + fieldGroups = Set(CommonFields, EsEnvironmentFields, FullRequestContentFields), allowedEventMode = Include(Set(Verbosity.Info)) ) } - -private[ror] object QueryAuditLogSerializerV2 { - val queryV2AuditFields: Map[AuditFieldName, AuditFieldValueDescriptor] = - DefaultAuditLogSerializerV2.defaultV2AuditFields ++ - Map(AuditFieldName("content") -> AuditFieldValueDescriptor.Content) -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala b/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala index 5c16dbc120..38d0b323b5 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/utils/AuditSerializationHelper.scala @@ -18,8 +18,7 @@ package tech.beshu.ror.audit.utils import org.json.JSONObject import tech.beshu.ror.audit.AuditResponseContext._ -import tech.beshu.ror.audit.instances.SerializeUser -import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditRequestContext, AuditResponseContext} +import tech.beshu.ror.audit.{AuditRequestContext, AuditResponseContext} import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -30,23 +29,40 @@ private[ror] object AuditSerializationHelper { private val timestampFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZone(ZoneId.of("GMT")) + def serialize(responseContext: AuditResponseContext, + fieldGroups: Set[AuditFieldGroup], + allowedEventMode: AllowedEventMode): Option[JSONObject] = { + val fields = fieldGroups.flatMap { + case AuditFieldGroup.CommonFields => commonFields + case AuditFieldGroup.EsEnvironmentFields => esEnvironmentFields + case AuditFieldGroup.FullRequestContentFields => requestContentFields + }.toMap + serialize( + responseContext = responseContext, + fields = fields, + allowedEventMode = allowedEventMode + ) + } + def serialize(responseContext: AuditResponseContext, fields: Map[AuditFieldName, AuditFieldValueDescriptor], - allowedEventMode: AllowedEventMode): Option[JSONObject] = responseContext match { - case Allowed(requestContext, verbosity, reason) => - allowedEvent( - allowedEventMode, - verbosity, - createEntry(fields, EventData(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) - ) - case ForbiddenBy(requestContext, _, reason) => - Some(createEntry(fields, EventData(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None))) - case Forbidden(requestContext) => - Some(createEntry(fields, EventData(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None))) - case RequestedIndexNotExist(requestContext) => - Some(createEntry(fields, EventData(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None))) - case Errored(requestContext, cause) => - Some(createEntry(fields, EventData(matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause)))) + allowedEventMode: AllowedEventMode): Option[JSONObject] = { + responseContext match { + case Allowed(requestContext, verbosity, reason) => + allowedEvent( + allowedEventMode, + verbosity, + createEntry(fields, EventData(matched = true, "ALLOWED", reason, responseContext.duration, requestContext, None)) + ) + case ForbiddenBy(requestContext, _, reason) => + Some(createEntry(fields, EventData(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None))) + case Forbidden(requestContext) => + Some(createEntry(fields, EventData(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None))) + case RequestedIndexNotExist(requestContext) => + Some(createEntry(fields, EventData(matched = false, "INDEX NOT EXIST", "Requested index doesn't exist", responseContext.duration, requestContext, None))) + case Errored(requestContext, cause) => + Some(createEntry(fields, EventData(matched = false, "ERRORED", "error", responseContext.duration, requestContext, Some(cause)))) + } } private def allowedEvent(allowedEventMode: AllowedEventMode, verbosity: Verbosity, entry: JSONObject) = { @@ -211,4 +227,50 @@ private[ror] object AuditSerializationHelper { } + sealed trait AuditFieldGroup + + object AuditFieldGroup { + case object CommonFields extends AuditFieldGroup + + case object EsEnvironmentFields extends AuditFieldGroup + + case object FullRequestContentFields extends AuditFieldGroup + } + + private val commonFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( + AuditFieldName("match") -> AuditFieldValueDescriptor.IsMatched, + AuditFieldName("block") -> AuditFieldValueDescriptor.Reason, + AuditFieldName("id") -> AuditFieldValueDescriptor.Id, + AuditFieldName("final_state") -> AuditFieldValueDescriptor.FinalState, + AuditFieldName("@timestamp") -> AuditFieldValueDescriptor.Timestamp, + AuditFieldName("correlation_id") -> AuditFieldValueDescriptor.CorrelationId, + AuditFieldName("processingMillis") -> AuditFieldValueDescriptor.ProcessingDurationMillis, + AuditFieldName("error_type") -> AuditFieldValueDescriptor.ErrorType, + AuditFieldName("error_message") -> AuditFieldValueDescriptor.ErrorMessage, + AuditFieldName("content_len") -> AuditFieldValueDescriptor.ContentLengthInBytes, + AuditFieldName("content_len_kb") -> AuditFieldValueDescriptor.ContentLengthInKb, + AuditFieldName("type") -> AuditFieldValueDescriptor.Type, + AuditFieldName("origin") -> AuditFieldValueDescriptor.RemoteAddress, + AuditFieldName("destination") -> AuditFieldValueDescriptor.LocalAddress, + AuditFieldName("xff") -> AuditFieldValueDescriptor.XForwardedForHttpHeader, + AuditFieldName("task_id") -> AuditFieldValueDescriptor.TaskId, + AuditFieldName("req_method") -> AuditFieldValueDescriptor.HttpMethod, + AuditFieldName("headers") -> AuditFieldValueDescriptor.HttpHeaderNames, + AuditFieldName("path") -> AuditFieldValueDescriptor.HttpPath, + AuditFieldName("user") -> AuditFieldValueDescriptor.User, + AuditFieldName("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser, + AuditFieldName("action") -> AuditFieldValueDescriptor.Action, + AuditFieldName("indices") -> AuditFieldValueDescriptor.InvolvedIndices, + AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory + ) + + private val esEnvironmentFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( + AuditFieldName("es_node_name") -> AuditFieldValueDescriptor.EsNodeName, + AuditFieldName("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName + ) + + private val requestContentFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( + AuditFieldName("content") -> AuditFieldValueDescriptor.Content + ) + } diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/SerializeUser.scala b/audit/src/main/scala/tech/beshu/ror/audit/utils/SerializeUser.scala similarity index 93% rename from audit/src/main/scala/tech/beshu/ror/audit/instances/SerializeUser.scala rename to audit/src/main/scala/tech/beshu/ror/audit/utils/SerializeUser.scala index 0e4330df41..a580dc74d8 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/SerializeUser.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/utils/SerializeUser.scala @@ -14,11 +14,11 @@ * You should have received a copy of the GNU General Public License * along with ReadonlyREST. If not, see http://www.gnu.org/licenses/ */ -package tech.beshu.ror.audit.instances +package tech.beshu.ror.audit.utils import tech.beshu.ror.audit.AuditRequestContext -object SerializeUser { +private[ror] object SerializeUser { def serialize(requestContext: AuditRequestContext): Option[String] = { requestContext.loggedInUserName.orElse(requestContext.attemptedUserName).orElse(requestContext.rawAuthHeader) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index e936bbc4e4..008847f442 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -18,7 +18,7 @@ package tech.beshu.ror.accesscontrol.factory.decoders import cats.data.NonEmptyList import io.circe.Decoder.* -import io.circe.{Decoder, DecodingFailure, HCursor, Json, KeyDecoder} +import io.circe.{Decoder, DecodingFailure, Json, HCursor, KeyDecoder} import io.lemonlabs.uri.Uri import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.accesscontrol.audit.AuditingTool @@ -207,24 +207,21 @@ object AuditingSettingsDecoder extends Logging { } .decoder - given auditLogSerializerDecoder: Decoder[Option[AuditLogSerializer]] = SyncDecoderCreator.from( - Decoder.instance[Option[AuditLogSerializer]] { c => - for { - serializerType <- c.as[SerializerType] - serializer <- serializerType match { - case SerializerType.StaticSerializerInOutputSection => - c.as[Option[AuditLogSerializer]](classNameBasedSerializerDecoder) - case SerializerType.StaticSerializerInSerializerSection => - c.downField("serializer").as[Option[AuditLogSerializer]](classNameBasedSerializerDecoder) - case SerializerType.ConfigurableSerializer => - c.downField("serializer").as[Option[AuditLogSerializer]](configurableSerializerDecoder) - } - } yield serializer + given auditLogSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + for { + serializerTypeStr <- c.downField("serializer").downField("type").as[SerializerType] + result <- serializerTypeStr match { + case SerializerType.SimpleSyntaxStaticSerializer => + c.as[Option[AuditLogSerializer]](simpleSyntaxSerializerDecoder) + case SerializerType.ExtendedSyntaxStaticSerializer => + c.downField("serializer").as[Option[AuditLogSerializer]](extendedSyntaxStaticSerializerDecoder) + case SerializerType.ExtendedSyntaxConfigurableSerializer => + c.downField("serializer").as[Option[AuditLogSerializer]](extendedSyntaxConfigurableSerializerDecoder) } - ) - .decoder + } yield result + } - private def configurableSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + private def extendedSyntaxConfigurableSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => for { allowedEventMode <- c.downField("verbosity_level_serialization_mode").as[AllowedEventMode] .left.map(withAuditingSettingsCreationErrorMessage(msg => s"Configurable serializer is used, but the 'verbosity_level_serialization_mode' setting is invalid: $msg")) @@ -234,21 +231,60 @@ object AuditingSettingsDecoder extends Logging { } yield Some(serializer) } - private def classNameBasedSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + private def extendedSyntaxStaticSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + for { + fullClassNameOpt <- c.downField("class_name").as[Option[String]] + serializerOpt <- fullClassNameOpt match { + case Some(fullClassName) => serializerByClassName(fullClassName) + case None => Right(None) + } + } yield serializerOpt + } + + private def simpleSyntaxSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => for { - classNameOpt <- c.downField("class_name").as[Option[String]] fullClassNameOpt <- c.downField("serializer").as[Option[String]] legacyFullClassNameOpt <- c.downField("audit_serializer").as[Option[String]] - serializerOpt <- classNameOpt.orElse(fullClassNameOpt).orElse(legacyFullClassNameOpt) match { - case Some(fullClassName) => - createSerializerInstanceFromClassName(fullClassName).map(Some(_)) - .left.map(error => DecodingFailure(AclCreationErrorCoders.stringify(error), Nil)) - case None => - Right(None) + serializerOpt <- fullClassNameOpt.orElse(legacyFullClassNameOpt) match { + case Some(fullClassName) => serializerByClassName(fullClassName) + case None => Right(None) } } yield serializerOpt } + private def serializerByClassName(className: String): Either[DecodingFailure, Some[AuditLogSerializer]] = { + createSerializerInstanceFromClassName(className).map(Some(_)) + .left.map(error => DecodingFailure(AclCreationErrorCoders.stringify(error), Nil)) + } + + private given serializerTypeDecoder: Decoder[SerializerType] = Decoder.instance { c => + c.downField("serializer").as[Option[Json]].flatMap { + case Some(json) if json.isObject => + json.hcursor.downField("type").as[String].map(_.toLowerCase).flatMap { + case "static" => + Right(SerializerType.ExtendedSyntaxStaticSerializer) + case "configurable" => + Right(SerializerType.ExtendedSyntaxConfigurableSerializer) + case other => + Left(DecodingFailure(AclCreationErrorCoders.stringify( + AuditingSettingsCreationError(Message(s"Invalid serializer type '$other', allowed values [static, configurable]")) + ), Nil)) + } + case Some(_) | None => + Right(SerializerType.SimpleSyntaxStaticSerializer) + } + } + + private sealed trait SerializerType + + private object SerializerType { + case object SimpleSyntaxStaticSerializer extends SerializerType + + case object ExtendedSyntaxStaticSerializer extends SerializerType + + case object ExtendedSyntaxConfigurableSerializer extends SerializerType + } + private def withAuditingSettingsCreationErrorMessage(message: String => String)(decodingFailure: DecodingFailure) = { decodingFailure.withMessage(AclCreationErrorCoders.stringify(AuditingSettingsCreationError(Message(message(decodingFailure.message))))) } @@ -420,29 +456,4 @@ object AuditingSettingsDecoder extends Logging { } } - private given serializerTypeDecoder: Decoder[SerializerType] = Decoder.instance { c => - c.downField("serializer").as[Option[Json]].flatMap { - case Some(json) if json.isObject => - json.hcursor.downField("type").as[String].map(_.toLowerCase).flatMap { - case "static" => - Right(SerializerType.StaticSerializerInSerializerSection) - case "configurable" => - Right(SerializerType.ConfigurableSerializer) - case other => - Left(DecodingFailure(AclCreationErrorCoders.stringify( - AuditingSettingsCreationError(Message(s"Invalid serializer type '$other', allowed values [static, configurable]")) - ), Nil)) - } - case Some(_) | None => - Right(SerializerType.StaticSerializerInOutputSection) - } - } - - private sealed trait SerializerType - - private object SerializerType { - case object StaticSerializerInOutputSection extends SerializerType - case object StaticSerializerInSerializerSection extends SerializerType - case object ConfigurableSerializer extends SerializerType - } } From 627e005a8cdedfdd28203280fef40744efab9b2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Thu, 4 Sep 2025 22:11:55 +0200 Subject: [PATCH 22/27] review changes --- .../factory/decoders/AuditingSettingsDecoder.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala index 008847f442..15bd689760 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/factory/decoders/AuditingSettingsDecoder.scala @@ -209,7 +209,7 @@ object AuditingSettingsDecoder extends Logging { given auditLogSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => for { - serializerTypeStr <- c.downField("serializer").downField("type").as[SerializerType] + serializerTypeStr <- c.as[SerializerType] result <- serializerTypeStr match { case SerializerType.SimpleSyntaxStaticSerializer => c.as[Option[AuditLogSerializer]](simpleSyntaxSerializerDecoder) From 182f091138279b1a2d9bff7c7e9f7dedaf25cd78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sat, 13 Sep 2025 16:40:11 +0200 Subject: [PATCH 23/27] review changes --- ...lockVerbosityAwareAuditLogSerializer.scala | 86 ++++++++++ .../instances/DefaultAuditLogSerializer.scala | 37 ----- .../DefaultAuditLogSerializerV1.scala | 51 ------ .../DefaultAuditLogSerializerV2.scala | 52 ------ .../instances/FullAuditLogSerializer.scala | 34 +++- .../FullAuditLogWithQuerySerializer.scala | 35 +++- .../instances/QueryAuditLogSerializer.scala | 135 +++++++++++++++- .../instances/QueryAuditLogSerializerV1.scala | 47 ------ .../instances/QueryAuditLogSerializerV2.scala | 47 ------ .../accesscontrol/audit/AuditingTool.scala | 8 +- .../integration/AuditOutputFormatTests.scala | 6 +- .../unit/acl/factory/AuditSettingsTests.scala | 52 +++--- .../unit/acl/logging/AuditingToolTests.scala | 11 +- .../LocalClusterAuditingToolsSuite.scala | 149 ++++++++++++++++++ 14 files changed, 465 insertions(+), 285 deletions(-) create mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/BlockVerbosityAwareAuditLogSerializer.scala delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala delete mode 100644 audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BlockVerbosityAwareAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BlockVerbosityAwareAuditLogSerializer.scala new file mode 100644 index 0000000000..8ae6259a9d --- /dev/null +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/BlockVerbosityAwareAuditLogSerializer.scala @@ -0,0 +1,86 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit.instances + +import org.json.JSONObject +import tech.beshu.ror.audit.AuditResponseContext.Verbosity +import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.{CommonFields, EsEnvironmentFields} +import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} + +/** + * Serializer for **full audit events including request content**. + * - Serializes all events, including every `Allowed` request, + * regardless of rule verbosity. + * - Use this when request body capture is required. + * - Fields included: + * - `match` — whether the request matched a rule (boolean) + * - `block` — reason for blocking, if blocked (string) + * - `id` — audit event identifier (string) + * - `final_state` — final processing state (string) + * - `@timestamp` — event timestamp (ISO-8601 string) + * - `correlation_id` — correlation identifier for tracing (string) + * - `processingMillis` — request processing duration in milliseconds (number) + * - `error_type` — type of error, if any (string) + * - `error_message` — error message, if any (string) + * - `content_len` — request body size in bytes (number) + * - `content_len_kb` — request body size in kilobytes (number) + * - `type` — request type (string) + * - `origin` — client (remote) address (string) + * - `destination` — server (local) address (string) + * - `xff` — `X-Forwarded-For` HTTP header value (string) + * - `task_id` — Elasticsearch task ID (number) + * - `req_method` — HTTP request method (string) + * - `headers` — HTTP header names (array of strings) + * - `path` — HTTP request path (string) + * - `user` — authenticated user (string) + * - `impersonated_by` — impersonating user, if applicable (string) + * - `action` — Elasticsearch action name (string) + * - `indices` — indices involved in the request (array of strings) + * - `acl_history` — access control evaluation history (string) + * - `es_node_name` — Elasticsearch node name (string) + * - `es_cluster_name` — Elasticsearch cluster name (string) + */ +class BlockVerbosityAwareAuditLogSerializer extends DefaultAuditLogSerializer + +@deprecated("Use tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer instead", "1.67.0") +class DefaultAuditLogSerializer extends DefaultAuditLogSerializerV2 + +@deprecated("Use tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer instead", "1.67.0") +class DefaultAuditLogSerializerV2 extends AuditLogSerializer { + + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + AuditSerializationHelper.serialize( + responseContext = responseContext, + fieldGroups = Set(CommonFields, EsEnvironmentFields), + allowedEventMode = Include(Set(Verbosity.Info)) + ) + +} + +@deprecated("Use tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer instead", "1.67.0") +class DefaultAuditLogSerializerV1 extends AuditLogSerializer { + + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + AuditSerializationHelper.serialize( + responseContext = responseContext, + fieldGroups = Set(CommonFields), + allowedEventMode = Include(Set(Verbosity.Info)) + ) + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala deleted file mode 100644 index 0922a8b997..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializer.scala +++ /dev/null @@ -1,37 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit.instances - -/** - * Serializer for audit events that is aware of **rule-defined verbosity**. - * - * - Includes `CommonFields` and `EsEnvironmentFields`. - * - Serializes all non-Allowed events. - * - Serializes `Allowed` events only if the corresponding rule - * specifies that they should be logged at `Verbosity.Info`. - * - * This is the recommended serializer for standard audit logging. - */ -class BlockVerbosityAwareAuditLogSerializer extends DefaultAuditLogSerializer - -/** - * Base implementation delegating to [[DefaultAuditLogSerializerV2]]. - * - * - Not intended for direct external use. - * - Provides the underlying logic for [[BlockVerbosityAwareAuditLogSerializer]]. - */ -class DefaultAuditLogSerializer extends DefaultAuditLogSerializerV2 diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala deleted file mode 100644 index 8719eb89cd..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV1.scala +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit.instances - -import org.json.JSONObject -import tech.beshu.ror.audit._ -import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.utils.AuditSerializationHelper -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.CommonFields - -/** - * Serializer for audit events (V1) that is aware of **rule-defined verbosity**. - * - * - Includes only `CommonFields` (request metadata, user, action, etc.). - * - Serializes all non-Allowed events. - * - Serializes `Allowed` events only if the corresponding rule - * specifies that they should be logged at `Verbosity.Info`. - * - * Recommended when a minimal, compact audit log is sufficient. - */ -class BlockVerbosityAwareAuditLogSerializerV1 extends DefaultAuditLogSerializerV1 - -/** - * Base implementation for [[BlockVerbosityAwareAuditLogSerializerV1]]. - * Not intended for direct external use. - */ -class DefaultAuditLogSerializerV1 extends AuditLogSerializer { - - override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - AuditSerializationHelper.serialize( - responseContext = responseContext, - fieldGroups = Set(CommonFields), - allowedEventMode = Include(Set(Verbosity.Info)) - ) - -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala deleted file mode 100644 index 3f10e8d276..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/DefaultAuditLogSerializerV2.scala +++ /dev/null @@ -1,52 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit.instances - -import org.json.JSONObject -import tech.beshu.ror.audit._ -import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include -import tech.beshu.ror.audit.utils.AuditSerializationHelper -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.{CommonFields, EsEnvironmentFields} - -/** - * Serializer for audit events (V2) that is aware of **rule-defined verbosity**. - * - * - Includes `CommonFields` plus `EsEnvironmentFields` - * (cluster and node identifiers). - * - Serializes all non-Allowed events. - * - Serializes `Allowed` events only if the corresponding rule - * specifies that they should be logged at `Verbosity.Info`. - * - * Recommended when node and cluster context are also required. - */ -class BlockVerbosityAwareAuditLogSerializerV2 extends DefaultAuditLogSerializerV2 - -/** - * Base implementation for [[BlockVerbosityAwareAuditLogSerializerV2]]. - * Not intended for direct external use. - */ -class DefaultAuditLogSerializerV2 extends AuditLogSerializer { - - override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - AuditSerializationHelper.serialize( - responseContext = responseContext, - fieldGroups = Set(CommonFields, EsEnvironmentFields), - allowedEventMode = Include(Set(Verbosity.Info)) - ) - -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala index 593c148e4f..c315344c5e 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogSerializer.scala @@ -24,12 +24,36 @@ import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} /** * Serializer for **full audit events**. - * - * - Includes `CommonFields` and `EsEnvironmentFields`. * - Serializes all events, including every `Allowed` request, - * regardless of rule verbosity. - * - * Use this when you need complete coverage of all audit events. + regardless of rule verbosity. + * - Use this serializer, when you need complete coverage of all events. + * - Fields included: + * - `match` — whether the request matched a rule (boolean) + * - `block` — reason for blocking, if blocked (string) + * - `id` — audit event identifier (string) + * - `final_state` — final processing state (string) + * - `@timestamp` — event timestamp (ISO-8601 string) + * - `correlation_id` — correlation identifier for tracing (string) + * - `processingMillis` — request processing duration in milliseconds (number) + * - `error_type` — type of error, if any (string) + * - `error_message` — error message, if any (string) + * - `content_len` — request body size in bytes (number) + * - `content_len_kb` — request body size in kilobytes (number) + * - `type` — request type (string) + * - `origin` — client (remote) address (string) + * - `destination` — server (local) address (string) + * - `xff` — `X-Forwarded-For` HTTP header value (string) + * - `task_id` — Elasticsearch task ID (number) + * - `req_method` — HTTP request method (string) + * - `headers` — HTTP header names (array of strings) + * - `path` — HTTP request path (string) + * - `user` — authenticated user (string) + * - `impersonated_by` — impersonating user, if applicable (string) + * - `action` — Elasticsearch action name (string) + * - `indices` — indices involved in the request (array of strings) + * - `acl_history` — access control evaluation history (string) + * - `es_node_name` — Elasticsearch node name (string) + * - `es_cluster_name` — Elasticsearch cluster name (string) */ class FullAuditLogSerializer extends AuditLogSerializer { diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala index 4b7cae9030..9b59ee9610 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/FullAuditLogWithQuerySerializer.scala @@ -24,12 +24,37 @@ import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} /** * Serializer for **full audit events including request content**. - * - * - Includes `CommonFields`, `EsEnvironmentFields`, and `FullRequestContentFields`. * - Serializes all events, including every `Allowed` request, - * regardless of rule verbosity. - * - * Use this when request body capture is required. + * regardless of rule verbosity. + * - Use this serializer, when request body capture is required. + * - Fields included: + * - `match` — whether the request matched a rule (boolean) + * - `block` — reason for blocking, if blocked (string) + * - `id` — audit event identifier (string) + * - `final_state` — final processing state (string) + * - `@timestamp` — event timestamp (ISO-8601 string) + * - `correlation_id` — correlation identifier for tracing (string) + * - `processingMillis` — request processing duration in milliseconds (number) + * - `error_type` — type of error, if any (string) + * - `error_message` — error message, if any (string) + * - `content_len` — request body size in bytes (number) + * - `content_len_kb` — request body size in kilobytes (number) + * - `type` — request type (string) + * - `origin` — client (remote) address (string) + * - `destination` — server (local) address (string) + * - `xff` — `X-Forwarded-For` HTTP header value (string) + * - `task_id` — Elasticsearch task ID (number) + * - `req_method` — HTTP request method (string) + * - `headers` — HTTP header names (array of strings) + * - `path` — HTTP request path (string) + * - `user` — authenticated user (string) + * - `impersonated_by` — impersonating user, if applicable (string) + * - `action` — Elasticsearch action name (string) + * - `indices` — indices involved in the request (array of strings) + * - `acl_history` — access control evaluation history (string) + * - `es_node_name` — Elasticsearch node name (string) + * - `es_cluster_name` — Elasticsearch cluster name (string) + * - `content` — full request body (string) */ class FullAuditLogWithQuerySerializer extends AuditLogSerializer { diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala index 6c1c2a622d..8c23394fa8 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializer.scala @@ -16,13 +16,140 @@ */ package tech.beshu.ror.audit.instances +import org.json.JSONObject +import tech.beshu.ror.audit.AuditResponseContext.Verbosity +import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include +import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.{CommonFields, EsEnvironmentFields, FullRequestContentFields} +import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} + /** * Public alias for [[QueryAuditLogSerializerV2]]. - * * - Captures full request content along with common and ES environment fields. * - Respects rule-defined verbosity for `Allowed` events: * only serializes them if the corresponding rule allows logging at `Verbosity.Info`. - * - * Prefer this class name in configurations and client code for full-content auditing. + * - Prefer this class name in configurations and client code for full-content auditing. + * - Fields included: + * - `match` — whether the request matched a rule (boolean) + * - `block` — reason for blocking, if blocked (string) + * - `id` — audit event identifier (string) + * - `final_state` — final processing state (string) + * - `@timestamp` — event timestamp (ISO-8601 string) + * - `correlation_id` — correlation identifier for tracing (string) + * - `processingMillis` — request processing duration in milliseconds (number) + * - `error_type` — type of error, if any (string) + * - `error_message` — error message, if any (string) + * - `content_len` — request body size in bytes (number) + * - `content_len_kb` — request body size in kilobytes (number) + * - `type` — request type (string) + * - `origin` — client (remote) address (string) + * - `destination` — server (local) address (string) + * - `xff` — `X-Forwarded-For` HTTP header value (string) + * - `task_id` — Elasticsearch task ID (number) + * - `req_method` — HTTP request method (string) + * - `headers` — HTTP header names (array of strings) + * - `path` — HTTP request path (string) + * - `user` — authenticated user (string) + * - `impersonated_by` — impersonating user, if applicable (string) + * - `action` — Elasticsearch action name (string) + * - `indices` — indices involved in the request (array of strings) + * - `acl_history` — access control evaluation history (string) + * - `es_node_name` — Elasticsearch node name (string) + * - `es_cluster_name` — Elasticsearch cluster name (string) + * - `content` — full request body (string) */ -class QueryAuditLogSerializer extends QueryAuditLogSerializerV2 \ No newline at end of file +class QueryAuditLogSerializer extends QueryAuditLogSerializerV2 + +/** + * Serializer for audit events (V2) that is aware of **rule-defined verbosity** + * and includes **full request content**. + * - Serializes all non-Allowed events. + * - Serializes `Allowed` events only if the corresponding rule + * specifies that they should be logged at `Verbosity.Info`. + * - Recommended when capturing the full request content along with + * cluster and node context is needed. + * - Fields included: + * - `match` — whether the request matched a rule (boolean) + * - `block` — reason for blocking, if blocked (string) + * - `id` — audit event identifier (string) + * - `final_state` — final processing state (string) + * - `@timestamp` — event timestamp (ISO-8601 string) + * - `correlation_id` — correlation identifier for tracing (string) + * - `processingMillis` — request processing duration in milliseconds (number) + * - `error_type` — type of error, if any (string) + * - `error_message` — error message, if any (string) + * - `content_len` — request body size in bytes (number) + * - `content_len_kb` — request body size in kilobytes (number) + * - `type` — request type (string) + * - `origin` — client (remote) address (string) + * - `destination` — server (local) address (string) + * - `xff` — `X-Forwarded-For` HTTP header value (string) + * - `task_id` — Elasticsearch task ID (number) + * - `req_method` — HTTP request method (string) + * - `headers` — HTTP header names (array of strings) + * - `path` — HTTP request path (string) + * - `user` — authenticated user (string) + * - `impersonated_by` — impersonating user, if applicable (string) + * - `action` — Elasticsearch action name (string) + * - `indices` — indices involved in the request (array of strings) + * - `acl_history` — access control evaluation history (string) + * - `es_node_name` — Elasticsearch node name (string) + * - `es_cluster_name` — Elasticsearch cluster name (string) + * - `content` — full request body (string) + */ +class QueryAuditLogSerializerV2 extends AuditLogSerializer { + + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + AuditSerializationHelper.serialize( + responseContext = responseContext, + fieldGroups = Set(CommonFields, EsEnvironmentFields, FullRequestContentFields), + allowedEventMode = Include(Set(Verbosity.Info)) + ) + +} + +/** + * Serializer for audit events (V1) that is aware of **rule-defined verbosity** + * and includes **full request content**. + * - Serializes all non-Allowed events. + * - Serializes `Allowed` events only if the corresponding rule + * specifies that they should be logged at `Verbosity.Info`. + * - Recommended when capturing the full request content is important, + * without cluster context. + * - Fields included: + * - `match` — whether the request matched a rule (boolean) + * - `block` — reason for blocking, if blocked (string) + * - `id` — audit event identifier (string) + * - `final_state` — final processing state (string) + * - `@timestamp` — event timestamp (ISO-8601 string) + * - `correlation_id` — correlation identifier for tracing (string) + * - `processingMillis` — request processing duration in milliseconds (number) + * - `error_type` — type of error, if any (string) + * - `error_message` — error message, if any (string) + * - `content_len` — request body size in bytes (number) + * - `content_len_kb` — request body size in kilobytes (number) + * - `type` — request type (string) + * - `origin` — client (remote) address (string) + * - `destination` — server (local) address (string) + * - `xff` — `X-Forwarded-For` HTTP header value (string) + * - `task_id` — Elasticsearch task ID (number) + * - `req_method` — HTTP request method (string) + * - `headers` — HTTP header names (array of strings) + * - `path` — HTTP request path (string) + * - `user` — authenticated user (string) + * - `impersonated_by` — impersonating user, if applicable (string) + * - `action` — Elasticsearch action name (string) + * - `indices` — indices involved in the request (array of strings) + * - `acl_history` — access control evaluation history (string) + * - `content` — full request body (string) + */ +class QueryAuditLogSerializerV1 extends AuditLogSerializer { + + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = + AuditSerializationHelper.serialize( + responseContext = responseContext, + fieldGroups = Set(CommonFields, FullRequestContentFields), + allowedEventMode = Include(Set(Verbosity.Info)) + ) + +} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala deleted file mode 100644 index a67a43ab3d..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV1.scala +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit.instances - -import org.json.JSONObject -import tech.beshu.ror.audit._ -import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.utils.AuditSerializationHelper -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.{CommonFields, FullRequestContentFields} - -/** - * Serializer for audit events (V1) that is aware of **rule-defined verbosity** - * and includes **full request content**. - * - * - Includes `CommonFields` and `FullRequestContentFields`. - * - Serializes all non-Allowed events. - * - Serializes `Allowed` events only if the corresponding rule - * specifies that they should be logged at `Verbosity.Info`. - * - * Recommended when capturing the full request content is important, - * without cluster context. - */ -class QueryAuditLogSerializerV1 extends AuditLogSerializer { - - override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - AuditSerializationHelper.serialize( - responseContext = responseContext, - fieldGroups = Set(CommonFields, FullRequestContentFields), - allowedEventMode = Include(Set(Verbosity.Info)) - ) - -} diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala deleted file mode 100644 index 8900c687aa..0000000000 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/QueryAuditLogSerializerV2.scala +++ /dev/null @@ -1,47 +0,0 @@ -/* - * This file is part of ReadonlyREST. - * - * ReadonlyREST 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. - * - * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ - */ -package tech.beshu.ror.audit.instances - -import org.json.JSONObject -import tech.beshu.ror.audit._ -import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AllowedEventMode.Include -import tech.beshu.ror.audit.utils.AuditSerializationHelper -import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup._ - -/** - * Serializer for audit events (V2) that is aware of **rule-defined verbosity** - * and includes **full request content**. - * - * - Includes `CommonFields`, `EsEnvironmentFields`, and `FullRequestContentFields`. - * - Serializes all non-Allowed events. - * - Serializes `Allowed` events only if the corresponding rule - * specifies that they should be logged at `Verbosity.Info`. - * - * Recommended when capturing the full request content along with - * cluster and node context is needed. - */ -class QueryAuditLogSerializerV2 extends AuditLogSerializer { - - override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = - AuditSerializationHelper.serialize( - responseContext = responseContext, - fieldGroups = Set(CommonFields, EsEnvironmentFields, FullRequestContentFields), - allowedEventMode = Include(Set(Verbosity.Info)) - ) - -} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditingTool.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditingTool.scala index 1121ef86eb..0ba7e544d1 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditingTool.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditingTool.scala @@ -31,7 +31,7 @@ import tech.beshu.ror.accesscontrol.blocks.{Block, BlockContext} import tech.beshu.ror.accesscontrol.domain.{AuditCluster, RorAuditDataStream, RorAuditIndexTemplate, RorAuditLoggerName} import tech.beshu.ror.accesscontrol.logging.ResponseContext import tech.beshu.ror.accesscontrol.request.RequestContext -import tech.beshu.ror.audit.instances.DefaultAuditLogSerializer +import tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer import tech.beshu.ror.audit.{AuditEnvironmentContext, AuditLogSerializer, AuditRequestContext, AuditResponseContext} import tech.beshu.ror.implicits.* @@ -171,7 +171,7 @@ object AuditingTool extends Logging { object EsIndexBasedSink { val default: EsIndexBasedSink = EsIndexBasedSink( - logSerializer = new DefaultAuditLogSerializer, + logSerializer = new BlockVerbosityAwareAuditLogSerializer, rorAuditIndexTemplate = RorAuditIndexTemplate.default, auditCluster = AuditCluster.LocalAuditCluster, ) @@ -183,7 +183,7 @@ object AuditingTool extends Logging { object EsDataStreamBasedSink { val default: EsDataStreamBasedSink = EsDataStreamBasedSink( - logSerializer = new DefaultAuditLogSerializer, + logSerializer = new BlockVerbosityAwareAuditLogSerializer, rorAuditDataStream = RorAuditDataStream.default, auditCluster = AuditCluster.LocalAuditCluster, ) @@ -194,7 +194,7 @@ object AuditingTool extends Logging { object LogBasedSink { val default: LogBasedSink = LogBasedSink( - logSerializer = new DefaultAuditLogSerializer, + logSerializer = new BlockVerbosityAwareAuditLogSerializer, loggerName = RorAuditLoggerName.default ) } diff --git a/core/src/test/scala/tech/beshu/ror/integration/AuditOutputFormatTests.scala b/core/src/test/scala/tech/beshu/ror/integration/AuditOutputFormatTests.scala index 7237d56cd9..58fec29d31 100644 --- a/core/src/test/scala/tech/beshu/ror/integration/AuditOutputFormatTests.scala +++ b/core/src/test/scala/tech/beshu/ror/integration/AuditOutputFormatTests.scala @@ -30,7 +30,7 @@ import tech.beshu.ror.accesscontrol.audit.{AuditingTool, LoggingContext} import tech.beshu.ror.accesscontrol.domain.* import tech.beshu.ror.accesscontrol.logging.AccessControlListLoggingDecorator import tech.beshu.ror.audit.AuditEnvironmentContext -import tech.beshu.ror.audit.instances.DefaultAuditLogSerializer +import tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer import tech.beshu.ror.es.{DataStreamBasedAuditSinkService, DataStreamService, IndexBasedAuditSinkService} import tech.beshu.ror.mocks.MockRequestContext import tech.beshu.ror.syntax.* @@ -162,12 +162,12 @@ class AuditOutputFormatTests extends AnyWordSpec with BaseYamlLoadedAccessContro val settings = AuditingTool.AuditSettings( NonEmptyList.of( AuditSink.Enabled(Config.EsIndexBasedSink( - new DefaultAuditLogSerializer, + new BlockVerbosityAwareAuditLogSerializer, RorAuditIndexTemplate.default, AuditCluster.LocalAuditCluster )), AuditSink.Enabled(Config.EsDataStreamBasedSink( - new DefaultAuditLogSerializer, + new BlockVerbosityAwareAuditLogSerializer, RorAuditDataStream.default, AuditCluster.LocalAuditCluster )) diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index 7c9e01d552..56b5691b6a 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -37,7 +37,7 @@ import tech.beshu.ror.audit.* import tech.beshu.ror.audit.AuditResponseContext.Verbosity import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.adapters.{DeprecatedAuditLogSerializerAdapter, EnvironmentAwareAuditLogSerializerAdapter} -import tech.beshu.ror.audit.instances.{DefaultAuditLogSerializer, QueryAuditLogSerializer} +import tech.beshu.ror.audit.instances.{BlockVerbosityAwareAuditLogSerializer, QueryAuditLogSerializer} import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.mocks.{MockHttpClientsFactory, MockLdapConnectionPoolProvider} @@ -128,7 +128,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = LocalAuditCluster @@ -149,7 +149,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = LocalAuditCluster @@ -191,7 +191,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { enabledSink1 shouldBe a[Config.EsIndexBasedSink] val sink1Config = enabledSink1.asInstanceOf[Config.EsIndexBasedSink] sink1Config.rorAuditIndexTemplate.indexName(zonedDateTime.toInstant) should be(indexName("readonlyrest_audit-2018-12-31")) - sink1Config.logSerializer shouldBe a[DefaultAuditLogSerializer] + sink1Config.logSerializer shouldBe a[BlockVerbosityAwareAuditLogSerializer] sink1Config.auditCluster shouldBe AuditCluster.LocalAuditCluster val sink2 = auditingSettings.auditSinks.toList(1) @@ -200,7 +200,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { enabledSink2 shouldBe a[Config.LogBasedSink] val sink2Config = enabledSink2.asInstanceOf[Config.LogBasedSink] sink2Config.loggerName should be(RorAuditLoggerName("readonlyrest_audit")) - sink2Config.logSerializer shouldBe a[DefaultAuditLogSerializer] + sink2Config.logSerializer shouldBe a[BlockVerbosityAwareAuditLogSerializer] val sink3 = auditingSettings.auditSinks.toList(2) sink3 shouldBe a[AuditSink.Enabled] @@ -208,7 +208,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { enabledSink3 shouldBe a[Config.EsDataStreamBasedSink] val sink3Config = enabledSink3.asInstanceOf[Config.EsDataStreamBasedSink] sink3Config.rorAuditDataStream.dataStream should be(fullDataStreamName("readonlyrest_audit")) - sink3Config.logSerializer shouldBe a[DefaultAuditLogSerializer] + sink3Config.logSerializer shouldBe a[BlockVerbosityAwareAuditLogSerializer] sink3Config.auditCluster shouldBe AuditCluster.LocalAuditCluster } } @@ -230,7 +230,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertLogBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertLogBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedLoggerName = "readonlyrest_audit" ) @@ -254,7 +254,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertLogBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertLogBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedLoggerName = "readonlyrest_audit" ) @@ -301,7 +301,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertLogBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertLogBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedLoggerName = "custom_logger" ) @@ -435,7 +435,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = LocalAuditCluster @@ -460,7 +460,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = LocalAuditCluster @@ -508,7 +508,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "custom_template_20181231", expectedAuditCluster = LocalAuditCluster @@ -610,7 +610,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = RemoteAuditCluster(NonEmptyList.one(Uri.parse("1.1.1.1"))) @@ -661,7 +661,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertDataStreamAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertDataStreamAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedDataStreamName = "readonlyrest_audit", expectedAuditCluster = LocalAuditCluster @@ -686,7 +686,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertDataStreamAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertDataStreamAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedDataStreamName = "readonlyrest_audit", expectedAuditCluster = LocalAuditCluster @@ -734,7 +734,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertDataStreamAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertDataStreamAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedDataStreamName = "custom_audit_data_stream", expectedAuditCluster = LocalAuditCluster @@ -806,7 +806,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertDataStreamAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertDataStreamAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedDataStreamName = "readonlyrest_audit", expectedAuditCluster = RemoteAuditCluster(NonEmptyList.one(Uri.parse("1.1.1.1"))) @@ -918,7 +918,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { enabledSink1 shouldBe a[Config.EsIndexBasedSink] val sink1Config = enabledSink1.asInstanceOf[Config.EsIndexBasedSink] sink1Config.rorAuditIndexTemplate.indexName(zonedDateTime.toInstant) should be(indexName("readonlyrest_audit-2018-12-31")) - sink1Config.logSerializer shouldBe a[DefaultAuditLogSerializer] + sink1Config.logSerializer shouldBe a[BlockVerbosityAwareAuditLogSerializer] sink1Config.auditCluster shouldBe AuditCluster.LocalAuditCluster val sink2 = auditingSettings.auditSinks.toList(1) @@ -935,7 +935,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { enabledSink3 shouldBe a[Config.EsDataStreamBasedSink] val sink3Config = enabledSink3.asInstanceOf[Config.EsDataStreamBasedSink] sink3Config.rorAuditDataStream.dataStream should be(fullDataStreamName("readonlyrest_audit")) - sink3Config.logSerializer shouldBe a[DefaultAuditLogSerializer] + sink3Config.logSerializer shouldBe a[BlockVerbosityAwareAuditLogSerializer] sink3Config.auditCluster shouldBe AuditCluster.LocalAuditCluster } } @@ -1361,7 +1361,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = LocalAuditCluster @@ -1421,7 +1421,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = LocalAuditCluster @@ -1443,7 +1443,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "custom_template_20181231", expectedAuditCluster = LocalAuditCluster @@ -1509,7 +1509,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = RemoteAuditCluster(NonEmptyList.one(Uri.parse("1.1.1.1"))) @@ -1555,7 +1555,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = LocalAuditCluster @@ -1576,7 +1576,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "custom_template_20181231", expectedAuditCluster = LocalAuditCluster @@ -1639,7 +1639,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | """.stripMargin) - assertIndexBasedAuditSinkSettingsPresent[DefaultAuditLogSerializer]( + assertIndexBasedAuditSinkSettingsPresent[BlockVerbosityAwareAuditLogSerializer]( config, expectedIndexName = "readonlyrest_audit-2018-12-31", expectedAuditCluster = RemoteAuditCluster(NonEmptyList.one(Uri.parse("user:test@1.1.1.1"))) diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala index f0a1d28266..1d97c650cb 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/logging/AuditingToolTests.scala @@ -49,6 +49,7 @@ import tech.beshu.ror.utils.TestsUtils.{fullDataStreamName, fullIndexName, nes, import java.time.* import java.util.UUID +import scala.annotation.nowarn class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfterAll { @@ -61,6 +62,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter "es index sink is used" should { "not submit any audit entry" when { "request was allowed and verbosity level was ERROR" in { + @nowarn("cat=deprecation") val auditingTool = AuditingTool.create( settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { @@ -93,7 +95,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter (indexAuditSink.submit _).expects(fullIndexName("test_2018-12-31"), "mock-1", *).returning(()) val dataStreamAuditSink = mockedDataStreamBasedAuditSinkService (dataStreamAuditSink.submit _).expects(fullDataStreamName("test_ds"), "mock-1", *).returning(()) - + @nowarn("cat=deprecation") val auditingTool = AuditingTool.create( settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { @@ -109,7 +111,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter (indexAuditSink.submit _).expects(fullIndexName("test_2018-12-31"), "mock-1", *).returning(()) val dataStreamAuditSink = mockedDataStreamBasedAuditSinkService (dataStreamAuditSink.submit _).expects(fullDataStreamName("test_ds"), "mock-1", *).returning(()) - + @nowarn("cat=deprecation") val auditingTool = AuditingTool.create( settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { @@ -139,7 +141,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter (indexAuditSink.submit _).expects(fullIndexName("test_2018-12-31"), "mock-1", *).returning(()) val dataStreamAuditSink = mockedDataStreamBasedAuditSinkService (dataStreamAuditSink.submit _).expects(fullDataStreamName("test_ds"), "mock-1", *).returning(()) - + @nowarn("cat=deprecation") val auditingTool = AuditingTool.create( settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { @@ -159,7 +161,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter (indexAuditSink.submit _).expects(fullIndexName("test_2018-12-31"), "mock-1", *).returning(()) val dataStreamAuditSink = mockedDataStreamBasedAuditSinkService (dataStreamAuditSink.submit _).expects(fullDataStreamName("test_ds"), "mock-1", *).returning(()) - + @nowarn("cat=deprecation") val auditingTool = AuditingTool.create( settings = auditSettings(new DefaultAuditLogSerializer), auditSinkServiceCreator = new DataStreamAndIndexBasedAuditSinkServiceCreator { @@ -178,6 +180,7 @@ class AuditingToolTests extends AnyWordSpec with MockFactory with BeforeAndAfter } "log sink is used" should { "saved audit log to file defined in log4j config" in { + @nowarn("cat=deprecation") val auditingTool = AuditingTool.create( settings = AuditSettings( NonEmptyList.of( diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index 185f8f1baa..339e75d5a4 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -170,7 +170,156 @@ class LocalClusterAuditingToolsSuite } updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") } + "serialized event contains only expected fields" should { + "QueryAuditLogSerializer" in { + testSerializerFieldsWithTypes( + serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializer", + expectedFieldsWithTypes = commonFields ++ Map( + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string" + ) + ) + } + "QueryAuditLogSerializerV2" in { + testSerializerFieldsWithTypes( + serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2", + expectedFieldsWithTypes = commonFields ++ Map( + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string" + ) + ) + } + "QueryAuditLogSerializerV1" in { + testSerializerFieldsWithTypes( + serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1", + expectedFieldsWithTypes = commonFields ++ Map( + "content" -> "string" + ) + ) + } + "FullAuditLogWithQuerySerializer" in { + testSerializerFieldsWithTypes( + serializerClassName = "tech.beshu.ror.audit.instances.FullAuditLogWithQuerySerializer", + expectedFieldsWithTypes = commonFields ++ Map( + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string" + ) + ) + } + "FullAuditLogSerializer" in { + testSerializerFieldsWithTypes( + serializerClassName = "tech.beshu.ror.audit.instances.FullAuditLogSerializer", + expectedFieldsWithTypes = commonFields ++ Map( + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "BlockVerbosityAwareAuditLogSerializer" in { + testSerializerFieldsWithTypes( + serializerClassName = "tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer", + expectedFieldsWithTypes = commonFields ++ Map( + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "DefaultAuditLogSerializerV2" in { + testSerializerFieldsWithTypes( + serializerClassName = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2", + expectedFieldsWithTypes = commonFields ++ Map( + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "DefaultAuditLogSerializerV1" in { + testSerializerFieldsWithTypes( + serializerClassName = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1", + expectedFieldsWithTypes = commonFields, + ) + } + } + } + } + + private def commonFields = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + // Fields below are present in all events in all serializers provided + // in package tech.beshu.ror.audit.instances, but are optional + // "error_type" -> "string", + // "error_message" -> "string", + // "xff" -> "string", + // "impersonated_by" -> "string", + ) + + private def testSerializerFieldsWithTypes(serializerClassName: String, + expectedFieldsWithTypes: Map[String, String]): Unit = { + val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) + + updateRorConfigToUseSerializer(serializerClassName) + performAndAssertExampleSearchRequest(indexManager) + + forEachAuditManager { adminAuditManager => + eventually { + val auditEntries = adminAuditManager.getEntries.force().jsons + auditEntries.size should be >= 1 + + // At least one event must match the exact fields and types + val matches = auditEntries.exists { entry => + val entryFields = entry.obj.keySet + println(entryFields) + println(expectedFieldsWithTypes.keySet) + if (entryFields != expectedFieldsWithTypes.keySet) { + false + } else { + expectedFieldsWithTypes.forall { case (fieldName, expectedType) => + val value: ujson.Value = entry.obj(fieldName) + try { + expectedType match { + case "string" => noException should be thrownBy value.str + case "boolean" => noException should be thrownBy value.bool + case "number" => noException should be thrownBy value.num + case "array" => noException should be thrownBy value.arr + case other => fail(s"Unknown expected type: $other") + } + true + } catch { + case _: Throwable => false + } + } + } + } + + matches should be(true) + } } + + // Reset serializer to default + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") + Thread.sleep(3000) } private def performAndAssertExampleSearchRequest(indexManager: IndexManager) = { From b198ac2261270bf51a98a6a4f57df40f138119da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 14 Sep 2025 12:58:18 +0200 Subject: [PATCH 24/27] fix doc --- .../BlockVerbosityAwareAuditLogSerializer.scala | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/audit/src/main/scala/tech/beshu/ror/audit/instances/BlockVerbosityAwareAuditLogSerializer.scala b/audit/src/main/scala/tech/beshu/ror/audit/instances/BlockVerbosityAwareAuditLogSerializer.scala index 8ae6259a9d..cee6e5a649 100644 --- a/audit/src/main/scala/tech/beshu/ror/audit/instances/BlockVerbosityAwareAuditLogSerializer.scala +++ b/audit/src/main/scala/tech/beshu/ror/audit/instances/BlockVerbosityAwareAuditLogSerializer.scala @@ -24,10 +24,15 @@ import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldGroup.{Comm import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} /** - * Serializer for **full audit events including request content**. - * - Serializes all events, including every `Allowed` request, - * regardless of rule verbosity. - * - Use this when request body capture is required. + * Serializer for audit events that is aware of **rule-defined verbosity**. + * + * - Serializes all non-`Allowed` events. + * - Serializes `Allowed` events only if the corresponding rule + * specifies that they should be logged at `Verbosity.Info`. + * + * Recommended for standard audit logging where request body capture + * is not required, but cluster and node context are desired. + * * - Fields included: * - `match` — whether the request matched a rule (boolean) * - `block` — reason for blocking, if blocked (string) From ceadacb483c76148dc65c7a9981598e25c0d74a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 14 Sep 2025 19:51:00 +0200 Subject: [PATCH 25/27] review changes --- .../unit/acl/factory/AuditSettingsTests.scala | 290 +++++++++++++++++- .../LocalClusterAuditingToolsSuite.scala | 149 --------- 2 files changed, 287 insertions(+), 152 deletions(-) diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index 56b5691b6a..8707c36d70 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -35,15 +35,17 @@ import tech.beshu.ror.accesscontrol.factory.RawRorConfigBasedCoreFactory.CoreCre import tech.beshu.ror.accesscontrol.factory.{Core, RawRorConfigBasedCoreFactory} import tech.beshu.ror.audit.* import tech.beshu.ror.audit.AuditResponseContext.Verbosity -import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.audit.adapters.{DeprecatedAuditLogSerializerAdapter, EnvironmentAwareAuditLogSerializerAdapter} -import tech.beshu.ror.audit.instances.{BlockVerbosityAwareAuditLogSerializer, QueryAuditLogSerializer} +import tech.beshu.ror.audit.instances.* +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.mocks.{MockHttpClientsFactory, MockLdapConnectionPoolProvider} import tech.beshu.ror.utils.TestsUtils.* import java.time.{Instant, ZoneId, ZonedDateTime} +import scala.annotation.nowarn +import scala.jdk.CollectionConverters.CollectionHasAsScala import scala.reflect.ClassTag class AuditSettingsTests extends AnyWordSpec with Inside { @@ -1340,6 +1342,240 @@ class AuditSettingsTests extends AnyWordSpec with Inside { } } } + "serialized event contains only expected fields" when { + "QueryAuditLogSerializer" in { + testSerializerFieldsWithTypes[QueryAuditLogSerializer]( + serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializer", + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string", + ) + ) + } + "QueryAuditLogSerializerV2" in { + testSerializerFieldsWithTypes[QueryAuditLogSerializerV2]( + serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2", + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string", + ) + ) + } + "QueryAuditLogSerializerV1" in { + testSerializerFieldsWithTypes[QueryAuditLogSerializerV1]( + serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1", + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "content" -> "string", + ) + ) + } + "FullAuditLogWithQuerySerializer" in { + testSerializerFieldsWithTypes[FullAuditLogWithQuerySerializer]( + serializerClassName = "tech.beshu.ror.audit.instances.FullAuditLogWithQuerySerializer", + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string", + ) + ) + } + "FullAuditLogSerializer" in { + testSerializerFieldsWithTypes[FullAuditLogSerializer]( + serializerClassName = "tech.beshu.ror.audit.instances.FullAuditLogSerializer", + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "BlockVerbosityAwareAuditLogSerializer" in { + testSerializerFieldsWithTypes[BlockVerbosityAwareAuditLogSerializer]( + serializerClassName = "tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer", + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "DefaultAuditLogSerializerV2" in { + testSerializerFieldsWithTypes[DefaultAuditLogSerializerV2 @nowarn("cat=deprecation")]( + serializerClassName = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2", + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "DefaultAuditLogSerializerV1" in { + testSerializerFieldsWithTypes[DefaultAuditLogSerializerV1 @nowarn("cat=deprecation")]( + serializerClassName = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1", + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + ), + ) + } + } "deprecated format is used" should { "ignore deprecated fields when both formats are used at once" in { val config = rorConfigFromUnsafe( @@ -1804,6 +2040,54 @@ class AuditSettingsTests extends AnyWordSpec with Inside { } } + private def testSerializerFieldsWithTypes[SERIALIZER: ClassTag](serializerClassName: String, + expectedFieldsWithTypes: Map[String, String]): Unit = { + val config = rorConfigFromUnsafe( + s""" + |readonlyrest: + | audit: + | enabled: true + | outputs: + | - type: log + | serializer: + | type: static + | class_name: "$serializerClassName" + | + | access_control_rules: + | - name: test_block + | type: allow + | auth_key: admin:container + """.stripMargin) + + assertLogBasedAuditSinkSettingsPresent[SERIALIZER]( + config, + expectedLoggerName = "readonlyrest_audit" + ) + + val serialized = serializer(config).onResponse(AuditResponseContext.Forbidden(DummyAuditRequestContext)).get + val entryFields = serialized.keySet.asScala.toSet + + if (entryFields != expectedFieldsWithTypes.keySet) { + fail(s"Serialized event does not contains exactly fields that were expected") + } else { + expectedFieldsWithTypes.foreach { case (fieldName, expectedType) => + expectedType match { + case "string" => noException should be thrownBy serialized.getString(fieldName) + case "boolean" => noException should be thrownBy serialized.getBoolean(fieldName) + case "number" => noException should be thrownBy serialized.getDouble(fieldName) + case "array" => + val value = serialized.get(fieldName) + value match { + case _: org.json.JSONArray => succeed + case s: java.util.Collection[_] => noException should be thrownBy new org.json.JSONArray(s) + case other => fail(s"Expected '$fieldName' to be JSONArray, but got ${other.getClass.getName}: $other") + } + case other => fail(s"Unknown expected type: $other") + } + } + } + } + private def assertSettingsNoPresent(config: RawRorConfig): Unit = { val core = factory() .createCoreFrom( @@ -2002,7 +2286,7 @@ private object DummyAuditRequestContext extends AuditRequestContext { override def httpMethod: String = "" - override def loggedInUserName: Option[String] = None + override def loggedInUserName: Option[String] = Some("logged_user") override def impersonatedByUserName: Option[String] = None diff --git a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala index 339e75d5a4..185f8f1baa 100644 --- a/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala +++ b/integration-tests/src/test/scala/tech/beshu/ror/integration/suites/audit/LocalClusterAuditingToolsSuite.scala @@ -170,156 +170,7 @@ class LocalClusterAuditingToolsSuite } updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") } - "serialized event contains only expected fields" should { - "QueryAuditLogSerializer" in { - testSerializerFieldsWithTypes( - serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializer", - expectedFieldsWithTypes = commonFields ++ Map( - "es_node_name" -> "string", - "es_cluster_name" -> "string", - "content" -> "string" - ) - ) - } - "QueryAuditLogSerializerV2" in { - testSerializerFieldsWithTypes( - serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2", - expectedFieldsWithTypes = commonFields ++ Map( - "es_node_name" -> "string", - "es_cluster_name" -> "string", - "content" -> "string" - ) - ) - } - "QueryAuditLogSerializerV1" in { - testSerializerFieldsWithTypes( - serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1", - expectedFieldsWithTypes = commonFields ++ Map( - "content" -> "string" - ) - ) - } - "FullAuditLogWithQuerySerializer" in { - testSerializerFieldsWithTypes( - serializerClassName = "tech.beshu.ror.audit.instances.FullAuditLogWithQuerySerializer", - expectedFieldsWithTypes = commonFields ++ Map( - "es_node_name" -> "string", - "es_cluster_name" -> "string", - "content" -> "string" - ) - ) - } - "FullAuditLogSerializer" in { - testSerializerFieldsWithTypes( - serializerClassName = "tech.beshu.ror.audit.instances.FullAuditLogSerializer", - expectedFieldsWithTypes = commonFields ++ Map( - "es_node_name" -> "string", - "es_cluster_name" -> "string", - ) - ) - } - "BlockVerbosityAwareAuditLogSerializer" in { - testSerializerFieldsWithTypes( - serializerClassName = "tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer", - expectedFieldsWithTypes = commonFields ++ Map( - "es_node_name" -> "string", - "es_cluster_name" -> "string", - ) - ) - } - "DefaultAuditLogSerializerV2" in { - testSerializerFieldsWithTypes( - serializerClassName = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2", - expectedFieldsWithTypes = commonFields ++ Map( - "es_node_name" -> "string", - "es_cluster_name" -> "string", - ) - ) - } - "DefaultAuditLogSerializerV1" in { - testSerializerFieldsWithTypes( - serializerClassName = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1", - expectedFieldsWithTypes = commonFields, - ) - } - } - } - } - - private def commonFields = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - // Fields below are present in all events in all serializers provided - // in package tech.beshu.ror.audit.instances, but are optional - // "error_type" -> "string", - // "error_message" -> "string", - // "xff" -> "string", - // "impersonated_by" -> "string", - ) - - private def testSerializerFieldsWithTypes(serializerClassName: String, - expectedFieldsWithTypes: Map[String, String]): Unit = { - val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) - - updateRorConfigToUseSerializer(serializerClassName) - performAndAssertExampleSearchRequest(indexManager) - - forEachAuditManager { adminAuditManager => - eventually { - val auditEntries = adminAuditManager.getEntries.force().jsons - auditEntries.size should be >= 1 - - // At least one event must match the exact fields and types - val matches = auditEntries.exists { entry => - val entryFields = entry.obj.keySet - println(entryFields) - println(expectedFieldsWithTypes.keySet) - if (entryFields != expectedFieldsWithTypes.keySet) { - false - } else { - expectedFieldsWithTypes.forall { case (fieldName, expectedType) => - val value: ujson.Value = entry.obj(fieldName) - try { - expectedType match { - case "string" => noException should be thrownBy value.str - case "boolean" => noException should be thrownBy value.bool - case "number" => noException should be thrownBy value.num - case "array" => noException should be thrownBy value.arr - case other => fail(s"Unknown expected type: $other") - } - true - } catch { - case _: Throwable => false - } - } - } - } - - matches should be(true) - } } - - // Reset serializer to default - updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") - Thread.sleep(3000) } private def performAndAssertExampleSearchRequest(indexManager: IndexManager) = { From 249e18479906cca5e85144e60a80ccad4683d6f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Sun, 14 Sep 2025 20:11:02 +0200 Subject: [PATCH 26/27] bump pre version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 61393c34ff..152ac548a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ publishedPluginVersion=1.66.1 -pluginVersion=1.67.0-pre2 +pluginVersion=1.67.0-pre3 pluginName=readonlyrest org.gradle.jvmargs=-Xmx6144m From cd752234c06ef9c61f153afbb21245afc1d0ee99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Goworko?= Date: Mon, 15 Sep 2025 23:52:59 +0200 Subject: [PATCH 27/27] tests moved to audit module --- audit/build.gradle | 2 + .../tech/beshu/ror/audit/SerializerTest.scala | 345 ++++++++++++++++++ ci/run-pipeline.sh | 2 +- .../unit/acl/factory/AuditSettingsTests.scala | 284 -------------- 4 files changed, 348 insertions(+), 285 deletions(-) create mode 100644 audit/src/test/scala/tech/beshu/ror/audit/SerializerTest.scala diff --git a/audit/build.gradle b/audit/build.gradle index 43f3bbf626..9d0ce5adf3 100644 --- a/audit/build.gradle +++ b/audit/build.gradle @@ -23,6 +23,7 @@ plugins { id "java-library" id "maven-publish" id "signing" + id "com.github.maiflai.scalatest" version "0.33" } @@ -58,6 +59,7 @@ dependencies { crossBuildV213Implementation group: 'org.scala-lang', name: 'scala-library', version: '2.13.13' crossBuildV3Implementation group: 'org.scala-lang', name: 'scala3-library_3', version: '3.3.3' compileOnly group: 'org.scala-lang', name: 'scala3-library_3', version: '3.3.3' + testImplementation group: 'org.scalatest', name: 'scalatest_3', version: '3.2.16' } test { diff --git a/audit/src/test/scala/tech/beshu/ror/audit/SerializerTest.scala b/audit/src/test/scala/tech/beshu/ror/audit/SerializerTest.scala new file mode 100644 index 0000000000..ec7e399338 --- /dev/null +++ b/audit/src/test/scala/tech/beshu/ror/audit/SerializerTest.scala @@ -0,0 +1,345 @@ +/* + * This file is part of ReadonlyREST. + * + * ReadonlyREST 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. + * + * ReadonlyREST 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 ReadonlyREST. If not, see http://www.gnu.org/licenses/ + */ +package tech.beshu.ror.audit + +import org.json.JSONObject +import org.scalatest.matchers.should.Matchers._ +import org.scalatest.wordspec.AnyWordSpec +import tech.beshu.ror.audit.instances._ + +import java.time.Instant +import scala.annotation.nowarn +import scala.jdk.CollectionConverters._ + +class SerializerTest extends AnyWordSpec { + + "Serializer" when { + "serialized event contains only expected fields" when { + "QueryAuditLogSerializer" in { + testSerializerFieldsWithTypes( + serializer = new QueryAuditLogSerializer, + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string", + ) + ) + } + "QueryAuditLogSerializerV2" in { + testSerializerFieldsWithTypes( + serializer = new QueryAuditLogSerializerV2, + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string", + ) + ) + } + "QueryAuditLogSerializerV1" in { + testSerializerFieldsWithTypes( + serializer = new QueryAuditLogSerializerV1, + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "content" -> "string", + ) + ) + } + "FullAuditLogWithQuerySerializer" in { + testSerializerFieldsWithTypes( + serializer = new FullAuditLogWithQuerySerializer, + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + "content" -> "string", + ) + ) + } + "FullAuditLogSerializer" in { + testSerializerFieldsWithTypes( + serializer = new FullAuditLogSerializer, + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "BlockVerbosityAwareAuditLogSerializer" in { + testSerializerFieldsWithTypes( + serializer = new BlockVerbosityAwareAuditLogSerializer, + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "DefaultAuditLogSerializerV2" in { + testSerializerFieldsWithTypes( + serializer = new DefaultAuditLogSerializerV2 @nowarn("cat=deprecation"), + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + "es_node_name" -> "string", + "es_cluster_name" -> "string", + ) + ) + } + "DefaultAuditLogSerializerV1" in { + testSerializerFieldsWithTypes( + serializer = new DefaultAuditLogSerializerV1 @nowarn("cat=deprecation"), + expectedFieldsWithTypes = Map( + "match" -> "boolean", + "block" -> "string", + "id" -> "string", + "final_state" -> "string", + "@timestamp" -> "string", + "correlation_id" -> "string", + "processingMillis" -> "number", + "content_len" -> "number", + "content_len_kb" -> "number", + "type" -> "string", + "origin" -> "string", + "destination" -> "string", + "task_id" -> "number", + "req_method" -> "string", + "headers" -> "array", + "path" -> "string", + "user" -> "string", + "action" -> "string", + "indices" -> "array", + "acl_history" -> "string", + ), + ) + } + } + } + + private def testSerializerFieldsWithTypes(serializer: AuditLogSerializer, + expectedFieldsWithTypes: Map[String, String]): Unit = { + val serialized = serializer.onResponse(AuditResponseContext.Forbidden(DummyAuditRequestContext)).get + val entryFields = serialized.keySet.asScala.toSet + + if (entryFields != expectedFieldsWithTypes.keySet) { + fail(s"Serialized event does not contains exactly fields that were expected") + } else { + expectedFieldsWithTypes.foreach { case (fieldName, expectedType) => + expectedType match { + case "string" => noException should be thrownBy serialized.getString(fieldName) + case "boolean" => noException should be thrownBy serialized.getBoolean(fieldName) + case "number" => noException should be thrownBy serialized.getDouble(fieldName) + case "array" => + val value = serialized.get(fieldName) + value match { + case _: org.json.JSONArray => succeed + case s: java.util.Collection[_] => noException should be thrownBy new org.json.JSONArray(s) + case other => fail(s"Expected '$fieldName' to be JSONArray, but got ${other.getClass.getName}: $other") + } + case other => fail(s"Unknown expected type: $other") + } + } + } + } + +} + +private object DummyAuditRequestContext extends AuditRequestContext { + override def timestamp: Instant = Instant.now() + + override def id: String = "" + + override def correlationId: String = "" + + override def indices: Set[String] = Set.empty + + override def action: String = "" + + override def headers: Map[String, String] = Map.empty + + override def requestHeaders: Headers = Headers(Map.empty) + + override def uriPath: String = "" + + override def history: String = "" + + override def content: String = "" + + override def contentLength: Integer = 0 + + override def remoteAddress: String = "" + + override def localAddress: String = "" + + override def `type`: String = "" + + override def taskId: Long = 0 + + override def httpMethod: String = "" + + override def loggedInUserName: Option[String] = Some("logged_user") + + override def impersonatedByUserName: Option[String] = None + + override def involvesIndices: Boolean = false + + override def attemptedUserName: Option[String] = None + + override def rawAuthHeader: Option[String] = None + + override def generalAuditEvents: JSONObject = new JSONObject + + override def auditEnvironmentContext: AuditEnvironmentContext = new AuditEnvironmentContext { + override def esNodeName: String = "testEsNode" + + override def esClusterName: String = "testEsCluster" + } +} diff --git a/ci/run-pipeline.sh b/ci/run-pipeline.sh index 1163e7e2b8..d9dad1ffb5 100755 --- a/ci/run-pipeline.sh +++ b/ci/run-pipeline.sh @@ -42,7 +42,7 @@ fi if [[ -z $TRAVIS ]] || [[ $ROR_TASK == "core_tests" ]]; then echo ">>> Running unit tests.." - ./gradlew --no-daemon --stacktrace core:test + ./gradlew --no-daemon --stacktrace core:test audit:test fi run_integration_tests() { diff --git a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala index 8707c36d70..61f35fd39c 100644 --- a/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala +++ b/core/src/test/scala/tech/beshu/ror/unit/acl/factory/AuditSettingsTests.scala @@ -44,8 +44,6 @@ import tech.beshu.ror.mocks.{MockHttpClientsFactory, MockLdapConnectionPoolProvi import tech.beshu.ror.utils.TestsUtils.* import java.time.{Instant, ZoneId, ZonedDateTime} -import scala.annotation.nowarn -import scala.jdk.CollectionConverters.CollectionHasAsScala import scala.reflect.ClassTag class AuditSettingsTests extends AnyWordSpec with Inside { @@ -1342,240 +1340,6 @@ class AuditSettingsTests extends AnyWordSpec with Inside { } } } - "serialized event contains only expected fields" when { - "QueryAuditLogSerializer" in { - testSerializerFieldsWithTypes[QueryAuditLogSerializer]( - serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializer", - expectedFieldsWithTypes = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - "es_node_name" -> "string", - "es_cluster_name" -> "string", - "content" -> "string", - ) - ) - } - "QueryAuditLogSerializerV2" in { - testSerializerFieldsWithTypes[QueryAuditLogSerializerV2]( - serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializerV2", - expectedFieldsWithTypes = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - "es_node_name" -> "string", - "es_cluster_name" -> "string", - "content" -> "string", - ) - ) - } - "QueryAuditLogSerializerV1" in { - testSerializerFieldsWithTypes[QueryAuditLogSerializerV1]( - serializerClassName = "tech.beshu.ror.audit.instances.QueryAuditLogSerializerV1", - expectedFieldsWithTypes = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - "content" -> "string", - ) - ) - } - "FullAuditLogWithQuerySerializer" in { - testSerializerFieldsWithTypes[FullAuditLogWithQuerySerializer]( - serializerClassName = "tech.beshu.ror.audit.instances.FullAuditLogWithQuerySerializer", - expectedFieldsWithTypes = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - "es_node_name" -> "string", - "es_cluster_name" -> "string", - "content" -> "string", - ) - ) - } - "FullAuditLogSerializer" in { - testSerializerFieldsWithTypes[FullAuditLogSerializer]( - serializerClassName = "tech.beshu.ror.audit.instances.FullAuditLogSerializer", - expectedFieldsWithTypes = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - "es_node_name" -> "string", - "es_cluster_name" -> "string", - ) - ) - } - "BlockVerbosityAwareAuditLogSerializer" in { - testSerializerFieldsWithTypes[BlockVerbosityAwareAuditLogSerializer]( - serializerClassName = "tech.beshu.ror.audit.instances.BlockVerbosityAwareAuditLogSerializer", - expectedFieldsWithTypes = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - "es_node_name" -> "string", - "es_cluster_name" -> "string", - ) - ) - } - "DefaultAuditLogSerializerV2" in { - testSerializerFieldsWithTypes[DefaultAuditLogSerializerV2 @nowarn("cat=deprecation")]( - serializerClassName = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV2", - expectedFieldsWithTypes = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - "es_node_name" -> "string", - "es_cluster_name" -> "string", - ) - ) - } - "DefaultAuditLogSerializerV1" in { - testSerializerFieldsWithTypes[DefaultAuditLogSerializerV1 @nowarn("cat=deprecation")]( - serializerClassName = "tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1", - expectedFieldsWithTypes = Map( - "match" -> "boolean", - "block" -> "string", - "id" -> "string", - "final_state" -> "string", - "@timestamp" -> "string", - "correlation_id" -> "string", - "processingMillis" -> "number", - "content_len" -> "number", - "content_len_kb" -> "number", - "type" -> "string", - "origin" -> "string", - "destination" -> "string", - "task_id" -> "number", - "req_method" -> "string", - "headers" -> "array", - "path" -> "string", - "user" -> "string", - "action" -> "string", - "indices" -> "array", - "acl_history" -> "string", - ), - ) - } - } "deprecated format is used" should { "ignore deprecated fields when both formats are used at once" in { val config = rorConfigFromUnsafe( @@ -2040,54 +1804,6 @@ class AuditSettingsTests extends AnyWordSpec with Inside { } } - private def testSerializerFieldsWithTypes[SERIALIZER: ClassTag](serializerClassName: String, - expectedFieldsWithTypes: Map[String, String]): Unit = { - val config = rorConfigFromUnsafe( - s""" - |readonlyrest: - | audit: - | enabled: true - | outputs: - | - type: log - | serializer: - | type: static - | class_name: "$serializerClassName" - | - | access_control_rules: - | - name: test_block - | type: allow - | auth_key: admin:container - """.stripMargin) - - assertLogBasedAuditSinkSettingsPresent[SERIALIZER]( - config, - expectedLoggerName = "readonlyrest_audit" - ) - - val serialized = serializer(config).onResponse(AuditResponseContext.Forbidden(DummyAuditRequestContext)).get - val entryFields = serialized.keySet.asScala.toSet - - if (entryFields != expectedFieldsWithTypes.keySet) { - fail(s"Serialized event does not contains exactly fields that were expected") - } else { - expectedFieldsWithTypes.foreach { case (fieldName, expectedType) => - expectedType match { - case "string" => noException should be thrownBy serialized.getString(fieldName) - case "boolean" => noException should be thrownBy serialized.getBoolean(fieldName) - case "number" => noException should be thrownBy serialized.getDouble(fieldName) - case "array" => - val value = serialized.get(fieldName) - value match { - case _: org.json.JSONArray => succeed - case s: java.util.Collection[_] => noException should be thrownBy new org.json.JSONArray(s) - case other => fail(s"Expected '$fieldName' to be JSONArray, but got ${other.getClass.getName}: $other") - } - case other => fail(s"Unknown expected type: $other") - } - } - } - } - private def assertSettingsNoPresent(config: RawRorConfig): Unit = { val core = factory() .createCoreFrom(