Model audit events and test deserialization
Modeled SshAuditEvent, HttpAuditEvent and ExtendedHttpAuditEvent
and tested ability to deserialize json strings into case classes
Feature: Issue 9866
Change-Id: I8dc738b9300e6a24afd97ac8dfde96ec7fd02be1
diff --git a/auditlog/src/main/scala/com/gerritforge/analytics/auditlog/model/AuditEvent.scala b/auditlog/src/main/scala/com/gerritforge/analytics/auditlog/model/AuditEvent.scala
new file mode 100644
index 0000000..c577e78
--- /dev/null
+++ b/auditlog/src/main/scala/com/gerritforge/analytics/auditlog/model/AuditEvent.scala
@@ -0,0 +1,77 @@
+package com.gerritforge.analytics.auditlog.model
+import org.json4s.native.JsonMethods._
+import org.json4s.{DefaultFormats, _}
+
+import scala.util.{Failure, Success, Try}
+
+object AuditEvent {
+
+ implicit private val formats = DefaultFormats
+
+ def fromJsonString(json: String): Try[AuditEvent] = {
+ Try(parse(json)).flatMap { jsValueEvent =>
+ jsValueEvent \ "type" match {
+ case JString(eventType) if eventType == "HttpAuditEvent" =>
+ Success((jsValueEvent \ "event").camelizeKeys.extract[HttpAuditEvent])
+ case JString(eventType) if eventType == "ExtendedHttpAuditEvent" =>
+ Success((jsValueEvent \ "event").camelizeKeys.extract[ExtendedHttpAuditEvent])
+ case JString(eventType) if eventType == "SshAuditEvent" =>
+ Success((jsValueEvent \ "event").camelizeKeys.extract[SshAuditEvent])
+ case _ => Failure(new MappingException(s"Could not parse json into an audit event: $json"))
+ }
+ }
+ }
+}
+
+sealed trait AuditEvent {
+ def sessionId: String
+ def timeAtStart: Long
+ def what: String
+ def elapsed: Int
+ def uuid: AuditUUID
+}
+
+final case class SshAuditEvent(
+ sessionId: String,
+ who: Option[CurrentUser],
+ timeAtStart: Long,
+ what: String,
+ elapsed: Int,
+ uuid: AuditUUID
+) extends AuditEvent
+
+sealed trait BaseHttpAuditEvent extends AuditEvent {
+ def httpMethod: String
+ def httpStatus: Int
+ def who: CurrentUser
+}
+
+final case class HttpAuditEvent(
+ httpMethod: String,
+ httpStatus: Int,
+ sessionId: String,
+ who: CurrentUser,
+ timeAtStart: Long,
+ what: String,
+ elapsed: Int,
+ uuid: AuditUUID
+) extends BaseHttpAuditEvent
+
+final case class ExtendedHttpAuditEvent(
+ httpMethod: String,
+ httpStatus: Int,
+ sessionId: String,
+ who: CurrentUser,
+ timeAtStart: Long,
+ what: String,
+ elapsed: Int,
+ uuid: AuditUUID
+) extends BaseHttpAuditEvent
+
+final case class CurrentUser(
+ accessPath: String,
+ accountId: Option[AccountId]
+)
+
+final case class AccountId(id: Int)
+final case class AuditUUID(uuid: String)
\ No newline at end of file
diff --git a/auditlog/src/test/scala/com/gerritforge/analytics/auditlog/model/AuditEventSpec.scala b/auditlog/src/test/scala/com/gerritforge/analytics/auditlog/model/AuditEventSpec.scala
new file mode 100644
index 0000000..a6ec23d
--- /dev/null
+++ b/auditlog/src/test/scala/com/gerritforge/analytics/auditlog/model/AuditEventSpec.scala
@@ -0,0 +1,212 @@
+package com.gerritforge.analytics.auditlog.model
+import org.json4s.MappingException
+import org.json4s.ParserUtil.ParseException
+import org.scalatest.{FlatSpec, Inside, Matchers}
+import org.scalatest.TryValues._
+
+class AuditEventSpec extends FlatSpec with Matchers with Inside {
+
+ "parsing a string that is not json" should "result in a ParseException failure" in {
+ val someJson = """this_is_not_a_valid_json"""
+ AuditEvent.fromJsonString(someJson).failure.exception shouldBe a[ParseException]
+ }
+
+ "parsing a json not representing an audit event log" should "result in a MappingException failure" in {
+ val someJson = """{"some": "json", "not": "expected"}"""
+
+ AuditEvent.fromJsonString(someJson).failure.exception shouldBe a[MappingException]
+ }
+
+ "A json representing an http audit event log" should "be parsed into a success of HttpAuditEvent" in {
+
+ val httpMethod = "POST"
+ val httpStatus = 200
+ val sessionId = "1r7ywi4vd3jk410dv60pvd19vk"
+ val accountId = 1009124
+ val accessPath = "GIT"
+ val timeAtStart = 1542240322369L
+ val what = "https://review.gerrithub.io/Mirantis/tcp-qa/git-upload-pack"
+ val elapsed = 12
+ val auditUUID = "audit:fe4cff68-d094-474a-9d97-502270b0b2e6"
+ val jsonEvent =
+ s"""
+ |{
+ | "type": "HttpAuditEvent",
+ | "event": {
+ | "http_method": "$httpMethod",
+ | "http_status": $httpStatus,
+ | "session_id": "$sessionId",
+ | "who": {
+ | "account_id": {
+ | "id": $accountId
+ | },
+ | "real_user": {
+ | "account_id": {
+ | "id": $accountId
+ | },
+ | "access_path": "UNKNOWN",
+ | "last_login_external_id_property_key": {}
+ | },
+ | "access_path": "$accessPath",
+ | "last_login_external_id_property_key": {}
+ | },
+ | "when": $timeAtStart,
+ | "what": "$what",
+ | "params": {},
+ | "time_at_start": $timeAtStart,
+ | "elapsed": $elapsed,
+ | "uuid": {
+ | "uuid": "$auditUUID"
+ | }
+ | }
+ |}
+ """.stripMargin
+
+ val triedEvent = AuditEvent.fromJsonString(jsonEvent)
+ inside (triedEvent.success.value) {
+ case HttpAuditEvent(gotHttpMethod, gotHttpStatus, gotSessionId, gotWho, gotTimeAtStart, gotWhat, gotElapsed, gotUUID) =>
+ gotHttpMethod shouldBe httpMethod
+ gotHttpStatus shouldBe httpStatus
+ gotSessionId shouldBe sessionId
+ gotWho shouldBe CurrentUser(accessPath = accessPath, accountId = Some(AccountId(accountId)))
+ gotTimeAtStart shouldBe timeAtStart
+ gotWhat shouldBe what
+ gotElapsed shouldBe elapsed
+ gotUUID.uuid shouldBe auditUUID
+ }
+ }
+
+ "A json representing a SSH audit event log" should "be parsed into a success of SshAuditEvent" in {
+
+ val sessionId = "2adc5bef"
+ val accountId = 1009124
+ val accessPath = "SSH_COMMAND"
+ val timeAtStart = 1542240322369L
+ val what = "gerrit.stream-events.-s.patchset-created.-s.change-restored.-s.comment-added"
+ val elapsed = 12
+ val auditUUID = "audit:dd74e098-9260-4720-9143-38a0a0a5e500"
+ val jsonEvent =
+ s"""
+ |{
+ | "type": "SshAuditEvent",
+ | "event": {
+ | "session_id": "$sessionId",
+ | "who": {
+ | "account_id": {
+ | "id": $accountId
+ | },
+ | "access_path": "$accessPath",
+ | "last_login_external_id_property_key": {}
+ | },
+ | "when": $timeAtStart,
+ | "what": "$what",
+ | "params": {},
+ | "result": "0",
+ | "time_at_start": $timeAtStart,
+ | "elapsed": $elapsed,
+ | "uuid": {
+ | "uuid": "$auditUUID"
+ | }
+ | }
+ |}
+ """.stripMargin
+
+ inside (AuditEvent.fromJsonString(jsonEvent).success.value) {
+ case SshAuditEvent(gotSessionId, gotWho, gotTimeAtStart, gotWhat, gotElapsed, gotUUID) =>
+ gotSessionId shouldBe sessionId
+ gotWho shouldBe Some(CurrentUser(accessPath = accessPath, accountId = Some(AccountId(accountId))))
+ gotTimeAtStart shouldBe timeAtStart
+ gotWhat shouldBe what
+ gotElapsed shouldBe elapsed
+ gotUUID.uuid shouldBe auditUUID
+ }
+ }
+
+ "A json representing a failed SSH authentication failure" should "be parsed into a success of SshAuditEvent" in {
+
+ val sessionId = "000000000000000000000000000"
+ val timeAtStart = 1542240154088L
+ val what = "AUTH"
+ val elapsed = 0
+ val auditUUID = "audit:8d40c495-7b51-4003-81f2-718bc04addf3"
+ val jsonEvent =
+ s"""
+ |{
+ | "type": "SshAuditEvent",
+ | "event": {
+ | "session_id": "$sessionId",
+ | "when": 1542240154088,
+ | "what": "$what",
+ | "params": {},
+ | "result": "FAIL",
+ | "time_at_start": $timeAtStart,
+ | "elapsed": $elapsed,
+ | "uuid": {
+ | "uuid": "$auditUUID"
+ | }
+ | }
+ |}
+ """.stripMargin
+
+ inside (AuditEvent.fromJsonString(jsonEvent).success.value) {
+ case SshAuditEvent(gotSessionId, gotWho, gotTimeAtStart, gotWhat, gotElapsed, gotUUID) =>
+ gotSessionId shouldBe sessionId
+ gotWho shouldBe None
+ gotTimeAtStart shouldBe timeAtStart
+ gotWhat shouldBe what
+ gotElapsed shouldBe elapsed
+ gotUUID.uuid shouldBe auditUUID
+ }
+ }
+
+ "A json representing an extended http audit event log" should "be parsed into a success of ExtendedHttpAuditEvent" in {
+
+ val httpMethod = "GET"
+ val httpStatus = 200
+ val sessionId = "aQ3Dprttdq3tT25AMDHhF7zKpMOph64XnW"
+ val accountId = 1011373
+ val accessPath = "REST_API"
+ val timeAtStart = 1542242297450L
+ val what = "/config/server/info"
+ val elapsed = 177
+ val auditUUID = "audit:5f10fea5-35d1-4252-b86f-99db7a9b549b"
+ val jsonEvent =
+ s"""
+ |{
+ | "type": "ExtendedHttpAuditEvent",
+ | "event": {
+ | "http_method": "$httpMethod",
+ | "http_status": $httpStatus,
+ | "session_id": "$sessionId",
+ | "who": {
+ | "account_id": {
+ | "id": $accountId
+ | },
+ | "access_path": "$accessPath",
+ | "last_login_external_id_property_key": {}
+ | },
+ | "when": 1542242297450,
+ | "what": "$what",
+ | "params": {},
+ | "time_at_start": $timeAtStart,
+ | "elapsed": $elapsed,
+ | "uuid": {
+ | "uuid": "$auditUUID"
+ | }
+ | }
+ |}
+ """.stripMargin
+
+ inside (AuditEvent.fromJsonString(jsonEvent).success.value) {
+ case ExtendedHttpAuditEvent(gotHttpMethod, gotHttpStatus, gotSessionId, gotWho, gotTimeAtStart, gotWhat, gotElapsed, gotUUID) =>
+ gotHttpMethod shouldBe httpMethod
+ gotHttpStatus shouldBe httpStatus
+ gotSessionId shouldBe sessionId
+ gotWho shouldBe CurrentUser(accessPath = accessPath, accountId = Some(AccountId(accountId)))
+ gotTimeAtStart shouldBe timeAtStart
+ gotWhat shouldBe what
+ gotElapsed shouldBe elapsed
+ gotUUID.uuid shouldBe auditUUID
+ }
+ }
+}
\ No newline at end of file