title | date |
---|---|
Ontwerpkeuzes |
31-7-2018 |
De ID-parameter, hieronder aangeduid met {uuid}
wordt gebruikt om via de URL
een enkel object van een bepaald type resource te vinden. Bijvoorbeeld een
Zaak
:
URL | Voorbeeld |
---|---|
https://www.example.com/zaken/{uuid} |
https://www.example.com/zaken/550e8400-e29b-41d4-a716-446655440000 |
Een UUID (versie 4) is in de praktijk altijd uniek, zonder dat deze centraal
hoeft te worden bijgehouden. Deze keuze laat onverlet de mogelijkheid om op
andere manieren bij een enkel object te komen, zoals op een combinatie van
velden die samen uniek zijn, zoals bronorganisatie
en zaakidentificatie
:
https://www.example.com/zaken/?bronorganisatie=0329&zaakidentificatie=MOR-0000001
Achtergrond
De paden van API endpoints bevatten referenties naar de objecten in de
achterliggende datastore. Deze parameters zouden semantisch kunnen ingevuld
worden, zoals gebruikmaken van bronorganisatie
en zaakidentificatie
voor
een zaak. Echter, na analyse blijkt dat dit lastig consequent toe te passen is
door de hele de API heen. Tevens wekt het de indruk dat dat deze parameters
genoeg zijn om een Zaak te vinden maar dat is niet correct. De volledige URL is
nodig voor het opvragen van een enkele Zaak.
Daarom is besloten om gebruik te maken van UUID (versie 4) voor deze parameters. De motivatie is verder dat deze:
- uniciteit garandeert, ook over meerdere systemen heen;
- geen volgordelijkheid/database IDs lekt;
- de DSO API-richtlijnen volgt;
- semantisch bevragen niet onmogelijk maakt.
Voor het uitdrukken van duur wordt gebruikt gemaakt van ISO-8601 durations. Dit sluit aan bij ISO-8601 weergave van timestamps doorheen de API.
In het RGBZ zijn er voor sommige relaties meerdere types van gerelateerde resources mogelijk. Concrete voorbeelden hiervan zijn:
-
betrokkenen bij een zaak - dit kunnen
Natuurlijke Personen
,Medewerkers
,Organisatorische Eenheden
en meer zijn. Elk van deze betrokkenen heeft verschillende attributen, en een aantal komen (bijna) overal voor. -
objecten gerelateerd aan een zaak via ZAAKOBJECT. Hier zijn erg veel types mogelijk, bijvoorbeeld:
Huishouden
,OpenbareRuimte
,Wegdeel
etc.
In essentie is dit een vorm van polymorfisme.
Door het uitgangspunt van Common Ground om data-bij-de-bron-opslaan te hanteren, werken we in de ZGW API specificaties met URL referenties, wat in deze context betekent dat de relaties een referentie-url geven naar de gerelateerde resource. Een client/consumer weet op voorhand niet naar welke vorm van gerelateerde resource er verwezen wordt.
Daarom is ervoor gekozen om op de relatie bij te houden om welk type resource het gaat. Een concreet voorbeeld van een response is dan:
GET https://zaken.api.haarlem.nl/v1/zaakobjecten/c9a651 HTTP/1.0
Content-Type: application/json
{
"url": "https://zaken.api.haarlem.nl/v1/zaakobjecten/c9a651",
"zaak": "https://zaken.api.haarlem.nl/v1/zaken/66a38b",
"object": "https://objecten.api.haarlem.nl/v1/wijken/3fa723",
"relatieomschrijving": "Speelt alleen in deze wijk",
"type": "Wijk"
}
Het gaat hier dan om het type
veld.
De URLs in dit voorbeeld zijn uiteraard fictief.
-
Prefixes die slaan op de eigen resource, worden niet gebruikt. Voorbeeld: Attribuutsoort informatieobjecttype.informatieobjecttype-omschrijving in RGBZ2 krijgt de naam "omschrijving" in de API, aangezien het een veld is binnen de resource
informatieobjecttype
. Dit komt overeen met de in RGBZ genoemde XML-tag.{ "url": "https://example.com/drc/api/v1/informatieobjecttypen/8534ba6a-bcde-4387-b054-6bde6d1ded8f", "omschrijving": "Document" // ... }
-
Relaties worden niet aangeduid met hun relatie omschrijving (
isVan
,heeft
,kent
, etc.). Dit heeft mede te maken met het feit dat er geen mnemonic attributen zijn (zoals in StUF-ZKN) in de REST API, waardoor het niet direct duidelijk is naar wat voor type resource verwezen wordt. -
Indien verwezen wordt naar een andere resource (relatie), wordt de volledige naam van de resource gebruikt als veldnaam. Voorbeeld: De resource
zaak
heeft een veldzaaktype
en niettype
. Ter vergelijking, in StUF-ZKN is dit:zaak-isVan-<zaak type data>
waarisVan
verwijst naar hetzaaktype
.{ "url": "https://example.com/zrc/api/v1/zaken/6232ba6a-beee-4357-b054-6bde6d1ded6c", "zaaktype": "https://example.com/ztc/api/v1/zaaktypen/6232ba6a-beee-4357-b054-6bde6d1ded6c" // ... }
Indien een object een groepattribuutsoort heeft (een groep van bij elkaar behorende attribuutsoorten), al dan niet herhalend d.w.z. kardinaliteit nul of meer (een 'lijst' van waarden van de attributen van het groepattribuut), dan wordt het groepattribuut inline
(binnen de resource voor het object) ontsloten. Het krijgt dus geen eigen resource, het heeft immers geen zelfstandig bestaansrecht. Het heeft alleen bestaansrecht als eigenschap van het object, net zoals attribuutsoorten.
Typische voorbeelden zijn (zaak-)kenmerken en (zaak-)eigenschappen waarin Zaak
het hoofdobject betreft,
en Kenmerk
en Eigenschap
de groepattributen.
Voorbeeld:
{
"url": "https://example.com/zrc/api/v1/zaken/6232ba6a-beee-4357-b054-6bde6d1ded6c",
"zaaktype": "https://example.com/ztc/api/v1/zaaktypen/6232ba6a-beee-4357-b054-6bde6d1ded6c"
// ...
"kenmerken": [
{
"kenmerk": "test",
"bron": "http://www.example.com/"
}
]
}
Componenten die APIs aanbieden (ZRC, DRC, ZTC) moet aan volledige inputvalidatie doen. Dit betekent dat er meer validatie nodig is dan enkel garanderen dat een veld aan het url-formaat voldoet, en dat er communicatie tussen systemen is in het geval van gedistribueerde componenten.
Een concreet voorbeeld hiervan is het zetten van een STATUS op een ZAAK:
- Het aanmaken van een status gebeurt door een nieuwe status te POSTen, met
onder andere de URL van het
statusType
. - Het ZRC MOET deze
statusType
url opvragen uit het ZTC, en controleren dat het zaaktype van dit statustype overeenkomt met het zaaktype van de betreffende zaak.
De foutberichten voor deze validatie zullen opgesteld worden en opgenomen in de API spec.
Noot: voor suites kan de implementatie hiervan uiteraard afwijken - indien alles in 1 enkele database leeft, en de suite ontsluit 3 APIs, dan kan prima de validatie intern gebeuren op een geoptimaliseerde manier. Belangrijk is dat de fout-responses (zie ook #130) correct teruggegeven worden.
Noot 2: authenticatie en authorisatie worden in een later stadium uitgewerkt. Het is bekend dat hier goed over nagedacht moet worden.
De input validatie kan enkel in tekstuele vorm in de OAS api-spec opgenomen worden. We houden het formaat:
Er wordt gevalideerd op:
- geldigheid besluit URL
- geldigheid informatieobject URL
Deze documentatie wordt opgenomen in de description
key voor de operation
(bijvoorbeeld besluitinformatieobject_create
).
Voorbeeld:
paths:
/besluiten:
post:
operationId: besluitinformatieobject_create
description: |-
Registreer in welk(e) INFORMATIEOBJECT(en) een BESLUIT vastgelegd is.
Er wordt gevalideerd op:
- geldigheid besluit URL
- geldigheid informatieobject URL
Er komt een aparte besluitenregistratie (naast ZRC, ZTC en DRC) om besluiten in vast te leggen. De motivatie hiervoor is dat besluiten een bestaansrecht hebben onafhankelijk van zaken: niet elk besluit ontstaat gedurende de uitvoering van een zaak (denk aan raadsbesluiten).
Hierbij lopen we wat vooruit, maar volgen we wel data-bij-de-bron principes uit Common Ground. BESLUITTYPE laten we vooralsnog in de ZTC. Nader bepaald moet gaan worden hoe we hiermee omgaan, zoals onderbrengen in de BRC, aparte type-componenten cq. api's e.d.
Dit betekent dat er voor BESLUIT een informatiemodel moet komen, ontrokken aan het RGBZ. BESLUIT krijgt een optionele relatie naar ZAAK.
De verwachting is dat er later vergelijkbare designkeuzes gemaakt worden voor andere resources, zoals bijvoorbeeld klantcontacten.
De DSO schrijft een structuur voor waar een fout-response aan moet voldoen in het geval van validatiefouten:
{
"type": "...",
"title": "...",
"status": 400,
"detail": "...",
"instance": "...",
"invalid-params": [{
"type": "...",
"name": "<veldnaam>",
"reason": "Maximale lengte overschreden.",
}]
}
Op het forum is nagevraagd hoe er moet omgegaan worden met meerdere fouten op hetzelfde veld, met de conclusie dat dit beter gespecifieerd moet worden.
In de ZGW API's kiezen we ervoor om elke fout op een veld als apart object op te
nemen binnen de "invalid-params"
sleutel. Dit laat clients toe om elke
individuele fout te renderen zoals zij wensen.
Tevens voegen we op hoofdniveau van fouten en binnen een object in
"invalid-params"
een sleutel "code"
toe. Dit veld bevat een waarde die door
machines kan begrepen worden, bijvoorbeeld "invalid"
of
"max_length_exceeded"
. De "type"
sleutel is namelijk optioneel en bedoeld
voor developers om meer informatie over het fouttype te lezen, en niet voor
automatische verwerking.
Een voorbeeld response is dan:
{
"type": "URI: https://ref.tst.vng.cloud/ref/fouten/ValidationError/",
"title": "Ongeldige gegevens",
"status": 400,
"code": "invalid",
"detail": "Er deden zich validatiefouten voor",
"instance": "urn:uuid:2d3f8adb-470d-4bf4-8c43-4b2ebeef7504",
"invalid-params": [
{
"name": "identificatie",
"code": "max-length",
"reason": "Maximale lengte overschreden.",
},
{
"name": "identificatie",
"code": "special-characters",
"reason": "De identificatie mag geen speciale karakters bevatten.",
}
]
}
Merk op dat de concrete codes hier fictief zijn en puur ter illustratie zijn.
Om relaties zowel naar een andere resource (zoals status
) als naar een genest attribuut (zoals kenmerken
) te gebruiken binnen query parameters, wordt de __
(dubbele underscore) gebruikt tussen de attribuutnamen.
Voorbeelden
Voor filteren:
?zaak__kenmerken=test
?zaak__status=http://example.com
Voor de fields
query parameter:
?fields=zaak__kenmerken
?fields=zaak__status
De URIs die gebruikt worden om collecties van objecten, of individuele objecten op te vragen, eindigen nooit op een trailing slash:
Voorbeelden
Goed
/api/v1/zaken
/api/v1/zaken?identificatie=12345
/api/v1/zaken/67890
Fout
/api/v1/zaken/
/api/v1/zaken/?identificatie=12345
/api/v1/zaken/67890/
Rationale De DSO heeft hier geen expliciet standpunt over maar alle voorbeelden zijn zonder trailing slash. Daarnaast zijn veel commerciele APIs die dit voorbeeld volgen zoals Google en Facebook. Verschillende bronnen zijn hier wel over verdeeld, zoals REST API tutorial en Wikipedia maar er is gekozen om te kijken naar de praktijk en DSO.
De DSO API-strategie stelt:
API-09 Relaties van geneste resources worden binnen het eindpunt gecreëerd" Als een relatie alleen kan bestaan binnen een andere resource (geneste resource), wordt de relatie binnen het eindpunt gecreëerd. De afhankelijke resource heeft geen eigen eindpunt
Aanvullingen hierop:
- Er mag worden afgeweken van deze richtlijn, mits goed onderbouwd en gedocumenteerd.
- Er wordt niet dieper genest dan 1 niveau, tenzij goed onderbouwd en gedocumenteerd.
Rationale
Conceptueel is dit precies zoals het moet zijn. Echter, voortschrijdend inzicht, voortkomend uit gemaakte implementaties van referentie componenten en demo consumers, leidt er toe dat deze richtlijn wordt opgerekt.
Concreet voorbeeld is een consumer die een lijst van ZAKEN van verschillende ZAAKTYPEN laat zien en daar het STATUSTYPE bij wil laten zien. Conceptueel bevind een STATUSTYPE zich altijd binnen ZAAKTYPE (in het ImZTC). Echter, dit zou in het voorbeeld betekenen dat we voor elk ZAAKTYPE dat voorkomt in de lijst van ZAKEN, een aparte call gemaakt moet worden om de STATUSTYPEs op te halen. Veel efficienter is het om in 1x een lijst van STATUSTYPEN op te halen en de consumer deze te koppelen aan de relevante ZAAK.
Een 0-op-N of 1-op-N relatie tussen objecttypen A en B in een informatiemodel zoals RGBZ of ImZTC kan eenvoudig vertaald worden naar een REST API door een hyperlink op te nemen in de resource dat meerdere keren kan voorkomen in de relatie, B in dit geval. Het veld met de hyperlink in resource B verwijst naar de gerelateerde resource A. Dit is het principe van 'linked data'.
Bijvoorbeeld in het RGBZ kan een zaak kan nul of meer statussen hebben en een
status kan niet bestaan zonder een zaak, oftewel er is sprake van een 1-op-N
relatie tussen Zaak
en Status
. De resource Status
bevat naast zijn eigen
gegevens het (toegevoegde) veld "zaak"
dat de url naar de resource van de
gerelateerde zaak bevat.
Opmerking: We gaan er gemakshalve vanuit dat 0-op-N of 1-op-N relaties geen eigen gegevens hebben. In het RGBZ komt dit in ieder geval niet voor.
Een N-op-M relatie tussen objecttypen A en B wordt vertaald als een nieuwe resource R die fungeert als een kruistabel zoals in databases. De resource R bevat de volgende velden:
- url naar zichzelf (resource R),
- url naar resource A,
- url naar resource B,
- de gegevens van de relatie zelf (als die er zijn).
Het voordeel om een N-op-M relatie als resource te vertalen is dat je POST
en
DELETE
operaties kunt uitvoeren om relaties toe te voegen of te verwijderen.
Als je dat niet doet en je de relaties bijvoorbeeld bijhoudt bij één of beide
gerelateerde resources, dan belandt je al snel in een minder handige situtie
waabij je de PATCH
(of PUT
)-operaties moet gebruiken om het lijstje met
hyperlinks naar de gerelateerde resources bij te werken.
Bijvoorbeeld in het RGBZ is de relatie tussen Zaak
en Informatieobject
N-op-M.
Deze relatie is vertaald naar de resource ZaakInformatieObject
met de
volgende velden:
- url (hyperlink naar zichzelf),
- zaak (hyperlink naar de gerelateerde zaak),
- informatieobject (hyperlink naar het gerelateerde informatieobject).
In geval van de ZGW API's is de relatie tussen zaken en informatieobjecten gedistribueerd over meerdere ZRC- en DRC-instanties en daarmee een speciaal geval. Zie Many-to-many relaties verspreid over API's voor de designkeuzes hierbij.
Deze beslissing komt voort uit issue #166.
Tussen twee componenten A en B (met component A de component waar in het IM de relatie naar component B loopt), wordt de relatie in beide componenten bewaard. Eventuele metagegevens (extra informatie op de relatie) komt in component A te liggen, en component B bevat enkel de relatieinformatie zelf.
Dit is een technische oplossing zodat beide componenten de relaties kunnen opvragen zonder alle (mogelijks honderden) componenten af te moeten lopen om te vragen of ze een relatie hebben.
Een consumer maakt 1x een relatie aan, tussen component A en B, met metagegevens over de relatie in component A. Component A is vervolgens verantwoordelijk om de relatie in component B aan te maken.
We stemmen de objectTypes
af zodat die mappen op de namen van resources om
generieke synchronisatieimplementaties mogelijk te maken.
Component B moet dan aan de volgende spelregels voldoen:
- de naam van de resource is
{resourceB}{resourceA}
, in component B - de resource wordt genest ontsloten binnen
ResourceB
- de resource accepteert een URL-veld met de naam
resourceA
Deze spelregels en interactie worden actief getest in de integratietests om compliancy af te kunnen dwingen.
Een concreet voorbeeld hiervan is een INFORMATIEOBJECT
in het DRC en een
ZAAK
in het ZRC:
-
De consumer maakt in het DRC een relatie (met polymorfe relatieinformatie):
POST https://drc.nl/api/v1/objectinformatieobjecten { "informatieobject": "https://drc.nl/api/v1/enkelvoudigeinformatieobjecten/1234", "object": "https://zrc.nl/api/v1/zaken/456789", "objectType": "zaak", "titel": "", "beschrijving": "", "registratiedatum": "2018-09-19T17:57:08+0200" }
-
Het DRC doet vervolgens een request naar het ZRC (op basis van de URL van
object
):POST https://zrc.nl/api/v1/zaken/456789/zaakinformatieobjecten { "informatieobject": "https://drc.nl/api/v1/enkelvoudigeinformatieobjecten/1234", }
Een zaak wordt afgesloten door een eindstatus toe te kennen aan een ZAAK
. Elk
ZAAKTYPE
heeft minimaal één STATUSTYPE
. De eindstatus binnen een ZAAKTYPE
is het STATUSTYPE
met het hoogste volgnummer
.
Het toekennen van dit STATUSTYPE
aan een ZAAK
bepaalt ook een logisch af te
leiden ZAAK.einddatum
; dit is namelijk de datum waarop de eindstatus
is toegekend. Om die reden is ZAAK.einddatum
een alleen-lezen attribuut van
ZAAK
.
Als een ZAAK
een eindstatus heeft dan is de zaak afgesloten en mogen gegevens
van de zaak niet meer aangepast worden (behalve om redenen van correctie). Dit
is voorlopig een verantwoordelijkheid van de consumer en/of autorisatielaag.
In API-calls, kan de flow er als volgt uit zien:
- Consumer wil onderstaande
ZAAK
afsluiten:
GET /zrc/api/v1/zaken/12345
HTTP 200
{
"einddatum": null,
// ...
}
- Consumer wil onderstaande eindstatus zetten:
GET /ztc/api/v1/catalogus/12345/statustypen?zaaktype=/ztc/api/v1/zaaktype/44912
HTTP 200
[{
"uuid": 99321,
"volgnummer": 1,
"isEindstatus": false,
// ...
},{
"uuid": 67890,
"volgnummer": 2, # Het laatste STATUSTYPE binnen dit ZAAKTYPE
"isEindstatus": true,
// ...
}]
- Consumer werkt een
ZAAK
bij met de eindstatus:
POST /zrc/api/v1/zaakstatussen
{
"zaak": "/zrc/api/v1/zaken/45678",
"statustype": "/ztc/api/v1/catalogus/12345/statustypen/67890",
"datumStatusGezet": "2018-10-08T12:23:07+01:00",
// ...
}
- Consumer haalt de
ZAAK
opnieuw op:
GET /zrc/api/v1/zaken/12345
HTTP 200
{
"einddatum": "2018-10-08",
// ...
}
Rationale
In de ZDS 1.x standaard is er nog geen eenduidig besluit genomen over hoe een
zaak wordt afgesloten:
Dit kan door het toevoegen van de laatste status (STATUSTYPE
met het hoogste
volgnummer
) aan de ZAAK
of door het vullen van de einddatum
(van de
ZAAK
).
Er is echter behoefte aan een consistente manier om zaken af te sluiten. In
deze oplossing worden zowel ZAAK.einddatum
als STATUSTYPE
gebruikt waarbij
er geen onduidelijkheid meer ontstaat over hoe een ZAAK
wordt afgesloten.
De DSO schrijft voor om OAUTH2 te gebruiken op API niveau voor authenticatie en autorisatie. Kort samengevat is OAUTH2 te complex voor onze behoeften en maken we geen gebruik van de features ervan. We gebruiken JSON Web Tokens om autorisatie te regelen, en dan in de HMAC 'signed' vorm (dus geen assymetrische encryptie).
In de payload van het JWT voorzien we in een scopes
key, met als waarde
een lijst van strings die de scopes voorstellen:
{
"scopes": [
"zds.scopes.scope1",
"zds.scopes.scope2.1",
"zds.scopes.scope2.3",
]
}
(scope namen zijn fictief)
Rationale
OAUTH2 is een framework voor token-uitgifte, wat complex is voor server en client. De grondslag is delegation - de eindgebruiker geeft een applicatie toestemming om als die gebruiker acties uit te voeren en/of hun gegevens te gebruiken. Hierbij identificeert de applicatie zich bij de service, wat registratie van applicaties bij elke instantie van de zaken-APIs met zich meebrengt (en dus extra beheer).
Daarbij komt dat OAUTH2 geen authenticatie-service is, dus gemeentes moeten nog steeds een mechanisme hebben in de (centrale) authenticatie-service om een persoon te kunnen authenticeren met wat de gemeente gebruikt (bijvoorbeeld Active Directory).
OAUTH2 kent twee soorten tokens die uitgegeven worden - opake tokens en transparante tokens (zoals JWT). JWT wordt vaak gecombineerd met OAUTH2 omdat je claims kan meegeven en het token kan gevalideerd worden tegen tampering.
JWT wordt typisch in een federatiecontext gebruikt - hierbij wordt de gebruiker losgekoppeld van het token zelf en gaat het om wat er wel/niet kan. De claims in JWT kunnen volgens afspraak ingesteld worden, wat alle flexibiliteit toelaat.
Het grote voordeel van JWT is dat het stateless is, en er dus niet naar de autorisatieservice moet teruggekeerd worden om de claims te valideren.
Vergeleken met OAUTH2 biedt JWT ons alles wat we nodig hebben, en het is veel meer lichtgewicht.
Door de aard van de claims (aangeven van scopes), is het ook geen probleem dat de payload van de tokens niet versleuteld uitgewisseld wordt. Dit laat ons ook toe om bijvoorbeeld in NLX achter te kijken welke scopes/payloads over de lijn gingen - dit zou met encryptie niet mogelijk zijn.
In de JWT-situatie wordt de organisatie zelf ook verantwoorlijke voor de token-uitgifte. Organisaties hebben alle vrijheid om dit naar eigen wensen in te richten.
De volledige architectuur is uitgewerk in de documentatie
Enumeraties zijn in essentie lijstjes met mogelijke waarden. Deze waarden zijn semi-technisch van aard en geven niet altijd de volledige intentie aan van de waarde. Deze waarden staan op dit moment niet in het RGBZ.
We kiezen er voor waarden in zo compact mogelijke tekstuele representatie van
op te nemen in de OAS. Daarnaast mag voor elk attribuut dat een enumeratie
betreft, een weergave-attribuut worden toegevoegd als string
, die de
volledige tekstuele waarde als alleen-lezen teruggeeft. Zo'n attribuut eindigt
altijd op Weergave
.
In de omschrijving van het enumeratie-attribuut, staat de mapping omschreven tussen de waarde en de omschrijving die wordt getoond in het weergave-attribuut.
Voorbeelden:
---
taal: en
taalWeergave: Engels
In het schema:
---
taal:
type: string
description: |-
De taal afkorting volgens ISO 639-2:
* dut - Dutch
* eng - English
enum:
- dut
- eng
taalWeergave:
type: string
description: De volledige naam van de taal
In RGBZ worden bestandsnamen als gegevensgroep bestandsnaam
+ extensie
gespecifieerd. We kiezen ervoor om in de API deze te combineren in 1 veld
bestandsnaam
. Dit is in lijn met bestaande APIs in de industrie en ook
besturingssystemen slaan de twee als 1 entiteit op.
In het RGBZ zijn attributen i.v.m. archiveren opgenomen, afkomstig uit TMLO. Deze zijn te vaag gespecifieerd om er echt (geautomatiseerd) iets mee te kunnen doen. Daarom is een initiële lijst van gangbare checksum-algoritmes opgenomen als enum. Als er een landelijke referentielijst komt, dan zal de enum in een latere versie daarnaar verwijzen.
De kwestie is gedetailleerd beschreven in de landelijke KP-Api repository.
Een statische lijst is makkelijk aan versionering te koppelen, wat het voor partijen heel duidelijk maakt welke algoritmes ze moeten ondersteunen.