Gå til hovedinnhold

Koble en subgraf på tilgangsstyringen med MP-JWT og Postgres

Dette er en praktisk oppskrift for å koble en subgraf på den felles tilgangsstyringen ved hjelp av MicroProfile JWT (for å håndtere det interne tokenet) og Postgres (for å håndheve tilgangen). Mønsteret er uavhengig av kjøretidsmiljø og gjelder både Quarkus og WildFly.

For bakgrunnen – arkitekturen, det interne tokenet og hvorfor alt styres fra produksjon – se Tilgangsstyring.

Kort fortalt mottar subgrafen et internt, signert JWT fra supergrafen i headeren supergraph-authorization. Du setter opp MicroProfile JWT til å verifisere tokenet, videreformidler claimene derfra til Postgres som en session-variabel (GUC), og lar tilgangsreglene håndheves med row-level security (RLS) som leser den variabelen.

Ikke et delt bibliotek ennå

Mønsteret under er per i dag implementert i hver subgraf for seg – det finnes ikke et felles bibliotek å importere. Den anbefalte konvensjonen (app.claims) er den tilgangsstyringen selv bruker. En tilsvarende veiledning for subgrafer på Oracle kommer senere.

Sjekkliste

  1. Aktiver MicroProfile JWT.
  2. Konfigurer MicroProfile JWT.
  3. Les tokenet og hent ut claimene du trenger.
  4. Videreformidle claimene til Postgres i én app.claims-variabel.
  5. Håndhev tilgang med row-level security som leser app.claims.
  6. Velg oppførsel når token mangler.
  7. Legg til rette for lokal utvikling med «på vegne av»-headere.

1. Aktiver MicroProfile JWT

På Quarkus legger du til avhengigheten:

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>

På WildFly legger du til avhengigheten:

<dependency>
<groupId>org.eclipse.microprofile.jwt</groupId>
<artifactId>microprofile-jwt-auth-api</artifactId>
<scope>provided</scope>
</dependency>

I tillegg må du passe på å inkludere <layer>microprofile-jwt</layer> i konfigurasjonen.

2. Konfigurer MicroProfile JWT

I application.properties:

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 to ting:

  • publickey.location peker på produksjonsversjonen av tilgangsstyringen uansett hvilket miljø subgrafen kjører i. Det er bevisst – brukerens faktiske tilganger ligger i produksjon (se Tilgangsstyring fra produksjon i alle miljøer).
  • audiences settes til ditt eget miljø via ${deployment.environment}, slik at et token utstedt for ett miljø ikke godtas i et annet.

3. Les tokenet

Injiser JsonWebToken. Grunnen til å bruke Instance<JsonWebToken> (med isResolvable()) er for å støtte at MP-JWT-støtten kan være ukonfigurert i enkelte miljøer, for eksempel lokal testing. Da finnes det ingen JsonWebToken-bønne å injisere, og koden kan håndtere det i stedet for å feile. Fraværet av et token i en konkret forespørsel håndteres separat i steg 6. Når et token er til stede, er det allerede verifisert av MicroProfile JWT – signatur, issuer og audience er sjekket ut fra konfigen i steg 2:

@RequestScoped
public class AuthenticatedContextProvider {

@Inject
Instance<JsonWebToken> jwtInstance;

// ... fortsetter i steg 4
}

Claimene du normalt trenger er nin, email og orgtilganger. orgtilganger er et map fra organisasjonskode til liste av rollekoder, for eksempel { "9991": ["SE_OPPTAK", "MODIFISERE_OPPTAK"] }. Du kan selvsagt bruke denne informasjonen i services etc, men vi må i tillegg sende dem videre til databasen slik at RLS-regler kan bruke dem (steg 4 og 5).

4. Videreformidle claimene til Postgres

Sett claimene som én JSON-variabel, app.claims, på databasetilkoblingen før spørringene kjøres. Dette er den anbefalte konvensjonen, og den tilgangsstyringen selv bruker. Med jOOQ:

@RequestScoped
public class AuthenticatedContextProvider {

private static final List<String> FORWARDED_STRING_CLAIMS = List.of("email", "nin");

@Inject
Instance<JsonWebToken> jwtInstance;

@Inject
AgroalDataSource dataSource;

private Connection connection;
private DSLContext ctx;

@PostConstruct
void setup() throws SQLException {
connection = dataSource.getConnection();
ctx = DSL.using(connection, SQLDialect.POSTGRES);
// Sett alltid app.claims – også til et tomt sett uten token – slik at en
// gjenbrukt pool-tilkobling aldri arver forrige forespørsels claims.
JsonWebToken jwt = jwtInstance.isResolvable() ? jwtInstance.get() : null;
ctx.execute("SELECT set_config('app.claims', {0}, false)", claimsJson(jwt));
}

static String claimsJson(JsonWebToken jwt) {
JsonObjectBuilder b = Json.createObjectBuilder();
if (jwt != null) {
for (String name : FORWARDED_STRING_CLAIMS) {
String value = jwt.getClaim(name);
if (value != null) {
b.add(name, value);
}
}
JsonObject orgtilganger = jwt.getClaim("orgtilganger");
if (orgtilganger != null) {
b.add("orgtilganger", orgtilganger);
}
}
return b.build().toString(); // "{}" når token mangler
}

public DSLContext getContext() {
return ctx;
}

@PreDestroy
void releaseConnection() throws SQLException {
if (connection != null) {
connection.close();
}
}
}

Verdien som settes er hele det relevante claim-settet som JSON:

{
"email": "ola@ntnu.no",
"nin": "01234567890",
"orgtilganger": { "9991": ["SE_OPPTAK", "MODIFISERE_OPPTAK"] }
}
Én tilkobling per forespørsel

set_config('app.claims', …, false) gjelder for hele tilkoblingen, ikke bare én transaksjon, og en tilkoblingspool nullstiller den ikke automatisk ved gjenbruk. Derfor settes app.claims ved starten av hver forespørsel – også til et tomt sett når det ikke finnes token – slik at en gjenbrukt pool-tilkobling aldri arver en annen brukers claims. Prinsippet er at det er ved utsjekk av tilkoblingen – ved starten av forespørselen – at gjeldende tilganger fastsettes, ikke ved retur. @PreDestroy brukes her bare til å frigjøre tilkoblingen tilbake til poolen.

5. Håndhev tilgang med row-level security

Reglene i databasen leser app.claims og håndhever tilgangen. Lag hjelpefunksjoner som plukker ut det de trenger fra variabelen, for eksempel:

CREATE OR REPLACE FUNCTION auth.current_email() RETURNS text LANGUAGE sql STABLE AS $$
SELECT current_setting('app.claims', true)::jsonb ->> 'email'
$$;

orgtilganger parses tilsvarende til et sett av (organisasjonskode, rollekode), slik at man lett kan hente ut organisasjonene brukeren har en gitt rettighet til. Merk at rollesettet i tokenet allerede er ekspandert: rolleimplikasjoner (ADMINSKRIVLES) løses opp i coprocessoren før tokenet utstedes, så subgrafen og databasen trenger bare å sjekke om rollen er til stede. Disse brukes så i RLS-policyene på tabellene:

CREATE POLICY organisasjon_les ON minsubgraf.organisasjon
FOR SELECT
USING (
organisasjonskode IN (
SELECT auth.orgkoder_med_tilgang('SE_OPPTAK', auth.current_orgtilganger())
)
);

Funksjonene over – auth.current_orgtilganger(), auth.orgkoder_med_tilgang() med flere – er definert i tilgangsstyringens databasemigreringer, og kan brukes som mal for tilsvarende funksjoner i din egen base.

Slik blir håndhevingen liggende i databasen, der reglene står seg over tid og kan forvaltes som datapredikater – jf. hvorfor tilgangsregler håndheves i databasen.

6. Velg oppførsel når token mangler

Hvis supergraph-authorization mangler eller er ugyldig, settes app.claims til et tomt sett (se steg 4), og RLS-policyene returnerer ingenting for de organisasjonsstyrte tabellene. Globale, åpne tabeller kan fortsatt være lesbare.

I ikke-syntetiske miljøer (produksjon og produksjonslike) anbefaler vi for en ny subgraf å avvise forespørselen eksplisitt når et gyldig token mangler, i tråd med at vi ikke planlegger å støtte anonym tilgang. I syntetiske miljøer er bildet bevisst mer åpent, slik at utviklere kan opptre som ønsket bruker – se steg 7.

Dagens praksis varierer

Enkelte subgrafer faller i dag tilbake til en anonym kontekst (kun åpne data) i stedet for å avvise. Velger du det, gjør det til et bevisst, dokumentert valg – ikke en stilltiende default.

7. Legg til rette for lokal utvikling

Lokalt og i syntetiske miljøer skal en utvikler enkelt kunne opptre som ønsket bruker, uten Feide eller Maskinporten. Dette gjøres med «på vegne av»-headere, som kun er aktive når miljøet er merket som syntetisk:

# application-dev.properties
application.environment.synthetic=true
sudo-fodselsnr: 11111111111
sudo-organisasjonsnr: 999111111

Se på vegne av-funksjonalitet for hvordan dette henger sammen med tokenet. Funksjonaliteten skal aldri være aktiv i miljøer med ekte data.

Se også