Felles tilgangsstyring i FS-grafen
FS GraphQL API består av en supergraf med flere subgrafer. Alle forespørsler til APIet må tilgangsstyres, slik at vi kun gir tilgang til de dataene og handlingene avsenderen faktisk skal ha tilgang til.
Denne siden er produsentens referanse for hvordan dette henger sammen, og hvorfor arkitekturen ser ut som den gjør, og er ment for deg som utvikler en subgraf eller jobber med tilgangsstyringen selv. Hovedfokuset er infrastrukturen for tilgangsstyring; brukerflaten for å forvalte tilganger – FS Admin – er fortsatt under arbeid og dekkes senere. Konsumentenes oversikt over hvilke roller som finnes ligger i Tilgangsstyring med RAS, og det overordnede sikkerhetsbildet i Sikkerhet og tilgangskontroll.
Bakgrunn og begreper
Tilgangsstyring er hos oss et todelt begrep:
- Hvem forespørselen kommer fra – informasjonen vi har om avsenderen, uttrykt som claims.
- Tilgangsregler og håndheving av disse – reglene som avgjør hva avsenderen får gjøre, og mekanismen som håndhever dem.
Tidligere ble begge deler håndtert i hver enkelt subgraf, i applikasjons- og/eller databaselaget. Det førte til dobbeltarbeid og høyere risiko for feil: samme logikk måtte implementeres og vedlikeholdes flere steder, og det var vanskelig å jobbe effektivt på tvers av subgrafene. Derfor har vi samlet oss om en felles løsning og arkitektur, beskrevet i resten av dette dokumentet.
Hvem forespørselen kommer fra
En forespørsel kommer alltid fra en applikasjon. Dersom forespørselen i tillegg er gjort på vegne av en innlogget person, har vi også informasjon om vedkommende. Hvilke tilganger som faktisk foreligger, følger av tilgangsstyringen for både applikasjonen og personen. Det skal for eksempel være mulig for tredjeparter å lage applikasjoner som aldri får tilgang til mer enn det lærestedene tillater, uavhengig av hvem som logger inn.
Vi støtter i dag flere autentiseringsløsninger:
- Feide – både for innloggede personer (autorisasjonskode-flyt) og for maskin-til-maskin (klient-credentials).
- Maskinporten – for maskin-til-maskin-integrasjoner.
- Interne maskinbrukere – etablerte «wsbrukere» som autentiseres med brukernavn og passord (HTTP Basic).
Disse forteller oss ulike ting om avsenderen, noe som skaper en del kompleksitet. Den kompleksiteten håndteres ett sted: coprocessoren normaliserer informasjonen fra alle autentiseringsløsningene til ett felles format i det interne tokenet, slik at subgrafene kan behandle alle avsendere likt – uavhengig av hvordan de er autentisert. Forbedringer og nye autentiseringsløsninger gjøres dermed på ett sted og blir tilgjengelige for alle subgrafer gjennom vårt interne grensesnitt, uten endringer i den enkelte subgrafen.
Målarkitektur
Skissen under viser flyten i tilgangsstyringen.
På et overordnet nivå skjer følgende:
- En autentisert API-forespørsel kommer inn til supergrafen.
- Supergrafen verifiserer autentiseringen og veksler den inn i et internt JWT ved hjelp av coprocessoren.
- Denne beriker tokenet med informasjon fra tilgangsstyringsdatabasen.
- Supergrafen fødererer forespørselen videre til subgrafene med det interne JWT-et som autentiseringsinformasjon.
- Subgrafene utfører spørringene mot sine databaser og videreformidler brukerens claims dit, slik at tilgangsreglene kan håndheves der.
Det interne tokenet skal kun brukes mellom supergraf, subgrafer og databaser. Det er å regne som hemmelig og skal ikke eksponeres til applikasjoner.
Tilgangsstyrings-subgrafen leser fra tilgangsstyringsdatabasen i produksjon (AUTH (PROD))
uavhengig av miljø, slik skissen viser. Brukerens faktiske tilganger ligger per definisjon i
produksjon – se Tilgangsstyring fra produksjon i alle miljøer
og Eksponering av effektive tilganger.
Coprocessoren i supergrafen
Supergrafen utvides med en coprocessor. Den kobler seg på RouterRequest-fasen i forespørselens
livssyklus, leser den eksterne Authorization-headeren, autentiserer forespørselen, og erstatter
headeren med en intern header, supergraph-authorization, før forespørselen fødereres videre.
Coprocessoren autentiserer mot Feide og Maskinporten og slår opp interne maskinbrukere. Den veksler
informasjonen inn i et internt JWT som er normalisert (likt format uavhengig av
autentiseringsløsning) og beriket med claims fra tilgangsstyringsdatabasen, og signerer det. I
produksjon brukes ES256, og den offentlige nøkkelen publiseres på et /jwks.json-endepunkt som
subgrafene henter for å verifisere signaturen. Det interne tokenet har i dag en levetid på én time.
Coprocessoren har en innebygd tidsfrist (i dag 800 ms). Bruker den lengre tid enn dette, faller den tilbake til å sende forespørselen videre uten internt token – se Uautentiserte forespørsler.
Tilgangsmodellen
Berikelsen henter brukerens effektive tilganger fra databasen. Tilganger er modellert som kombinasjoner av organisasjonskode, rollekode og miljøkode, og settes sammen fra flere kilder:
| Kilde | Betydning |
|---|---|
| Egentilgang | Roller tildelt direkte til subjektet |
| Gulv | Minimumstilgang alle brukere i en organisasjon har |
| Tak | Det meste en applikasjon kan gjøre på vegne av en bruker |
| Åpne roller | Tilgang til offentlige data, for alle |
| Nekt | Inndragning av tilgang per bruker |
Den effektive tilgangen beregnes som et samspill mellom disse: når en bruker handler gjennom en
applikasjon, snittes brukerens roller (inkludert gulv) mot applikasjonens tak, åpne roller legges
til, og eventuelle nekt trekkes fra. Roller impliserer hverandre transitivt (for eksempel impliserer
ADMIN SKRIV, som impliserer LES), og et nekt drar med seg det som impliserer den nektede
rollen. Når en applikasjon opptrer som seg selv (uten innlogget bruker), er den effektive tilgangen
applikasjonens egne roller pluss åpne roller.
De fullstendige detaljene i tilgangsmodellen – kildene, mengde-operasjonene og rolleimplikasjonene – er dokumentert i tilgangsmodell.md.
Subgrafens ansvar
Subgrafene forholder seg ikke lenger til Feide eller eksterne token. De forholder seg kun til
det interne JWT-et, som de mottar i supergraph-authorization-headeren, og videreformidler claims
derfra til databasen, der tilgangsreglene håndheves.
Subgrafene bruker MicroProfile JWT eller tilsvarende for å verifisere og lese tokenet. Et representativt oppsett (fra SIS-subgrafen) ser slik ut:
mp.jwt.verify.publickey.location=http://production-tilgangsstyring.fs-fs-plattform.svc.cluster.local:8080/jwks.json
mp.jwt.verify.issuer=tilgangsstyring
mp.jwt.verify.audiences=${deployment.environment}
mp.jwt.verify.publickey.algorithm=ES256
mp.jwt.token.header=supergraph-authorization
Merk at publickey.location peker på produksjonsversjonen av tilgangsstyringen uavhengig av
hvilket miljø subgrafen kjører i, mens audiences settes til subgrafens eget miljø. Coprocessorens
endepunkt /coprocessor/{callingEnvironment} gjør at coprocessoren i produksjon kan utstede token
med riktig aud per kallende miljø. Dette er den konkrete mekanismen bak at all tilgangsstyring skjer
fra produksjon – se Tilgangsstyring fra produksjon i alle miljøer.
For en steg-for-steg-oppskrift for en subgraf med MP-JWT og Postgres, se HOWTO: Koble en subgraf på tilgangsstyringen med MP-JWT og Postgres.
Fra tokenet plukker subgrafen ut de claimene den trenger for håndheving – i SIS er det client_id,
nin og orgtilganger. Deretter settes brukerkonteksten mot databasen før spørringene kjøres.
SIS-subgrafen er spesiell: den kjører på WildFly mot Oracle og bruker
Oracle Real Application Security (RAS)
for håndheving. For hver institusjon forespørselen gjelder, oppretter og kobler subgrafen til en
RAS-sesjon (DBMS_XS_SESSIONS), setter applikasjonen (client_id) som brukernavn i sesjonen, og
aktiverer rollene fra orgtilganger for den aktuelle institusjonen. Datapredikatene i databasen
håndhever resten. De øvrige subgrafene (stort sett Quarkus mot Postgres) videreformidler de samme
claimene til sine databaser på tilsvarende vis.
Under migreringen støtter subgrafene fortsatt den gamle modellen som fallback: Feide-JWT terminert i
subgrafen via den ordinære Authorization-headeren, med rolleoppslag mot FSWS_AUTH-databasen. Den
nye modellen – internt token i supergraph-authorization med roller fra orgtilganger – er primær,
og den gamle veien fases ut så fort som mulig.
Uautentiserte forespørsler
Målet er at supergrafen skal stoppe uautentiserte eller ugyldige forespørsler sentralt, før de når
subgrafene. Dette er ikke aktivert ennå. I dagens løsning fører manglende eller ugyldig
autentisering (samt timeout i coprocessoren) til at forespørselen sendes videre uten
supergraph-authorization-header. Dermed faller ansvaret for å godkjenne eller avvise den på hver enkelt subgraf.
I SIS havner en slik forespørsel til slutt i en anonym kontekst uten roller, og blir dermed avvist
siden den ikke har tilgang til noen institusjon. Sentralisert avvisning i supergrafen skal på plass,
jf. prinsippet om at vi
ikke planlegger å støtte anonym tilgang.
Hvorfor tilgangsregler håndheves i databasen
Vi har valgt å kreve at tilgangsregler håndheves i databasen. Det følger blant annet av at:
- Databasen står seg bedre over tid enn applikasjonskoden. FS har eksistert i 30 år, og vi investerer nå for de neste 30. Erfaringen vår er at informasjonsmodellen for domenet vårt endrer seg saktere enn teknologien, og at databasene vi har brukt for å representere den, har vist seg mer tidsbestandige enn programmeringsspråkene og rammeverkene vi har brukt i applikasjonslaget. Tilgangsregler i databasen står seg dermed bedre over tid enn tilgangsregler i applikasjonskode.
- Tilgangsregler er forretningslogikk. Vi ønsker at domeneeksperter skal kunne bidra aktivt i arbeidet med å bestemme og forvalte reglene. Regler uttrykt som datapredikater fungerer godt for denne typen samarbeid.
- Flere applikasjoner deler databasene våre. Disse skal være underlagt de samme reglene for tilgangsstyring. Det er enklere å håndheve og vedlikeholde når reglene ligger ett sted – i databasen.
Av samme grunn ønsker vi å standardisere så langt det er hensiktsmessig både hvordan informasjon om brukeren videreformidles til databasen, og hvordan tilgangsreglene som bruker denne informasjonen skrives.
Tilgangsstyring fra produksjon i alle miljøer
Produksjonsmiljøet er den autoritative kilden for tilgangsstyring, uavhengig av miljø. Subgrafene i alle miljøer verifiserer det interne tokenet mot coprocessoren i produksjon, og brukerens effektive tilganger hentes alltid fra produksjon.
Begrunnelsen er prinsipiell:
- All tilgangsstyring skal skje fra produksjon, uavhengig av miljø. Brukere skal ikke måtte bytte til testmiljøet for å styre tilganger der.
- Testmiljøet inneholder per definisjon aldri ekte data. Tilganger tildeles ekte personer, og kan derfor ikke ligge i testmiljøet.
- Hvis en subgraf hentet tilganger fra en annen datakilde enn der brukerens faktiske tilganger ligger, ville svaret blitt feil. Derfor er det produksjonskilden som gjelder overalt.
Det samme prinsippet gjelder integrasjoner, for eksempel Felles IAM: endringer må gjøres mot produksjon for å ha effekt, mens testmiljøet trygt kan brukes til å teste integrasjonen uten å påvirke ekte tilganger.
Dette dokumentet beskriver infrastrukturen for tilgangsstyring. Selve brukerflaten for å forvalte tilganger, FS Admin, er under arbeid og ikke klar ennå. Intensjonen er at brukeradministratorer logger inn i FS Admin i produksjon for å endre tilganger, og at man kan teste selve brukeradministrasjonsfunksjonaliteten i testmiljøet ved å opptre som brukeradministrator – jf. på vegne av-funksjonaliteten – uten å påvirke andre.
På vegne av-funksjonalitet (kun test)
I testmiljøer ønsker vi ekte autentisering samtidig som miljøet ikke skal inneholde data om ekte personer. Derfor kan en applikasjon i disse miljøene overstyre claims – altså spesifisere hvem den ønsker å opptre som, og hvilke tilganger som skal være aktive – ved hjelp av egne headere:
| Header | Effekt |
|---|---|
sudo-username | Overstyrer email-claimet i tokenet |
sudo-fodselsnr | Overstyrer nin-claimet i tokenet |
sudo-roller | Overstyrer orgtilganger (format: 7777,ADMIN;7778,LES) |
I tillegg til de overstyrte verdiene inneholder tokenet et act-claim med identiteten til den
ekte innloggede brukeren (sub og eventuelt nin), slik at vi alltid kan spore hvem som faktisk
opptrådte på vegne av noen.
Denne funksjonaliteten er aktiv kun i miljøer som er definert som syntetiske
(synthetic-data-environments, i dag blant annet test, review, local og development). I
produksjon ignoreres disse headerne fullstendig.
Det interne JWT-et
Format og innhold i det interne tokenet er å regne som et internt API. Det betyr at vi kan iterere og forbedre det etter hvert som vi får mer erfaring. Innholdet skal derfor ikke regnes som en stabil kontrakt utenfor subgrafene.
Siden sub-claimet ofte brukes til logging, legger vi ikke fødselsnummer der. I stedet har vi et
eget nin-claim, slik at vi har bedre kontroll på bruken.
Sentrale claims i dag:
| Claim | Innhold |
|---|---|
iss | Alltid tilgangsstyring |
aud | Miljøkoden, f.eks. ["test"] eller ["production"] |
sub | Identifikator: Feide-UUID for en bruker, eller klient-id for en maskinbruker |
exp, iat | Utløp og utstedelsestidspunkt |
client_id | Applikasjonen forespørselen kommer fra |
amr | Autentiseringsmetode, f.eks. ["feide"], ["maskinporten"], ["internal"] |
name | Navn på innlogget person (kun ved personbruker) |
email | eduPersonPrincipalName for innlogget person (kun ved personbruker) |
nin | Fødselsnummer for innlogget person (kun ved personbruker) |
orgtilganger | Effektive tilganger: { "<organisasjonskode>": ["<rollekode>", …] } |
act | Ved på vegne av: identiteten til den ekte innloggede brukeren |
Tokenet inneholder i dag også et midlertidig hjemmeorganisasjonskode-claim. Det er en
mellomløsning og bør ikke bygges på som en varig del av kontrakten.
Eksempel – token ved personlig bruker (Feide):
{
"iss": "tilgangsstyring",
"aud": ["production"],
"sub": "feide-uuid-123",
"iat": 1737846000,
"exp": 1737849600,
"client_id": "04b32ca3-ab64-4d04-89f2-aa1dca79c4b3",
"amr": ["feide"],
"name": "Ola Nordmann",
"email": "ola@ntnu.no",
"nin": "01234567890",
"orgtilganger": {
"7777": ["STUDENTDATA_LES1", "PERSONDATA_LES1"]
}
}
Eksempel – token ved maskinbruker (Maskinporten):
{
"iss": "tilgangsstyring",
"aud": ["production"],
"sub": "data-sync-service",
"iat": 1737846000,
"exp": 1737849600,
"client_id": "data-sync-service",
"amr": ["maskinporten"],
"orgtilganger": {
"7777": ["STUDIEELEMENTER_LES1"]
}
}
Eksempel – token ved bruk av på vegne av-funksjonalitet (kun test):
{
"iss": "tilgangsstyring",
"aud": ["test"],
"sub": "feide-uuid-123",
"email": "test.student@uio.no",
"nin": "12345678901",
"orgtilganger": {
"7777": ["ADMIN"]
},
"act": {
"sub": "feide-uuid-123",
"nin": "01234567890"
}
}
Vær oppmerksom på at enkelte forespørsler kan få en stor mengde tilganger. Det finnes en øvre grense for hvor stort et JWT kan bli før det oppstår ytelsesutfordringer. Vi vet ikke nøyaktig hvor den grensen går, men det er et hensyn vi har hatt i bakhodet i designet. Skulle det bli et problem i praksis, er det lett å endre formatet til noe mer effektivt – men det ville samtidig gjort tokenet mer komplisert og vanskeligere å bruke, så vi velger bevisst den naive løsningen enn så lenge.
Eksponering av effektive tilganger via API
En applikasjon må kunne hente ut hvilke tilganger som foreligger for den autentiserte brukeren. FS
Admin trenger for eksempel dette for å kunne skjule eller deaktivere funksjonalitet som brukeren
ikke har tilgang til. Informasjonen finnes allerede i det interne tokenet (orgtilganger); poenget
er å eksponere den på en kontrollert måte gjennom grafen.
Det er nettopp av hensyn til dette at produksjonsversjonen av tilgangsstyrings-subgrafen er koblet opp i alle miljøer: brukerens faktiske tilganger kommer per definisjon fra produksjon, og svaret ville blitt feil om subgrafen gikk mot en annen datakilde enn den tilgangene faktisk stammer fra.
Selve API-feltet/spørringen som eksponerer effektive tilganger til konsumentene, er ennå ikke ferdig verifisert i implementasjonen og beskrives når det er på plass.
Lokalt utviklingsmiljø
Lokal utvikling er ikke underlagt målarkitekturen og skal ikke bruke coprocessoren i produksjon. I stedet skal det være trivielt for utviklere å opptre som ønsket bruker uten å måtte forholde seg til Feide, Maskinporten eller andre eksterne autentiseringsløsninger – i praksis ved hjelp av på vegne av-funksjonaliteten, som er aktiv i lokale og syntetiske miljøer.
Jobber du med autentisering mot Feide, Maskinporten eller liknende, er det nå i praksis coprocessoren du skal jobbe med. Hvordan den utviklingen gjøres, dokumenteres i coprocessorens egen dokumentasjon.
Risiko og driftshensyn
Arkitekturen gjør coprocessoren i produksjon til et single point of failure: dersom den feiler, vil alle forespørsler til APIet feile. Den blir også et kritisk punkt for eventuelle sikkerhetsproblemer. Fordelen med å samle ansvaret ett sted er at det blir enklere å innføre ekstra krav til kvalitetskontroll for nettopp coprocessoren.
Det er også noe risiko forbundet med at tilgangsstyringen for test- og demomiljøene gjøres fra produksjonsmiljøet. Både APIet og det grafiske brukergrensesnittet for brukeradministrasjon må derfor utformes slik at de reduserer risikoen for at en sluttbruker ved et uhell endrer tilganger knyttet til feil miljø.