diff --git a/config/static.ts b/config/static.ts index 49c30b1f..0be85b2e 100644 --- a/config/static.ts +++ b/config/static.ts @@ -19,9 +19,6 @@ const client: Config = { const server: Config = { defaultConfig: { configValue: { - authentication: { - strategy: 'none', - }, client: { configOverrides: {}, transport: {}, @@ -40,6 +37,9 @@ const server: Config = { contextRoot: '/', port: 9080, transport: {}, + authentication: { + type: 'none', + }, }, session: { name: 'strimzi-ui', diff --git a/linting/eslint.config.js b/linting/eslint.config.js index 450dde5c..4656d9ef 100644 --- a/linting/eslint.config.js +++ b/linting/eslint.config.js @@ -12,7 +12,7 @@ const rulesets = [ const customRules = { // prefer spaces (2) over tabs for indentation - tab width can be changed, space cannot - indent: ['error', 2], + indent: ['error', 2, { SwitchCase: 1 }], // all lines to have semicolons to end statements semi: ['error', 'always'], }; diff --git a/package-lock.json b/package-lock.json index 77589ef6..7a417634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6295,6 +6295,36 @@ "integrity": "sha512-kUNnecmtkunAoQ3CnjmMkzNU/gtxG8guhi+Fk2U/kOpIKjIMKnXGp4IJCgQJrXSgMsWYimYG4TGjz/UzbGEBTw==", "dev": true }, + "@types/passport": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.4.tgz", + "integrity": "sha512-h5OfAbfBBYSzjeU0GTuuqYEk9McTgWeGQql9g3gUw2/NNCfD7VgExVRYJVVeU13Twn202Mvk9BT0bUrl30sEgA==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/passport-local": { + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/@types/passport-local/-/passport-local-1.0.33.tgz", + "integrity": "sha512-+rn6ZIxje0jZ2+DAiWFI8vGG7ZFKB0hXx2cUdMmudSWsigSq6ES7Emso46r4HJk0qCgrZVfI8sJiM7HIYf4SbA==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-strategy": "*" + } + }, + "@types/passport-strategy": { + "version": "0.2.35", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz", + "integrity": "sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g==", + "dev": true, + "requires": { + "@types/express": "*", + "@types/passport": "*" + } + }, "@types/pino": { "version": "6.3.3", "resolved": "https://registry.npmjs.org/@types/pino/-/pino-6.3.3.tgz", @@ -7677,6 +7707,15 @@ "picomatch": "^2.0.4" } }, + "apollo-cache": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.5.tgz", + "integrity": "sha512-1XoDy8kJnyWY/i/+gLTEbYLnoiVtS8y7ikBr/IfmML4Qb+CM7dEEbIUOjnY716WqmZ/UpXIxTfJsY7rMcqiCXA==", + "requires": { + "apollo-utilities": "^1.3.4", + "tslib": "^1.10.0" + } + }, "apollo-cache-control": { "version": "0.11.4", "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.11.4.tgz", @@ -7686,6 +7725,21 @@ "apollo-server-plugin-base": "^0.10.2" } }, + "apollo-client": { + "version": "2.6.10", + "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.10.tgz", + "integrity": "sha512-jiPlMTN6/5CjZpJOkGeUV0mb4zxx33uXWdj/xQCfAMkuNAC3HN7CvYDyMHHEzmcQ5GV12LszWoQ/VlxET24CtA==", + "requires": { + "@types/zen-observable": "^0.8.0", + "apollo-cache": "1.3.5", + "apollo-link": "^1.0.0", + "apollo-utilities": "1.3.4", + "symbol-observable": "^1.0.2", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0", + "zen-observable": "^0.8.0" + } + }, "apollo-datasource": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.2.tgz", @@ -8405,6 +8459,14 @@ "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", "dev": true }, + "axios": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz", + "integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "babel-code-frame": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", @@ -15354,6 +15416,21 @@ } } }, + "graphql-ws": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-1.14.0.tgz", + "integrity": "sha512-cZ7ly3m6Wj1PiP8dydEqy9X5QO8UxeqvHDAAEGBRWZWHVGPmIoDWAT9Lj3uFXP+H9bTfmt8K6qIu0fsUDb3kHQ==", + "requires": { + "ws": "^7.4.0" + }, + "dependencies": { + "ws": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.0.tgz", + "integrity": "sha512-kyFwXuV/5ymf+IXhS6f0+eAFvydbaBW3zjpT6hUdAh/hbVjTIB5EHBGi0bPoCLSK2wcuz3BrEkB9LrYv1Nm4NQ==" + } + } + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -24418,6 +24495,18 @@ "tslib": "^1.10.0" } }, + "nock": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.0.5.tgz", + "integrity": "sha512-1ILZl0zfFm2G4TIeJFW0iHknxr2NyA+aGCMTjDVUsBY4CkMRispF1pfIYkTRdAR/3Bg+UzdEuK0B6HczMQZcCg==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash.set": "^4.3.2", + "propagate": "^2.0.0" + } + }, "node-dir": { "version": "0.1.17", "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", @@ -25324,6 +25413,28 @@ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -25387,6 +25498,11 @@ "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", "dev": true }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "pbkdf2": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", @@ -26542,6 +26658,12 @@ "react-is": "^16.8.1" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "property-information": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", diff --git a/package.json b/package.json index 50a84b92..757c30ac 100644 --- a/package.json +++ b/package.json @@ -45,17 +45,22 @@ "dependencies": { "@apollo/client": "^3.2.5", "@apollo/react-hooks": "^4.0.0", - "@walmartlabs/json-to-simple-graphql-schema": "^2.0.3", "@types/express-session": "^1.17.2", + "@walmartlabs/json-to-simple-graphql-schema": "^2.0.3", + "apollo-client": "^2.6.10", "apollo-link-http": "^1.5.17", "apollo-server-express": "^2.18.2", "compression-webpack-plugin": "^6.0.5", "css-minimizer-webpack-plugin": "^1.1.5", + "axios": "^0.21.0", + "body-parser": "^1.19.0", "express": "^4.17.1", "express-session": "^1.17.1", "express-static-gzip": "^2.1.0", "fromentries": "^1.3.2", "graphql": "^15.4.0", + "graphql-tag": "^2.11.0", + "graphql-ws": "^1.14.0", "helmet": "^4.2.0", "html-webpack-plugin": "^5.0.0-alpha.14", "http-proxy": "^1.18.1", @@ -68,6 +73,8 @@ "lodash.set": "^4.3.2", "mini-css-extract-plugin": "^1.2.1", "mustache": "^4.0.1", + "passport": "^0.4.1", + "passport-local": "^1.0.0", "pino": "^6.7.0", "pino-filter": "^1.0.0", "pino-http": "^5.3.0", @@ -99,6 +106,8 @@ "@types/jest": "^26.0.15", "@types/mustache": "^4.0.1", "@types/node": "^14.14.6", + "@types/passport": "^1.0.4", + "@types/passport-local": "^1.0.33", "@types/pino": "^6.3.3", "@types/pino-http": "^5.0.5", "@types/react-dom": "^16.9.9", @@ -127,6 +136,7 @@ "license-check-and-add": "^3.0.4", "lint-staged": "^10.5.1", "mock-socket": "^9.0.3", + "nock": "^13.0.5", "nodemon": "^2.0.6", "npm-run-all": "^4.1.5", "prettier": "^2.1.2", diff --git a/server/README.md b/server/README.md index 24837192..7ecc266b 100644 --- a/server/README.md +++ b/server/README.md @@ -20,22 +20,22 @@ This directory contains all server code for the Strimzi UI - ie code which is re As described in [the configuration approach](../docs/Architecture.md#configuration-and-feature-flagging), the UI server's configuration is provided via a file, which is then watched at runtime for modification. This configuration file is expected to be called `server.config.json` (available in the same directory as the `node` executable is run from), but this can be configured at runtime via environment variable `configPath`, dictating a different path and file name. The file must be either valid JSON or JS. The server also hosts configuration for discovery by the client via the `config` module. The configuration options for the server provided in the previously mentioned configuration file are as follows: -| Configuration | Required | Default | Purpose | -| ---------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| authentication.strategy | No | `none` | What authentication strategy to use to authenticate users. See [the security section](#security) for details of the available options. | -| authentication.configuration | No | `{}` | Any additional configuration required for the provided authentication strategy `authentication.strategy` . See [the security section](#security) for details of the available options. | -| client.configOverrides | No | `{}` | Overrides to send to the client. See [client configuration for further details](#client-configuration). These values will take precedence over any others provided. | -| client.publicDir | No | `/dist/client` | The location of the built client to serve. | -| client.transport.cert | No | N/A - if one of `client.transport.cert` or `client.transport.key` are not provided, server will be HTTP | PEM certificate presented to browsers on connecting to the UI server. | -| client.transport.key | No | N/A - if one of `client.transport.cert` or `client.transport.key` are not provided, server will be HTTP | PEM certificate private key for the certificate provided in `client.transport.cert`. | -| client.transport.ciphers | No | default set from [node's tls module](https://nodejs.org/api/tls.html#tls_modifying_the_default_tls_cipher_suite) | TLS ciphers used/supported by the HTTPS server for client negotiation. Only applies if starting an HTTPS server. | -| client.transport.minTLS | No | `TLSv1.2` | Minimum TLS version supported by the server. Only applies if starting an HTTPS server. Set to `TLSv1.2` for browser compatibility. | -| featureFlags | No | `{}` | Feature flag overrides to set. The configuration is as per the format specified [here](#feature-flags). These values will take precedence over any others provided. | -| hostname | No | '0.0.0.0' | The hostname the UI server will be bound to. | -| logging | No | TBD | Logging configuration settings. Format to be defined in https://github.com/strimzi/strimzi-ui/issues/24 | -| modules | No | Object - [enabled modules and configuration can be found here](../docs/Architecture.md#router-controller-data-pattern) | The modules which are either enabled or disabled. | -| port | No | 3000 | The port the UI server will be bound to. | -| proxy.transport.cert | No | If not provided, SSL certificate validation of the upstream admin server is disabled | CA certificate in PEM format of the backend admin server api requests are to be sent to. | -| proxy.hostname | Yes | N/A | The hostname of the admin server to send api requests to. | -| proxy.port | Yes | N/A | The port of the admin server to send api requests to. | -| session.name | no | `strimzi-ui` | The name used to identify the session cookie | +| Configuration | Required | Default | Purpose | +| ---------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| client.configOverrides | No | `{}` | Overrides to send to the client. See [client configuration for further details](#client-configuration). These values will take precedence over any others provided. | +| client.publicDir | No | `/dist/client` | The location of the built client to serve. | +| client.transport.cert | No | N/A - if one of `client.transport.cert` or `client.transport.key` are not provided, server will be HTTP | PEM certificate presented to browsers on connecting to the UI server. | +| client.transport.key | No | N/A - if one of `client.transport.cert` or `client.transport.key` are not provided, server will be HTTP | PEM certificate private key for the certificate provided in `client.transport.cert`. | +| client.transport.ciphers | No | default set from [node's tls module](https://nodejs.org/api/tls.html#tls_modifying_the_default_tls_cipher_suite) | TLS ciphers used/supported by the HTTPS server for client negotiation. Only applies if starting an HTTPS server. | +| client.transport.minTLS | No | `TLSv1.2` | Minimum TLS version supported by the server. Only applies if starting an HTTPS server. Set to `TLSv1.2` for browser compatibility. | +| featureFlags | No | `{}` | Feature flag overrides to set. The configuration is as per the format specified [here](#feature-flags). These values will take precedence over any others provided. | +| hostname | No | '0.0.0.0' | The hostname the UI server will be bound to. | +| logging | No | TBD | Logging configuration settings. Format to be defined in https://github.com/strimzi/strimzi-ui/issues/24 | +| modules | No | Object - [enabled modules and configuration can be found here](../docs/Architecture.md#router-controller-data-pattern) | The modules which are either enabled or disabled. | +| port | No | 3000 | The port the UI server will be bound to. | +| proxy.transport.cert | No | If not provided, SSL certificate validation of the upstream admin server is disabled | CA certificate in PEM format of the backend admin server api requests are to be sent to. | +| proxy.hostname | Yes | N/A | The hostname of the admin server to send api requests to. | +| proxy.port | Yes | N/A | The port of the admin server to send api requests to. | +| proxy.authentication.type | No | `none` | What authentication strategy to use to authenticate users. See [the security section](#security) for details of the available options. | +| proxy.authentication.configuration | No | `{}` | Any additional configuration required for the provided authentication strategy `authentication.strategy` . See [the security section](#security) for details of the available options. | +| session.name | no | `strimzi-ui` | The name used to identify the session cookie | diff --git a/server/api/api.feature b/server/api/api.feature index 35eb1d5d..43b35a5b 100644 --- a/server/api/api.feature +++ b/server/api/api.feature @@ -5,26 +5,26 @@ Feature: api module Behaviours and capabilities provided by the api module Scenario: Proxies all requests made to /api to the configured backend - Given a 'api_only' server configuration - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '/api/foo' - Then I make the expected proxy request and get the expected proxied response + Given a server with a 'api' configuration + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '/api/foo' + Then I make the expected proxy request and get the expected proxied response Scenario: Proxies all requests made to /api to the securley configured backend - Given a 'api_secured_only' server configuration - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '/api/foo' - Then I make the expected proxy request and get the expected proxied response + Given a server with a 'api_secured' configuration + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '/api/foo' + Then I make the expected proxy request and get the expected proxied response Scenario: Handles errors from the proxied backend gracefully - Given a 'api_secured_only' server configuration - And the backend proxy returns an error response - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '/api/foo' - Then I make the expected proxy request and get the expected proxied response + Given a server with a 'api_secured' configuration + And the backend proxy returns an error response + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '/api/foo' + Then I make the expected proxy request and get the expected proxied response Scenario: Proxies all requests made to /api to the specified context root - Given a 'api_with_custom_context_root' server configuration - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '/api/foo' - Then I make the expected proxy request and get the expected proxied response \ No newline at end of file + Given a server with a 'api_with_custom_context_root' configuration + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '/api/foo' + Then I make the expected proxy request and get the expected proxied response \ No newline at end of file diff --git a/server/api/api.steps.ts b/server/api/api.steps.ts index 56c6f1aa..2eb2ed95 100644 --- a/server/api/api.steps.ts +++ b/server/api/api.steps.ts @@ -2,13 +2,13 @@ * Copyright Strimzi authors. * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). */ - +import { RequestHandler } from 'express'; import { createProxyServer } from 'http-proxy'; // without setting up a second server (with secure and insecure modes), the best way to simulate the proxying of calls is mocking them/verifying the api usage type mockProxyServerType = { - on: (event: string, handler: expressMiddleware) => void; - web: jest.Mock; + on: (event: string, handler: RequestHandler) => void; + web: jest.Mock; }; const placeholderProxyEvent = jest.fn(); @@ -55,7 +55,6 @@ import { stepWhichUpdatesWorld, stepWithWorld, } from 'test_common/commonServerSteps'; -import { expressMiddleware } from 'types'; Before(() => { createProxyServer.mockReturnValue(createMockServerFn(false)); diff --git a/server/api/router.ts b/server/api/router.ts index e4ba1d07..e6c9186d 100644 --- a/server/api/router.ts +++ b/server/api/router.ts @@ -16,7 +16,7 @@ const moduleName = 'api'; export const ApiModule: UIServerModule = { moduleName, - addModule: (logger, authFn, serverConfig) => { + addModule: (logger, { checkAuth }, serverConfig) => { const { proxy } = serverConfig; const { exit } = logger.entry('addModule', proxy); const { hostname, port, contextRoot, transport } = proxy; @@ -40,7 +40,9 @@ export const ApiModule: UIServerModule = { backendProxy.on('proxyReq', proxyStartHandler); backendProxy.on('proxyRes', proxyCompleteHandler); // proxy all requests post auth check - routerForModule.all('*', authFn, (req, res) => backendProxy.web(req, res)); + routerForModule.all('*', checkAuth, (req, res) => + backendProxy.web(req, res) + ); return exit({ mountPoint: '/api', routerForModule }); }, diff --git a/server/client/client.feature b/server/client/client.feature index c50fc51f..9bbe9180 100644 --- a/server/client/client.feature +++ b/server/client/client.feature @@ -5,44 +5,81 @@ Feature: client module Behaviours and capabilities provided by the client module Scenario Outline: If no asset can be served, the client module returns 404 - Given a 'client_only' server configuration - And There are no files to serve - And Authentication is required - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '' - Then I get the expected status code '' response - - Examples: - | Asset | StatusCode | - | /index.html | 404 | - | /images/picture.svg | 404 | - | /doesnotexist.html | 404 | - | /someroute | 404 | - | /protected.html | 404 | - | / | 404 | - - Scenario Outline: If assets can be served, the client module returns the appropriate return code for a request of - Given a 'client_only' server configuration - And There are files to serve - And Authentication is required - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '' - Then I get the expected status code '' response - # if the route (not file) is not matched, we redirect to index.html. Hence / and someroute response - Examples: - | Asset | StatusCode | - | /index.html | 200 | - | /images/picture.svg | 200 | - | /doesnotexist.html | 404 | - | /someroute | 302 | - | /protected.html | 511 | - | / | 200 | + Given a server with a 'client' configuration + And There are no files to serve + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '' + Then I get the expected status code '' response + Examples: + | Asset | StatusCode | + | /index.html | 404 | + | /images/picture.svg | 404 | + | /doesnotexist.html | 404 | + | /someroute | 404 | + | /protected.html | 404 | + | / | 404 | Scenario: Critical configuration is templated into index.html so the client can bootstrap - Given a 'client_only' server configuration - And There are files to serve - And Authentication is required - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '/index.html' - Then the file is returned as with the expected configuration included + Given a server with a 'client' configuration + And There are files to serve + And authentication type 'none' is required + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '/index.html' + Then the file is returned as with the expected configuration included + + Scenario Outline: With no authentication, the client module returns the appropriate return code for a request of + Given a server with a 'client' configuration + And There are files to serve + And authentication type 'none' is required + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '' + Then I get the expected status code '' response + # if the route (not file) is not matched, we redirect to /. Hence /someroute response + Examples: + | Asset | StatusCode | + | /index.html | 200 | + | /images/picture.svg | 200 | + | /doesnotexist.html | 404 | + | /someroute | 302 | + | /protected.html | 200 | + | / | 200 | + + Scenario Outline: With scram authentication, the client module returns the appropriate return code for an unauthenticated request of + Given a server with a 'client' configuration + And There are files to serve + And authentication type 'scram' is required + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '' + Then I get the expected status code '' response + # if the route is protected we redirect to login, and if the route isn't matched we redirect to / + Examples: + | Asset | StatusCode | + | /index.html | 302 | + | /images/picture.svg | 200 | + | /doesnotexist.html | 404 | + | /someroute | 302 | + | /protected.html | 302 | + | / | 302 | + + + Scenario Outline: With scram authentication, the client module returns the appropriate return code for an authenticated request of when I am logged in + Given a server with a 'client' configuration + And a 'security' module + And There are files to serve + And authentication type 'scram' is required + And the scram authentication accepts credentials + And I run an instance of the Strimzi-UI server + And all requests use the same session + When I send credentials to endpoint '/auth/login' + And I make a 'get' request to '' + Then I get the expected status code '' response + # if the route (not file) is not matched, we redirect to /. Hence /someroute response + Examples: + | Asset | StatusCode | + | /index.html | 200 | + | /images/picture.svg | 200 | + | /doesnotexist.html | 404 | + | /someroute | 302 | + | /protected.html | 200 | + | / | 200 | diff --git a/server/client/client.steps.ts b/server/client/client.steps.ts index 1510cef4..ec5cd0fa 100644 --- a/server/client/client.steps.ts +++ b/server/client/client.steps.ts @@ -3,10 +3,10 @@ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). */ import merge from 'lodash.merge'; -import { And, Then, Fusion } from 'jest-cucumber-fusion'; +import { And, Fusion, Then } from 'jest-cucumber-fusion'; import { - stepWithWorld, stepWhichUpdatesWorld, + stepWithWorld, } from 'test_common/commonServerSteps'; And( @@ -24,23 +24,15 @@ And( ); And('There are files to serve', () => { - // NO_OP - the `client_only` configuration is already configured to serve fixture files + // NO_OP - the `client` configuration is already configured to serve fixture files }); Then( /I get the expected status code '(.+)' response/, - stepWithWorld(async (world, statusCode) => { + stepWithWorld((world, statusCode) => { + const expectedStatus = parseInt(statusCode as string); const { request } = world; - await request.then( - (res) => { - const { status } = res; - const expectedStatus = parseInt(statusCode as string); - expect(status).toBe(expectedStatus); - }, - (err) => { - throw err; - } - ); + return request.expect(expectedStatus); }) ); @@ -48,7 +40,7 @@ Then( 'the file is returned as with the expected configuration included', stepWithWorld(async (world) => { const { request, configuration } = world; - const configuredAuthType = configuration.authentication.strategy; + const configuredAuthType = configuration.proxy.authentication.type; await request.then( (res) => { diff --git a/server/client/controller.ts b/server/client/controller.ts index 4b7e62bd..fcdb3670 100644 --- a/server/client/controller.ts +++ b/server/client/controller.ts @@ -25,7 +25,6 @@ const publicFiles = [ 'images/*', 'fonts/*', 'favicon.ico', - 'index.html', 'main.css', 'main.bundle.js', 'main.bundle.js.gz', @@ -79,10 +78,9 @@ export const renderTemplate: (indexTemplate: string) => (req, res) => void = ( ) => (req, res) => { const { entry, debug } = res.locals.strimziuicontext.logger; const { exit } = entry('renderTemplate'); - const { authentication } = res.locals.strimziuicontext - .config as serverConfigType; + const { proxy } = res.locals.strimziuicontext.config as serverConfigType; const bootstrapConfigs = { - authType: authentication.strategy, + authType: proxy.authentication.type, }; debug('Templating bootstrap config containing %o', bootstrapConfigs); res.send( diff --git a/server/client/router.ts b/server/client/router.ts index 09f3df4a..c2c6707d 100644 --- a/server/client/router.ts +++ b/server/client/router.ts @@ -11,7 +11,7 @@ const moduleName = 'client'; export const ClientModule: UIServerModule = { moduleName, - addModule: (logger, authFn, serverConfig) => { + addModule: (logger, { checkAuth }, serverConfig) => { const { publicDir } = serverConfig.client; const { exit } = logger.entry('addModule', publicDir); const routerForModule = express.Router(); @@ -30,7 +30,8 @@ export const ClientModule: UIServerModule = { logger.debug(`Client has index.html to serve? ${hasIndexFile}`); // add the auth middleware to all non public files - protectedFiles.forEach((file) => routerForModule.get(`${file}`, authFn)); + protectedFiles.forEach((file) => routerForModule.get(`${file}`, checkAuth)); + routerForModule.get('/', checkAuth); // return index.html, with configuration templated in hasIndexFile && @@ -45,9 +46,10 @@ export const ClientModule: UIServerModule = { // if no match, not a file (path contains '.'), and we have an index.html file, redirect to it (ie return index so client navigation logic kicks in). Else do nothing (404 unless another module handles it) hasIndexFile && - routerForModule.get(/^((?!\.).)+$/, (req, res) => - res.redirect(`/index.html`) - ); + routerForModule.get(/^((?!\.).)+$/, (req, res) => { + logger.info('Redirecting to index'); + res.redirect(`/`); + }); return exit({ mountPoint: '/', routerForModule }); }, diff --git a/server/config/config.feature b/server/config/config.feature index 8b92f196..f85c13f2 100644 --- a/server/config/config.feature +++ b/server/config/config.feature @@ -5,13 +5,13 @@ Feature: config module Behaviours and capabilities provided by the config module Scenario: Returns with the expected response for a config call - Given a 'config_only' server configuration - And I run an instance of the Strimzi-UI server - When I make a 'getConfigAndFeatureFlagQuery' gql request to '/config' - Then I get the expected config response + Given a server with a 'config' configuration + And I run an instance of the Strimzi-UI server + When I make a 'getConfigAndFeatureFlagQuery' gql request to '/config' + Then I get the expected config response Scenario: Returns with the expected response for a config call when config and feature flag overrides are present in the server configuration - Given a 'config_only_with_config_overrides' server configuration - And I run an instance of the Strimzi-UI server - When I make a 'getConfigAndFeatureFlagQueryWithConfigOverrides' gql request to '/config' - Then I get the expected config response with the config overrides present \ No newline at end of file + Given a server with a 'config_with_overrides' configuration + And I run an instance of the Strimzi-UI server + When I make a 'getConfigAndFeatureFlagQueryWithConfigOverrides' gql request to '/config' + Then I get the expected config response with the config overrides present \ No newline at end of file diff --git a/server/config/router.ts b/server/config/router.ts index 8661abbc..33ac9915 100644 --- a/server/config/router.ts +++ b/server/config/router.ts @@ -13,7 +13,7 @@ const moduleName = 'config'; export const ConfigModule: UIServerModule = { moduleName, - addModule: (logger, authFn, config) => { + addModule: (logger, { checkAuth }, config) => { const { exit } = logger.entry('addModule'); const routerForModule = express.Router(); @@ -22,7 +22,7 @@ export const ConfigModule: UIServerModule = { }); routerForModule.use( - authFn, + checkAuth, bodyParser.json(), server.getMiddleware({ path: '/' }) ); diff --git a/server/core/README.md b/server/core/README.md index 6f7a6247..ea7a2cec 100644 --- a/server/core/README.md +++ b/server/core/README.md @@ -15,13 +15,13 @@ The core module will import and interact with all other modules to implement the The core module will import all modules' default export. The export is expected to contain two keys - `moduleName` and `addModule`, as per the `UIServerModule` interface. `addModule` is expected to be a function, which takes the following parameters: ``` -const { mountPoint, routerFromModule } = myModule(logGenerator, authMiddleware, serverConfig); +const { mountPoint, routerFromModule } = myModule(logGenerator, authFunctions, serverConfig); ``` Where: - `logGenerator` is a function which will return a logger to be used in `addModule` only to trace entry, exit and any helpful diagnostics while this module mounts -- `authMiddleware` is an express middleware function to be inserted/used when a module's routes require a user to be authenticated to access them +- `authFunctions` is an object containing authentication express middleware function to be inserted/used when a module's routes require a user to be authenticated to access them - `serverConfig` is the server's configuration at start up. If your module requires configuration at mount, it can be accessed here. This function is to return an object containing two items. The first is the context route this module will be mounted on (eg `/dev`). The second is an express [Router](https://expressjs.com/en/4x/api.html#router), which this module will have appended it's handlers to. diff --git a/server/core/app.ts b/server/core/app.ts index 1c210378..4ecaaa8c 100644 --- a/server/core/app.ts +++ b/server/core/app.ts @@ -6,13 +6,14 @@ import express from 'express'; import helmet from 'helmet'; import * as availableModules from './modules'; import { serverConfigType, UIServerModule } from 'types'; -import { authFunction } from 'placeholderFunctionsToReplace'; import expressSession, { SessionOptions } from 'express-session'; +import bodyParser from 'body-parser'; import { generateLogger, generateHttpLogger, STRIMZI_UI_REQUEST_ID_HEADER, } from 'logging'; +import { bootstrapAuthentication } from 'security'; export const returnExpress: ( getConfig: () => serverConfigType @@ -20,7 +21,7 @@ export const returnExpress: ( const logger = generateLogger('core'); const app = express(); - const { session } = getConfig(); + const { session: sessionConfig, proxy: proxyConfig } = getConfig(); // add helmet middleware app.use(helmet()); @@ -31,12 +32,15 @@ export const returnExpress: ( //Add session middleware const sessionOpts: SessionOptions = { secret: 'CHANGEME', //TODO replace with value from config https://github.com/strimzi/strimzi-ui/issues/111 - name: session.name, + name: sessionConfig.name, cookie: { maxAge: 1000 * 3600 * 24 * 30, //30 days as a starting point //TODO replace with value from config https://github.com/strimzi/strimzi-ui/issues/111 }, }; app.use(expressSession(sessionOpts)); + app.use(bodyParser.json()); + + const authentication = bootstrapAuthentication(app, proxyConfig); // for each module, call the function to add it to the routing table const routingTable = Object.values(availableModules).reduce( @@ -51,7 +55,7 @@ export const returnExpress: ( const config = getConfig(); const { mountPoint, routerForModule } = addModule( generateLogger(moduleName), - authFunction(config.authentication), + authentication, config ); logger.info(`Mounted module '${moduleName}' on '${mountPoint}'`); diff --git a/server/core/core.feature b/server/core/core.feature index f325c262..ab64cb29 100644 --- a/server/core/core.feature +++ b/server/core/core.feature @@ -5,31 +5,31 @@ Feature: core module Behaviours and capabilities provided by the core module Scenario: When making a call with no strimzi-ui header, one is added for later requests - Given a 'mockapi_only' server configuration + Given a server with a 'mockapi' configuration And I run an instance of the Strimzi-UI server When I make a request with no unique request header Then a unique request header is returned in the response Scenario: When making a call with a strimzi-ui header, that header is used in the request - Given a 'mockapi_only' server configuration + Given a server with a 'mockapi' configuration And I run an instance of the Strimzi-UI server When I make a request with a unique request header Then the unique request header sent is returned in the response Scenario: When making a call to the strimzi-ui server, the expected secuirty headers are present - Given a 'mockapi_only' server configuration + Given a server with a 'mockapi' configuration And I run an instance of the Strimzi-UI server When I make a 'get' request to '/api/test' Then all expected security headers are present Scenario: If two modules mount routes on the same mounting point, and one is disabled, the enabled module is invoked - Given a 'mockapi_only' server configuration + Given a server with a 'mockapi' configuration And I run an instance of the Strimzi-UI server When I make a 'get' request to '/api/test' Then the mockapi handler is called Scenario: When making a call to the strimzi-ui server, the expected session cookie is present - Given a 'mockapi_only' server configuration + Given a server with a 'mockapi' configuration And a session identifier of 'server-name' And I run an instance of the Strimzi-UI server When I make a 'get' request to '/' diff --git a/server/core/modules.ts b/server/core/modules.ts index bdf7b514..a4ea9266 100644 --- a/server/core/modules.ts +++ b/server/core/modules.ts @@ -3,7 +3,8 @@ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). */ export * from 'api/index'; -export * from 'client/index'; export * from 'config/index'; export * from 'log/index'; export * from 'mockapi/index'; +export { SecurityModule } from 'security/index'; +export * from 'client/index'; diff --git a/server/log/log.feature b/server/log/log.feature index 426b7c6c..a6dbfa7a 100644 --- a/server/log/log.feature +++ b/server/log/log.feature @@ -5,21 +5,21 @@ Feature: log module Behaviours and capabilities provided by the log module Scenario: Returns with the expected HTTP response for a /log call - Given a 'log_only' server configuration - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '/log' - Then I get the expected log response + Given a server with a 'log' configuration + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '/log' + Then I get the expected log response Scenario: Sets up the WebSocket connection on /log call - Given a 'log_only' server configuration - And I run an instance of the Strimzi-UI server - And I enable WebSocket connections on the Strimzi-UI server - When I make a WebSocket connection request to '/log' - And I send a logging WebSocket message - And I send a logging WebSocket message without a clientLevel - And I send a logging WebSocket message that is not a JSON array - And I send an unparsable string logging WebSocket message - And I send a non-string logging WebSocket message - And I close the WebSocket - Then the WebSocket has received 5 messages - And the WebSocket is closed \ No newline at end of file + Given a server with a 'log' configuration + And I run an instance of the Strimzi-UI server + And I enable WebSocket connections on the Strimzi-UI server + When I make a WebSocket connection request to '/log' + And I send a logging WebSocket message + And I send a logging WebSocket message without a clientLevel + And I send a logging WebSocket message that is not a JSON array + And I send an unparsable string logging WebSocket message + And I send a non-string logging WebSocket message + And I close the WebSocket + Then the WebSocket has received 5 messages + And the WebSocket is closed \ No newline at end of file diff --git a/server/log/router.ts b/server/log/router.ts index 911de28a..c5ccfe40 100644 --- a/server/log/router.ts +++ b/server/log/router.ts @@ -16,12 +16,12 @@ const moduleName = 'log'; export const LogModule: UIServerModule = { moduleName, - addModule: (logger, authFn) => { + addModule: (logger, { checkAuth }) => { const { exit } = logger.entry('addModule'); const routerForModule = express.Router(); // implementation to follow - routerForModule.get('*', authFn, (req, res) => { + routerForModule.get('*', checkAuth, (req, res) => { const { isWs } = req as strimziUIRequestType; const { ws } = res as strimziUIResponseType; if (isWs) { diff --git a/server/mockapi/data.ts b/server/mockapi/data.ts index 034afa0a..5c63d0df 100644 --- a/server/mockapi/data.ts +++ b/server/mockapi/data.ts @@ -2,7 +2,17 @@ * Copyright Strimzi authors. * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). */ - +import { gql } from 'apollo-server-express'; // placeholder GQL schema for a topic/topic list - ideally to come from file -export const schema = - 'type Topic {name: String partitions: Int replicas: Int } type Query { topic(name: String): Topic topics: [Topic] } '; +export const schema = gql` + type Topic { + name: String + partitions: Int + replicas: Int + } + type Query { + topic(name: String): Topic + topics: [Topic] + clusterInfo: String + } +`; diff --git a/server/mockapi/mockapi.feature b/server/mockapi/mockapi.feature index 79b2a0cc..3513f9db 100644 --- a/server/mockapi/mockapi.feature +++ b/server/mockapi/mockapi.feature @@ -5,13 +5,13 @@ Feature: mockapi module Behaviours and capabilities provided by the mockapi module Scenario: Returns with the expected response for a mocked api call - Given a 'mockapi_only' server configuration - And I run an instance of the Strimzi-UI server - When I make a 'mockTopicsQuery' gql request to '/api' - Then I get the expected mockapi response + Given a server with a 'mockapi' configuration + And I run an instance of the Strimzi-UI server + When I make a 'mockTopicsQuery' gql request to '/api' + Then I get the expected mockapi response Scenario: Returns with the expected response for a call to the test endpoint - Given a 'mockapi_only' server configuration - And I run an instance of the Strimzi-UI server - When I make a 'get' request to '/api/test' - Then I get the expected mockapi test endpoint response \ No newline at end of file + Given a server with a 'mockapi' configuration + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '/api/test' + Then I get the expected mockapi test endpoint response \ No newline at end of file diff --git a/server/placeholderFunctionsToReplace.ts b/server/placeholderFunctionsToReplace.ts deleted file mode 100644 index 459e5948..00000000 --- a/server/placeholderFunctionsToReplace.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Strimzi authors. - * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). - */ -// placeholder functions - to be replaced by actual implementation later - -import express from 'express'; -import { authenticationConfigType } from 'types'; - -// https://github.com/orgs/strimzi/projects/2#card-44265081 -// function which returns a piece of express middleware for a given auth strategy -const authFunction: ( - config: authenticationConfigType -) => ( - req: express.Request, - res: express.Response, - next: express.NextFunction -) => void = ({ strategy }) => { - switch (strategy) { - default: - case 'none': - return (req, res, next) => next(); - case 'scram': - case 'oauth': - return (req, res) => res.sendStatus(511); // if auth on, reject for sake of example. This is a middleware, akin to passport doing its checks. - } -}; - -export { authFunction }; diff --git a/server/security/bootstrap.feature b/server/security/bootstrap.feature new file mode 100644 index 00000000..105ca973 --- /dev/null +++ b/server/security/bootstrap.feature @@ -0,0 +1,34 @@ +# Copyright Strimzi authors. +# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). +Feature: Bootstrapping passport + + Scenario: Check auth function when not authenticated + Given an Application + When I bootstrap passport with authentication type 'scram' + Then check auth redirects to '/auth/login' + + Scenario: Check auth function when authenticated - Scram + Given an Application + When I bootstrap passport with authentication type 'scram' + And the request is authenticated + Then check auth returns '200' + + Scenario: Check auth function - No auth + Given an Application + When I bootstrap passport with authentication type 'none' + Then check auth returns '200' + + Scenario: Logout function - Scram + Given an Application + When I bootstrap passport with authentication type 'scram' + Then logout removes the user + + Scenario: Logout function - No auth + Given an Application + When I bootstrap passport with authentication type 'none' + Then logout returns '200' + + Scenario: Unsupported auth type + Given an Application + When I bootstrap passport with authentication type 'unsupported' + Then an error is thrown diff --git a/server/security/bootstrap.steps.ts b/server/security/bootstrap.steps.ts new file mode 100644 index 00000000..276c4474 --- /dev/null +++ b/server/security/bootstrap.steps.ts @@ -0,0 +1,135 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { bootstrapPassport } from './bootstrap'; +import express from 'express'; +import { authenticationStrategies } from 'types'; +import { + stepWhichUpdatesWorld, + stepWithWorld, +} from 'test_common/commonServerSteps'; +import { When, Then, Fusion, And } from 'jest-cucumber-fusion'; +import request from 'supertest'; +import { Authentication } from 'security'; + +const proxyDefaults = { + hostname: '', + port: 0, + contextRoot: '/', + transport: {}, +}; + +When( + /^I bootstrap passport with authentication type '(\w+)'$/, + stepWhichUpdatesWorld((world, authType) => { + try { + const auth = bootstrapPassport(world.context.app as express.Application, { + authentication: { type: authType as authenticationStrategies }, + ...proxyDefaults, + }); + world.context.auth = auth; + } catch (e) { + world.context.error = e; + } + + return world; + }) +); + +And( + 'the request is authenticated', + stepWithWorld((world) => { + const app = world.context.app as express.Application; + app.get('*', (req, _res, next) => { + req.isAuthenticated = () => true; + return next(); + }); + }) +); + +Then( + /^check auth redirects to '(.+)'$/, + stepWithWorld((world, redirectUrl) => { + const app = world.context.app as express.Application; + const auth = world.context.auth as Authentication; + app.get('/test', auth.checkAuth, (_req, res) => { + res.send('success'); + }); + + return request(app) + .get('/test') + .expect(302) + .expect('Location', redirectUrl as string); + }) +); + +Then( + /^check auth returns '(\d+)'$/, + stepWithWorld((world) => { + const app = world.context.app as express.Application; + const auth = world.context.auth as Authentication; + app.get('/test', auth.checkAuth, (_req, res) => { + res.send('success'); + }); + return request(app).get('/test').expect(200); + }) +); + +Then( + /^logout returns '(\d+)'$/, + stepWithWorld((world) => { + const app = world.context.app as express.Application; + const auth = world.context.auth as Authentication; + app.get('/test', auth.logout, (_req, res) => { + res.send('success'); + }); + return request(app).get('/test').expect(200); + }) +); + +Then( + 'logout removes the user', + stepWithWorld((world) => { + const app = world.context.app as express.Application; + const auth = world.context.auth as Authentication; + const logout = jest.fn(); + app.use('*', (req, _res, next) => { + req.logout = logout; + next(); + }); + app.get('/test', auth.logout, (_req, res) => { + res.send('success'); + }); + return request(app) + .get('/test') + .expect(200) + .then(() => expect(logout).toHaveBeenCalled); + }) +); + +Then( + /^I bootstrap passport with authentication type '(\w+)'$/, + stepWhichUpdatesWorld((world, authType) => { + const auth = bootstrapPassport(world.context.app as express.Application, { + authentication: { type: authType as authenticationStrategies }, + ...proxyDefaults, + }); + + world.context.auth = auth; + return world; + }) +); + +Then( + 'an error is thrown', + stepWithWorld((world) => { + const { + context: { error }, + } = world; + + expect(error).toBeTruthy(); + }) +); + +Fusion('bootstrap.feature'); diff --git a/server/security/bootstrap.ts b/server/security/bootstrap.ts new file mode 100644 index 00000000..2887d829 --- /dev/null +++ b/server/security/bootstrap.ts @@ -0,0 +1,58 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { getStrategy } from './strategy/strategyFactory'; +import passport from 'passport'; +import { RequestHandler } from 'express'; +import { proxyConfigType, authenticationStrategies } from 'types'; +import { Application } from 'express'; +import { Authentication } from './types'; +import { apiRoot, scram } from './routeConfig'; +import { join } from 'path'; + +const noOp: RequestHandler = (_req, _res, next) => next(); +const noAuth = { + authenticate: noOp, + checkAuth: noOp, + logout: noOp, +}; + +export const bootstrapPassport = ( + app: Application, + config: proxyConfigType +): Authentication => { + const authenticationConfig = config.authentication; + + if (authenticationConfig.type === authenticationStrategies.NONE) { + return noAuth; + } + + app.use(passport.initialize()); + app.use(passport.session()); + + const authStrategy = getStrategy(config); + + passport.use(authStrategy.name, authStrategy.strategy); + + switch (authenticationConfig.type) { + case authenticationStrategies.SCRAM: { + passport.serializeUser((user, done) => done(null, user)); + passport.deserializeUser((user, done) => done(null, user)); + return { + authenticate: passport.authenticate(authStrategy.name), + checkAuth: (req, res, next) => { + return req.isAuthenticated() + ? next() + : res.redirect(join(apiRoot, scram.login)); + }, + logout: (req, _res, next) => { + req.logout(); + next(); + }, + }; + } + default: + throw new Error(`Unsupported type "${authenticationConfig.type}"`); + } +}; diff --git a/server/security/index.ts b/server/security/index.ts new file mode 100644 index 00000000..c69bb1ff --- /dev/null +++ b/server/security/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +export { bootstrapPassport as bootstrapAuthentication } from './bootstrap'; +export * from './types'; +export { SecurityModule } from './router'; diff --git a/server/security/routeConfig.ts b/server/security/routeConfig.ts new file mode 100644 index 00000000..fa808e75 --- /dev/null +++ b/server/security/routeConfig.ts @@ -0,0 +1,10 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +export const scram = { + login: '/login', + logout: '/logout', +}; + +export const apiRoot = '/auth'; diff --git a/server/security/router.ts b/server/security/router.ts new file mode 100644 index 00000000..89fd05dd --- /dev/null +++ b/server/security/router.ts @@ -0,0 +1,41 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import express from 'express'; +import { authenticationStrategies, UIServerModule } from '../types'; +import { apiRoot, scram } from './routeConfig'; + +const moduleName = 'security'; + +export const SecurityModule: UIServerModule = { + moduleName, + addModule: (logger, auth, { proxy }) => { + const authentication = proxy.authentication; + const { exit } = logger.entry('addModule', authentication); + const routerForModule = express.Router(); + + switch (authentication.type) { + case authenticationStrategies.SCRAM: { + logger.info('Mouting SCRAM security routes'); + routerForModule.post(scram.login, auth.authenticate, (_req, res) => + res.send(200) + ); + routerForModule.get( + scram.login, + (_req, res) => res.send('This will later be the login page') //https://github.com/strimzi/strimzi-ui/issues/110 + ); + routerForModule.post(scram.logout, auth.logout, (_req, res) => + res.send(200) + ); + break; + } + case authenticationStrategies.NONE: { + //noop + break; + } + } + + return exit({ mountPoint: apiRoot, routerForModule }); + }, +}; diff --git a/server/security/security.feature b/server/security/security.feature new file mode 100644 index 00000000..e68c98a4 --- /dev/null +++ b/server/security/security.feature @@ -0,0 +1,62 @@ +# Copyright Strimzi authors. +# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). +Feature: Security module + + Scenario: SCRAM - authenticate valid credentials + + Given a server with a 'security' configuration + And authentication type 'scram' is required + And I run an instance of the Strimzi-UI server + And the scram authentication accepts credentials + When I send credentials to endpoint '/auth/login' + Then I get the expected status code '200' response + + Scenario: SCRAM - authenticate invalid credentials + + Given a server with a 'security' configuration + And authentication type 'scram' is required + And I run an instance of the Strimzi-UI server + And the scram authentication rejects credentials + When I send credentials to endpoint '/auth/login' + Then I get the expected status code '401' response + + Scenario: SCRAM - login page + + Given a server with a 'security' configuration + And authentication type 'scram' is required + And I run an instance of the Strimzi-UI server + When I make a 'get' request to '/auth/login' + Then I get the expected status code '200' response and body 'This will later be the login page' + + + Scenario: SCRAM - logout + Given a server with a 'security' configuration + And authentication type 'scram' is required + And I run an instance of the Strimzi-UI server + And the scram authentication accepts credentials + When I make a 'post' request to '/auth/logout' + Then I get the expected status code '200' response + + Scenario: Off - authenticate + + Given a server with a 'security' configuration + And authentication type 'none' is required + And I run an instance of the Strimzi-UI server + When I send credentials to endpoint '/auth/login' + Then I get the expected status code '404' response + + Scenario: Off - login route + + Given a server with a 'security' configuration + And authentication type 'none' is required + And I run an instance of the Strimzi-UI server + When I send credentials to endpoint '/auth/login' + Then I get the expected status code '404' response + + Scenario: Off - logout + + Given a server with a 'security' configuration + And authentication type 'none' is required + And I run an instance of the Strimzi-UI server + When I make a 'post' request to '/auth/logout' + Then I get the expected status code '404' response diff --git a/server/security/security.steps.ts b/server/security/security.steps.ts new file mode 100644 index 00000000..7a8076d3 --- /dev/null +++ b/server/security/security.steps.ts @@ -0,0 +1,7 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { Fusion } from 'jest-cucumber-fusion'; +import 'test_common/commonServerSteps'; +Fusion('security.feature'); diff --git a/server/security/strategy/scram/scramAuthenticator.feature b/server/security/strategy/scram/scramAuthenticator.feature new file mode 100644 index 00000000..84dab5eb --- /dev/null +++ b/server/security/strategy/scram/scramAuthenticator.feature @@ -0,0 +1,28 @@ +# Copyright Strimzi authors. +# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). +Feature: Scram Authenticator + + Scenario Outline: Accepts a valid username and password + Given an authentication endpoint of 'https://strimzi-admin' + And the authentication endpoint accepts username '' and password '' + When a verify callback is generated + And I call verify with username '' and password '' + Then the callback should return a user with username '' and a token + + Examples: + | username | password | + | test-user | test-pw | + + Scenario: Rejects an invalid username and password + Given an authentication endpoint of 'https://strimzi-admin' + And the authentication endpoint refuses any credentials + When a verify callback is generated + And I call verify with username 'username' and password 'password' + Then the callback should return 'false' + + Scenario: Rejects when unable to authenticate + Given an authentication endpoint of 'https://strimzi-admin' + And the authentication endpoint returns a server error + When a verify callback is generated + And I call verify with username 'username' and password 'password' + Then the callback should return an error \ No newline at end of file diff --git a/server/security/strategy/scram/scramAuthenticator.steps.ts b/server/security/strategy/scram/scramAuthenticator.steps.ts new file mode 100644 index 00000000..6bdbc28c --- /dev/null +++ b/server/security/strategy/scram/scramAuthenticator.steps.ts @@ -0,0 +1,139 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { When, Then, Fusion, Given, And } from 'jest-cucumber-fusion'; +import { + stepWhichUpdatesWorld, + stepWithWorld, +} from 'test_common/commonServerSteps'; + +import { createVerifyCallback } from './scramAuthenticator'; +import { VerifyFunction } from 'passport-local'; +import nock from 'nock'; + +let verify: VerifyFunction; + +Given( + /^an authentication endpoint of '(\S+)'$/, + stepWhichUpdatesWorld((world, endpoint) => { + const context = world.context; + context.endpoint = endpoint as string; + context.nock = nock(context.endpoint as string); + return { + ...world, + context, + }; + }) +); + +And( + /^the authentication endpoint accepts username '(\S+)' and password '(\S+)'$/, + stepWhichUpdatesWorld((world, username, password) => { + const token = `Basic ${Buffer.from(`${username}:${password}`).toString( + 'base64' + )}`; + const { context } = world; + context.token = token; + context.nock = (context.nock as nock.Scope) + .matchHeader('authentication', token) + .post('/') + .reply(200, {}); + return { + ...world, + context, + }; + }) +); + +And( + 'the authentication endpoint refuses any credentials', + stepWhichUpdatesWorld((world) => { + const { context } = world; + + context.nock = (context.nock as nock.Scope).post('/').reply(200, { + errors: [ + { + message: 'auth error', + }, + ], + }); + return { + ...world, + context, + }; + }) +); + +And( + 'the authentication endpoint returns a server error', + stepWhichUpdatesWorld((world) => { + const { context } = world; + + context.nock = (context.nock as nock.Scope).post('/').reply(500); + return { + ...world, + context, + }; + }) +); + +When( + 'a verify callback is generated', + stepWithWorld((world) => { + const { + context: { endpoint }, + } = world; + verify = createVerifyCallback(endpoint as string); + }) +); + +And( + /^I call verify with username '(\S+)' and password '(\S+)'$/, + stepWhichUpdatesWorld((world, username, password) => { + return new Promise((resolve) => { + verify(username as string, password as string, (error, user) => { + const context = world.context; + resolve({ + ...world, + context: { ...context, error, user }, + }); + }); + }); + }) +); + +Then( + /^the callback should return a user with username '(\S+)' and a token$/, + stepWithWorld((world, username) => { + const { + context: { error, user, token }, + } = world; + expect(error).toBeNull(); + expect(user).toEqual({ username, token }); + }) +); + +Then( + /^the callback should return '(\S+)'$/, + stepWithWorld((world, result) => { + const { + context: { error, user }, + } = world; + expect(error).toBeNull(); + expect(JSON.stringify(user)).toEqual(result); + }) +); + +Then( + 'the callback should return an error', + stepWithWorld((world) => { + const { + context: { error, user }, + } = world; + expect(error).toBeTruthy(); + expect(user).toBeNull(); + }) +); + +Fusion('scramAuthenticator.feature'); diff --git a/server/security/strategy/scram/scramAuthenticator.ts b/server/security/strategy/scram/scramAuthenticator.ts new file mode 100644 index 00000000..0c81d61d --- /dev/null +++ b/server/security/strategy/scram/scramAuthenticator.ts @@ -0,0 +1,43 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import axios from 'axios'; +import { VerifyFunction } from 'passport-local'; +import { generateLogger } from 'logging'; + +const logger = generateLogger('ScramAuthenticator'); + +const createVerifyCallback = (endpoint: string): VerifyFunction => { + const { exit } = logger.entry('createVerifyCallback'); + + const verify = async (username, password, done) => { + const { exit } = logger.entry('createVerifyCallback - callback'); + const query = { query: 'query {clusterInfo}' }; + try { + const token = Buffer.from(`${username}:${password}`).toString('base64'); + const basicAuth = `Basic ${token}`; + const { data } = await axios.post(endpoint, query, { + //TODO HTTPS support + headers: { + Authentication: basicAuth, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); + + if (data.errors) { + logger.error('Error in Admin server response', data.errors); + return exit(done(null, false)); + } + + return exit(done(null, { username, token: basicAuth })); + } catch (err) { + logger.error('Error with admin server', err); + return exit(done(err, null)); + } + }; + return exit(verify); +}; + +export { createVerifyCallback }; diff --git a/server/security/strategy/strategyFactory.feature b/server/security/strategy/strategyFactory.feature new file mode 100644 index 00000000..423fa90a --- /dev/null +++ b/server/security/strategy/strategyFactory.feature @@ -0,0 +1,13 @@ +# Copyright Strimzi authors. +# License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). +Feature: Strategy Factory + + Scenario Outline: Strategies are returned of the expected type + + When a strategy factory is asked for type '' + Then the returned strategy is of type '' + Examples: + | type | expected | + | scram | scram | + | none | none | + | unknown | none | \ No newline at end of file diff --git a/server/security/strategy/strategyFactory.steps.ts b/server/security/strategy/strategyFactory.steps.ts new file mode 100644 index 00000000..5f690c13 --- /dev/null +++ b/server/security/strategy/strategyFactory.steps.ts @@ -0,0 +1,44 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { When, Then, Fusion } from 'jest-cucumber-fusion'; +import { + stepWhichUpdatesWorld, + stepWithWorld, +} from 'test_common/commonServerSteps'; + +import { AuthenticationStrategy, getStrategy } from './strategyFactory'; +import { authenticationStrategies, proxyConfigType } from 'types'; + +When( + /^a strategy factory is asked for type '(.+)'$/, + stepWhichUpdatesWorld((world, type) => { + const context = world.context; + const config: proxyConfigType = { + authentication: { + type: type as authenticationStrategies, + }, + hostname: '', + port: 0, + contextRoot: '', + transport: {}, + }; + context.strategy = getStrategy(config); + return { + ...world, + context, + }; + }) +); + +Then( + /^the returned strategy is of type '(.+)'$/, + stepWithWorld((world, type) => { + const { context } = world; + const strategy = context.strategy as AuthenticationStrategy; + expect(strategy.name).toEqual(type as string); + }) +); + +Fusion('strategyFactory.feature'); diff --git a/server/security/strategy/strategyFactory.ts b/server/security/strategy/strategyFactory.ts new file mode 100644 index 00000000..5482820a --- /dev/null +++ b/server/security/strategy/strategyFactory.ts @@ -0,0 +1,39 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { Strategy } from 'passport'; +import { authenticationStrategies, proxyConfigType } from 'types'; +import { Strategy as LocalStrategy } from 'passport-local'; +import { createVerifyCallback as createSaslCallback } from './scram/scramAuthenticator'; + +export interface AuthenticationStrategy { + name: string; + strategy: Strategy; +} + +export const getStrategy = ( + config: proxyConfigType +): AuthenticationStrategy => { + const authConfig = config.authentication; + + const strategy = { + name: authConfig.type.toString(), + }; + switch (authConfig.type) { + case authenticationStrategies.SCRAM: { + const endpoint = `http://${config.hostname}:${config.port}${config.contextRoot}`; //TODO https support + return { + ...strategy, + strategy: new LocalStrategy(createSaslCallback(endpoint)), + }; + } + case authenticationStrategies.NONE: + default: { + return { + name: authenticationStrategies.NONE, + strategy: new Strategy(), + }; + } + } +}; diff --git a/server/security/types.ts b/server/security/types.ts new file mode 100644 index 00000000..64bbb605 --- /dev/null +++ b/server/security/types.ts @@ -0,0 +1,16 @@ +/* + * Copyright Strimzi authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +import { RequestHandler } from 'express'; + +export interface Authentication { + authenticate: RequestHandler; + checkAuth: RequestHandler; + logout: RequestHandler; +} + +export interface User { + name: string; + accessToken: string; +} diff --git a/server/test_common/commonServerSteps.ts b/server/test_common/commonServerSteps.ts index 21f5fa12..40d228a1 100644 --- a/server/test_common/commonServerSteps.ts +++ b/server/test_common/commonServerSteps.ts @@ -4,13 +4,15 @@ */ import request from 'supertest'; import { returnExpress } from 'core'; -import { Given, When, And, Then } from 'jest-cucumber-fusion'; +import nock from 'nock'; +import { Given, When, And, Then, CallBack } from 'jest-cucumber-fusion'; import { serverConfigType, strimziUIRequestType, strimziUIResponseType, + authenticationStrategies, } from 'types'; -import { getConfigForName } from './testConfigs'; +import { getConfigForName, enableModule } from './testConfigs'; import express from 'express'; import merge from 'lodash.merge'; import { @@ -49,25 +51,56 @@ beforeEach(() => { resetWorld(); }); -Given( - /a '(.+)' server configuration/, +const withModule: [RegExp, CallBack] = [ + /^a '(.+)' module$/, + stepWhichUpdatesWorld((world, module) => { + const { configuration } = world; + + return { + ...world, + configuration: enableModule( + module as string, + configuration as serverConfigType + ), + }; + }), +]; + +Given(...withModule); +And(...withModule); + +const withConfig: [RegExp, CallBack] = [ + /^a server with a '(.+)' configuration$/, stepWhichUpdatesWorld((world, config) => { return { ...world, configuration: getConfigForName(config as string), }; + }), +]; + +Given(...withConfig); +And(...withConfig); + +Given( + 'an Application', + stepWhichUpdatesWorld((world) => { + world.context.app = express(); + return world; }) ); And( - 'Authentication is required', - stepWhichUpdatesWorld((world) => { - const { configuration } = world; + /^authentication type '(\w+)' is required$/, + stepWhichUpdatesWorld((world, type) => { + const configuration = merge(world.configuration, { + proxy: { + authentication: { type: type as authenticationStrategies }, + }, + }); return { ...world, - configuration: merge(configuration, { - authentication: { strategy: 'oauth' }, - }), + configuration, }; }) ); @@ -84,6 +117,18 @@ And( }) ); +And( + 'all requests use the same session', + stepWhichUpdatesWorld((world) => { + const { app } = world; + return { + ...world, + app, + server: request.agent(app), + }; + }) +); + And( 'I enable WebSocket connections on the Strimzi-UI server', stepWhichUpdatesWorld((world) => { @@ -109,21 +154,30 @@ And( }) ); -When( +const httpRequest: [RegExp, CallBack] = [ /^I make a '(.+)' request to '(.+)'$/, - stepWhichUpdatesWorld((world, method, endpoint) => { + stepWhichUpdatesWorld(async (world, method, endpoint) => { const { server } = world; + if (world.request) { + await world.request; + } return { ...world, request: server[method as string](endpoint), }; - }) -); + }), +]; + +When.call(null, ...httpRequest); +And.call(null, ...httpRequest); When( /^I make a '(.+)' gql request to '(.+)'$/, - stepWhichUpdatesWorld((world, requestName, endpoint) => { + stepWhichUpdatesWorld(async (world, requestName, endpoint) => { const { server } = world; + if (world.request) { + await world.request; + } const query = requests[requestName as string] || {}; @@ -207,4 +261,56 @@ const getMockedWebSocket: () => MockedWebSocket = () => { return websocket; }; +Then( + /I get the expected status code '(.+)' response$/, + stepWithWorld(async (world, statusCode) => { + const expectedStatus = parseInt(statusCode as string); + const { request } = world; + return request.expect(expectedStatus); + }) +); + +Then( + /I get the expected status code '(.+)' response and body '(.+)'$/, + stepWithWorld(async (world, statusCode, response) => { + const expectedStatus = parseInt(statusCode as string); + const { request } = world; + return request.expect(expectedStatus, response as string); + }) +); + +And( + 'the scram authentication accepts credentials', + stepWithWorld((world) => { + const { hostname, contextRoot, port } = world.configuration.proxy; + nock(`http://${hostname}:${port}`).post(contextRoot).reply(200, {}); + }) +); +And( + 'the scram authentication rejects credentials', + stepWithWorld((world) => { + const { hostname, contextRoot, port } = world.configuration.proxy; + nock(`http://${hostname}:${port}`) + .post(contextRoot) + .reply(200, { errors: ['unauth'] }); + }) +); + +When( + /^I send credentials to endpoint '(.+)'$/, + stepWhichUpdatesWorld(async (world, endpoint) => { + const { server } = world; + if (world.request) { + await world.request; + } + return { + ...world, + request: server.post(endpoint as string).send({ + username: 'user', + password: 'password', + }), + }; + }) +); + export { stepWhichUpdatesWorld, stepWithWorld }; diff --git a/server/test_common/testConfigs.ts b/server/test_common/testConfigs.ts index 696b6241..06e4c8d6 100644 --- a/server/test_common/testConfigs.ts +++ b/server/test_common/testConfigs.ts @@ -18,25 +18,23 @@ const modules = { config: false, log: false, mockapi: false, + security: false, }; -const mockapiModuleConfig: () => serverConfigType = () => +const singleModule: (module: string) => serverConfigType = (module) => merge({}, defaultTestConfig(), { - modules: { ...modules, mockapi: true }, + modules: { ...modules, ...{ [module]: true } }, }); -const logModuleConfig: () => serverConfigType = () => - merge({}, defaultTestConfig(), { - modules: { ...modules, log: true }, - }); - -const configModuleConfig: () => serverConfigType = () => - merge({}, defaultTestConfig(), { - modules: { ...modules, config: true }, +const clientModuleConfig: () => serverConfigType = () => + merge(singleModule('client'), { + client: { + publicDir: resolve(__dirname, './__test_fixtures__/client'), + }, }); const configModuleWithConfigOverrides: () => serverConfigType = () => - merge({}, configModuleConfig(), { + merge(singleModule('config'), { client: { configOverrides: { version: '34.0.0', @@ -52,35 +50,25 @@ const configModuleWithConfigOverrides: () => serverConfigType = () => }, }); -const clientModuleConfig: () => serverConfigType = () => - merge({}, defaultTestConfig(), { - client: { - publicDir: resolve(__dirname, './__test_fixtures__/client'), - }, - modules: { ...modules, client: true }, - }); - const apiModuleConfig: () => serverConfigType = () => - merge({}, defaultTestConfig(), { + merge(singleModule('api'), { proxy: { hostname: 'test-backend', port: 3434, }, - modules: { ...modules, api: true }, }); const apiModuleConfigWithCustomContextRoot: () => serverConfigType = () => - merge({}, defaultTestConfig(), { + merge(singleModule('api'), { proxy: { hostname: 'test-backend', port: 3434, contextRoot: '/myCustomContextRoot', }, - modules: { ...modules, api: true }, }); const securedApiModuleConfig: () => serverConfigType = () => - merge(apiModuleConfig(), { + merge(singleModule('api'), { proxy: { transport: { cert: 'mock certificate', @@ -88,27 +76,30 @@ const securedApiModuleConfig: () => serverConfigType = () => }, }); +export const enableModule: ( + module: string, + config: serverConfigType +) => serverConfigType = (module, config) => + merge({}, config, { + modules: { [module]: true }, + }); + export const getConfigForName: (name: string) => serverConfigType = (name) => { switch (name) { - default: - case 'default': - case 'production': - return defaultTestConfig(); - case 'mockapi_only': - return mockapiModuleConfig(); - case 'log_only': - return logModuleConfig(); - case 'config_only': - return configModuleConfig(); - case 'config_only_with_config_overrides': - return configModuleWithConfigOverrides(); - case 'client_only': - return clientModuleConfig(); - case 'api_only': - return apiModuleConfig(); - case 'api_secured_only': - return securedApiModuleConfig(); - case 'api_with_custom_context_root': - return apiModuleConfigWithCustomContextRoot(); + case 'default': + case 'production': + return defaultTestConfig(); + case 'config_with_overrides': + return configModuleWithConfigOverrides(); + case 'client': + return clientModuleConfig(); + case 'api': + return apiModuleConfig(); + case 'api_secured': + return securedApiModuleConfig(); + case 'api_with_custom_context_root': + return apiModuleConfigWithCustomContextRoot(); + default: + return singleModule(name); } }; diff --git a/server/tsconfig.json b/server/tsconfig.json index a128f89f..38add7ca 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "CommonJS", "outDir": "../js/server", - "baseUrl": "./", + "baseUrl": ".", "paths": { "ui-config/*": ["../config/*"] } diff --git a/server/types.ts b/server/types.ts index f947ed77..9759f1ec 100644 --- a/server/types.ts +++ b/server/types.ts @@ -7,15 +7,17 @@ import WebSocket from 'ws'; import { SecureVersion } from 'tls'; import { Level, Logger, LoggerOptions } from 'pino'; import { exposedClientType, exposedFeatureFlagsType } from 'ui-config/types'; +import { Authentication } from 'security'; -export type supportedAuthenticationStrategyTypes = 'none' | 'scram' | 'oauth'; +export enum authenticationStrategies { + NONE = 'none', + SCRAM = 'scram', +} -export type authenticationConfigType = { +export interface authenticationConfig { /** What authentication strategy to use to authenticate users */ - strategy: supportedAuthenticationStrategyTypes; - /** Any additional configuration required for the provided authentication strategy */ - configuration?: Record; -}; + type: authenticationStrategies; +} type sslCertificateType = { /** certificate in PEM format */ @@ -50,7 +52,7 @@ type moduleConfigType = { mockapi: boolean; }; -type proxyConfigType = { +export type proxyConfigType = { /** The Hostname of the backend server to send API requests to */ hostname: string; /** The port number of the backend server to send API requests to */ @@ -59,6 +61,8 @@ type proxyConfigType = { contextRoot: string; /** SSL transport configuration */ transport: sslCertificateType; + /** authentication configuration */ + authentication: authenticationConfig; }; type sessionConfigType = { @@ -67,8 +71,6 @@ type sessionConfigType = { }; export type serverConfigType = { - /** authentication configuration */ - authentication: authenticationConfigType; /** client (browser) facing configuration */ client: clientConfigType; /** feature flag configuration overrides (for both client and server) */ @@ -115,7 +117,7 @@ interface addModule { /** function called to add a module to the UI server */ ( mountLogger: entryExitLoggerType, - authFunction: expressMiddleware, + authentication: Authentication, configAtServerStart: serverConfigType ): { /** the root/mounting point for requests made to this module */ @@ -132,14 +134,6 @@ export type UIServerModule = { addModule: addModule; }; -export interface expressMiddleware { - /** typing of a general piece of express middleware */ - ( - req: express.Request, - res: express.Response, - next: express.NextFunction - ): void; -} /** the request object provided on UI server request. Core express request plus additions */ export type strimziUIRequestType = express.Request & { /** indicates this request is a websocket request (and that the response will have a ws object to interact with) */ diff --git a/test_common/jest_cucumber_support/commonTestTypes.ts b/test_common/jest_cucumber_support/commonTestTypes.ts index 0de3f6a3..862c5ba3 100644 --- a/test_common/jest_cucumber_support/commonTestTypes.ts +++ b/test_common/jest_cucumber_support/commonTestTypes.ts @@ -11,6 +11,9 @@ interface withWorldInterface { (callback: (world: T, ...others: Array) => T): ( ...others: Array ) => void; + (callback: (world: T, ...others: Array) => Promise): ( + ...others: Array + ) => void; (callback: (world: T, ...others: Array) => void): ( ...others: Array ) => void; diff --git a/test_common/jest_cucumber_support/index.ts b/test_common/jest_cucumber_support/index.ts index 41c84c14..bed6cd93 100644 --- a/test_common/jest_cucumber_support/index.ts +++ b/test_common/jest_cucumber_support/index.ts @@ -3,5 +3,5 @@ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). */ /* eslint-disable */ -import { common_stepdefs } from './commonStepdefs'; +import './commonStepdefs'; import * as commonServerStepDefinitions from '../../server/test_common/commonServerSteps'; diff --git a/utils/dev_config/mockadmin.config.js b/utils/dev_config/mockadmin.config.js index 7876a65e..0436a969 100644 --- a/utils/dev_config/mockadmin.config.js +++ b/utils/dev_config/mockadmin.config.js @@ -9,9 +9,6 @@ const { const { mockadminServer } = devEnvValues; module.exports = { - authentication: { - strategy: 'none', - }, client: { transport: { ...mockAdminCertificates, @@ -31,6 +28,9 @@ module.exports = { }, proxy: { transport: {}, + authentication: { + type: 'none', + }, }, ...mockadminServer, }; diff --git a/utils/dev_config/server.dev.config.js b/utils/dev_config/server.dev.config.js index c1205fba..d6009e96 100644 --- a/utils/dev_config/server.dev.config.js +++ b/utils/dev_config/server.dev.config.js @@ -27,12 +27,16 @@ module.exports = { config: true, log: true, mockapi: false, + security: true, }, proxy: { ...mockadminServer, transport: { ...mockAdminCertificates, }, + authentication: { + type: 'none', + }, }, ...devServer, }; diff --git a/utils/dev_config/server.e2e.config.js b/utils/dev_config/server.e2e.config.js index c74771ca..1389acfb 100644 --- a/utils/dev_config/server.e2e.config.js +++ b/utils/dev_config/server.e2e.config.js @@ -27,6 +27,9 @@ module.exports = { transport: { ...mockAdminCertificates, }, + authentication: { + type: 'none', + }, }, ...devServer, }; diff --git a/utils/tooling/runtimeDevUtils.js b/utils/tooling/runtimeDevUtils.js index fd6cc23a..030a6369 100644 --- a/utils/tooling/runtimeDevUtils.js +++ b/utils/tooling/runtimeDevUtils.js @@ -17,6 +17,9 @@ const devEnvValues = { hostname: process.env.MA_HOSTNAME || 'localhost', port: process.env.MA_PORT || 9080, contextRoot: process.env.MA_CONTEXT_ROOT || '/api', + authentication: { + type: 'none', + }, }, // (development instance) server hostname and port devServer: {