Spring Boot Audit Logging

Jotain backendimpää taas vaihteeksi: Projekteissa tulee melkolailla tiheään vaadetta saada aikaan audit loggausta. Vaikkei tulisikaan, se antaa pitkän elinkaaren projekteissa itsellekin mielenrauhaa, että kykenee vastaamaan kysymykseen kuka teki mitä teki milloin teki (miksi teki ei vielä onnistu mutta ehkä IoT avulla sekin ratkaistavissa).

Audit loggausta voi tehdä villistikin eri tavoin ja eri vaatimuksilla. Joissain projekteissa on tultu nähtyä yksinkertainen audit service jota kutsutaan aina tarvittaessa, halutuista paikoista. Tässä on huonoa se, että pitää muistaa kutsua sitä, eli ei ole taattua että suuremmassa projektissa joka koodaaja on laittanut auditit paikalleen, lisäksi se rikkoo DRY periaatetta aika rumasti. Toisaalta on mahdollista tehdä monellakin tapaa filter/interceptor, joka tulee aina väliin ja loggaa vaikka kaiken. Mutta tässä mallissa voi olla ongelmana suuri hälyn määrä, eli voi olla että logi täyttyy tapahtumista jotka eivät ole oikeasti kiinnostavia mutta joita on paljon.

Kirjoittelen tätä blogia koska löysin mielestäni fiksun ratkaisun Spring Frameworkin puolelta, vieläpä Spring Boot yhteensopivana, eli ei xml:ää vaativana. Ratkaisu on fiksu koska se on mukava kompromissi kahdesta mainitusta ääripään tavasta – sisältäen tavallaan molempien huonoja ja hyviä puolia. Mutta ennenkaikkea se on melko kaunis, esteettinen, eikä riko yhtälailla ikävästi DRY periaatetta. Kirjaan näitä ylös myös ennenkaikkea itselleni muistiin, vähentää kivasti tarvittavaa aikaa soveltaa uudelleen, kun on tiedossa testattua luotettavaa ja (tällä hetkellä) ajantasaista tietoa.

Se mitä halusin on oikeastaan mahdollisuus auditoida metoditasolla on-demand, missä haluan. Ei täysautomaattisesti kaikkea, mutta ei myöskään samaa koodia copy-pasteillen joka paikkaan. Lisäksi halusin että voin halutessani määrittää audit eventille nimen, ja/tai kategorian, ja/tai koodin, pelkän metodi/luokannimen sijasta.

Homma lähtee liikkeelle ihan perinteisistä Spring AOP annotaatioista. Eli tarvitaan ensin Spring Boot projekti. Niistä olen kirjaillut jo aiemmin eli en lähde ihan sillä tasolla asiaa avaamaan tällä kertaa. Mutta sen päälle tarvitaan AOP dependency, näin:

 <dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
 <version>${spring.boot.version}</version>
 </dependency>

Ja nyt ollaan jo aika pitkällä 😉 Hyvä huomata että Spring Boot on aika herkkä sille mitä kaikkea automatiikkaa olet kytkenyt päälle, itse olen saanut AOP featuret vahingossa joskus pois päältä esim. väärillä annotaatiolla Application/Configuration-luokassa. Mutta yleisin syy silti AOP toimimattomuuteen on rikkinäiset pointcutit. Joten testataanpa ensin iisisti mahdollisimman lavealla interceptorilla:

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AuditAOP {
@After("execution(* *.*(..))")
 public void logServiceAccess(JoinPoint joinPoint) {
 System.out.println("AuditAOP: Completed : " + joinPoint);
 }
}

Jep, tuossa on AspectJ joinpoint joka tarraa kiinni ihan kaikkeen, niin kauan kuin mennään Springin läpi eli kohteena on Spring-manageroitu komponentti.Tässä kohtaa vain logataan joinpoint. Hyvä katsoa toimiiko, loggaako. Jos loggaa, erinomaista. Tarvittaessa Joinpointilta voidaan louhia lisääkin tietoja:

@After("execution(* *.*(..))")
public void logServiceAccess(JoinPoint joinPoint) {
  System.out.println("AuditAOP: Completed : " + joinPoint);
  Signature signature = joinPoint.getSignature();
  String methodName = signature.getName();
  String arguments = Arrays.toString(joinPoint.getArgs());
  System.out.println("Method: " + methodName + " with arguments "
    + arguments +  " has just been called");
}

Toimiiko tämäkin? Loistavaa. Nyt on sitten aika siirtyä itse pihviin. Voit nimittäin tehdä tästä annotaatiovetoista, annotaatiota voi käyttää halusi mukaan joko kääntämään auditin pois päältä, tai päälle. Itse tykkäisin että on annotaatio audit, jolla voin valita auditoitavan eventin nimen. Sen käyttö tapahtuisi näin:

@Component
class JokuRandomiSpringService {
  @Audit("ACCOUNT_DELETE")
  public void poistaPirunTarkeeTili() {
    // Jotain ihan järkyn fiksua koodia tähän kohtaan
  }
}

Jeah, aika mukava? Joten tehdään tämmöinen:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Audit {
  String value() default "";
}

Sitten siihen todelliseen taikuuteen. Eli miten aop interceptor aktivoituu vain annotaation havaitessaan? Näin:

@Before("execution(* *.*(..)) && @annotation(audit)")
public void logServiceAccess(JoinPoint joinPoint, Audit audit) {
  String event = audit.value();
  if ("".equals(event)) {
    event = joinPoint.getSignature().getName();
  }
  Principal user = (Principal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
  String remoteAddress = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes())
    .getRequest().getRemoteAddr();
  auditEventService.createEvent(new AuditEventEntity(user.getName(), event, remoteAddress));
}

Huomaa myös että annotaation mäpätään joinpointtiin muuttujanimellä, ja tulee parametriksi interceptorille. Annotaation sisältä voidaan kaivaa halutut parametrit, tässä tapauksessa value, joka olisi audit eventin nimi.

Tämän esimerkin koodi menee vähän pidemmälle. Jos nimeä ei ole annettu, oletusnimi on kutsuttavan metodin nimi, eli value on valinnainen. Lisäksi kaivellaan käyttäjän identiteetti security contextista, ja ip-osoite request contextista. Huom! Esitetty malli ei ole yksikkötestiystävällisintä, voi olla että on elegantimpiakin tapoja injektoida nämä contextit.

Mitäs vielä? Tuossa koodissa oleva auditEventService on ihan tavallinen Spring komponentti/service, jossa on yksi rivi koodia jolla talletetaan audit eventti kantaan, sopivaan tauluun, jossa on halutut sarakkeet. Samoin auditevententity on yksinkertaisesti Entity Object, jossa on kentät username, event, remoteaddress – id ja aikaleima ovat autogeneroituja. Lisätään tietoa sen mukaan mikä on paranoian taso.

Joskus tuli tehtyä sellaistakin järjestelmää jossa haluttiin mahdollisimman iisi tietoturva – yleinen tietoturvan sääntö kun on, että mitä tiukemmin kiristää käyttäjille näkyvää tietoturvaa, ja vaikeuttaa arkea, sitä luovemmin opitaan kiertämään se tietoturva, luoden usein jopa turvattomampi ratkaisu kuin alunperin (salasanoja muistilapuilla, sama salasana kaikkialla, kulunvalvottujen ovien availu kohteliaisuudesta, jne). Hyviä tietoturvaratkaisuja ovat eritoten ne systeemit joissa tietoturva ei hankaloita käyttäjän arkea. (Tämän takia salasanat ovat helvetistä)

Esim. tarkka auditointi tarkkojen roolilokeroiden sijasta, kaikki saavat tehdä lähes kaikkea mutta kaikesta jää jäljet. Tai jos haluaa niin molemmat päälle. Riippuu ympäristöstä mikä on fiksua, tarpeellista tai lainsäädännön sanelemaa.

Hyvä huomata että tämän tason auditointi ei loggaa virheitä jotka johtivat keskeytymiseen jo aiemmin ketjussa, eli jos haluat vielä laveammalla siveltimellä, voit täydentää esim. servlet tason filttereillä ja virhekäsittelijöillä.

 

Advertisements

Vastaa

Täytä tietosi alle tai klikkaa kuvaketta kirjautuaksesi sisään:

WordPress.com-logo

Olet kommentoimassa WordPress.com -tilin nimissä. Log Out / Muuta )

Twitter-kuva

Olet kommentoimassa Twitter -tilin nimissä. Log Out / Muuta )

Facebook-kuva

Olet kommentoimassa Facebook -tilin nimissä. Log Out / Muuta )

Google+ photo

Olet kommentoimassa Google+ -tilin nimissä. Log Out / Muuta )

Muodostetaan yhteyttä palveluun %s