diff --git a/.env.template b/.env.template index 7c300da0..4110d0b0 100644 --- a/.env.template +++ b/.env.template @@ -20,3 +20,6 @@ SESSION_SECRET= FEEDBACK_SLACK_URL= FEEDBACK_URL_LINK= + +# frame-ancestors attribute of CSP. Separate multiple values with a space +FRAME_ANCESTORS= diff --git a/README.md b/README.md index 215074a3..e60ec827 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # ui-frontend -## About this project +## About This Project This repository contains code relevant for the frontend component required in the Managed Control Plane UI (MCP UI), which is part of the @openmcp-project, more info [here](https://github.com/openmcp-project). @@ -10,36 +10,64 @@ The MCP UI enables endusers to work with Managed Control Planes, without having Overall, the UI provides an easy jump-start for everyone interested in checking the status of Managed Control Planes, without having to use kubectl. -## Requirements and Setup +## Getting Started -### Development +### Development Setup -1. install dependencies: `npm i` +#### Install Dependencies -1. Copy the `frontend-config.json` to `public/frontend-config.json` and adapt the `backendUrl` according to your setup (see section Dynamic Frontend Config). +```bash +npm i +``` + +#### Configure Frontend + +- Copy `frontend-config.json` to `public/frontend-config.json` and adapt the `backendUrl` according to your setup (see section Dynamic Frontend Config). +- Copy `.env.template` to `.env` and fill in the missing values. -1. Connect to the ui-backend server - **Run it locally**: - - See `https://github.com/openmcp-project/ui-backend` +#### Run the Application + +```bash +npm run dev +``` -1. Start the application: +The UI will be served on http://localhost:5173. - Run `npm run dev` -### Build +#### Safari Support -1. Build the application: +**Note:** The frontend is currently incompatible with Safari when running locally on `localhost`. - Run `npm run build` +To enable local development with Safari, follow these steps on your local machine: -2. Serve the application locally: +1. **Update Cookie Settings:** + In [`server/encrypted-session.js`](server/encrypted-session.js), set the `secure` property to `false` in both occurrences. - Run `npm run preview` +2. **Disable Helmet Registration:** + In [`server.js`](server.js), comment out or remove the registration of `helmet`. -3. For production: - Use the docker image which uses nginx for best performance and small bundle size. - `docker build -t my-label .` +### Build & Production + +#### Build the Application + +```bash +npm run build +``` + +#### Serve the Production Build Locally + +```bash +npm run preview +``` + +#### Production Deployment + +Use the docker image which uses nginx for best performance and small bundle size. + +```bash +docker build -t my-label . +``` ### Dynamic FrontendConfig @@ -50,11 +78,11 @@ An example docker run command would be docker run -p 5001:80 -e BACKEND_CONFIG="$(cat frontend-config.json)" -t ui-test ``` -## Support, Feedback, Contributing +## Support & Contributing This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/openmcp-project/ui-frontend/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md). -## Security / Disclosure +## Security & Disclosure If you find any bug that may be a security problem, please follow our instructions at [in our security policy](https://github.com/openmcp-project/ui-frontend/security/policy) on how to report it. Please do not create GitHub issues for security-related doubts or problems. ## Code of Conduct @@ -64,3 +92,7 @@ We as members, contributors, and leaders pledge to make participation in our com ## Licensing Copyright 2025 SAP SE or an SAP affiliate company and ui-frontend contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/openmcp-project/ui-frontend). + +--- + +**Happy contributing! 🚀** \ No newline at end of file diff --git a/index.html b/index.html index f97fa54e..6212d53f 100644 --- a/index.html +++ b/index.html @@ -8,13 +8,9 @@ MCP - -
- + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f20fecc0..314f6611 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@fastify/autoload": "^6.3.0", "@fastify/cookie": "^11.0.2", "@fastify/env": "^5.0.2", + "@fastify/helmet": "^13.0.1", "@fastify/http-proxy": "^11.1.2", "@fastify/secure-session": "^8.2.0", "@fastify/sensible": "^6.0.3", @@ -1322,6 +1323,26 @@ "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", "license": "MIT" }, + "node_modules/@fastify/helmet": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-13.0.1.tgz", + "integrity": "sha512-i+ifqazG3d0HwHL3zuZdg6B/WPc9Ee6kVfGpwGho4nxm0UaK1htss0zq+1rVhOoAorZlCgTZ3/i4S58hUGkkoA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "helmet": "^8.0.0" + } + }, "node_modules/@fastify/http-proxy": { "version": "11.3.0", "resolved": "https://registry.npmjs.org/@fastify/http-proxy/-/http-proxy-11.3.0.tgz", @@ -8744,6 +8765,15 @@ "tslib": "^2.0.3" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", diff --git a/package.json b/package.json index 9462b941..289ed548 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "npm": "^11.0.0" }, "scripts": { - "dev": "node server.js --dev", + "dev": "node server.js --local-dev", "start": "node server.js", "build": "tsc && vite build", "lint": "eslint ./src --report-unused-disable-directives --max-warnings 0", @@ -23,9 +23,10 @@ "@fastify/autoload": "^6.3.0", "@fastify/cookie": "^11.0.2", "@fastify/env": "^5.0.2", + "@fastify/helmet": "^13.0.1", "@fastify/http-proxy": "^11.1.2", - "@fastify/sensible": "^6.0.3", "@fastify/secure-session": "^8.2.0", + "@fastify/sensible": "^6.0.3", "@fastify/session": "^11.1.0", "@fastify/static": "^8.1.1", "@fastify/vite": "^8.1.3", diff --git a/server.js b/server.js index 2734ac9c..05c7b991 100644 --- a/server.js +++ b/server.js @@ -1,18 +1,22 @@ import Fastify from 'fastify'; import FastifyVite from '@fastify/vite'; +import helmet from '@fastify/helmet'; import { fileURLToPath } from 'node:url'; import path from 'node:path'; import dotenv from 'dotenv'; import proxy from './server/app.js'; +import envPlugin from "./server/config/env.js"; import { copyFileSync } from 'node:fs'; dotenv.config(); -const isDev = process.argv.includes('--dev'); +const isLocalDev = process.argv.includes('--local-dev'); const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const frontendConfigLocation = isDev ? 'public/frontend-config.json' : 'dist/client/frontend-config.json'; +const frontendConfigLocation = isLocalDev + ? 'public/frontend-config.json' + : 'dist/client/frontend-config.json'; if (process.env.FRONTEND_CONFIG_PATH !== undefined && process.env.FRONTEND_CONFIG_PATH.length > 0) { console.log('FRONTEND_CONFIG_PATH is specified. Will copy the frontend-config from there.'); @@ -24,13 +28,28 @@ const fastify = Fastify({ logger: true, }); +await fastify.register(envPlugin); + +fastify.register( + helmet, + { + contentSecurityPolicy: { + directives: { + "connect-src": ["'self'", "sdk.openui5.org"], + "script-src": isLocalDev ? ["'self'", "'unsafe-inline'"] : ["'self'"], + "frame-ancestors": [fastify.config.FRAME_ANCESTORS] + }, + } + } +) + fastify.register(proxy, { prefix: '/api', }); await fastify.register(FastifyVite, { root: __dirname, - dev: isDev, + dev: isLocalDev, spa: true, }); diff --git a/server/app.js b/server/app.js index 39f92085..ced98ac0 100644 --- a/server/app.js +++ b/server/app.js @@ -1,8 +1,7 @@ -import path, { join, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import AutoLoad from '@fastify/autoload'; -import envPlugin from './config/env.js'; -import encryptedSession from './encrypted-session.js'; +import path, { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import AutoLoad from "@fastify/autoload"; +import encryptedSession from "./encrypted-session.js"; export const options = {}; @@ -10,7 +9,6 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); export default async function (fastify, opts) { - await fastify.register(envPlugin); fastify.register(encryptedSession, { ...opts, }); diff --git a/server/config/env.js b/server/config/env.js index 30cdb0f5..b2c765b2 100644 --- a/server/config/env.js +++ b/server/config/env.js @@ -13,6 +13,7 @@ const schema = { 'COOKIE_SECRET', 'SESSION_SECRET', 'API_BACKEND_URL', + 'FRAME_ANCESTORS', ], properties: { // Application variables (.env) @@ -27,6 +28,7 @@ const schema = { API_BACKEND_URL: { type: 'string' }, FEEDBACK_SLACK_URL: { type: 'string' }, FEEDBACK_URL_LINK: { type: 'string' }, + FRAME_ANCESTORS: { type: 'string' }, // System variables NODE_ENV: { type: 'string', enum: ['development', 'production'] }, diff --git a/server/encrypted-session.js b/server/encrypted-session.js index 6fc5b73a..bc91e016 100644 --- a/server/encrypted-session.js +++ b/server/encrypted-session.js @@ -20,7 +20,7 @@ export const ENCRYPTED_COOKIE_KEY_ENCRYPTION_KEY = 'encryptionKey'; export const SESSION_COOKIE_NAME = 'session-cookie'; async function encryptedSession(fastify) { - const { COOKIE_SECRET, SESSION_SECRET, NODE_ENV } = fastify.config; + const { COOKIE_SECRET, SESSION_SECRET } = fastify.config; await fastify.register(fastifyCookie); @@ -31,8 +31,9 @@ async function encryptedSession(fastify) { cookie: { path: '/', httpOnly: true, - sameSite: 'lax', - secure: NODE_ENV === 'production', + sameSite: "None", // cross-site cookies are needed for the session to work when embedded. By setting CORS to None and CSP.frame-anchestors we restrict the api calls from the browser that contain the cookies to originating from our site only. + partitioned: true, // use for modern isolation of third party cookies when embedded, every embedded iframe (or not embedded) gets its own cookie partition + secure: true, maxAge: 60 * 60 * 24 * 7, // 7 days }, }); @@ -43,8 +44,9 @@ async function encryptedSession(fastify) { cookie: { path: '/', httpOnly: true, - sameSite: 'lax', - secure: NODE_ENV === 'production', + sameSite: "None", // see secureSession cookie for explanation + partitioned: true, // see secureSession cookie for explanation + secure: true, maxAge: 60 * 60 * 24 * 7, // 7 days }, });