Gå til hovedinnhold

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.

Arkitekturskisse for felles tilgangsstyring

På et overordnet nivå skjer følgende:

  1. En autentisert API-forespørsel kommer inn til supergrafen.
  2. Supergrafen verifiserer autentiseringen og veksler den inn i et internt JWT ved hjelp av coprocessoren.
  3. Denne beriker tokenet med informasjon fra tilgangsstyringsdatabasen.
  4. Supergrafen fødererer forespørselen videre til subgrafene med det interne JWT-et som autentiseringsinformasjon.
  5. 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.

Om skissen

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:

Tilgangsmodellen
KildeBetydning
EgentilgangRoller tildelt direkte til subjektet
GulvMinimumstilgang alle brukere i en organisasjon har
TakDet meste en applikasjon kan gjøre på vegne av en bruker
Åpne rollerTilgang til offentlige data, for alle
NektInndragning 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.

Migrering fra gammel modell

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

Kjent gap – under arbeid

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.

Planlagt – brukeradministrasjon i FS Admin

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 brukeradministrasjons­funksjonaliteten 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:

På vegne av-funksjonalitet (kun test)
HeaderEffekt
sudo-usernameOverstyrer email-claimet i tokenet
sudo-fodselsnrOverstyrer nin-claimet i tokenet
sudo-rollerOverstyrer 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.

Kun i miljøer uten ekte data

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:

ClaimInnhold
issAlltid tilgangsstyring
audMiljøkoden, f.eks. ["test"] eller ["production"]
subIdentifikator: Feide-UUID for en bruker, eller klient-id for en maskinbruker
exp, iatUtløp og utstedelsestidspunkt
client_idApplikasjonen forespørselen kommer fra
amrAutentiseringsmetode, f.eks. ["feide"], ["maskinporten"], ["internal"]
nameNavn på innlogget person (kun ved personbruker)
emaileduPersonPrincipalName for innlogget person (kun ved personbruker)
ninFødselsnummer for innlogget person (kun ved personbruker)
orgtilgangerEffektive tilganger: { "<organisasjonskode>": ["<rollekode>", …] }
actVed på vegne av: identiteten til den ekte innloggede brukeren
Midlertidig claim

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.

Planlagt

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

Se også