Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.idea
*.tgz
/charts/postgrest/keyserver/test.*
2 changes: 1 addition & 1 deletion charts/postgrest/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: v2
name: postgrest
icon: https://docs.postgrest.org/en/v14/_images/postgrest.png
version: 0.2.5
version: 0.3.0
maintainers:
- name: jared-prime
email: jared.davis@pelo.tech
Expand Down
35 changes: 16 additions & 19 deletions charts/postgrest/Quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@

```shell
kubectl port-forward service/postgrest 30001:3000
kubectl port-forward service/postgrest 30002:8000

```

### anonymous
### anon - unauthenticated default

schema usage is allowed; therefore, basic information can be queried

Expand All @@ -26,42 +28,37 @@ but specific data must have permissions granted
curl localhost:30001/notes | jq
```

### authenticated - view
### peek - short-lived JWT token

first, construct a JWT
Using the keyserver api key, request a short-lived token

```shell
secret=a-string-secret-at-least-256-bits-long
_base64 () { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | _base64)
payload=$(echo -n "{\"role\":\"view\"}" | _base64)
signature=$(echo -n "$header.$payload" | openssl dgst -sha256 -hmac "$secret" -binary | _base64)
token=$(echo -n "$header.$payload.$signature")
export token=$(curl -H "Authorization: Bearer a-string-secret-at-least-256-bits-long" localhost:30002/peek | jq -r '.access_token')
```

you can now view
and make a request

```shell
curl -H "Authorization: Bearer $token" localhost:30001/notes | jq
```

but not edit
### view - authenticated by OIDC service

first, obtain a JWT from your OIDC service. The service's JWK *must* be loaded into the keyserver to verify the token. Then, can view

```shell
curl -H "Content-Type: application/json" -H "Authorization: Bearer $token" localhost:30001/notes -d '{"note":"meow"}'
curl -H "Authorization: Bearer $token" localhost:30001/notes | jq
```

### authenticated - edit

now construct a JWT with the edit role
but not edit

```shell
payload=$(echo -n "{\"role\":\"edit\"}" | _base64)
signature=$(echo -n "$header.$payload" | openssl dgst -sha256 -hmac "$secret" -binary | _base64)
token=$(echo -n "$header.$payload.$signature")
curl -H "Content-Type: application/json" -H "Authorization: Bearer $token" localhost:30001/notes -d '{"note":"meow"}'
```

you can still view
### edit - authenticated by OIDC service

Now obtain a JWT with the `edit` role. You can still view

```shell
curl -H "Authorization: Bearer $token" localhost:30001/notes | jq
Expand Down
2 changes: 2 additions & 0 deletions charts/postgrest/keyserver/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
test.env
test.jwks.json
13 changes: 13 additions & 0 deletions charts/postgrest/keyserver/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM denoland/deno:alpine

LABEL authors="Jared Davis <jared.davis@pelo.tech>"

WORKDIR /keyserver

COPY deno.json deno.json
COPY deno.lock deno.lock
COPY main.ts main.ts

RUN deno install --entrypoint main.ts

CMD [ "deno", "run", "--allow-net", "--allow-env", "--allow-read", "--allow-write", "main.ts" ]
10 changes: 10 additions & 0 deletions charts/postgrest/keyserver/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"tasks": {
"dev": "deno run --env-file=test.env --watch main.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@1",
"hono": "npm:hono@^4.11.5",
"jose": "npm:jose@^6.1.3"
}
}
36 changes: 36 additions & 0 deletions charts/postgrest/keyserver/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 110 additions & 0 deletions charts/postgrest/keyserver/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Hono } from 'hono';
import { generateKeyPair, JSONWebKeySet, JWK, calculateJwkThumbprintUri } from 'jose';
import { GenerateKeyPairResult } from 'jose/key/generate/keypair';
import { exportJWK } from "jose/key/export";
import { SignJWT } from "jose/jwt/sign";
import { cors } from 'hono/cors';

const path = Deno.env.get('PGRST_JWT_SECRET')?.replace('@', '') ?? '/tmp/jwks.json';
const claims = JSON.parse(Deno.env.get('PGRST_JWT_CLAIMS') ?? '{}');
const origin = Deno.env.get('PGRST_CLIENT_ORIGIN')?.split(',') ?? [];
const trusted = Deno.env.get('PGRST_JWK_TRUST')?.split(',') ?? [];
const cert = Deno.env.get('PGRST_JWK_CERT') ?? 'cert.pem';
const alg = Deno.env.get('PGRST_JWT_ALG') ?? 'RS256';
const iss = Deno.env.get('PGRST_JWT_ISS') ?? 'http://localhost:8000/jwks'
const aud = Deno.env.get('PGRST_JWT_AUD') ?? 'postgrest';
const exp = Deno.env.get('PGRST_JWT_EXP') ?? '5 minutes';
const sub = Deno.env.get('PGRST_JWT_SUB') ?? 'anon';
const api = Deno.env.get('PGRST_CLIENT_KEY') ?? '';

let keypair: GenerateKeyPairResult;
const initialize = async () => {
keypair = await generateKeyPair(alg, { extractable: true });
const keysets = await jwk(keypair.publicKey, ...await upstream());

keysets.keys[0].kid = await calculateJwkThumbprintUri(keysets.keys[0])

await write(keysets);
localStorage.setItem('jwk:kid', keysets.keys[0].kid);
localStorage.setItem('jwk:val', JSON.stringify(keysets.keys[0]))
}

const upstream = async (): Promise<Array<JWK>> => {
const certificate = await Deno.readTextFile(cert).catch((error) => {
console.warn(error)
return undefined
})
const caCerts = certificate ? [certificate] : [];

const client = Deno.createHttpClient({ caCerts })

const keyset = new Array<JWK>();

for await (const address of trusted) {
try {
const response = await fetch(address, { client })
console.log(response.status)
const data = await response.json() as JSONWebKeySet;
keyset.push(...data.keys)
} catch (error) {
console.warn(error)
}
}
console.debug(keyset);

return keyset;
}

const jwk = async (key: CryptoKey, ...jwks: Array<JWK>): Promise<JSONWebKeySet> => {
const jwk = await exportJWK(key);

const keys = [ jwk, ...jwks].filter(value => !!value);

return { keys }
}

const jwt = async (key: CryptoKey, kid?: string): Promise<string> => await new SignJWT(claims)
.setProtectedHeader({ alg, kid })
.setAudience(aud)
.setIssuedAt()
.setExpirationTime(exp)
.setSubject(sub)
.setIssuer(iss)
.sign(key);


const write = async (keys: JSONWebKeySet) => {
const file = await Deno.create(path);
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(keys))

await file.write(data)
}

const app = new Hono();

app.use('/jwks/.well-known/openid-configuration', cors({ origin }))
app.get('/jwks/.well-known/openid-configuration', (context) => {
const keyset = localStorage.getItem('jwk:val');

if (keyset) return context.json(JSON.parse(keyset));

return context.json({message:'not found'}, 404);
});

app.get('/', (context) => context.redirect('/jwks/.well-known/openid-configuration', 301));
app.get('/jwks', (context) => context.redirect('/jwks/.well-known/openid-configuration', 301));

app.get('/peek', async (context) => {
const auth = context.req.header('Authorization')?.replace('Bearer ', '');

if (api && api != auth) return context.json({message:'not authorized'}, 401)

const kid = localStorage.getItem('jwk:kid') ?? '';

return context.json({ access_token: await jwt(keypair.privateKey, kid) })
});

initialize().finally(() => console.log(`initialized application with kid ${localStorage.getItem('kid')}`));

Deno.serve(app.fetch);
10 changes: 10 additions & 0 deletions charts/postgrest/keyserver/test.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
PGRST_JWT_SECRET="test.jwks.json"
PGRST_JWT_CLAIMS="{\"mode\":\"test\"}"
PGRST_CLIENT_ORIGIN="https://jwt.io"
PGRST_JWK_TRUST="https://sso.localhost/auth/realms/od360-kind/protocol/openid-connect/certs"
PGRST_JWT_ALG="RS256"
PGRST_JWT_ISS="http://localhost:8000/jwks"
PGRST_JWT_AUD="postgrest"
PGRST_JWT_EXP="5 minutes"
PGRST_JWT_SUB="test"
PGRST_CLIENT_KEY="a-string-secret-at-least-256-bits-long"
4 changes: 4 additions & 0 deletions charts/postgrest/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@
{{- $hostname := .Values.database.migrations.hostname }}
{{- printf "user=%s password=%s host=%s dbname=%s sslmode=disable" $username $password $hostname $database }}
{{- end -}}

{{- define "postgrest.jwt.claims" }}
{{- printf "{%s:%s}" (.Values.application.jwt.claim | quote ) (.Values.application.anon | quote )}}
{{- end }}
35 changes: 35 additions & 0 deletions charts/postgrest/templates/configurations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: goose
data:
GOOSE_DRIVER: "postgres"

---

apiVersion: v1
kind: ConfigMap
metadata:
name: keyserver
data:
PGRST_JWT_ALG: {{ .Values.keyserver.jwt.alg | quote }}
PGRST_JWT_ISS: {{ .Values.keyserver.jwt.iss | quote }}
PGRST_JWT_EXP: {{ .Values.keyserver.jwt.exp | quote }}
PGRST_JWT_SUB: {{ .Values.keyserver.jwt.sub | quote }}
PGRST_CLIENT_ORIGIN: {{ .Values.keyserver.jwt.origin | quote }}
PGRST_JWK_TRUST: {{ .Values.keyserver.jwt.trust | quote }}
PGRST_JWT_CLAIMS: {{ .Values.keyserver.jwt.claims | toJson | quote }}

---

apiVersion: v1
kind: ConfigMap
metadata:
name: postgrest
data:
PGRST_DB_ANON_ROLE: {{ .Values.application.anon | quote }}
PGRST_DB_SCHEMAS: {{ .Values.application.schemas | quote }}
PGRST_JWT_ROLE_CLAIM_KEY: {{ .Values.application.jwt.claim.selector | quote }}
PGRST_JWT_SECRET: "@/etc/opt/postgrest/certificates/jwks.json"

---
Loading
Loading