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 38d0b323b5..f826bcc1cb 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 @@ -95,6 +95,8 @@ private[ror] object AuditSerializationHelper { case AuditFieldValueDescriptor.FinalState => eventData.finalState case AuditFieldValueDescriptor.Reason => eventData.reason case AuditFieldValueDescriptor.User => SerializeUser.serialize(requestContext).orNull + case AuditFieldValueDescriptor.LoggedUser => requestContext.loggedInUserName.orNull + case AuditFieldValueDescriptor.PresentedIdentity => requestContext.attemptedUserName.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 @@ -166,8 +168,13 @@ private[ror] object AuditSerializationHelper { 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") case object User extends AuditFieldValueDescriptor + case object LoggedUser extends AuditFieldValueDescriptor + + case object PresentedIdentity extends AuditFieldValueDescriptor + case object ImpersonatedByUser extends AuditFieldValueDescriptor case object Action extends AuditFieldValueDescriptor @@ -258,6 +265,8 @@ private[ror] object AuditSerializationHelper { 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, diff --git a/audit/src/test/scala/tech/beshu/ror/audit/SerializerTest.scala b/audit/src/test/scala/tech/beshu/ror/audit/SerializerTest.scala index ec7e399338..dd2b3f3762 100644 --- a/audit/src/test/scala/tech/beshu/ror/audit/SerializerTest.scala +++ b/audit/src/test/scala/tech/beshu/ror/audit/SerializerTest.scala @@ -50,6 +50,8 @@ class SerializerTest extends AnyWordSpec { "headers" -> "array", "path" -> "string", "user" -> "string", + "logged_user" -> "string", + "presented_identity" -> "string", "action" -> "string", "indices" -> "array", "acl_history" -> "string", @@ -80,6 +82,8 @@ class SerializerTest extends AnyWordSpec { "headers" -> "array", "path" -> "string", "user" -> "string", + "logged_user" -> "string", + "presented_identity" -> "string", "action" -> "string", "indices" -> "array", "acl_history" -> "string", @@ -110,6 +114,8 @@ class SerializerTest extends AnyWordSpec { "headers" -> "array", "path" -> "string", "user" -> "string", + "logged_user" -> "string", + "presented_identity" -> "string", "action" -> "string", "indices" -> "array", "acl_history" -> "string", @@ -138,6 +144,8 @@ class SerializerTest extends AnyWordSpec { "headers" -> "array", "path" -> "string", "user" -> "string", + "logged_user" -> "string", + "presented_identity" -> "string", "action" -> "string", "indices" -> "array", "acl_history" -> "string", @@ -168,6 +176,8 @@ class SerializerTest extends AnyWordSpec { "headers" -> "array", "path" -> "string", "user" -> "string", + "logged_user" -> "string", + "presented_identity" -> "string", "action" -> "string", "indices" -> "array", "acl_history" -> "string", @@ -197,6 +207,8 @@ class SerializerTest extends AnyWordSpec { "headers" -> "array", "path" -> "string", "user" -> "string", + "logged_user" -> "string", + "presented_identity" -> "string", "action" -> "string", "indices" -> "array", "acl_history" -> "string", @@ -226,6 +238,8 @@ class SerializerTest extends AnyWordSpec { "headers" -> "array", "path" -> "string", "user" -> "string", + "logged_user" -> "string", + "presented_identity" -> "string", "action" -> "string", "indices" -> "array", "acl_history" -> "string", @@ -255,6 +269,8 @@ class SerializerTest extends AnyWordSpec { "headers" -> "array", "path" -> "string", "user" -> "string", + "logged_user" -> "string", + "presented_identity" -> "string", "action" -> "string", "indices" -> "array", "acl_history" -> "string", @@ -331,7 +347,7 @@ private object DummyAuditRequestContext extends AuditRequestContext { override def involvesIndices: Boolean = false - override def attemptedUserName: Option[String] = None + override def attemptedUserName: Option[String] = Some("basic_auth_username") override def rawAuthHeader: Option[String] = None 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 e07f1bb7a3..2bc6013968 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 @@ -18,9 +18,10 @@ package tech.beshu.ror.accesscontrol.audit.configurable import cats.parse.{Parser0, Parser as P} import cats.syntax.list.* +import org.apache.logging.log4j.scala.Logging import tech.beshu.ror.audit.utils.AuditSerializationHelper.AuditFieldValueDescriptor -object AuditFieldValueDescriptorParser { +object AuditFieldValueDescriptorParser extends Logging { private val lbrace = P.char('{') private val rbrace = P.char('}') @@ -59,7 +60,16 @@ object AuditFieldValueDescriptorParser { case "IS_MATCHED" => Some(AuditFieldValueDescriptor.IsMatched) case "FINAL_STATE" => Some(AuditFieldValueDescriptor.FinalState) case "REASON" => Some(AuditFieldValueDescriptor.Reason) - case "USER" => Some(AuditFieldValueDescriptor.User) + case "USER" => + logger.warn( + """The USER audit value placeholder is deprecated and should not be used in the configurable audit log serializer. + |Please use LOGGED_USER or PRESENTED_IDENTITY instead. Check the list of available placeholders in the documentation: + |https://docs.readonlyrest.com/elasticsearch/audit#using-configurable-serializer. + |""".stripMargin + ) + Some(AuditFieldValueDescriptor.User) + case "LOGGED_USER" => Some(AuditFieldValueDescriptor.LoggedUser) + case "PRESENTED_IDENTITY" => Some(AuditFieldValueDescriptor.PresentedIdentity) case "IMPERSONATED_BY_USER" => Some(AuditFieldValueDescriptor.ImpersonatedByUser) case "ACTION" => Some(AuditFieldValueDescriptor.Action) case "INVOLVED_INDICES" => Some(AuditFieldValueDescriptor.InvolvedIndices) 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 61f35fd39c..a88c89f148 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 @@ -514,7 +514,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { expectedAuditCluster = LocalAuditCluster ) } - "custom serializer is set" in { + "QueryAuditLogSerializer serializer is set" in { val config = rorConfigFromUnsafe( """ |readonlyrest: @@ -538,6 +538,72 @@ class AuditSettingsTests extends AnyWordSpec with Inside { expectedAuditCluster = LocalAuditCluster ) } + "QueryAuditLogSerializer serializer is set and correctly serializes event without logged user" in { + val config = rorConfigFromUnsafe( + """ + |readonlyrest: + | audit: + | enabled: true + | outputs: + | - type: index + | serializer: "tech.beshu.ror.audit.instances.QueryAuditLogSerializer" + | + | access_control_rules: + | + | - name: test_block + | type: allow + | auth_key: admin:container + | + """.stripMargin) + + assertIndexBasedAuditSinkSettingsPresent[QueryAuditLogSerializer]( + config, + expectedIndexName = "readonlyrest_audit-2018-12-31", + expectedAuditCluster = LocalAuditCluster + ) + val createdSerializer = serializer(config) + val serializedResponse = createdSerializer.onResponse( + AuditResponseContext.Forbidden(new DummyAuditRequestContext(loggedInUserName = None, attemptedUserName = None)) + ) + + serializedResponse shouldBe defined + serializedResponse.get.get("user") shouldBe "Bearer 123" + serializedResponse.get.isNull("presented_identity") + serializedResponse.get.isNull("logged_user") + } + "QueryAuditLogSerializer serializer is set and correctly serializes event with logged user" in { + val config = rorConfigFromUnsafe( + """ + |readonlyrest: + | audit: + | enabled: true + | outputs: + | - type: index + | serializer: "tech.beshu.ror.audit.instances.QueryAuditLogSerializer" + | + | access_control_rules: + | + | - name: test_block + | type: allow + | auth_key: admin:container + | + """.stripMargin) + + assertIndexBasedAuditSinkSettingsPresent[QueryAuditLogSerializer]( + config, + expectedIndexName = "readonlyrest_audit-2018-12-31", + expectedAuditCluster = LocalAuditCluster + ) + val createdSerializer = serializer(config) + val serializedResponse = createdSerializer.onResponse( + AuditResponseContext.Forbidden(new DummyAuditRequestContext(loggedInUserName = Some("my_user"))) + ) + + serializedResponse shouldBe defined + serializedResponse.get.get("user") shouldBe "my_user" + serializedResponse.get.get("presented_identity") shouldBe "basic auth user" + serializedResponse.get.get("logged_user") shouldBe "my_user" + } "custom environment-aware serializer is set and correctly serializes events" in { val config = rorConfigFromUnsafe( """ @@ -562,7 +628,7 @@ class AuditSettingsTests extends AnyWordSpec with Inside { expectedAuditCluster = LocalAuditCluster ) val createdSerializer = serializer(config) - val serializedResponse = createdSerializer.onResponse(AuditResponseContext.Forbidden(DummyAuditRequestContext)) + val serializedResponse = createdSerializer.onResponse(AuditResponseContext.Forbidden(new DummyAuditRequestContext)) serializedResponse shouldBe defined serializedResponse.get.get("custom_field_for_es_node_name") shouldBe "testEsNode" @@ -841,13 +907,13 @@ class AuditSettingsTests extends AnyWordSpec with Inside { "ES version is greater than or equal 7.9.0" in { val esVersions = List( - EsVersion(8,17,0), - EsVersion(8,1,0), - EsVersion(8,0,0), - EsVersion(7,17,27), - EsVersion(7,10,0), - EsVersion(7,9,1), - EsVersion(7,9,0), + EsVersion(8, 17, 0), + EsVersion(8, 1, 0), + EsVersion(8, 0, 0), + EsVersion(7, 17, 27), + EsVersion(7, 10, 0), + EsVersion(7, 9, 1), + EsVersion(7, 9, 0), ) val config = rorConfigFromUnsafe( @@ -1969,7 +2035,8 @@ private class TestEnvironmentAwareAuditLogSerializer extends EnvironmentAwareAud } -private object DummyAuditRequestContext extends AuditRequestContext { +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 id: String = "" @@ -2002,15 +2069,11 @@ private object DummyAuditRequestContext extends AuditRequestContext { 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 rawAuthHeader: Option[String] = Some("Bearer 123") override def generalAuditEvents: JSONObject = new JSONObject diff --git a/gradle.properties b/gradle.properties index 1f9b4a9b56..0af616778d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ publishedPluginVersion=1.67.0 -pluginVersion=1.67.1 +pluginVersion=1.68.0-pre5 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 c7f616c985..123c60730b 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 @@ -80,6 +80,8 @@ class LocalClusterAuditingToolsSuite auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && + entry("logged_user").str == "username" && + entry("presented_identity").str == "username" && entry("block").str.contains("name: 'Rule 1'") && entry.obj.get("es_node_name").isEmpty && entry.obj.get("es_cluster_name").isEmpty @@ -87,6 +89,8 @@ class LocalClusterAuditingToolsSuite auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && + entry("logged_user").str == "username" && + entry("presented_identity").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") @@ -109,6 +113,8 @@ class LocalClusterAuditingToolsSuite auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && + entry("logged_user").str == "username" && + entry("presented_identity").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") && @@ -136,6 +142,8 @@ class LocalClusterAuditingToolsSuite auditEntries.exists(entry => entry("final_state").str == "ALLOWED" && entry("user").str == "username" && + entry("logged_user").str == "username" && + entry("presented_identity").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") &&