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 f826bcc1cb..1f60a31f40 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 @@ -45,23 +45,23 @@ private[ror] object AuditSerializationHelper { } def serialize(responseContext: AuditResponseContext, - fields: Map[AuditFieldName, AuditFieldValueDescriptor], + fields: Map[AuditFieldPath, 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)) + createEntry(fields, EventData(matched = true, FinalState.Allowed, reason, responseContext.duration, requestContext, None)) ) case ForbiddenBy(requestContext, _, reason) => - Some(createEntry(fields, EventData(matched = true, "FORBIDDEN", reason, responseContext.duration, requestContext, None))) + Some(createEntry(fields, EventData(matched = true, FinalState.Forbidden, reason, responseContext.duration, requestContext, None))) case Forbidden(requestContext) => - Some(createEntry(fields, EventData(matched = false, "FORBIDDEN", "default", responseContext.duration, requestContext, None))) + Some(createEntry(fields, EventData(matched = false, FinalState.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))) + Some(createEntry(fields, EventData(matched = false, FinalState.IndexNotExist, "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)))) + Some(createEntry(fields, EventData(matched = false, FinalState.Errored, "error", responseContext.duration, requestContext, Some(cause)))) } } @@ -76,23 +76,48 @@ private[ror] object AuditSerializationHelper { } } - private def createEntry(fields: Map[AuditFieldName, AuditFieldValueDescriptor], + private def createEntry(fields: Map[AuditFieldPath, AuditFieldValueDescriptor], eventData: EventData) = { 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) } + val resolvedFields: Map[AuditFieldPath, Any] = + Map(AuditFieldPath("@timestamp") -> timestampFormatter.format(eventData.requestContext.timestamp)) ++ + fields.map { case (name, valueDescriptor) => name -> resolveAuditFieldValue(valueDescriptor) } - resolvedFields - .foldLeft(new JSONObject()) { case (soFar, (key, value)) => soFar.put(key, value) } - .mergeWith(eventData.requestContext.generalAuditEvents) + resolvedFields.foldLeft(new JSONObject()) { case (soFar, (path, value)) => + putNested(soFar, path.path.toList, value) + }.mergeWith(eventData.requestContext.generalAuditEvents) + } + + private def putNested(json: JSONObject, path: List[String], value: Any): JSONObject = { + path match { + case Nil => + json + case key :: Nil => + json.put(key, value) + json + case key :: tail => + val child = Option(json.optJSONObject(key)).getOrElse(new JSONObject()) + json.put(key, putNested(child, tail, value)) + json + } } private def resolver(eventData: EventData): AuditFieldValueDescriptor => Any = auditValue => { val requestContext = eventData.requestContext auditValue match { case AuditFieldValueDescriptor.IsMatched => eventData.matched - case AuditFieldValueDescriptor.FinalState => eventData.finalState + case AuditFieldValueDescriptor.FinalState => eventData.finalState match { + case FinalState.Allowed => "ALLOWED" + case FinalState.Forbidden => "FORBIDDEN" + case FinalState.Errored => "ERRORED" + case FinalState.IndexNotExist => "INDEX NOT EXIST" + } + case AuditFieldValueDescriptor.EcsEventOutcome => eventData.finalState match { + case FinalState.Allowed => "success" + case FinalState.Forbidden => "failure" + case FinalState.Errored => "unknown" + case FinalState.IndexNotExist => "unknown" + } case AuditFieldValueDescriptor.Reason => eventData.reason case AuditFieldValueDescriptor.User => SerializeUser.serialize(requestContext).orNull case AuditFieldValueDescriptor.LoggedUser => requestContext.loggedInUserName.orNull @@ -102,6 +127,7 @@ private[ror] object AuditSerializationHelper { 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.ProcessingDurationNanos => eventData.duration.toNanos case AuditFieldValueDescriptor.Timestamp => timestampFormatter.format(requestContext.timestamp) case AuditFieldValueDescriptor.Id => requestContext.id case AuditFieldValueDescriptor.CorrelationId => requestContext.correlationId @@ -121,6 +147,8 @@ private[ror] object AuditSerializationHelper { case AuditFieldValueDescriptor.EsNodeName => eventData.requestContext.auditEnvironmentContext.esNodeName case AuditFieldValueDescriptor.EsClusterName => eventData.requestContext.auditEnvironmentContext.esClusterName case AuditFieldValueDescriptor.StaticText(text) => text + case AuditFieldValueDescriptor.NumericValue(value) => value.bigDecimal + case AuditFieldValueDescriptor.BooleanValue(value) => value case AuditFieldValueDescriptor.Combined(values) => values.map(resolver(eventData)).mkString } } @@ -141,12 +169,24 @@ private[ror] object AuditSerializationHelper { } private final case class EventData(matched: Boolean, - finalState: String, + finalState: FinalState, reason: String, duration: FiniteDuration, requestContext: AuditRequestContext, error: Option[Throwable]) + private sealed trait FinalState + + private object FinalState { + case object Allowed extends FinalState + + case object Forbidden extends FinalState + + case object Errored extends FinalState + + case object IndexNotExist extends FinalState + } + sealed trait AllowedEventMode object AllowedEventMode { @@ -155,7 +195,15 @@ private[ror] object AuditSerializationHelper { final case class Include(types: Set[Verbosity]) extends AllowedEventMode } - final case class AuditFieldName(value: String) + final case class AuditFieldPath private(path: List[String]) + + object AuditFieldPath { + def apply(name: String): AuditFieldPath = + AuditFieldPath(List(name)) + + def apply(head: String, tail: List[String]): AuditFieldPath = + AuditFieldPath(head :: tail) + } sealed trait AuditFieldValueDescriptor @@ -166,6 +214,8 @@ private[ror] object AuditSerializationHelper { case object FinalState extends AuditFieldValueDescriptor + case object EcsEventOutcome extends AuditFieldValueDescriptor + case object Reason extends AuditFieldValueDescriptor @deprecated("[ROR] The User audit field value descriptor should not be used. Use LoggedUser or PresentedIdentity instead", "1.68.0") @@ -185,6 +235,8 @@ private[ror] object AuditSerializationHelper { case object ProcessingDurationMillis extends AuditFieldValueDescriptor + case object ProcessingDurationNanos extends AuditFieldValueDescriptor + // Identifiers case object Timestamp extends AuditFieldValueDescriptor @@ -230,6 +282,10 @@ private[ror] object AuditSerializationHelper { final case class StaticText(value: String) extends AuditFieldValueDescriptor + final case class BooleanValue(value: Boolean) extends AuditFieldValueDescriptor + + final case class NumericValue(value: BigDecimal) extends AuditFieldValueDescriptor + final case class Combined(values: List[AuditFieldValueDescriptor]) extends AuditFieldValueDescriptor } @@ -244,42 +300,42 @@ private[ror] object AuditSerializationHelper { 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("logged_user") -> AuditFieldValueDescriptor.LoggedUser, - AuditFieldName("presented_identity") -> AuditFieldValueDescriptor.PresentedIdentity, - AuditFieldName("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser, - AuditFieldName("action") -> AuditFieldValueDescriptor.Action, - AuditFieldName("indices") -> AuditFieldValueDescriptor.InvolvedIndices, - AuditFieldName("acl_history") -> AuditFieldValueDescriptor.AclHistory + private val commonFields: Map[AuditFieldPath, AuditFieldValueDescriptor] = Map( + AuditFieldPath("match") -> AuditFieldValueDescriptor.IsMatched, + AuditFieldPath("block") -> AuditFieldValueDescriptor.Reason, + AuditFieldPath("id") -> AuditFieldValueDescriptor.Id, + AuditFieldPath("final_state") -> AuditFieldValueDescriptor.FinalState, + AuditFieldPath("@timestamp") -> AuditFieldValueDescriptor.Timestamp, + AuditFieldPath("correlation_id") -> AuditFieldValueDescriptor.CorrelationId, + AuditFieldPath("processingMillis") -> AuditFieldValueDescriptor.ProcessingDurationMillis, + AuditFieldPath("error_type") -> AuditFieldValueDescriptor.ErrorType, + AuditFieldPath("error_message") -> AuditFieldValueDescriptor.ErrorMessage, + AuditFieldPath("content_len") -> AuditFieldValueDescriptor.ContentLengthInBytes, + AuditFieldPath("content_len_kb") -> AuditFieldValueDescriptor.ContentLengthInKb, + AuditFieldPath("type") -> AuditFieldValueDescriptor.Type, + AuditFieldPath("origin") -> AuditFieldValueDescriptor.RemoteAddress, + AuditFieldPath("destination") -> AuditFieldValueDescriptor.LocalAddress, + AuditFieldPath("xff") -> AuditFieldValueDescriptor.XForwardedForHttpHeader, + AuditFieldPath("task_id") -> AuditFieldValueDescriptor.TaskId, + AuditFieldPath("req_method") -> AuditFieldValueDescriptor.HttpMethod, + AuditFieldPath("headers") -> AuditFieldValueDescriptor.HttpHeaderNames, + AuditFieldPath("path") -> AuditFieldValueDescriptor.HttpPath, + AuditFieldPath("user") -> AuditFieldValueDescriptor.User, + AuditFieldPath("logged_user") -> AuditFieldValueDescriptor.LoggedUser, + AuditFieldPath("presented_identity") -> AuditFieldValueDescriptor.PresentedIdentity, + AuditFieldPath("impersonated_by") -> AuditFieldValueDescriptor.ImpersonatedByUser, + AuditFieldPath("action") -> AuditFieldValueDescriptor.Action, + AuditFieldPath("indices") -> AuditFieldValueDescriptor.InvolvedIndices, + AuditFieldPath("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 esEnvironmentFields: Map[AuditFieldPath, AuditFieldValueDescriptor] = Map( + AuditFieldPath("es_node_name") -> AuditFieldValueDescriptor.EsNodeName, + AuditFieldPath("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName ) - private val requestContentFields: Map[AuditFieldName, AuditFieldValueDescriptor] = Map( - AuditFieldName("content") -> AuditFieldValueDescriptor.Content + private val requestContentFields: Map[AuditFieldPath, AuditFieldValueDescriptor] = Map( + AuditFieldPath("content") -> AuditFieldValueDescriptor.Content ) } diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditFieldUtils.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditFieldUtils.scala new file mode 100644 index 0000000000..ee2d37820a --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/AuditFieldUtils.scala @@ -0,0 +1,46 @@ +/* + * 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 + +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AuditFieldPath, AuditFieldValueDescriptor} + +object AuditFieldUtils { + def fields(values: ((AuditFieldPath, AuditFieldValueDescriptor) | Map[AuditFieldPath, AuditFieldValueDescriptor])*): Map[AuditFieldPath, AuditFieldValueDescriptor] = + values.flatMap(toMap).toMap + + def withPrefix(prefix: String)( + values: ((AuditFieldPath, AuditFieldValueDescriptor) | Map[AuditFieldPath, AuditFieldValueDescriptor])* + ): Map[AuditFieldPath, AuditFieldValueDescriptor] = + withPrefix(prefix, values.flatMap(toMap).toMap) + + private def withPrefix(prefix: String, + values: Map[AuditFieldPath, AuditFieldValueDescriptor]): Map[AuditFieldPath, AuditFieldValueDescriptor] = { + values.map { case (path, desc) => + val newPath = AuditFieldPath(prefix, path.path) + newPath -> desc + } + } + + private def toMap(value: (AuditFieldPath, AuditFieldValueDescriptor) | Map[AuditFieldPath, AuditFieldValueDescriptor]): Map[AuditFieldPath, AuditFieldValueDescriptor] = { + value match { + case (path: AuditFieldPath, value: AuditFieldValueDescriptor) => + Map(path -> value) + case values: Map[AuditFieldPath, AuditFieldValueDescriptor] => + values + } + } +} diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDescriptorParser.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDescriptorParser.scala index 2bc6013968..f9789306df 100644 --- a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDescriptorParser.scala +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/configurable/AuditFieldValueDescriptorParser.scala @@ -28,7 +28,7 @@ object AuditFieldValueDescriptorParser extends Logging { private val key = P.charsWhile(c => c != '{' && c != '}') private val placeholder: P[Either[String, AuditFieldValueDescriptor]] = - (lbrace *> key <* rbrace).map(k => deserializerAuditFieldValueDescriptor(k.trim.toUpperCase).toRight(k)) + (lbrace *> key <* rbrace).map(k => deserializerAuditFieldValueDescriptor(k.trim).toRight(k)) private val text: P[AuditFieldValueDescriptor] = P.charsWhile(_ != '{').map(AuditFieldValueDescriptor.StaticText.apply) @@ -59,6 +59,7 @@ object AuditFieldValueDescriptorParser extends Logging { str.toUpperCase match { case "IS_MATCHED" => Some(AuditFieldValueDescriptor.IsMatched) case "FINAL_STATE" => Some(AuditFieldValueDescriptor.FinalState) + case "ECS_EVENT_OUTCOME" => Some(AuditFieldValueDescriptor.EcsEventOutcome) case "REASON" => Some(AuditFieldValueDescriptor.Reason) case "USER" => logger.warn( @@ -75,6 +76,7 @@ object AuditFieldValueDescriptorParser extends Logging { case "INVOLVED_INDICES" => Some(AuditFieldValueDescriptor.InvolvedIndices) case "ACL_HISTORY" => Some(AuditFieldValueDescriptor.AclHistory) case "PROCESSING_DURATION_MILLIS" => Some(AuditFieldValueDescriptor.ProcessingDurationMillis) + case "PROCESSING_DURATION_NANOS" => Some(AuditFieldValueDescriptor.ProcessingDurationNanos) case "TIMESTAMP" => Some(AuditFieldValueDescriptor.Timestamp) case "ID" => Some(AuditFieldValueDescriptor.Id) case "CORRELATION_ID" => Some(AuditFieldValueDescriptor.CorrelationId) 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 3e2d6a76b8..f77edd3a1f 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 @@ -18,11 +18,11 @@ 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.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldPath, AuditFieldValueDescriptor} import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} class ConfigurableAuditLogSerializer(val allowedEventMode: AllowedEventMode, - val fields: Map[AuditFieldName, AuditFieldValueDescriptor]) extends AuditLogSerializer { + val fields: Map[AuditFieldPath, AuditFieldValueDescriptor]) extends AuditLogSerializer { override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = AuditSerializationHelper.serialize(responseContext, fields, allowedEventMode) diff --git a/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/ecs/EcsV1AuditLogSerializer.scala b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/ecs/EcsV1AuditLogSerializer.scala new file mode 100644 index 0000000000..104644d769 --- /dev/null +++ b/core/src/main/scala/tech/beshu/ror/accesscontrol/audit/ecs/EcsV1AuditLogSerializer.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.accesscontrol.audit.ecs + +import org.json.JSONObject +import tech.beshu.ror.accesscontrol.audit.AuditFieldUtils.* +import tech.beshu.ror.accesscontrol.audit.ecs.EcsV1AuditLogSerializer.* +import tech.beshu.ror.audit.utils.AuditSerializationHelper +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldPath, AuditFieldValueDescriptor} +import tech.beshu.ror.audit.{AuditLogSerializer, AuditResponseContext} + +class EcsV1AuditLogSerializer(val allowedEventMode: AllowedEventMode) extends AuditLogSerializer { + + override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] = { + AuditSerializationHelper.serialize(responseContext, auditFields, allowedEventMode) + } + +} + +object EcsV1AuditLogSerializer { + private val auditFields: Map[AuditFieldPath, AuditFieldValueDescriptor] = fields( + withPrefix("ecs")( + // Schema defined by EcsV1AuditLogSerializer is ECS 1.6.0 compliant and does not use newer features + // introduced by later versions (https://www.elastic.co/guide/en/ecs/1.6/ecs-field-reference.html) + AuditFieldPath("version") -> AuditFieldValueDescriptor.StaticText("1.6.0"), + ), + withPrefix("trace")( + AuditFieldPath("id") -> AuditFieldValueDescriptor.CorrelationId, + ), + withPrefix("url")( + AuditFieldPath("path") -> AuditFieldValueDescriptor.HttpPath, + ), + withPrefix("source")( + AuditFieldPath("address") -> AuditFieldValueDescriptor.RemoteAddress, + ), + withPrefix("destination")( + AuditFieldPath("address") -> AuditFieldValueDescriptor.LocalAddress, + ), + withPrefix("http")( + withPrefix("request")( + AuditFieldPath("method") -> AuditFieldValueDescriptor.HttpMethod, + withPrefix("body")( + AuditFieldPath("content") -> AuditFieldValueDescriptor.Content, + AuditFieldPath("bytes") -> AuditFieldValueDescriptor.ContentLengthInBytes, + ), + ), + ), + withPrefix("user")( + AuditFieldPath("name") -> AuditFieldValueDescriptor.User, + withPrefix("effective")( + AuditFieldPath("name") -> AuditFieldValueDescriptor.ImpersonatedByUser, + ), + ), + withPrefix("event")( + AuditFieldPath("id") -> AuditFieldValueDescriptor.Id, + AuditFieldPath("duration") -> AuditFieldValueDescriptor.ProcessingDurationNanos, + AuditFieldPath("action") -> AuditFieldValueDescriptor.Action, + AuditFieldPath("reason") -> AuditFieldValueDescriptor.Type, + AuditFieldPath("outcome") -> AuditFieldValueDescriptor.EcsEventOutcome, + ), + withPrefix("error")( + AuditFieldPath("type") -> AuditFieldValueDescriptor.ErrorType, + AuditFieldPath("message") -> AuditFieldValueDescriptor.ErrorMessage, + ), + withPrefix("labels")( + AuditFieldPath("x_forwarded_for") -> AuditFieldValueDescriptor.XForwardedForHttpHeader, + AuditFieldPath("es_cluster_name") -> AuditFieldValueDescriptor.EsClusterName, + AuditFieldPath("es_node_name") -> AuditFieldValueDescriptor.EsNodeName, + AuditFieldPath("es_task_id") -> AuditFieldValueDescriptor.TaskId, + AuditFieldPath("ror_involved_indices") -> AuditFieldValueDescriptor.InvolvedIndices, + AuditFieldPath("ror_acl_history") -> AuditFieldValueDescriptor.AclHistory, + AuditFieldPath("ror_final_state") -> AuditFieldValueDescriptor.FinalState, + AuditFieldPath("ror_detailed_reason") -> AuditFieldValueDescriptor.Reason, + ), + ) +} 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 15bd689760..c4057fe975 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.* import io.circe.Decoder.* -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 @@ -26,6 +26,7 @@ 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.{AuditFieldValueDescriptorParser, ConfigurableAuditLogSerializer} +import tech.beshu.ror.accesscontrol.audit.ecs.EcsV1AuditLogSerializer 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 +34,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.AuditLogSerializer 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.AuditLogSerializer +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldPath, AuditFieldValueDescriptor} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.implicits.* import tech.beshu.ror.utils.yaml.YamlKeyDecoder @@ -217,15 +218,32 @@ object AuditingSettingsDecoder extends Logging { c.downField("serializer").as[Option[AuditLogSerializer]](extendedSyntaxStaticSerializerDecoder) case SerializerType.ExtendedSyntaxConfigurableSerializer => c.downField("serializer").as[Option[AuditLogSerializer]](extendedSyntaxConfigurableSerializerDecoder) + case SerializerType.EcsSerializer => + c.downField("serializer").as[Option[AuditLogSerializer]](ecsSerializerDecoder) } } yield result } + private def ecsSerializerDecoder: Decoder[Option[AuditLogSerializer]] = Decoder.instance { c => + for { + version <- c.downField("version").as[Option[EcsSerializerVersion]] + .left.map(withAuditingSettingsCreationErrorMessage(msg => s"ECS serializer 'version' is invalid: $msg")) + allowedEventMode <- c.downField("verbosity_level_serialization_mode").as[AllowedEventMode] + .left.map(withAuditingSettingsCreationErrorMessage(msg => s"ECS serializer is used, but the 'verbosity_level_serialization_mode' setting is invalid: $msg")) + serializer = version match { + case None => + new EcsV1AuditLogSerializer(allowedEventMode) + case Some(EcsSerializerVersion.V1) => + new EcsV1AuditLogSerializer(allowedEventMode) + } + } yield Some(serializer) + } + 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")) - fields <- c.downField("fields").as[Map[AuditFieldName, AuditFieldValueDescriptor]] + fields <- c.downField("fields").as[Map[AuditFieldPath, AuditFieldValueDescriptor]] .left.map(withAuditingSettingsCreationErrorMessage(msg => s"Configurable serializer is used, but the 'fields' setting is missing or invalid: $msg")) serializer = new ConfigurableAuditLogSerializer(allowedEventMode, fields) } yield Some(serializer) @@ -265,9 +283,11 @@ object AuditingSettingsDecoder extends Logging { Right(SerializerType.ExtendedSyntaxStaticSerializer) case "configurable" => Right(SerializerType.ExtendedSyntaxConfigurableSerializer) + case "ecs" => + Right(SerializerType.EcsSerializer) case other => Left(DecodingFailure(AclCreationErrorCoders.stringify( - AuditingSettingsCreationError(Message(s"Invalid serializer type '$other', allowed values [static, configurable]")) + AuditingSettingsCreationError(Message(s"Invalid serializer type '$other', allowed values [static, configurable, ecs]")) ), Nil)) } case Some(_) | None => @@ -283,6 +303,19 @@ object AuditingSettingsDecoder extends Logging { case object ExtendedSyntaxStaticSerializer extends SerializerType case object ExtendedSyntaxConfigurableSerializer extends SerializerType + + case object EcsSerializer extends SerializerType + } + + private given ecsSerializerVersionDecoder: Decoder[EcsSerializerVersion] = Decoder.decodeString.map(_.toLowerCase).emap { + case "v1" => Right(EcsSerializerVersion.V1) + case other => Left(s"Invalid ECS serializer version $other") + } + + private sealed trait EcsSerializerVersion + + private object EcsSerializerVersion { + case object V1 extends EcsSerializerVersion } private def withAuditingSettingsCreationErrorMessage(message: String => String)(decodingFailure: DecodingFailure) = { @@ -337,16 +370,45 @@ object AuditingSettingsDecoder extends Logging { .decoder } - given auditFieldNameDecoder: KeyDecoder[AuditFieldName] = { - KeyDecoder.decodeKeyString.map(AuditFieldName.apply) - } + private given auditFieldsDecoder: Decoder[Map[AuditFieldPath, AuditFieldValueDescriptor]] = + Decoder.instance { cursor => + def decodeJson(json: Json, + path: List[String]): Decoder.Result[Map[List[String], AuditFieldValueDescriptor]] = { + json.fold( + jsonNull = + Left(DecodingFailure("Expected AuditFieldValueDescriptor, got null", cursor.history)), + jsonBoolean = b => + Right(Map(path -> AuditFieldValueDescriptor.BooleanValue(b))), + jsonNumber = n => + n.toBigDecimal + .map(bd => Map(path -> AuditFieldValueDescriptor.NumericValue(bd))) + .toRight(DecodingFailure("Cannot decode number", cursor.history)), + jsonString = s => + AuditFieldValueDescriptorParser + .parse(s) + .map(desc => Map(path -> desc)) + .left.map(err => DecodingFailure(err, cursor.history)), + jsonArray = _ => + Left(DecodingFailure("AuditFieldValueDescriptor cannot be an array", cursor.history)), + jsonObject = obj => + obj.toList + .traverse { case (k, v) => decodeJson(v, path :+ k) } + .map(_.foldLeft(Map.empty[List[String], AuditFieldValueDescriptor])(_ ++ _)) + ) + } - given auditFieldValueDecoder: Decoder[AuditFieldValueDescriptor] = { - SyncDecoderCreator - .from(Decoder.decodeString) - .emap(AuditFieldValueDescriptorParser.parse) - .decoder - } + for { + rawResult <- decodeJson(cursor.value, Nil) + result = rawResult.view.map{ case (path, value) => + NonEmptyList.fromList(path) match { + case Some(nonEmptyPath) => + AuditFieldPath(nonEmptyPath.head, nonEmptyPath.tail) -> value + case None => + throw new IllegalStateException(s"Empty audit field path encountered when decoding configurable audit fields definition.") + } + }.toMap + } yield result + } given verbosityDecoder: Decoder[Verbosity] = { SyncDecoderCreator 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 a88c89f148..2e7d24bdda 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 @@ -18,15 +18,18 @@ package tech.beshu.ror.unit.acl.factory import cats.data.NonEmptyList import eu.timepit.refined.types.string.NonEmptyString +import io.circe.{Json, parser} import io.lemonlabs.uri.Uri import monix.execution.Scheduler.Implicits.global import org.json.JSONObject import org.scalatest.Inside import org.scalatest.matchers.should.Matchers.* import org.scalatest.wordspec.AnyWordSpec +import tech.beshu.ror.accesscontrol.audit.AuditFieldUtils 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.audit.ecs.EcsV1AuditLogSerializer 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} @@ -37,7 +40,7 @@ import tech.beshu.ror.audit.* import tech.beshu.ror.audit.AuditResponseContext.Verbosity import tech.beshu.ror.audit.adapters.{DeprecatedAuditLogSerializerAdapter, EnvironmentAwareAuditLogSerializerAdapter} import tech.beshu.ror.audit.instances.* -import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldName, AuditFieldValueDescriptor} +import tech.beshu.ror.audit.utils.AuditSerializationHelper.{AllowedEventMode, AuditFieldPath, AuditFieldValueDescriptor} import tech.beshu.ror.configuration.{EnvironmentConfig, RawRorConfig, RorConfig} import tech.beshu.ror.es.EsVersion import tech.beshu.ror.mocks.{MockHttpClientsFactory, MockLdapConnectionPoolProvider} @@ -388,6 +391,14 @@ class AuditSettingsTests extends AnyWordSpec with Inside { | type: configurable | verbosity_level_serialization_mode: [INFO] | fields: + | custom_section: + | nested_text: "nt" + | nested_number: 123 + | nested_boolean: true + | double_nested: + | double_nested_next: "dnt" + | triple_nested: + | triple_nested_next: "tnt" | node_name_with_static_suffix: "{ES_NODE_NAME} with suffix" | another_field: "{ES_CLUSTER_NAME} {HTTP_METHOD}" | tid: "{TASK_ID}" @@ -409,11 +420,24 @@ class AuditSettingsTests extends AnyWordSpec with Inside { val configuredSerializer = serializer(config).asInstanceOf[ConfigurableAuditLogSerializer] 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"))), - AuditFieldName("another_field") -> AuditFieldValueDescriptor.Combined(List(AuditFieldValueDescriptor.EsClusterName, AuditFieldValueDescriptor.StaticText(" "), AuditFieldValueDescriptor.HttpMethod)), - AuditFieldName("tid") -> AuditFieldValueDescriptor.TaskId, - AuditFieldName("bytes") -> AuditFieldValueDescriptor.ContentLengthInBytes, + configuredSerializer.fields shouldBe AuditFieldUtils.fields( + AuditFieldUtils.withPrefix("custom_section")( + AuditFieldPath("nested_text") -> AuditFieldValueDescriptor.StaticText("nt"), + AuditFieldPath("nested_number") -> AuditFieldValueDescriptor.NumericValue(123), + AuditFieldPath("nested_boolean") -> AuditFieldValueDescriptor.BooleanValue(true), + AuditFieldUtils.withPrefix("double_nested")( + AuditFieldPath("double_nested_next") -> AuditFieldValueDescriptor.StaticText("dnt"), + AuditFieldUtils.withPrefix("triple_nested")( + Map( + AuditFieldPath("triple_nested_next") -> AuditFieldValueDescriptor.StaticText("tnt"), + ) + ), + ), + ), + AuditFieldPath("node_name_with_static_suffix") -> AuditFieldValueDescriptor.Combined(List(AuditFieldValueDescriptor.EsNodeName, AuditFieldValueDescriptor.StaticText(" with suffix"))), + AuditFieldPath("another_field") -> AuditFieldValueDescriptor.Combined(List(AuditFieldValueDescriptor.EsClusterName, AuditFieldValueDescriptor.StaticText(" "), AuditFieldValueDescriptor.HttpMethod)), + AuditFieldPath("tid") -> AuditFieldValueDescriptor.TaskId, + AuditFieldPath("bytes") -> AuditFieldValueDescriptor.ContentLengthInBytes, ) } } @@ -634,6 +658,89 @@ class AuditSettingsTests extends AnyWordSpec with Inside { serializedResponse.get.get("custom_field_for_es_node_name") shouldBe "testEsNode" serializedResponse.get.get("custom_field_for_es_cluster_name") shouldBe "testEsCluster" } + "ECS serializer is set" in { + val config = rorConfigFromUnsafe( + """ + |readonlyrest: + | audit: + | enabled: true + | outputs: + | - type: index + | serializer: + | type: ecs + | verbosity_level_serialization_mode: [INFO] + | + | access_control_rules: + | + | - name: test_block + | type: allow + | auth_key: admin:container + | + """.stripMargin) + + assertIndexBasedAuditSinkSettingsPresent[EcsV1AuditLogSerializer]( + config, + expectedIndexName = "readonlyrest_audit-2018-12-31", + expectedAuditCluster = LocalAuditCluster + ) + val createdSerializer = serializer(config) + val serializedResponse = createdSerializer.onResponse(AuditResponseContext.Forbidden(new DummyAuditRequestContext)) + + val expectedJsonStr = + """{ + | "trace" : { + | "id" : "corr_id_123" + | }, + | "@timestamp" : "IGNORED", + | "ecs" : { + | "version" : "1.6.0" + | }, + | "destination" : { + | "address" : "192.168.0.124" + | }, + | "http" : { + | "request" : { + | "method" : "GET", + | "body" : { + | "bytes" : 123, + | "content" : "Full content of the request" + | } + | } + | }, + | "source" : { + | "address" : "192.168.0.123" + | }, + | "event" : { + | "duration" : 5000000000, + | "reason" : "RRTestConfigRequest", + | "action" : "cluster:internal_ror/user_metadata/get", + | "id" : "trace_id_123", + | "outcome" : "failure" + | }, + | "error" : {}, + | "user" : { + | "effective" : { + | "name" : "impersonated_by_user" + | }, + | "name" : "logged_user" + | }, + | "url" : { + | "path" : "/path/to/resource" + | }, + | "labels" : { + | "es_cluster_name" : "testEsCluster", + | "es_task_id" : 123, + | "es_node_name" : "testEsNode", + | "ror_acl_history" : "historyEntry1, historyEntry2", + | "ror_detailed_reason" : "default", + | "ror_involved_indices" : [], + | "ror_final_state" : "FORBIDDEN" + | } + |}""".stripMargin + val actualJson = serializedResponse.flatMap(circeJsonWithIgnoredTimestamp) + val expectedJson = circeJsonWithIgnoredTimestamp(new JSONObject(expectedJsonStr)) + actualJson should be(expectedJson) + } "deprecated custom serializer is set" in { val config = rorConfigFromUnsafe( """ @@ -2022,6 +2129,21 @@ class AuditSettingsTests extends AnyWordSpec with Inside { } } + private def circeJsonWithIgnoredTimestamp(json: JSONObject): Option[Json] = { + json + .withTimestampValue("IGNORED") + .circeJsonE + .toOption + } + + extension (jsonObject: JSONObject) { + private def withTimestampValue(value: String): JSONObject = { + jsonObject.put("@timestamp", value) + } + private def circeJsonE: Either[String, Json] = + parser.parse(jsonObject.toString(0)).left.map(_.getMessage) + } + } private class TestEnvironmentAwareAuditLogSerializer extends EnvironmentAwareAuditLogSerializer { @@ -2037,39 +2159,39 @@ private class TestEnvironmentAwareAuditLogSerializer extends EnvironmentAwareAud private class DummyAuditRequestContext(override val loggedInUserName: Option[String] = Some("logged_user"), override val attemptedUserName: Option[String] = Some("basic auth user")) extends AuditRequestContext { - override def timestamp: Instant = Instant.now() + override def timestamp: Instant = Instant.now().minusSeconds(5) - override def id: String = "" + override def id: String = "trace_id_123" - override def correlationId: String = "" + override def correlationId: String = "corr_id_123" - override def indices: Set[String] = Set.empty + override def indices: Set[String] = Set("a1", "a2", "b1", "b2") - override def action: String = "" + override def action: String = "cluster:internal_ror/user_metadata/get" - override def headers: Map[String, String] = Map.empty + override def headers: Map[String, String] = Map("HEADER1" -> "HVALUE1", "HEADER2" -> "HVALUE2") - override def requestHeaders: Headers = Headers(Map.empty) + override def requestHeaders: Headers = Headers(headers.view.mapValues(v => Set(v)).toMap) - override def uriPath: String = "" + override def uriPath: String = "/path/to/resource" - override def history: String = "" + override def history: String = "historyEntry1, historyEntry2" - override def content: String = "" + override def content: String = "Full content of the request" - override def contentLength: Integer = 0 + override def contentLength: Integer = 123 - override def remoteAddress: String = "" + override def remoteAddress: String = "192.168.0.123" - override def localAddress: String = "" + override def localAddress: String = "192.168.0.124" - override def `type`: String = "" + override def `type`: String = "RRTestConfigRequest" - override def taskId: Long = 0 + override def taskId: Long = 123 - override def httpMethod: String = "" + override def httpMethod: String = "GET" - override def impersonatedByUserName: Option[String] = None + override def impersonatedByUserName: Option[String] = Some("impersonated_by_user") override def involvesIndices: Boolean = false diff --git a/gradle.properties b/gradle.properties index 41d51912bf..f5edcb86f9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ publishedPluginVersion=1.67.1 -pluginVersion=1.68.0-pre6 +pluginVersion=1.68.0-pre7 pluginName=readonlyrest org.gradle.jvmargs=-Xmx6144m 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 2c80bbd4ae..674093d299 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 @@ -22,7 +22,7 @@ import tech.beshu.ror.integration.utils.SingletonPluginTestSupport import tech.beshu.ror.utils.containers.ElasticsearchNodeDataInitializer import tech.beshu.ror.utils.containers.providers.ClientProvider import tech.beshu.ror.utils.elasticsearch.BaseManager.JSON -import tech.beshu.ror.utils.elasticsearch.{ElasticsearchTweetsInitializer, IndexManager} +import tech.beshu.ror.utils.elasticsearch.{AuditIndexManager, ElasticsearchTweetsInitializer, IndexManager} import tech.beshu.ror.utils.misc.Resources.getResourceContent import tech.beshu.ror.utils.misc.Version @@ -206,6 +206,61 @@ class LocalClusterAuditingToolsSuite ) updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") } + "using ECS serializer" in { + val indexManager = new IndexManager(basicAuthClient("username", "dev"), esVersionUsed) + // We need to create a new index with a different name for this test, because the ECS schema + // is not compatible with the Json object created by other serializers in previous tests. + val ecsAuditIndexName = "ecs_audit_index" + updateRorConfig( + replacements = Map( + """type: "static"""" -> """type: "ecs"""", + "audit_index" -> ecsAuditIndexName, + ) + ) + val auditIndexManager = new AuditIndexManager(destNodeClientProvider.adminClient, esVersionUsed, ecsAuditIndexName) + performAndAssertExampleSearchRequest(indexManager) + eventually { + val auditEntries = auditIndexManager.getEntries.force().jsons + auditEntries.exists { entry => + // ecs + entry("ecs")("version").str == "1.6.0" && + // trace + entry("trace")("id").strOpt.isDefined && + // timestamp (exists, not verified for exact value) + entry("@timestamp").strOpt.isDefined && + // destination + entry("destination")("address").strOpt.isDefined && + // source + entry("source")("address").strOpt.isDefined && + // http request + entry("http")("request")("method").str == "GET" && + entry("http")("request")("body")("bytes").num == 0 && + entry("http")("request")("body")("content").str == "" && + // event + entry("event")("id").strOpt.isDefined && + entry("event")("duration").numOpt.isDefined && + entry("event")("action").str == "indices:admin/get" && + entry("event")("reason").str == "GetIndexRequest" && + entry("event")("outcome").str == "success" && + // error (empty object) + entry("error").obj.isEmpty && + // user + entry("user")("name").str == "username" && + entry("user")("effective").obj.isEmpty && + // url + entry("url")("path").str == "/twitter/" && + // labels + entry("labels")("es_cluster_name").str == "ROR_SINGLE" && + entry("labels")("es_node_name").str == "ROR_SINGLE_1" && + entry("labels")("es_task_id").numOpt.isDefined && + entry("labels")("ror_involved_indices").arrOpt.isDefined && + entry("labels")("ror_acl_history").str == "[CONTAINER ADMIN-> RULES:[auth_key->false] RESOLVED:[indices=twitter]], [Rule 1-> RULES:[auth_key->true, methods->true, indices->true] RESOLVED:[user=username;indices=twitter]]" && + entry("labels")("ror_final_state").str == "ALLOWED" && + entry("labels")("ror_detailed_reason").str == "{ name: 'Rule 1', policy: ALLOW, rules: [auth_key, methods, indices]" + } shouldBe true + } + updateRorConfigToUseSerializer("tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1") + } } } @@ -219,9 +274,14 @@ class LocalClusterAuditingToolsSuite newString = s"""class_name: "$serializer"""" ) - private def updateRorConfig(originalString: String, newString: String) = { + private def updateRorConfig(originalString: String, newString: String): Unit = + updateRorConfig(Map(originalString -> newString)) + + private def updateRorConfig(replacements: Map[String, String]): Unit = { val initialConfig = getResourceContent(rorConfigFileName) - val modifiedConfig = initialConfig.replace(originalString, newString) + val modifiedConfig = replacements.foldLeft(initialConfig) { case (soFar, (originalString, newString)) => + soFar.replace(originalString, newString) + } rorApiManager.updateRorInIndexConfig(modifiedConfig).forceOKStatusOrConfigAlreadyLoaded() rorApiManager.reloadRorConfig().force() }