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

 

First rule of exception handling: Do not do exception handling!

Error: There is no error!

Tästä onkin pitänyt jo hetken aikaa pistää ajatuksia ylös, koulutuksissa aiheesta aina saarnaan mutta yritetään koota ajatuksia enemmän tai vähemmän koherentisti jopa artikkelin muotoon. Lainasin iskevän Fight Club henkisen artikkelin toisesta paikkaa, mutta ajatukset ovat ihan omia. Niissähän voi olla myös mielipiteitä ja jopa väärinkäsityksiä, eli tunne olosi vapaaksi kertoa mielipiteitäsi 😉 😉

NullpointerException at line 5872/Error: There is no error!

Konsulttihommissa ja ihan vain käyttäessä kotimaisia ja ulkomaisia web-sovelluksia on tullut nähtyä mitä kauheampia lähestymistapoja virhekäsittelyyn. Alkaen ohjelmiston kaatumisesta ja stacktrace-virhepinosta ruudulla tai lähdekoodin näkemisestä ihan vain tyhjään valkoiseen sivuun tai klassisiin: generic error, something unexpected happened tyyppisiin. Kun virhekäsittely on tehty huonosti, se luo suunnattomia tietoturva-haavottuvuuksia, ja luo ohjelmistosta myös huonolaatuisen ja epävakaan kuvan käyttäjille – mikä kaiken lisäksi on useimmiten totta.

Takaisin polulle

Miksi sitten virhekäsittelyä ei tehdä oikein? Kun projektin aikataulupaineet puskevat päälle mennään usein kiireen alla koodaamaan ja silloin saattaa olla että keskitytään vain siihen yhteen ainoaan happy-skenaarioon testauksessa, siihen kun kaikki toimii. Yhtä tärkeää olisi myös miettiä polut: Mikä voi mennä pieleen. Testata ne, rakentaa niille käsittelyt jotka mahdollisimman sulavasti ohjaavat käyttäjän taas tuottavalle polulle. Tässä onkin virheiden käsittelyn tärkein oppi (ja hyvä neuvo elämässä noin muutenkin). Älä keskity siihen mikä meni pieleen – keskity siihen miten normaalitoiminta voi taas jatkua.

Sovelluksessa tapahtuvat epänormaalit tilanteet johtuvat tyypillisesti kahdesta eri asiasta: Käyttäjästä, tai olosuhteista. Edelliset eivät ole oikeastaan virheitä, vaan väärinkäsityksiä, jotka pitää lempeästi ohjaten oikaista. Jälkimmäiset taas ovat asioita joista käyttäjälle on tarpeetonta tiedottaa näyttäen poikkeuspinoa ja koodin rivinumeroita: Loggaa niiden tekniset yksityiskohdat talteen, korjaa ne välittömästi jos pystyt, tai jos et pysty, kerro käyttäjälle että järjestelmässä on ongelmaa, anna ohjeet miten tästä voi edetä, ja mielellään tiketti jolla logiviestit voidaan jäljittää.

Opasta käyttäjää lempeästi

Ohjaa siis käyttäjä takaisin tuottavalle polulle. Se tarkoittaa että ennakoit mahdollisia virhetiloja. Tuliko syöte joka ei ole sallittu? Opasta käyttäjää hyvin esimerkein. Älä anna sovelluksen kaatua ylimääräiseen heittomerkkin tai pienempi-kuin merkkiin. Älä tyhjennä syötettyä lomaketta tiedoista ja näytä käyttäjälle virheilmoitusta tyyliin: ”Syötteessä oli virhe. Yritä uudelleen”.  Hyvä virhekäsittely ei hävitä jo syötettyjä tietoja mutta näyttää tarkalleen missä syötevirhe oli ja mieluiten hyvän esimerkin kera millaista syötettä tässä odotetaan. Esim. puhelinnumerot, päivämäärät, numerot menevät herkästi sekaisin, ja väärinkäsityksen vuoksi merkkijono voi olla aivan liian lyhyt tai pitkä. Tarkista myös että syötetyt merkit ovat järkeviä sovelluksen kannalta: Unicodessa on n. 65 000 merkkiä joista periaatteessa jokainen voi olla turvariski väärissä olosuhteissa. Ehkä siis on parempi olla tarkistamatta erikseen vaarallisia merkkejä, ja sensijaan miettiä tarkemmin mikä on sallittua.

Jotain odottamatonta tapahtui – odota hetki ja yritä uudelleen, tai ota yhteys ylläpitoon

Kun jotain odottamatonta menee pieleen se voi johtua esim. odottamattomasta kuormituksesta, verkkoyhteyksien menettämisestä, sähkökatkoksesta, odottamattomasta uudelleenkäynnistyksestä, viiveiden aiheuttamasta timeoutista, siitä että joku kompastui verkko tai sähköjohtoon, kovalevy hajosi, etc. Mikään näistähän ei ole oikeasti odottamatonta, eihän? Näitä tapahtuu, ja näitä voi myös testata. Voit tarkistaa miten sovelluksessasi näkyy kun kantapalvelin ajetaan alas tai joku osakomponentti hajoaa tai sammuu. Kuormitusta voidaan simuloida ja vasteaikoja testata. Toisin sanoen, kun odotettuja odottamattomia virhetiloja ilmenee, hyvin tehty järjestelmä kertoo käyttäjälle riittävästi, loggaa yksityiskohdat talteen, ja tarvittaessa hälyttää ylläpidon jotta jotain voidaan tehdä. Ja kun jotain todella odottamatonta tapahtuu, prosessi on periaatteessa sama mutta ehkä kiireisempi. On mahdollista tehdä geneerinen virhekäsittely joka kattaa loput tilanteet joita ei todellakaan osattu odottaa. Miten nopeasti virheen sattumisesta päästään taas takaisin tuottavalle polulle? Siinä erottuvat jyvät akanoista.

Virheiden käsittelyn Antipatterneja

Sanaa antipattern en edes yritä suomentaa (Vastahahmo? ;), mutta on tullut nähtyä kooditasolla levinneitä turmiollisia lähestymistapoja siihen miten virheitä käsitellään. Mitä vikaa on seuraavissa malleissa?

try {
      // Some code that causes errors here
} catch (NullPointerException npe) {
} catch (ArrayIndexOutOfBoundsException aiooe) {
      aiooe.printStackTrace();
} catch (Exception ex) {
      showErrors("Connection failure, please try again");
}

Hoksaatko mitään turmiollista? Siinä on ainakin kolme antipatternia käytössä. Osa näistä johtui Java kielen valitettavasta visiosta käyttää checked exception mallia eli pakko-käsitellä tyyppisiä poikkeuksia. Se on saanut ohjelmistokehittäjät generoimaan usein tarpeettomia try-catch-lauseita, piilottelemaan poikkeuksia, käsittelemään niitä kun ei ole aika käsitellä, ja käsittelemään niitä huonosti. Mikä meni pieleen?

– Huomasit varmaan tyhjän catch-lohkon. Sellaisia löytyy jopa tuotantokoodista. Oletan että syynä on, että poikkeus laukeaa herkästi mutta on suhteellisen harmiton joten se lakaistaan maton alle. On ehkä 1 tapaus 100:ssa missä on perusteltua tehdä sellainen, eli poikkeus pitää piilottaa. Sellainen poikkeuksellinen tilanne pitäisi kuitenkin dokumentoida ja sen pitäisi olla harvinaisuus, ei yleinen käytäntö. Jos jotain menee pieleen, ja piilotat sen, ongelma ei ole poistunut, ja se tyypillisesti rantautuu myöhemmin jossain muualla mitä mielikuvituksellisimmin tavoin, ja tekee alkuperäisen syyn löytämisen äärettömän vaikeaksi. Se harvinainen tilanne jossa tuo voi olla ok on jos pitää tulla toimeen taustajärjestelmän päällä joka jostain syystä käyttää poikkeuskäsittelyä väärin, ei kerro virhetiloista vaan tekee niillä jotain muuta. Tyhjä catch lohko on paholaisesta.

– Toinen antipattern on äärimmäisen yleinen ex.printStackTrace() kutsu, generoituna suoraan Eclipsestä. Se toimii kehittäjän koneella hienosti, kun virhepino menee konsoliin, mutta asennettuna ei konsolia olekaan. Jos kyseessä on android tai palvelinsofta, virheet tulostuvat logiin. Mutta kukaan ei ehkä koskan katsele logia. Tai jos katselee, merkityksellinen tieto hukkuu sadantuhannen stacktrace tulostuksen alle. Ei näin. Tämä on oikeastaan variaatio edellisestä eli tyhjästä catch lohkosta. Mitä pitää tehdä kun havaitaan poikkeus? Pysähdy, pistä detailit logiin jos virhe todella johtuu ympäristöstä tai on syytä logata. Korjaa ongelma jos se on korjattavissa. Jos ei ole, tiedota niitä jotka pystyvät sen korjaamaan. Ellet pysty tekemään mitään näistä, kuplita virhe eteenpäin tasoon jossa voidaan näin tehdä, yleensä UI taso. Tiedota ylläpitoa jos tarpeen. Tiedota käyttäjää aina. NullPointerException ei kerro mitään käyttäjälle (toivon mukaan), muista kertoa miten tästä päästään eteenpäin eikä sitä mikä meni vikaan.

– Kolmas antipattern yllä on catch-all lohko. Sitä näkee usein ainoana try-catch lohkona, ja usein viimeisenä käsittelynä kuten yllä. Tällekin voi olla joskus perusteensa, mutta useimmiten sitä käytetään aivan liian aikaisin. Catch-all lohkon ongelma on että se todella saa kaikki virheet kiinni, mistä ikinä ne johtuivatkaan. Jos se on liian aikaisin otettu käyttöön, siihen sisältyy röyhkeä oletus että pystyt käsittelemään minkä hyvänsä virheen samalla kaavalla. Mikä voi olla totta, mutta ellei se olekaan, olet taas piilottanut ongelmia muiden alle. Eli käsittely on ok kun todella haluat käsitellä kaikki jäljellä olevat ongelmat, käsittely on väärin kun otat laajasti kiinni mutta sovellat suppeasti virheen näyttöä, kuten yllä.

Mikään ylläolevista ei ole 100% ajasta tuhoisa tai edes väärin, kyse on nyansseista.

Suosituksia poikkeuskäsittelyyn

Älä ota poikkeuksia kiinni liian aikaisin. Jos et ole valmis korjaamaan ongelmaa tai keskustelemaan käyttäjän kanssa, heitä sama poikkeus tai uusi poikkeus uudelleen, kuplita, delegoi seuraavalle kutsupinossa kunnes päästään paikkaan missä voidaan korjata ongelma tai keskustella käyttäjän kanssa siitä miten se korjataan.

Älä loggaa samaa ongelmaa kahdesti tai useammin, kerta riittää. 

Paras tapa saada aikaan hyvä poikkeuskäsittely on hyvä testaus. Kehitä automatisoidut testit käyttöliittymään ja simuloi virheellisiä syötteitä ja muita virhetiloja siinä missä onnistumistakin. Kehitä myös kuormatestausta. Näin näet miten ne ilmenevät käyttöliittymässä, ja bonuksena tällaisten testien uudelleenkäytettävyysarvo on erinomainen. Testaa, testaa, testaa.

– Käyttäjävirheitä jotka johtuvat esim. validoinnista ei yleensä kannata logata (ja tukkia logia tarpeettomalla hälyllä), vaan keskity ohjaamaan käyttäjä oikealle suorituspolulle takaisin. Älä keskity siihen mikä meni pieleen, vaan keskity ohjaamaan käyttäjää miten toimia oikein, hyvän esimerkin kera. Jos käyttöliittymäsi käyttää idioottivarmoja kontrolleja tiedon keruuseen ja omaa hyvän validointilogiikan, voi olla että käyttäjävirheitä ei juuri tapahdu. Pyri siihen että käyttäjän aiheuttamat virheet eivät aiheuta poikkeuksia.

– Järjestelmä ja ympäristövirheet ovat vakavia, ja usein toiminnan pysäyttäviä. Käyttäjä harvoin voi niitä itse korjata. Osa virheistä voi korjautua itselläänkin, mutta tyypillisesti on hyvä logata yksityiskohdat, herätellä joku ylläpidosta tekemään jotain. Jos automatiikka ei siihen riitä, manuaalinen tapa on näyttää käyttäjälle geneerinen sivu jossa on ylläpidon yhteystiedot ja tikettinumero. Tässä tapauksessa älä anna käyttäjälle yksityiskohtia jo tietoturvasyistäkään, mutta loggaa ne huolella, ja pidä huoli että asialle tehdään jotain nopeasti.

– Sitten on ryhmä, todella odottamattomat virheet. Jos näihin yleensä pystyy reagoimaan, lähestymistapa on jotakuinkin sama kuin yllä, sillä erotuksella että nyt on KRIITTISTÄ pistää tietoa ylös virheestä ja nopeasti saada korjausta aikaan. Nämä tulisi myös logissa nostaa selkeästi omaan kategoriaansa että johtolangat löytyvät selkeästi. On ihan ok tehdä koko pinon päälle vielä geneerinen catch-all lohko joka ottaa kiinni kaikki todella odottamattomat virheet – jotta käyttäjän ruudulle ei pauku poikkeuskoodeja ja pinoja. Se ei vain saa olla ainoa poikkeuskäsittelymalli. Pidä myös huoli että käsittely tallettaa riittävästi yksityiskohtaista tietoa siitä mikä meni vikaan.

Siinäpä se. Suorituskyvyn suhteen on myös hyvä muistaa että poikkeuksien heittäminen ja etenkin poikkeuksen ottaminen kiinni ja heittäminen uudelleen on erittäin tehosyöppöä. Mutta sovelluskehityksen ensimmäiset prioriteetit ovat toimivuus ja selkeys, ja kuten opetan kursseilla, optimointia ei pidä lähteä tekemään ellei siihen ole syytä. Jos on, poikkeuskäsittelyn minimointi antaa vastalahjaksi usein suoritusnopeutta lisää. Tästä on kirjoitettu lukemattomia artikkeleita jos aihe kiinnostaa.

Muutamia muita hajatuksia aiheesta:

http://mikehadlow.blogspot.fi/2009/08/first-rule-of-exception-handling-do-not.html

http://today.java.net/article/2006/04/04/exception-handling-antipatterns

AJAX Tietoturvahuolet

Pikkasen olen jo pitkän aikaa uumoillut miten tulevina vuosina tullaan kiroilemaan kaikkia AJAX:in ylikäyttöön liittyviä tietoturvahuolia ja virheitä ja sotkuisia JavaScript mylläköitä. Edes Google ei ole ollut haavoittumaton näiden suhteen. Vietin suurimman osan 2000-luvun alkua saarnaten miten JavaScriptin käyttö on saatanasta koska se johtaa selainriippuvuuksiin ja on lähes mahdotonta testata eri selainyhdistelmillä, ja mitä enemmän JavaScriptiä sitä enemmän myös siirrämme tilatietoa ja vastuuta selainpäähän – päähän johon ei voi luottaa. Mikä sitten on muuttunut niin että AJAX on yllättäen turvallisempaa? Ei oikeastaan mikään..

AJAX oireilee samaa ongelmaa. Käyttäjä kirjautuu sisään – missä käyttäjän autentikointistatus ja id luuraavat? Onko syötteiden validointi sellaista että luotetaan JavaScriptin olevan aina päällä ja toimivan moitteettomasti? Mitä enemmän teet client päässä sitä enemmän kysymyksiä herää. Selain on vihamielistä maaperää. Keskiverrot tietokonekyvyt osaavalle on triviaalia muuttaa kaikki selaimeen lähetetty toimimaan täysin eri tavalla, oli sitten kyse cookieista tai javascript logiikasta. Voimme tietysti siirtää riskialtista tilaa enemmän palvelinpäähän, ja mahdollisesti logiikkaa myös, mutta sitä taas monet frameworkit ja koodit eivät tee.. Eli tulevina vuosina odotettavissa enemmän ja enemmän hauskoja pikku sivuefektejä rikkaita käyttöliittymiä hyödyntäville sovelluksille – no ainakin osille niistä. Aika ajoin törmää ratkaisuihin joissa nämä on tehty fiksummin, ja toki kehnosti tehdynkin sovelluksen voi auditoida ja panssaroida jälkikäteen.

Sitä odotellessa, tässä muidenkin mietteitä, enemmän tuon JavaScriptin puolelta kuin niinkään AJAX:ista: http://www.dzone.com/links/r/javascript_must_die.html

Ja tässä harvinaisen selkeä selitys Scrum:sta: http://www.dzone.com/links/r/presentation_scrum_at_bbv_software_services_ag.html