Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions audit/src/main/scala/tech/beshu/ror/audit/AuditFieldValue.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private[ror]
Is it enough?
When we extend the sealed trait values set in the future, are we sure that users won't have to recompile their custom serializers?


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

}
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again, I was fooled by the name: AuditFieldValue ;)
For me, it's rather AuditFieldValueDescriptor. Because, base on this description and the available data, we create value for the audit event document's filed.

TBH, the requestContext, finalState, ..., error values should be gathered in some container for a sake of readability:

private def resolve(valueDescriptor: AuditFieldValueDescriptor, eventData: EventData): Any

but introducing and creating a new type for a sake of internal implementation may not be a base choice. That's why I propose to do it like this:

private def resolver(matched: Boolean,
                          finalState: String,
                          reason: String,
                          duration: FiniteDuration,
                          requestContext: AuditRequestContext,
                          environmentContext: Option[AuditEnvironmentContext],
                          error: Option[Throwable]): AuditFieldValueDescriptor => Any

Now, it's pretty clear that you create a resolver function to get value from the descriptor.

WDYT?

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT?

sealed trait AllowedEventMode
object AllowedEventMode {
  case object IncludeAll extends AllowedEventMode
  case object Include(eventTypes: NonEmptyList[EventType]) extends AllowedEventMode  
}

The current version is too concreted. It'd be hard to maintain backward compatibility using it in the future.


object AllowedEventSerializationMode {
case object SerializeOnlyAllowedEventsWithInfoLevelVerbose extends AllowedEventSerializationMode

case object SerializeAllAllowedEvents extends AllowedEventSerializationMode
}

final case class AuditFieldName(value: String)

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,16 @@
package tech.beshu.ror.audit

import org.json.JSONObject
import tech.beshu.ror.audit.AuditSerializationHelper.AuditFieldName

trait EnvironmentAwareAuditLogSerializer {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found no implementation of this serializer.
Now, I'm not sure if we still support the changes described in RORDEV-1414.

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
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,82 +17,47 @@
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 java.time.ZoneId
import java.time.format.DateTimeFormatter
import scala.collection.JavaConverters._
import scala.concurrent.duration.FiniteDuration
import tech.beshu.ror.audit._
import tech.beshu.ror.audit.AuditSerializationHelper.{AllowedEventSerializationMode, AuditFieldName}
import tech.beshu.ror.audit.instances.DefaultAuditLogSerializerV1.defaultV1AuditFields

class DefaultAuditLogSerializerV1 extends AuditLogSerializer {

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)))
}
override def onResponse(responseContext: AuditResponseContext): Option[JSONObject] =
AuditSerializationHelper.serialize(
responseContext = responseContext,
environmentContext = None,
fields = defaultV1AuditFields,
allowedEventSerializationMode = AllowedEventSerializationMode.SerializeOnlyAllowedEventsWithInfoLevelVerbose
)

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[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
)
}
Loading