From 84497c34212545a30d5480a9d8b37d0dc87566a7 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Wed, 30 Jul 2025 04:52:47 +0800 Subject: [PATCH 1/6] First round on devconainer - devcontainer.json - transient instanaces of couchdb and keycloak - script to populate them --- .devcontainer/Dockerfile | 7 + .devcontainer/devcontainer.json | 41 ++++++ .devcontainer/docker-compose.yml | 40 +++++ .devcontainer/jwks2couch.mjs | 242 +++++++++++++++++++++++++++++++ .devcontainer/postCreate.sh | 6 + .devcontainer/postStart.sh | 232 +++++++++++++++++++++++++++++ .gitignore | 3 + package-lock.json | 2 +- 8 files changed, 572 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml create mode 100644 .devcontainer/jwks2couch.mjs create mode 100755 .devcontainer/postCreate.sh create mode 100755 .devcontainer/postStart.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..178969679 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,7 @@ +FROM mcr.microsoft.com/devcontainers/base:ubuntu +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y \ + jq && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..46f0f29ad --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,41 @@ +{ + "name": "Fauxton & CouchDB", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/sshd:1": {}, + "ghcr.io/devcontainers/features/node:1": { + "version": "lts" + } + }, + "postCreateCommand": "./.devcontainer/postCreate.sh", + "postStartCommand": "./.devcontainer/postStart.sh", + "customizations": { + "vscode": { + "extensions": [ + "bierner.github-markdown-preview", + "bierner.markdown-mermaid", + "bpruitt-goddard.mermaid-markdown-syntax-highlighting", + "github.copilot", + "github.copilot-chat", + "ms-azuretools.vscode-docker", + "shengchen.vscode-checkstyle", + "visualstudioexptteam.vscodeintellicode", + "wix.vscode-import-cost" + ], + "settings": { + "prettier.enable": false, + "editor.formatOnSave": false + } + } + }, + "forwardPorts": [ + 8000, + 5984, + 8080, + 8090 + ] +} \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..c1a23c78a --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,40 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ../..:/workspaces:cached + command: sleep infinity + network_mode: service:couchdb + env_file: + - .env + depends_on: + - couchdb + + couchdb: + image: couchdb:latest + restart: unless-stopped + environment: + - COUCHDB_USER=${COUCHDB_USER} + - COUCHDB_PASSWORD=${COUCHDB_PASSWORD} + - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN} + - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} + - INSIDE_CONTAINER=true + env_file: + - .env + + keycloak: + image: quay.io/keycloak/keycloak:latest + restart: unless-stopped + environment: + - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN} + - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} + - KC_DB=dev-file + - KC_HTTP_ENABLED=true + - KC_HOSTNAME_STRICT=false + - KC_HOSTNAME_STRICT_HTTPS=false + network_mode: service:couchdb + env_file: + - .env + command: start-dev --http-port=8090 \ No newline at end of file diff --git a/.devcontainer/jwks2couch.mjs b/.devcontainer/jwks2couch.mjs new file mode 100644 index 000000000..50c5fa00e --- /dev/null +++ b/.devcontainer/jwks2couch.mjs @@ -0,0 +1,242 @@ +import crypto from 'crypto'; + +// Licensed under the Apache License, Version 2.0 (the "License"); you may not +// use this file except in compliance with the License. You may obtain a copy of +// the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +// License for the specific language governing permissions and limitations under +// the License. + +// This data +const config = { + "sourceUrl": `${process.env.KEYCLOAK || 'http://localhost:8090'}/realms/empire/.well-known/openid-configuration`, + "targetUrl": `${process.env.SRV ||'http://localhost:5984'}/_node/nonode@nohost/_config/jwt_keys`, + "adminCredentials": { + "username": COUCHDB_USER || "admin", + "password": process.env.COUCHDB_PASSWORD ||"password" + } +}; + +/** + * Converts RSA JWK to proper PEM format using Node.js crypto + */ +const rsaJwkToPem = (n, e) => { + try { + // Create RSA public key from JWK components + const keyObject = crypto.createPublicKey({ + key: { + kty: 'RSA', + n: n, + e: e + }, + format: 'jwk' + }); + + // Export as PEM + return keyObject.export({ + type: 'spki', + format: 'pem' + }); + } catch (error) { + throw new Error(`Failed to convert RSA JWK to PEM: ${error.message}`); + } +}; + +/** + * Converts EC JWK to proper PEM format using Node.js crypto + */ +const ecJwkToPem = (x, y, crv) => { + try { + // Create EC public key from JWK components + const keyObject = crypto.createPublicKey({ + key: { + kty: 'EC', + x: x, + y: y, + crv: crv + }, + format: 'jwk' + }); + + // Export as PEM + return keyObject.export({ + type: 'spki', + format: 'pem' + }); + } catch (error) { + throw new Error(`Failed to convert EC JWK to PEM: ${error.message}`); + } +}; + +/** + * Creates basic auth header for CouchDB + */ +const createAuthHeader = (username, password) => { + const credentials = Buffer.from(`${username}:${password}`).toString('base64'); + return `Basic ${credentials}`; +}; + +/** + * Makes a fetch request with error handling + */ +const fetchWithErrorHandling = async (url, options = {}) => { + const response = await fetch(url, { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'JWT-Magic-Script/1.0', + ...options.headers + }, + ...options + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + return await response.json(); + } + + return await response.text(); +}; + +/** + * Main function to process JWT keys + */ +const processJwtMagic = async () => { + try { + console.log('Starting JWT Magic script...'); + + console.log(`Source URL: ${config.sourceUrl}`); + console.log(`Target URL: ${config.targetUrl}`); + + // Fetch OpenID configuration + console.log('Fetching OpenID configuration...'); + const oidcData = await fetchWithErrorHandling(config.sourceUrl); + + if (!oidcData.jwks_uri) { + throw new Error('jwks_uri not found in OpenID configuration'); + } + + console.log(`JWKS URI found: ${oidcData.jwks_uri}`); + + // Fetch JWKS data + console.log('Fetching JWKS data...'); + const jwksData = await fetchWithErrorHandling(oidcData.jwks_uri); + + if (!jwksData.keys || !Array.isArray(jwksData.keys)) { + throw new Error('Invalid JWKS response: keys array not found'); + } + + console.log(`Found ${jwksData.keys.length} keys in JWKS`); + + // Process keys with "sig" use + const sigKeys = jwksData.keys.filter(key => key.use === 'sig'); + console.log(`Found ${sigKeys.length} signing keys`); + + if (sigKeys.length === 0) { + console.log('No signing keys found. Exiting.'); + return; + } + + // Prepare auth header for CouchDB + const authHeader = createAuthHeader( + config.adminCredentials.username, + config.adminCredentials.password + ); + + // Process each signing key + for (const key of sigKeys) { + try { + console.log(`Processing key: ${key.kid || 'unknown'}`); + + if (!key.kty) { + console.log(`Skipping key ${key.kid}: missing kty (key type)`); + continue; + } + + if (!key.kid) { + console.log(`Skipping key: missing kid (key ID)`); + continue; + } + + // Convert JWK to PEM format as required by CouchDB + let pemKey; + try { + if (key.kty === 'RSA') { + if (!key.n || !key.e) { + console.log(`Skipping RSA key ${key.kid}: missing n or e components`); + continue; + } + pemKey = rsaJwkToPem(key.n, key.e); + } else if (key.kty === 'EC') { + if (!key.x || !key.y) { + console.log(`Skipping EC key ${key.kid}: missing x or y coordinates`); + continue; + } + pemKey = ecJwkToPem(key.x, key.y, key.crv); + } else { + console.log(`Skipping key ${key.kid}: unsupported key type ${key.kty}`); + continue; + } + } catch (keyConvertError) { + console.log(`Skipping key ${key.kid}: error converting to PEM - ${keyConvertError.message}`); + continue; + } + + // Construct target URL: targetUrl + "/" + lowercase(kty) + ":" + kid + const documentId = `${key.kty.toLowerCase()}:${key.kid}`; + const targetDocUrl = `${config.targetUrl.replace(/\/$/, '')}/${documentId}`; + + console.log(`Posting to: ${targetDocUrl}`); + + // Store PEM format as single line with escaped newlines for CouchDB _config + const pemSingleLine = pemKey.replace(/\n/g, '\\n'); + const jsonValue = JSON.stringify(pemSingleLine); + + console.log(`Posting PEM key (single line): ${jsonValue}`); + + // Post to CouchDB _config endpoint + const response = await fetch(targetDocUrl, { + method: 'PUT', + headers: { + 'Authorization': authHeader, + 'Content-Type': 'application/json' + }, + body: jsonValue + }); + + if (response.ok) { + const responseData = await response.json(); + console.log(`✓ Successfully posted key ${key.kid} (${response.status})`); + if (responseData.rev) { + console.log(` Document revision: ${responseData.rev}`); + } + } else { + const errorText = await response.text(); + console.log(`⚠ Error posting key ${documentId}: ${response.status}`); + console.log(` Response: ${errorText}`); + } + + } catch (keyError) { + console.error(`✗ Error processing key ${key.kid || 'unknown'}:`, keyError.message); + } + } + + console.log('JWT Magic script completed successfully!'); + + } catch (error) { + console.error('Error in JWT Magic script:', error.message); + process.exit(1); + } +}; + +// Run the script +processJwtMagic(); \ No newline at end of file diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100755 index 000000000..fa8f6993e --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# Runs after dev container creation +npm install + +# Give containers some space +sleep 5 \ No newline at end of file diff --git a/.devcontainer/postStart.sh b/.devcontainer/postStart.sh new file mode 100755 index 000000000..65fb03ace --- /dev/null +++ b/.devcontainer/postStart.sh @@ -0,0 +1,232 @@ +#!/bin/bash +# Runs after dev container startup + +# Variables needed +now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +REALM=empire + +# Check required CouchDB environment variables +if [ -z "${COUCHDB_USER}" ] || [ -z "${COUCHDB_PASSWORD}" ]; then + echo "Error: COUCHDB_USER and COUCHDB_PASSWORD environment variables must be set" + exit 1 +fi +# Populate CouchDB +COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} +COUCHDB_PORT=5984 + +#Inside the container +if [ "${INSIDE_CONTAINER}" = "true" ]; then + export SRV=http://couchdb:${COUCHDB_PORT} + export KEYCLOAK=http://keycloak:8090 +else + export SRV=http://localhost:${COUCHDB_PORT} + export KEYCLOAK=http://localhost:8090 +fi + +#SYSTEM databases +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_users +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_replicator +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_global_changes + +#DEMO database +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/demo +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/demo/firstdoc -d '{"name" : "Peter Pan", "location" : "Neverland"}' | jq +curl -u ${COUCHDB_USRPWD} -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postCreate\"}" | jq + +# Status +curl -u ${COUCHDB_USRPWD} ${SRV} | jq + +# Session +curl -u ${COUCHDB_USRPWD} ${SRV}/_session | jq + +#DEMO back +curl -u ${COUCHDB_USRPWD} ${SRV}/demo/firstdoc | jq + + +# Check required Keycloak environment variables +if [ -z "${KEYCLOAK_ADMIN}" ] || [ -z "${KEYCLOAK_ADMIN_PASSWORD}" ]; then + echo "Error: KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD environment variables must be set" + exit 1 +fi + +# Populate Keycloak +curl ${KEYCLOAK} + +echo Admin login +KEYCLOAK_ACCESS_TOKEN=$(curl -X POST ${KEYCLOAK}/realms/master/protocol/openid-connect/token \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data client_id=admin-cli \ + --data "username=${KEYCLOAK_ADMIN}" \ + --data "password=${KEYCLOAK_ADMIN_PASSWORD}" \ + --data grant_type=password \ + --silent | jq -r '.access_token') + +echo Create realm +curl -X POST ${KEYCLOAK}/admin/realms \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header "content-type: application/json" \ + --data '{ + "id": "'"${REALM}"'", + "realm": "'"${REALM}"'", + "displayName": "The mighty realm of '"${REALM}"'", + "enabled": true, + "sslRequired": "NONE", + "registrationAllowed": true, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": true, + "bruteForceProtected": true +}' + +echo Create client fauxton +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/clients \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "clientId": "fauxton", + "name" : "CouchDB Fauxton", + "enabled": true, + "publicClient": true, + "directAccessGrantsEnabled": true, + "redirectUris":["http://localhost:8000","http://localhost:8000/"], + "webOrigins": ["localhost:8000"], + "protocolMappers": [ + { + "name": "email to sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "sub", + "jsonType.label": "String" + } + } + ] +}' + +echo Create role _admin +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "name":"_admin", + "description":"Full access" +}' + +echo retrive id for _admin +ROLE_ADMIN=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?first=0&max=101&search=_admin" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'caontent-type: application/json' | jq -r '.[0].id') + +echo create role role1 +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "name":"role1", + "description":"Member of role1" +}' + +echo retrieve id for role1 +ROLE_ROLE1=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?first=0&max=101&search=role1" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Create user Hari Seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "requiredActions": [], + "username": "hariseldon", + "enabled": true, + "firstName": "Hari", + "lastName": "Seldon", + "email": "hari.seldon@terminus.empire", + "emailVerified": true, + "credentials": [ + { + "type": "password", + "value": "password", + "temporary": false + } + ] +}' + +echo retrieve ID for user harisedon +USER_SELDON=$(curl "${KEYCLOAK}/admin/realms/${REALM}/ui-ext/brute-force-user?first=0&max=101&q=&search=hariseldon" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Create user Gaal Dornick +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "requiredActions": [], + "username": "gaaldornick", + "enabled": true, + "firstName": "Gaal", + "lastName": "Dornick", + "email": "gaal.dornick@terminus.empire", + "emailVerified": true, + "credentials": [ + { + "type": "password", + "value": "password", + "temporary": false + } + ] +}' + +echo retrive ID for user gaaldornick +USER_DORNICK=$(curl "${KEYCLOAK}/admin/realms/${REALM}/ui-ext/brute-force-user?first=0&max=101&q=&search=gaaldornick" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Assign role _admin to seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_SELDON}/role-mappings/realm/${ROLE_ADMIN} \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" + +echo Assign role role1 to seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_SELDON}/role-mappings/realm/${ROLE_ROLE1} \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" + +echo Assign role role1 to gaaldornick +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_DORNICK}/role-mappings/realm/${ROLE_ROLE1} \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" + +echo enable CouchDB JWT login +curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/chttpd/authentication_handlers" \ +-H "Content-Type: text/plain" \ +-d '"{chttpd_auth, cookie_authentication_handler}, {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, default_authentication_handler}"' + +echo point to idp_host +curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/jwt_auth/idp_host" \ + --header 'content-type: text/plain' \ + --data "\"${KEYCLOAK}/realms/${REALM}\"" + +echo set require exp,iat +curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/jwt_auth/required_claims" \ + --header 'content-type: text/plain' \ + --data "\"exp,iat,email\"" + +echo ADD PUblic key +node .devcontainer/jwks2couch.mjs + +echo Set path for role resolution +curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path" \ +-H "Content-Type: text/plain" \ +-d "\"realm_access.roles\"" + +echo Restart CouchDB +curl -u ${COUCHDB_USRPWD} -X POST "${SRV}/_node/_local/_restart" + +echo DONE \ No newline at end of file diff --git a/.gitignore b/.gitignore index 96b40103b..e0e1bca78 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ coverage # IDEs .idea/ .vscode + +# Env variables +.env diff --git a/package-lock.json b/package-lock.json index a562ea345..b5d68d4fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,7 +70,7 @@ "babel-loader": "^8.2.5", "babel-plugin-array-includes": "^2.0.3", "bootstrap": "^5.2.3", - "chromedriver": "*", + "chromedriver": "latest", "css-loader": "^6.7.1", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.6", From 522b7b1d8898b945256445e666cb18a371420dae Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Tue, 29 Jul 2025 21:26:31 +0000 Subject: [PATCH 2/6] Swap start & create --- .devcontainer/docker-compose.yml | 2 +- .devcontainer/jwks2couch.mjs | 2 +- .devcontainer/postCreate.sh | 229 ++++++++++++++++++++++++++++++- .devcontainer/postStart.sh | 228 +----------------------------- 4 files changed, 234 insertions(+), 227 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index c1a23c78a..fa1aa35be 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -11,6 +11,7 @@ services: - .env depends_on: - couchdb + - keycloak couchdb: image: couchdb:latest @@ -20,7 +21,6 @@ services: - COUCHDB_PASSWORD=${COUCHDB_PASSWORD} - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN} - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} - - INSIDE_CONTAINER=true env_file: - .env diff --git a/.devcontainer/jwks2couch.mjs b/.devcontainer/jwks2couch.mjs index 50c5fa00e..b6fd4940f 100644 --- a/.devcontainer/jwks2couch.mjs +++ b/.devcontainer/jwks2couch.mjs @@ -17,7 +17,7 @@ const config = { "sourceUrl": `${process.env.KEYCLOAK || 'http://localhost:8090'}/realms/empire/.well-known/openid-configuration`, "targetUrl": `${process.env.SRV ||'http://localhost:5984'}/_node/nonode@nohost/_config/jwt_keys`, "adminCredentials": { - "username": COUCHDB_USER || "admin", + "username": process.env.COUCHDB_USER || "admin", "password": process.env.COUCHDB_PASSWORD ||"password" } }; diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index fa8f6993e..ab8505656 100755 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -3,4 +3,231 @@ npm install # Give containers some space -sleep 5 \ No newline at end of file +sleep 15 +# Variables needed +now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +REALM=empire + +# Check required CouchDB environment variables +if [ -z "${COUCHDB_USER}" ] || [ -z "${COUCHDB_PASSWORD}" ]; then + echo "Error: COUCHDB_USER and COUCHDB_PASSWORD environment variables must be set" + exit 1 +fi +# Populate CouchDB +COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} +COUCHDB_PORT=5984 + +export SRV=http://localhost:${COUCHDB_PORT} +export KEYCLOAK=http://localhost:8090 + + +echo Couch $SRV +echo keycloak $KEYCLOAK + +#SYSTEM databases +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_users +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_replicator +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_global_changes + +#DEMO database +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/demo +curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/demo/firstdoc -d '{"name" : "Peter Pan", "location" : "Neverland"}' | jq +curl -u ${COUCHDB_USRPWD} -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postCreate\"}" | jq + +# Status +curl -u ${COUCHDB_USRPWD} ${SRV} | jq + +# Session +curl -u ${COUCHDB_USRPWD} ${SRV}/_session | jq + +#DEMO back +curl -u ${COUCHDB_USRPWD} ${SRV}/demo/firstdoc | jq + + +# Check required Keycloak environment variables +if [ -z "${KEYCLOAK_ADMIN}" ] || [ -z "${KEYCLOAK_ADMIN_PASSWORD}" ]; then + echo "Error: KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD environment variables must be set" + exit 1 +fi + +# Populate Keycloak +curl ${KEYCLOAK} + +echo Admin login +KEYCLOAK_ACCESS_TOKEN=$(curl -X POST ${KEYCLOAK}/realms/master/protocol/openid-connect/token \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data client_id=admin-cli \ + --data "username=${KEYCLOAK_ADMIN}" \ + --data "password=${KEYCLOAK_ADMIN_PASSWORD}" \ + --data grant_type=password \ + --silent | jq -r '.access_token') + +echo Create realm +curl -X POST ${KEYCLOAK}/admin/realms \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header "content-type: application/json" \ + --data '{ + "id": "'"${REALM}"'", + "realm": "'"${REALM}"'", + "displayName": "The mighty realm of '"${REALM}"'", + "enabled": true, + "sslRequired": "NONE", + "registrationAllowed": true, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": true, + "bruteForceProtected": true +}' + +echo Create client fauxton +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/clients \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "clientId": "fauxton", + "name" : "CouchDB Fauxton", + "enabled": true, + "publicClient": true, + "directAccessGrantsEnabled": true, + "redirectUris":["http://localhost:8000","http://localhost:8000/"], + "webOrigins": ["localhost:8000"], + "protocolMappers": [ + { + "name": "email to sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "sub", + "jsonType.label": "String" + } + } + ] +}' + +echo Create role _admin +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "name":"_admin", + "description":"Full access" +}' + +echo retrive id for _admin +ROLE_ADMIN=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?first=0&max=101&search=_admin" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'caontent-type: application/json' | jq -r '.[0].id') + +echo create role role1 +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "name":"role1", + "description":"Member of role1" +}' + +echo retrieve id for role1 +ROLE_ROLE1=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?first=0&max=101&search=role1" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Create user Hari Seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "requiredActions": [], + "username": "hariseldon", + "enabled": true, + "firstName": "Hari", + "lastName": "Seldon", + "email": "hari.seldon@terminus.empire", + "emailVerified": true, + "credentials": [ + { + "type": "password", + "value": "password", + "temporary": false + } + ] +}' + +echo retrieve ID for user harisedon +USER_SELDON=$(curl "${KEYCLOAK}/admin/realms/${REALM}/ui-ext/brute-force-user?first=0&max=101&q=&search=hariseldon" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Create user Gaal Dornick +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "requiredActions": [], + "username": "gaaldornick", + "enabled": true, + "firstName": "Gaal", + "lastName": "Dornick", + "email": "gaal.dornick@terminus.empire", + "emailVerified": true, + "credentials": [ + { + "type": "password", + "value": "password", + "temporary": false + } + ] +}' + +echo retrive ID for user gaaldornick +USER_DORNICK=$(curl "${KEYCLOAK}/admin/realms/${REALM}/ui-ext/brute-force-user?first=0&max=101&q=&search=gaaldornick" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Assign role _admin to seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_SELDON}/role-mappings/realm/${ROLE_ADMIN} \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" + +echo Assign role role1 to seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_SELDON}/role-mappings/realm/${ROLE_ROLE1} \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" + +echo Assign role role1 to gaaldornick +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_DORNICK}/role-mappings/realm/${ROLE_ROLE1} \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" + +echo enable CouchDB JWT login +curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/chttpd/authentication_handlers" \ +-H "Content-Type: text/plain" \ +-d '"{chttpd_auth, cookie_authentication_handler}, {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, default_authentication_handler}"' + +echo point to idp_host +curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/jwt_auth/idp_host" \ + --header 'content-type: text/plain' \ + --data "\"${KEYCLOAK}/realms/${REALM}\"" + +echo set require exp,iat +curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/jwt_auth/required_claims" \ + --header 'content-type: text/plain' \ + --data "\"exp,iat\"" + +echo ADD PUblic key +node .devcontainer/jwks2couch.mjs + +echo Set path for role resolution +curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path" \ +-H "Content-Type: text/plain" \ +-d "\"realm_access.roles\"" + +echo Restart CouchDB +curl -u ${COUCHDB_USRPWD} -X POST "${SRV}/_node/_local/_restart" + +echo DONE \ No newline at end of file diff --git a/.devcontainer/postStart.sh b/.devcontainer/postStart.sh index 65fb03ace..ad57c65de 100755 --- a/.devcontainer/postStart.sh +++ b/.devcontainer/postStart.sh @@ -1,232 +1,12 @@ #!/bin/bash # Runs after dev container startup - -# Variables needed now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -REALM=empire +# Give containers some space +sleep 5 -# Check required CouchDB environment variables -if [ -z "${COUCHDB_USER}" ] || [ -z "${COUCHDB_PASSWORD}" ]; then - echo "Error: COUCHDB_USER and COUCHDB_PASSWORD environment variables must be set" - exit 1 -fi -# Populate CouchDB COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} COUCHDB_PORT=5984 -#Inside the container -if [ "${INSIDE_CONTAINER}" = "true" ]; then - export SRV=http://couchdb:${COUCHDB_PORT} - export KEYCLOAK=http://keycloak:8090 -else - export SRV=http://localhost:${COUCHDB_PORT} - export KEYCLOAK=http://localhost:8090 -fi - -#SYSTEM databases -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_users -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_replicator -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_global_changes - -#DEMO database -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/demo -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/demo/firstdoc -d '{"name" : "Peter Pan", "location" : "Neverland"}' | jq -curl -u ${COUCHDB_USRPWD} -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postCreate\"}" | jq - -# Status -curl -u ${COUCHDB_USRPWD} ${SRV} | jq - -# Session -curl -u ${COUCHDB_USRPWD} ${SRV}/_session | jq - -#DEMO back -curl -u ${COUCHDB_USRPWD} ${SRV}/demo/firstdoc | jq - - -# Check required Keycloak environment variables -if [ -z "${KEYCLOAK_ADMIN}" ] || [ -z "${KEYCLOAK_ADMIN_PASSWORD}" ]; then - echo "Error: KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD environment variables must be set" - exit 1 -fi - -# Populate Keycloak -curl ${KEYCLOAK} - -echo Admin login -KEYCLOAK_ACCESS_TOKEN=$(curl -X POST ${KEYCLOAK}/realms/master/protocol/openid-connect/token \ - --header 'content-type: application/x-www-form-urlencoded' \ - --data client_id=admin-cli \ - --data "username=${KEYCLOAK_ADMIN}" \ - --data "password=${KEYCLOAK_ADMIN_PASSWORD}" \ - --data grant_type=password \ - --silent | jq -r '.access_token') - -echo Create realm -curl -X POST ${KEYCLOAK}/admin/realms \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header "content-type: application/json" \ - --data '{ - "id": "'"${REALM}"'", - "realm": "'"${REALM}"'", - "displayName": "The mighty realm of '"${REALM}"'", - "enabled": true, - "sslRequired": "NONE", - "registrationAllowed": true, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": true, - "editUsernameAllowed": true, - "bruteForceProtected": true -}' - -echo Create client fauxton -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/clients \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "clientId": "fauxton", - "name" : "CouchDB Fauxton", - "enabled": true, - "publicClient": true, - "directAccessGrantsEnabled": true, - "redirectUris":["http://localhost:8000","http://localhost:8000/"], - "webOrigins": ["localhost:8000"], - "protocolMappers": [ - { - "name": "email to sub", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "lightweight.claim": "false", - "access.token.claim": "true", - "claim.name": "sub", - "jsonType.label": "String" - } - } - ] -}' - -echo Create role _admin -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "name":"_admin", - "description":"Full access" -}' - -echo retrive id for _admin -ROLE_ADMIN=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?first=0&max=101&search=_admin" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'caontent-type: application/json' | jq -r '.[0].id') - -echo create role role1 -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "name":"role1", - "description":"Member of role1" -}' - -echo retrieve id for role1 -ROLE_ROLE1=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?first=0&max=101&search=role1" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' | jq -r '.[0].id') - -echo Create user Hari Seldon -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "requiredActions": [], - "username": "hariseldon", - "enabled": true, - "firstName": "Hari", - "lastName": "Seldon", - "email": "hari.seldon@terminus.empire", - "emailVerified": true, - "credentials": [ - { - "type": "password", - "value": "password", - "temporary": false - } - ] -}' - -echo retrieve ID for user harisedon -USER_SELDON=$(curl "${KEYCLOAK}/admin/realms/${REALM}/ui-ext/brute-force-user?first=0&max=101&q=&search=hariseldon" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' | jq -r '.[0].id') - -echo Create user Gaal Dornick -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "requiredActions": [], - "username": "gaaldornick", - "enabled": true, - "firstName": "Gaal", - "lastName": "Dornick", - "email": "gaal.dornick@terminus.empire", - "emailVerified": true, - "credentials": [ - { - "type": "password", - "value": "password", - "temporary": false - } - ] -}' - -echo retrive ID for user gaaldornick -USER_DORNICK=$(curl "${KEYCLOAK}/admin/realms/${REALM}/ui-ext/brute-force-user?first=0&max=101&q=&search=gaaldornick" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' | jq -r '.[0].id') - -echo Assign role _admin to seldon -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_SELDON}/role-mappings/realm/${ROLE_ADMIN} \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" - -echo Assign role role1 to seldon -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_SELDON}/role-mappings/realm/${ROLE_ROLE1} \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" - -echo Assign role role1 to gaaldornick -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_DORNICK}/role-mappings/realm/${ROLE_ROLE1} \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" - -echo enable CouchDB JWT login -curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/chttpd/authentication_handlers" \ --H "Content-Type: text/plain" \ --d '"{chttpd_auth, cookie_authentication_handler}, {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, default_authentication_handler}"' - -echo point to idp_host -curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/jwt_auth/idp_host" \ - --header 'content-type: text/plain' \ - --data "\"${KEYCLOAK}/realms/${REALM}\"" - -echo set require exp,iat -curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/jwt_auth/required_claims" \ - --header 'content-type: text/plain' \ - --data "\"exp,iat,email\"" - -echo ADD PUblic key -node .devcontainer/jwks2couch.mjs - -echo Set path for role resolution -curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path" \ --H "Content-Type: text/plain" \ --d "\"realm_access.roles\"" - -echo Restart CouchDB -curl -u ${COUCHDB_USRPWD} -X POST "${SRV}/_node/_local/_restart" +export SRV=http://localhost:${COUCHDB_PORT} -echo DONE \ No newline at end of file +curl -u ${COUCHDB_USRPWD} -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postCreate\"}" | jq \ No newline at end of file From 5259b9fbbc36af9deda4e68370986e20af4efe9d Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Wed, 30 Jul 2025 18:04:06 +0000 Subject: [PATCH 3/6] apply Coderabbit --- .devcontainer/Dockerfile | 3 +- .devcontainer/docker-compose.yml | 10 +++-- .devcontainer/jwks2couch.mjs | 6 +-- .devcontainer/postCreate.sh | 67 +++++++++++++++++++------------- .devcontainer/postStart.sh | 4 +- readme.md | 37 +++++++++++++++--- 6 files changed, 86 insertions(+), 41 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 178969679..22863b7cc 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,7 +1,6 @@ FROM mcr.microsoft.com/devcontainers/base:ubuntu RUN apt-get update && \ apt-get upgrade -y && \ - apt-get install -y \ - jq && \ + apt-get install -y --no-install-recommends jq && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index fa1aa35be..4fe8c92a1 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -6,7 +6,7 @@ services: volumes: - ../..:/workspaces:cached command: sleep infinity - network_mode: service:couchdb + networks: [devnet] env_file: - .env depends_on: @@ -21,6 +21,7 @@ services: - COUCHDB_PASSWORD=${COUCHDB_PASSWORD} - KEYCLOAK_ADMIN=${KEYCLOAK_ADMIN} - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD} + networks: [devnet] env_file: - .env @@ -34,7 +35,10 @@ services: - KC_HTTP_ENABLED=true - KC_HOSTNAME_STRICT=false - KC_HOSTNAME_STRICT_HTTPS=false - network_mode: service:couchdb + networks: [devnet] env_file: - .env - command: start-dev --http-port=8090 \ No newline at end of file + command: start-dev --http-port=8090 + +networks: + devnet: diff --git a/.devcontainer/jwks2couch.mjs b/.devcontainer/jwks2couch.mjs index b6fd4940f..a916de7ec 100644 --- a/.devcontainer/jwks2couch.mjs +++ b/.devcontainer/jwks2couch.mjs @@ -1,5 +1,3 @@ -import crypto from 'crypto'; - // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at @@ -12,13 +10,15 @@ import crypto from 'crypto'; // License for the specific language governing permissions and limitations under // the License. +import crypto from 'crypto'; + // This data const config = { "sourceUrl": `${process.env.KEYCLOAK || 'http://localhost:8090'}/realms/empire/.well-known/openid-configuration`, "targetUrl": `${process.env.SRV ||'http://localhost:5984'}/_node/nonode@nohost/_config/jwt_keys`, "adminCredentials": { "username": process.env.COUCHDB_USER || "admin", - "password": process.env.COUCHDB_PASSWORD ||"password" + "password": process.env.COUCHDB_PASSWORD ||"password" } }; diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index ab8505656..a48a0aa01 100755 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -3,7 +3,11 @@ npm install # Give containers some space -sleep 15 +# Wait until CouchDB answers +until curl -fsS "http://localhost:5984/_up" >/dev/null 2>&1; do sleep 2; done +# Wait until Keycloak answers +until curl -fsS "http://localhost:8090/realms/master" >/dev/null 2>&1; do sleep 2; done + # Variables needed now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") REALM=empire @@ -25,23 +29,23 @@ echo Couch $SRV echo keycloak $KEYCLOAK #SYSTEM databases -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_users -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_replicator -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/_global_changes +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_users +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_replicator +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_global_changes #DEMO database -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/demo -curl -u ${COUCHDB_USRPWD} -X PUT ${SRV}/demo/firstdoc -d '{"name" : "Peter Pan", "location" : "Neverland"}' | jq -curl -u ${COUCHDB_USRPWD} -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postCreate\"}" | jq +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/demo +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/demo/firstdoc -d '{"name" : "Peter Pan", "location" : "Neverland"}' | jq +curl -u "${COUCHDB_USRPWD}" -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postCreate\"}" | jq # Status -curl -u ${COUCHDB_USRPWD} ${SRV} | jq +curl -u "${COUCHDB_USRPWD}" ${SRV} | jq # Session -curl -u ${COUCHDB_USRPWD} ${SRV}/_session | jq +curl -u "${COUCHDB_USRPWD}" ${SRV}/_session | jq #DEMO back -curl -u ${COUCHDB_USRPWD} ${SRV}/demo/firstdoc | jq +curl -u "${COUCHDB_USRPWD}" ${SRV}/demo/firstdoc | jq # Check required Keycloak environment variables @@ -122,9 +126,9 @@ curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ }' echo retrive id for _admin -ROLE_ADMIN=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?first=0&max=101&search=_admin" \ +ROLE_ADMIN=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?search=_admin" \ --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'caontent-type: application/json' | jq -r '.[0].id') + --header 'content-type: application/json' | jq -r '.[0].id') echo create role role1 curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ @@ -136,7 +140,7 @@ curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ }' echo retrieve id for role1 -ROLE_ROLE1=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?first=0&max=101&search=role1" \ +ROLE_ROLE1=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?search=role1" \ --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ --header 'content-type: application/json' | jq -r '.[0].id') @@ -162,7 +166,7 @@ curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ }' echo retrieve ID for user harisedon -USER_SELDON=$(curl "${KEYCLOAK}/admin/realms/${REALM}/ui-ext/brute-force-user?first=0&max=101&q=&search=hariseldon" \ +USER_SELDON=$(curl "${KEYCLOAK}/admin/realms/${REALM}/users?username=hariseldon" \ --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ --header 'content-type: application/json' | jq -r '.[0].id') @@ -188,46 +192,55 @@ curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ }' echo retrive ID for user gaaldornick -USER_DORNICK=$(curl "${KEYCLOAK}/admin/realms/${REALM}/ui-ext/brute-force-user?first=0&max=101&q=&search=gaaldornick" \ +USER_DORNICK=$(curl "${KEYCLOAK}/admin/realms/${REALM}/users?username=gaaldornick" \ --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ --header 'content-type: application/json' | jq -r '.[0].id') echo Assign role _admin to seldon -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_SELDON}/role-mappings/realm/${ROLE_ADMIN} \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_SELDON}"/role-mappings/realm/"${ROLE_ADMIN}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ADMIN}\",\"name\":\"_admin\"}]" echo Assign role role1 to seldon -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_SELDON}/role-mappings/realm/${ROLE_ROLE1} \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_SELDON}"/role-mappings/realm/"${ROLE_ROLE1}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ROLE1}\",\"name\":\"role1\"}]" echo Assign role role1 to gaaldornick -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/${USER_DORNICK}/role-mappings/realm/${ROLE_ROLE1} \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_DORNICK}"/role-mappings/realm/"${ROLE_ROLE1}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ROLE1}\",\"name\":\"role1\"}]" echo enable CouchDB JWT login -curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/chttpd/authentication_handlers" \ +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/chttpd/authentication_handlers" \ -H "Content-Type: text/plain" \ -d '"{chttpd_auth, cookie_authentication_handler}, {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, default_authentication_handler}"' echo point to idp_host -curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/jwt_auth/idp_host" \ +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/jwt_auth/idp_host" \ --header 'content-type: text/plain' \ --data "\"${KEYCLOAK}/realms/${REALM}\"" echo set require exp,iat -curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/_local/_config/jwt_auth/required_claims" \ +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/jwt_auth/required_claims" \ --header 'content-type: text/plain' \ --data "\"exp,iat\"" echo ADD PUblic key -node .devcontainer/jwks2couch.mjs +node .devcontainer/jwks2couch.mjs || { + echo "jwks2couch failed" >&2 + exit 1 +} echo Set path for role resolution -curl -u ${COUCHDB_USRPWD} -X PUT "${SRV}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path" \ +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path" \ -H "Content-Type: text/plain" \ -d "\"realm_access.roles\"" echo Restart CouchDB -curl -u ${COUCHDB_USRPWD} -X POST "${SRV}/_node/_local/_restart" +curl -u "${COUCHDB_USRPWD}" -X POST "${SRV}/_node/_local/_restart" echo DONE \ No newline at end of file diff --git a/.devcontainer/postStart.sh b/.devcontainer/postStart.sh index ad57c65de..691f3b17b 100755 --- a/.devcontainer/postStart.sh +++ b/.devcontainer/postStart.sh @@ -9,4 +9,6 @@ COUCHDB_PORT=5984 export SRV=http://localhost:${COUCHDB_PORT} -curl -u ${COUCHDB_USRPWD} -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postCreate\"}" | jq \ No newline at end of file +curl -u "${COUCHDB_USRPWD}" -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postStart\"}" | jq + +npm run dev \ No newline at end of file diff --git a/readme.md b/readme.md index bad0cb160..f8a87f4a6 100644 --- a/readme.md +++ b/readme.md @@ -16,20 +16,22 @@ See `fauxton --help` for extra options. ## Setting up Fauxton +(alternative see below: Running Fauxton in a devcontainer) + Please note that [node.js](http://nodejs.org/) and npm is required. Specifically, Fauxton requires at least Node 6 and npm 3. 1. Fork this repo (see [GitHub help](https://help.github.com/articles/fork-a-repo/) for details) 1. Clone your fork: `git clone https://github.com/YOUR-USERNAME/couchdb-fauxton.git` 1. Go to your cloned copy: `cd couchdb-fauxton` -1. Set up the upstream repo: +1. Set up the upstream repo: * `git remote add upstream https://github.com/apache/couchdb-fauxton.git` * `git fetch upstream` * `git branch --set-upstream-to=upstream/main main` 1. Download all dependencies: `npm install` 1. Make sure you have CouchDB installed. - Option 1 (**recommended**): Use `npm run docker:up` to start a Docker container running CouchDB with user `tester` and password `testerpass`. - - You need to have [Docker](https://docs.docker.com/engine/installation/) installed to use this option. - - Option 2: Follow instructions + - You need to have [Docker](https://docs.docker.com/engine/installation/) installed to use this option. + - Option 2: Follow instructions [found here](http://couchdb.readthedocs.org/en/latest/install/index.html) @@ -52,7 +54,7 @@ You should be able to access Fauxton at `http://localhost:8000` ### Preparing a Fauxton Release -Follow the "Setting up Fauxton" section above, then edit the `settings.json` variable root where the document will live, +Follow the "Setting up Fauxton" section above, then edit the `settings.json` variable root where the document will live, e.g. `/_utils/`. Then type: ``` @@ -82,9 +84,34 @@ part of the deployable release artifact. # Or fully compiled install npm run couchdb +## Running Fauxton in a devcontainer + +This repository contains a folder `.devcontainer` that hold a container definition following the [devcontainer standard](https://containers.dev). +It allows to start Fauxton and CouchDB together with a predefinded configuration. It also runs a Keycloak instance to be able to test (a future) IdP integration. The instances are ephidermal, so a container rebuild gets you back to a defined pristine state. + +Using the devcontainer is your choice and optional. Prerequisites: + +- a container runtime installed: Docker desktop, Rancher deskop, Orbstack etc. +- a compatible Ide: VS-Code, IntelliJ etc +- a file `.devcontainer/.env` (not version controlled) + +```env +# CouchDB +COUCHDB_USER=admin +COUCHDB_PASSWORD=password + +# Keycloak +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=password +``` + +Follow the instructions of your Ide to build and start the container. In VS-Code select "Dev Containers: Rebuild Container". Your container gets build, the system databases for CouchDB created, Keycloak configured and CouchDB JWT enabled. You can reach the following endpoints: +- http://localhost:8000 The Fauxton UI +- http://localhost:5984 The CouchDB +- http://localhost:8090 The Keycloak IdP -## More information +## More information Check out the following pages for a lot more information about Fauxton: From 8f964d93592d50d186783d1c58452b8cb57a8ef6 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 4 Aug 2025 01:21:30 +0800 Subject: [PATCH 4/6] remove automagic, provide separate confiugration scripts --- .devcontainer/docker-compose.yml | 4 +- .devcontainer/populate_couchdb.sh | 36 +++ .devcontainer/populate_keycloak.sh | 224 ++++++++++++++++ .devcontainer/postCreate.sh | 242 ------------------ .devcontainer/postStart.sh | 9 +- .quarkus/cli/plugins/quarkus-cli-catalog.json | 5 + readme.md | 35 ++- 7 files changed, 305 insertions(+), 250 deletions(-) create mode 100755 .devcontainer/populate_couchdb.sh create mode 100755 .devcontainer/populate_keycloak.sh create mode 100644 .quarkus/cli/plugins/quarkus-cli-catalog.json diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 4fe8c92a1..fd48737d2 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -11,7 +11,6 @@ services: - .env depends_on: - couchdb - - keycloak couchdb: image: couchdb:latest @@ -39,6 +38,9 @@ services: env_file: - .env command: start-dev --http-port=8090 + profiles: + - idp + - keycloak networks: devnet: diff --git a/.devcontainer/populate_couchdb.sh b/.devcontainer/populate_couchdb.sh new file mode 100755 index 000000000..0f7c0f157 --- /dev/null +++ b/.devcontainer/populate_couchdb.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# Use to create CouchDB system databases and one demo database + +# Wait until CouchDB answers +until curl -fsS "http://localhost:5984/_up" >/dev/null 2>&1; do sleep 2; done + +# Variables needed +now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +# Check required CouchDB environment variables +if [ -z "${COUCHDB_USER}" ] || [ -z "${COUCHDB_PASSWORD}" ]; then + echo "Error: COUCHDB_USER and COUCHDB_PASSWORD environment variables must be set" + exit 1 +fi +# Populate CouchDB +COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} +COUCHDB_PORT=5984 +SRV=http://localhost:${COUCHDB_PORT} + +echo Couch $SRV + +#SYSTEM databases +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_users +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_replicator +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_global_changes + +#DEMO database +curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/demo + +# Status +curl -u "${COUCHDB_USRPWD}" ${SRV} | jq + +# Session +curl -u "${COUCHDB_USRPWD}" ${SRV}/_session | jq + +echo DONE \ No newline at end of file diff --git a/.devcontainer/populate_keycloak.sh b/.devcontainer/populate_keycloak.sh new file mode 100755 index 000000000..7d7f0d9a9 --- /dev/null +++ b/.devcontainer/populate_keycloak.sh @@ -0,0 +1,224 @@ +#!/bin/bash +# Populates Keycloak with the realm "empire" and users +# and all settings for CouchDB to trust the IdP's certs + +# Wait until CouchDB answers +until curl -fsS "http://localhost:5984/_up" >/dev/null 2>&1; do sleep 2; done +# Wait until Keycloak answers +until curl -fsS "http://localhost:8090/realms/master" >/dev/null 2>&1; do sleep 2; done + +# Variables needed +now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +REALM=empire + +# Check required CouchDB environment variables +if [ -z "${COUCHDB_USER}" ] || [ -z "${COUCHDB_PASSWORD}" ]; then + echo "Error: COUCHDB_USER and COUCHDB_PASSWORD environment variables must be set" + exit 1 +fi +# Populate CouchDB +COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} +COUCHDB_PORT=5984 +SRV=http://localhost:${COUCHDB_PORT} +KEYCLOAK=http://localhost:8090 + +echo Couch $SRV +echo keycloak $KEYCLOAK + +# Check required Keycloak environment variables +if [ -z "${KEYCLOAK_ADMIN}" ] || [ -z "${KEYCLOAK_ADMIN_PASSWORD}" ]; then + echo "Error: KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD environment variables must be set" + exit 1 +fi + +# Populate Keycloak +curl ${KEYCLOAK} + +echo Admin login +KEYCLOAK_ACCESS_TOKEN=$(curl -X POST ${KEYCLOAK}/realms/master/protocol/openid-connect/token \ + --header 'content-type: application/x-www-form-urlencoded' \ + --data client_id=admin-cli \ + --data "username=${KEYCLOAK_ADMIN}" \ + --data "password=${KEYCLOAK_ADMIN_PASSWORD}" \ + --data grant_type=password \ + --silent | jq -r '.access_token') + +echo Create realm +curl -X POST ${KEYCLOAK}/admin/realms \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header "content-type: application/json" \ + --data '{ + "id": "'"${REALM}"'", + "realm": "'"${REALM}"'", + "displayName": "The mighty realm of '"${REALM}"'", + "enabled": true, + "sslRequired": "NONE", + "registrationAllowed": true, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": true, + "editUsernameAllowed": true, + "bruteForceProtected": true +}' + +echo Create client fauxton +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/clients \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "clientId": "fauxton", + "name" : "CouchDB Fauxton", + "enabled": true, + "publicClient": true, + "directAccessGrantsEnabled": true, + "redirectUris":["http://localhost:8000","http://localhost:8000/"], + "webOrigins": ["localhost:8000"], + "protocolMappers": [ + { + "name": "email to sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "lightweight.claim": "false", + "access.token.claim": "true", + "claim.name": "sub", + "jsonType.label": "String" + } + } + ] +}' + +echo Create role _admin +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "name":"_admin", + "description":"Full access" +}' + +echo retrive id for _admin +ROLE_ADMIN=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?search=_admin" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') +echo ADMIN_ID $ROLE_ADMIN + +echo create role role1 +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "name":"role1", + "description":"Member of role1" +}' + +echo retrieve id for role1 +ROLE_ROLE1=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?search=role1" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Create user Hari Seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "requiredActions": [], + "username": "hariseldon", + "enabled": true, + "firstName": "Hari", + "lastName": "Seldon", + "email": "hari.seldon@terminus.empire", + "emailVerified": true, + "credentials": [ + { + "type": "password", + "value": "password", + "temporary": false + } + ] +}' + +echo retrieve ID for user harisedon +USER_SELDON=$(curl "${KEYCLOAK}/admin/realms/${REALM}/users?username=hariseldon" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Create user Gaal Dornick +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data '{ + "requiredActions": [], + "username": "gaaldornick", + "enabled": true, + "firstName": "Gaal", + "lastName": "Dornick", + "email": "gaal.dornick@terminus.empire", + "emailVerified": true, + "credentials": [ + { + "type": "password", + "value": "password", + "temporary": false + } + ] +}' + +echo retrive ID for user gaaldornick +USER_DORNICK=$(curl "${KEYCLOAK}/admin/realms/${REALM}/users?username=gaaldornick" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' | jq -r '.[0].id') + +echo Assign role _admin to seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_SELDON}"/role-mappings/realm/"${ROLE_ADMIN}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ADMIN}\",\"name\":\"_admin\"}]" + +echo Assign role role1 to seldon +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_SELDON}"/role-mappings/realm/"${ROLE_ROLE1}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ROLE1}\",\"name\":\"role1\"}]" + +echo Assign role role1 to gaaldornick +curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_DORNICK}"/role-mappings/realm/"${ROLE_ROLE1}" \ + --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ + --header 'content-type: application/json' \ + --data "[{\"id\":\"${ROLE_ROLE1}\",\"name\":\"role1\"}]" + +echo enable CouchDB JWT login +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/chttpd/authentication_handlers" \ +-H "Content-Type: text/plain" \ +-d '"{chttpd_auth, cookie_authentication_handler}, {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, default_authentication_handler}"' + +echo point to idp_host +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/jwt_auth/idp_host" \ + --header 'content-type: text/plain' \ + --data "\"${KEYCLOAK}/realms/${REALM}\"" + +echo set require exp,iat +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/jwt_auth/required_claims" \ + --header 'content-type: text/plain' \ + --data "\"exp,iat\"" + +echo ADD PUblic key +node .devcontainer/jwks2couch.mjs || { + echo "jwks2couch failed" >&2 + exit 1 +} + +echo Set path for role resolution +curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path" \ +-H "Content-Type: text/plain" \ +-d "\"realm_access.roles\"" + +echo Restart CouchDB +curl -u "${COUCHDB_USRPWD}" -X POST "${SRV}/_node/_local/_restart" + +echo DONE \ No newline at end of file diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index a48a0aa01..431c2ad70 100755 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -1,246 +1,4 @@ #!/bin/bash # Runs after dev container creation npm install - -# Give containers some space -# Wait until CouchDB answers -until curl -fsS "http://localhost:5984/_up" >/dev/null 2>&1; do sleep 2; done -# Wait until Keycloak answers -until curl -fsS "http://localhost:8090/realms/master" >/dev/null 2>&1; do sleep 2; done - -# Variables needed -now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -REALM=empire - -# Check required CouchDB environment variables -if [ -z "${COUCHDB_USER}" ] || [ -z "${COUCHDB_PASSWORD}" ]; then - echo "Error: COUCHDB_USER and COUCHDB_PASSWORD environment variables must be set" - exit 1 -fi -# Populate CouchDB -COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} -COUCHDB_PORT=5984 - -export SRV=http://localhost:${COUCHDB_PORT} -export KEYCLOAK=http://localhost:8090 - - -echo Couch $SRV -echo keycloak $KEYCLOAK - -#SYSTEM databases -curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_users -curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_replicator -curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/_global_changes - -#DEMO database -curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/demo -curl -u "${COUCHDB_USRPWD}" -X PUT ${SRV}/demo/firstdoc -d '{"name" : "Peter Pan", "location" : "Neverland"}' | jq -curl -u "${COUCHDB_USRPWD}" -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postCreate\"}" | jq - -# Status -curl -u "${COUCHDB_USRPWD}" ${SRV} | jq - -# Session -curl -u "${COUCHDB_USRPWD}" ${SRV}/_session | jq - -#DEMO back -curl -u "${COUCHDB_USRPWD}" ${SRV}/demo/firstdoc | jq - - -# Check required Keycloak environment variables -if [ -z "${KEYCLOAK_ADMIN}" ] || [ -z "${KEYCLOAK_ADMIN_PASSWORD}" ]; then - echo "Error: KEYCLOAK_ADMIN and KEYCLOAK_ADMIN_PASSWORD environment variables must be set" - exit 1 -fi - -# Populate Keycloak -curl ${KEYCLOAK} - -echo Admin login -KEYCLOAK_ACCESS_TOKEN=$(curl -X POST ${KEYCLOAK}/realms/master/protocol/openid-connect/token \ - --header 'content-type: application/x-www-form-urlencoded' \ - --data client_id=admin-cli \ - --data "username=${KEYCLOAK_ADMIN}" \ - --data "password=${KEYCLOAK_ADMIN_PASSWORD}" \ - --data grant_type=password \ - --silent | jq -r '.access_token') - -echo Create realm -curl -X POST ${KEYCLOAK}/admin/realms \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header "content-type: application/json" \ - --data '{ - "id": "'"${REALM}"'", - "realm": "'"${REALM}"'", - "displayName": "The mighty realm of '"${REALM}"'", - "enabled": true, - "sslRequired": "NONE", - "registrationAllowed": true, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": true, - "editUsernameAllowed": true, - "bruteForceProtected": true -}' - -echo Create client fauxton -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/clients \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "clientId": "fauxton", - "name" : "CouchDB Fauxton", - "enabled": true, - "publicClient": true, - "directAccessGrantsEnabled": true, - "redirectUris":["http://localhost:8000","http://localhost:8000/"], - "webOrigins": ["localhost:8000"], - "protocolMappers": [ - { - "name": "email to sub", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "introspection.token.claim": "true", - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "lightweight.claim": "false", - "access.token.claim": "true", - "claim.name": "sub", - "jsonType.label": "String" - } - } - ] -}' - -echo Create role _admin -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "name":"_admin", - "description":"Full access" -}' - -echo retrive id for _admin -ROLE_ADMIN=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?search=_admin" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' | jq -r '.[0].id') - -echo create role role1 -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/roles \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "name":"role1", - "description":"Member of role1" -}' - -echo retrieve id for role1 -ROLE_ROLE1=$(curl "${KEYCLOAK}/admin/realms/${REALM}/roles?search=role1" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' | jq -r '.[0].id') - -echo Create user Hari Seldon -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "requiredActions": [], - "username": "hariseldon", - "enabled": true, - "firstName": "Hari", - "lastName": "Seldon", - "email": "hari.seldon@terminus.empire", - "emailVerified": true, - "credentials": [ - { - "type": "password", - "value": "password", - "temporary": false - } - ] -}' - -echo retrieve ID for user harisedon -USER_SELDON=$(curl "${KEYCLOAK}/admin/realms/${REALM}/users?username=hariseldon" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' | jq -r '.[0].id') - -echo Create user Gaal Dornick -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data '{ - "requiredActions": [], - "username": "gaaldornick", - "enabled": true, - "firstName": "Gaal", - "lastName": "Dornick", - "email": "gaal.dornick@terminus.empire", - "emailVerified": true, - "credentials": [ - { - "type": "password", - "value": "password", - "temporary": false - } - ] -}' - -echo retrive ID for user gaaldornick -USER_DORNICK=$(curl "${KEYCLOAK}/admin/realms/${REALM}/users?username=gaaldornick" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' | jq -r '.[0].id') - -echo Assign role _admin to seldon -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_SELDON}"/role-mappings/realm/"${ROLE_ADMIN}" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data "[{\"id\":\"${ROLE_ADMIN}\",\"name\":\"_admin\"}]" - -echo Assign role role1 to seldon -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_SELDON}"/role-mappings/realm/"${ROLE_ROLE1}" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data "[{\"id\":\"${ROLE_ROLE1}\",\"name\":\"role1\"}]" - -echo Assign role role1 to gaaldornick -curl -X POST ${KEYCLOAK}/admin/realms/${REALM}/users/"${USER_DORNICK}"/role-mappings/realm/"${ROLE_ROLE1}" \ - --header "authorization: Bearer ${KEYCLOAK_ACCESS_TOKEN}" \ - --header 'content-type: application/json' \ - --data "[{\"id\":\"${ROLE_ROLE1}\",\"name\":\"role1\"}]" - -echo enable CouchDB JWT login -curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/chttpd/authentication_handlers" \ --H "Content-Type: text/plain" \ --d '"{chttpd_auth, cookie_authentication_handler}, {chttpd_auth, jwt_authentication_handler}, {chttpd_auth, default_authentication_handler}"' - -echo point to idp_host -curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/jwt_auth/idp_host" \ - --header 'content-type: text/plain' \ - --data "\"${KEYCLOAK}/realms/${REALM}\"" - -echo set require exp,iat -curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/_local/_config/jwt_auth/required_claims" \ - --header 'content-type: text/plain' \ - --data "\"exp,iat\"" - -echo ADD PUblic key -node .devcontainer/jwks2couch.mjs || { - echo "jwks2couch failed" >&2 - exit 1 -} - -echo Set path for role resolution -curl -u "${COUCHDB_USRPWD}" -X PUT "${SRV}/_node/nonode@nohost/_config/jwt_auth/roles_claim_path" \ --H "Content-Type: text/plain" \ --d "\"realm_access.roles\"" - -echo Restart CouchDB -curl -u "${COUCHDB_USRPWD}" -X POST "${SRV}/_node/_local/_restart" - echo DONE \ No newline at end of file diff --git a/.devcontainer/postStart.sh b/.devcontainer/postStart.sh index 691f3b17b..0c121fe4f 100755 --- a/.devcontainer/postStart.sh +++ b/.devcontainer/postStart.sh @@ -1,14 +1,15 @@ #!/bin/bash # Runs after dev container startup +# Wait until CouchDB answers +until curl -fsS "http://localhost:5984/_up" >/dev/null 2>&1; do sleep 2; done + now=$(date -u +"%Y-%m-%dT%H:%M:%SZ") -# Give containers some space -sleep 5 COUCHDB_USRPWD=${COUCHDB_USER}:${COUCHDB_PASSWORD} COUCHDB_PORT=5984 - -export SRV=http://localhost:${COUCHDB_PORT} +SRV=http://localhost:${COUCHDB_PORT} curl -u "${COUCHDB_USRPWD}" -X POST ${SRV}/demo -H "Content-Type: application/json" -d "{\"date\" : \"$now\", \"action\" : \"postStart\"}" | jq + npm run dev \ No newline at end of file diff --git a/.quarkus/cli/plugins/quarkus-cli-catalog.json b/.quarkus/cli/plugins/quarkus-cli-catalog.json new file mode 100644 index 000000000..082bfb93e --- /dev/null +++ b/.quarkus/cli/plugins/quarkus-cli-catalog.json @@ -0,0 +1,5 @@ +{ + "version" : "v1", + "lastUpdate" : "31/07/2025 20:13:44", + "plugins" : { } +} \ No newline at end of file diff --git a/readme.md b/readme.md index f8a87f4a6..3c6a7d465 100644 --- a/readme.md +++ b/readme.md @@ -87,9 +87,12 @@ part of the deployable release artifact. ## Running Fauxton in a devcontainer This repository contains a folder `.devcontainer` that hold a container definition following the [devcontainer standard](https://containers.dev). -It allows to start Fauxton and CouchDB together with a predefinded configuration. It also runs a Keycloak instance to be able to test (a future) IdP integration. The instances are ephidermal, so a container rebuild gets you back to a defined pristine state. -Using the devcontainer is your choice and optional. Prerequisites: +It allows to start Fauxton and CouchDB together with a predefinded configuration. It also optionally runs a Keycloak instance to be able to test (a future) IdP integration. The instances are ephidermal, so rebuilding containers (see below) gets you back to a defined pristine state. + +Using the devcontainer is your choice and optional. + +### Prerequisites - a container runtime installed: Docker desktop, Rancher deskop, Orbstack etc. - a compatible Ide: VS-Code, IntelliJ etc @@ -105,12 +108,38 @@ KEYCLOAK_ADMIN=admin KEYCLOAK_ADMIN_PASSWORD=password ``` -Follow the instructions of your Ide to build and start the container. In VS-Code select "Dev Containers: Rebuild Container". Your container gets build, the system databases for CouchDB created, Keycloak configured and CouchDB JWT enabled. You can reach the following endpoints: +Follow the instructions of your Ide to build and start the container. In VS-Code select "Dev Containers: Rebuild Container". + +As result your container gets build and you have access to thses URLs: - http://localhost:8000 The Fauxton UI - http://localhost:5984 The CouchDB + +### Running the devcontainer with keycloak + +Since the keycloak IdP is not nescesary unless you want to use JWT related operations, it doesn't automatically start. You have to use a terminal/commandline after you started the dev container. + + +```bash +cd .devcontainer +docker compose --profile idp up +``` + +You then gain an additional endpoint: + - http://localhost:8090 The Keycloak IdP +### Configure CouchDB and Keycloak + +The `.devcontainer` folder contains helper scripts you can use to configure CouchDB and Keycloak. Since you might have different ideas how the environment should be configured, the scripts don't run automatically, but need to be called from a terminal inside the devcontainer. + +* `populate_couchdb.sh`: create databases `_users`, `_relicator`, `_global_changes` and `demo` +* `populate_keycloak.sh`: configure the kecloak server with the realm `empire`, a client `fauxton` and the users `hariseldon`, `gaaldormick`. Furthermore extract the public key from the JWKS, convert it to PEM and configure JWT authentication in CouchDB including trusting that key + +### Resetting the containers + +Both side containers (CouchDB, keycloak) don't use volumes to store their data, deleting the container will trigger the rebuild with a clean slate. See [`docker compose down`](https://docs.docker.com/reference/cli/docker/compose/down/) + ## More information Check out the following pages for a lot more information about Fauxton: From 0ba66943de00fe690ea1aab104f8a8d52701aec3 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 4 Aug 2025 01:50:19 +0800 Subject: [PATCH 5/6] Typos and remove .quarkus --- .gitignore | 1 + readme.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e0e1bca78..d6541a671 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ coverage # IDEs .idea/ .vscode +.quarkus # Env variables .env diff --git a/readme.md b/readme.md index 3c6a7d465..c43e0ef89 100644 --- a/readme.md +++ b/readme.md @@ -134,7 +134,7 @@ You then gain an additional endpoint: The `.devcontainer` folder contains helper scripts you can use to configure CouchDB and Keycloak. Since you might have different ideas how the environment should be configured, the scripts don't run automatically, but need to be called from a terminal inside the devcontainer. * `populate_couchdb.sh`: create databases `_users`, `_relicator`, `_global_changes` and `demo` -* `populate_keycloak.sh`: configure the kecloak server with the realm `empire`, a client `fauxton` and the users `hariseldon`, `gaaldormick`. Furthermore extract the public key from the JWKS, convert it to PEM and configure JWT authentication in CouchDB including trusting that key +* `populate_keycloak.sh`: configure the keycloak server with the realm `empire`, a client `fauxton` and the users `hariseldon`, `gaaldormick`. Furthermore extract the public key from the JWKS, convert it to PEM and configure JWT authentication in CouchDB including trusting that key ### Resetting the containers From b025d196e8c7ab450064708fef9ae55b2bfc5680 Mon Sep 17 00:00:00 2001 From: "Stephan H. Wissel" Date: Mon, 4 Aug 2025 01:54:33 +0800 Subject: [PATCH 6/6] remove .quarkus --- .quarkus/cli/plugins/quarkus-cli-catalog.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .quarkus/cli/plugins/quarkus-cli-catalog.json diff --git a/.quarkus/cli/plugins/quarkus-cli-catalog.json b/.quarkus/cli/plugins/quarkus-cli-catalog.json deleted file mode 100644 index 082bfb93e..000000000 --- a/.quarkus/cli/plugins/quarkus-cli-catalog.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "version" : "v1", - "lastUpdate" : "31/07/2025 20:13:44", - "plugins" : { } -} \ No newline at end of file