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.
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
- Aktiver MicroProfile JWT.
- Konfigurer MicroProfile JWT.
- Les tokenet og hent ut claimene du trenger.
- Videreformidle claimene til Postgres i én
app.claims-variabel. - Håndhev tilgang med row-level security som leser
app.claims. - Velg oppførsel når token mangler.
- 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.locationpeker 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).audiencessettes 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"] }
}
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 (ADMIN ⊃ SKRIV ⊃ LES) 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.
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å
- Tilgangsstyring – arkitektur og «hvorfor»
- Supergraf