├── docs
├── CNAME
├── favicon.ico
├── mobileapp.md
├── versioning.md
├── kubernetes.md
├── architecture.md
├── dockercompose.md
├── providers.md
├── requirements.md
├── buildsource.md
├── proxy.md
├── securitymodel.md
├── first-agent.md
├── protocol.md
└── README.md
├── src
├── ee
│ ├── .gitignore
│ ├── .npmignore
│ ├── OpenAPIauthentication.ts
│ ├── LICENSE
│ ├── FaaS.ts
│ ├── Billings.ts
│ └── OpenAPIProxy.ts
├── TokenRequest.ts
├── Mutex.ts
├── test
│ ├── Audit.test.ts
│ ├── testConfig.ts
│ ├── Auth.test.ts
│ ├── Message.test.ts
│ ├── KubeUtil.test.ts
│ ├── Crypt.test.ts
│ ├── Logger.test.ts
│ ├── Config.test.ts
│ ├── amqp.test.ts
│ ├── loadtest.test.ts
│ ├── DBHelper.test.ts
│ ├── basic_entities.test.ts
│ ├── workitemqueue.test.ts
│ ├── workitemqueue-messages.test.ts
│ └── DatabaseConnection.test.ts
├── EntityRestriction.ts
├── SocketMessage.ts
├── Util.ts
├── rabbitmq.ts
├── QueueClient.ts
├── Crypt.ts
├── SamlProvider.ts
├── Auth.ts
├── MongoAdapter.ts
└── index.ts
├── .mocharc.json
├── public.template
├── libs
│ └── fonts
│ │ └── fontawesome-webfont.woff2
└── index.html
├── .vscode
├── settings.json
└── launch.json
├── .nyrc.json
├── .gitignore
├── .dockerignore
├── register.js
├── .github
├── ISSUE_TEMPLATE
│ ├── config.yml
│ └── bug_report.md
├── FUNDING.yml
└── workflows
│ └── codeql-analysis.yml
├── readme.md
├── .npmignore
├── Dockerfile
├── tsconfig.json
├── tsoa.json
├── SECURITY.md
├── Makefile
└── package.json
/docs/CNAME:
--------------------------------------------------------------------------------
1 | openflow.openiap.io
--------------------------------------------------------------------------------
/docs/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openiap/opencore/HEAD/docs/favicon.ico
--------------------------------------------------------------------------------
/src/ee/.gitignore:
--------------------------------------------------------------------------------
1 | license-file.ts
2 | otel.ts
3 | KubeUtil.ts
4 | kubedriver.ts
5 | grafana-proxy.ts
--------------------------------------------------------------------------------
/src/ee/.npmignore:
--------------------------------------------------------------------------------
1 | license-file.ts
2 | otel.ts
3 | KubeUtil.ts
4 | kubedriver.ts
5 | grafana-proxy.ts
--------------------------------------------------------------------------------
/.mocharc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/mocharc.json",
3 | "require": ["./register.js","tsx"]
4 | }
--------------------------------------------------------------------------------
/public.template/libs/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/openiap/opencore/HEAD/public.template/libs/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/docs/mobileapp.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/versioning.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/History.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/kubernetes.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/Kubernetes.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/architecture.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/Architecture.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/dockercompose.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/DockerCompose.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/providers.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/SigninProviders.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/requirements.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/Requirements.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "protoc": {
3 | "options": [
4 | "--proto_path=messages"
5 | ]
6 | },
7 | "compile-hero.disable-compile-files-on-did-save-code": true,
8 | "cmake.ignoreCMakeListsMissing": true,
9 | "makefile.configureOnOpen": false
10 | }
--------------------------------------------------------------------------------
/docs/buildsource.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/Build-from-source.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/proxy.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/Offline-Proxy-Server.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/securitymodel.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/Security-Model.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/docs/first-agent.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/Agent-Getting-Started.html)
3 |
6 |
7 |
--------------------------------------------------------------------------------
/.nyrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@istanbuljs/nyc-config-typescript",
3 | "include": [
4 | "src/**/*.ts"
5 | ],
6 | "exclude": [
7 | "node_modules/"
8 | ],
9 | "extension": [
10 | ".ts"
11 | ],
12 | "reporter": [
13 | "text-summary",
14 | "html"
15 | ],
16 | "report-dir": "./coverage"
17 | }
--------------------------------------------------------------------------------
/src/TokenRequest.ts:
--------------------------------------------------------------------------------
1 | import { Base } from "./commoninterfaces.js";
2 | import { Util } from "./Util.js";
3 |
4 | export class TokenRequest extends Base {
5 | constructor(code: string) {
6 | super();
7 | this._type = "tokenrequest";
8 | if (Util.IsNullEmpty(code)) this.code = "";
9 | }
10 | public code: string;
11 | public jwt: string;
12 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | logs
3 | dist
4 | config
5 | temp
6 | letsencrypt
7 | .cache
8 | .npmrc
9 | .scannerwork
10 | docker-package.json
11 | .env
12 | .nyc_output
13 | sonar-scanner.properties
14 | sonar-project.properties
15 | crash.log
16 | package-lock.json
17 | *.heapsnapshot
18 | /proto
19 | .clinic
20 | docker-compose.yml
21 | .aider*
22 | /public
23 | /public2
24 | stripe
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | logs
3 | config
4 | temp
5 | letsencrypt
6 | .cache
7 | .npmrc
8 | .scannerwork
9 | docker-package.json
10 | .env
11 | .nyc_output
12 | sonar-scanner.properties
13 | sonar-project.properties
14 | crash.log
15 | .devcontainer.old
16 | .clinic
17 | docker-compose.yml
18 | Makefile
19 | readme.md
20 | src/test
21 | register.js
22 | .mocharc.json
23 | .nyrc.json
24 | tsoa.json
25 | stripe
26 | Dockerfile
--------------------------------------------------------------------------------
/register.js:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | const env = path.join(process.cwd(), "config", ".env");
3 | import { config } from "dotenv";
4 | config({ path: env }); // , debug: false
5 | process.env.TSX_TSCONFIG_PATH = path.join(process.cwd(), "tsconfig.json");
6 |
7 | // import tsNode from "ts-node";
8 | // tsNode.register({
9 | // files: true,
10 | // transpileOnly: true,
11 | // project: "./tsconfig.json"
12 | // });
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 💬🤷💻🤦 RocketChat
4 | url: https://rocket.openiap.io
5 | about: Please go here for usage help and general questions and to meet everyone, use github only for bug reports.
6 | #- name: 🤷💻🤦 Slack
7 | # url: https://slack.openrpa.dk/
8 | # about: Please go here for usage help and general questions, use only github is only for bug reports.
9 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Core
2 | Collect, transport, store and report on events and data from humans, IT systems and the physical world
3 |
4 | Try it online here [here](https://app.openiap.io/)
5 |
6 | Installation guides and documentation at [docs.openiap.io](https://docs.openiap.io/docs/flow/)
7 |
8 | ## **community help**
9 | Join the 🤷💻🤦 [community forum](https://discourse.openiap.io/)
10 |
11 | ## **Commercial Support**
12 | Click here for💲🤷 [Commercial Support](https://openiap.io/)
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .github
2 | .vscode
3 | .npmrc
4 | .cache
5 | src
6 | config
7 | docs
8 | node_modules
9 | logs
10 | package-lock.json
11 | tsconfig.json
12 | temp
13 | letsencrypt
14 | webpack.config.js
15 | gulpfile.js
16 | CONTRIBUTING
17 | Dockerfile
18 | docker-compose.yml
19 | sonar-project.properties
20 | .nyc_output
21 | .scannerwor
22 | test
23 | crash.log
24 | .scannerwork
25 | .mocharc.json
26 | .nyrc.json
27 | dockerignore
28 | register.js
29 | .devcontainer.old
30 | docker-package.json
31 | .dockerignore
32 | *.heapsnapshot
33 | .clinic
34 | /public
35 | /public2
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20 AS build-env
2 | WORKDIR /app
3 |
4 | RUN npm i -g typescript
5 | COPY package.json /app/
6 | COPY package-lock.json /app/
7 | RUN npm ci --omit=dev
8 | COPY . /app
9 | RUN npm run build
10 |
11 | # https://github.com/GoogleContainerTools/distroless
12 | FROM gcr.io/distroless/nodejs20-debian12
13 | COPY --from=build-env /app /app
14 | COPY public /app/dist/public
15 | COPY public.template /app/dist/public.template
16 | WORKDIR /app/dist
17 |
18 | ENV HOME=.
19 | EXPOSE 3000
20 | EXPOSE 5858
21 | CMD ["--inspect=0.0.0.0:5858", "index.js"]
22 |
--------------------------------------------------------------------------------
/docs/protocol.md:
--------------------------------------------------------------------------------
1 |
2 | [Moved to](https://docs.openiap.io/docs/flow/SigninProviders.html)
3 |
6 |
7 |
8 | [Moved to](https://docs.openiap.io/docs/flow/ProtocolDetails.html)
9 |
12 |
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "emitDecoratorMetadata": true,
4 | "experimentalDecorators": true,
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "target": "ESNext",
8 | "outDir": "dist",
9 | "rootDir": "src",
10 | "resolveJsonModule": true,
11 | "esModuleInterop": true,
12 | "moduleResolution": "node",
13 | "sourceMap": true,
14 | "declaration": true,
15 | "lib": [
16 | "es2018",
17 | "dom"
18 | ]
19 | },
20 | "exclude": [
21 | "dist",
22 | "public",
23 | "node_modules"
24 | ]
25 | }
--------------------------------------------------------------------------------
/src/Mutex.ts:
--------------------------------------------------------------------------------
1 | export class Mutex {
2 | private mutex = Promise.resolve();
3 |
4 | lock(): PromiseLike<() => void> {
5 | let begin: (unlock: () => void) => void = unlock => { };
6 |
7 | this.mutex = this.mutex.then(() => {
8 | return new Promise(begin);
9 | });
10 |
11 | return new Promise(res => {
12 | begin = res;
13 | });
14 | }
15 |
16 | async dispatch(fn: (() => T) | (() => PromiseLike)): Promise {
17 | const unlock = await this.lock();
18 | try {
19 | return await Promise.resolve(fn());
20 | } finally {
21 | unlock();
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [skadefro]
2 | # These are supported funding model platforms
3 | # github: [skadefro]
4 | # patreon: # Replace with a single Patreon username
5 | # open_collective: # Replace with a single Open Collective username
6 | # ko_fi: # Replace with a single Ko-fi username
7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | # liberapay: skadefro
10 | # issuehunt: # Replace with a single IssueHunt username
11 | # otechie: # Replace with a single Otechie username
12 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/tsoa.json:
--------------------------------------------------------------------------------
1 | {
2 | "entryFile": "src/ee/OpenAPIProxy.ts",
3 | "noImplicitAdditionalProperties": "throw-on-extras",
4 | "controllerPathGlobs2": ["src/**/*Controller.ts"],
5 | "controllerPathGlobs": ["src/ee/OpenAPIuser.ts"],
6 | "spec": {
7 | "outputDirectory": "src/public",
8 | "specVersion": 3,
9 | "securityDefinitions": {
10 | "oidc": {
11 | "type": "openIdConnect",
12 | "openIdConnectUrl": "https://app.openiap.io/oidc/.well-known/openid-configuration"
13 | }
14 |
15 | }
16 | },
17 | "routes": {
18 | "routesDir": "src/ee/build",
19 | "authenticationModule": "src/ee/OpenAPIauthentication.ts"
20 | }
21 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | Either create a simple workflow, export it and attach to this issue or explain in details
16 | 1. Go to '...'
17 | 2. Click on '....'
18 | 3. Scroll down to '....'
19 | 4. See error
20 |
21 | **Expected behavior**
22 | A clear and concise description of what you expected to happen.
23 |
24 | **Screenshots**
25 | If applicable, add screenshots to help explain your problem.
26 |
27 | **Desktop (please complete the following information):**
28 | - Using app.openiap.io or selfhosted
29 | - If selfhosted, what version ( version is showed admin menu )
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/src/ee/OpenAPIauthentication.ts:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { Auth } from "../Auth.js";
3 | import { User } from "../commoninterfaces.js";
4 |
5 | export function expressAuthentication(request: express.Request, securityName: string, scopes?: string[]): Promise {
6 | if (securityName === "api_key") {
7 | let token;
8 | if (request.query && request.query.access_token) {
9 | token = request.query.access_token;
10 | }
11 | }
12 |
13 | if (securityName === "jwt" || securityName === "oidc") {
14 | let token =
15 | request.body.token ||
16 | request.query.token ||
17 | request.headers["x-access-token"] ||
18 | request.headers["authorization"];
19 | token = token?.replace("Bearer ", "");
20 |
21 | return new Promise(async (resolve, reject) => {
22 | let user: User = null;
23 | try {
24 | user = await Auth.Token2User(token, null);
25 | if(user == null) return reject(new Error("Access denied"));
26 | resolve(user);
27 | } catch (error) {
28 | reject(error);
29 | }
30 | });
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | If you discover any security issues, please let us know by one of the following methods:
6 |
7 | - **GitHub Private Security Advisories**
8 | Submit a private advisory on this repository.
9 | - **Email**
10 | Send an email to **security@openiap.io**.
11 |
12 | We aim to acknowledge all valid reports within **48 hours**.
13 |
14 | ## Supported Versions
15 |
16 | We actively provide security fixes for the **two most recent major releases**.
17 | If you’re running an older version, please upgrade to continue receiving important updates.
18 |
19 | ## Security Updates
20 |
21 | - **GitHub Security Advisories**
22 | Subscribe to be notified of any published advisories.
23 | - **Dependabot & Automated Scans**
24 | We use Dependabot and GitHub’s code-scanning tools to catch vulnerabilities early.
25 | - **Third-Party Review & Penetration Testing**
26 | We engage with independent auditors—hired by organizations using our platform—to perform annual code reviews and active penetration tests.
27 |
28 | ## Disclosure & Bounty
29 |
30 | - Public disclosure is encouraged once a fix is available.
31 | - We do **not** currently run a paid bug-bounty program.
32 |
33 | Thank you for helping us keep the project secure!
34 |
--------------------------------------------------------------------------------
/public.template/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
12 | OpenIAP Core
13 |
14 |
15 | /public is missing.
16 |
17 |
18 | - To use angularjs version (current) clone and build /open-rpa/openflow-web and place it in /public folder
19 | - To use new svelte version (next) clone and build /openiap/core-web and place it in /public folder
20 | - To use a custom version using vue3, clone and build /openiap/vue3-web-template and place it in /public folder
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/test/Audit.test.ts:
--------------------------------------------------------------------------------
1 | import { suite, test, timeout } from "@testdeck/mocha";
2 | import { Audit } from "../Audit.js";
3 | import { Config } from "../Config.js";
4 | import { Crypt } from "../Crypt.js";
5 | import { testConfig } from "./testConfig.js";
6 |
7 | @suite class audit_test {
8 | @timeout(10000)
9 | async before() {
10 | await testConfig.configure();
11 | }
12 | async after() {
13 | await testConfig.cleanup();
14 | }
15 | @test async "reload"() {
16 | await Audit.LoginSuccess(testConfig.testUser, "local", "local", "127.0.0.1", "test", Config.version, null);
17 | await Audit.LoginFailed(testConfig.testUser.username, "local", "local", "127.0.0.1", "test", Config.version, null);
18 | await Audit.ImpersonateSuccess(testConfig.testUser, Crypt.rootUser(), "test", Config.version, null);
19 | await Audit.ImpersonateFailed(testConfig.testUser, Crypt.rootUser(), "test", Config.version, null);
20 | await Audit.CloudAgentAction(testConfig.testUser, true, testConfig.testUser.username, "createdeployment", "openiap/nodered", testConfig.testUser.username, null);
21 | await Audit.CloudAgentAction(testConfig.testUser, true, testConfig.testUser.username, "deletedeployment", "openiap/nodered:latest", testConfig.testUser.username, null);
22 | await new Promise(resolve => { setTimeout(resolve, 1000) })
23 | }
24 | }
25 | // clear && ./node_modules/.bin/_mocha "src/test/Audit.test.ts"
26 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "args": [],
6 | "cwd": "${workspaceRoot}",
7 | "envFile": "${workspaceFolder}/config/.env",
8 | "name": "Core",
9 | "outFiles": [
10 | "${workspaceRoot}/dist/**/*",
11 | "**/node_modules/@openiap/**/*",
12 | ],
13 | "outputCapture": "std",
14 | "program": "${workspaceRoot}/src/index.ts",
15 | "request": "launch",
16 | "preLaunchTask": "tsc: watch - tsconfig.json",
17 | "runtimeArgs": [
18 | "--inspect"
19 | ],
20 | "runtimeExecutable": null,
21 | "sourceMaps": true,
22 | "stopOnEntry": false,
23 | "type": "node",
24 | "env": {
25 | "otel_log_level": "info",
26 | "GIT_DEBUG": "1"
27 | },
28 | "resolveSourceMapLocations": [
29 | "${workspaceFolder}/**",
30 | "!**/node_modules/**",
31 | "**/node_modules/@openiap/**",
32 | ]
33 | },
34 | {
35 | "type": "node",
36 | "request": "attach",
37 | "name": "Attach to localhost",
38 | "preLaunchTask": "tsc: watch - tsconfig.json",
39 | "address": "localhost",
40 | "port": 5858,
41 | "localRoot": "${workspaceFolder}/dist",
42 | "remoteRoot": "/app/dist"
43 | }
44 | ]
45 | }
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: build
2 |
3 | VERSION = 1.5.11.124
4 | HASH = $(shell git rev-parse --short HEAD)
5 | bump:
6 | @echo "Bumping version to $(VERSION) recursively..."
7 |
8 | @sed -i 's/"version": "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+"/"version": "$(VERSION)"/' package.json
9 | @find public.template -name "swagger.json" -exec sed -i 's/"version": "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+"/"version": "$(VERSION)"/' {} \;
10 | @find src/public -name "swagger.json" -exec sed -i 's/"version": "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+"/"version": "$(VERSION)"/' {} \;
11 | build: bump
12 | @npm run build
13 | initdocker:
14 | @docker buildx create --name openiap --use
15 | @docker buildx inspect --bootstrap
16 | load:
17 | @docker buildx build -t openiap/openflow:$(VERSION) -t openiap/openflow:$(HASH) -t openiap/openflow:edge --platform linux/amd64 --load .
18 | compose-no-cache: bump
19 | @docker buildx build --no-cache -t openiap/openflow:$(VERSION) -t openiap/openflow:$(HASH) -t openiap/openflow:edge --platform linux/amd64 --push .
20 | compose: bump
21 | @docker buildx build -t openiap/openflow:$(VERSION) -t openiap/openflow:$(HASH) -t openiap/openflow:edge --platform linux/amd64 --push .
22 | publish: bump
23 | @docker buildx build -t openiap/openflow:$(VERSION) -t openiap/openflow:$(HASH) -t openiap/openflow:latest --platform linux/amd64,linux/arm64,linux/arm/v7 --push .
24 | copypublic: bump
25 | @rm -rf public && cp -r ../core-web/build/ public
26 | copypublicold: bump
27 | @rm -rf public && cp -r ../openflow-web/dist/ public
28 | linkpublicold: bump
29 | @rm -rf public && ln -s /mnt/data/vscode/config/workspace/code/openflow-web/dist /mnt/data/vscode/config/workspace/code/openflow/public
30 |
--------------------------------------------------------------------------------
/src/test/testConfig.ts:
--------------------------------------------------------------------------------
1 | // import wtf from "wtfnode";
2 | import { suite } from "@testdeck/mocha";
3 | import { Config } from "../Config.js";
4 | import { Crypt } from "../Crypt.js";
5 | import { DatabaseConnection } from "../DatabaseConnection.js";
6 | import { Logger } from "../Logger.js";
7 | import { User } from "../commoninterfaces.js";
8 | import { Util } from "../Util.js";
9 | @suite
10 | export class testConfig {
11 | static db: DatabaseConnection;
12 | static testUser: User;
13 | static testUsername: string = "";
14 | static testPassword: string = "";
15 | static userToken: string;
16 | public static async configure() {
17 | if (Config.db != null) return;
18 | this.testUsername = process.env.testusername;
19 | this.testPassword = process.env.testpassword;
20 | if(Util.IsNullEmpty(this.testUsername)) throw new Error("testusername not set in environment");
21 | Config.workitem_queue_monitoring_enabled = false;
22 | Config.disablelogging();
23 | await Logger.configure(true, false);
24 | Config.db = new DatabaseConnection(Config.mongodb_url, Config.mongodb_db);
25 | await Config.db.connect(null);
26 | await Config.Load(null);
27 | try {
28 | testConfig.testUser = await Logger.DBHelper.FindByUsername(testConfig.testUsername, Crypt.rootToken(), null)
29 | testConfig.userToken = Crypt.createSlimToken(testConfig.testUser._id, null, Config.shorttoken_expires_in);
30 | } catch (error) {
31 | console.error("Error finding testuser: " + error);
32 | }
33 | }
34 | public static async cleanup() {
35 | // Config_test.amqp?.shutdown();
36 | // Logger.License.shutdown();
37 | // // if (Config.db != null) await Config.db.shutdown();
38 | // await Logger.otel.shutdown();
39 | // // wtf.dump();
40 | // await Logger.shutdown();
41 | // Config.db = null;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/EntityRestriction.ts:
--------------------------------------------------------------------------------
1 | import { JSONPath } from "jsonpath-plus";
2 | import { DatabaseConnection } from "./DatabaseConnection.js";
3 | import { Logger } from "./Logger.js";
4 | import { Base, Rights, TokenUser, User } from "./commoninterfaces.js";
5 | import { Util } from "./Util.js";
6 |
7 | export class EntityRestriction extends Base {
8 | public collection: string;
9 | public copyperm: boolean;
10 | public paths: string[];
11 | constructor(
12 | ) {
13 | super();
14 | this._type = "restriction";
15 | }
16 | static assign(o: any): EntityRestriction {
17 | if (typeof o === "string" || o instanceof String) {
18 | return Object.assign(new EntityRestriction(), JSON.parse(o.toString()));
19 | }
20 | return Object.assign(new EntityRestriction(), o);
21 | }
22 | public IsMatch(object: object): boolean {
23 | if (Util.IsNullUndefinded(object)) {
24 | return false;
25 | }
26 | for (let path of this.paths) {
27 | if (!Util.IsNullEmpty(path)) {
28 | var json = { a: object };
29 | Logger.instanse.verbose(path, null, { cls: "EntityRestriction", func: "IsMatch" });
30 | Logger.instanse.silly(JSON.stringify(json, null, 2), null);
31 | try {
32 | const result = JSONPath({ path, json });
33 | if (result && result.length > 0) {
34 | Logger.instanse.verbose("true", null, { cls: "EntityRestriction", func: "IsMatch" });
35 | return true;
36 | }
37 | } catch (error) {
38 | }
39 | }
40 | }
41 | Logger.instanse.verbose("false", null, { cls: "EntityRestriction", func: "IsMatch" });
42 | return false;
43 | }
44 | public IsAuthorized(user: TokenUser | User): boolean {
45 | return DatabaseConnection.hasAuthorization(user as TokenUser, this, Rights.create);
46 | }
47 | }
--------------------------------------------------------------------------------
/src/test/Auth.test.ts:
--------------------------------------------------------------------------------
1 | import { SigninMessage } from "@openiap/openflow-api";
2 | import { suite, test, timeout } from "@testdeck/mocha";
3 | import assert from "assert";
4 | import { Auth } from "../Auth.js";
5 | import { Message } from "../Messages/Message.js";
6 | import { Util } from "../Util.js";
7 | import { testConfig } from "./testConfig.js";
8 |
9 | @suite class auth_test {
10 | @timeout(10000)
11 | async before() {
12 | await testConfig.configure();
13 | }
14 | async after() {
15 | await testConfig.cleanup();
16 | }
17 | @test async "ValidateByPassword"() {
18 | await assert.rejects(async () => {
19 | await Auth.ValidateByPassword(testConfig.testUser.username, null, null);
20 | }, "Did not fail on null password")
21 | await assert.rejects(async () => {
22 | await Auth.ValidateByPassword(null, testConfig.testUser.username, null);
23 | }, "Did not fail on null username")
24 | var user1 = await Auth.ValidateByPassword(testConfig.testUser.username, testConfig.testPassword, null);
25 | assert.notStrictEqual(user1, null, "Failed validating valid username and password")
26 | assert.strictEqual(user1.username, testConfig.testUser.username, "returned user has wrong username")
27 | var user2 = await Auth.ValidateByPassword(testConfig.testUser.username, "not-my-password", null);
28 | assert.strictEqual(user2, null, "Did not fail on wrong password")
29 | }
30 | @test async "test full login"() {
31 | var q: any = new SigninMessage();
32 | var msg = new Message();
33 | msg.command = "signin";
34 | q.username = testConfig.testUser.username; q.password = testConfig.testPassword;
35 | msg.data = JSON.stringify(q);
36 | await msg.Signin(null, null);
37 | q = JSON.parse(msg.data);
38 | assert.strictEqual(Util.IsNullEmpty(q.user), false, "Sigin returned no data")
39 | assert.strictEqual(q.user.username, testConfig.testUser.username, "Sigin did not return testuser user object")
40 |
41 | }
42 | }
43 | // clear && ./node_modules/.bin/_mocha "src/test/**/Auth.test.ts"
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # # OpenIAP flow
2 |
3 | OpenIAP flow is a security layer that sits on top of an installation of [MongoDB](https://www.mongodb.com/) and [RabbitMQ](https://www.rabbitmq.com/). Its purpose is to orchestrate agents, such as NodeJS, Python, NodeRED, elsa workflow, dotnet, and [OpenRPA](https://github.com/open-rpa/openrpa) agents.
4 |
5 | The platform is designed to supplement digitalization strategies by providing an easy-to-use, highly scalable, and secure platform capable of supporting human workflows, IT system automation, and both Internet of Things (IoT) and Industry Internet of Things/Industry 4.0 automation.
6 |
7 | If you are installing OpenIAP for the first time, we highly recommend using Docker. You can find the necessary resources and instructions to do so by visiting the OpenIAP Docker Github page: https://github.com/open-rpa/docker.
8 |
9 | Using Docker ensures that you have all the required dependencies and configuration in place for seamless set up and deployment of OpenIAP.
10 |
11 | Creating your first [agent package](first-agent).
12 |
13 | Read more about the [security model here](securitymodel).
14 |
15 | Read more about the [architecture here](architecture).
16 |
17 | Read more about the [protocol](protocol).
18 |
19 | Read more about [size recommendations](requirements).
20 |
21 | #### Quick start using docker
22 | Installing using [docker-compose](https://github.com/open-rpa/docker)
23 |
24 | #### Examples and a few guides
25 |
26 | Working with [versioning](versioning)
27 |
28 | Creating your first [user form](forms_old) using the old form designer
29 |
30 | Using the [mobile app](mobileapp)
31 |
32 | Notes when running without internet or [behind a proxy server](proxy)
33 |
34 | #### How to deployment on kubernetes
35 |
36 | Installing on [kubernetes](kubernetes)
37 |
38 | using our [helm-charts](https://github.com/open-rpa/helm-charts/)
39 |
40 | #### How to build and run from source
41 | build [from source](buildsource)
42 |
43 | #### Getting help from the community
44 | Join rocket chat [#openrpa](https://rocket.openiap.io/)
45 | or check out the [community forum](https://nn.openiap.io/)
46 |
47 | For commercial support and access to premium features, contact [openiap](https://openiap.io/)
48 |
--------------------------------------------------------------------------------
/src/test/Message.test.ts:
--------------------------------------------------------------------------------
1 | import { SelectCustomerMessage } from "@openiap/openflow-api";
2 | import { suite, test, timeout } from "@testdeck/mocha";
3 | import assert from "assert";
4 | import { Crypt } from "../Crypt.js";
5 | import { Message } from "../Messages/Message.js";
6 | import { testConfig } from "./testConfig.js";
7 |
8 | @suite class message_test {
9 | @timeout(10000)
10 | async before() {
11 | await testConfig.configure();
12 | }
13 | async after() {
14 | await testConfig.cleanup();
15 | }
16 | @test async "Unselect customer as root"() {
17 | var q = new SelectCustomerMessage();
18 | var msg = new Message(); msg.jwt = Crypt.rootToken();
19 | await msg.EnsureJWT(null, false)
20 | assert.rejects(msg.SelectCustomer(null), "Builtin entities cannot select a company")
21 | }
22 | @test async "select customer as root"() {
23 | var q = new SelectCustomerMessage(); q.customerid = "60b683e12382b05d20762f09";
24 | var msg = new Message(); msg.jwt = Crypt.rootToken();
25 | await msg.EnsureJWT(null, false)
26 | assert.rejects(msg.SelectCustomer(null), "Builtin entities cannot select a company")
27 | }
28 | @test async "Unselect customer as testuser"() {
29 | var q = new SelectCustomerMessage();
30 | var msg = new Message(); msg.jwt = testConfig.userToken;
31 | await msg.EnsureJWT(null, false)
32 | await msg.SelectCustomer(null);
33 | }
34 | @test async "select customer as testuser"() {
35 | var q = new SelectCustomerMessage(); q.customerid = "60b683e12382b05d20762f09";
36 | var msg = new Message(); msg.jwt = testConfig.userToken;
37 | await msg.EnsureJWT(null, false)
38 | await msg.SelectCustomer(null);
39 | }
40 | // @test async "signin with username and password"() {
41 | // var q = new SigninMessage(); q.username = testConfig.testUser.username; q.password = testConfig.testUserPassword
42 | // var msg = new Message();
43 | // await msg.Signin(null, null);
44 | // q = JSON.parse(msg.data);
45 | // assert.ok(q && !q.error, q.error);
46 | // }
47 | }
48 | // clear && ./node_modules/.bin/_mocha "src/test/**/Message.test.ts"
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [master]
6 | pull_request:
7 | # The branches below must be a subset of the branches above
8 | branches: [master]
9 | schedule:
10 | - cron: '0 14 * * 5'
11 |
12 | jobs:
13 | analyze:
14 | name: Analyze
15 | runs-on: ubuntu-latest
16 |
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | # Override automatic language detection by changing the below list
21 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
22 | language: ['javascript']
23 | # Learn more...
24 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
25 |
26 | steps:
27 | - name: Checkout repository
28 | uses: actions/checkout@v2
29 | with:
30 | # We must fetch at least the immediate parents so that if this is
31 | # a pull request then we can checkout the head.
32 | fetch-depth: 2
33 |
34 | # If this run was triggered by a pull request event, then checkout
35 | # the head of the pull request instead of the merge commit.
36 | - run: git checkout HEAD^2
37 | if: ${{ github.event_name == 'pull_request' }}
38 |
39 | # Initializes the CodeQL tools for scanning.
40 | - name: Initialize CodeQL
41 | uses: github/codeql-action/init@v1
42 | with:
43 | languages: ${{ matrix.language }}
44 |
45 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
46 | # If this step fails, then you should remove it and run the build manually (see below)
47 | - name: Autobuild
48 | uses: github/codeql-action/autobuild@v1
49 |
50 | # ℹ️ Command-line programs to run using the OS shell.
51 | # 📚 https://git.io/JvXDl
52 |
53 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
54 | # and modify them (or add more) to build your code if your project
55 | # uses a compiled language
56 |
57 | #- run: |
58 | # make bootstrap
59 | # make release
60 |
61 | - name: Perform CodeQL Analysis
62 | uses: github/codeql-action/analyze@v1
63 |
--------------------------------------------------------------------------------
/src/SocketMessage.ts:
--------------------------------------------------------------------------------
1 | import { Message } from "./Messages/Message.js";
2 | import { Util } from "./Util.js";
3 |
4 | function isNumber(value: string | number): boolean {
5 | return ((value != null) && !isNaN(Number(value.toString())));
6 | }
7 | export class SocketMessage {
8 | public id: string;
9 | public replyto: string;
10 | public command: string;
11 | public data: string;
12 | public count: number;
13 | public index: number;
14 | public priority: number = 1;
15 | public clientagent: string;
16 | public clientversion: string;
17 | public static fromjson(json: string): SocketMessage {
18 | let result: SocketMessage = new SocketMessage();
19 | let obj: any = JSON.parse(json);
20 | result.command = obj.command;
21 | result.id = obj.id;
22 | result.replyto = obj.replyto;
23 | result.count = 1;
24 | result.index = 0;
25 | result.clientagent = obj.clientagent;
26 | result.clientversion = obj.clientversion;
27 | if (!Util.IsNullEmpty(obj.priority)) result.priority = obj.priority;
28 | result.data = obj.data;
29 | if (isNumber(obj.count)) { result.count = obj.count; }
30 | if (isNumber(obj.index)) { result.index = obj.index; }
31 | if (result.id === null || result.id === undefined || result.id === "") {
32 | result.id = Util.GetUniqueIdentifier();
33 | }
34 | return result;
35 | }
36 | public static fromcommand(command: string): SocketMessage {
37 | const result: SocketMessage = new SocketMessage();
38 | result.command = command;
39 | result.count = 1;
40 | result.index = 0;
41 | result.id = Util.GetUniqueIdentifier();
42 | return result;
43 | }
44 | public static frommessage(msg: Message, data: string, count: number, index: number): SocketMessage {
45 | const result: SocketMessage = new SocketMessage();
46 | result.id = msg.id;
47 | result.replyto = msg.replyto;
48 | result.command = msg.command;
49 | result.count = count;
50 | result.index = index;
51 | result.data = data;
52 | return result;
53 | }
54 | public tojson(): string {
55 | return JSON.stringify(this);
56 | }
57 | }
--------------------------------------------------------------------------------
/src/ee/LICENSE:
--------------------------------------------------------------------------------
1 | The OpenIAP Enterprise Edition (EE) license (the "EE License")
2 | Copyright (c) 2015-2024 OpenIAP ApS
3 |
4 | With regard to the OpenIAP flow Software:
5 |
6 | This software and associated documentation files (the "Software") may only be
7 | used in production, if you (and any entity that you represent) have agreed to,
8 | and are in compliance with, the OpenIAP Subscription Terms of Service,
9 | available at https://openiap.io/terms (the "EE Terms"), or other agreement
10 | governing the use of the Software, as agreed by you and OpenIAP, and otherwise
11 | have a valid OpenIAP subscription for the correct number of instances.
12 | Subject to the foregoing sentence, you are free to modify this Software and publish
13 | patches to the Software. You agree that OpenIAP and/or its licensors (as applicable)
14 | retain all right, title and interest in and to all such modifications and/or patches,
15 | and all such modifications and/or patches may only be used, copied, modified, displayed,
16 | distributed, or otherwise exploited with a valid OpenIAP subscription for the correct number
17 | of instances. Notwithstanding the foregoing, you may copy and modify the Software
18 | for development and testing purposes, without requiring a Subscription. You agree
19 | that OpenIAP and/or its licensors (as applicable) retain all right, title and
20 | interest in and to all such modifications. You are not granted any other rights
21 | beyond what is expressly stated herein. Subject to the foregoing, it is forbidden
22 | to copy, merge, publish, distribute, sublicense, and/or sell the Software.
23 |
24 | This EE License applies only to the part of this Software that is located in and under the ee folder.
25 | All other parts is geerally copyrighted under the MPL-2.0 license license unless the source file specifies otherwize
26 |
27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
33 | SOFTWARE.
34 |
35 | For all third-party components incorporated into the OpenIAP Software,
36 | those components are licensed under the original license provided by the owner
37 | of the applicable component.
--------------------------------------------------------------------------------
/src/Util.ts:
--------------------------------------------------------------------------------
1 | import crypto from "crypto";
2 | export class Wellknown {
3 | static root = {_id: "59f1f6e6f0a22200126638d8", name: "root"};
4 | static guest = {_id: "65cb30c40ff51e174095573c", name: "guest"};
5 | static admins = {_id: "5a1702fa245d9013697656fb", name: "admins"};
6 | static users = {_id: "5a17f157c4815318c8536c21", name: "users"};
7 | static robots = {_id: "5ac0850ca538fee1ebdb996c", name: "robots"};
8 | static customer_admins = {_id: "5a1702fa245d9013697656fc", name: "customer admins"};
9 | static workspace_admins = {_id: "5a1702fa245d9013697656fe", name: "workspace admins"};
10 | static resellers = {_id: "5a1702fa245d9013697656fd", name: "resellers"};
11 | static nodered_users = {_id: "5a23f18a2e8987292ddbe061", name: "nodered users"};
12 | static nodered_admins = {_id: "5a17f157c4815318c8536c20", name: "nodered admins"};
13 | static nodered_api_users = {_id: "5a17f157c4815318c8536c22", name: "nodered api users"};
14 | static filestore_users = {_id: "5b6ab63c8d4a64b7c47f4a8f", name: "filestore users"};
15 | static filestore_admins = {_id: "5b6ab63c8d4a64b7c47f4a8e", name: "filestore admins"};
16 | static robot_users = {_id: "5aef0142f3683977b0aa3dd3", name: "robot users"};
17 | static robot_admins = {_id: "5aef0142f3683977b0aa3dd2", name: "robot admins"};
18 | static personal_nodered_users = {_id: "5a23f18a2e8987292ddbe062", name: "personal nodered users"};
19 | static robot_agent_users = {_id: "5f33c29d8fe78504bd259a04", name: "robot agent users"};
20 | static workitem_queue_admins = {_id: "625440c4231309af5f2052cd", name: "workitem queue admins"};
21 | static workitem_queue_users = {_id: "62544134231309e2cd2052ce", name: "workitem queue users"};
22 | }
23 | export class Util {
24 | public static Delay = ms => new Promise(res => setTimeout(res, ms));
25 | public static IsNullUndefinded(obj: any) {
26 | if (obj === null || obj === undefined) {
27 | return true;
28 | }
29 | return false;
30 | }
31 | public static IsNullEmpty(obj: any) {
32 | if (obj === null || obj === undefined || obj === "") {
33 | return true;
34 | }
35 | return false;
36 | }
37 | public static IsString(obj: any) {
38 | if (typeof obj === "string" || obj instanceof String) {
39 | return true;
40 | }
41 | return false;
42 | }
43 | public static isObject(obj: any): boolean {
44 | return obj === Object(obj);
45 | }
46 | public static GetUniqueIdentifier(length: number = 16): string {
47 | return crypto.randomBytes(16).toString("hex")
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/src/test/KubeUtil.test.ts:
--------------------------------------------------------------------------------
1 | import { suite, test, timeout } from "@testdeck/mocha";
2 | import assert from "assert";
3 | import { Config } from "../Config.js";
4 | // @ts-ignore
5 | import { KubeUtil } from "../ee/KubeUtil.js";
6 | import { testConfig } from "./testConfig.js";
7 |
8 | @suite class kubeutil_test {
9 | @timeout(10000)
10 | async before() {
11 | await testConfig.configure();
12 | }
13 | async after() {
14 | await testConfig.cleanup();
15 | }
16 | @timeout(60000)
17 | @test async "GetStatefulSet"() {
18 | var sfs = await KubeUtil.instance().GetStatefulSet(Config.namespace, "findme");
19 | assert.strictEqual(sfs, null)
20 | }
21 | @test async "GetDeployment"() {
22 | var dep = await KubeUtil.instance().GetDeployment(Config.namespace, "api");
23 | assert.notStrictEqual(dep, null);
24 | assert.strictEqual(dep.metadata.name, "api");
25 | }
26 | @test async "GetIngressV1"() {
27 | var dep = await KubeUtil.instance().GetIngressV1(Config.namespace, "useringress");
28 | assert.notStrictEqual(dep, null);
29 | assert.strictEqual(dep.metadata.name, "useringress");
30 | }
31 | @timeout(60000)
32 | @test async "listNamespacedPod"() {
33 | const list = await KubeUtil.instance().CoreV1Api.listNamespacedPod(Config.namespace);
34 | assert.notStrictEqual(list, null);
35 | assert.notStrictEqual(list.body, null);
36 | assert.notStrictEqual(list.body.items, null);
37 | assert.ok(list.body.items.length > 0)
38 | var pod = list.body.items[0];
39 | var name = pod.metadata.name;
40 | pod = await KubeUtil.instance().GetPod(Config.namespace, pod.metadata.name);
41 | assert.notStrictEqual(pod, null);
42 | assert.strictEqual(pod.metadata.name, name);
43 |
44 | var metrics = await KubeUtil.instance().GetPodMetrics(Config.namespace, name);
45 | assert.notStrictEqual(metrics, null);
46 | assert.notStrictEqual(metrics.cpu, null);
47 | assert.notStrictEqual(metrics.memory, null);
48 | }
49 | @test async "GetService"() {
50 | var service = await KubeUtil.instance().GetService(Config.namespace, "api");
51 | assert.notStrictEqual(service, null);
52 | assert.strictEqual(service.metadata.name, "api");
53 |
54 | }
55 | @test async "GetReplicaset"() {
56 | var rep = await KubeUtil.instance().GetReplicaset(Config.namespace, "name", "test");
57 | assert.strictEqual(rep, null);
58 | }
59 | @test async "getpods"() {
60 | var list = await KubeUtil.instance().GetPods(Config.namespace);
61 | assert.notStrictEqual(list, null);
62 | assert.notStrictEqual(list.body, null);
63 | assert.notStrictEqual(list.body.items, null);
64 | assert.ok(list.body.items.length > 0)
65 | }
66 | }
67 | // clear && ./node_modules/.bin/_mocha "src/test/**/KubeUtil.test.ts"
--------------------------------------------------------------------------------
/src/ee/FaaS.ts:
--------------------------------------------------------------------------------
1 | import { ResourceUsage, User } from '../commoninterfaces.js';
2 | import { Config } from '../Config.js';
3 | let KubeUtil: any = null;
4 |
5 | async function init() {
6 | try {
7 | // @ts-ignore
8 | let _driver: any = await import("./KubeUtil.js");
9 | KubeUtil = _driver.KubeUtil.instance();
10 | } catch (error) {
11 | console.error(error.message);
12 | }
13 | }
14 | init();
15 | export class FaaS {
16 | public static async GetImage(tuser: User, jwt: string, pack: any) {
17 | let image = await KubeUtil.GetImage(Config.namespace, pack.name);
18 | return image;
19 | }
20 | public static async BuildImage(tuser: User, jwt: string, pack: any) {
21 | let image = await KubeUtil.GetImage(Config.namespace, pack.name);
22 | if(image != null) {
23 | await KubeUtil.DeleteImage(Config.namespace, pack.name);
24 | }
25 | let url = Config.baseurl() + "download/" + pack.fileid + "?jwt=" + jwt;
26 | image = await KubeUtil.CreateImage(Config.namespace, Config.namespace, "demo-builder", pack.name, url);
27 | let start = new Date();
28 |
29 | while (new Date().getTime() - start.getTime() < 5 * 60 * 1000) {
30 | image = await KubeUtil.GetImage(Config.namespace, pack.name);
31 | if (!image?.status?.conditions) {
32 | await new Promise(resolve => setTimeout(resolve, 1000));
33 | continue;
34 | }
35 |
36 | const ready = image.status.conditions.find(c => c.type === "Ready");
37 | const failed = image.status.conditions.find(c => c.type === "Failed");
38 |
39 | if (ready?.message && Date.now() % 10000 < 1000) {
40 | console.log(`[build status] ${ready.status}: ${ready.message}`);
41 | }
42 |
43 | if (failed?.status === "True") {
44 | throw new Error("Image build failed: " + (failed.message || "Unknown failure"));
45 | }
46 |
47 | if (ready?.status === "True") {
48 | const timetaken = (new Date().getTime() - start.getTime()) / 1000;
49 | console.log("Image build complete in " + timetaken + " seconds");
50 | return {image, timetaken};
51 | }
52 |
53 | if (ready?.status === "False") {
54 | throw new Error("Image build failed: " + (ready.message || "Build marked as not ready"));
55 | }
56 |
57 | // Still building (status == "Unknown" or not set yet)
58 | await new Promise(resolve => setTimeout(resolve, 1000));
59 | }
60 |
61 | throw new Error("Timed out waiting for image to be built");
62 | }
63 | public static async DeleteImage(tuser: User, jwt: string, pack: any) {
64 | let image = await KubeUtil.GetImage(Config.namespace, pack.name);
65 | if(image != null) {
66 | await KubeUtil.DeleteImage(Config.namespace, pack.name);
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/test/Crypt.test.ts:
--------------------------------------------------------------------------------
1 | import { suite, test, timeout } from "@testdeck/mocha";
2 | import assert from "assert";
3 | import { Crypt } from "../Crypt.js";
4 | import { OAuthProvider } from "../OAuthProvider.js";
5 | import { testConfig } from "./testConfig.js";
6 | @suite class crypt_test {
7 | @timeout(10000)
8 | async before() {
9 | await testConfig.configure();
10 | }
11 | async after() {
12 | await testConfig.cleanup();
13 | }
14 | @test async "TestGenerateKeys"() {
15 | let jwks = await OAuthProvider.generatekeys();
16 | assert.ok(jwks, "Failed generating keys");
17 | }
18 | @timeout(10000)
19 | @test async "ValidatePassword"() {
20 | await Crypt.SetPassword(testConfig.testUser, "randompassword", null);
21 | var result = await Crypt.ValidatePassword(testConfig.testUser, "randompassword", null);
22 | assert.ok(result, "Failed validating with the correct password");
23 | result = await Crypt.ValidatePassword(testConfig.testUser, "not-my-randompassword", null);
24 | assert.ok(!result, "ValidatePassword did not fail with wrong password");
25 | var hash = await Crypt.hash("secondrandompassword");
26 | result = await Crypt.compare("secondrandompassword", hash, null)
27 | assert.ok(result, "Failed validating with the correct password");
28 | result = await Crypt.compare("not-my-randompassword", hash, null);
29 | assert.ok(!result, "compare did not fail with wrong password");
30 |
31 | await assert.rejects(Crypt.SetPassword(null, "randompassword", null));
32 | await assert.rejects(Crypt.SetPassword(testConfig.testUser, null, null));
33 | await assert.rejects(Crypt.SetPassword(null, null, null));
34 | await assert.rejects(Crypt.ValidatePassword(null, "randompassword", null));
35 | await assert.rejects(Crypt.ValidatePassword(testConfig.testUser, null, null));
36 | await assert.rejects(Crypt.ValidatePassword(null, null, null));
37 | await assert.rejects(Crypt.compare(null, null, null));
38 |
39 | }
40 | @test async "encrypt"() {
41 | const basestring = "Hi mom, i miss you.";
42 | var encryptedstring = Crypt.encrypt(basestring);
43 | var decryptedstring = Crypt.decrypt(encryptedstring);
44 | assert.ok(decryptedstring == basestring, "Failed encrypting and decrypting string");
45 | assert.throws(() => { Crypt.decrypt("Bogusstring") }, Error, "Decrypt did not fail with an illegal string");
46 | }
47 | @test async "decrypt"() {
48 | const gcm = "8a23d6b7b2282b09a32994faf724f05e:8c2551427845c60b4e394302057bc4e4";
49 | const cbc = "4beca50248100a14d06c8d284258eda7:aee11025ef03216d0068:4b23f4875b8bda4be5b1a0b3a4b4cd3c";
50 | var gcmdecrypted = Crypt.decrypt(gcm);
51 | assert.ok(gcmdecrypted == "teststring", "Failed decrypting string using gcm encryption");
52 | var cbcdecrypted = Crypt.decrypt(cbc);
53 | assert.ok(cbcdecrypted == "teststring", "Failed decrypting string using gcm encryption");
54 | }
55 | }
56 | // clear && ./node_modules/.bin/_mocha "src/test/**/Crypt.test.ts"
--------------------------------------------------------------------------------
/src/test/Logger.test.ts:
--------------------------------------------------------------------------------
1 | import { suite, test, timeout } from "@testdeck/mocha";
2 | import assert from "assert";
3 | import { i_license_data } from "../commoninterfaces.js";
4 | import { Config } from "../Config.js";
5 | import { Logger } from "../Logger.js";
6 | import { Util } from "../Util.js";
7 | import { testConfig } from "./testConfig.js";
8 |
9 | @suite class logger_test {
10 | @timeout(10000)
11 | async before() {
12 | await testConfig.configure();
13 | }
14 | async after() {
15 | await testConfig.cleanup();
16 | }
17 | @test async "test info"() {
18 | // assert.ok(!NoderedUtil.IsNullUndefinded(Logger.myFormat), "Logger missing winston error formatter");
19 | var ofid = Logger.ofid();
20 | assert.strictEqual(Util.IsNullEmpty(ofid), false);
21 | }
22 | @test async "v1_lic"() {
23 | const months: number = 1;
24 | const data: i_license_data = {} as any;
25 | let template = Logger.License.template_v1;
26 | data.licenseVersion = 1;
27 | data.email = "test@user.com";
28 | var dt = new Date(new Date().toISOString());
29 | dt.setMonth(dt.getMonth() + months);
30 | data.expirationDate = dt.toISOString() as any;
31 | const licenseFileContent = Logger.License.generate({
32 | privateKeyPath: "config/private_key.pem",
33 | template,
34 | data: data
35 | });
36 | Config.license_key = Buffer.from(licenseFileContent).toString("base64");
37 | Logger.License.validate();
38 | assert.strictEqual(Logger.License.validlicense, true);
39 | assert.strictEqual(Logger.License.data.email, "test@user.com");
40 |
41 | }
42 | @test async "v2_lic"() {
43 | const months: number = 1;
44 | const data: i_license_data = {} as any;
45 | let template = Logger.License.template_v2;
46 | let ofid = Logger.License.ofid(false);
47 | assert.ok(!Util.IsNullEmpty(ofid));
48 | data.licenseVersion = 2;
49 | data.email = "test@user.com";
50 | data.domain = "localhost.openiap.io"
51 | Config.domain = "localhost.openiap.io";
52 | var dt = new Date(new Date().toISOString());
53 | dt.setMonth(dt.getMonth() + months);
54 | data.expirationDate = dt.toISOString() as any;
55 | const licenseFileContent = Logger.License.generate({
56 | privateKeyPath: "config/private_key.pem",
57 | template,
58 | data: data
59 | });
60 | var lic = Logger.License;
61 | Config.license_key = Buffer.from(licenseFileContent).toString("base64");
62 | Logger.License.validate();
63 | assert.strictEqual(Logger.License.validlicense, true);
64 | assert.strictEqual(Logger.License.data.email, "test@user.com");
65 | assert.strictEqual(Logger.License.data.domain, "localhost.openiap.io");
66 |
67 | Config.domain = "notlocalhost.openiap.io";
68 | Logger.License.validate(); // will not error anymore, will just set validlicense to false // assert.throws(lic.validate.bind(lic), Error);
69 | assert.strictEqual(Logger.License.validlicense, false);
70 | assert.strictEqual(Logger.License.data.domain, "localhost.openiap.io");
71 | let ofid2 = Logger.License.ofid(true);
72 | assert.ok(!Util.IsNullEmpty(ofid2));
73 | assert.notStrictEqual(ofid, ofid2);
74 | }
75 | }
76 | // clear && ./node_modules/.bin/_mocha "src/test/**/Logger.test.ts"
--------------------------------------------------------------------------------
/src/test/Config.test.ts:
--------------------------------------------------------------------------------
1 | import { suite, test, timeout } from "@testdeck/mocha";
2 | import assert from "assert";
3 | import { Config } from "../Config.js";
4 | import { Util } from "../Util.js";
5 | import { testConfig } from "./testConfig.js";
6 | @suite
7 | export class Config_test {
8 | @timeout(10000)
9 | async before() {
10 | await testConfig.configure();
11 | }
12 | async after() {
13 | await testConfig.cleanup();
14 | }
15 | @test "baseurl"() {
16 | assert.strictEqual(Util.IsNullEmpty(Config.domain), false, "domain missing from baseurl");
17 | var url = Config.baseurl();
18 | assert.notStrictEqual(url.indexOf(Config.domain), -1, "domain missing from baseurl");
19 | assert.notStrictEqual(url.startsWith("https://"), false, "baseurl is not using https");
20 | var wsurl = Config.basewsurl();
21 | assert.notStrictEqual(wsurl.indexOf(Config.domain), -1, "basewsurl missing from baseurl");
22 | assert.notStrictEqual(wsurl.startsWith("wss://"), false, "basewsurl is not using https");
23 |
24 | Config.tls_crt = "";
25 | Config.tls_key = "";
26 |
27 | var wsurl = Config.basewsurl();
28 | assert.notStrictEqual(wsurl.startsWith("wss://"), false, "basewsurl is not using https");
29 |
30 | Config.protocol = "http";
31 | var url = Config.baseurl();
32 | assert.notStrictEqual(url.indexOf(Config.domain), -1, "domain missing from baseurl");
33 | assert.notStrictEqual(url.startsWith("http://"), false, "baseurl is not using http");
34 | var wsurl = Config.basewsurl();
35 | assert.notStrictEqual(wsurl.indexOf(Config.domain), -1, "domain missing from basewsurl");
36 | assert.notStrictEqual(wsurl.startsWith("ws://"), false, "basewsurl is not using http");
37 |
38 |
39 | let port = Config.port;
40 | Config.port = 12345;
41 | var url = Config.baseurl();
42 | assert.notStrictEqual(url.indexOf(":12345"), -1, "port missing from baseurl");
43 | var wsurl = Config.basewsurl();
44 | assert.notStrictEqual(wsurl.indexOf(":12345"), -1, "port missing from basewsurl");
45 | Config.port = port;
46 | }
47 | @test "parseBoolean"() {
48 | assert.strictEqual(Config.parseBoolean(true), true)
49 | assert.strictEqual(Config.parseBoolean(false), false)
50 | assert.strictEqual(Config.parseBoolean("true"), true)
51 | assert.strictEqual(Config.parseBoolean("false"), false)
52 | assert.strictEqual(Config.parseBoolean("hullu-bullu"), true)
53 | assert.strictEqual(Config.parseBoolean(1), true)
54 | assert.strictEqual(Config.parseBoolean(0), false)
55 | assert.throws(() => { Config.parseBoolean({}) }, Error, "parseBoolean did not fail on illegal arguement");
56 | }
57 | @test async "parse_federation_metadata"() {
58 | // var metadata = await Config.parse_federation_metadata(null, "https://login.microsoftonline.com/common/FederationMetadata/2007-06/FederationMetadata.xml");
59 | var metadata = await Config.parse_federation_metadata(null, "http://localhost:" + Config.port + "/issue/FederationMetadata/2007-06/FederationMetadata.xml");
60 |
61 | assert.ok(!Util.IsNullEmpty(metadata.identityProviderUrl))
62 | assert.ok(!Util.IsNullEmpty(metadata.entryPoint))
63 | assert.ok(!Util.IsNullEmpty(metadata.logoutUrl))
64 | assert.ok(Array.isArray(metadata.cert));
65 | assert.ok(metadata.cert.length > 0);
66 | }
67 | }
68 | // clear && ./node_modules/.bin/_mocha "src/test/**/Config.test.ts"
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@openiap/core",
3 | "version": "1.5.11.124",
4 | "description": "Easy orchestration of data, code and automation tools.\r Also the \"backend\" for [OpenRPA](https://github.com/skadefro/OpenRPA)",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "_mocha 'src/test/**/*.test.ts'",
8 | "build": "tsc --build tsconfig.json",
9 | "coverage": "nyc ./node_modules/.bin/_mocha 'test/**/*.test.ts'",
10 | "compose": "gulp compose",
11 | "latest": "gulp latest",
12 | "updateapilocal": "npm uninstall @openiap/nodeapi && npm i ../nodeapi",
13 | "updateapidev": "npm uninstall @openiap/nodeapi && npm i openiap/nodeapi",
14 | "updateapi": "npm uninstall @openiap/nodeapi && npm i @openiap/nodeapi",
15 | "updatejsapi": "npm uninstall @openiap/jsapi && npm i openiap/jsapi#esm",
16 | "ncu": "npx -y npm-check-updates",
17 | "spec": "tsoa spec",
18 | "routes": "tsoa routes",
19 | "generate": "tsoa spec && tsoa routes",
20 | "copypublic": "rm -rf public && cp -r ../openflow-web/dist/ public",
21 | "linkpublic": "rm -rf public && ln -s /mnt/data/vscode/config/workspace/code/openflow-web/dist /mnt/data/vscode/config/workspace/code/openflow/public",
22 | "copypublic2": "rm -rf public && cp -r ../core-web/build/ public",
23 | "linkpublic2": "rm -rf public && ln -s /mnt/data/vscode/config/workspace/code/core-web/build /mnt/data/vscode/config/workspace/code/openflow/public",
24 | "inspector": "npx @modelcontextprotocol/inspector "
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "git+https://github.com/open-rpa/openflow.git"
29 | },
30 | "publishConfig": {
31 | "access": "public"
32 | },
33 | "author": "OpenIAP ApS / Allan Zimmermann",
34 | "license": "MPL-2.0",
35 | "bugs": {
36 | "url": "https://community.openiap.io/"
37 | },
38 | "type": "module",
39 | "homepage": "https://github.com/open-rpa/openflow",
40 | "funding": "https://github.com/sponsors/skadefro",
41 | "dependencies": {
42 | "@kubernetes/client-node": "0.21.0",
43 | "@modelcontextprotocol/sdk": "^1.9.0",
44 | "@node-saml/passport-saml": "4.0.1",
45 | "@openiap/cloud-git-mongodb": "1.0.39",
46 | "@openiap/jsapi": "github:openiap/jsapi#esm",
47 | "@openiap/nodeapi": "0.0.41",
48 | "@openiap/openflow-api": "2.1.12",
49 | "@opentelemetry/api-logs": "0.57.1",
50 | "@opentelemetry/exporter-logs-otlp-grpc": "0.57.1",
51 | "@opentelemetry/exporter-metrics-otlp-grpc": "0.57.1",
52 | "@opentelemetry/exporter-metrics-otlp-http": "^0.57.1",
53 | "@opentelemetry/exporter-trace-otlp-http": "^0.57.1",
54 | "@opentelemetry/sdk-node": "0.57.1",
55 | "amqplib": "0.10.4",
56 | "bcryptjs": "2.4.3",
57 | "cache-manager": "5.2.3",
58 | "cache-manager-ioredis-yet": "1.2.2",
59 | "compression": "1.7.4",
60 | "cookie-parser": "1.4.6",
61 | "cookie-session": "2.1.0",
62 | "dockerode": "3.3.4",
63 | "dotenv": "16.4.5",
64 | "express": "4.19.2",
65 | "global-agent": "^3.0.0",
66 | "got": "11.8.5",
67 | "hjson": "3.2.2",
68 | "ip": "2.0.1",
69 | "jose": "2.0.6",
70 | "json-stable-stringify": "1.0.2",
71 | "jsondiffpatch": "0.4.1",
72 | "jsonpath-plus": "7.2.0",
73 | "jsonwebtoken": "8.5.1",
74 | "mimetype": "0.0.8",
75 | "mongodb": "4.11.0",
76 | "multer": "1.4.2",
77 | "multer-gridfs-storage": "5.0.2",
78 | "nodemailer": "6.9.14",
79 | "oidc-provider": "6.31.0",
80 | "openid-client": "5.3.0",
81 | "pako": "2.1.0",
82 | "passport": "0.5.3",
83 | "passport-google-oauth20": "2.0.0",
84 | "passport-local": "1.0.0",
85 | "passport-openidconnect": "0.1.1",
86 | "pidusage": "3.0.2",
87 | "rate-limiter-flexible": "2.4.1",
88 | "request": "2.88.2",
89 | "saml20": "0.1.14",
90 | "samlp": "6.0.2",
91 | "showdown": "2.1.0",
92 | "swagger-ui-express": "4.6.3",
93 | "systeminformation": "5.22.11",
94 | "tsoa": "5.1.1",
95 | "tsx": "4.16.0",
96 | "web-push": "3.6.7",
97 | "ws": "8.17.1",
98 | "xml2js": "0.4.23"
99 | },
100 | "devDependencies": {
101 | "@testdeck/mocha": "0.3.3",
102 | "@types/amqplib": "0.10.5",
103 | "@types/mocha": "10.0.7",
104 | "@types/node": "20.14.9",
105 | "@types/passport": "1.0.16",
106 | "gulp": "4.0.2",
107 | "gulp-shell": "0.8.0",
108 | "mocha": "10.5.2",
109 | "nyc": "17.0.0",
110 | "typescript": "4.8.4",
111 | "wtfnode": "0.10.0"
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/test/amqp.test.ts:
--------------------------------------------------------------------------------
1 | import { suite, test, timeout } from "@testdeck/mocha";
2 | import assert from "assert";
3 | import { Crypt } from "../Crypt.js";
4 | import { Util } from "../Util.js";
5 | import { testConfig } from "./testConfig.js";
6 | import { amqpwrapper } from "../amqpwrapper.js";
7 | import { Config } from "../Config.js";
8 | @suite class amqp_test {
9 | amqp: amqpwrapper;
10 | @timeout(10000)
11 | async before() {
12 | await testConfig.configure();
13 | this.amqp = new amqpwrapper(Config.amqp_url);
14 | amqpwrapper.SetInstance(this.amqp);
15 | Config.log_amqp = false;
16 | await this.amqp.connect(null);
17 | }
18 | @timeout(5000)
19 | async after() {
20 | this.amqp?.shutdown();
21 | await testConfig.cleanup();
22 | }
23 | @timeout(5000)
24 | @test async "queuetest"() {
25 | const queuename = "demotestqueue";
26 | var q = await this.amqp.AddQueueConsumer(testConfig.testUser, queuename, null, Crypt.rootToken(), async (msg, options, ack) => {
27 | if (!Util.IsNullEmpty(options.replyTo)) {
28 | if (msg == "hi mom, i miss you") {
29 | msg = "hi";
30 | } else {
31 | msg = "unknown message";
32 | }
33 | await this.amqp.send(options.exchangename, options.replyTo, msg, 1500, options.correlationId, options.routingKey, null, 1);
34 | }
35 | ack();
36 | }, null);
37 | assert.ok(!Util.IsNullUndefinded(q));
38 | assert.ok(!Util.IsNullEmpty(q.queuename));
39 |
40 | reply = await this.amqp.sendWithReply(null, queuename, "hi mom, i miss you", 300, null, null, null);
41 | assert.strictEqual(reply, "hi");
42 |
43 |
44 | var reply = await this.amqp.sendWithReply(null, queuename, "hi mom, i miss you", 300, null, null, null);
45 | assert.strictEqual(reply, "hi");
46 | await this.amqp.RemoveQueueConsumer(testConfig.testUser, q, null);
47 | reply = await this.amqp.sendWithReply("", "bogusName", "hi mom, i miss you", 300, null, null, null);
48 | assert.strictEqual(reply, "timeout");
49 |
50 | // why does this die ? after sending to bogusName
51 | // reply = await this.amqp.sendWithReply(null, queuename, "hi mom, i miss you", 300, null, null);
52 | // assert.strictEqual(reply, "timeout");
53 | }
54 | @timeout(5000)
55 | @test
56 | async "personalqueuetest"() {
57 | var q = await this.amqp.AddQueueConsumer(testConfig.testUser, testConfig.testUser._id, null, Crypt.rootToken(), async (msg, options, ack) => {
58 | if (!Util.IsNullEmpty(options.replyTo)) {
59 | if (msg.indexOf("hi mom, i miss you") > -1) {
60 | msg = JSON.stringify({ "test": "hi" });
61 | } else {
62 | msg = JSON.stringify({ "test": "unknown message" });
63 | }
64 | await this.amqp.send(options.exchangename, options.replyTo, msg, 1500, options.correlationId, options.routingKey, null, 1);
65 | }
66 | ack();
67 | }, null);
68 | assert.ok(!Util.IsNullUndefinded(q));
69 | assert.ok(!Util.IsNullEmpty(q.queuename));
70 | var reply = await this.amqp.sendWithReply(null, testConfig.testUser._id, { "test": "hi mom, i miss you" }, 300, null, null, null);
71 | assert.notStrictEqual(reply.indexOf("hi"), -1);
72 | await this.amqp.RemoveQueueConsumer(testConfig.testUser, q, null);
73 | reply = await this.amqp.sendWithReply("", "bogusName", { "test": "hi mom, i miss you" }, 300, null, null, null);
74 | assert.notStrictEqual(reply.indexOf("timeout"), -1);
75 |
76 | // why does this die ? after sending to bogusName
77 | // reply = await this.amqp.sendWithReply(null, testConfig.testUser._id, "hi mom, i miss you", 300, null, null);
78 | // assert.strictEqual(reply, "timeout");
79 | }
80 | @timeout(5000)
81 | @test
82 | async "exchangetest"() {
83 | const exchangename = "demotestexchange";
84 | var q = await this.amqp.AddExchangeConsumer(testConfig.testUser, exchangename, "direct", "", null, Crypt.rootToken(), true, async (msg, options, ack) => {
85 | if (!Util.IsNullEmpty(options.replyTo)) {
86 | if (msg.indexOf("hi mom, i miss you") > -1) {
87 | msg = JSON.stringify({ "test": "hi" });
88 | } else {
89 | msg = JSON.stringify({ "test": "unknown message" });
90 | }
91 | await this.amqp.send("", options.replyTo, msg, 1500, options.correlationId, "", null, 1);
92 | }
93 | ack();
94 | }, null);
95 | // Give rabbitmq a little room
96 | await new Promise(resolve => { setTimeout(resolve, 1000) })
97 | var reply = await this.amqp.sendWithReply(exchangename, "", { "test": "hi mom, i miss you" }, 300, null, null, null);
98 | assert.notStrictEqual(reply.indexOf("hi"), -1);
99 | var reply = await this.amqp.sendWithReply(exchangename, "", { "test": "hi dad, i miss you" }, 300, null, null, null);
100 | assert.notStrictEqual(reply.indexOf("unknown message"), -1);
101 | var reply = await this.amqp.sendWithReply(exchangename, "", { "test": "hi mom, i miss you" }, 300, null, null, null);
102 | assert.notStrictEqual(reply.indexOf("hi"), -1);
103 | await this.amqp.RemoveQueueConsumer(testConfig.testUser, q.queue, null);
104 | await assert.rejects(this.amqp.RemoveQueueConsumer(testConfig.testUser, null, null));
105 | }
106 | }
107 | // clear && ./node_modules/.bin/_mocha "src/test/amqp.test.ts"
--------------------------------------------------------------------------------
/src/rabbitmq.ts:
--------------------------------------------------------------------------------
1 | import got from "got";
2 | import url from "url";
3 | import { AssertQueue } from "./amqpwrapper.js";
4 | import { Config } from "./Config.js";
5 | import { Logger, promiseRetry } from "./Logger.js";
6 | import { Util } from "./Util.js";
7 |
8 | export class rabbitmq {
9 | static parseurl(amqp_url): url.UrlWithParsedQuery {
10 | const q = url.parse(amqp_url, true);
11 | if (q.port == null || q.port == "") { q.port = "15672"; }
12 | if (q.auth != null && q.auth != "") {
13 | const arr = q.auth.split(":");
14 | (q as any).username = arr[0];
15 | (q as any).password = arr[1];
16 | } else {
17 | (q as any).username = Config.amqp_username;
18 | (q as any).password = Config.amqp_password;
19 | }
20 | q.protocol = "http://";
21 | return q;
22 | }
23 |
24 | // This will crash the channel, that does not seem scalable
25 | async checkQueue(queuename: string): Promise {
26 | if (Config.amqp_check_for_consumer) {
27 | let test: AssertQueue = null;
28 | try {
29 | if (Config.amqp_check_for_consumer_count) {
30 | return this.checkQueueConsumerCount(queuename);
31 | }
32 | test = await rabbitmq.getqueue(Config.amqp_url, "/", queuename);
33 | if (test == null) {
34 | return false;
35 | }
36 | } catch (error) {
37 | test = null;
38 | }
39 | if (test == null || test.consumerCount == 0) {
40 | return false;
41 | }
42 | }
43 | return true;
44 | }
45 | async checkQueueConsumerCount(queuename: string): Promise {
46 | let result: boolean = false;
47 | try {
48 | result = await promiseRetry(async () => {
49 | const queue = await rabbitmq.getqueue(Config.amqp_url, "/", queuename);
50 | let hasConsumers: boolean = false;
51 | if (queue.consumers > 0) {
52 | hasConsumers = true;
53 | }
54 | if (!hasConsumers) {
55 | if (queue.consumer_details != null && queue.consumer_details.length > 0) {
56 | hasConsumers = true;
57 | } else {
58 | hasConsumers = false;
59 | }
60 | }
61 | if (hasConsumers == false) {
62 | hasConsumers = false;
63 | throw new Error("No consumer listening at " + queuename);
64 | }
65 | return hasConsumers;
66 | }, 10, 1000);
67 | } catch (error) {
68 | Logger.instanse.error(error, null, { cls: "rabbitmq", func: "checkQueueConsumerCount" });
69 | }
70 | if (result == true) {
71 | return result;
72 | }
73 | return false;
74 | }
75 | static async getvhosts(amqp_url) {
76 | const q = this.parseurl(amqp_url);
77 | const options = {
78 | headers: {
79 | "Content-type": "application/x-www-form-urlencoded"
80 | },
81 | username: (q as any).username,
82 | password: (q as any).password
83 | };
84 | const _url = "http://" + q.host + ":" + q.port + "/api/vhosts";
85 | const response = await got.get(_url, options);
86 | const payload = JSON.parse(response.body);
87 | return payload;
88 | }
89 | static async getqueues(amqp_url: string, vhost: string = null) {
90 | const q = this.parseurl(amqp_url);
91 | const options = {
92 | headers: {
93 | "Content-type": "application/x-www-form-urlencoded"
94 | },
95 | username: (q as any).username,
96 | password: (q as any).password
97 | };
98 | let _url = "http://" + q.host + ":" + q.port + "/api/queues";
99 | if (!Util.IsNullEmpty(vhost)) _url += "/" + encodeURIComponent(vhost);
100 | const response = await got.get(_url, options);
101 | const payload = JSON.parse(response.body);
102 | return payload;
103 | }
104 | static async getqueue(amqp_url: string, vhost: string, queuename) {
105 | const q = this.parseurl(amqp_url);
106 | const options = {
107 | headers: {
108 | "Content-type": "application/x-www-form-urlencoded"
109 | },
110 | username: (q as any).username,
111 | password: (q as any).password,
112 | timeout: 500, retry: 1
113 | };
114 | const _url = "http://" + q.host + ":" + q.port + "/api/queues/" + encodeURIComponent(vhost) + "/" + encodeURIComponent(queuename);
115 | const response = await got.get(_url, options);
116 | const payload = JSON.parse(response.body);
117 | return payload;
118 | }
119 | static async deletequeue(amqp_url: string, vhost: string, queuename) {
120 | const q = this.parseurl(amqp_url);
121 | const options = {
122 | headers: {
123 | "Content-type": "application/x-www-form-urlencoded"
124 | },
125 | username: (q as any).username,
126 | password: (q as any).password,
127 | timeout: 500, retry: 1
128 | };
129 | const _url = "http://" + q.host + ":" + q.port + "/api/queues/" + encodeURIComponent(vhost) + "/" + encodeURIComponent(queuename);
130 | const response = await got.delete(_url, options);
131 | const payload = JSON.parse(response.body);
132 | return payload;
133 | }
134 | }
--------------------------------------------------------------------------------
/src/test/loadtest.test.ts:
--------------------------------------------------------------------------------
1 | import { NoderedUtil, WebSocketClient } from "@openiap/openflow-api";
2 | import { suite, test, timeout } from "@testdeck/mocha";
3 | import * as crypto from "crypto";
4 | import { Config } from "../Config.js";
5 | import { testConfig } from "./testConfig.js";
6 |
7 | @suite class loadtest {
8 | public clients: WebSocketClient[] = [];
9 |
10 | @timeout(10000)
11 | async before() {
12 | await testConfig.configure();
13 | }
14 | @timeout(5000)
15 | async after() {
16 | for (var i = 0; i < this.clients.length; i++) {
17 | await this.clients[i].close(1000, "Close by user");
18 | this.clients[i].events.removeAllListeners()
19 | }
20 | await testConfig.cleanup();
21 | }
22 | sleep(ms) {
23 | return new Promise(resolve => {
24 | setTimeout(resolve, ms)
25 | })
26 | }
27 | public jwt: string = "";
28 | public async createandconnect(i: number) {
29 | try {
30 | // console.log("Creating client " + i);
31 | var logger: any =
32 | {
33 | info(msg) { console.log(i + ") " + msg); },
34 | verbose(msg) { console.debug(i + ") " + msg); },
35 | error(msg) { console.error(i + ") " + msg); },
36 | debug(msg) { console.log(i + ") " + msg); },
37 | silly(msg) { console.log(i + ") " + msg); }
38 | }
39 | // ApiConfig.log_trafic_verbose = true;
40 | // ApiConfig.log_trafic_silly = true;
41 | // ApiConfig.log_information = true;
42 |
43 | var websocket = new WebSocketClient(logger, "ws://localhost:" + Config.port, true);
44 | let randomNum = crypto.randomInt(1, 5)
45 | websocket.agent = "openrpa";
46 | if (randomNum == 1) websocket.agent = "nodered";
47 | if (randomNum == 3) websocket.agent = "webapp";
48 | websocket.agent = websocket.agent;
49 | await websocket.Connect();
50 | this.jwt = testConfig.userToken;
51 | if (NoderedUtil.IsNullEmpty(this.jwt)) {
52 | var signin = await NoderedUtil.SigninWithUsername({ username: testConfig.testUser.username, password: testConfig.testPassword, websocket });
53 | this.jwt = signin.jwt; // password validating 200 users will kill the CPU
54 | // console.log("Client " + i + " signed in with jwt " + this.jwt?.substring(0, 10) + "...");
55 | } else {
56 | await NoderedUtil.SigninWithToken({ jwt: this.jwt, websocket });
57 | // console.log("Client " + i + " signed in with jwt " + this.jwt?.substring(0, 10) + "...");
58 | }
59 | this.clients.push(websocket);
60 | // console.log("Client " + i + " connected and signed in");
61 |
62 | if (websocket.agent = "openrpa") {
63 | let arr = await NoderedUtil.Query({ jwt: this.jwt, query: { "_type": "workflowinstance" }, collectionname: "openrpa_instances", websocket });
64 | console.log("Client " + i + " recevied " + arr.length + " items from openrpa_instances");
65 | arr = await NoderedUtil.Query({ jwt: this.jwt, query: { "_type": "workflow" }, collectionname: "openrpa", websocket });
66 | console.log("Client " + i + " recevied " + arr.length + " items from openrpa");
67 | }
68 | randomNum = crypto.randomInt(1, 50) + 15;
69 | setInterval(async () => {
70 | if (websocket.agent = "openrpa") {
71 | let arr = await NoderedUtil.Query({ jwt: this.jwt, query: { "_type": "workflow" }, collectionname: "openrpa", websocket });
72 | console.log("Client " + i + " recevied " + arr.length + " items from openrpa");
73 | } else if (websocket.agent = "nodered") {
74 | let arr = await NoderedUtil.Query({ jwt: this.jwt, query: { "_type": "flow" }, collectionname: "nodered", websocket });
75 | console.log("Client " + i + " recevied " + arr.length + " items from nodered");
76 | } else {
77 | let arr = await NoderedUtil.Query({ jwt: this.jwt, query: {}, collectionname: "entities", websocket });
78 | console.log("Client " + i + " recevied " + arr.length + " items from entities");
79 | }
80 | // let arr = await NoderedUtil.Query({ jwt: this.jwt, query: { "_type": "workflowinstance" }, collectionname: "openrpa_instances", websocket });
81 | // console.log("Client " + i + " recevied " + arr.length + " items from openrpa_instances");
82 | }, 1000 * randomNum)
83 | } catch (error) {
84 | var e = error;
85 | if (error == null) {
86 | console.error("unknown error, is ws://localhost:" + Config.port + " running ?");
87 | } else {
88 | console.error("unknown error", error);
89 | }
90 | }
91 | }
92 |
93 | // @timeout(6000000)
94 | // @test
95 | async "crud connection load test"() {
96 | await this.createandconnect(0);
97 | var Promises: Promise[] = [];
98 | for (var i = 0; i < 500; i++) {
99 | Promises.push(this.createandconnect(i));
100 | if (i && i % 10 == 0) {
101 | await Promise.all(Promises.map(p => p.catch(e => e)))
102 | Promises = [];
103 | }
104 | }
105 | await this.sleep(1000 * 60 * 30);
106 | }
107 | }
108 | // clear && ./node_modules/.bin/_mocha "src/test/**/loadtest.test.ts"
109 |
110 | // node_modules\.bin\_mocha "src/test/**/loadtest.test.ts"
111 | // clear && ./node_modules/.bin/_mocha "src/test/**/loadtest.test.ts"
112 |
--------------------------------------------------------------------------------
/src/test/DBHelper.test.ts:
--------------------------------------------------------------------------------
1 | import { suite, test, timeout } from "@testdeck/mocha";
2 | import assert from "assert";
3 | import { Config } from "../Config.js";
4 | import { Crypt } from "../Crypt.js";
5 | import { Logger } from "../Logger.js";
6 | import { Util, Wellknown } from "../Util.js";
7 | import { FederationId, TokenUser, User } from "../commoninterfaces.js";
8 | import { testConfig } from "./testConfig.js";
9 |
10 | @suite class dbhelper_test {
11 | @timeout(10000)
12 | async before() {
13 | await testConfig.configure();
14 | }
15 | async after() {
16 | await testConfig.cleanup();
17 | }
18 | @test async "FindByUsername"() {
19 | var user = await Logger.DBHelper.FindByUsername(testConfig.testUser.username, Crypt.rootToken(), null);
20 | assert.notStrictEqual(user, null, "Failed locating test user as root")
21 | user = await Logger.DBHelper.FindByUsername(testConfig.testUser.username, testConfig.userToken, null);
22 | assert.notStrictEqual(user, null, "Failed locating test user as self")
23 | user = await Logger.DBHelper.FindByUsername(null, Crypt.rootToken(), null);
24 | assert.strictEqual(user, null, "Returned user with null as username")
25 | // await assert.rejects(DBHelper.FindByUsername(null, Crypt.rootToken(), null));
26 | }
27 | @test async "FindById"() {
28 | var user = await Logger.DBHelper.FindById(testConfig.testUser._id, null);
29 | assert.notStrictEqual(user, null, "Failed locating test user as root")
30 | }
31 | @test async "FindByUsernameOrFederationid"() {
32 | var user = await Logger.DBHelper.FindByUsernameOrFederationid(testConfig.testUser.username, null, null);
33 | assert.notStrictEqual(user, null, "Failed locating user by username")
34 | user = await Logger.DBHelper.FindByUsernameOrFederationid("test@federation.id", "google", null);
35 | assert.notStrictEqual(user, null, "Failed locating user by federation id")
36 | user = await Logger.DBHelper.FindByUsernameOrFederationid(null, null, null)
37 | assert.strictEqual(user, null, "Returned user with null as username and Federationid")
38 | // await assert.rejects(DBHelper.FindByUsernameOrFederationid(null, null));
39 | }
40 | @test async "DecorateWithRoles"() {
41 | var tuser = TokenUser.From(testConfig.testUser);
42 | tuser.roles = [];
43 | tuser = await Logger.DBHelper.DecorateWithRoles(tuser, null);
44 | assert.notStrictEqual(tuser.roles.length, 0, "No roles added to user")
45 | Config.decorate_roles_fetching_all_roles = !Config.decorate_roles_fetching_all_roles;
46 | tuser.roles = [];
47 | tuser = await Logger.DBHelper.DecorateWithRoles(tuser, null);
48 | assert.notStrictEqual(tuser.roles.length, 0, "No roles added to user")
49 | tuser = await Logger.DBHelper.DecorateWithRoles(null, null)
50 | assert.strictEqual(tuser, null, "DecorateWithRoles Returned user with null argument")
51 | // await assert.rejects(DBHelper.DecorateWithRoles(null, null));
52 | }
53 | @test async "FindRoleByName"() {
54 | var role = await Logger.DBHelper.FindRoleByName(testConfig.testUser.username, null, null);
55 | assert.strictEqual(role, null, "returned something with illegal name")
56 | role = await Logger.DBHelper.FindRoleByName(Wellknown.users.name, null, null);
57 | assert.notStrictEqual(role, null, "Failed locating role users")
58 | }
59 | @test async "FindRoleByNameOrId"() {
60 | var role = await Logger.DBHelper.FindRoleByName(testConfig.testUser.username, null, null);
61 | assert.strictEqual(role, null, "returned something with illegal name")
62 | role = await Logger.DBHelper.FindRoleByName(Wellknown.users.name, null, null);
63 | assert.notStrictEqual(role, null, "Failed locating role users")
64 | role = await Logger.DBHelper.FindRoleById(Wellknown.users._id, null, null);
65 | assert.notStrictEqual(role, null, "Failed locating role users")
66 | role = await Logger.DBHelper.FindRoleByName(null, null, null);
67 | assert.strictEqual(role, null, "Returned role with null as username")
68 | role = await Logger.DBHelper.FindRoleById(null, null, null);
69 | assert.strictEqual(role, null, "Returned userrole with null as id")
70 |
71 | // await assert.rejects(DBHelper.FindRoleByName(null, null));
72 | // await assert.rejects(DBHelper.FindRoleById(null, null, null));
73 | }
74 | @timeout(5000)
75 | @test async "EnsureUser"() {
76 | var name = "dummytestuser" + Util.GetUniqueIdentifier();
77 | let extraoptions = {
78 | federationids: [new FederationId("test@federation.id", "google")],
79 | emailvalidated: true,
80 | formvalidated: true,
81 | validated: true
82 | }
83 | var dummyuser: User = await Logger.DBHelper.EnsureUser(Crypt.rootToken(), name, name, null, "RandomPassword", extraoptions, null);
84 | var result = await Crypt.ValidatePassword(dummyuser, "RandomPassword", null);
85 |
86 | await Logger.DBHelper.EnsureNoderedRoles(dummyuser, Crypt.rootToken(), true, null);
87 |
88 | dummyuser = await Logger.DBHelper.DecorateWithRoles(dummyuser, null);
89 | assert.ok(dummyuser.roles.filter(x => x.name.endsWith("noderedadmins")), "EnsureNoderedRoles did not make dummy user member of noderedadmins");
90 | assert.ok(dummyuser.roles.filter(x => x.name.endsWith("nodered api users")), "EnsureNoderedRoles did not make dummy user member of nodered api users");
91 |
92 |
93 | assert.ok(result, "Failed validating with the correct password");
94 | await Config.db.DeleteOne(dummyuser._id, Wellknown.users.name, false, Crypt.rootToken(), null);
95 |
96 | await assert.rejects(Logger.DBHelper.EnsureUser(Crypt.rootToken(), null, null, null, null, null, null));
97 | }
98 | @test async "EnsureRole"() {
99 | var name = "dummytestrole" + Util.GetUniqueIdentifier();
100 | var dummyrole = await Logger.DBHelper.EnsureRole(name, null, null);
101 | await Config.db.DeleteOne(dummyrole._id, Wellknown.users.name, false, Crypt.rootToken(), null);
102 | }
103 | }
104 | // clear && ./node_modules/.bin/_mocha "src/test/**/DBHelper.test.ts"
--------------------------------------------------------------------------------
/src/QueueClient.ts:
--------------------------------------------------------------------------------
1 | import { Span } from "@opentelemetry/api";
2 | import { amqpqueue, amqpwrapper, QueueMessageOptions } from "./amqpwrapper.js";
3 | import { Config } from "./Config.js";
4 | import { Crypt } from "./Crypt.js";
5 | import { Logger } from "./Logger.js";
6 | import { Message } from "./Messages/Message.js";
7 | import { Util } from "./Util.js";
8 | export class QueueClient {
9 | static async configure(parent: Span): Promise {
10 | const span: Span = Logger.otel.startSubSpan("QueueClient.configure", parent);
11 | try {
12 | await QueueClient.connect();
13 | var instance = amqpwrapper.Instance();
14 | instance.on("connected", () => {
15 | QueueClient.connect();
16 | });
17 | } finally {
18 | Logger.otel.endSpan(span);
19 | }
20 | }
21 | private static async connect() {
22 | await this.RegisterMyQueue();
23 | await this.RegisterOpenflowQueue();
24 | }
25 | private static queue: amqpqueue = null;
26 | private static queuename: string = "openflow";
27 | public static async RegisterOpenflowQueue() {
28 | const AssertQueueOptions: any = Object.assign({}, (amqpwrapper.Instance().AssertQueueOptions));
29 | AssertQueueOptions.exclusive = false;
30 | AssertQueueOptions["x-max-priority"] = 5;
31 | AssertQueueOptions.maxPriority = 5;
32 | await amqpwrapper.Instance().AddQueueConsumer(Crypt.rootUser(), this.queuename, AssertQueueOptions, null, async (data: any, options: QueueMessageOptions, ack: any, done: any) => {
33 | const msg: Message = Message.fromjson(data);
34 | let span: Span = null;
35 | if (!Config.db.isConnected) {
36 | ack(false);
37 | return;
38 | }
39 | try {
40 | msg.priority = options.priority;
41 | if (!Util.IsNullEmpty(options.replyTo)) {
42 |
43 | span = Logger.otel.startSpan("OpenFlow Queue Process Message", msg.traceId, msg.spanId);
44 | Logger.instanse.debug("Process command: " + msg.command + " id: " + msg.id + " correlationId: " + options.correlationId, span, {cls: "QueueClient", func: "RegisterOpenflowQueue"});
45 | await msg.QueueProcess(options, span);
46 | ack();
47 | await amqpwrapper.Instance().send(options.exchangename, options.replyTo, msg, Config.openflow_amqp_expiration, options.correlationId, options.routingKey, span);
48 | } else {
49 | ack(false);
50 | Logger.instanse.debug("[queue][ack] No replyto !!!!", span, {cls: "QueueClient", func: "RegisterOpenflowQueue"});
51 | }
52 | } catch (error) {
53 | try {
54 | ack(false);
55 | } catch (error) {
56 | }
57 | } finally {
58 | Logger.otel.endSpan(span);
59 | }
60 | }, null);
61 | }
62 | public static async RegisterMyQueue() {
63 | const AssertQueueOptions: any = Object.assign({}, (amqpwrapper.Instance().AssertQueueOptions));
64 | AssertQueueOptions.exclusive = false;
65 | this.queue = await amqpwrapper.Instance().AddQueueConsumer(Crypt.rootUser(), "", AssertQueueOptions, null, async (data: any, options: QueueMessageOptions, ack: any, done: any) => {
66 | const msg: Message = Message.fromjson(data);
67 | try {
68 | if (Util.IsNullEmpty(options.replyTo)) {
69 | ack();
70 | const exists = this.messages.filter(x => x.correlationId == options.correlationId);
71 | if (exists.length > 0) {
72 | Logger.instanse.silly("[queue][ack] Received response for command: " + msg.command + " queuename: " + this.queuename + " replyto: " + options.replyTo + " correlationId: " + options.correlationId, null, {cls: "QueueClient", func: "RegisterMyQueue"});
73 | this.messages = this.messages.filter(x => x.correlationId != options.correlationId);
74 | exists[0].cb(msg);
75 | }
76 | } else {
77 | ack(false);
78 | }
79 | } catch (error) {
80 | ack(false);
81 | }
82 | }, null);
83 | }
84 | private static messages: Message[] = [];
85 | public static async SendForProcessing(msg: Message, priority: number, span: Span) {
86 | return new Promise(async (resolve, reject) => {
87 | try {
88 | if (this.queue == null) {
89 | throw new Error("Queue is not initialized");
90 | }
91 | var d = Object.assign({}, msg);
92 | delete d.tuser;
93 | var json = JSON.stringify(d)
94 | msg.correlationId = Util.GetUniqueIdentifier();
95 | this.messages.push(msg);
96 | Logger.instanse.debug("Submit command: " + msg.command + " id: " + msg.id + " correlationId: " + msg.correlationId, span, {cls: "QueueClient", func: "SendForProcessing"});
97 | msg.cb = (result) => {
98 | if (result.replyto != msg.id) {
99 | Logger.instanse.warn("Received response failed for command: " + msg.command + " id: " + result.id + " replyto: " + result.replyto + " but expected reply to be " + msg.id + " correlationId: " + result.correlationId, span, {cls: "QueueClient", func: "SendForProcessing"});
100 | result.id = Util.GetUniqueIdentifier();
101 | result.replyto = msg.id;
102 | }
103 | result.correlationId = msg.correlationId;
104 | Logger.instanse.debug("Got reply command: " + msg.command + " id: " + result.id + " replyto: " + result.replyto + " correlationId: " + result.correlationId, span, {cls: "QueueClient", func: "SendForProcessing"});
105 | resolve(result);
106 | }
107 | Logger.instanse.silly("Submit request for command: " + msg.command + " queuename: " + this.queuename + " replyto: " + this.queue.queue + " correlationId: " + msg.correlationId, null, {cls: "QueueClient", func: "SendForProcessing"});
108 | await amqpwrapper.Instance().sendWithReplyTo("", this.queuename, this.queue.queue, json, Config.openflow_amqp_expiration, msg.correlationId, "", span, priority);
109 | } catch (error) {
110 | if (Util.IsNullUndefinded(this.queue)) {
111 | Logger.instanse.warn("SendForProcessing queue is null, shutdown amqp connection", span, {cls: "QueueClient", func: "SendForProcessing"});
112 | process.exit(406);
113 | } else {
114 | Logger.instanse.error(error, span, {cls: "QueueClient", func: "SendForProcessing"});
115 | }
116 | reject(error);
117 | }
118 | });
119 | }
120 | }
--------------------------------------------------------------------------------
/src/test/basic_entities.test.ts:
--------------------------------------------------------------------------------
1 | import { NoderedUtil, WebSocketClient } from "@openiap/openflow-api";
2 | import { suite, test, timeout } from "@testdeck/mocha";
3 | import assert from "assert";
4 | import { Config } from "../Config.js";
5 | import { testConfig } from "./testConfig.js";
6 |
7 | @suite class basic_entities {
8 | private socket: WebSocketClient = null;
9 | @timeout(2000)
10 | async before() {
11 | await testConfig.configure();
12 | if (!this.socket) this.socket = new WebSocketClient(null, "ws://localhost:" + Config.port, true);
13 | this.socket.agent = "test-cli";
14 | try {
15 | await this.socket.Connect();
16 | await NoderedUtil.SigninWithUsername({ username: testConfig.testUser.username, password: testConfig.testPassword });
17 | } catch (error) {
18 | if (error == null) error = new Error("Failed connecting to ws://localhost:" + Config.port)
19 | throw error;
20 | }
21 | }
22 | @timeout(5000)
23 | async after() {
24 | await this.socket.close(1000, "Close by user");
25 | this.socket.events.removeAllListeners()
26 | await testConfig.cleanup();
27 | }
28 | @timeout(500000)
29 | @test async "validate collectioname"() {
30 | await assert.rejects(NoderedUtil.Query({ query: { "_type": "test" }, collectionname: null }));
31 | await assert.rejects(NoderedUtil.Query({ query: { "_type": "test" }, collectionname: undefined }));
32 | await assert.rejects(NoderedUtil.Query({ query: { "_type": "test" }, collectionname: "" }));
33 |
34 | await assert.rejects(NoderedUtil.Query({ query: null, collectionname: "entities" }));
35 | await assert.rejects(NoderedUtil.Query({ query: undefined, collectionname: "entities" }));
36 | await assert.rejects(NoderedUtil.Query({ query: "", collectionname: "entities" }));
37 |
38 | await assert.rejects(NoderedUtil.Aggregate({ aggregates: [{ "$match": { "_type": "test" } }], collectionname: null }));
39 | await assert.rejects(NoderedUtil.Aggregate({ aggregates: [{ "$match": { "_type": "test" } }], collectionname: undefined }));
40 | await assert.rejects(NoderedUtil.Aggregate({ aggregates: [{ "$match": { "_type": "test" } }], collectionname: "" }));
41 |
42 | await assert.rejects(NoderedUtil.Aggregate({ aggregates: null, collectionname: "entities" }));
43 | await assert.rejects(NoderedUtil.Aggregate({ aggregates: undefined, collectionname: "entities" }));
44 |
45 | await assert.rejects(NoderedUtil.DeleteOne({ id: "625d73a451a60cfe7b70daa2", collectionname: null }));
46 | await assert.rejects(NoderedUtil.DeleteOne({ id: "625d73a451a60cfe7b70daa2", collectionname: undefined }));
47 | await assert.rejects(NoderedUtil.DeleteOne({ id: "625d73a451a60cfe7b70daa2", collectionname: "" }));
48 |
49 | await assert.rejects(NoderedUtil.DeleteOne({ id: null, collectionname: "entities" }));
50 | await assert.rejects(NoderedUtil.DeleteOne({ id: undefined, collectionname: "entities" }));
51 | await assert.rejects(NoderedUtil.DeleteOne({ id: "", collectionname: "entities" }));
52 |
53 | await assert.rejects(NoderedUtil.DeleteOne({ id: null, collectionname: "entities" }));
54 | await assert.rejects(NoderedUtil.DeleteOne({ id: undefined, collectionname: "entities" }));
55 | await assert.rejects(NoderedUtil.DeleteOne({ id: "", collectionname: "entities" }));
56 |
57 | await assert.rejects(NoderedUtil.DeleteMany({ query: { "_type": "test" }, collectionname: null }));
58 | await assert.rejects(NoderedUtil.DeleteMany({ query: { "_type": "test" }, collectionname: undefined }));
59 | await assert.rejects(NoderedUtil.DeleteMany({ query: { "_type": "test" }, collectionname: "" }));
60 |
61 | await assert.rejects(NoderedUtil.DropCollection({ collectionname: null }));
62 | await assert.rejects(NoderedUtil.DropCollection({ collectionname: undefined }));
63 | await assert.rejects(NoderedUtil.DropCollection({ collectionname: "" }));
64 |
65 | }
66 | @timeout(5000)
67 | @test async "querytest"() {
68 | await NoderedUtil.DeleteMany({ query: { "_type": "test" }, collectionname: "entities" });
69 |
70 | let item = await NoderedUtil.InsertOne({ item: { "_type": "test", "name": "test entities item" }, collectionname: "entities" });
71 | assert.strictEqual(item.name, "test entities item");
72 | assert.strictEqual(item._type, "test");
73 | item.name = "test entities item updated"
74 | item = await NoderedUtil.UpdateOne({ item: item, collectionname: "entities" });
75 | assert.strictEqual(item.name, "test entities item updated");
76 |
77 | let items = await NoderedUtil.Query({ query: { "_type": "test" }, collectionname: "entities" });
78 | assert.strictEqual(items.length, 1);
79 | item = items[0];
80 | assert.strictEqual(item.name, "test entities item updated");
81 |
82 | await NoderedUtil.DeleteOne({ id: item._id, collectionname: "entities" });
83 |
84 | items = await NoderedUtil.Query({ query: { "_type": "test" }, collectionname: "entities" });
85 | assert.strictEqual(items.length, 0);
86 |
87 | items = [];
88 | items.push({ name: "test item 1", "_type": "test" });
89 | items.push({ name: "test item 2", "_type": "test" });
90 | items.push({ name: "test item 3", "_type": "test" });
91 | items.push({ name: "test item 4", "_type": "test" });
92 | items.push({ name: "test item 5", "_type": "test" });
93 | items = await NoderedUtil.InsertMany({ items: items, collectionname: "entities", skipresults: false });
94 | assert.strictEqual(items.length, 5);
95 | for (var i = 0; i < items.length; i++) {
96 | item = items[i];
97 | assert.notStrictEqual(["test item 1", "test item 2", "test item 3", "test item 4", "test item 5"].indexOf(item.name), -1, "Failed matching name on item");
98 | assert.strictEqual(item._type, "test");
99 | assert.notStrictEqual(item._created, undefined);
100 | assert.notStrictEqual(item._created, null);
101 | }
102 | items = await NoderedUtil.Query({ query: { "_type": "test" }, collectionname: "entities" });
103 | assert.strictEqual(items.length, 5);
104 | for (var i = 0; i < items.length; i++) {
105 | item = items[i];
106 | assert.notStrictEqual(["test item 1", "test item 2", "test item 3", "test item 4", "test item 5"].indexOf(item.name), -1, "Failed matching name on item");
107 | assert.strictEqual(item._type, "test");
108 | assert.notStrictEqual(item._created, undefined);
109 | assert.notStrictEqual(item._created, null);
110 | }
111 |
112 | items = await NoderedUtil.Query({ query: { "_type": "test" }, collectionname: "entities" });
113 | let ids = items.map(x => x._id);
114 | if (ids.length > 0) await NoderedUtil.DeleteMany({ ids, collectionname: "entities" });
115 |
116 | items = await NoderedUtil.Query({ query: { "_type": "test" }, collectionname: "entities" });
117 | assert.strictEqual(items.length, 0, "Failed cleaning up");
118 | }
119 | }
120 | // clear && ./node_modules/.bin/_mocha "src/test/**/basic_entities.test.ts"
121 |
--------------------------------------------------------------------------------
/src/ee/Billings.ts:
--------------------------------------------------------------------------------
1 | import { Rights } from "@openiap/nodeapi";
2 | import { Span } from "@opentelemetry/api";
3 | import { Base, Billing, Customer, Role, User, Workspace } from '../commoninterfaces.js';
4 | import { Config } from "../Config.js";
5 | import { Crypt } from "../Crypt.js";
6 | import { Logger } from "../Logger.js";
7 | import { Util, Wellknown } from "../Util.js";
8 | import { Payments } from "./Payments.js";
9 | import { Resources } from "./Resources.js";
10 | import { Workspaces } from "./Workspaces.js";
11 | import { DatabaseConnection } from "../DatabaseConnection.js";
12 |
13 | export class Billings {
14 | public static async EnsureBilling(tuser: User, jwt: string, billing: Billing, parent: Span): Promise {
15 | let result: Billing = new Billing();
16 | if (billing == null) throw new Error("Billing is required");
17 | if (billing._id != null && billing._id != "") {
18 | result = await Config.db.GetOne({ collectionname: "users", query: { _id: billing._id, _type: "customer" }, jwt }, parent);
19 | if (result == null) throw new Error(Logger.enricherror(tuser, billing, "Billing object not found"));
20 | }
21 | const billingadmins = await Logger.DBHelper.EnsureUniqueRole(billing.name + " billing admins", result.admins, parent);
22 | if (billing._id != null && billing._id != "") {
23 | if (!tuser.HasRoleName(Wellknown.admins.name)) {
24 | if (!billingadmins.IsMember(tuser._id)) throw new Error(Logger.enricherror(tuser, billing, "User is not a member of the billing admins"));
25 | }
26 | } else {
27 | Base.addRight(billingadmins, billingadmins._id, billingadmins.name, [Rights.read]);
28 | billingadmins.AddMember(tuser);
29 | }
30 | const rootjwt = Crypt.rootToken();
31 | Base.removeRight(billingadmins, billingadmins._id, [Rights.full_control]);
32 | Base.addRight(billingadmins, billingadmins._id, billingadmins.name, [Rights.read]);
33 | await Logger.DBHelper.Save(billingadmins, rootjwt, parent);
34 | Base.removeRight(result, billingadmins._id, [Rights.full_control]);
35 | Base.addRight(result, billingadmins._id, billingadmins.name, [Rights.read]);
36 |
37 | result.name = billing.name;
38 | result.admins = billingadmins._id;
39 | if (billing.email != null && billing.email != "") result.email = billing.email;
40 | if (result.email == null || result.email == "") result.email = tuser.email;
41 | if (result.email == null || result.email == "") result.email = tuser.username;
42 | const stripe_customer = await Payments.EnsureCustomer(tuser, jwt, result, parent);
43 | if (stripe_customer != null) {
44 | result.stripeid = stripe_customer.id;
45 | if (!Util.IsNullEmpty((stripe_customer as any).currency))
46 | result.currency = (stripe_customer as any).currency;
47 | }
48 |
49 | result = await Config.db.InsertOrUpdateOne(result, "users", "_id", 1, true, rootjwt, parent);
50 | return result;
51 | }
52 | public static async RemoveBilling(tuser: User, jwt: string, billingid: string, parent: Span): Promise {
53 | if (Util.IsNullEmpty(billingid)) throw new Error("Billing id is required");
54 | let billing: Billing = new Billing();
55 | billing = await Config.db.GetOne({ collectionname: "users", query: { _id: billingid, _type: "customer" }, jwt }, parent);
56 | if (billing == null) throw new Error(Logger.enricherror(tuser, billing, "Billing object not found"));
57 | const billingadmins = await Logger.DBHelper.EnsureUniqueRole(billing.name + " billing admins", billing.admins, parent);
58 | if (!tuser.HasRoleName(Wellknown.admins.name)) {
59 | if (!billingadmins.IsMember(tuser._id)) throw new Error(Logger.enricherror(tuser, billing, "User is not a member of the billing admins"));
60 | }
61 | const rootjwt = Crypt.rootToken();
62 | const count = await Resources.GetCustomerResourcesCount(billingid, parent);
63 | if (count > 0) throw new Error(Logger.enricherror(tuser, billing, "There are resources using this Billing account"));
64 | await Config.db.DeleteOne(billingadmins._id, "users", false, rootjwt, parent);
65 | await Config.db.DeleteOne(billingid, "users", false, rootjwt, parent);
66 | }
67 | public static async GetBillingPortalLink(tuser: User, jwt: string, billingid: string, parent: Span): Promise {
68 | if (Util.IsNullEmpty(billingid)) throw new Error("Billing id is required");
69 | const billing = await Config.db.GetOne({ collectionname: "users", query: { _id: billingid, _type: "customer" }, jwt }, parent);
70 | if (billing == null) throw new Error(Logger.enricherror(tuser, billing, "Billing object not found"));
71 | const session = await Payments.CreateBillingPortalSession(tuser, billing.stripeid, parent);
72 | if (session == null) throw new Error(Logger.enricherror(tuser, billing, "Error creating billing portal session"));
73 | return session.url;
74 | }
75 | public static async UpgradeBillingAccount(tuser: User, jwt: string, billingid: string, parent: Span): Promise {
76 | if (!tuser.HasRoleId(Wellknown.admins._id)) throw new Error("Access denied");
77 | if (Util.IsNullEmpty(billingid)) throw new Error("Billing id is required");
78 | const ucustomer = await Config.db.GetOne({ query: { _id: billingid, "_type": "customer" }, collectionname: "users", jwt }, parent);
79 | if (ucustomer == null) throw new Error("Customer not found, or access denied");
80 | let uworkspace = await Config.db.GetOne({ query: { name: ucustomer.name, "_type": "workspace", _billingid: ucustomer._id }, collectionname: "users", jwt }, parent);
81 | // if(ucustomer.users == null || ucustomer.users == "") {
82 | // return ucustomer as any;
83 | // }
84 | let u2: User = null;
85 | let uusers = await Config.db.GetOne({ query: { _id: ucustomer.users, "_type": "role" }, collectionname: "users", jwt }, parent);
86 | if(uusers != null && uusers.name == "users") {
87 | uusers = null;
88 | }
89 | if(uusers == null) {
90 | uusers = await Config.db.GetOne({ query: { name: ucustomer.name + " users", "_type": "role" }, collectionname: "users", jwt }, parent);
91 | }
92 | let uadmins = await Config.db.GetOne({ query: { _id: ucustomer.admins, "_type": "role" }, collectionname: "users", jwt }, parent);
93 | if(uadmins != null && uadmins.name == "admins") {
94 | uadmins = null;
95 | }
96 | if(uadmins == null) {
97 | uadmins = await Config.db.GetOne({ query: { name: ucustomer.name + " admins", "_type": "role" }, collectionname: "users", jwt }, parent);
98 | }
99 | if(ucustomer.userid != null && ucustomer.userid != "") {
100 | u2 = await Config.db.GetOne({ query: { _id: ucustomer.userid, "_type": "user" }, collectionname: "users", jwt }, parent);
101 | }
102 | if(uusers == null && uadmins == null && u2 == null) {
103 | // console.warn("Deleting customer " + ucustomer.name + " (" + ucustomer._id + ") with no users attached");
104 | // await Config.db.DeleteOne(ucustomer._id, "users", false, jwt, parent);
105 | return ucustomer as any;
106 | }
107 | if(uworkspace == null) {
108 | uworkspace = await Workspaces.EnsureWorkspace(tuser, jwt, {"name": ucustomer.name, _billingid: ucustomer._id } as any, true, parent);
109 | }
110 | if(uusers != null) {
111 | for(let i = 0; i < uusers.members.length; i++) {
112 | const member = uusers.members[i];
113 | let u = await Config.db.GetOne({ query: { _id: member._id, "_type": "user" }, collectionname: "users", jwt }, parent);
114 | if(u == null) continue;
115 | if(DatabaseConnection.WellknownIdsArray.indexOf(u._id) == -1 ) {
116 | await Workspaces.AddUserToWorkspace(tuser, jwt, u._id, u.email || u.username, uworkspace._id, "member", parent);
117 | }
118 | }
119 | }
120 | if(uadmins != null) {
121 | for(let i = 0; i < uadmins.members.length; i++) {
122 | const member = uadmins.members[i];
123 | let u = await Config.db.GetOne({ query: { _id: member._id, "_type": "user" }, collectionname: "users", jwt }, parent);
124 | if(u == null) continue;
125 | if(DatabaseConnection.WellknownIdsArray.indexOf(u._id) == -1 ) {
126 | await Workspaces.AddUserToWorkspace(tuser, jwt, u._id, u.email || u.username, uworkspace._id, "admin", parent);
127 | }
128 | }
129 | }
130 | if(u2 != null && DatabaseConnection.WellknownIdsArray.indexOf(u2._id) == -1) {
131 | await Workspaces.AddUserToWorkspace(tuser, jwt, u2._id, u2.email || u2.username, uworkspace._id, "admin", parent);
132 | }
133 | delete ucustomer.users;
134 | delete ucustomer.userid;
135 | await Config.db.UpdateOne(ucustomer, "users", 1, true, Crypt.rootToken(), parent);
136 | return ucustomer as any;
137 | }
138 | }
--------------------------------------------------------------------------------
/src/test/workitemqueue.test.ts:
--------------------------------------------------------------------------------
1 | import { AddWorkitem, MessageWorkitemFile, NoderedUtil, WebSocketClient, Workitem } from "@openiap/openflow-api";
2 | import { suite, test, timeout } from "@testdeck/mocha";
3 | import assert from "assert";
4 | import fs from "fs";
5 | import pako from "pako";
6 | import path from "path";
7 | import { fileURLToPath } from "url";
8 | import { Config } from "../Config.js";
9 | import { testConfig } from "./testConfig.js";
10 | const __filename = fileURLToPath(import.meta.url);
11 | const __dirname = path.dirname(__filename);
12 |
13 | @suite class workitemqueue {
14 | private socket: WebSocketClient = null;
15 | @timeout(2000)
16 | async before() {
17 | await testConfig.configure();
18 | if (!this.socket) this.socket = new WebSocketClient(null, "ws://localhost:" + Config.port, true);
19 | this.socket.agent = "test-cli";
20 | await this.socket.Connect();
21 | await NoderedUtil.SigninWithUsername({ username: testConfig.testUser.username, password: testConfig.testPassword });
22 | }
23 | @timeout(5000)
24 | async after() {
25 | await this.socket.close(1000, "Close by user");
26 | this.socket.events.removeAllListeners()
27 | await testConfig.cleanup();
28 | }
29 | @timeout(10000)
30 | @test
31 | async "basic workitem test"() {
32 | let q = await NoderedUtil.GetWorkitemQueue({ name: "test queue" });
33 | if (q == null) q = await NoderedUtil.AddWorkitemQueue({ name: "test queue" });
34 | assert.notStrictEqual(q, null, "Failed getting test queue");
35 | assert.notStrictEqual(q, undefined, "Failed getting test queue");
36 |
37 | let item: Workitem;
38 | do {
39 | item = await NoderedUtil.PopWorkitem({ wiq: q.name });
40 | if (item != null) await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "successful" });
41 | } while (item != null)
42 |
43 | item = await NoderedUtil.AddWorkitem({ name: "Test Work Item", payload: { "find": "me" }, wiq: q.name });
44 | assert.notStrictEqual(item, null, "Failed adding test work item");
45 | assert.notStrictEqual(item, undefined, "Failed adding test work item");
46 | assert.strictEqual(item.name, "Test Work Item", "Failed matching name on new work item");
47 | assert.strictEqual(item.state, "new", "New Workitem is not in status new");
48 |
49 | item = await NoderedUtil.PopWorkitem({ wiq: q.name });
50 | assert.notStrictEqual(item, null, "Failed getting test work item");
51 | assert.notStrictEqual(item, undefined, "Failed getting test work item");
52 | assert.strictEqual(item.name, "Test Work Item", "Failed matching name on work item");
53 | assert.strictEqual(item.state, "processing", "Updated Workitem is not in state processing");
54 |
55 | item = await NoderedUtil.UpdateWorkitem({ _id: item._id });
56 |
57 | let testitem = await NoderedUtil.PopWorkitem({ wiq: q.name });
58 | assert.strictEqual(testitem, undefined, "Failed queue test, can still pop items while processing the added item!");
59 |
60 | item = await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "retry" });
61 | assert.strictEqual(item.state, "new", "Workitem sent for retry is not in status new");
62 |
63 | item = await NoderedUtil.PopWorkitem({ wiq: q.name });
64 | assert.strictEqual(item.state, "processing");
65 | assert.notStrictEqual(item, null, "Failed getting test work item");
66 | assert.notStrictEqual(item, undefined, "Failed getting test work item");
67 | assert.strictEqual(item.name, "Test Work Item", "Failed matching name on work item");
68 | await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "successful" });
69 |
70 | // await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "successful" });
71 | }
72 | public static async CreateWorkitemFilesArray(files: string[], compressed: boolean): Promise {
73 | var result: MessageWorkitemFile[] = [];
74 | for (var i = 0; i < files.length; i++) {
75 | let file: MessageWorkitemFile = new MessageWorkitemFile();
76 | file.filename = path.basename(files[i]);
77 | if (fs.existsSync(files[i])) {
78 | if (compressed) {
79 | file.compressed = true;
80 | file.file = Buffer.from(pako.deflate(fs.readFileSync(files[i], null))).toString("base64");
81 | } else {
82 | file.file = fs.readFileSync(files[i], { encoding: "base64" });
83 | }
84 | result.push(file);
85 | } else { throw new Error("File not found " + files[i]) }
86 | }
87 | return result;
88 | }
89 | @timeout(10000)
90 | @test async "basic workitem test with files"() {
91 | let q = await NoderedUtil.GetWorkitemQueue({ name: "test queue" });
92 | if (q == null) q = await NoderedUtil.AddWorkitemQueue({ name: "test queue" });
93 | assert.notStrictEqual(q, null, "Failed getting test queue");
94 | assert.notStrictEqual(q, undefined, "Failed getting test queue");
95 |
96 | let item: Workitem;
97 | do {
98 | item = await NoderedUtil.PopWorkitem({ wiq: q.name });
99 | if (item != null) await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "successful" });
100 | } while (item != null)
101 |
102 |
103 | item = await NoderedUtil.AddWorkitem({
104 | name: "Test Work Item with files", payload: { "find": "me" }, wiq: q.name,
105 | files: await workitemqueue.CreateWorkitemFilesArray([__filename], true)
106 | });
107 | assert.notStrictEqual(item, null, "Failed adding Test Work Item with files");
108 | assert.notStrictEqual(item, undefined, "Failed adding Test Work Item with files");
109 | assert.strictEqual(item.name, "Test Work Item with files", "Failed matching name on new work item");
110 | assert.strictEqual(item.state, "new");
111 |
112 | item = await NoderedUtil.PopWorkitem({ wiq: q.name });
113 | assert.notStrictEqual(item, null, "Failed getting Test Work Item with files");
114 | assert.notStrictEqual(item, undefined, "Failed getting Test Work Item with files");
115 | assert.strictEqual(item.files.length, 1);
116 | assert.strictEqual(item.name, "Test Work Item with files", "Failed matching name on work item");
117 | assert.strictEqual(item.state, "processing");
118 |
119 | item = await NoderedUtil.UpdateWorkitem({
120 | _id: item._id,
121 | files: await workitemqueue.CreateWorkitemFilesArray([path.join(__dirname, "../..", "tsconfig.json")], false)
122 | });
123 |
124 | let testitem = await NoderedUtil.PopWorkitem({ wiq: q.name });
125 | assert.strictEqual(testitem, undefined, "Failed queue test, can still pop items while processing the added item!");
126 |
127 | item = await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "retry" });
128 | assert.strictEqual(item.state, "new");
129 | assert.strictEqual(item.files.length, 2);
130 |
131 | item = await NoderedUtil.PopWorkitem({ wiq: q.name });
132 | assert.strictEqual(item.state, "processing");
133 | assert.notStrictEqual(item, null, "Failed getting Test Work Item with files");
134 | assert.notStrictEqual(item, undefined, "Failed getting Test Work Item with files");
135 | assert.strictEqual(item.name, "Test Work Item with files", "Failed matching name on work item");
136 | assert.strictEqual(item.files.length, 2);
137 | await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "successful" });
138 |
139 | // await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "successful" });
140 | }
141 | @timeout(10000)
142 | @test async "multiple workitem test with files"() {
143 | let q = await NoderedUtil.GetWorkitemQueue({ name: "test queue" });
144 | if (q == null) q = await NoderedUtil.AddWorkitemQueue({ name: "test queue" });
145 | assert.notStrictEqual(q, null, "Failed getting test queue");
146 | assert.notStrictEqual(q, undefined, "Failed getting test queue");
147 |
148 | let item: Workitem;
149 | do {
150 | item = await NoderedUtil.PopWorkitem({ wiq: q.name });
151 | if (item != null) await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "successful" });
152 | } while (item != null)
153 |
154 | const items: AddWorkitem[] = [];
155 | items.push(AddWorkitem.parse({ name: "multi item 1", files: await workitemqueue.CreateWorkitemFilesArray([__filename], true) }));
156 | items.push(AddWorkitem.parse({ name: "multi item 2", files: await workitemqueue.CreateWorkitemFilesArray([__filename], true) }));
157 | items.push(AddWorkitem.parse({ name: "multi item 3", files: await workitemqueue.CreateWorkitemFilesArray([__filename], true) }));
158 |
159 |
160 | await NoderedUtil.AddWorkitems({ items, wiq: q.name })
161 | for (var i = 0; i < 3; i++) {
162 | item = await NoderedUtil.PopWorkitem({ wiq: q.name, });
163 | assert.notStrictEqual(item, null, "Failed getting test work item");
164 | assert.notStrictEqual(item, undefined, "Failed getting test work item");
165 | assert.strictEqual(item.files.length, 1);
166 | assert.notStrictEqual(["multi item 1", "multi item 2", "multi item 3"].indexOf(item.name), -1, "Failed matching name on work item");
167 | assert.strictEqual(item.state, "processing");
168 |
169 | item = await NoderedUtil.UpdateWorkitem({
170 | _id: item._id,
171 | files: await workitemqueue.CreateWorkitemFilesArray([path.join(__dirname, "../..", "tsconfig.json")], false)
172 | });
173 | assert.strictEqual(item.files.length, 2);
174 | await NoderedUtil.UpdateWorkitem({ _id: item._id, state: "successful" });
175 |
176 | }
177 |
178 | let testitem = await NoderedUtil.PopWorkitem({ wiq: q.name });
179 | assert.strictEqual(testitem, undefined, "Failed multiple queue test, can still pop items after processing the 3 items!");
180 |
181 | }
182 | }
183 | // clear && ./node_modules/.bin/_mocha "src/test/**/workitemqueue.test.ts"
184 |
--------------------------------------------------------------------------------
/src/test/workitemqueue-messages.test.ts:
--------------------------------------------------------------------------------
1 | import { AddWorkitemMessage, AddWorkitemQueueMessage, DeleteWorkitemMessage, DeleteWorkitemQueueMessage, GetWorkitemQueueMessage, PopWorkitemMessage, SaveFileMessage, UpdateWorkitemMessage, UpdateWorkitemQueueMessage } from "@openiap/openflow-api";
2 | import { suite, test, timeout } from "@testdeck/mocha";
3 | import assert from "assert";
4 | import fs from "fs";
5 | import pako from "pako";
6 | import path from "path";
7 | import { Message } from "../Messages/Message.js";
8 | import { Util } from "../Util.js";
9 | import { testConfig } from "./testConfig.js";
10 |
11 | @suite class workitemqueue_messages_test {
12 | @timeout(10000)
13 | async before() {
14 | await testConfig.configure();
15 | }
16 | @timeout(10000)
17 | async after() {
18 | await testConfig.cleanup();
19 | // wtf.dump();
20 | }
21 | async GetItem(name) {
22 | var q: any = new GetWorkitemQueueMessage();
23 | var msg = new Message(); msg.jwt = testConfig.userToken;
24 | q.name = name
25 | msg.data = JSON.stringify(q);
26 | await msg.EnsureJWT(null, false)
27 | await msg.GetWorkitemQueue(null);
28 | q = JSON.parse(msg.data);
29 | return q.result;
30 | }
31 | formatBytes(bytes, decimals = 2) {
32 | if (bytes === 0) return "0 Bytes";
33 |
34 | const k = 1024;
35 | const dm = decimals < 0 ? 0 : decimals;
36 | const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
37 |
38 | const i = Math.floor(Math.log(bytes) / Math.log(k));
39 |
40 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
41 | }
42 | @timeout(5000)
43 | async "Save File Base64"() {
44 | var filepath = "./config/invoice2.pdf";
45 | var filepath = "./config/invoice.zip";
46 | var filepath = "./config/invoice2.zip";
47 | var filepath = "./config/invoice.png";
48 | if (!(fs.existsSync(filepath))) return;
49 | var q: SaveFileMessage = new SaveFileMessage();
50 | q.filename = "base64" + path.basename(filepath);
51 | q.file = fs.readFileSync(filepath, { encoding: "base64" });
52 | var msg = new Message(); msg.jwt = testConfig.userToken;
53 | msg.data = JSON.stringify(q);
54 | await msg.EnsureJWT(null, false)
55 | await msg.SaveFile(null);
56 | q = JSON.parse(msg.data);
57 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
58 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
59 | assert.ok(!Util.IsNullUndefinded(q.result), "no result");
60 | return q.result;
61 | }
62 | @timeout(5000)
63 | async "Save File zlib"() {
64 | var filepath = "./config/invoice2.pdf";
65 | var filepath = "./config/invoice.zip";
66 | var filepath = "./config/invoice2.zip";
67 | var filepath = "./config/invoice.png";
68 | if (!(fs.existsSync(filepath))) return;
69 | var q: SaveFileMessage = new SaveFileMessage();
70 | (q as any).compressed = true;
71 | q.filename = "zlib" + path.basename(filepath);
72 | q.file = Buffer.from(pako.deflate(fs.readFileSync(filepath, null))).toString("base64");
73 | var msg = new Message(); msg.jwt = testConfig.userToken;
74 | msg.data = JSON.stringify(q);
75 | await msg.EnsureJWT(null, false)
76 | await msg.SaveFile(null);
77 | q = JSON.parse(msg.data);
78 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
79 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
80 | assert.ok(!Util.IsNullUndefinded(q.result), "no result");
81 | return q.result;
82 | }
83 | @timeout(5000)
84 | async "Create update and delete test work item queue"() {
85 | var exists = await this.GetItem("test queue")
86 | if (exists) {
87 | await this["delete test work item queue"](null);
88 | }
89 | await this["Create work item queue"](null)
90 | exists = await this.GetItem("test queue")
91 | assert.ok(!Util.IsNullUndefinded(exists), "work item queue not found after creation");
92 | await this["update test work item queue"](null);
93 |
94 | await this["delete test work item queue"](null);
95 | }
96 | @timeout(15000)
97 | @test async "Workwith work item"() {
98 | await this["Create update and delete test work item queue"]();
99 | var wiq = await this.GetItem("test queue")
100 | if (!wiq) {
101 | wiq = await this["Create work item queue"]("test queue")
102 | }
103 | var wi: any = { "_id": "62488f88bf045a7e58228f2f", files: [] }
104 |
105 | wi = await this["Create work item"](wiq);
106 | wi = await this["Update work item"](wi);
107 |
108 |
109 |
110 | var q: any = new PopWorkitemMessage();
111 | var msg = new Message(); msg.jwt = testConfig.userToken;
112 | q.wiqid = wiq._id; q.wiq = wiq.name;
113 | msg.data = JSON.stringify(q);
114 | await msg.EnsureJWT(null, false)
115 | await msg.PopWorkitem(null);
116 | q = JSON.parse(msg.data);
117 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
118 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
119 | assert.ok(!Util.IsNullUndefinded(q.result), "no result");
120 | wi = q.result
121 |
122 |
123 |
124 | await this["Delete work item"](wi);
125 | }
126 | @timeout(5000)
127 | async "Delete work item"(wi) {
128 | var q: any = new DeleteWorkitemMessage();
129 | var msg = new Message(); msg.jwt = testConfig.userToken;
130 | q._id = wi._id;
131 | msg.data = JSON.stringify(q);
132 | await msg.EnsureJWT(null, false)
133 | await msg.DeleteWorkitem(null);
134 | q = JSON.parse(msg.data);
135 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
136 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
137 | }
138 | @timeout(5000)
139 | async "Update work item"(wi) {
140 | var q: any = new UpdateWorkitemMessage();
141 | var msg = new Message(); msg.jwt = testConfig.userToken;
142 | q._id = wi._id;
143 | q.files = [];
144 |
145 | var filepath = "./config/invoice.pdf";
146 | if (fs.existsSync(filepath)) {
147 | var f = {
148 | compressed: true, filename: path.basename(filepath),
149 | file: Buffer.from(pako.deflate(fs.readFileSync(filepath, null))).toString("base64")
150 | }
151 | q.files.push(f);
152 | }
153 | msg.data = JSON.stringify(q);
154 | await msg.EnsureJWT(null, false)
155 | await msg.UpdateWorkitem(null);
156 | q = JSON.parse(msg.data);
157 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
158 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
159 | assert.ok(!Util.IsNullUndefinded(q.result), "no result");
160 | return q.result;
161 | }
162 | @timeout(5000)
163 | async "Create work item"(wiq) {
164 | var q: any = new AddWorkitemMessage();
165 | var msg = new Message(); msg.jwt = testConfig.userToken;
166 | q.wiq = wiq.name;
167 | q.wiqid = wiq._id;
168 | q.files = [];
169 | var filepath = "./config/invoice2.pdf";
170 | if (fs.existsSync(filepath)) {
171 | var f = {
172 | compressed: true, filename: path.basename(filepath),
173 | file: Buffer.from(pako.deflate(fs.readFileSync(filepath, null))).toString("base64")
174 | }
175 | q.files.push(f);
176 | }
177 | var filepath = "./config/invoice.png";
178 | if (fs.existsSync(filepath)) {
179 | var f2 = {
180 | compressed: false, filename: path.basename(filepath),
181 | file: fs.readFileSync(filepath, { encoding: "base64" })
182 | }
183 | q.files.push(f2);
184 | }
185 | msg.data = JSON.stringify(q);
186 | await msg.EnsureJWT(null, false)
187 | await msg.AddWorkitem(null);
188 | q = JSON.parse(msg.data);
189 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
190 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
191 | assert.ok(!Util.IsNullUndefinded(q.result), "no result");
192 | return q.result;
193 | }
194 |
195 | @timeout(50000)
196 | async "Create work item queue"(name) {
197 | var q: any = new AddWorkitemQueueMessage();
198 | q.maxretries = 3; q.retrydelay = 0; q.initialdelay = 0;
199 | var msg = new Message(); msg.jwt = testConfig.userToken;
200 | q.name = name ? name : "test queue"
201 | msg.data = JSON.stringify(q);
202 | await msg.EnsureJWT(null, false)
203 | await msg.AddWorkitemQueue(null, null);
204 | q = JSON.parse(msg.data);
205 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
206 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
207 | assert.ok(!Util.IsNullUndefinded(q.result), "no result");
208 | return q.result;
209 | }
210 | @timeout(50000)
211 | async "update test work item queue"(name) {
212 | var q: any = new UpdateWorkitemQueueMessage();
213 | var msg = new Message(); msg.jwt = testConfig.userToken;
214 | q.name = name ? name : "test queue"
215 | msg.data = JSON.stringify(q);
216 | await msg.EnsureJWT(null, false)
217 | await msg.UpdateWorkitemQueue(null);
218 | q = JSON.parse(msg.data);
219 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
220 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
221 | assert.ok(!Util.IsNullUndefinded(q.result), "no result");
222 | return q.result;
223 | }
224 | @timeout(50000)
225 | async "delete test work item queue"(name) {
226 | var q: any = new DeleteWorkitemQueueMessage();
227 | var msg = new Message(); msg.jwt = testConfig.userToken;
228 | q.name = name ? name : "test queue";
229 | q.purge = true;
230 | msg.data = JSON.stringify(q);
231 | await msg.EnsureJWT(null, false)
232 | await msg.DeleteWorkitemQueue(null);
233 | q = JSON.parse(msg.data);
234 | assert.ok(!Util.IsNullUndefinded(q), "msg data missing");
235 | assert.ok(Util.IsNullUndefinded(q.error), q.error);
236 | }
237 |
238 | }
239 | // clear && ./node_modules/.bin/_mocha "src/test/workitemqueue-messages.test.ts"
--------------------------------------------------------------------------------
/src/Crypt.ts:
--------------------------------------------------------------------------------
1 | import { Span } from "@opentelemetry/api";
2 | import bcrypt from "bcryptjs";
3 | import crypto from "crypto";
4 | import jsonwebtoken from "jsonwebtoken";
5 | import { Config } from "./Config.js";
6 | import { Logger } from "./Logger.js";
7 | import { WebSocketServerClient } from "./WebSocketServerClient.js";
8 | import { Util, Wellknown } from "./Util.js";
9 | import { Rolemember, TokenUser, User } from "./commoninterfaces.js";
10 | export class Crypt {
11 | static encryption_key: string = null; // must be 256 bytes (32 characters))
12 | static iv_length: number = 16; // for AES, this is always 16
13 | static bcrypt_salt_rounds: number = 12;
14 | static rootUser(): User {
15 | const result: User = new User();
16 | result._type = "user"; result.name = Wellknown.root.name; result.username = Wellknown.root.name; result._id = Wellknown.root._id;
17 | result.roles = []; result.roles.push(new Rolemember(Wellknown.admins.name, Wellknown.admins._id));
18 | return result;
19 | }
20 | static async guestUser(): Promise {
21 | let result: User = new User();
22 | result.validated = true;
23 | result.formvalidated = true;
24 | result.emailvalidated = true;
25 | result._type = "user"; result.name = Wellknown.guest.name; result.username = Wellknown.guest.name; result._id = Wellknown.guest._id;
26 | result.roles = [];
27 | Logger.instanse.verbose(`Decorating guest user with roles`, null, { cls: "Crypt", func: "guestUser" });
28 | result = await Logger.DBHelper.DecorateWithRoles(result, null);
29 | return result;
30 | }
31 | static rootToken(): string {
32 | return Crypt.createToken(this.rootUser(), Config.shorttoken_expires_in);
33 | }
34 | public static async SetPassword(user: User, password: string, parent: Span): Promise {
35 | const span: Span = Logger.otel.startSubSpan("Crypt.SetPassword", parent);
36 | try {
37 | if (Util.IsNullUndefinded(user)) throw new Error("user is mandatody")
38 | if (Util.IsNullEmpty(password)) throw new Error("password is mandatody")
39 | user.passwordhash = await Crypt.hash(password);
40 | if (!(this.ValidatePassword(user, password, span))) { throw new Error("Failed validating password after hasing"); }
41 | } finally {
42 | Logger.otel.endSpan(span);
43 | }
44 | }
45 | public static GetUniqueIdentifier(length: number = 16): string {
46 | return crypto.randomBytes(16).toString("hex")
47 | }
48 | public static async ValidatePassword(user: User, password: string, parent: Span): Promise {
49 | const span: Span = Logger.otel.startSubSpan("Crypt.ValidatePassword", parent);
50 | try {
51 | if (Util.IsNullUndefinded(user)) throw new Error("user is mandatody")
52 | if (Config.enable_guest && user.username == "guest") return true;
53 | if (Util.IsNullEmpty(password)) throw new Error("password is mandatody")
54 | return await Crypt.compare(password, user.passwordhash, span);
55 | } finally {
56 | Logger.otel.endSpan(span);
57 | }
58 | }
59 | static encrypt(text: string): string {
60 | let iv: Buffer = crypto.randomBytes(Crypt.iv_length);
61 | if (Util.IsNullEmpty(Crypt.encryption_key)) Crypt.encryption_key = Config.aes_secret.substring(0, 32);
62 | let cipher: crypto.CipherGCM = crypto.createCipheriv("aes-256-gcm", Buffer.from(Crypt.encryption_key), iv);
63 | let encrypted: Buffer = cipher.update((text as any));
64 | encrypted = Buffer.concat([encrypted, cipher.final()]);
65 | const authTag = cipher.getAuthTag()
66 | return iv.toString("hex") + ":" + encrypted.toString("hex") + ":" + authTag.toString("hex");
67 | }
68 | static decrypt(text: string): string {
69 | let textParts: string[] = text.split(":");
70 | let iv: Buffer = Buffer.from(textParts.shift(), "hex");
71 | let encryptedText: Buffer = Buffer.from(textParts.shift(), "hex");
72 | let authTag: Buffer = null;
73 | if (textParts.length > 0) authTag = Buffer.from(textParts.shift(), "hex");
74 | let decrypted: Buffer;
75 | if (Util.IsNullEmpty(Crypt.encryption_key)) Crypt.encryption_key = Config.aes_secret.substring(0, 32);
76 | if (authTag != null) {
77 | let decipher: crypto.DecipherGCM = crypto.createDecipheriv("aes-256-gcm", Buffer.from(Crypt.encryption_key), iv);
78 | decipher.setAuthTag(authTag);
79 | decrypted = decipher.update(encryptedText);
80 | decrypted = Buffer.concat([decrypted, decipher.final()]);
81 | } else {
82 | let decipher2: crypto.Decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(Crypt.encryption_key), iv);
83 | decrypted = decipher2.update(encryptedText);
84 | decrypted = Buffer.concat([decrypted, decipher2.final()]);
85 | }
86 | return decrypted.toString();
87 | }
88 | static async hash(password: string): Promise {
89 | return new Promise(async (resolve, reject) => {
90 | try {
91 | bcrypt.hash(password, Crypt.bcrypt_salt_rounds, async (error, hash) => {
92 | if (error) { return reject(error); }
93 | resolve(hash);
94 | });
95 | } catch (error) {
96 | reject(error);
97 | }
98 | });
99 | }
100 | static async compare(password: string, passwordhash: string, parent: Span): Promise {
101 | const span: Span = Logger.otel.startSubSpan("Crypt.compare", parent);
102 | return new Promise((resolve, reject) => {
103 | try {
104 | if (Util.IsNullEmpty(password)) { return reject("Password cannot be empty"); }
105 | if (Util.IsNullEmpty(passwordhash)) { return reject("Passwordhash cannot be empty"); }
106 | bcrypt.compare(password, passwordhash, (error, res) => {
107 | if (error) { Logger.otel.endSpan(span); return reject(error); }
108 | Logger.otel.endSpan(span);
109 | resolve(res);
110 | });
111 | } catch (error) {
112 | reject(error);
113 | Logger.otel.endSpan(span);
114 | }
115 | });
116 | }
117 | static createSlimToken(id: string, impostor: string, expiresIn: string): string {
118 | if (Util.IsNullEmpty(id)) throw new Error("id is mandatory");
119 | if (Util.IsNullEmpty(Crypt.encryption_key)) Crypt.encryption_key = Config.aes_secret.substring(0, 32);
120 | const key = Crypt.encryption_key;
121 | if (Util.IsNullEmpty(Config.aes_secret)) throw new Error("Config missing aes_secret");
122 | if (Util.IsNullEmpty(key)) throw new Error("Config missing aes_secret");
123 | const user = { _id: id, impostor: impostor }
124 | return jsonwebtoken.sign({ data: user }, key,
125 | { expiresIn: expiresIn }); // 60 (seconds), "2 days", "10h", "7d"
126 | }
127 | static createToken(item: User | TokenUser, expiresIn: string): string {
128 | const user: TokenUser = new TokenUser();
129 | user._type = (item as User)._type;
130 | user._id = item._id;
131 | user.impostor = (item as TokenUser).impostor;
132 | user.name = item.name;
133 | user.username = item.username;
134 | user.roles = item.roles;
135 | user.customerid = item.customerid;
136 | user.selectedcustomerid = item.selectedcustomerid;
137 | user.dblocked = item.dblocked;
138 |
139 | if (Util.IsNullEmpty(Crypt.encryption_key)) Crypt.encryption_key = Config.aes_secret.substring(0, 32);
140 | const key = Crypt.encryption_key;
141 | if (Util.IsNullEmpty(Config.aes_secret)) throw new Error("Config missing aes_secret");
142 | if (Util.IsNullEmpty(key)) throw new Error("Config missing aes_secret");
143 | return jsonwebtoken.sign({ data: user }, key,
144 | { expiresIn: expiresIn }); // 60 (seconds), "2 days", "10h", "7d"
145 | }
146 | static async verityToken(token: string, cli?: WebSocketServerClient, ignoreExpiration: boolean = false): Promise {
147 | try {
148 | if (Util.IsNullEmpty(token)) {
149 | throw new Error("jwt must be provided");
150 | }
151 | if (Util.IsNullEmpty(Crypt.encryption_key)) Crypt.encryption_key = Config.aes_secret.substring(0, 32);
152 | if (Config.allow_signin_with_expired_jwt == false) ignoreExpiration = false;
153 | const o: any = jsonwebtoken.verify(token, Crypt.encryption_key, { ignoreExpiration: ignoreExpiration });
154 | let impostor: string = null;
155 | if (!Util.IsNullUndefinded(o) && !Util.IsNullUndefinded(o.data) && !Util.IsNullEmpty(o.data._id)) {
156 | if (!Util.IsNullEmpty(o.data.impostor)) {
157 | impostor = o.data.impostor;
158 | }
159 | }
160 | if (!Util.IsNullUndefinded(o) && !Util.IsNullUndefinded(o.data) && !Util.IsNullEmpty(o.data._id) && o.data._id != Wellknown.root._id) {
161 | var id = o.data._id;
162 | o.data = await Logger.DBHelper.FindById(o.data._id, null);
163 | if (Util.IsNullUndefinded(o) || Util.IsNullUndefinded(o.data)) {
164 | throw new Error("Token signature valid, but unable to find user with id " + id);
165 | }
166 | }
167 | if (!Util.IsNullEmpty(impostor)) o.data.impostor = impostor;
168 | return TokenUser.assign(o.data);
169 | } catch (error) {
170 | var e = error;
171 | try {
172 | if (!Util.IsNullEmpty(token)) {
173 | const o: any = jsonwebtoken.verify(token, Crypt.encryption_key, { ignoreExpiration: true });
174 | if (!Util.IsNullUndefinded(o) && !Util.IsNullUndefinded(o.data) && !Util.IsNullEmpty(o.data._id)) {
175 | // fake login, so we can track who was trying to login with an expired token
176 | if (cli != null && cli.user == null) {
177 | cli.user = TokenUser.assign(o.data);
178 | cli.username = cli.user?.username;
179 | }
180 | e = new Error(error.message + " for token with exp " + o.exp + " for " + o.data.name + " username: " + o.data.username + " and id: " + o.data._id);
181 | }
182 | }
183 | } catch (error) {
184 | }
185 | throw e
186 | }
187 | }
188 | static decryptToken(token: string): any {
189 | if (Util.IsNullEmpty(Crypt.encryption_key)) Crypt.encryption_key = Config.aes_secret.substring(0, 32);
190 | return jsonwebtoken.verify(token, Crypt.encryption_key, { ignoreExpiration: Config.allow_signin_with_expired_jwt });
191 | }
192 | }
--------------------------------------------------------------------------------
/src/SamlProvider.ts:
--------------------------------------------------------------------------------
1 | import { Span } from "@opentelemetry/api";
2 | import express from "express";
3 | import samlp from "samlp";
4 | import { Audit } from "./Audit.js";
5 | import { Config } from "./Config.js";
6 | import { Logger } from "./Logger.js";
7 | import { Util } from "./Util.js";
8 | import { User } from "./commoninterfaces.js";
9 |
10 | export class SamlProvider {
11 | public static profileMapper(pu: any): any {
12 | return {
13 | pu: pu,
14 | getClaims: function (): any {
15 | const claims: any = {};
16 | const k: string[] = Object.keys(this.pu);
17 | k.forEach(key => {
18 | if (key.indexOf("http://") === 0) {
19 | claims[key] = this.pu[key];
20 | } else {
21 | switch (key) {
22 | case "id":
23 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"] = this.pu[key]; break;
24 | case "displayName":
25 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"] = this.pu[key]; break;
26 | case "name":
27 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"] = this.pu[key]; break;
28 | case "mobile":
29 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobile"] = this.pu[key]; break;
30 | case "username":
31 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"] = this.pu[key]; break;
32 | case "emails":
33 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"] = this.pu[key][0];
34 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"] = this.pu[key][0]; break;
35 | case "roles":
36 | const roles: string[] = [];
37 | this.pu[key].forEach(role => {
38 | roles.push(role.name);
39 | });
40 | claims["http://schemas.xmlsoap.org/claims/Group"] = roles;
41 | }
42 | }
43 | });
44 | return claims;
45 | },
46 | getNameIdentifier: function (): any {
47 | const claims: any = this.getClaims();
48 | return {
49 | nameIdentifier: claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"] ||
50 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"] ||
51 | claims["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"]
52 | };
53 | }
54 | };
55 | }
56 | public static remoteip(req: express.Request) {
57 | let remoteip: string = req.socket.remoteAddress;
58 | if (req.headers["X-Forwarded-For"] != null) remoteip = req.headers["X-Forwarded-For"] as string;
59 | if (req.headers["X-real-IP"] != null) remoteip = req.headers["X-real-IP"] as string;
60 | if (req.headers["x-forwarded-for"] != null) remoteip = req.headers["x-forwarded-for"] as string;
61 | if (req.headers["x-real-ip"] != null) remoteip = req.headers["x-real-ip"] as string;
62 | return remoteip;
63 | }
64 |
65 | static configure(app: express.Express, baseurl: string): void {
66 | const cert: string = Buffer.from(Config.signing_crt, "base64").toString("ascii");
67 | const key: string = Buffer.from(Config.singing_key, "base64").toString("ascii");
68 |
69 | if (cert != null && cert != "") {
70 | let saml_issuer: string = Config.saml_issuer;
71 | if (saml_issuer == null || saml_issuer == "") saml_issuer = "uri:" + Config.domain;
72 | const samlpoptions: any = {
73 | issuer: saml_issuer,
74 | cert: cert,
75 | key: key,
76 | getPostURL: (wtrealm: any, wreply: any, req: any, callback: any) => {
77 | (async () => {
78 | if (typeof wreply === "object") {
79 | wreply = wreply.documentElement.getAttribute("AssertionConsumerServiceURL");
80 | }
81 | return callback(null, wreply);
82 | })();
83 |
84 | },
85 | getUserFromRequest: (req: any) => {
86 | const span: Span = Logger.otel.startSpanExpress("SAML.getUserFromRequest", req);
87 | try {
88 | const tuser: User = req.user;
89 | const remoteip = SamlProvider.remoteip(req);
90 | span?.setAttribute("remoteip", remoteip);
91 | Audit.LoginSuccess(tuser, "tokenissued", "saml", remoteip, "unknown", "unknown", span).catch((e) => {
92 | Logger.instanse.error(e, span, {cls: "SamlProvider", func: "getUserFromRequest"});
93 | });
94 | } catch (error) {
95 | Logger.instanse.error(error, span, {cls: "SamlProvider", func: "getUserFromRequest"});
96 | } finally {
97 | Logger.otel.endSpan(span);
98 | }
99 | return req.user;
100 | },
101 | profileMapper: SamlProvider.profileMapper,
102 | lifetimeInSeconds: (3600 * 24)
103 | };
104 |
105 | app.get("/issue/", (req: any, res: any, next: any): void => {
106 | if (req.query.SAMLRequest !== undefined && req.query.SAMLRequest !== null) {
107 | if ((req.user === undefined || req.user === null)) {
108 | try {
109 | samlp.parseRequest(req, samlpoptions, async (_err: any, samlRequestDom: any): Promise => {
110 | try {
111 | res.cookie("originalUrl", req.originalUrl, { maxAge: 900000, httpOnly: true });
112 | } catch (error) {
113 | }
114 | res.redirect("/");
115 | });
116 | } catch (error) {
117 | res.body(error.message ? error.message : error);
118 | res.end();
119 | Logger.instanse.error(error, null, {cls: "SamlProvider", func: "app.get(/issue/)"});
120 | }
121 | } else {
122 | // continue with issuing token using samlp
123 | next();
124 | }
125 | } else {
126 | res.send("Please login again");
127 | res.end();
128 | }
129 | });
130 |
131 | try {
132 | app.get("/issue/", samlp.auth(samlpoptions));
133 | app.get("/issue/FederationMetadata/2007-06/FederationMetadata.xml", samlp.metadata({
134 | issuer: saml_issuer,
135 | cert: cert,
136 | }));
137 | } catch (error) {
138 | Logger.instanse.error(error, null, {cls: "SamlProvider", func: "app.get(/issue/)"});
139 | }
140 | // TODO: FIX !!!!
141 | app.get("/wssignout", async (req: any, res: any, next: any) => {
142 | req.logout();
143 | let html = "";
144 | html += "Du er nu logget ud
";
145 | html += "";
146 | res.send(html);
147 | });
148 | app.post("/wssignout", async (req: any, res: any, next: any) => {
149 | req.logout();
150 | let html = "";
151 | html += "Du er nu logget ud
";
152 | html += "";
153 | res.send(html);
154 | });
155 | } else {
156 | Logger.instanse.warn("SAML signing certificate is not configured, saml not possible", null, {cls: "SamlProvider", func: "configure"});
157 | }
158 | app.get("/logout", async (req: any, res: any, next: any) => {
159 | const referer: string = req.headers.referer;
160 | const providerid: any = req.cookies.provider;
161 | req.logout();
162 |
163 | if (!Util.IsNullEmpty(providerid)) {
164 | var providers = await Logger.DBHelper.GetProviders(null);
165 | const p = providers.filter(x => x.id == providerid);
166 | if (p.length > 0) {
167 | const provider = p[0];
168 | if (!Util.IsNullEmpty(provider.saml_signout_url)) {
169 | let html = "";
170 | html += "Logud
";
171 | if (!Util.IsNullEmpty(referer)) {
172 | html += `
Til login
`;
173 | } else {
174 | html += `
Til login
`;
175 | }
176 | html += ``;
177 | if (!Util.IsNullEmpty(referer)) {
178 | html += `
Til login
`;
179 | } else {
180 | html += `
Til login
`;
181 | }
182 | html += "";
183 | res.send(html);
184 | return;
185 | }
186 | }
187 | }
188 | if (!Util.IsNullEmpty(referer)) {
189 | res.redirect(referer);
190 | } else {
191 | res.redirect("/");
192 | }
193 | });
194 | app.post("/logout", (req: any, res: any, next: any): void => {
195 | if (cert != null && cert != "") {
196 | let saml_issuer: string = Config.saml_issuer;
197 | if (saml_issuer == null || saml_issuer == "") saml_issuer = "uri:" + Config.domain;
198 | samlp.logout({
199 | issuer: saml_issuer,
200 | protocolBinding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
201 | cert: cert,
202 | key: key
203 | })(req, res, next);
204 | } else {
205 | req.logout();
206 | res.redirect("/");
207 | }
208 | });
209 |
210 | }
211 | }
--------------------------------------------------------------------------------
/src/Auth.ts:
--------------------------------------------------------------------------------
1 | import { Span } from "@opentelemetry/api";
2 | import os from "os";
3 | import { Config } from "./Config.js";
4 | import { Crypt } from "./Crypt.js";
5 | import { Logger } from "./Logger.js";
6 | import { LoginProvider } from "./LoginProvider.js";
7 | import { OAuthProvider } from "./OAuthProvider.js";
8 | import { Wellknown } from "./Util.js";
9 | import { FederationId, TokenUser, User } from "./commoninterfaces.js";
10 | export class Auth {
11 | public static async ValidateByPassword(username: string, password: string, parent: Span): Promise {
12 | const span: Span = Logger.otel.startSubSpan("Auth.ValidateByPassword", parent);
13 | try {
14 | if (username === null || username === undefined || username === "") { throw new Error("Username cannot be null"); }
15 | span?.setAttribute("username", username);
16 | if (Config.enable_guest && username == "guest") {
17 | return await Crypt.guestUser();
18 | }
19 | if (password === null || password === undefined || password === "") { throw new Error("Password cannot be null"); }
20 | const user: User = await Logger.DBHelper.FindByUsername(username, null, span);
21 | if (user === null || user === undefined) { return null; }
22 | if ((await Crypt.compare(password, user.passwordhash, span)) !== true) { return null; }
23 | return user;
24 | } finally {
25 | Logger.otel.endSpan(span);
26 | }
27 | }
28 | public static async RefreshUser(user: User, impostor: string, parent: Span) {
29 | let result = await Logger.DBHelper.FindById(user._id, parent)
30 | if (result == null) {
31 | throw new Error("User " + user._id + " not found");
32 | }
33 | // Assign to ensure overload functions are available
34 | result = User.assign(result);
35 | if (impostor != null && impostor != "") {
36 | (user as any).impostor = impostor;
37 | }
38 | return result;
39 | }
40 | public static async Token2User(jwt: string, parent: Span) {
41 | let tuser: TokenUser = null;
42 | let user: User = null;
43 | let impostor: string = undefined;
44 | if (jwt == null || jwt.trim() == "") {
45 | if (Config.enable_guest == true) {
46 | // Assign to ensure overload functions are available
47 | user = User.assign(await Crypt.guestUser());
48 | return user;
49 | }
50 | throw new Error("Empty token is not valid");
51 | }
52 | if (jwt.indexOf(" ") > 1 && (jwt.toLowerCase().startsWith("bearer") || jwt.toLowerCase().startsWith("jwt"))) {
53 | const token = jwt.split(" ")[1].toString();
54 | jwt = token;
55 | } else if (jwt.indexOf(" ") > 1 && jwt.toLowerCase().startsWith("basic")) {
56 | jwt = jwt.split(" ")[1].toString() || "";
57 | user = await Logger.DBHelper.FindJWT(jwt, parent);
58 | if (user == null) {
59 | const [login, password] = Buffer.from(jwt, "base64").toString().split(":")
60 | if (login != null && login != "" && password != null && password != "") {
61 | user = await Auth.ValidateByPassword(login, password, parent);
62 | }
63 | }
64 | }
65 | // Valid JWT token ?
66 | if (tuser == null && user == null) {
67 | try {
68 | tuser = await Crypt.verityToken(jwt);
69 | if (tuser != null) {
70 | impostor = tuser.impostor;
71 | }
72 | } catch (error) {
73 | }
74 | }
75 | // cached ?
76 | if (user == null) {
77 | user = await Logger.DBHelper.FindJWT(jwt, parent)
78 | if (user != null) {
79 | user = User.assign(user);
80 | (user as any).impostor = impostor;
81 | return Auth.RefreshUser(user, impostor, parent);
82 | }
83 | }
84 | // if root, pass through to avoid circular calls
85 | if (tuser != null && tuser._id == Wellknown.root._id) {
86 | // Assign to ensure overload functions are available
87 | user = User.assign(Crypt.rootUser());
88 | return user;
89 | }
90 | // if guest, pass through to avoid circular calls
91 | if (tuser != null && tuser._id == Wellknown.guest._id) {
92 | if (Config.enable_guest == true) {
93 | // Assign to ensure overload functions are available
94 | user = User.assign(await Crypt.guestUser());
95 | return user;
96 | } else {
97 | throw new Error("Guest user is not enabled");
98 | }
99 | }
100 | // Valid SAML token ?
101 | if (tuser == null && user == null) {
102 | try {
103 | if (jwt.indexOf("saml") > 0) { // valid.email.endsWith(x)).length > 0) { createUser = true; }
165 | if (createUser == true && _user == null) {
166 | _user = new User();
167 | _user.name = valid.name;
168 | _user.email = valid.email.toLowerCase();
169 | _user.username = valid.email.toLowerCase();
170 | let extraoptions = {
171 | federationids: [new FederationId(valid.sub, valid.iss)],
172 | emailvalidated: true,
173 | formvalidated: true, // TODO: Is this an security issue?
174 | validated: true
175 | }
176 | _user = await Logger.DBHelper.EnsureUser(Crypt.rootToken(), _user.name, _user.username, null, null, extraoptions, parent);
177 | }
178 | if (_user != null) {
179 |
180 | if (os.hostname().toLowerCase() != "nixos") {
181 | Logger.DBHelper.AddJWT(jwt, _user, parent);
182 | }
183 | return Auth.RefreshUser(_user, impostor, parent);
184 | }
185 |
186 | }
187 | } catch (error) {
188 | Logger.instanse.error("Auth.Token2User", error)
189 | }
190 | }
191 | }
192 | return null;
193 | }
194 | // Look up user
195 | if (user == null) user = await Logger.DBHelper.FindById(tuser._id, parent)
196 | if (user == null) {
197 | throw new Error("User " + tuser._id + " not found");
198 | }
199 | await Logger.DBHelper.AddJWT(jwt, user, parent);
200 | if (user._id == Wellknown.guest._id && Config.enable_guest == false) {
201 | throw new Error("Guest user is not enabled");
202 | }
203 | user = await Auth.RefreshUser(user, impostor, parent);
204 | return user;
205 | }
206 | public static async Id2Token(id: string, impostor: string, expiresIn: string, parent: Span) {
207 | if (id == Wellknown.guest._id && Config.enable_guest == false) {
208 | throw new Error("Guest user is not enabled");
209 | }
210 | const jwt = await Crypt.createSlimToken(id, impostor, expiresIn)
211 | return jwt;
212 | }
213 | public static async User2Token(item: User | TokenUser, expiresIn: string, parent: Span) {
214 | if (item._id == Wellknown.guest._id && Config.enable_guest == false) {
215 | throw new Error("Guest user is not enabled");
216 | }
217 | if (item instanceof TokenUser) {
218 | return Crypt.createSlimToken(item._id, item.impostor, expiresIn)
219 | } else {
220 | return Crypt.createSlimToken(item._id, (item as any).impostor, expiresIn)
221 | }
222 | }
223 | }
224 |
--------------------------------------------------------------------------------
/src/test/DatabaseConnection.test.ts:
--------------------------------------------------------------------------------
1 | import { suite, test, timeout } from "@testdeck/mocha";
2 | import assert from "assert";
3 | import { Base } from "../commoninterfaces.js";
4 | import { Config } from "../Config.js";
5 | import { Crypt } from "../Crypt.js";
6 | import { Util, Wellknown } from "../Util.js";
7 | import { testConfig } from "./testConfig.js";
8 |
9 | @suite class databaseConnection_test {
10 | @timeout(50000)
11 | async before() {
12 | await testConfig.configure();
13 | }
14 | async after() {
15 | await testConfig.cleanup();
16 | }
17 | @timeout(5000)
18 | @test async "indextest"() {
19 | // await Config.db.ensureindexes(null)
20 | let indexes = await Config.db.db.collection("entities").indexes();
21 | let indexnames = indexes.map(x => x.name);
22 | if (indexnames.indexOf("test_index") !== -1) {
23 | await Config.db.deleteIndex("entities", "test_index", null);
24 | }
25 | await Config.db.createIndex("entities", "test_index", { "myname": 1 }, null, null);
26 | indexes = await Config.db.db.collection("entities").indexes();
27 | indexnames = indexes.map(x => x.name);
28 | assert.notStrictEqual(indexnames.indexOf("test_index"), -1, "test_index not found after being created");
29 | await Config.db.deleteIndex("entities", "test_index", null);
30 | indexes = await Config.db.db.collection("entities").indexes();
31 | indexnames = indexes.map(x => x.name);
32 | assert.strictEqual(indexnames.indexOf("test_index"), -1, "test_index was found after being deleted");
33 |
34 | }
35 |
36 | @test async "ListCollections"() {
37 | var rootcollections = await Config.db.ListCollections(false, Crypt.rootToken());
38 | rootcollections = rootcollections.filter(x => x.name.indexOf("system.") === -1);
39 | assert.notDeepStrictEqual(rootcollections, null);
40 | assert.notDeepStrictEqual(rootcollections.length, 0);
41 | }
42 | @test async "DropCollections"() {
43 | const colname = "testcollection"
44 | var rootcollections = await Config.db.ListCollections(false, Crypt.rootToken());
45 | rootcollections = rootcollections.filter(x => x.name.indexOf("system.") === -1);
46 | assert.notDeepStrictEqual(rootcollections, null);
47 | assert.notDeepStrictEqual(rootcollections.length, 0);
48 | var exists = rootcollections.filter(x => x.name == colname);
49 | if (exists.length > 0) {
50 | await Config.db.DropCollection(colname, Crypt.rootToken(), null);
51 | }
52 | var item = new Base(); item.name = "test item";
53 | await Config.db.InsertOne(item, colname, 1, true, Crypt.rootToken(), null);
54 |
55 | var rootcollections = await Config.db.ListCollections(false, Crypt.rootToken());
56 | exists = rootcollections.filter(x => x.name == colname);
57 | assert.notDeepStrictEqual(exists.length, 0);
58 | await Config.db.DropCollection(colname, Crypt.rootToken(), null);
59 | }
60 | @test async "query"() {
61 | var items = await Config.db.query({ collectionname: "users", query: {}, top: 5, jwt: Crypt.rootToken() }, null);
62 | assert.notDeepStrictEqual(items, null);
63 | assert.strictEqual(items.length, 5);
64 | items = await Config.db.query({ collectionname: "users", query: { "_type": "role" }, top: 5, jwt: Crypt.rootToken() }, null);
65 | for (var item of items) {
66 | assert.strictEqual(item._type, "role");
67 | }
68 | var ids = items.map(x => x._id);
69 | items = await Config.db.query({ collectionname: "users", query: { "_type": "role" }, top: 5, skip: 5, jwt: Crypt.rootToken() }, null);
70 | assert.strictEqual(items.length, 5);
71 | for (var item of items) {
72 | assert.strictEqual(ids.indexOf(item._id), -1, "Got id that should have been skipped!");
73 | }
74 | items = await Config.db.query({ collectionname: "users", query: { "_type": "role" }, projection: { "_id": 1, "name": 1 }, top: 5, skip: 5, jwt: Crypt.rootToken() }, null);
75 | for (var item of items) {
76 | assert.strictEqual(item._acl, undefined, "Projection failed for _acl");
77 | assert.strictEqual(item._type, undefined, "Projection failed for _type");
78 | }
79 |
80 | items = await Config.db.query({ collectionname: "users", query: { "_id": Wellknown.admins._id }, top: 5, jwt: Crypt.rootToken() }, null);
81 | assert.strictEqual(items.length, 1, "Root cannot see admins role!");
82 |
83 | items = await Config.db.query({ collectionname: "users", query: { "_id": Wellknown.admins._id }, top: 5, jwt: Crypt.rootToken(), queryas: testConfig.testUser._id }, null);
84 | assert.strictEqual(items.length, 0, "demouser should not be able to see admins role!");
85 |
86 | items = await Config.db.query({ collectionname: "users", query: { "_id": Wellknown.admins._id }, top: 5, jwt: testConfig.userToken }, null);
87 | assert.strictEqual(items.length, 0, "demouser should not be able to see admins role!");
88 |
89 | items = await Config.db.query({ collectionname: "users", query: { "_id": Wellknown.admins._id }, top: 5, jwt: testConfig.userToken, queryas: Wellknown.root._id }, null);
90 | assert.strictEqual(items.length, 0, "demouser should not be able to see admins role!");
91 |
92 | items = await Config.db.query({ collectionname: "files", query: {}, top: 5, jwt: Crypt.rootToken() }, null);
93 | assert.strictEqual(items.length, 5, "Root did not find any files");
94 | }
95 | @timeout(5000)
96 | @test async "count"() {
97 | var usercount = await Config.db.count({ collectionname: "users", query: { "_type": "user" }, jwt: Crypt.rootToken() }, null);
98 | assert.notDeepStrictEqual(usercount, null);
99 | assert.notStrictEqual(usercount, 0);
100 | var rolecount = await Config.db.count({ collectionname: "users", query: { "_type": "role" }, jwt: Crypt.rootToken() }, null);
101 | assert.notDeepStrictEqual(rolecount, null);
102 | assert.notStrictEqual(rolecount, 0);
103 | // assert.notStrictEqual(usercount, rolecount);
104 | }
105 | @timeout(5000)
106 | @test async "GetDocumentVersion"() {
107 | let item = new Base(); item.name = "item version 0";
108 | item = await Config.db.InsertOne(item, "entities", 1, true, testConfig.userToken, null);
109 | assert.notDeepStrictEqual(item, null);
110 | assert.strictEqual(Util.IsNullEmpty(item._id), false);
111 | assert.strictEqual(item.name, "item version 0");
112 | assert.strictEqual(item._version, 0);
113 |
114 | await new Promise(resolve => { setTimeout(resolve, 1000) })
115 | item.name = "item version 1";
116 | item = await Config.db.UpdateOne(item, "entities", 1, true, testConfig.userToken, null);
117 | assert.strictEqual(item.name, "item version 1");
118 | assert.strictEqual(item._version, 1);
119 | item.name = "item version 2";
120 | item = await Config.db.UpdateOne(item, "entities", 1, true, testConfig.userToken, null);
121 | assert.strictEqual(item.name, "item version 2");
122 | assert.strictEqual(item._version, 2);
123 | let testitem = await Config.db.GetDocumentVersion({ collectionname: "entities", id: item._id, version: 1, jwt: testConfig.userToken }, null);
124 | assert.strictEqual(testitem.name, "item version 1");
125 | assert.strictEqual(testitem._version, 1);
126 | testitem = await Config.db.GetDocumentVersion({ collectionname: "entities", id: item._id, version: 0, jwt: testConfig.userToken }, null);
127 | assert.strictEqual(testitem.name, "item version 0");
128 | assert.strictEqual(testitem._version, 0);
129 | testitem = await Config.db.GetDocumentVersion({ collectionname: "entities", id: item._id, version: 2, jwt: testConfig.userToken }, null);
130 | assert.strictEqual(testitem.name, "item version 2");
131 | assert.strictEqual(testitem._version, 2);
132 | }
133 | @test async "getbyid"() {
134 | var user = await Config.db.getbyid(testConfig.testUser._id, "users", testConfig.userToken, true, null);
135 | assert.notDeepStrictEqual(user, null);
136 | assert.strictEqual(user._id, testConfig.testUser._id);
137 | user = await Config.db.getbyid(Wellknown.root._id, "users", Crypt.rootToken(), true, null);
138 | assert.notDeepStrictEqual(user, null);
139 | assert.strictEqual(user._id, Wellknown.root._id);
140 | user = await Config.db.getbyid(Wellknown.root._id, "users", testConfig.userToken, true, null);
141 | assert.strictEqual(user, null);
142 | }
143 | @test async "aggregate"() {
144 | var userssize = await Config.db.aggregate([
145 | {
146 | "$project": {
147 | "_modifiedbyid": 1,
148 | "object_size": {
149 | "$bsonSize": "$$ROOT"
150 | }
151 | }
152 | },
153 | {
154 | "$group": {
155 | "_id": "$_modifiedbyid",
156 | "size": {
157 | "$sum": "$object_size"
158 | }
159 | }
160 | }
161 | ], "users", Crypt.rootToken(), null, null, false, null);
162 |
163 | assert.notDeepStrictEqual(userssize, null);
164 | assert.notDeepStrictEqual(userssize.length, 0);
165 | assert.ok(!Util.IsNullEmpty(userssize[0]._id));
166 | assert.ok((userssize[0] as any).size > 0);
167 | }
168 | @timeout(5000)
169 | @test async "Many"() {
170 | await Config.db.DeleteMany({}, null, "entities", null, false, testConfig.userToken, null);
171 | await new Promise(resolve => { setTimeout(resolve, 1000) })
172 | var items = await Config.db.query({ query: {}, collectionname: "entities", top: 100, jwt: testConfig.userToken }, null);
173 | assert.notDeepStrictEqual(items, null);
174 | assert.strictEqual(items.length, 0);
175 | items = [];
176 | for (let i = 0; i < 50; i++) {
177 | let item = new Base(); item.name = "Item " + i;
178 | items.push(item);
179 | }
180 | items = await Config.db.InsertMany(items, "entities", 1, true, testConfig.userToken, null);
181 | assert.notDeepStrictEqual(items, null);
182 | assert.strictEqual(items.length, 50);
183 | assert.strictEqual(items[0].name, "Item 0");
184 | assert.ok(!Util.IsNullEmpty(items[0]._id));
185 | await new Promise(resolve => { setTimeout(resolve, 1000) })
186 | await Config.db.DeleteMany({}, null, "entities", null, false, testConfig.userToken, null);
187 | await new Promise(resolve => { setTimeout(resolve, 1000) })
188 | var items = await Config.db.query({ query: {}, collectionname: "entities", top: 100, jwt: testConfig.userToken }, null);
189 | assert.notDeepStrictEqual(items, null);
190 | assert.strictEqual(items.length, 0);
191 | }
192 | @test async "updatedoc"() {
193 | var item = new Base(); item.name = "test item";
194 | item = await Config.db.InsertOne(item, "entities", 1, true, testConfig.userToken, null);
195 | assert.notDeepStrictEqual(item, null);
196 | assert.strictEqual(item.name, "test item");
197 | assert.ok(!Util.IsNullEmpty(item._id));
198 | assert.strictEqual(item._version, 0);
199 |
200 | let updateDoc = { "$set": { "name": "test item updated" } };
201 | await Config.db.UpdateDocument({ "_id": item._id }, updateDoc as any, "entities", 1, true, testConfig.userToken, null);
202 | await new Promise(resolve => { setTimeout(resolve, 1000) })
203 | item = await Config.db.getbyid(item._id, "entities", testConfig.userToken, true, null);
204 |
205 | assert.notDeepStrictEqual(item, null);
206 | assert.strictEqual(item.name, "test item updated");
207 | assert.ok(!Util.IsNullEmpty(item._id));
208 | assert.strictEqual(item._version, 1);
209 | // await Config.db.DeleteOne(item._id, "entities", false, testConfig.userToken, null);
210 | }
211 | }
212 | // clear && ./node_modules/.bin/_mocha "src/test/DatabaseConnection.test.ts"
213 |
--------------------------------------------------------------------------------
/src/ee/OpenAPIProxy.ts:
--------------------------------------------------------------------------------
1 | import { Span } from "@opentelemetry/api";
2 | import express from "express";
3 | import fs from "fs";
4 | import hjson from "hjson";
5 | import swaggerUi from "swagger-ui-express";
6 | import { Config } from "../Config.js";
7 | import { Crypt } from "../Crypt.js";
8 | import { Logger } from "../Logger.js";
9 | import { WebServer } from "../WebServer.js";
10 | import { amqpwrapper } from "../amqpwrapper.js";
11 | let schema2 = {}
12 | try {
13 | if (fs.existsSync("src/public/swagger.json") == true) {
14 | const json = fs.readFileSync("src/public/swagger.json", "utf8");
15 | schema2 = JSON.parse(json);
16 | } else if (fs.existsSync("../public/swagger.json") == true) {
17 | const json = fs.readFileSync("../public/swagger.json", "utf8");
18 | schema2 = JSON.parse(json);
19 | } else if (fs.existsSync("./public/swagger.json") == true) {
20 | const json = fs.readFileSync("./public/swagger.json", "utf8");
21 | schema2 = JSON.parse(json);
22 | } else if (fs.existsSync("src/public.template/swagger.json") == true) {
23 | const json = fs.readFileSync("src/public.template/swagger.json", "utf8");
24 | schema2 = JSON.parse(json);
25 | } else if (fs.existsSync("../public.template/swagger.json") == true) {
26 | const json = fs.readFileSync("../public.template/swagger.json", "utf8");
27 | schema2 = JSON.parse(json);
28 | } else if (fs.existsSync("./public.template/swagger.json") == true) {
29 | const json = fs.readFileSync("./public.template/swagger.json", "utf8");
30 | schema2 = JSON.parse(json);
31 | } else {
32 | Logger.instanse.warn("swagger.json not found", null, { cls: "OpenAPIProxy", func: "configure" });
33 | console.warn("swagger.json not found");
34 |
35 | }
36 | } catch (error) {
37 | Logger.instanse.error(error, null, { cls: "OpenAPIProxy", func: "configure" });
38 | }
39 |
40 | import { NextFunction } from "express";
41 | import { ValidateError } from "tsoa";
42 | import { Auth } from "../Auth.js";
43 | import { RegisterRoutes } from "./build/routes.js";
44 | import { Util } from "../Util.js";
45 | import { User } from "../commoninterfaces.js";
46 | export class OpenAPIProxy {
47 | public static amqp_promises = [];
48 | public static createPromise = () => {
49 | const correlationId = Util.GetUniqueIdentifier();
50 | var promise = new Promise((resolve, reject) => {
51 | const promise = {
52 | correlationId: correlationId,
53 | resolve: resolve, // Store the resolve function
54 | reject: reject, // Store the reject function
55 | };
56 | OpenAPIProxy.amqp_promises.push(promise);
57 | });
58 | return { correlationId, promise };
59 | }
60 |
61 | public static queue
62 | static async configure(app: express.Express, parent: Span): Promise {
63 |
64 | const openapiuser: User = { "username": "OpenAPI" } as any;
65 | OpenAPIProxy.queue = amqpwrapper.Instance().AddQueueConsumer(openapiuser, "openapi", null, null, async (data: any, options: any, ack: any, done: any) => {
66 | try {
67 | var promise = OpenAPIProxy.amqp_promises.find(x => x.correlationId == options.correlationId);
68 | if (promise != null) {
69 | Logger.instanse.debug("[" + options.correlationId + "] " + data, null, { cls: "OpenAPIProxy", func: "configure" });
70 |
71 | if (typeof data === "string" || (data instanceof String)) {
72 | data = hjson.parse(data as any);
73 | }
74 |
75 | if (data.command == "invokecompleted") {
76 | promise.resolve(data.data);
77 | OpenAPIProxy.amqp_promises.splice(OpenAPIProxy.amqp_promises.indexOf(promise), 1);
78 | } else if (data.command == "invokefailed") {
79 | promise.reject(data.data);
80 | OpenAPIProxy.amqp_promises.splice(OpenAPIProxy.amqp_promises.indexOf(promise), 1);
81 | } else if (data.command == "timeout") {
82 | promise.reject(new Error("Unknown queue or timeout. Either target is not valid or noone was online to answer."));
83 | OpenAPIProxy.amqp_promises.splice(OpenAPIProxy.amqp_promises.indexOf(promise), 1);
84 | }
85 |
86 | } else {
87 | Logger.instanse.warn("[" + options.correlationId + "] No promise found for correlationId: " + options.correlationId, null, { cls: "OpenAPIProxy", func: "configure" });
88 | }
89 | } catch (error) {
90 | Logger.instanse.error(error, null, { cls: "OpenAPIProxy", func: "configure" });
91 | }
92 | ack();
93 | }, null);
94 |
95 | app.all("/api/v1/*", async (req, res, next) => {
96 | if (Config.enable_openapi == false) return res.status(404).json({ error: "openapi not enabled" });
97 | if (!Logger.License.validlicense) await Logger.License.validate();
98 | const urlPath = req.path;
99 | const method = req.method.toUpperCase();
100 | const remoteip = WebServer.remoteip(req as any);
101 | Logger.instanse.debug("[" + method + "] " + urlPath + " from " + remoteip, null, { cls: "OpenAPIProxy" , func: "configure"});
102 | next();
103 | });
104 |
105 | RegisterRoutes(app);
106 |
107 |
108 | app.use(function errorHandler(
109 | err: unknown,
110 | req: any,
111 | res: any,
112 | next: NextFunction
113 | ): any | void {
114 | if (err instanceof ValidateError) {
115 | console.warn(`Caught Validation Error for ${req.path}:`, err.fields);
116 | return res.status(422).json({
117 | message: "Validation Failed",
118 | details: err?.fields,
119 | });
120 | }
121 | if (err instanceof Error) {
122 | console.warn(`Caught Error for ${req.path}:`, err.message);
123 | var message = err.message;
124 | var stack = err.stack;
125 | return res.status(500).json({
126 | error: message, message, stack
127 | });
128 | }
129 |
130 | next();
131 | });
132 |
133 | var collections = await Config.db.ListCollections(false, Crypt.rootToken());
134 | collections = collections.filter(x => x.name != "fs.chunks");
135 | collections = collections.filter(x => x.name != "uploads.files");
136 | collections = collections.filter(x => x.name != "uploads.chunks");
137 | collections = collections.filter(x => !x.name.endsWith("_hist"));
138 |
139 | app.all("/rest/v1/*", async (req, res, next) => {
140 | if (Config.enable_openapi == false) return res.status(404).json({ error: "openapi not enabled" });
141 | const urlPath = req.path;
142 | const method = req.method.toUpperCase();
143 | Logger.instanse.debug("[" + method + "] " + urlPath, null, { cls: "OpenAPIProxy", func: "configure" });
144 |
145 | if (urlPath == "/rest/v1/InvokeOpenRPA") {
146 | let body = req.body;
147 | if (body != null && body.body != null) body = body.body;
148 | try {
149 | var jwt = await OpenAPIProxy.GetToken(req);
150 | let result = { status: "ok", reply: undefined, error: undefined }
151 | if (typeof body === "string" || (body instanceof String)) {
152 | body = hjson.parse(body as any);
153 | }
154 |
155 | if (body.payload === null || body.payload === undefined) { body.payload = {}; }
156 | body.rpc = Config.parseBoolean(body.rpc as any);
157 | const rpacommand = {
158 | command: "invoke",
159 | workflowid: body.workflowid,
160 | data: body.payload
161 | }
162 | var robotuser = null;
163 | if (body.robotid != null && body.robotid != "") {
164 | robotuser = await Config.db.getbyid(body.robotid, "users", jwt, true, null)
165 | if (robotuser == null) {
166 | robotuser = await Config.db.GetOne({
167 | collectionname: "users", query: {
168 | "_type": "user",
169 | "$or": [{ "name": { "$regex": "^" + body.robotid + ".$", "$options": "i" } },
170 | { "username": { "$regex": "^" + body.robotid + ".$", "$options": "i" } }]
171 | }, jwt
172 | }, null);
173 | }
174 | }
175 | var workflow = null;
176 | if (body.robotid != null && body.robotid != "") {
177 | workflow = await Config.db.getbyid(body.workflowid, "openrpa", jwt, true, null)
178 | if (workflow == null) {
179 | workflow = await Config.db.GetOne({
180 | collectionname: "openrpa", query: {
181 | "_type": "workflow",
182 | "$or": [{ "name": { "$regex": "^" + body.workflowid + ".$", "$options": "i" } },
183 | { "projectandname": { "$regex": "^" + body.workflowid + ".$", "$options": "i" } }]
184 | }, jwt
185 | }, null);
186 | }
187 | }
188 | if (robotuser == null) {
189 | result.status = "error";
190 | result.error = "No such robot, please make sure robotid is a valid user _id from users collection";
191 | } else if (workflow == null) {
192 | result.status = "error";
193 | result.error = "No such workflow, please make sure workflowid is a valid workflow _id from openrpa collection";
194 | } else if (body.rpc == true) {
195 | body.userid = robotuser._id;
196 | const { correlationId, promise } = OpenAPIProxy.createPromise();
197 | Logger.instanse.debug("Send message to openrpa with correlationId: " + correlationId + " and message: " + JSON.stringify(rpacommand), null, { cls: "OpenAPIProxy", func: "configure" });
198 | try {
199 | await amqpwrapper.Instance().sendWithReplyTo("", body.userid, "openapi", rpacommand, 5000, correlationId, "", null);
200 | result.reply = await promise;
201 | var b = true;
202 | } catch (error) {
203 | result.status = "error";
204 | result.error = (error.message != null ? error.message : error);
205 | if (result.error == null) {
206 | result.error = (error.Message != null ? error.Message : error);
207 | }
208 | }
209 |
210 | } else {
211 | const correlationId = Util.GetUniqueIdentifier();
212 | Logger.instanse.debug("Send message to openrpa with correlationId: " + correlationId + " and message: " + JSON.stringify(rpacommand), null, { cls: "OpenAPIProxy", func: "configure" });
213 | await amqpwrapper.Instance().send("", body.userid, rpacommand, 5000, correlationId, "", null);
214 | }
215 | Logger.instanse.debug("Returned " + JSON.stringify(result) + " for body: " + JSON.stringify(body), null, { cls: "OpenAPIProxy", func: "configure" });
216 | res.json(result);
217 |
218 | } catch (error) {
219 | Logger.instanse.error(error, null, { cls: "OpenAPIProxy", func: "configure" });
220 | Logger.instanse.debug(JSON.stringify(body), null, { cls: "OpenAPIProxy", func: "configure" });
221 | res.status(500).json({ error: error.message });
222 | }
223 | } else {
224 | try {
225 | var jwt = await OpenAPIProxy.GetToken(req);
226 | const tuser = await Auth.Token2User(jwt, null);
227 | let result = await WebServer.ProcessMessage(req, tuser, jwt);
228 | res.json(result.data);
229 | } catch (error) {
230 | Logger.instanse.error(error, null, { cls: "OpenAPIProxy", func: "configure" });
231 | res.status(500).json({ error: error.message });
232 | }
233 | }
234 | next();
235 | });
236 |
237 | var url = Config.externalbaseurl()
238 | if (Config.domain == "localhost" && Config.port != 80) {
239 | url = "http://" + Config.domain + ":" + Config.port
240 | }
241 | if (url.endsWith("/")) {
242 | url = url.substring(0, url.length - 1)
243 | }
244 | Logger.instanse.debug("Updating servers to " + url, null, { cls: "OpenAPIProxy", func: "configure" });
245 | schema2["servers"] = [{ url }]
246 | schema2["openapi"] = "3.1.0";
247 | // @ts-ignore
248 | let components: any = schema2?.components;
249 | if (components?.securitySchemes?.oidc?.openIdConnectUrl != null) {
250 | components.securitySchemes.oidc.openIdConnectUrl = Config.baseurl() + "oidc/.well-known/openid-configuration";
251 | }
252 | var options = {
253 | explorer: true
254 | };
255 |
256 | app.use("/docs", swaggerUi.serve, async (_req: any, res: any) => {
257 | if (Config.enable_openapi == false) return res.status(404).json({ error: "openapi not enabled" });
258 | Logger.instanse.debug("Serving /docs", null, { cls: "OpenAPIProxy", func: "configure" });
259 | return res.send(
260 | swaggerUi.generateHTML(schema2)
261 | );
262 | });
263 | app.get("/openapi.json", (req, res) => {
264 | if (Config.enable_openapi == false) return res.status(404).json({ error: "openapi not enabled" });
265 | // #swagger.ignore = true
266 | Logger.instanse.debug("[GET] /openapi.json", null, { cls: "OpenAPIProxy", func: "configure" });
267 | res.json(schema2);
268 | });
269 | app.get("/swagger_output.json", (req, res) => {
270 | if (Config.enable_openapi == false) return res.status(404).json({ error: "openapi not enabled" });
271 | // #swagger.ignore = true
272 | Logger.instanse.debug("[GET] /swagger_output.json", null, { cls: "OpenAPIProxy", func: "configure" });
273 | res.json(schema2);
274 | });
275 |
276 | }
277 | static async GetToken(req) {
278 | let authorization = "";
279 | let jwt = "";
280 | if (req.headers["authorization"]) {
281 | authorization = req.headers["authorization"];
282 | }
283 | if (authorization != null && authorization != "") {
284 | var user = await Auth.Token2User(authorization, null);
285 | if (user != null) {
286 | jwt = await Auth.User2Token(user, Config.shorttoken_expires_in, null);
287 | } else {
288 | throw new Error("Valid authorization header is required");
289 | }
290 | } else {
291 | throw new Error("Authorization header is required");
292 | }
293 | return jwt;
294 | }
295 |
296 | }
297 |
--------------------------------------------------------------------------------
/src/MongoAdapter.ts:
--------------------------------------------------------------------------------
1 | import { Config } from "./Config.js";
2 |
3 | export class MongoAdapter {
4 | public name: string = "";
5 | /**
6 | *
7 | * Creates an instance of MyAdapter for an oidc-provider model.
8 | *
9 | * @constructor
10 | * @param {string} name Name of the oidc-provider model. One of "Session", "AccessToken",
11 | * "AuthorizationCode", "RefreshToken", "ClientCredentials", "Client", "InitialAccessToken",
12 | * "RegistrationAccessToken", "DeviceCode", "Interaction", "ReplayDetection", or "PushedAuthorizationRequest"
13 | *
14 | */
15 | constructor(name: string) {
16 | this.name = name.toLowerCase();
17 | }
18 |
19 | /**
20 | *
21 | * Update or Create an instance of an oidc-provider model.
22 | *
23 | * @return {Promise} Promise fulfilled when the operation succeeded. Rejected with error when
24 | * encountered.
25 | * @param {string} id Identifier that oidc-provider will use to reference this model instance for
26 | * future operations.
27 | * @param {object} payload Object with all properties intended for storage.
28 | * @param {integer} expiresIn Number of seconds intended for this model to be stored.
29 | *
30 | */
31 | async upsert(id, payload, expiresIn) {
32 | let expiresAt;
33 |
34 | if (expiresIn) {
35 | expiresAt = new Date(Date.now() + (expiresIn * 1000));
36 | }
37 |
38 | await this.coll().updateOne(
39 | { id },
40 | { $set: { payload, ...(expiresAt ? { expiresAt } : undefined) } },
41 | { upsert: true },
42 | );
43 | /**
44 | *
45 | * When this is one of AccessToken, AuthorizationCode, RefreshToken, ClientCredentials,
46 | * InitialAccessToken, RegistrationAccessToken or DeviceCode the payload will contain the
47 | * following properties depending on the used `formats` value for the given token (or default).
48 | *
49 | * Note: This list is not exhaustive and properties may be added in the future, it is highly
50 | * recommended to use a schema that allows for this.
51 | *
52 | * when `opaque` (default)
53 | * - jti {string} - unique identifier of the token
54 | * - kind {string} - token class name
55 | * - format {string} - the format used for the token storage and representation
56 | * - exp {number} - timestamp of the token's expiration
57 | * - iat {number} - timestamp of the token's creation
58 | * - accountId {string} - account identifier the token belongs to
59 | * - clientId {string} - client identifier the token belongs to
60 | * - aud {string[]} - array of audiences the token is intended for
61 | * - authTime {number} - timestamp of the end-user's authentication
62 | * - claims {object} - claims parameter (see claims in OIDC Core 1.0), rejected claims
63 | * are, in addition, pushed in as an Array of Strings in the `rejected` property.
64 | * - extra {object} - extra claims returned by the extraAccessTokenClaims helper
65 | * - codeChallenge {string} - client provided PKCE code_challenge value
66 | * - codeChallengeMethod {string} - client provided PKCE code_challenge_method value
67 | * - sessionUid {string} - uid of a session this token stems from
68 | * - expiresWithSession {boolean} - whether the token is valid when session expires
69 | * - grantId {string} - grant identifier, tokens with the same value belong together
70 | * - nonce {string} - random nonce from an authorization request
71 | * - redirectUri {string} - redirect_uri value from an authorization request
72 | * - resource {string} - granted or requested resource indicator value (auth code, device code, refresh token)
73 | * - rotations {number} - [RefreshToken only] - number of times the refresh token was rotated
74 | * - iiat {number} - [RefreshToken only] - the very first (initial) issued at before rotations
75 | * - acr {string} - authentication context class reference value
76 | * - amr {string[]} - Authentication methods references
77 | * - scope {string} - scope value from an authorization request, rejected scopes are removed
78 | * from the value
79 | * - sid {string} - session identifier the token comes from
80 | * - 'x5t#S256' {string} - X.509 Certificate SHA-256 Thumbprint of a certificate bound access or
81 | * refresh token
82 | * - 'jkt' {string} - JWK SHA-256 Thumbprint (according to [RFC7638]) of a DPoP bound
83 | * access or refresh token
84 | * - gty {string} - [AccessToken, RefreshToken only] space delimited grant values, indicating
85 | * the grant type(s) they originate from (implicit, authorization_code, refresh_token or
86 | * device_code) the original one is always first, second is refresh_token if refreshed
87 | * - params {object} - [DeviceCode only] an object with the authorization request parameters
88 | * as requested by the client with device_authorization_endpoint
89 | * - userCode {string} - [DeviceCode only] user code value
90 | * - deviceInfo {object} - [DeviceCode only] an object with details about the
91 | * device_authorization_endpoint request
92 | * - inFlight {boolean} - [DeviceCode only]
93 | * - error {string} - [DeviceCode only] - error from authnz to be returned to the polling client
94 | * - errorDescription {string} - [DeviceCode only] - error_description from authnz to be returned
95 | * to the polling client
96 | * - policies {string[]} - [InitialAccessToken, RegistrationAccessToken only] array of policies
97 | * - request {string} - [PushedAuthorizationRequest only] Pushed Request Object value
98 | *
99 | *
100 | * when `jwt`
101 | * - same as `opaque` with the addition of
102 | * - jwt {string} - the JWT value returned to the client
103 | *
104 | * when `jwt-ietf`
105 | * - same as `opaque` with the addition of
106 | * - 'jwt-ietf' {string} - the JWT value returned to the client
107 | *
108 | * when `paseto`
109 | * - same as `opaque` with the addition of
110 | * - paseto {string} - the PASETO value returned to the client
111 | *
112 | * Client model will only use this when registered through Dynamic Registration features and
113 | * will contain all client properties.
114 | *
115 | * OIDC Session model payload contains the following properties:
116 | * - jti {string} - Session's unique identifier, it changes on some occasions
117 | * - uid {string} - Session's unique fixed internal identifier
118 | * - kind {string} - "Session" fixed string value
119 | * - exp {number} - timestamp of the session's expiration
120 | * - iat {number} - timestamp of the session's creation
121 | * - account {string} - the session account identifier
122 | * - authorizations {object} - object with session authorized clients and their session identifiers
123 | * - loginTs {number} - timestamp of user's authentication
124 | * - acr {string} - authentication context class reference value
125 | * - amr {string[]} - Authentication methods references
126 | * - transient {boolean} - whether the session is using a persistant or session cookie
127 | * - state: {object} - temporary objects used for one-time csrf and state persistance between
128 | * form submissions
129 | *
130 | * Short-lived Interaction model payload contains the following properties:
131 | * - jti {string} - unique identifier of the interaction session
132 | * - kind {string} - "Interaction" fixed string value
133 | * - exp {number} - timestamp of the interaction's expiration
134 | * - iat {number} - timestamp of the interaction's creation
135 | * - uid {string} - the uid of the authorizing client's established session
136 | * - returnTo {string} - after resolving interactions send the user-agent to this url
137 | * - params {object} - parsed recognized parameters object
138 | * - lastSubmission {object} - previous interaction result submission
139 | * - signed {string[]} - parameter names that come from a trusted source
140 | * - result {object} - interaction results object is expected here
141 | * - session {object}
142 | * - session.uid {string} - uid of the session this Interaction belongs to
143 | * - session.cookie {string} - jti of the session this Interaction belongs to
144 | * - session.acr {string} - existing acr of the session Interaction belongs to
145 | * - session.amr {string[]} - existing amr of the session Interaction belongs to
146 | * - session.accountId {string} - existing account id from the seession Interaction belongs to
147 | *
148 | * Replay prevention ReplayDetection model contains the following properties:
149 | * - jti {string} - unique identifier of the replay object
150 | * - kind {string} - "ReplayDetection" fixed string value
151 | * - exp {number} - timestamp of the replay object cache expiration
152 | * - iat {number} - timestamp of the replay object cache's creation
153 | */
154 | }
155 |
156 | /**
157 | *
158 | * Return previously stored instance of an oidc-provider model.
159 | *
160 | * @return {Promise} Promise fulfilled with what was previously stored for the id (when found and
161 | * not dropped yet due to expiration) or falsy value when not found anymore. Rejected with error
162 | * when encountered.
163 | * @param {string} id Identifier of oidc-provider model
164 | *
165 | */
166 | async find(id: string) {
167 | return MongoAdapter.find(id);
168 | }
169 | static async find(id: string) {
170 | const result = await MongoAdapter.coll().find({ id }).limit(1).next();
171 | if (!result) return undefined;
172 | return result.payload;
173 | }
174 |
175 | /**
176 | *
177 | * Return previously stored instance of DeviceCode by the end-user entered user code. You only
178 | * need this method for the deviceFlow feature
179 | *
180 | * @return {Promise} Promise fulfilled with the stored device code object (when found and not
181 | * dropped yet due to expiration) or falsy value when not found anymore. Rejected with error
182 | * when encountered.
183 | * @param {string} userCode the user_code value associated with a DeviceCode instance
184 | *
185 | */
186 | async findByUserCode(userCode) {
187 | return MongoAdapter.findByUserCode(userCode);
188 | }
189 | static async findByUserCode(userCode) {
190 | const result = await this.coll().find({ 'payload.userCode': userCode }).limit(1).next();
191 | if (!result) return undefined;
192 | return result.payload;
193 | }
194 | /**
195 | *
196 | * Return previously stored instance of Session by its uid reference property.
197 | *
198 | * @return {Promise} Promise fulfilled with the stored session object (when found and not
199 | * dropped yet due to expiration) or falsy value when not found anymore. Rejected with error
200 | * when encountered.
201 | * @param {string} uid the uid value associated with a Session instance
202 | *
203 | */
204 | async findByUid(uid) {
205 | return MongoAdapter.findByUid(uid);
206 | }
207 | static async findByUid(uid) {
208 | const result = await this.coll().find({ 'payload.uid': uid }).limit(1).next();
209 | if (!result) return undefined;
210 | return result.payload;
211 | }
212 |
213 | /**
214 | *
215 | * Destroy/Drop/Remove a stored oidc-provider model. Future finds for this id should be fulfilled
216 | * with falsy values.
217 | *
218 | * @return {Promise} Promise fulfilled when the operation succeeded. Rejected with error when
219 | * encountered.
220 | * @param {string} id Identifier of oidc-provider model
221 | *
222 | */
223 | async destroy(id) {
224 | MongoAdapter.destroy(id);
225 | }
226 | static async destroy(id) {
227 | await this.coll().deleteOne({ id });
228 | }
229 |
230 | /**
231 | *
232 | * Destroy/Drop/Remove a stored oidc-provider model by its grantId property reference. Future
233 | * finds for all tokens having this grantId value should be fulfilled with falsy values.
234 | *
235 | * @return {Promise} Promise fulfilled when the operation succeeded. Rejected with error when
236 | * encountered.
237 | * @param {string} grantId the grantId value associated with a this model's instance
238 | *
239 | */
240 | async revokeByGrantId(grantId) {
241 | MongoAdapter.revokeByGrantId(grantId);
242 | }
243 | static async revokeByGrantId(grantId) {
244 | await this.coll().deleteMany({ 'payload.grantId': grantId });
245 | }
246 |
247 | /**
248 | *
249 | * Mark a stored oidc-provider model as consumed (not yet expired though!). Future finds for this
250 | * id should be fulfilled with an object containing additional property named "consumed" with a
251 | * truthy value (timestamp, date, boolean, etc).
252 | *
253 | * @return {Promise} Promise fulfilled when the operation succeeded. Rejected with error when
254 | * encountered.
255 | * @param {string} id Identifier of oidc-provider model
256 | *
257 | */
258 | async consume(id) {
259 | MongoAdapter.consume(id);
260 | }
261 | static async consume(id) {
262 | var updoc: any = { $set: { 'payload.consumed': Math.floor(Date.now() / 1000) } };
263 | await this.coll().findOneAndUpdate({ id }, updoc);
264 | }
265 | coll() {
266 | return MongoAdapter.coll();
267 | }
268 | static coll() {
269 | return Config.db.db.collection("oauthtokens");
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | function clog(message) {
2 | let dt = new Date();
3 | let dts: string = dt.getHours() + ":" + dt.getMinutes() + ":" + dt.getSeconds() + "." + dt.getMilliseconds();
4 | console.log(dts + " " + message);
5 | }
6 | function cerror(error) {
7 | let dt = new Date();
8 | let dts: string = dt.getHours() + ":" + dt.getMinutes() + ":" + dt.getSeconds() + "." + dt.getMilliseconds();
9 | console.error(dts, error.message ? error.message : error);
10 | }
11 | clog("Starting @openiap/core");
12 | import 'global-agent/bootstrap.js';
13 | import { Span } from "@opentelemetry/api";
14 | import crypto from "crypto";
15 | import http from "http";
16 | import { Config, dbConfig } from "./Config.js";
17 | import { Crypt } from "./Crypt.js";
18 | import { DatabaseConnection } from "./DatabaseConnection.js";
19 | import { HouseKeeping } from "./HouseKeeping.js";
20 | import { Logger } from "./Logger.js";
21 | import { OAuthProvider } from "./OAuthProvider.js";
22 | import { QueueClient } from "./QueueClient.js";
23 | import { WebServer } from "./WebServer.js";
24 | import { WebSocketServer } from "./WebSocketServer.js";
25 | import { amqpwrapper } from "./amqpwrapper.js";
26 | import { Base, User } from "./commoninterfaces.js";
27 | import { Payments } from "./ee/Payments.js";
28 | clog("Done loading imports");
29 | let amqp: amqpwrapper = null;
30 | async function initamqp(parent: Span) {
31 | const span: Span = Logger.otel.startSubSpan("initamqp", parent);
32 | try {
33 | amqp = new amqpwrapper(Config.amqp_url);
34 | amqpwrapper.SetInstance(amqp);
35 | await amqp.connect(span);
36 | } catch (error) {
37 | Logger.instanse.error(error, span, { cls: "index", func: "initamqp" });
38 | return false;
39 | } finally {
40 | Logger.otel.endSpan(span);
41 | }
42 | }
43 | async function ValidateUserForm(parent: Span) {
44 | if (Config.validate_user_form != null && Config.validate_user_form != "") {
45 | const span: Span = Logger.otel.startSubSpan("ValidateUserForm", parent);
46 | try {
47 | var forms = await Config.db.query({ query: { _id: Config.validate_user_form, _type: "form" }, top: 1, collectionname: "forms", jwt: Crypt.rootToken() }, null);
48 | if (forms.length == 0) {
49 | Logger.instanse.info("validate_user_form " + Config.validate_user_form + " does not exists!", span, { cls: "index", func: "ValidateUserForm" });
50 | Config.validate_user_form = "";
51 | }
52 | } catch (error) {
53 | Logger.instanse.error(error, span, { cls: "index", func: "ValidateUserForm" });
54 | return false;
55 | } finally {
56 | Logger.otel.endSpan(span);
57 | }
58 | }
59 | }
60 | function doHouseKeeping(span: Span) {
61 | if (HouseKeeping.lastHouseKeeping == null) {
62 | HouseKeeping.lastHouseKeeping = new Date();
63 | HouseKeeping.lastHouseKeeping.setDate(HouseKeeping.lastHouseKeeping.getDate() - 1);
64 | }
65 | amqpwrapper.Instance().send("openflow", "", { "command": "housekeeping", "lastrun": (new Date()).toISOString() }, 20000, null, "", span, 1);
66 | var dt = new Date(HouseKeeping.lastHouseKeeping.toISOString());
67 | var housekeeping_skip_calculate_size: boolean = !(dt.getHours() == 1 || dt.getHours() == 13);
68 | var housekeeping_skip_update_user_size: boolean = !(dt.getHours() == 1 || dt.getHours() == 13);
69 | if (Config.housekeeping_skip_calculate_size) housekeeping_skip_calculate_size = true;
70 | if (Config.housekeeping_skip_update_user_size) housekeeping_skip_update_user_size = true;
71 | if (Config.NODE_ENV == "production") {
72 | HouseKeeping.DoHouseKeeping(false, housekeeping_skip_calculate_size, housekeeping_skip_update_user_size, null).catch((error) => Logger.instanse.error(error, null, { cls: "index", func: "doHouseKeeping" }));
73 | } else {
74 | // While debugging, always do all calculations
75 | HouseKeeping.DoHouseKeeping(false, false, false, null).catch((error) => Logger.instanse.error(error, null, { cls: "index", func: "doHouseKeeping" }));
76 | }
77 | }
78 | function initHouseKeeping(span: Span) {
79 | const randomNum = crypto.randomInt(1, 100);
80 | // Every 15 minutes, give and take a few minutes, send out a message to do house keeping, if ready
81 | Logger.instanse.verbose("Housekeeping every 15 minutes plus " + randomNum + " seconds", span, { cls: "index", func: "initHouseKeeping" });
82 | housekeeping = setInterval(async () => {
83 | if (Config.enable_openflow_amqp) {
84 | if (!HouseKeeping.ReadyForHousekeeping()) {
85 | return;
86 | }
87 | amqpwrapper.Instance().send("openflow", "", { "command": "housekeeping" }, 10000, null, "", span, 1);
88 | await new Promise(resolve => { setTimeout(resolve, 10000) });
89 | if (HouseKeeping.ReadyForHousekeeping()) {
90 | doHouseKeeping(span);
91 | } else {
92 | Logger.instanse.verbose("SKIP housekeeping", span, { cls: "index", func: "initHouseKeeping" });
93 | }
94 | } else {
95 | doHouseKeeping(span);
96 | }
97 | }, (15 * 60 * 1000) + (randomNum * 1000));
98 | // If I'm first and noone else has run it, lets trigger it now
99 | const randomNum2 = crypto.randomInt(1, 10);
100 | Logger.instanse.info("Trigger first Housekeeping in " + randomNum2 + " seconds", span, { cls: "index", func: "initHouseKeeping" });
101 | setTimeout(async () => {
102 | if (Config.enable_openflow_amqp) {
103 | if (!HouseKeeping.ReadyForHousekeeping()) {
104 | return;
105 | }
106 | amqpwrapper.Instance().send("openflow", "", { "command": "housekeeping" }, 10000, null, "", span, 1);
107 | await new Promise(resolve => { setTimeout(resolve, 10000) });
108 | if (HouseKeeping.ReadyForHousekeeping()) {
109 | doHouseKeeping(span);
110 | } else {
111 | Logger.instanse.verbose("SKIP housekeeping", span, { cls: "index", func: "initHouseKeeping" });
112 | }
113 | } else {
114 | doHouseKeeping(span);
115 | }
116 | }, randomNum2 * 1000);
117 | }
118 | async function initDatabase(parent: Span): Promise {
119 | const span: Span = Logger.otel.startSubSpan("initDatabase", parent);
120 | try {
121 | const jwt: string = Crypt.rootToken();
122 | Config.dbConfig = await dbConfig.Load(jwt, false, span);
123 | try {
124 | if (Config.license_key != null && Config.license_key != "") {
125 | var lic = Logger.License;
126 | await lic?.validate();
127 | }
128 | } catch (error) {
129 | }
130 | try {
131 | await Logger.configure(false, true);
132 | amqpwrapper.Instance().setPrefetch(Config.amqp_prefetch, span);
133 | } catch (error) {
134 | Logger.instanse.error(error, span, { cls: "index", func: "initDatabase" });
135 | process.exit(404);
136 | }
137 | const users = await Config.db.query({ query: { _type: "role" }, top: 4, collectionname: "users", projection: { "name": 1 }, jwt: jwt }, span);
138 | if (users.length != 4) {
139 | await HouseKeeping.ensureBuiltInUsersAndRoles(span);
140 | }
141 | return true;
142 | } catch (error) {
143 | Logger.instanse.error(error, span, { cls: "index", func: "initDatabase" });
144 | return false;
145 | } finally {
146 | Logger.otel.endSpan(span);
147 | }
148 | }
149 | async function PreRegisterExchanges(span: Span) {
150 | var exchanges = await Config.db.query({ query: { _type: "exchange" }, collectionname: "mq", jwt: Crypt.rootToken() }, span);
151 | for (let i = 0; i < exchanges.length; i++) {
152 | const exchange = exchanges[i];
153 | await amqpwrapper.Instance().PreRegisterExchange(exchange, span);
154 | }
155 | }
156 | process.on("beforeExit", (code) => {
157 | Logger.instanse.error(code as any, null, { cls: "index", func: "beforeExit" });
158 | });
159 | process.on("exit", (code) => {
160 | Logger.instanse.error(code as any, null, { cls: "index", func: "exit" });
161 | });
162 | const unhandledRejections = new Map();
163 | process.on("unhandledRejection", (reason, promise) => {
164 | Logger.instanse.error(reason as any, null, { cls: "index", func: "unhandledRejection" });
165 | unhandledRejections.set(promise, reason);
166 | });
167 | process.on("rejectionHandled", (promise) => {
168 | unhandledRejections.delete(promise);
169 | });
170 | process.on("uncaughtException", (err, origin) => {
171 | Logger.instanse.error(err, null, { cls: "index", func: "uncaughtException" });
172 | });
173 | function onerror(err, origin) {
174 | Logger.instanse.error(err, null, { cls: "index", func: "onerror" });
175 | }
176 | process.on("uncaughtExceptionMonitor", onerror);
177 | function onWarning(warning) {
178 | try {
179 | Logger.instanse.warn(warning.name + ": " + warning.message, null, { cls: "index", func: "onWarning" });
180 | } catch (error) {
181 | }
182 | }
183 | process.on("warning", onWarning);
184 | // The signals we want to handle
185 | // NOTE: although it is tempting, the SIGKILL signal (9) cannot be intercepted and handled
186 | var signals = {
187 | "SIGHUP": 1,
188 | "SIGINT": 2,
189 | "SIGTERM": 15
190 | };
191 | var housekeeping = null;
192 | async function handle(signal, value) {
193 | Logger.instanse.info(`process received a ${signal} signal with value ${value}`, null, { cls: "index", func: "handle" });
194 | try {
195 | if (Config.heapdump_onstop) {
196 | await Logger.otel.createheapdump(null);
197 | }
198 | Config.db.shutdown();
199 | Logger.otel.shutdown();
200 | Logger.License.shutdown()
201 | if (housekeeping != null) {
202 | try {
203 | clearInterval(housekeeping);
204 | } catch (error) {
205 | }
206 | }
207 | setTimeout(() => {
208 | process.exit(128 + value);
209 | }, 1000);
210 | if (server != null && server.close) {
211 | server.close((err) => {
212 | Logger.instanse.info(`server stopped by ${signal} with value ${value}`, null, { cls: "index", func: "handle" });
213 | Logger.instanse.error(err, null, { cls: "index", func: "handle" });
214 | process.exit(128 + value);
215 | })
216 | }
217 | } catch (error) {
218 | Logger.instanse.error(error, null, { cls: "index", func: "handle" });
219 | Logger.instanse.info(`server stopped by ${signal} with value ${value}`, null, { cls: "index", func: "handle" });
220 | process.exit(128 + value);
221 | }
222 | }
223 | Object.keys(signals).forEach((signal) => process.on(signal, handle));
224 |
225 | const originalStdoutWrite = process.stdout.write.bind(process.stdout);
226 | const originalStderrWrite = process.stderr.write.bind(process.stderr);
227 | (process.stdout.write as any) = (chunk: string, encoding?: string, callback?: (err?: Error | null) => void): boolean => {
228 | return originalStdoutWrite(chunk, encoding, callback);
229 | };
230 | (process.stderr.write as any) = (chunk: string, encoding?: string, callback?: (err?: Error | null) => void): boolean => {
231 | return originalStderrWrite(chunk, encoding, callback);
232 | };
233 |
234 | var server: http.Server = null;
235 | (async function (): Promise {
236 | try {
237 | await Logger.configure(false, false);
238 | } catch (error) {
239 | cerror(error);
240 | process.exit(404);
241 | }
242 | Config.db = new DatabaseConnection(Config.mongodb_url, Config.mongodb_db);
243 | const span: Span = Logger.otel.startSpan("openflow.startup", null, null);
244 | try {
245 | await Config.db.connect(span);
246 | await initamqp(span);
247 | await PreRegisterExchanges(span);
248 | Logger.instanse.info("VERSION: " + Config.version, span, { cls: "index", func: "init" });
249 | Logger.instanse.debug("Configure Webserver", span, { cls: "index", func: "init" });
250 | server = await WebServer.configure(Config.baseurl(), span);
251 | try {
252 | // @ts-ignore
253 | let GrafanaProxy: any = await import("./ee/grafana-proxy.js");
254 | Logger.instanse.debug("Configure grafana", span, { cls: "index", func: "init" });
255 | const grafana = await GrafanaProxy.GrafanaProxy.configure(WebServer.app, span);
256 | } catch (error) {
257 | Logger.instanse.error(error, span, { cls: "index", func: "init" });
258 | }
259 | try {
260 | let OpenAPIProxy: any = await import("./ee/OpenAPIProxy.js");
261 | Logger.instanse.debug("Configure open api", span, { cls: "index", func: "init" });
262 | const OpenAI = await OpenAPIProxy.OpenAPIProxy.configure(WebServer.app, span);
263 | } catch (error) {
264 | Logger.instanse.error(error, span, { cls: "index", func: "initDatabase" });
265 | }
266 | try {
267 | let GitProxy: any = await import("./ee/GitProxy.js");
268 | Logger.instanse.debug("Configure git server", span, { cls: "index", func: "init" });
269 | const Git = await GitProxy.GitProxy.configure(WebServer.app, span);
270 | } catch (error) {
271 | Logger.instanse.error(error, span, { cls: "index", func: "initDatabase" });
272 | }
273 | Payments.configure(WebServer.app, span);
274 | Logger.instanse.debug("Configure oauth provider", span, { cls: "index", func: "init" });
275 | OAuthProvider.configure(WebServer.app, span);
276 | Logger.instanse.debug("Configure websocket server", span, { cls: "index", func: "init" });
277 | WebSocketServer.configure(server, span);
278 | Logger.instanse.debug("init database", span, { cls: "index", func: "init" });
279 | if (!await initDatabase(span)) {
280 | process.exit(404);
281 | }
282 | Logger.instanse.debug("init house keeping", span, { cls: "index", func: "init" });
283 | initHouseKeeping(span);
284 | Logger.instanse.debug("Init queue handler (openflow amqp)", span, { cls: "index", func: "init" });
285 | await QueueClient.configure(span);
286 | Logger.instanse.debug("Validate user validation form exists, if needed", span, { cls: "index", func: "init" });
287 | await ValidateUserForm(span);
288 | Logger.instanse.debug("Begin listening on ports", span, { cls: "index", func: "init" });
289 | WebServer.Listen();
290 | if (Config.workitem_queue_monitoring_enabled) {
291 | Logger.instanse.verbose("Start workitem queue monitor", span, { cls: "index", func: "init" });
292 | Config.db.ensureQueueMonitoring();
293 | }
294 | } catch (error) {
295 | Logger.instanse.error(error, span, { cls: "index", func: "init" });
296 | process.exit(404);
297 | } finally {
298 | Logger.otel.endSpan(span);
299 | }
300 | })();
301 |
302 |
--------------------------------------------------------------------------------