-
Notifications
You must be signed in to change notification settings - Fork 165
[RORDEV-1481] New audit serializers, including fully configurable serializer #1140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
549a1b4
9054992
058639b
2fa284c
4ef563b
ab6a55c
ac12fea
003c334
b39f217
1c65e81
5cc3848
f063e8d
083050f
b3e287d
5b5cbc8
0e598b0
ff9347c
1cb7be1
0ab38e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
|
||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once again, I was fooled by the name: TBH, the 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYT?
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 |
---|---|---|
|
@@ -17,8 +17,16 @@ | |
package tech.beshu.ror.audit | ||
|
||
import org.json.JSONObject | ||
import tech.beshu.ror.audit.AuditSerializationHelper.AuditFieldName | ||
|
||
trait EnvironmentAwareAuditLogSerializer { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I found no implementation of this serializer. |
||
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 | ||
) | ||
} |
There was a problem hiding this comment.
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?