As a useful utility to use this audit event structure, a gin-based middleware structure is available:
ginaudit.Middleware
. This structure allows one to set gin routes to log audit events to a
specified io.Writer
via the aforementioned auditevent.EventWriter
structure.
One would create a ginaudit.Middleware
instance as follows:
mdw := ginaudit.NewMiddleware("my-test-component", eventwriter)
Given that JSON is a reasonable default, a utility function that defaults to using a JSON writer was implemented:
mdw := ginaudit.NewJSONMiddleware("my-test-component", writer)
Here, writer
is an instance of an structure that implements the io.Writer
interface.
It is often the case that one must not start to process events until the audit logging capabilities are set up. For this, the following pattern is suggested:
fd, err := helpers.OpenAuditLogFileUntilSuccess(auditLogPath)
if err != nil {
panic(err)
}
// The file descriptor shall be closed only if the gin server is shut down
defer fd.Close()
// Set up middleware with the file descriptor
mdw := ginaudit.NewJSONMiddleware("my-test-component", fd)
The function helpers.OpenAuditLogFileUntilSuccess
attempts to open the audit log
file, and will block until it's available. This file may be created beforehand or it
may be created by another process e.g. a sidecar container. It opens the file with
O_APPEND
which enables atomic writes as long as the audit events are less than 4096 bytes.
Now that we have a middleware instance available, it's a matter of taking it into
use in our gin Router
:
// Get router instance
r := gin.New(...)
// Add middleware
r.Use(mdw.Audit())
// ... All paths after the middleware addition will issue
// audit events
r.GET("/", myGetHandler)
Since it's standard gin middleware, it's also possible to set it up per handler:
r.GET("/foo", mdw.Audit() myGetFooHandler)
When using the gin middleware, the audit ID will be added to the context and available for use in subsequent handlers in the chain.
auditID := c.GetString(mdw.AuditIDContextKey)
Additional audit data can be passed down to the audit middleware via the context key
AuditDataContextKey
. This can be leveraged to enrich the audit events with
diff information or other data for forensic analysis. The value is expected to be
a *json.RawMessage
and you are responsible for ensuring proper JSON structure.
// add additional data to context to be logged in the
// audit event by the middleware
mydata := json.RawMessage(`{"foo":"bar"}`)
c.Set(mdw.AuditDataContextKey, &mydata)
Audit event types identify the action that happened on a given request.
By default, the event type will take the following form: <HTTP Method>:<Path>
.
It is often a best practice to have human readable names, and to have an exhaustive
list of event types that your application may produce. So, in order to
register a type and tie it to a handler, the RegisterEventType
function is available.
It may be used as follows:
// Add middleware
r.Use(mdw.Audit())
// ... All paths after the middleware addition will issue
// audit events
mdw.RegisterEventType("ListFoos", http.MethodGet, "/foo")
r.GET("/foo", myGetHandler)
mdw.RegisterEventType("CreateFoo", http.MethodPost, "/foo")
r.POST("/foo", myGetHandler)
It's also possible to both set the audit middleware for a specific path and set a specific audit event type for the path:
router.GET("/user/:name", mdw.AuditWithType("GetUserInfo"), userInfoHandler)
NOTE: It is not recommended to assign a default or shared event type to all events as audit events need to be uniquely identifiable actions.
By default, the audit events generated by this middleware will be created with the following heuristics:
-
For HTTP statuses 500 and above:
failed
-
For HTTP statuses 400 and above, but below 500:
denied
-
Anything else:
succeeded
If this pattern isn't appropriate for your server logic, it's possible to overwrite the default outcome handler as follows:
// Create middleware instance
mdw := ginaudit.NewMiddleware("my-test-component", eventwriter)
// Overwrite handler. This one always succeeds
mdw.WithOutcomeHandler(func(c *gin.Context) string {
return auditevent.OutcomeSucceeded
})
NOTE: It's recommended to not add random strings as an outcome, as one normally wants predictable strings here. If a custom outcome is desired, make sure to document it well.
By default, the audit events generated by this middleware will be created with subjects parsed by the ginjwt middleware.
jwt.subject
will be assigned to thesub
key if it exists, otherwiseUnknown
will be set.jwt.user
will be assigned to theuser
key if it exists, otherwise the middleware will look for theX-User-Id
header. If neither is found,Unknown
will be set.
If this pattern isn't appropriate for your application, it's possible to override the subject handler as follows:
// Create middleware instance
mdw := ginaudit.NewMiddleware("my-test-component", eventwriter)
// Overwrite handler with a dummy handler
mdw.WithSubjectHandler(func(c *gin.Context) map[string]string {
return map[string]string{"foo": "bar"}
})
ginaudit.Middleware
instances may generate metrics for events and errors.
For more information, see the metrics documentation.