Apereo CAS uses the Inspektr framework for all audit-related functionality. Inspektr is a very small library designed to capture and record the pieces of runtime information that attempt to answer questions like “who did what to which resource, when and how?” and in this post, we will review a few strategies that allow one to control and format the output of the audit log in Apereo CAS.
Our starting position is as follows:
7.0.x
21
Audited records are typically sent to the CAS logging framework, which in turn and by default may decide to log output to the console. A basic example of an audited record in the logs, condensed to one line, would be:
2023-07-15T10:48:10.802814875|casuser|\
{service=https://localhost:9859/anything/1, return=ST-1-********Olip7NI-fv-az407-374}| \
SERVICE_TICKET_CREATED|0:0:0:0:0:0:0:1|0:0:0:0:0:0:0:1
While this might seem reasonable enough, there are of course options that allow one to take full control of the audit output and its formatting. One such option is to render and build the output via Groovy.
Groovy-based audits can receive and process the auditable context parameters and build the final auditable record in any text format or representation. The final auditable record is then passed to the logging framework, typically tagged under INFO
.
Let’s start by disabling the default log-based audit trail manager and enabling Groovy-based audits:
cas.audit.slf4j.enabled=false
cas.audit.groovy.template.location=file:/path/to/GroovyAuditor.groovy
A basic template for the Groovy script tasked to build the audit output might be:
who: ${who}, what: ${what}, when: ${when}, ip: ${clientIpAddress}
The fields that are available in the script should hopefully be self-explanatory. Note that the script above has very basic scripting abilities and it is processed by Groovy’s SimpleTemplateEngine
. This component processes template source files substituting variables and expressions into placeholders in a template source text to produce the desired output. The template engine uses JSP style <% %>
script and <%= %>
expression syntax or GString style expressions.
A fancier (though certainly uglier) version of the script could attempt to geolocate IP addresses and have the result recorded in the final audit log. This option would require a valid subscription to a geolocation service such as MaxMind.
According to the MaxMind website:
MaxMind GeoIP2 offerings identify the location and other characteristics of Internet users for a wide range of applications including content personalization, fraud detection, ad targeting, traffic analysis, compliance, geo-targeting, geo-fencing, and digital rights management.
To activate this variant of session pinning, one would need to tweak a CAS build with the right extension module and configure CAS to integrate with MaxMind. Then, we move on with the script:
who: ${who},
what: ${what},
when: ${when},
ip: ${
org.apereo.cas.util.spring.ApplicationContextProvider
.getApplicationContext()
.getBean(org.apereo.cas.authentication.adaptive.geo.GeoLocationService.BEAN_NAME,
org.apereo.cas.authentication.adaptive.geo.GeoLocationService.class)
.locate(clientIpAddress)?.build()
}
It of course does the job to some extent, but looks fairly ugly to me! Dynamically accessing CAS internal components inside a Groovy script typically leads to long-term maintenance issues, especially when/if such components move around and get refactored in the CAS codebase. Execution failures remain somewhat unknown and silent until runtime when the feature is exercised and this might remain unnoticed for a while.
A safer though slightly heavy-handed option for this task would be to house the logic inside a ClientInfoResolver
component:
@Bean
public ClientInfoResolver casAuditClientInfoResolver(GeoLocationService service) {
return new MyClientInfoResolver(service);
}
Once the bean is correctly registered with the Spring application context, your implementation should be able to safely geolocate client IP addresses:
@RequiredArgsConstructor
public class MyClientInfoResolver extends DefaultClientInfoResolver {
private final GeoLocationService geoLocationService;
@Override
public ClientInfo resolveClientInfo(ClientInfo clientInfo) {
var geolocation = geoLocationService.locate(clientInfo.getClientIpAddress()).build();
return clientInfo.include("geo", geolocation);
}
}
You get the idea.
If you have questions about the contents and the topic of this blog post, or if you need additional guidance and support, feel free to send us a note and ask about consulting and support services.
I hope this review was of some help to you and I am sure that both this post as well as the functionality it attempts to explain can be improved in any number of ways. Please feel free to engage and contribute as best as you can.
Happy Coding,
Monday-Friday
9am-6pm, Central European Time
7am-1pm, U.S. Eastern Time
Monday-Friday
9am-6pm, Central European Time