RESTAssured ja Istuntokeksit

REST ja istunnot ovat aikalailla paradoksi. Mutta silti joskus löytää itsensä tilanteesta jossa tilattomat REST palvelut ovat tilallisen autentikoinnin takana, esim. JSESSIONID evästeellä seurattuna (Ja kyllä, Suomen Kielitoimisto suosittaa nykyisin Cookielle termiä eväste, aiemmin keksi, ja uskokaa tai älkää, alunperin Taikapipari). Jos vielä heikommin käy, edessä voi olla joku single-signon hirviö joka tokenien sijasta käyttää useampaa istunto-evästettä. Ja jotenkin pitäisi saada kirjautumisen jälkeen istunnot säilymään.

Jos kyseessä on vain yksinkertainen JSESSIONID case, alkaen RestAssured versiosta 2 sille on tuki valmiina, parillakin eri tavalla. Nopein tapa saada sessio-cookie lähetettyä edestakaisin on tähän tapaan:

RestAssured.filters(sessionFilter);

Tuo pitää huolen, että sessio-eväste kaapataan vastauksista, ja liitetään pyyntöihin. Toimii heittämällä jos nimi on JSESSIONID – kunhan vain pidät huolen siitä, että sessionFilter on kaikissa kutsuissa sama, esim. luomalla sen staattisena:

public static final SessionFilter sessionFilter = new SessionFilter();

Entä jos evästeen nimi ei ole JSESSIONID? No, sen voi helposti muuttaa RESTAssured konfiguraatiolla:

RestAssured.config = newConfig().sessionConfig(new SessionConfig().sessionIdName("KOTLINSESSIONID"));

Entä jos pitäisi puljata useampia tracking cookieita, ja niiden nimet muuttuvat välissä? No ei hätää, sen voi hoitaa omalla custom filterillä, tähän tapaan:

public class CookieResenderFilter implements com.jayway.restassured.filter.Filter {

  private Map<String, Object> cookiesMap = new HashMap<String, Object>();

  @Override
  public Response filter(FilterableRequestSpecification filterableRequestSpecification,
                         FilterableResponseSpecification filterableResponseSpecification,
                         FilterContext filterContext) {
    filterableRequestSpecification.cookies(cookiesMap);
    Response response = filterContext.next(
      filterableRequestSpecification,
      filterableResponseSpecification);
    Map<String, String> cookiesReceived = response.getCookies();
    if (cookiesMap.size() == 0 && cookiesReceived.size() > 0) {
      for (String key : cookiesReceived.keySet()) {
        cookiesMap.put(key, cookiesReceived.get(key));
      }
    }
    return response;
  }
}

Ja tuo rekisteröidään käyttöön kuten yllä, taas pitäen huolta siitä että kyseessä on aina sama instanssi, ei aina uusi – koska sen jäsenmuuttujia käytetään tiedon varastointiin testisession ajan.

Oikeastihan tilallisuus + REST rajapinnat ovat helvetin seitsemännen tason keksintö ja jo tätä artikkelia kirjoittaessa tuli likainen olo, mutta aina ei pääse valitsemaan työkalupakkiaan. Kun pääsee, käyttäkää BCRYPT Token headereita, ihmiset!

Angular2 + NPM + Webpack

Lupailin kirjailla vähän lisää fiksusta Angular2 setupista tuotantohommiin. Tarkoitus oli tunkkailla minimaalinen paketti jossa on helppo erottaa eri tekniikoiden tehtävät. Valitettavasti Angular2 on aika liikkuva kohde tällä hetkellä – ja kiirettäkin on pitänyt, joten yksinkertainen POC projekti on jäänyt haaveeksi.

Onneksi joku muu on tehnyt sellaisen. Tällä hetkellä fiksuinta on aloittaa kokeilut ylläpidetystä Angular2 Webpack Starter projektista: https://github.com/AngularClass/angular2-webpack-starter

Valitettavasti tuo projekti ei ole yksinkertainen eikä siisti, mutta hoitaa homman. Ja se on kuitenkin hyvin linjassa sen kanssa mitä itse ajattelen. Angular2 kehittyy vauhdilla juuri nyt, ja joka releasen myötä se on siistimpi ja eheämpi ja vähemmän tarvitaan purkkaa taustalle. Sitä myöten voisin kuvitella että tämä webpack starterkin yksinkertaistuu.

Joka tapauksessa, vähän saatesanoja projektin suhteen. Senhän saa käyttöön nätisti kokeiluun (kun nodet on asennettuna), esim. näin:

 

git clone https://github.com/angularclass/angular2-webpack-starter.git
cd angular2-webpack-starter
npm install
npm start
curl http://localhost:3000

Curlin sijasta voit luonnollisesti tyypittää oikealla selaimella. Kuten kynäilin viimeksi, Bower on vähän menemässä muodista pois, samoin Grunt – paljon kuin sitä ja Gulpia rakastankin. Jos haluaa, voihan molempia noita käytellä vieläkin, ja voi olla joku tehtävä jossa Gulp voi olla esim. hyödyllinen. Mutta vanilla on päivän maku näissä asioissa, ja aikalailla paljon saa aikaan ihan vain puhtaalla JavaScriptillä ja NPM:llä.

Kannattaa luoda silmäys package.json tiedostoon. Se on tässä kaiken avain. Ja voi olla pelottava kokemus tässä vaiheessa jos on tottunut että se on vain kirjastomanifesti. Tosiaan devdependencies-lista on aika kattava näinkin yksinkertaiselle projektille, mutta tulee yksinkertaistumaan jatkossa. Ja toki siinä on jo aika kattavasti testaustyökaluja mukana. Pääjuju kuitenkin, että riippuvuudet on fiksuinta hakea kaikki npm keinoin, softan omat riippuvuudet ovat dependencies kohdassa, ja Boweria ei käytetä.

Mielenkiintoinen osio on kuitenkin scripts-osio. Vielä pari vuotta sitten oli tavanomaista että tämä oli tyhjä tai siinä oli 1-2 kohtaa, build ja test. Nyt tässä näkyy hyvin painopisteen muutos. Perinteiset grunt/gulp työvaiheet on nyt npm scripts työvaiheina, ja ne ovat modulaarisia ja ketjutettuja. Tarvittaessa käytetään noita dpm dev moduuleja, esim. rimraf poistaa tyylikkäästi tarvittavat hakemistopuut. Jos tuosta poimii muutaman mehukkaan ja keskeisen komennon niin ne ovat:

"clean": "npm cache clean && rimraf node_modules doc typings coverage dist"
"build:dev": "webpack --progress --profile --colors --display-error-details --display-cached"
"server:dev": "webpack-dev-server --progress --profile --colors --display-error-details --display-cached --content-base src/"

Näitä pääsee ajamaan komennolla npm run, esim.

npm run build:dev (ajaa kehitysversion buildin)

Samaan listaan on myös määritetty oikopolkuja yleisimpiin, esim:

"build": "npm run build:dev"
"server": "npm run server:dev"

Siinäpä nuo yleisimmät. Ja loput hoitaa webpack. Webpackillä on tässä kaksi keskeistä tehtävää: Tärkein on lähdekoodin prosessoiminen halutuksi paketiksi (sisältäen tarpeen mukaan babelifioinnit, minifioinnit, obfuskoinnit, jne), ja sen ohella myös sisäänrakennettu testiserveri on mukava. Konfiguraatiot käännöksiin löytyvät projektin juuresta, esim. tsconfig.json ja webpack.config.js. En pui niiden sisältöä tässä sen enempiä, mutta ne ovat aika selkeitä ja tehtäväänsä keskittyneitä. Ehkä hyvä mainita että toisin kuin Angularin omassa yksinkertaisessa tutoriaalissa, moduulijärjestelmänä SystemJS on korvattu CommonJS:llä.

Mitäpä muuta? Sain aiemman artikkelin suhteen vähän kommenttia/kysymystä tuosta Mavenin käytöstä. Itsehän tyypillisesti teen backendit Javalla/Spring Bootilla, ja frontend on vain osa kokonaisprojektia. Ja koska asiat on kiva pitää yksinkertaisena, samalla kun buildataan Mavenillä backend hyväksi, on mukava rusauttaa läpi myös frontend buildit. Käytän tähän tosiaan eirslett maven plugaria, joka voi yksinkertaisimmillaan näyttää tältä:

<plugin>
  <groupId>com.github.eirslett</groupId>
  <artifactId>frontend-maven-plugin</artifactId>
  <version>0.0.27</version>
  <executions>
    <execution>
      <id>install node and npm</id>
      <goals>
        <goal>install-node-and-npm</goal>
      </goals>
      <configuration>
        <nodeVersion>v5.5.0</nodeVersion>
        <npmVersion>3.3.12</npmVersion>
      </configuration>
    </execution>
    <execution>
      <id>npm install</id>
      <goals>
        <goal>npm</goal>
      </goals>
    </execution>
    <execution>
      <id>npm build</id>
      <goals>
        <goal>npm</goal>
      </goals>
      <configuration>
        <arguments>run build:prod</arguments>
      </configuration>
      <phase>compile</phase>
    </execution>
  </executions>
</plugin>

Käytännössä siis tuo tarkoittaa:

  • Varmista että node ja npm ovat asennettuna paikallisesti
  • Aja npm install
  • Aja npm run build:prod scripti – automaattisesti Mavenin Compile vaiheessa, eli samalla kuin backend kooditkin käännetään

Yksinkertaista. Webpack on konfiguroitu puskemaan prosessoidut .js tiedostot, html:t ja muut staattiset osat sopivaan kansioon, josta Mavenin paketointivaihe ne sitten nappaa mukaan .war pakettiin.

Huomaa että jos haluat ajaa Mavenin läpi muitakin npm taskeja, se onnistuu määrittämällä sopiva Maven property jonka voit komentoriviltä ylikirjoittaa, ja käyttämällä sitä tuon npm taskin konfiguraatiossa, esim. näin:

<execution>
  <id>npm build</id>
  <goals>
    <goal>npm</goal>
  </goals>
  <configuration>
    <arguments>${npm.argument}</arguments>
  </configuration>
  <phase>compile</phase>
</execution>

En kuitenkaan nykyisellään koe tälle erityistä tarvetta. Mavenin päätehtävä on varmistaa että riippumatta koneen varustuksesta paketti osaa buildata itsensä eheäksi itsenäisesti, oli sitten kyseessä Jenkins ympäristö tai uuden projektiin tulevan kehittäjän kone, tai vanhan devaajan uusi kone. Eli toiminta varmistetaan kaikissa olosuhteissa. Mutta noita muita npm käskyjä voi hyvin ajaa ohi mavenin esim. frontend-painotteisten kehittäjien toimesta, esim.watch on varsin hyödyllinen kun haluaa saada käyttöliittymäkehityksessä jatkuvaa palautetta. Siksi perusbuildi Mavenin kautta ajettuna usein riittää, ja oletuksena production buildi on tietysti hyvä.

Tässä mietteitä tällä kertaa. Matka vilkkaassa frontend maailmassa jatkuu….

 

 

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ä.