Secure by Design - Hoofdstuk 6

Ingediend door Dirk Hornstra op 03-jul-2020 15:05

In januari zijn we gestart met hoofdstuk 1 (link), in februari met hoofdstuk 2 (link), in april met hoofdstuk 3 (link), in mei met hoofdstuk 4 (link), in juni met hoofdstuk 5 (link) en nu wordt het tijd voor het 6e hoofdstuk.

De titel van hoofdstuk 6 is "Ensure integrity of state".

"Mutable state", oftewel, wijzigbare objecten/waardes zijn voor een groot deel de basis van code. Waarom zou je naar een webshop gaan al je geen producten in je winkelwagen zou kunnen zetten (inhoud winkelwagen wijzigbaar), en deze niet kunnen afrekenen (afboeken voorraad, bijboeken inkooporder). Mutable state kan op meerdere manieren uitgewerkt worden, in dit hoofdstuk worden daar een aantal voorbeelden van gegeven. Een nieuwe entiteit die aangemaakt wordt moet zich aan jouw business-rules houden. Als dat niet gebeurt heb je de poppen aan het dansen, bugs en mogelijke veiligheidsproblemen die moeilijk op te sporen zijn kunnen jouw applicatie onderuit halen. In dit boek gaan we van start met domain driven design en het gebruik van entiteiten om dit probleem te lijf te gaan.

State beheren met behulp van entiteiten

Een centraal thema voor veel applicaties is het bijhouden hoe de staat van dingen kunnen veranderen. Als een tas in het vliegtuig geladen wordt veranderen zowel de staat van de tas als die van het vliegtuig waardoor nieuwe regels gaan gelden. De tas mag niet langer geopend worden (daarvoor mocht dat wel), het kan nu uitgeladen worden (dat kon niet, want het was nog niet ingeladen). Voor het vliegtuig geldt dat de lading toegenomen is. Als een tas niet gescand is, mag deze niet aan boord. Ook moet bijgehouden worden welke tas bij welke passagier hoort. Stapt de passagier niet aan boord, dan moet de tas weer uitgeladen worden.

Er zijn veel manieren om wijzigingen bij te houden. Onze voorkeur gaat naar de entiteiten, maar in de huidige systemen komt het voor dat:

  • state wordt in een cookie bijgehouden
  • via SQL of stored procedures worden wijzigingen in de database doorgevoerd
  • een applicatie laadt de state van de server, past de state aan en stuurt het terug naar de server

En dan heb je ook nog dat veel systemen juist een mix van bovenstaande zijn.

Het boek heeft de voorkeur voor DDD. Je gaat dan de keuze maken wat de juiste manier is om een model te maken. Wat is het belangrijkst? De tas, vlucht en passagier of moet je het zien als processen: 'check-in', 'loading', 'boarded travellers'? Als je voor het eerste scenario kiest, dan is de zin tas: "de tas mag alleen mee met de vlucht waar de passagier ook in mee gaat", vlucht: "bij een vlucht mogen alleen tassen mee die ingecheckt zijn die aan passagiers toebehoren die geboard zijn (dus in het vliegtuig ingecheckt)". De eerste zin klinkt het kortst en duidelijkst en zou waarschijnlijk je model worden.

Bij aanmaken consistent zijn

Als een entiteit niet overeenkomt met de business-rules, dan heb je een potentieel security-risico. Het voorbeeld wordt gegeven van een Aziatische bank, waarbij security-issues werden afgewimpeld als "kleine, technische probleempjes". Tot dat de tester een rekening aan kon maken zonder een eigenaar. Als dat gebeurt kun je als bank je licentie kwijt raken.

De gevaren van no-args constructors

De simpelste manier om een object te maken is om de constructor te gebruiken, zonder parameters. Maar die levert bijna nooit een consistent, "ready to use" object op. var p = new Person(); Tsja, hoe heet p dan? Hoe oud is p? In situaties waar dit gedaan wordt, wordt vervolgens vaak met setter-methodes de waardes alsnog ingesteld, maar vaak wordt dat niet afgedwongen. Waardoor het vergeten kan worden, of misbruikt. Als er later een setter bij komt, moet die op al die plekken waar de no-args constructor gebruikt wordt toegevoegd worden. Als je dit leest heb je al meteen de gedachte "dit moet je niet willen".

ORM frameworks en no-args constructors

Als je een object-relational-mapper gebruikt (in het boek wordt Java Persistence API en Hibernate genoemd, maar zou dat in ons geval Automapper zijn?) dan lijkt het erop dat je gedwongen wordt om een no-args constructor te gebruiken. Dat is niet helemaal waar, als je zelf je objecten los trekt naar een domain model, kun je daar de zaken afhandelen. Hibernate en JPA hebben die constructor nodig om data uit de database te laden en dan te vullen, dus als gebruiker hoef je die constructor niet te gebruiken. Door die dan ook private te maken en door field annotations te gebruiken kun je zorgen dat de ORM blijft werken en jij/de andere developers de code op de juiste manier gebruiken.

Alle verplichte velden als constructor argumenten

Dit vloekt een beetje met clean code. Daar werd gezegd: 1 parameter OK, 2 parameters DAT KAN, 3 of meer WEET JE HET ZEKER? Maar hiermee zorg je wel dat als het object aangemaakt is, de configuratie compleet is.

Aanmaken met fluent interface

Eric Evans en Martin Fowler hebben deze term bedacht. Het is bedoeld om je "vloeiend" de code te laten lezen. Doordat de functies het object zelf terug geven kun je "geschakelde" functies aanroepen (iets wat volgens mij ook wel in jQuery gebruikt wordt?). Dus je krijgt een Account a = new Account(nummer, accountOwner, interest).withCreditLimit(limiet).withFallBackAccount(fallbackAccount);

Hier zijn wel een paar aanmerkingen op. Het botst met het command-query separation principe (CQS) wat zegt dat iets een query óf een command moet zijn.

Geavanceerde voorwaarden afvangen in code

Bepaalde voorwaarden bepalen dat als iets waar is, iets anders niet waar mag zijn. Deze invarianten moeten tijdens aanmaken en duur van bestaan van het object gelden. In het voorbeeld van de bank mag iemand een kredietlimiet hebben of een fallback-bankrekening, maar niet beide. Hier wordt een aparte private functie voor gemaakt en deze moet aangeroepen worden in de functies waarin wijzigingen op deze eigenschappen uitgevoerd worden.

Het builder pattern om geavanceerde voorwaarden af te dwingen

Het Builder pattern is dat je een nieuwe class, bijvoorbeeld CarBuilder gebruikt om de Car als een "klaar voor gebruik" object op te leveren, waarmee je voorkomt dat er acties uitgevoerd worden op een onvolledig Car-object. The Gang of Four heeft dit in hun boek "Design Patterns" beschreven. Het moeilijke hieraan is om de Builder goed werkend te krijgen. Vaak wordt deze in dezelfde module geplaatst, maak een interface voor de class voor de buitenwereld en een interface voor de class waar de builder mee kan werken. Het werkt, maar maakt het eigenlijk te ingewikkeld. Door de static class CarBuilder binnen de class Car te plaatsen kun je bij de private methoden. En door de constructor private te maken kan alleen de CarBuilder er bij. Let even goed op dat in de build-functie een nieuw Car object wordt aangemaakt, deze wordt ingesteld op de eerder aangemaakt en geconfigureerde variabele en die wordt op null gezet, zodat als nogmaals build() wordt aangeroepen niet nogmaals hetzelfde object terug wordt gegeven! Dat is wel iets om rekening mee te houden!

ORM Frameworks en geavanceerde voorwaarden

Als je data laadt uit de database die van jezelf afkomstig is en waarvan je zeker weet dat het aan de business-rules voldoet, dan kun je de data 1 op 1 laden. Maar als dat niet zo is (je weet het niet zeker of je data komt ook uit externe bronnen), dan kun je nog wel het beste een post-controle uitvoeren op het moment dat de data geladen is.

Wanneer moet je welke constructie gebruiken

Verplichte velden in de constructor, fluent interface en het builder pattern. Alle 3 moeten ervoor zorgen dat bij aanmaken je een correct object in handen krijgt. Wanneer je welke moet gebruiken is afhankelijk van de situatie waarin het gebruikt wordt.

Integriteit van entiteiten

Door niets buiten de class wijzigbaar / mutable te maken dwing je de integriteit af. We gaan naar een aantal implementaties kijken.

Getter en setter methoden

We zien een order-class met private boolean paid. Maar met een public setPaid(true/false parameter) methode. In het alternatief is setPaid hernoemd naar markPaid en heeft geen parameters meer, paid wordt op true gezet. De andere kant op (weer false maken) mag/kan niet.

Voorkom het delen van mutable (aanpasbare) objecten

Als je functie een resultaat terug geeft, geef dan een value-type terug en niet een mutable type. We zien het voorbeeld van een String (immutable, goed) en een StringBuffer (mutable, niet goed). Het zou wat zijn dat je een adres terug krijgt en dat kunt legen en vervolgens vullen met je eigen adres. Het boek geeft het voorbeeld van java.util.Date die een mutable object terug geeft. Fix daarvoor is dat je een kloon maakt en dat terug geeft. Als iemand dan aanpassingen doorvoert, is dat op de kloon en niet op het originele object.

Beveilig de integriteit van collecties

Je moet de lijst niet openbaar maken naar de buitenwereld. En laat het vooral niet met een setOrderItemList(List<orderline>) overschrijfbaar maken, want dan werk je jezelf in de nesten. Maak een void addItem(...), een int getNrOfLines() om de collectie zelf maar niet terug te hoeven geven. Als dat toch echt moet, dan kun je weer de kloon-functie gebruiken. Maar er is een andere truck, een read-only-proxy. De functie .unmodifiablelist zorgt voor een read-only resultaat. Dat de lijst niet aanpasbaar is, betekent niet dat je niet hoeft te zorgen dat de items zelf aanpasbaar zijn. Als de items in de lijst blijven, maar iemand kan de prijs van de items op 10 cent zetten in plaats van 1 euro per stuk, dan heb je alsnog een probleem.