diff --git a/auth-oidc-proxy/.ceignore b/auth-oidc-proxy/.ceignore new file mode 100644 index 000000000..1f071c64a --- /dev/null +++ b/auth-oidc-proxy/.ceignore @@ -0,0 +1,2 @@ +oidc*.properties +node_modules diff --git a/auth-oidc-proxy/.dockerignore b/auth-oidc-proxy/.dockerignore new file mode 100644 index 000000000..24d9c3361 --- /dev/null +++ b/auth-oidc-proxy/.dockerignore @@ -0,0 +1,12 @@ +oidc*.properties +.ceignore +.dockerignore +.gitignore +auth +build +docs +Dockerfile +nginx +node_modules +README.md +run diff --git a/auth-oidc-proxy/.gitignore b/auth-oidc-proxy/.gitignore new file mode 100644 index 000000000..1f071c64a --- /dev/null +++ b/auth-oidc-proxy/.gitignore @@ -0,0 +1,2 @@ +oidc*.properties +node_modules diff --git a/auth-oidc-proxy/README.md b/auth-oidc-proxy/README.md new file mode 100644 index 000000000..3c9eda0a4 --- /dev/null +++ b/auth-oidc-proxy/README.md @@ -0,0 +1,106 @@ +# OIDC Proxy sample + +This sample demonstrates how to configure an authentication/authorization layer that fronts any arbitrary Code Engine application. In principal, this pattern is pretty generic. To demonstrate it, we chose to implement it with OpenID Connect (OIDC), an authentication framework that is built on top of the OAuth 2.0 protocol. + +The following diagram depicts the components that are involved: +![OIDC Proxy architecture overview](./docs/ce-oidc-proxy-overview.png) + +**Note:** The origin app is not exposed to the public or private network and can only be accessed through the authentication proxy that does an auth check towards an oidc app that got installed into the same project. + + +## Setting up an OIDC SSO configuration + +In order to be able to authenticate using OIDC SSO, you'll need to choose and configure a suitable OIDC provider. For this sample we demonstrate how this can be achieved by either using GitHub, or an IBM-internal provider. While many other OIDC providers will also work out-of-the-box, some may require few adjustments in the implementation of the `auth` app that we provide in this sample. + +### Github.com OIDC SSO + +Github.com provides a publicly available OIDC provider, that can be used to point to Code Engine applications, which you deployed in your IBM Cloud account. Use the following steps to configure an SSO app: + +* Create Github OIDC app through https://github.com/settings/developers + ``` + name: oidc-sample + homepage: https://oidc-sample-auth...codeengine.appdomain.cloud + callback URL: https://oidc-sample-auth...codeengine.appdomain.cloud/auth/callback + ``` +* Store the client id and the secret in local file called `oidc.properties` + ``` + echo "OIDC_CLIENT_ID=" > oidc.properties + echo "OIDC_CLIENT_SECRET=" >> oidc.properties + ``` +* Generate a random cookie secret that is used to encrypt the auth cookie value and add it to the `oidc.properties` file + ``` + echo "COOKIE_SIGNING_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> oidc.properties + ``` +* From your OIDC provider obtain the following values and add them to the `oidc.properties` file + ``` + echo "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT=https://github.com/login/oauth/authorize" >> oidc.properties + echo "OIDC_PROVIDER_TOKEN_ENDPOINT=https://github.com/login/oauth/access_token" >> oidc.properties + echo "OIDC_PROVIDER_USERINFO_ENDPOINT=https://api.github.com/user" >> oidc.properties + ``` +* To add authorization checks one can check for a specific user property + ``` + echo "AUTHZ_USER_PROPERTY=login" >> oidc.properties + echo "AUTHZ_ALLOWED_USERS=<" >> oidc.properties + ``` + +### IBMers-only: w3Id OIDC SSO + +To protect IBM's workforce, the SSO Provisioner provides the ability to configure an w3Id SSO. Note: This SSO provider can only be used by IBMers + +* Create w3Id OIDC configuration through https://w3.ibm.com/security/sso-provisioner + ``` + name: oidc-sample + homepage: https://oidc-sample-auth...codeengine.appdomain.cloud + callback URL: https://oidc-sample-auth...codeengine.appdomain.cloud/auth/callback + ``` +* Store the client id and the secret in local file called `oidc.properties` + ``` + echo "OIDC_CLIENT_ID=" > oidc.properties + echo "OIDC_CLIENT_SECRET=" >> oidc.properties + ``` +* Generate a random cookie secret that is used to encrypt the auth cookie value and add it to the `oidc.properties` file + ``` + echo "COOKIE_SIGNING_ENCRYPTION_KEY=$(openssl rand -base64 32)" >> oidc.properties + ``` +* From your OIDC provider obtain the following values and add them to the `oidc.properties` file + ``` + echo "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT=" >> oidc.properties + echo "OIDC_PROVIDER_TOKEN_ENDPOINT=" >> oidc.properties + echo "OIDC_PROVIDER_USERINFO_ENDPOINT=" >> oidc.properties + ``` +* To add authorization checks one can either check for a specific user property, for a group property match + ``` + echo "AUTHZ_USER_PROPERTY=preferred_username" >> oidc.properties + echo "AUTHZ_ALLOWED_USERS=" >> oidc.properties + ``` +* Or for a group property match + ``` + echo "AUTHZ_USER_PROPERTY=blueGroups" >> oidc.properties + echo "AUTHZ_ALLOWED_USERS=" >> oidc.properties + ``` + +## Installing the sample + +* Install the Code Engine projects and all required components + ``` + ./run + ``` + +* Tear down the example: + ``` + ./run clean + ``` + +* Install the example and make sure it does not get deleted right-away + ``` + CLEANUP_ON_SUCCESS=false ./run + ``` + +* Following environment variables can be used to tweak the run script + +| Name | Description | Default value | +|:----|:---|:---| +| REGION | Region of the Code Engine project | `eu-es` | +| NAME_PREFIX | Naming prefix used for all components (e.g. resource group, Code Engine project, apps) | `oidc-sample` | +| CLEANUP_ON_SUCCESS | Determines whether the setup should be deleted, right after its successful creation | `true` | +| CLEANUP_ON_ERROR | Determines whether the setup should be deleted, if the setup procedure failed | `true` | diff --git a/auth-oidc-proxy/auth/Dockerfile b/auth-oidc-proxy/auth/Dockerfile new file mode 100644 index 000000000..a8221a6df --- /dev/null +++ b/auth-oidc-proxy/auth/Dockerfile @@ -0,0 +1,12 @@ +FROM registry.access.redhat.com/ubi9/nodejs-22:latest AS build-env +WORKDIR /app +COPY package.json . +RUN npm install + +# Use a small distroless image for as runtime image +FROM gcr.io/distroless/nodejs22-debian12 +COPY --from=build-env /app /app +WORKDIR /app +COPY index.mjs public/ . +EXPOSE 8080 +CMD ["index.mjs"] \ No newline at end of file diff --git a/auth-oidc-proxy/auth/index.mjs b/auth-oidc-proxy/auth/index.mjs new file mode 100644 index 000000000..ea94b05b8 --- /dev/null +++ b/auth-oidc-proxy/auth/index.mjs @@ -0,0 +1,390 @@ +import express from "express"; +import cookieParser from "cookie-parser"; +import crypto from "crypto"; +import NodeCache from "node-cache"; + +const requiredEnvVars = [ + "OIDC_CLIENT_ID", + "OIDC_CLIENT_SECRET", + "OIDC_PROVIDER_AUTHORIZATION_ENDPOINT", + "OIDC_PROVIDER_TOKEN_ENDPOINT", + "OIDC_PROVIDER_USERINFO_ENDPOINT", + "OIDC_REDIRECT_URL", + "COOKIE_SIGNING_ENCRYPTION_KEY", + "COOKIE_DOMAIN", + "REDIRECT_URL", +]; + +requiredEnvVars.forEach((envVarName) => { + if (!process.env[envVarName]) { + console.log(`Missing '${envVarName}' environment variable`); + process.exit(1); + } +}); + +const SESSION_COOKIE = process.env.COOKIE_NAME || "session_token"; +const ENCRYPTION_KEY = Buffer.from(process.env.COOKIE_SIGNING_ENCRYPTION_KEY, "base64"); +const ENCRYPTION_IV = crypto.randomBytes(16); +const ENCRYPTION_ALGORITHM = "aes-256-cbc"; + +// check whether the KEY has got 32 bytes (256-bit) +if (ENCRYPTION_KEY.length != 32) { + console.log( + `Environment variable 'COOKIE_SIGNING_ENCRYPTION_KEY' has wrong length. Current: ${ENCRYPTION_KEY.length}. Expected: 32` + ); + process.exit(1); +} + +// initialize an in-memory cache to avoid fetching the user data on each request +const cache = new NodeCache({ stdTTL: 60 * 5, checkperiod: 120 }); + +// ================================================= +// HELPER FUNCTIONS +// ================================================= + +// helper function to encrypt a string using the given encryption key and iv +function encrypt(plaintext, key, iv) { + const cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, key, iv); + let ciphertext = cipher.update(plaintext, "utf8", "base64"); + ciphertext += cipher.final("base64"); + return ciphertext; +} + +// helper function to decrypt a string using the given encryption key and iv +function decrypt(ciphertext, key, iv) { + const decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, key, iv); + let plaintext = decipher.update(ciphertext, "base64", "utf8"); + plaintext += decipher.final("utf8"); + return plaintext; +} + +// helper function to send JSON responses +function sendJSONResponse(response, returnCode, jsonObject) { + response.status(returnCode); + response.setHeader("Content-Type", "application/json"); + response.end(JSON.stringify(jsonObject)); +} + +// helper function that reads allowlist +function parseAllowlist(listAsStr) { + if (!listAsStr) { + return []; + } + return listAsStr + .split(",") + .map((item) => item.trim()); +} + +// ================================================= +// AUTHN AND AUTHZ MIDDLEWARES +// ================================================= + +// +// Initialize authorization checks +const AUTHZ_USER_PROPERTY = process.env.AUTHZ_USER_PROPERTY; +const AUTHZ_ALLOWED_USERS_LIST = parseAllowlist(process.env.AUTHZ_ALLOWED_USERS); +const AUTHZ_GROUP_PROPERTY = process.env.AUTHZ_GROUP_PROPERTY; +const AUTHZ_ALLOWED_GROUPS_LIST = parseAllowlist(process.env.AUTHZ_ALLOWED_GROUPS); +let enforceUserAllowlist = false; +let enforceGroupAllowlist = false; +if (AUTHZ_USER_PROPERTY && AUTHZ_ALLOWED_USERS_LIST.length > 0) { + console.log( + `configured to perform allow list check towards the user property '${AUTHZ_USER_PROPERTY}'. ${AUTHZ_ALLOWED_USERS_LIST.length} allowed users` + ); + enforceUserAllowlist = true; +} else if (AUTHZ_GROUP_PROPERTY && AUTHZ_ALLOWED_GROUPS_LIST.length > 0) { + console.log( + `configured to perform allow list check towards the groups property '${AUTHZ_GROUP_PROPERTY}'. ${AUTHZ_ALLOWED_GROUPS_LIST.length} allowed groups` + ); + enforceGroupAllowlist = true; +} + +function getCorrelationId(req, res, next) { + req.correlationId = req.header("x-request-id") || crypto.randomBytes(8).toString("hex"); + next(); +} + +/** + * Check whether the given user is authentication properly. + * + * If the user is authenticated pass on the request to the middleware or handler + * If the user is NOT authenticated, not return a 401 response + */ +async function checkAuthn(req, res, next) { + const startTime = Date.now(); + const fn = `${req.correlationId} -`; + console.log(`${fn} performing authentication check`); + + const encryptedSessionToken = req.cookies[SESSION_COOKIE]; + + // If the user does not have a session token yet, return a 401 + // It is up to the client to trigger a login procedure + if (!encryptedSessionToken) { + console.log(`${fn} session cookie '${SESSION_COOKIE}' not found`); + return sendJSONResponse(res, 401, { reason: "no_auth" }); + } + + // Decrypt session token + let sessionToken; + try { + sessionToken = decrypt(encryptedSessionToken, ENCRYPTION_KEY, ENCRYPTION_IV); + } catch (err) { + console.log(`${fn} failed to decrypt existing sessionToken - cause: '${err.name}', reason: '${err.message}'`); + + // This error indicates that the encrypted string couldn't get decrypted using the encryption key + // maybe the cookie value has been encrypted with an old key + // full error: 'error:1C800064:Provider routines::bad decrypt' + if (err.message.includes("error:1C800064")) { + console.log(`${fn} enryption key has been changed. Deleting existing cookie`); + res.clearCookie(SESSION_COOKIE); + return sendJSONResponse(res, 400, { reason: "invalid_session" }); + } + + // error:1C80006B:Provider routines::wrong final block length + if (err.message.includes("error:1C80006B")) { + console.log(`${fn} enryption key has been changed. Deleting existing cookie`); + res.clearCookie(SESSION_COOKIE); + return sendJSONResponse(res, 401, { reason: "invalid_session" }); + } + + // If the decrypt mechanism failed, return a 500 + // It is up to the client to trigger a login procedure + return sendJSONResponse(res, 500, { reason: "decryption_failed" }); + } + + // + // Check whether the user data have been cached + const cachedData = cache.get(sessionToken); + if (cachedData) { + req.user = cachedData; + console.log(`${fn} passed authentication check (cache hit), duration: ${Date.now() - startTime}ms`); + return next(); + } + + // + // Obtain user information for session token (aka acess token) + try { + const fetchStart = Date.now(); + const opts = { + method: "GET", + headers: { + Authorization: `Bearer ${sessionToken}`, + }, + }; + + console.log(`${fn} fetching user data from '${process.env.OIDC_PROVIDER_USERINFO_ENDPOINT}' ...`); + const response = await fetch(process.env.OIDC_PROVIDER_USERINFO_ENDPOINT, opts); + + console.log( + `${fn} received response from '${process.env.OIDC_PROVIDER_USERINFO_ENDPOINT}' - response.ok: '${ + response.ok + }', response.status: '${response.status}', duration: ${Date.now() - fetchStart}ms` + ); + if (!response.ok) { + const errorResponse = await response.text(); + console.log(`${fn} error response: '${errorResponse}'`); + return sendJSONResponse(res, 401, { reason: "auth_failed" }); + } + + const user_data = await response.json(); + // console.log(`${fn} user_data: '${JSON.stringify(user_data)}'`); + + // store the user data in the in-memory cache + cache.set(sessionToken, user_data); + + // setting user into the request context + req.user = user_data; + } catch (err) { + console.log( + `${fn} failed to obtain user information from '${process.env.OIDC_PROVIDER_USERINFO_ENDPOINT}' for the given access token - cause: '${err.name}', reason: '${err.message}'` + ); + res.clearCookie(SESSION_COOKIE); + return sendJSONResponse(res, 401, { reason: "auth_failed" }); + } + + console.log(`${fn} passed authentication check, duration: ${Date.now() - startTime}ms`); + next(); +} + +/** + * Check whether the given user is authorized properly. + * + * If the user is authorized pass on the request to the middleware or handler + * If the user is NOT authorized, not return a 403 response + */ +async function checkAuthz(req, res, next) { + const startTime = Date.now(); + const fn = `${req.correlationId} -`; + console.log(`${fn} performing authorization check`); + + // perform an authorization check based on a user property match + if (enforceUserAllowlist) { + const userValue = req.user[AUTHZ_USER_PROPERTY]; + console.log(`${fn} checking whether given user.${AUTHZ_USER_PROPERTY}='${userValue}' is allow listed`); + + if (!AUTHZ_ALLOWED_USERS_LIST.includes(userValue)) { + console.log( + `${fn} authz denied. user.${AUTHZ_USER_PROPERTY}='${userValue}' is NOT allow listed. User: '${JSON.stringify( + req.user + )}'` + ); + return sendJSONResponse(res, 403, { reason: "forbidden" }); + } + } + + // perform an authorization check based on a user group match + if (enforceGroupAllowlist) { + const userGroups = req.user[AUTHZ_GROUP_PROPERTY] || []; + console.log(`${fn} checking whether at least one of the user.${AUTHZ_GROUP_PROPERTY} is allow listed.`); + + const authorized = Array.isArray(userGroups) && + userGroups.some((group) => { + return AUTHZ_ALLOWED_GROUPS_LIST.includes(group); + }); + + if (!authorized) { + console.log( + `${fn} authz denied. None of the groups listed in user.${AUTHZ_GROUP_PROPERTY} is allow listed. User: '${JSON.stringify( + req.user + )}'` + ); + return sendJSONResponse(res, 403, { reason: "forbidden" }); + } + } + + console.log(`${fn} passed authorization check, duration: ${Date.now() - startTime}ms`); + next(); +} + +// ================================================= +// EXPRESS SETUP +// ================================================= + +const app = express(); +app.use(express.json()); +app.use(cookieParser()); + +// Use router to bundle all routes to / +const router = express.Router(); +app.use("/", router); + +// Route that initiates the login procedure by redirecting to the OIDC provider +router.get("/auth/login", (req, res) => { + console.log(`handling /auth/login`); + + // redirect to the configured OIDC provider + res.redirect( + `${process.env.OIDC_PROVIDER_AUTHORIZATION_ENDPOINT}?client_id=${ + encodeURIComponent(process.env.OIDC_CLIENT_ID) + }&redirect_uri=${encodeURIComponent( + process.env.OIDC_REDIRECT_URL + )}&response_type=code&scope=openid+profile&state=state` + ); +}); + +// Route that completes the login procedure by receivng a code provided by the OIDC provider. +// To verify the code, this endpoint requests an access token from the OIDC provider in exchange for the given code +router.get("/auth/callback", async (req, res) => { + const startTime = Date.now(); + console.log(`handling /auth/callback`); + + // Exchange authorization code for access token & id_token + let accessTokenData; + try { + const { code } = req.query; + const data = { + code, + redirect_uri: process.env.OIDC_REDIRECT_URL, + grant_type: "authorization_code", + }; + + console.log(`obtaining access token from '${process.env.OIDC_PROVIDER_TOKEN_ENDPOINT}' ...`); + const response = await fetch( + `${process.env.OIDC_PROVIDER_TOKEN_ENDPOINT}?${new URLSearchParams(data).toString()}`, + { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Accept: "application/json", + Authorization: + "Basic " + + Buffer.from(process.env.OIDC_CLIENT_ID + ":" + process.env.OIDC_CLIENT_SECRET).toString("base64"), + }, + } + ); + + console.log( + `Received response from '${process.env.OIDC_PROVIDER_TOKEN_ENDPOINT}' - response.ok: '${ + response.ok + }', response.status: '${response.status}', duration: ${Date.now() - startTime}ms` + ); + if (!response.ok) { + const errorResponse = await response.text(); + console.log(`errorResponse: '${errorResponse}'`); + return res.redirect("/auth/failed"); + } + + accessTokenData = await response.json(); + } catch (err) { + console.log( + `Failed to obtain access token on '${process.env.OIDC_PROVIDER_TOKEN_ENDPOINT}' for the given code`, + err + ); + return res.redirect("/auth/failed"); + } + + // Encrypt the access token + const sessionCookieValue = encrypt(accessTokenData.access_token, ENCRYPTION_KEY, ENCRYPTION_IV); + + const maxAge = accessTokenData.expires_in ? 1000 * accessTokenData.expires_in : 600_000; // defaults to 10min + console.log( + `Setting session cookie '${SESSION_COOKIE}' for domain '${process.env.COOKIE_DOMAIN}' (max age: '${maxAge}ms')` + ); + res.cookie(SESSION_COOKIE, sessionCookieValue, { + maxAge, + httpOnly: true, + path: "/", + secure: true, + domain: process.env.COOKIE_DOMAIN, + }); + + // Redirect to the external redirect URL + console.log(`Redirecting to '${process.env.REDIRECT_URL}'...`); + return res.redirect(process.env.REDIRECT_URL); +}); + +// Route that renders an auth failed page +router.get("/auth/failed", (req, res) => { + console.log(`handling /auth/failed for '${req.url}'`); + sendJSONResponse(res, 401, { status: "auth_failed" }); +}); + +// Define a simple root route. +// This route does not enforce any authentication or authorization +router.get("/", (req, res) => { + console.log(`handling / for '${req.url}'`); + return sendJSONResponse(res, 200, { status: "ok" }); +}); + +// Define the auth route that actually enforces authentication and authorization +router.get("/auth", getCorrelationId, checkAuthn, checkAuthz, (req, res) => { + console.log(`${req.correlationId} - authn&authz checks passed!`); + return sendJSONResponse(res, 200, { status: "ok", authenticated: true, authorized: true }); +}); + +// Serve static files +app.use("/public", express.static("public")); + +// Start server +const port = process.env.PORT || 8080; +const server = app.listen(port, () => { + console.log(`HTTP server is up and running on port ${port}!`); +}); + +// Make sure the server terminates properly +process.on("SIGTERM", () => { + console.info("SIGTERM signal received."); + server.close(() => { + console.log("HTTP server closed."); + }); +}); diff --git a/auth-oidc-proxy/auth/package-lock.json b/auth-oidc-proxy/auth/package-lock.json new file mode 100644 index 000000000..e807e0b69 --- /dev/null +++ b/auth-oidc-proxy/auth/package-lock.json @@ -0,0 +1,878 @@ +{ + "name": "code-engine-oidc-auth-app", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "code-engine-oidc-auth-app", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "cookie-parser": "^1.4.7", + "express": "^4.21.2", + "node-cache": "^5.1.2" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-cache": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", + "integrity": "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==", + "license": "MIT", + "dependencies": { + "clone": "2.x" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + } + } +} diff --git a/auth-oidc-proxy/auth/package.json b/auth-oidc-proxy/auth/package.json new file mode 100644 index 000000000..febb3ed5b --- /dev/null +++ b/auth-oidc-proxy/auth/package.json @@ -0,0 +1,17 @@ +{ + "name": "code-engine-oidc-auth-app", + "version": "1.0.0", + "description": "Simple Node.js OIDC auth app hosted on IBM Cloud Code Engine", + "main": "index.mjs", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "cookie-parser": "^1.4.7", + "express": "^4.21.2", + "node-cache": "^5.1.2" + } +} diff --git a/auth-oidc-proxy/auth/public/images/favicon.ico b/auth-oidc-proxy/auth/public/images/favicon.ico new file mode 100644 index 000000000..8f688bed8 Binary files /dev/null and b/auth-oidc-proxy/auth/public/images/favicon.ico differ diff --git a/auth-oidc-proxy/build b/auth-oidc-proxy/build new file mode 100755 index 000000000..e05b15d42 --- /dev/null +++ b/auth-oidc-proxy/build @@ -0,0 +1,26 @@ +#!/bin/bash + +# Env Vars: +# REGISTRY: name of the image registry/namespace to store the images +# NOCACHE: set this to "--no-cache" to turn off the Docker build cache +# +# NOTE: to run this you MUST set the REGISTRY environment variable to +# your own image registry/namespace otherwise the `docker push` commands +# will fail due to an auth failure. Which means, you also need to be logged +# into that registry before you run it. + +set -ex +export REGISTRY=${REGISTRY:-icr.io/codeengine} + +# Build and push the oidc-auth image +cd oidc-auth +docker build ${NOCACHE} -t ${REGISTRY}/auth-oidc-proxy/auth . --platform linux/amd64 +docker push ${REGISTRY}/auth-oidc-proxy/auth +cd .. + +# Build and push the nginx image +cd nginx +docker build ${NOCACHE} -t ${REGISTRY}/auth-oidc-proxy/nginx . --platform linux/amd64 +docker push ${REGISTRY}/auth-oidc-proxy/nginx +cd .. + diff --git a/auth-oidc-proxy/docs/ce-oidc-proxy-overview.png b/auth-oidc-proxy/docs/ce-oidc-proxy-overview.png new file mode 100644 index 000000000..5d4f1bcd1 Binary files /dev/null and b/auth-oidc-proxy/docs/ce-oidc-proxy-overview.png differ diff --git a/auth-oidc-proxy/nginx/Dockerfile b/auth-oidc-proxy/nginx/Dockerfile new file mode 100644 index 000000000..1e95ce313 --- /dev/null +++ b/auth-oidc-proxy/nginx/Dockerfile @@ -0,0 +1,11 @@ +FROM registry.access.redhat.com/ubi9/nginx-124 + +# Start-nginx is a script that'll replace all environment variables in the nginx.conf file +COPY start-nginx / + +# Add the nginx configuration files +COPY origin-template.conf /tmp/origin-template.conf +ADD nginx.conf "${NGINX_CONF_PATH}" + +# At runtime, call the wrapper script to do the variable substitutions prior starting the NGINX server +ENTRYPOINT ["/start-nginx"] \ No newline at end of file diff --git a/auth-oidc-proxy/nginx/nginx.conf b/auth-oidc-proxy/nginx/nginx.conf new file mode 100644 index 000000000..3255b2901 --- /dev/null +++ b/auth-oidc-proxy/nginx/nginx.conf @@ -0,0 +1,41 @@ +# For more information on configuration, see: +# * Official English Documentation: http://nginx.org/en/docs/ +# * Official Russian Documentation: http://nginx.org/ru/docs/ + +worker_processes auto; +error_log /dev/stderr; +pid /run/nginx.pid; + +# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; +} + +http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /dev/stdout main; + + sendfile on; + tcp_nopush on; + keepalive_timeout 65; + types_hash_max_size 4096; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # https://nginx.org/en/docs/http/websocket.html + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + # Load modular configuration files from the /etc/nginx/conf.d directory. + # See http://nginx.org/en/docs/ngx_core_module.html#include + # for more information. + include /opt/app-root/etc/nginx.d/*.conf; +} diff --git a/auth-oidc-proxy/nginx/origin-template.conf b/auth-oidc-proxy/nginx/origin-template.conf new file mode 100644 index 000000000..e0db94706 --- /dev/null +++ b/auth-oidc-proxy/nginx/origin-template.conf @@ -0,0 +1,39 @@ +server { + + listen 8080; + server_name ${ORIGIN_APP_FQDN}; + root /opt/app-root/src; + + location / { + auth_request /auth; + error_page 401 = /auth/login; + + proxy_pass http://${ORIGIN_APP_NAME}.${CE_SUBDOMAIN}.svc.cluster.local; + proxy_set_header Host ${ORIGIN_APP_NAME}.${CE_SUBDOMAIN}.svc.cluster.local; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Auth-Request-Redirect $request_uri; + proxy_pass_request_headers on; + + # WebSocket support + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 86400; + } + + location /auth { + proxy_pass http://${AUTH_APP_NAME}.${CE_SUBDOMAIN}.svc.cluster.local; + proxy_set_header Host ${AUTH_APP_NAME}.${CE_SUBDOMAIN}.svc.cluster.local; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header X-Original-URI $request_uri; + proxy_pass_request_headers on; + + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_read_timeout 86400; + } +} + diff --git a/auth-oidc-proxy/nginx/start-nginx b/auth-oidc-proxy/nginx/start-nginx new file mode 100755 index 000000000..2afd72246 --- /dev/null +++ b/auth-oidc-proxy/nginx/start-nginx @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +# Replace all "CE_SUBDOMAIN" in the config file with the Code Engine subdomain (k8s ns) +# see: https://www.baeldung.com/linux/nginx-config-environment-variables +echo "Performing environment variable substitutions ..." +envsubst '\$ORIGIN_APP_FQDN \$ORIGIN_APP_NAME \$AUTH_APP_NAME \$CE_SUBDOMAIN' < /tmp/origin-template.conf > /opt/app-root/etc/nginx.d/origin.conf + +echo "Starting NGINX with the following config file '${NGINX_CONF_PATH}'" +cat ${NGINX_CONF_PATH} + +echo "Using following config '/opt/app-root/etc/nginx.d/origin.conf' to expose the Code Engine origin app:" +cat /opt/app-root/etc/nginx.d/origin.conf + +# Now run nginx +echo "Launching NGINX..." +nginx -g 'daemon off;' \ No newline at end of file diff --git a/auth-oidc-proxy/oidc.properties.template b/auth-oidc-proxy/oidc.properties.template new file mode 100644 index 000000000..b1eabc1d6 --- /dev/null +++ b/auth-oidc-proxy/oidc.properties.template @@ -0,0 +1,6 @@ +OIDC_CLIENT_ID= +OIDC_CLIENT_SECRET= +OIDC_PROVIDER_AUTHORIZATION_ENDPOINT= +OIDC_PROVIDER_TOKEN_ENDPOINT= +OIDC_PROVIDER_USERINFO_ENDPOINT= +COOKIE_SIGNING_ENCRYPTION_KEY= \ No newline at end of file diff --git a/auth-oidc-proxy/run b/auth-oidc-proxy/run new file mode 100755 index 000000000..7d6f2f5dd --- /dev/null +++ b/auth-oidc-proxy/run @@ -0,0 +1,271 @@ +#!/bin/bash +set -eo pipefail + +# Customizable vars +CLEANUP_ON_ERROR=${CLEANUP_ON_ERROR:=true} +CLEANUP_ON_SUCCESS=${CLEANUP_ON_SUCCESS:=true} +REGION="${REGION:=eu-es}" +NAME_PREFIX="${NAME_PREFIX:=oidc-sample}" + +# Static variables +RESOURCE_GROUP_NAME="oidc-sample--rg" +CE_PROJECT_NAME="${NAME_PREFIX}-project" +CE_APP_ORIGIN="${NAME_PREFIX}-origin" +CE_APP_PROXY="${NAME_PREFIX}-proxy" +CE_APP_AUTH="${NAME_PREFIX}-auth" +CE_SECRET_AUTH="${NAME_PREFIX}-auth-credentials" + + +# ============================== +# COMMON FUNCTIONS +# ============================== +RED="\033[31m" +BLUE="\033[94m" +GREEN="\033[32m" +ENDCOLOR="\033[0m" + +function print_error { + echo -e "${RED}\n==========================================${ENDCOLOR}" + echo -e "${RED} FAILED${ENDCOLOR}" + echo -e "${RED}==========================================\n${ENDCOLOR}" + echo -e "${RED}$1${ENDCOLOR}" + echo "" +} +function print_msg { + echo -e "${BLUE}$1${ENDCOLOR}" +} +function print_success { + echo -e "${GREEN}$1${ENDCOLOR}" +} + +# Helper function to check whether prerequisites are installed +function check_prerequisites { + # Ensure that jq tool is installed + if ! command -v jq &>/dev/null; then + print_error "'jq' tool is not installed" + exit 1 + fi + echo "Done!" +} + +# Clean up previous run +function clean() { + # cleanup everything within this resource group + + ibmcloud ce project delete --name ${CE_PROJECT_NAME} --hard --force 2>/dev/null + + ibmcloud resource group $RESOURCE_GROUP_NAME --quiet 2>/dev/null + if [[ $? == 0 ]]; then + COUNTER=0 + # some resources (e.g. boot volumes) are deleted with some delay. Hence, the script waits before exiting with an error + while (($(ibmcloud resource service-instances --type all -g $RESOURCE_GROUP_NAME --output json | jq -r '. | length') > 0)); do + sleep 5 + COUNTER=$((COUNTER + 1)) + if ((COUNTER > 30)); then + print_error "Cleanup failed! Please make sure to delete remaining resources manually to avoid unwanted charges." + ibmcloud resource service-instances --type all -g $RESOURCE_GROUP_NAME + exit 1 + fi + done + fi + + ibmcloud resource group-delete $RESOURCE_GROUP_NAME --force 2>/dev/null + + echo "Done!" +} + + +function abortScript() { + if [[ "${CLEANUP_ON_ERROR}" == true ]]; then + clean + else + print_msg "\nSkipping deletion of the created IBM Cloud resources." + echo "$ ibmcloud resource service-instances --type all -g $RESOURCE_GROUP_NAME" + ibmcloud resource service-instances --type all -g $RESOURCE_GROUP_NAME + fi + exit 1 +} + +# ============================== +# MAIN SCRIPT FLOW +# ============================== + +print_msg "\n======================================================" +print_msg " Setting up \"OIDC proxy on Code Engine \" sample" +print_msg "======================================================\n" + +echo "" +echo "Please note: This script will install various IBM Cloud resources within the resource group '$RESOURCE_GROUP_NAME'." + +print_msg "\nChecking prerequisites ..." +check_prerequisites + +# Ensure that latest versions of used IBM Cloud ClI is installed +print_msg "\nPulling latest IBM Cloud CLI release ..." +#ibmcloud update --force +echo "Done!" + +# Ensure that latest versions of used IBM Cloud CLI plugins are installed +print_msg "\nInstalling required IBM Cloud CLI plugins ..." +#ibmcloud plugin install code-engine -f --quiet +echo "Done!" + + +if [[ "$1" == "clean" ]]; then + print_msg "\nCleaning up the remains of previous executions ..." + clean + print_success "\n==========================================\n DONE\n==========================================\n" + exit 0 +fi + +print_msg "\nTargetting IBM Cloud region '$REGION' ..." +ibmcloud target -r $REGION + +# +# Create the resource group, if it does not exist +if ! ibmcloud resource group $RESOURCE_GROUP_NAME --quiet >/dev/null 2>&1; then + print_msg "\nCreating resource group '$RESOURCE_GROUP_NAME' ..." + ibmcloud resource group-create $RESOURCE_GROUP_NAME +fi +print_msg "\nTargetting resource group '$RESOURCE_GROUP_NAME' ..." +ibmcloud target -g $RESOURCE_GROUP_NAME + +# +# Create the Code Engine project, if it does not exist +print_msg "\nInitializing the Code Engine project '$CE_PROJECT_NAME' ..." +if ! ibmcloud ce proj select --name $CE_PROJECT_NAME 2>/dev/null; then + print_msg "\nCreating Code Engine project '$CE_PROJECT_NAME' ..." + ibmcloud ce proj create --name $CE_PROJECT_NAME + if [ $? -ne 0 ]; then + print_error "Code Engine project create failed!" + abortScript + fi +fi +CE_PROJECT=$(ibmcloud ce project current --output json) +CE_PROJECT_GUID=$(echo "$CE_PROJECT" | jq -r '.guid') +CE_PROJECT_DOMAIN=$(echo "$CE_PROJECT" | jq -r '.domain') +CE_PROJECT_NAMESPACE=$(echo "$CE_PROJECT" | jq -r '.kube_config_context') + +# Deploy the Code Engine app to run the origin +print_msg "\nInitializing the origin app '$CE_APP_ORIGIN' ..." +if ! ibmcloud ce app get --name $CE_APP_ORIGIN >/dev/null 2>&1; then + print_msg "\nCreating the origin app '$CE_APP_ORIGIN' ..." + ibmcloud ce app create --name $CE_APP_ORIGIN \ + --image icr.io/codeengine/helloworld \ + --cpu 0.125 \ + --memory 0.25G + if [ $? -ne 0 ]; then + print_error "Code Engine origin app create/update failed!" + abortScript + fi +else + echo "Done!" +fi + +ROOT_DOMAIN=.${CE_PROJECT_NAMESPACE}.${CE_PROJECT_DOMAIN} +FQDN_ORIGIN_APP=${CE_APP_ORIGIN}${ROOT_DOMAIN} +URL_ORIGIN_APP=https://${FQDN_ORIGIN_APP} + +# ================================================ +# OPTIONAL: Configuring Authn and Authz +# ================================================ + +print_msg "\nCheck whether the authentication credentials should be configured, or not ..." +if [ ! -f oidc.properties ]; then + echo "Skipping the configuration of the authentication credentials. Specify all authz/authn properties in 'oidc.properties' to enable it." +else + echo "Authn/Authz configuration file 'oidc.properties' found!" + if ibmcloud ce secret get --name $CE_SECRET_AUTH >/dev/null 2>&1; then + ibmcloud ce secret delete --name $CE_SECRET_AUTH --force + fi + ibmcloud ce secret create \ + --name $CE_SECRET_AUTH \ + --from-env-file oidc.properties + if [ $? -ne 0 ]; then + print_error "Code Engine auth secret create/update failed!" + abortScript + fi +fi + +print_msg "\nCheck whether the authentication app should be configured, or not ..." +if ! ibmcloud ce secret get --name $CE_SECRET_AUTH >/dev/null 2>&1; then + echo "Skipping the deployment of the authentication app" +else + echo "Yes! Setting up the authentication and the proxy apps" + + URL_AUTH_APP=https://${CE_APP_AUTH}${ROOT_DOMAIN} + FQDN_ORIGIN_APP=${CE_APP_PROXY}${ROOT_DOMAIN} + URL_ORIGIN_APP=https://${FQDN_ORIGIN_APP} + + authapp_op_create_or_update=update + if ! ibmcloud ce app get --name $CE_APP_AUTH >/dev/null 2>&1; then + print_msg "\nCreating the auth app '$CE_APP_AUTH' ..." + authapp_op_create_or_update=create + else + print_msg "\nUpdating the auth app '$CE_APP_AUTH' ..." + fi + + # Deploy the Code Engine app to run the OIDC authentication + ibmcloud ce app $authapp_op_create_or_update --name $CE_APP_AUTH \ + --build-source "." \ + --build-context-dir "auth/" \ + --max-scale 1 \ + --cpu 0.125 \ + --memory 0.25G \ + --scale-down-delay 600 \ + --port 8080 \ + --env-from-secret $CE_SECRET_AUTH \ + --env COOKIE_DOMAIN="$ROOT_DOMAIN" \ + --env REDIRECT_URL="$URL_ORIGIN_APP" \ + --env OIDC_REDIRECT_URL="${URL_AUTH_APP}/auth/callback" + if [ $? -ne 0 ]; then + print_error "Code Engine auth app create/update failed!" + abortScript + fi + + # Deploy the Code Engine app to the run the nginx reverse proxy + proxyapp_op_create_or_update=update + if ! ibmcloud ce app get --name $CE_APP_PROXY >/dev/null 2>&1; then + print_msg "\nCreating the proxy app '$CE_APP_PROXY' ..." + proxyapp_op_create_or_update=create + else + print_msg "\nUpdating the proxy app '$CE_APP_PROXY' ..." + fi + ibmcloud ce app $proxyapp_op_create_or_update --name $CE_APP_PROXY \ + --build-source "." \ + --build-context-dir "nginx/" \ + --max-scale 1 \ + --cpu 1 \ + --memory 2G \ + --scale-down-delay 600 \ + --env ORIGIN_APP_FQDN=$FQDN_ORIGIN_APP \ + --env ORIGIN_APP_NAME=$CE_APP_ORIGIN \ + --env AUTH_APP_NAME=$CE_APP_AUTH \ + --port 8080 + if [ $? -ne 0 ]; then + print_error "Code Engine proxy app create/update failed!" + abortScript + fi + + print_msg "\nMake sure the app '$CE_APP_ORIGIN' is not exposed publicly ..." + ibmcloud ce app update --name $CE_APP_ORIGIN --cluster-local +fi + +print_msg "\nThis end-to-end sample created the following set of IBM Cloud resources:" +ibmcloud resource service-instances --type all -g $RESOURCE_GROUP_NAME + +echo "" +ibmcloud ce app list + +if [[ "${CLEANUP_ON_SUCCESS}" == true ]]; then + print_msg "\nCleaning up the created IBM Cloud resources ..." + clean +else + print_msg "\nFollowing commands can be used to further play around with the sample setup:" + echo "1. Open the browser and type '$URL_ORIGIN_APP' to access the origin app" + echo "2. Tear down the sample setup: './run clean'" +fi + +print_success "\n==========================================" +print_success " SUCCESS" +print_success "==========================================\n"