-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Db v1 setup #280
base: main
Are you sure you want to change the base?
Db v1 setup #280
Changes from all commits
e2c4392
75b78fe
c37f9da
c1f408e
9530b41
b1f7d02
2aa11e8
75b77fd
8e47a13
e238b8b
0f8e278
b3b064c
e77a32e
ad1d644
c0dec2b
580833a
7753937
f1806be
42c9d9c
b1336fe
3b6b50a
cbf3e37
c0091f0
6a1974b
14f5cd2
759a916
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
:4444 { | ||
reverse_proxy https://localhost:4445 { | ||
transport http { | ||
tls_trusted_ca_certs server.pem | ||
tls_server_name db.localtest.me | ||
} | ||
header_up Host db.localtest.me:4445 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# hadolint global ignore=DL3008 | ||
|
||
ARG NEON_RELEASE_TAG=release-7845 | ||
|
||
FROM rust:bookworm AS rust-builder | ||
ARG NEON_RELEASE_TAG | ||
|
||
ARG DEBIAN_FRONTEND=noninteractive | ||
SHELL ["/bin/bash", "-o", "pipefail", "-c"] | ||
|
||
# install apt dependencies | ||
RUN \ | ||
apt-get update -qq \ | ||
&& apt-get install -qq --no-install-recommends -o DPkg::Options::=--force-confold -o DPkg::Options::=--force-confdef \ | ||
build-essential \ | ||
pkg-config \ | ||
git \ | ||
libssl-dev \ | ||
&& apt-get clean -qq && rm -rf /var/lib/apt/lists/* | ||
|
||
# get and build the proxy | ||
RUN git clone --depth=1 --branch $NEON_RELEASE_TAG https://github.com/neondatabase/neon.git | ||
WORKDIR /neon | ||
RUN cargo build --bin proxy --features "testing" | ||
|
||
|
||
FROM debian:bookworm-slim | ||
|
||
ARG DEBIAN_FRONTEND=noninteractive | ||
SHELL ["/bin/bash", "-o", "pipefail", "-c"] | ||
|
||
# install apt dependencies | ||
RUN \ | ||
apt-get update -qq \ | ||
&& apt-get install -qq --no-install-recommends -o DPkg::Options::=--force-confold -o DPkg::Options::=--force-confdef \ | ||
curl \ | ||
ca-certificates \ | ||
openssl \ | ||
postgresql-client \ | ||
&& apt-get clean -qq && rm -rf /var/lib/apt/lists/* | ||
|
||
# install caddy | ||
RUN \ | ||
apt-get update -qq \ | ||
&& apt-get install -qq --no-install-recommends -o DPkg::Options::=--force-confold -o DPkg::Options::=--force-confdef \ | ||
gnupg2 debian-keyring debian-archive-keyring apt-transport-https \ | ||
&& curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg2 --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg \ | ||
&& curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list \ | ||
&& apt-get update -qq \ | ||
&& apt-get install -qq --no-install-recommends -o DPkg::Options::=--force-confold -o DPkg::Options::=--force-confdef \ | ||
caddy \ | ||
&& apt-get clean -qq && rm -rf /var/lib/apt/lists/* | ||
|
||
WORKDIR /app | ||
|
||
# create a self-signed cert for *.localtest.me (see https://readme.localtest.me/) | ||
RUN openssl req -new -x509 \ | ||
-days 365 \ | ||
-nodes -text \ | ||
-out server.pem \ | ||
-keyout server.key \ | ||
-subj "/CN=*.localtest.me" \ | ||
-addext "subjectAltName = DNS:*.localtest.me" | ||
|
||
# copy the proxy binary | ||
COPY --from=rust-builder /neon/target/debug/proxy ./neon-proxy | ||
|
||
COPY ./Caddyfile Caddyfile | ||
COPY ./start.sh start.sh | ||
|
||
RUN chmod +x start.sh | ||
|
||
EXPOSE 4444 | ||
ENTRYPOINT ["./start.sh"] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
#!/bin/bash | ||
|
||
if [ -z "$PG_CONNECTION_STRING" ]; then | ||
echo "PG_CONNECTION_STRING is not set" | ||
exit 1 | ||
fi | ||
|
||
# Create required tables | ||
psql -Atx $PG_CONNECTION_STRING \ | ||
-c "CREATE SCHEMA IF NOT EXISTS neon_control_plane" \ | ||
-c "CREATE TABLE neon_control_plane.endpoints (endpoint_id VARCHAR(255) PRIMARY KEY, allowed_ips VARCHAR(255))" | ||
-c "CREATE TABLE neon_control_plane.endpoint_jwks (id VARCHAR PRIMARY KEY, jwks_url TEXT NOT NULL, audience TEXT NOT NULL, role_names TEXT[] NOT NULL, endpoint_id UUID NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now())" | ||
|
||
# Start the neon-proxy | ||
./neon-proxy \ | ||
-c server.pem \ | ||
-k server.key \ | ||
--auth-backend=postgres \ | ||
--auth-endpoint=$PG_CONNECTION_STRING \ | ||
--wss=0.0.0.0:4445 \ | ||
--is-auth-broker=true \ | ||
& | ||
|
||
# Start caddy reverse proxy | ||
caddy run \ | ||
--config ./Caddyfile \ | ||
--adapter caddyfile \ | ||
& | ||
|
||
wait -n | ||
exit $? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Not working at the moment... Would be nice to be able to get the actual pg_session_jwt extension working | ||
FROM rust:bookworm AS rust-builder | ||
SHELL ["/bin/bash", "-o", "pipefail", "-c"] | ||
|
||
RUN mkdir /ext-src | ||
WORKDIR /ext-src | ||
RUN apt-get update && apt-get install -y wget | ||
RUN wget https://github.com/neondatabase/pg_session_jwt/archive/refs/tags/v0.2.0.tar.gz -O pg_session_jwt.tar.gz && \ | ||
echo "5ace028e591f2e000ca10afa5b1ca62203ebff014c2907c0ec3b29c36f28a1bb pg_session_jwt.tar.gz" | sha256sum --check && \ | ||
mkdir pg_session_jwt-src && cd pg_session_jwt-src && tar xzf ../pg_session_jwt.tar.gz --strip-components=1 -C . && \ | ||
sed -i 's/pgrx = "0.12.6"/pgrx = { version = "0.12.9", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \ | ||
sed -i 's/version = "0.12.6"/version = "0.12.9"/g' pgrx-tests/Cargo.toml && \ | ||
sed -i 's/pgrx = "=0.12.6"/pgrx = { version = "=0.12.9", features = [ "unsafe-postgres" ] }/g' pgrx-tests/Cargo.toml && \ | ||
sed -i 's/pgrx-macros = "=0.12.6"/pgrx-macros = "=0.12.9"/g' pgrx-tests/Cargo.toml && \ | ||
sed -i 's/pgrx-pg-config = "=0.12.6"/pgrx-pg-config = "=0.12.9"/g' pgrx-tests/Cargo.toml | ||
RUN cargo pgrx install --release | ||
|
||
FROM postgres:latest | ||
|
||
COPY --from=rust-builder /ext-src /usr/share/postgresql/17/extension/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,44 @@ | ||
import {swagger} from '@elysiajs/swagger' | ||
import {Elysia} from 'elysia' | ||
import type {Database} from '@openint/db-v1' | ||
import {createDatabase, createNeonDatabase} from '@openint/db-v1' | ||
import {envRequired} from '@openint/env' | ||
import {createOpenApiHandler, createTrpcHandler} from './trpc/handlers' | ||
import {generateOpenAPISpec} from './trpc/openapi' | ||
|
||
async function checkLatency(db: Database) { | ||
const start = new Date() | ||
await db.execute('SELECT 1') | ||
const durationMs = Date.now() - start.getTime() | ||
return durationMs | ||
} | ||
|
||
export const app = new Elysia({prefix: '/api'}) | ||
.get('/health', () => ({status: 'ok'})) | ||
.get('/health', async () => { | ||
const postgresJsLatencyMs = await checkLatency( | ||
createDatabase({url: envRequired.DATABASE_URL}), | ||
) | ||
const neonLatencyMs = await checkLatency( | ||
createNeonDatabase({ | ||
url: envRequired.DATABASE_URL, | ||
}) as unknown as Database, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid unsafe casting: instead of casting createNeonDatabase to Database, update its type or interface to avoid the unknown cast. |
||
) | ||
return {healthy: true, postgresJsLatencyMs, neonLatencyMs} | ||
}) | ||
.use( | ||
swagger({ | ||
// For some reason spec.content doesn't work. so we are forced tos specify url instead | ||
scalarConfig: {spec: {url: '/api/v1/openapi.json'}}, | ||
path: '/v1', | ||
}), | ||
) | ||
// maybe this should be part of trpc and openapi spec itself? | ||
.get('/.well-known/jwks.json', () => { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const publicKeyJwk = JSON.parse(envRequired.NEXT_PUBLIC_JWT_PUBLIC_KEY) | ||
const jwks = {keys: [publicKeyJwk]} | ||
return jwks | ||
}) | ||
.get('/v1/openapi.json', () => generateOpenAPISpec({})) | ||
// These two ways of mounting are very inconsistent, but I don't know why. | ||
// empirically, the first one without * works for trpc, and the second one with * works for openapi | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import {exportJWK, generateKeyPair} from 'jose' | ||
import {z} from 'zod' | ||
|
||
const envName = z | ||
.enum(['development', 'preview', 'production']) | ||
.default('development') | ||
.parse(process.argv[2]) | ||
|
||
// Function to generate a new RSA key pair, converts the keys to JWK (JSON Web Key) format, | ||
// adds necessary metadata, and saves them to separate files. | ||
async function generateAndExportKeys() { | ||
// Generate a new RSA key pair using the RS256 algorithm. | ||
const {publicKey, privateKey} = await generateKeyPair('RS256', { | ||
extractable: true, | ||
}) | ||
// Convert the generated keys to JWK format | ||
const privateJwk = await exportJWK(privateKey) | ||
const publicJwk = await exportJWK(publicKey) | ||
// Add metadata to the private key JWK. | ||
// 'use': Indicates the key's intended use (e.g., 'sig' for signing). | ||
// 'kid': A unique identifier for the key, useful for key management and rotation. | ||
// 'alg': Specifies the algorithm to be used with the key (Neon RLS Authorize supports only RS256 and ES256 currently). | ||
privateJwk.use = 'sig' | ||
privateJwk.kid = `openint-${envName}` | ||
privateJwk.alg = 'RS256' | ||
// Add the same metadata to the public key JWK for consistency. | ||
publicJwk.use = 'sig' | ||
publicJwk.kid = `openint-${envName}` | ||
publicJwk.alg = 'RS256' | ||
// Save the keys to separate JSON files. | ||
console.log('--- privateKey.jwk.json') | ||
console.log(JSON.stringify(privateJwk)) | ||
console.log('--- publicKey.jwk.json') | ||
console.log(JSON.stringify(publicJwk)) | ||
console.log('--- Keys generated and saved to files.') | ||
} | ||
generateAndExportKeys() |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,12 +1,41 @@ | ||||||
import {initTRPC} from '@trpc/server' | ||||||
import {initTRPC, TRPCError} from '@trpc/server' | ||||||
import {type OpenApiMeta} from 'trpc-to-openapi' | ||||||
import type {Viewer} from '@openint/cdk' | ||||||
import {hasRole, type Viewer} from '@openint/cdk' | ||||||
import type {Database} from '@openint/db-v1' | ||||||
|
||||||
export interface RouterContext { | ||||||
viewer: Viewer | ||||||
db: Database | ||||||
} | ||||||
|
||||||
export const trpc = initTRPC.meta<OpenApiMeta>().context<RouterContext>().create() | ||||||
export const trpc = initTRPC | ||||||
.meta<OpenApiMeta>() | ||||||
.context<RouterContext>() | ||||||
.create() | ||||||
|
||||||
export const router = trpc.router | ||||||
export const publicProcedure = trpc.procedure | ||||||
|
||||||
export const authenticatedProcedure = publicProcedure.use(({next, ctx}) => { | ||||||
const viewer = ctx.viewer | ||||||
if (!hasRole(viewer, ['customer', 'user', 'org'])) { | ||||||
throw new TRPCError({code: 'FORBIDDEN', message: 'Admin only'}) | ||||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In authenticatedProcedure, the error message 'Admin only' may be misleading if non‐admin roles (like customer) are acceptable; consider a more generic access-denied message.
Suggested change
|
||||||
return next({ctx: {...ctx, viewer}}) | ||||||
}) | ||||||
|
||||||
export const customerProcedure = publicProcedure.use(({next, ctx}) => { | ||||||
const viewer = ctx.viewer | ||||||
if (!hasRole(ctx.viewer, ['customer'])) { | ||||||
throw new TRPCError({code: 'FORBIDDEN', message: 'Customer only'}) | ||||||
} | ||||||
return next({ctx: {...ctx, viewer}}) | ||||||
}) | ||||||
|
||||||
export const adminProcedure = publicProcedure.use(({next, ctx}) => { | ||||||
const viewer = ctx.viewer | ||||||
if (!hasRole(ctx.viewer, ['user', 'org'])) { | ||||||
throw new TRPCError({code: 'FORBIDDEN', message: 'Admin only'}) | ||||||
} | ||||||
return next({ctx: {...ctx, viewer}}) | ||||||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,9 @@ | ||
export function contextFromRequest(_req: Request) { | ||
return {viewer: {role: 'system' as const}} | ||
import {createDatabase} from '@openint/db-v1' | ||
import {env} from '@openint/env' | ||
import type {RouterContext} from './_base' | ||
|
||
export function contextFromRequest(_req: Request): RouterContext { | ||
// create db once rather than on every request | ||
const db = createDatabase({url: env.DATABASE_URL}) | ||
return {viewer: {role: 'system' as const}, db} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SQL commands to create the neon_control_plane tables work, but consider adding error checks to ensure these commands succeed before starting proxies.