From 6fb29ff39a86a34e2bda5ac400b1114643b4f906 Mon Sep 17 00:00:00 2001 From: dhommen Date: Tue, 25 Jul 2023 16:25:10 +0200 Subject: [PATCH 001/183] feat: semantic-release --- .gitignore | 3 +- .releaserc | 10 + package-lock.json | 6861 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 21 + 4 files changed, 6894 insertions(+), 1 deletion(-) create mode 100644 .releaserc create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 86bfd07..9d70ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ data *.log .idea/ **/*.lock -**/*.iml \ No newline at end of file +**/*.iml +node_modules/ \ No newline at end of file diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..9f98b8c --- /dev/null +++ b/.releaserc @@ -0,0 +1,10 @@ +{ + "branches": ["master"], + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + "@semantic-release/git", + "@semantic-release/github" + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0e56bb8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6861 @@ +{ + "name": "ids-basecamp-clearinghouse", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ids-basecamp-clearinghouse", + "version": "0.0.1", + "license": "ISC", + "devDependencies": { + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "semantic-release": "^21.0.7" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/core": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.0.tgz", + "integrity": "sha512-YbAtMWIrbZ9FCXbLwT9wWB8TyLjq9mxpKdgB3dUNxQcIVTf9hJ70gRPwAcqGZdY6WdJPZ0I7jLaaNDCiloGN2A==", + "dev": true, + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^11.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/endpoint": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.0.tgz", + "integrity": "sha512-szrQhiqJ88gghWY2Htt8MqUDO6++E/EIXqJ2ZEp5ma3uGS46o7LZAzSLt49myB7rT+Hfw5Y6gO3LmOxGzHijAQ==", + "dev": true, + "dependencies": { + "@octokit/types": "^11.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/graphql": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.1.tgz", + "integrity": "sha512-T5S3oZ1JOE58gom6MIcrgwZXzTaxRnxBso58xhozxHpOqSTgDS6YNeEUvZ/kRvXgPrRz/KHnZhtb7jUMRi9E6w==", + "dev": true, + "dependencies": { + "@octokit/request": "^8.0.1", + "@octokit/types": "^11.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz", + "integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==", + "dev": true + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-8.0.0.tgz", + "integrity": "sha512-2xZ+baZWUg+qudVXnnvXz7qfrTmDeYPCzangBVq/1gXxii/OiS//4shJp9dnCCvj1x+JAm9ji1Egwm1BA47lPQ==", + "dev": true, + "dependencies": { + "@octokit/types": "^11.0.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.0.tgz", + "integrity": "sha512-a1/A4A+PB1QoAHQfLJxGHhLfSAT03bR1jJz3GgQJZvty2ozawFWs93MiBQXO7SL2YbO7CIq0Goj4qLOBj8JeMQ==", + "dev": true, + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^11.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-7.0.0.tgz", + "integrity": "sha512-KL2k/d0uANc8XqP5S64YcNFCudR3F5AaKO39XWdUtlJIjT9Ni79ekWJ6Kj5xvAw87udkOMEPcVf9xEge2+ahew==", + "dev": true, + "dependencies": { + "@octokit/types": "^11.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.1.tgz", + "integrity": "sha512-8N+tdUz4aCqQmXl8FpHYfKG9GelDFd7XGVzyN8rc6WxVlYcfpHECnuRkgquzz+WzvHTK62co5di8gSXnzASZPQ==", + "dev": true, + "dependencies": { + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^11.1.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/request-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.0.tgz", + "integrity": "sha512-1ue0DH0Lif5iEqT52+Rf/hf0RmGO9NWFjrzmrkArpG9trFfDM/efx00BJHdLGuro4BR/gECxCU2Twf5OKrRFsQ==", + "dev": true, + "dependencies": { + "@octokit/types": "^11.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@octokit/types": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", + "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", + "dev": true, + "dependencies": { + "@octokit/openapi-types": "^18.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/@pnpm/npm-conf": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz", + "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==", + "dev": true, + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@semantic-release/changelog": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", + "integrity": "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "fs-extra": "^11.0.0", + "lodash": "^4.17.4" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/changelog/node_modules/@semantic-release/error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", + "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", + "dev": true, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@semantic-release/changelog/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/changelog/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@semantic-release/changelog/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/commit-analyzer": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-10.0.1.tgz", + "integrity": "sha512-9ejHzTAijYs9z246sY/dKBatmOPcd0GQ7lH4MgLCkv1q4GCiDZRkjHJkaQZXZVaK7mJybS+sH3Ng6G8i3pYMGQ==", + "dev": true, + "dependencies": { + "conventional-changelog-angular": "^6.0.0", + "conventional-commits-filter": "^3.0.0", + "conventional-commits-parser": "^4.0.0", + "debug": "^4.0.0", + "import-from": "^4.0.0", + "lodash-es": "^4.17.21", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/git": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", + "integrity": "sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "debug": "^4.0.0", + "dir-glob": "^3.0.0", + "execa": "^5.0.0", + "lodash": "^4.17.4", + "micromatch": "^4.0.0", + "p-reduce": "^2.0.0" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/git/node_modules/@semantic-release/error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", + "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", + "dev": true, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@semantic-release/git/node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/git/node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@semantic-release/git/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@semantic-release/git/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/@semantic-release/git/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/git/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/git/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@semantic-release/git/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/git/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/git/node_modules/p-reduce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", + "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@semantic-release/git/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@semantic-release/github": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-9.0.4.tgz", + "integrity": "sha512-kQCGFAsBErvCR6hzNuzu63cj4erQN2krm9zQlg8vl4j5X0mL0d/Ras0wmL5Gkr1TuSS2lweME7M4J5zvtDDDSA==", + "dev": true, + "dependencies": { + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^8.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^7.0.0", + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^4.0.1", + "debug": "^4.3.4", + "dir-glob": "^3.0.1", + "globby": "^13.1.4", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "issue-parser": "^6.0.0", + "lodash-es": "^4.17.21", + "mime": "^3.0.0", + "p-filter": "^3.0.0", + "url-join": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/npm": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-10.0.4.tgz", + "integrity": "sha512-6R3timIQ7VoL2QWRkc9DG8v74RQtRp7UOe/2KbNaqwJ815qOibAv65bH3RtTEhs4axEaHoZf7HDgFs5opaZ9Jw==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^4.0.1", + "execa": "^7.0.0", + "fs-extra": "^11.0.0", + "lodash-es": "^4.17.21", + "nerf-dart": "^1.0.0", + "normalize-url": "^8.0.0", + "npm": "^9.5.0", + "rc": "^1.2.8", + "read-pkg": "^8.0.0", + "registry-auth-token": "^5.0.0", + "semver": "^7.1.2", + "tempy": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/release-notes-generator": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-11.0.4.tgz", + "integrity": "sha512-j0Znnwq9IdWTCGzqSlkLv4MpALTsVDZxcVESzJCNN8pK2BYQlYaKsdZ1Ea/+7RlppI3vjhEi33ZKmjSGY1FLKw==", + "dev": true, + "dependencies": { + "conventional-changelog-angular": "^6.0.0", + "conventional-changelog-writer": "^6.0.0", + "conventional-commits-filter": "^3.0.0", + "conventional-commits-parser": "^4.0.0", + "debug": "^4.0.0", + "get-stream": "^7.0.0", + "import-from": "^4.0.0", + "into-stream": "^7.0.0", + "lodash-es": "^4.17.21", + "read-pkg-up": "^10.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/get-stream": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", + "integrity": "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "dev": true + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true + }, + "node_modules/agent-base": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", + "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", + "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", + "dev": true, + "dependencies": { + "clean-stack": "^4.0.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/argv-formatter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz", + "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", + "dev": true + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cardinal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", + "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", + "dev": true, + "dependencies": { + "ansicolors": "~0.3.2", + "redeyed": "~2.1.0" + }, + "bin": { + "cdl": "bin/cdl.js" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/clean-stack": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", + "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", + "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/conventional-changelog-angular": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", + "integrity": "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-6.0.1.tgz", + "integrity": "sha512-359t9aHorPw+U+nHzUXHS5ZnPBOizRxfQsWT5ZDHBfvfxQOAik+yfuhKXG66CN5LEWPpMNnIMHUTCKeYNprvHQ==", + "dev": true, + "dependencies": { + "conventional-commits-filter": "^3.0.0", + "dateformat": "^3.0.3", + "handlebars": "^4.7.7", + "json-stringify-safe": "^5.0.1", + "meow": "^8.1.2", + "semver": "^7.0.0", + "split": "^1.0.1" + }, + "bin": { + "conventional-changelog-writer": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-commits-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-3.0.0.tgz", + "integrity": "sha512-1ymej8b5LouPx9Ox0Dw/qAO2dVdfpRFq28e5Y0jJEU8ZrLdy0vOSkkIInwmxErFGhg6SALro60ZrwYFVTUDo4Q==", + "dev": true, + "dependencies": { + "lodash.ismatch": "^4.4.0", + "modify-values": "^1.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/conventional-commits-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-4.0.0.tgz", + "integrity": "sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg==", + "dev": true, + "dependencies": { + "is-text-path": "^1.0.1", + "JSONStream": "^1.3.5", + "meow": "^8.1.2", + "split2": "^3.2.2" + }, + "bin": { + "conventional-commits-parser": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cosmiconfig": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", + "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "dev": true, + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decamelize-keys/node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/env-ci": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-9.1.1.tgz", + "integrity": "sha512-Im2yEWeF4b2RAMAaWvGioXk6m0UNaIjD8hj28j2ij5ldnIFrDQT0+pzDvpbRkcjurhXhf/AsBKv8P2rtmGi9Aw==", + "dev": true, + "dependencies": { + "execa": "^7.0.0", + "java-properties": "^1.0.2" + }, + "engines": { + "node": "^16.14 || >=18" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/figures": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", + "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^5.0.0", + "is-unicode-supported": "^1.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dev": true, + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-log-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz", + "integrity": "sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==", + "dev": true, + "dependencies": { + "argv-formatter": "~1.0.0", + "spawn-error-forwarder": "~1.0.0", + "split2": "~1.0.0", + "stream-combiner2": "~1.1.1", + "through2": "~2.0.0", + "traverse": "~0.6.6" + } + }, + "node_modules/git-log-parser/node_modules/split2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz", + "integrity": "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==", + "dev": true, + "dependencies": { + "through2": "~2.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/handlebars": { + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/hook-std": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-3.0.0.tgz", + "integrity": "sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hosted-git-info": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", + "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", + "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz", + "integrity": "sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-from/-/import-from-4.0.0.tgz", + "integrity": "sha512-P9J71vT5nLlDeV8FHs5nNxaLbrpfAV5cF5srvbZfpwpcJoM/xZR3hiv+q+SAnuSmuGbXMWud063iIMx/V/EWZQ==", + "dev": true, + "engines": { + "node": ">=12.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/into-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", + "integrity": "sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==", + "dev": true, + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "dev": true, + "dependencies": { + "text-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/issue-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", + "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==", + "dev": true, + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.uniqby": "^4.7.0" + }, + "engines": { + "node": ">=10.13" + } + }, + "node_modules/java-properties": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", + "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", + "dev": true + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "dev": true + }, + "node_modules/lodash.ismatch": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", + "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-5.1.2.tgz", + "integrity": "sha512-ahRPGXJpjMjwSOlBoTMZAK7ATXkli5qCPxZ21TG44rx1KEo44bii4ekgTDQPNRQ4Kh7JMb9Ub1PVk1NxRSsorg==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/marked-terminal": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-5.2.0.tgz", + "integrity": "sha512-Piv6yNwAQXGFjZSaiNljyNFw7jKDdGrw70FSbtxEyldLsyeuV5ZHm/1wW++kWbrOF1VPnUgYOhB2oLL0ZpnekA==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cardinal": "^2.1.1", + "chalk": "^5.2.0", + "cli-table3": "^0.6.3", + "node-emoji": "^1.11.0", + "supports-hyperlinks": "^2.3.0" + }, + "engines": { + "node": ">=14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "marked": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/meow": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", + "integrity": "sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/meow/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/meow/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nerf-dart": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", + "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", + "dev": true + }, + "node_modules/node-emoji": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", + "integrity": "sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/normalize-url": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", + "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm": { + "version": "9.8.1", + "resolved": "https://registry.npmjs.org/npm/-/npm-9.8.1.tgz", + "integrity": "sha512-AfDvThQzsIXhYgk9zhbk5R+lh811lKkLAeQMMhSypf1BM7zUafeIIBzMzespeuVEJ0+LvY36oRQYf7IKLzU3rw==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/run-script", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "cli-table3", + "columnify", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "npmlog", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "sigstore", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dev": true, + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^6.3.0", + "@npmcli/config": "^6.2.1", + "@npmcli/fs": "^3.1.0", + "@npmcli/map-workspaces": "^3.0.4", + "@npmcli/package-json": "^4.0.1", + "@npmcli/promise-spawn": "^6.0.2", + "@npmcli/run-script": "^6.0.2", + "abbrev": "^2.0.0", + "archy": "~1.0.0", + "cacache": "^17.1.3", + "chalk": "^5.3.0", + "ci-info": "^3.8.0", + "cli-columns": "^4.0.0", + "cli-table3": "^0.6.3", + "columnify": "^1.6.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.2", + "glob": "^10.2.7", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^6.1.1", + "ini": "^4.1.1", + "init-package-json": "^5.0.0", + "is-cidr": "^4.0.2", + "json-parse-even-better-errors": "^3.0.0", + "libnpmaccess": "^7.0.2", + "libnpmdiff": "^5.0.19", + "libnpmexec": "^6.0.3", + "libnpmfund": "^4.0.19", + "libnpmhook": "^9.0.3", + "libnpmorg": "^5.0.4", + "libnpmpack": "^5.0.19", + "libnpmpublish": "^7.5.0", + "libnpmsearch": "^6.0.2", + "libnpmteam": "^5.0.3", + "libnpmversion": "^4.0.2", + "make-fetch-happen": "^11.1.1", + "minimatch": "^9.0.3", + "minipass": "^5.0.0", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^9.4.0", + "nopt": "^7.2.0", + "npm-audit-report": "^5.0.0", + "npm-install-checks": "^6.1.1", + "npm-package-arg": "^10.1.0", + "npm-pick-manifest": "^8.0.1", + "npm-profile": "^7.0.1", + "npm-registry-fetch": "^14.0.5", + "npm-user-validate": "^2.0.0", + "npmlog": "^7.0.1", + "p-map": "^4.0.0", + "pacote": "^15.2.0", + "parse-conflict-json": "^3.0.1", + "proc-log": "^3.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^2.1.0", + "semver": "^7.5.4", + "sigstore": "^1.7.0", + "ssri": "^10.0.4", + "supports-color": "^9.4.0", + "tar": "^6.1.15", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^5.0.0", + "which": "^3.0.1", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@colors/colors": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "6.3.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^3.1.0", + "@npmcli/installed-package-contents": "^2.0.2", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^5.0.0", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^4.0.0", + "@npmcli/query": "^3.0.0", + "@npmcli/run-script": "^6.0.0", + "bin-links": "^4.0.1", + "cacache": "^17.0.4", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "json-stringify-nice": "^1.1.4", + "minimatch": "^9.0.0", + "nopt": "^7.0.0", + "npm-install-checks": "^6.0.0", + "npm-package-arg": "^10.1.0", + "npm-pick-manifest": "^8.0.1", + "npm-registry-fetch": "^14.0.3", + "npmlog": "^7.0.1", + "pacote": "^15.0.8", + "parse-conflict-json": "^3.0.0", + "proc-log": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^1.0.2", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "ssri": "^10.0.1", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^3.0.2", + "ci-info": "^3.8.0", + "ini": "^4.1.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/disparity-colors": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ansi-styles": "^4.3.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^2.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^17.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^15.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.1.0", + "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.1.0", + "tuf-js": "^1.1.7" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abort-controller": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/npm/node_modules/agentkeepalive": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/npm/node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/are-we-there-yet": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^4.1.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/buffer": { + "version": "6.0.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/npm/node_modules/builtins": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "17.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "3.8.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^4.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cli-table3": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/npm/node_modules/clone": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/npm/node_modules/columnify": { + "version": "1.6.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "strip-ansi": "^6.0.1", + "wcwidth": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/defaults": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/delegates": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/depd": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.1.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/event-target-shim": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/events": { + "version": "3.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/function-bind": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/gauge": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^4.0.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.2.7", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2", + "path-scurry": "^1.7.0" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/has": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "6.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/humanize-ms": { + "version": "1.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "6.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/ini": { + "version": "4.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^10.0.0", + "promzard": "^1.0.0", + "read": "^2.0.0", + "read-package-json": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/ip": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "4.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^3.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/is-core-module": { + "version": "2.12.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^10.1.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "5.0.19", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.3.0", + "@npmcli/disparity-colors": "^3.0.0", + "@npmcli/installed-package-contents": "^2.0.2", + "binary-extensions": "^2.2.0", + "diff": "^5.1.0", + "minimatch": "^9.0.0", + "npm-package-arg": "^10.1.0", + "pacote": "^15.0.8", + "tar": "^6.1.13" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "6.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.3.0", + "@npmcli/run-script": "^6.0.0", + "ci-info": "^3.7.1", + "npm-package-arg": "^10.1.0", + "npmlog": "^7.0.1", + "pacote": "^15.0.8", + "proc-log": "^3.0.0", + "read": "^2.0.0", + "read-package-json-fast": "^3.0.2", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "4.0.19", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.3.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "9.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "5.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "5.0.19", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^6.3.0", + "@npmcli/run-script": "^6.0.0", + "npm-package-arg": "^10.1.0", + "pacote": "^15.0.8" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "7.5.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^3.6.1", + "normalize-package-data": "^5.0.0", + "npm-package-arg": "^10.1.0", + "npm-registry-fetch": "^14.0.3", + "proc-log": "^3.0.0", + "semver": "^7.3.7", + "sigstore": "^1.4.0", + "ssri": "^10.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "5.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^14.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.1", + "@npmcli/run-script": "^6.0.0", + "json-parse-even-better-errors": "^3.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "7.18.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "11.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/npm/node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "9.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^11.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/gauge": { + "version": "4.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/npmlog": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/readable-stream": { + "version": "3.6.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "7.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "6.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "10.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "7.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "14.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/npmlog": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^4.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^5.0.0", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/once": { + "version": "1.4.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "15.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^5.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.3.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.9.2", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^9.1.1", + "minipass": "^5.0.0 || ^6.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/path-scurry/node_modules/lru-cache": { + "version": "9.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "6.0.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/process": { + "version": "0.11.10", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~1.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json": { + "version": "6.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/readable-stream": { + "version": "4.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.5.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "1.7.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.1.0", + "@sigstore/tuf": "^1.0.1", + "make-fetch-happen": "^11.0.1" + }, + "bin": { + "sigstore": "bin/sigstore.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.7.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.13", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "10.0.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.1.15", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "1.1.7", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/npm/node_modules/which": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/wide-align": { + "version": "1.1.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-each-series": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", + "integrity": "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-3.0.0.tgz", + "integrity": "sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg==", + "dev": true, + "dependencies": { + "p-map": "^5.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz", + "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==", + "dev": true, + "dependencies": { + "aggregate-error": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-reduce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", + "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", + "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==", + "dev": true, + "dependencies": { + "find-up": "^2.0.0", + "load-json-file": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read-pkg": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.0.0.tgz", + "integrity": "sha512-Ajb9oSjxXBw0YyOiwtQ2dKbAA/vMnUPnY63XcCk+mXo0BwIdQEMgZLZiMWGttQHcUhUgbK0mH85ethMPKXxziw==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.1", + "normalize-package-data": "^5.0.0", + "parse-json": "^7.0.0", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.0.0.tgz", + "integrity": "sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0", + "read-pkg": "^8.0.0", + "type-fest": "^3.12.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-pkg/node_modules/lines-and-columns": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", + "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/read-pkg/node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.0.0.tgz", + "integrity": "sha512-kP+TQYAzAiVnzOlWOe0diD6L35s9bJh0SCn95PIbZFKrOYuIRQsQkeWEYxzVDuHTt9V9YqvYCJ2Qo4z9wdfZPw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.21.4", + "error-ex": "^1.3.2", + "json-parse-even-better-errors": "^3.0.0", + "lines-and-columns": "^2.0.3", + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/redeyed": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", + "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", + "dev": true, + "dependencies": { + "esprima": "~4.0.0" + } + }, + "node_modules/registry-auth-token": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", + "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", + "dev": true, + "dependencies": { + "@pnpm/npm-conf": "^2.1.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/semantic-release": { + "version": "21.0.7", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-21.0.7.tgz", + "integrity": "sha512-peRDSXN+hF8EFSKzze90ff/EnAmgITHQ/a3SZpRV3479ny0BIZWEJ33uX6/GlOSKdaSxo9hVRDyv2/u2MuF+Bw==", + "dev": true, + "dependencies": { + "@semantic-release/commit-analyzer": "^10.0.0", + "@semantic-release/error": "^4.0.0", + "@semantic-release/github": "^9.0.0", + "@semantic-release/npm": "^10.0.2", + "@semantic-release/release-notes-generator": "^11.0.0", + "aggregate-error": "^4.0.1", + "cosmiconfig": "^8.0.0", + "debug": "^4.0.0", + "env-ci": "^9.0.0", + "execa": "^7.0.0", + "figures": "^5.0.0", + "find-versions": "^5.1.0", + "get-stream": "^6.0.0", + "git-log-parser": "^1.2.0", + "hook-std": "^3.0.0", + "hosted-git-info": "^6.0.0", + "lodash-es": "^4.17.21", + "marked": "^5.0.0", + "marked-terminal": "^5.1.1", + "micromatch": "^4.0.2", + "p-each-series": "^3.0.0", + "p-reduce": "^3.0.0", + "read-pkg-up": "^10.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.3.2", + "semver-diff": "^4.0.0", + "signale": "^1.2.1", + "yargs": "^17.5.1" + }, + "bin": { + "semantic-release": "bin/semantic-release.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", + "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/signale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", + "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", + "dev": true, + "dependencies": { + "chalk": "^2.3.2", + "figures": "^2.0.0", + "pkg-conf": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/signale/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/signale/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/signale/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/signale/node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spawn-error-forwarder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", + "integrity": "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dev": true, + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/split2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", + "dev": true, + "dependencies": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", + "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", + "dev": true, + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-extensions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", + "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/traverse": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", + "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uglify-js": { + "version": "3.17.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", + "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "dev": true, + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7e973e4 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "ids-basecamp-clearinghouse", + "version": "0.0.1", + "description": "The IDS Clearing House Service is a prototype implementation of the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) component of the [Industrial Data Space](https://internationaldataspaces.org/).", + "main": "index.js", + "directories": { + "doc": "doc" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "repository": "https://github.com/truzzt/ids-basecamp-clearinghouse", + "author": "", + "license": "Apache-2.0", + "devDependencies": { + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/git": "^10.0.1", + "semantic-release": "^21.0.7" + } +} From 451277a1d77a6a1fffbd30fa27515d18ca728747 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 26 Jul 2023 11:24:20 +0200 Subject: [PATCH 002/183] add: prerelease settings --- .releaserc | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.releaserc b/.releaserc index 9f98b8c..87d910a 100644 --- a/.releaserc +++ b/.releaserc @@ -1,5 +1,5 @@ { - "branches": ["master"], + "branches": ["+([0-9])?(.{+([0-9]),x}).x", "master", {"name": "beta", "prerelease": true}, {"name": "alpha", "prerelease": true}], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", diff --git a/package.json b/package.json index 7e973e4..72050fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ids-basecamp-clearinghouse", - "version": "0.0.1", + "version": "1.0.0", "description": "The IDS Clearing House Service is a prototype implementation of the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) component of the [Industrial Data Space](https://internationaldataspaces.org/).", "main": "index.js", "directories": { From 4710fc0bde1a63ca6af2042a56b81b68c73860b1 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 26 Jul 2023 11:46:33 +0200 Subject: [PATCH 003/183] feat: release action --- .github/workflows/release.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..950d799 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: Release +on: + push: + branches: + - master + - alpha + - beta +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release From e0c40e2305e6433d64aeccc122187149a6ffcf5a Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 26 Jul 2023 13:04:49 +0200 Subject: [PATCH 004/183] chore: pr and issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 39 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 2 ++ .github/ISSUE_TEMPLATE/feature_request.md | 19 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 15 +++++++++ 4 files changed, 75 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..0f38032 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +# Bug Report + +## Describe the Bug +_A clear and concise description of the bug._ + +### Expected Behavior +_A clear and concise description of what you expected to happen._ + +### Observed Behavior +_A clear and concise description of what happened instead._ + +## Steps to Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## Context Information +_Add any other context about the problem here._ + +- Used version [e.g. EDC v1.0.0] +- OS: [e.g. iOS, Windows] +- ... + +## Detailed Description +_If applicable, add screenshots and logs to help explain your problem._ + +## Possible Implementation +_You already know the root cause of the erroneous state and how to fix it? Feel free to share your thoughts._ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..bd9dfe4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +--- +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..292266b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Help us with new ideas +title: '' +labels: '' +assignees: '' + +--- + +# Feature Request + +## Which Areas Would Be Affected? +_e.g., DPF, CI, build, transfer, etc._ + +## Why Is the Feature Desired? +_Are there any requirements?_ + +## Solution Proposal +_If possible, provide a (brief!) solution proposal._ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..55a8bd9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## What this PR changes/adds + +_Briefly describe WHAT your pr changes, which features it adds/modifies._ + +## Why it does that + +_Briefly state why the change was necessary._ + +## Further notes + +_List other areas of code that have changed but are not necessarily linked to the main feature. This could be method signature changes, package declarations, bugs that were encountered and were fixed inline, etc._ + +## Linked Issue(s) + +Closes # <-- _insert Issue number if one exists_ \ No newline at end of file From 807bcdf5fad95456dfcd008fcee990983facd711 Mon Sep 17 00:00:00 2001 From: dhommen Date: Mon, 7 Aug 2023 22:26:02 +0200 Subject: [PATCH 005/183] feat(ci): add test job for CH app --- .github/workflows/test.yml | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5f6935e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,64 @@ +name: test + +on: + pull_request: + branches: + - master + - beta + - alpha + + +jobs: + test-keyring: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Build and Test + run: | + cd clearing-house-app/keyring-api + cargo build --verbose + cargo test --verbose + + test-document: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Build and Test + run: | + cd clearing-house-app/document-api + cargo build --verbose + cargo test --verbose + + test-logging: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Build and Test + run: | + cd clearing-house-app/logging-service + cargo build --verbose + cargo test --verbose \ No newline at end of file From c69b246cf365c06ccfb23bdf0c85f0506f4a023e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 10 Aug 2023 19:58:35 +0200 Subject: [PATCH 006/183] fix(core): Disable integration tests, fix warnings and make the build reproducible --- .gitignore | 1 - clearing-house-app/Cargo.lock | 3164 +++++++++++++++++ clearing-house-app/core-lib/Cargo.toml | 2 +- clearing-house-app/core-lib/src/model/mod.rs | 59 +- .../tests/integration/daps_api_client.rs | 6 +- .../tests/integration/document_api_client.rs | 6 +- .../tests/integration/keyring_api_client.rs | 6 +- .../core-lib/tests/integration/main.rs | 7 +- .../tests/integration/token_validation.rs | 3 + clearing-house-app/document-api/src/db/mod.rs | 8 +- .../document-api/src/db/tests.rs | 6 +- .../document-api/src/doc_api.rs | 2 +- clearing-house-app/keyring-api/src/db/mod.rs | 3 +- .../logging-service/src/main.rs | 1 - .../logging-service/src/model/ids/mod.rs | 12 +- 15 files changed, 3234 insertions(+), 52 deletions(-) create mode 100644 clearing-house-app/Cargo.lock diff --git a/.gitignore b/.gitignore index 86bfd07..ca4b103 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,4 @@ target data *.log .idea/ -**/*.lock **/*.iml \ No newline at end of file diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock new file mode 100644 index 0000000..3382892 --- /dev/null +++ b/clearing-house-app/Cargo.lock @@ -0,0 +1,3164 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202a43562bc3e159554b7590f5fd1f432d9e8de0cc2c2ce4bb8d194a34b3b0f3" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "async-trait" +version = "0.1.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "binascii" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" + +[[package]] +name = "biscuit" +version = "0.6.0" +source = "git+https://github.com/lawliet89/biscuit?branch=master#16d5c91c0576ec40ec655b7f107b4df19fe4186f" +dependencies = [ + "chrono", + "data-encoding", + "num-bigint", + "num-traits", + "once_cell", + "ring", + "serde", + "serde_json", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bson" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aeb8bae494e49dbc330dd23cf78f6f7accee22f640ce3ab17841badaa4ce232" +dependencies = [ + "ahash", + "base64 0.13.1", + "bitvec", + "hex", + "indexmap 1.9.3", + "js-sys", + "lazy_static", + "rand", + "serde", + "serde_bytes", + "serde_json", + "time 0.3.23", + "uuid 1.4.1", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time 0.3.23", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "core-lib" +version = "0.10.0" +dependencies = [ + "aes", + "aes-gcm-siv", + "base64 0.9.3", + "biscuit", + "blake2-rfc", + "chrono", + "error-chain", + "fern", + "figment", + "generic-array", + "hex", + "log", + "mongodb", + "num-bigint", + "openssh-keys", + "percent-encoding", + "reqwest", + "ring", + "rocket", + "serde", + "serde_derive", + "serde_json", + "uuid 0.8.2", +] + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn 1.0.109", +] + +[[package]] +name = "devise" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" +dependencies = [ + "devise_core", + "quote", +] + +[[package]] +name = "devise_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" +dependencies = [ + "bitflags 2.3.3", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "document-api" +version = "0.10.0" +dependencies = [ + "biscuit", + "chrono", + "core-lib", + "error-chain", + "fern", + "futures", + "hex", + "log", + "mongodb", + "rocket", + "rocket_cors", + "serde", + "serde_derive", + "serde_json", + "tokio", + "tokio-test", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "fern" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e69ab0d5aca163e388c3a49d284fed6c3d0810700e77c5ae2756a50ec1a4daaa" +dependencies = [ + "chrono", + "log", +] + +[[package]] +name = "figment" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" +dependencies = [ + "atomic", + "pear", + "serde", + "serde_yaml", + "toml", + "uncased", + "version_check", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest 0.9.0", + "hmac 0.10.1", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.3", + "widestring", + "windows-sys", + "winreg 0.50.0", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keyring-api" +version = "0.10.0" +dependencies = [ + "aes", + "aes-gcm-siv", + "base64 0.9.3", + "biscuit", + "chrono", + "core-lib", + "error-chain", + "fern", + "generic-array", + "hex", + "hkdf", + "log", + "mongodb", + "openssl", + "rocket", + "serde", + "serde_derive", + "serde_json", + "sha2 0.9.9", + "tokio", + "tokio-test", + "yaml-rust", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "logging-service" +version = "0.10.0" +dependencies = [ + "biscuit", + "chrono", + "core-lib", + "error-chain", + "fern", + "log", + "mongodb", + "percent-encoding", + "rocket", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "mongodb" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd85ec209a5b84fd9f54b9e381f6fa17462bc74160d018fc94fd8b9f61faa8" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bitflags 1.3.2", + "bson", + "chrono", + "derivative", + "derive_more", + "futures-core", + "futures-executor", + "futures-io", + "futures-util", + "hex", + "hmac 0.12.1", + "lazy_static", + "md-5 0.10.5", + "pbkdf2", + "percent-encoding", + "rand", + "rustc_version_runtime", + "rustls", + "rustls-pemfile", + "serde", + "serde_bytes", + "serde_with", + "sha-1", + "sha2 0.10.7", + "socket2 0.4.9", + "stringprep", + "strsim", + "take_mut", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", + "typed-builder", + "uuid 1.4.1", + "webpki-roots", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.8", + "tokio", + "tokio-util", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssh-keys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7249a699cdeea261ac73f1bf9350777cb867324f44373aafb5a287365bf1771" +dependencies = [ + "base64 0.13.1", + "byteorder", + "md-5 0.9.1", + "sha2 0.9.9", + "thiserror", +] + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pear" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi 1.0.0-rc", +] + +[[package]] +name = "pear_codegen" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", + "version_check", + "yansi 1.0.0-rc", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "ref-cast" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ef7e18e8841942ddb1cf845054f8008410030a3997875d9e49b7a363063df1" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfaf0c85b766276c797f3791f5bc6d5bd116b41d53049af2789666b0c0bc9fa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.3.3", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.10.1", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rocket" +version = "0.5.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58734f7401ae5cfd129685b48f61182331745b357b96f2367f01aebaf1cc9cc9" +dependencies = [ + "async-stream", + "async-trait", + "atomic", + "binascii", + "bytes", + "either", + "figment", + "futures", + "indexmap 1.9.3", + "is-terminal", + "log", + "memchr", + "multer", + "num_cpus", + "parking_lot", + "pin-project-lite", + "rand", + "ref-cast", + "rocket_codegen", + "rocket_http", + "serde", + "serde_json", + "state", + "tempfile", + "time 0.3.23", + "tokio", + "tokio-stream", + "tokio-util", + "ubyte", + "version_check", + "yansi 0.5.1", +] + +[[package]] +name = "rocket_codegen" +version = "0.5.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7093353f14228c744982e409259fb54878ba9563d08214f2d880d59ff2fc508b" +dependencies = [ + "devise", + "glob", + "indexmap 1.9.3", + "proc-macro2", + "quote", + "rocket_http", + "syn 2.0.27", + "unicode-xid", +] + +[[package]] +name = "rocket_cors" +version = "0.6.0-alpha2" +source = "git+https://github.com/lawliet89/rocket_cors?branch=master#985098dd8f3b052716111eaa872d184cc21a1a68" +dependencies = [ + "http", + "log", + "regex", + "rocket", + "serde", + "serde_derive", + "unicase", + "unicase_serde", + "url", +] + +[[package]] +name = "rocket_http" +version = "0.5.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936012c99162a03a67f37f9836d5f938f662e26f2717809761a9ac46432090f4" +dependencies = [ + "cookie", + "either", + "futures", + "http", + "hyper", + "indexmap 1.9.3", + "log", + "memchr", + "pear", + "percent-encoding", + "pin-project-lite", + "ref-cast", + "serde", + "smallvec", + "stable-pattern", + "state", + "time 0.3.23", + "tokio", + "uncased", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.18", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31b7153270ebf48bf91c65ae5b0c00e749c4cfad505f66530ac74950249582f" +dependencies = [ + "rustc_version 0.2.3", + "semver 0.9.0", +] + +[[package]] +name = "rustix" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "indexmap 2.0.0", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable-pattern" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" +dependencies = [ + "memchr", +] + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "stringprep" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.4.9", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trust-dns-proto" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typed-builder" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ubyte" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c81f0dae7d286ad0d9366d7679a77934cfc3cf3a8d67e82669794412b2368fe6" +dependencies = [ + "serde", +] + +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicase_serde" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef53697679d874d69f3160af80bc28de12730a985d57bdf2b47456ccb8b11f1" +dependencies = [ + "serde", + "unicase", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna 0.4.0", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "yansi" +version = "1.0.0-rc" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee746ad3851dd3bc40e4a028ab3b00b99278d929e48957bcb2d111874a7e43e" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/clearing-house-app/core-lib/Cargo.toml b/clearing-house-app/core-lib/Cargo.toml index e3977e6..140385c 100644 --- a/clearing-house-app/core-lib/Cargo.toml +++ b/clearing-house-app/core-lib/Cargo.toml @@ -25,7 +25,7 @@ mongodb ="2.3.0" num-bigint = "0.4.3" openssh-keys = "0.5.0" percent-encoding = "2.1.0" -reqwest = { version="0.11.11", features = ["default", "json"]} +reqwest = { version="0.11.11", features = ["default", "json", "blocking"]} ring = "0.16.20" rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0" diff --git a/clearing-house-app/core-lib/src/model/mod.rs b/clearing-house-app/core-lib/src/model/mod.rs index f6e2124..ab2124e 100644 --- a/clearing-house-app/core-lib/src/model/mod.rs +++ b/clearing-house-app/core-lib/src/model/mod.rs @@ -1,10 +1,9 @@ -use chrono::{Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime}; - pub mod crypto; pub mod document; pub mod process; -#[cfg(test)] mod tests; +#[cfg(test)] +mod tests; pub fn new_uuid() -> String { use uuid::Uuid; @@ -12,34 +11,33 @@ pub fn new_uuid() -> String { } #[derive(Debug, Clone, Serialize, Deserialize, FromFormField)] -pub enum SortingOrder{ +pub enum SortingOrder { #[field(value = "asc")] #[serde(rename = "asc")] Ascending, #[field(value = "desc")] #[serde(rename = "desc")] - Descending + Descending, } -pub fn parse_date(date: Option, to_date: bool) -> Option{ +pub fn parse_date(date: Option, to_date: bool) -> Option { let time_format; - if to_date{ + if to_date { time_format = "23:59:59" - } - else{ + } else { time_format = "00:00:00" } - match date{ + match date { Some(d) => { debug!("Parsing date: {}", &d); - match NaiveDateTime::parse_from_str(format!("{} {}",&d, &time_format).as_str(), "%Y-%m-%d %H:%M:%S"){ + match chrono::NaiveDateTime::parse_from_str(format!("{} {}", &d, &time_format).as_str(), "%Y-%m-%d %H:%M:%S") { Ok(date) => { Some(date) } Err(e) => { error!("Error occurred: {:#?}", e); - return None + return None; } } } @@ -47,11 +45,11 @@ pub fn parse_date(date: Option, to_date: bool) -> Option{ } } -pub fn sanitize_dates(date_from: Option, date_to: Option) -> (NaiveDateTime, NaiveDateTime){ - let default_to_date = Local::now().naive_local(); - let d = NaiveDate::from_ymd(default_to_date.year(), default_to_date.month(), default_to_date.day()); - let t = NaiveTime::from_hms(0, 0, 0); - let default_from_date = NaiveDateTime::new(d,t) - Duration::weeks(2); +pub fn sanitize_dates(date_from: Option, date_to: Option) -> (chrono::NaiveDateTime, chrono::NaiveDateTime) { + let default_to_date = chrono::Local::now().naive_local(); + let default_from_date = default_to_date.date() + .and_hms_opt(0, 0, 0) + .expect("00:00:00 is a valid time") - chrono::Duration::weeks(2); println!("date_to: {:#?}", date_to); println!("date_from: {:#?}", date_from); @@ -59,26 +57,19 @@ pub fn sanitize_dates(date_from: Option, date_to: Option date_to - if date_from.is_some() && date_to.is_some(){ - return (date_from.unwrap(), date_to.unwrap()) + match (date_from, date_to) { + (Some(from), Some(to)) => (from, to), // validate already checked that date_from > date_to + (Some(from), None) => (from, default_to_date), // if to_date is missing, default to now + (None, Some(_to)) => todo!("Not defined yet; check"), + (None, None) => (default_from_date, default_to_date), // if both dates are none (case to_date is none and from_date is_some should be catched by validation); return dates for default duration (last 2 weeks) } - - // if to_date is missing, default to now - if date_from.is_some() && date_to.is_none(){ - return (date_from.unwrap(), default_to_date) - } - - // if both dates are none (case to_date is none and from_date is_some should be catched by validation) - // return dates for default duration (last 2 weeks) - return (default_from_date, default_to_date) } -pub fn validate_dates(date_from: Option, date_to: Option) -> bool{ - let date_now = Local::now().naive_local(); +pub fn validate_dates(date_from: Option, date_to: Option) -> bool { + let date_now = chrono::Local::now().naive_local(); debug!("... validating dates: now: {:#?} , from: {:#?} , to: {:#?}", &date_now, &date_from, &date_to); // date_from before now - if date_from.is_some() && date_from.as_ref().unwrap().clone() > date_now{ + if date_from.is_some() && date_from.as_ref().unwrap().clone() > date_now { debug!("oh no, date_from {:#?} is in the future! date_now is {:#?}", &date_from, &date_now); return false; } @@ -89,13 +80,13 @@ pub fn validate_dates(date_from: Option, date_to: Option= date_now{ + if date_to.is_some() && date_to.as_ref().unwrap().clone() >= date_now { debug!("oh no, date_to {:#?} is in the future! date_now is {:#?}", &date_to, &date_now); return false; } // date_from before date_to - if date_from.is_some() && date_to.is_some(){ + if date_from.is_some() && date_to.is_some() { if date_from.unwrap() > date_to.unwrap() { debug!("oh no, date_from {:#?} is before date_to {:#?}", &date_from, &date_to); return false; diff --git a/clearing-house-app/core-lib/tests/integration/daps_api_client.rs b/clearing-house-app/core-lib/tests/integration/daps_api_client.rs index f0fe59a..afe38bf 100644 --- a/clearing-house-app/core-lib/tests/integration/daps_api_client.rs +++ b/clearing-house-app/core-lib/tests/integration/daps_api_client.rs @@ -1,3 +1,5 @@ +/* TODO: Integration test currently not necessary + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // These tests are integration tests and need an up-and-running keyring-api // Use config.yml to configure the urls correctly. @@ -24,4 +26,6 @@ fn test_get_jwks() -> Result<()>{ assert_eq!(KeyType::RSA, jwk.algorithm.key_type()); assert_eq!(DAPS_KID, jwk.common.key_id.unwrap()); Ok(()) -} \ No newline at end of file +} + +*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/document_api_client.rs b/clearing-house-app/core-lib/tests/integration/document_api_client.rs index 69f0670..50dbe56 100644 --- a/clearing-house-app/core-lib/tests/integration/document_api_client.rs +++ b/clearing-house-app/core-lib/tests/integration/document_api_client.rs @@ -1,3 +1,5 @@ +/* TODO: Integration test currently not necessary + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // These tests are integration tests and need an up-and-running keyring-api and // document-api. Use config.yml to configure the urls correctly. @@ -221,4 +223,6 @@ fn test_create_document_url_encoded_id() -> Result<()>{ delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; Ok(()) -} \ No newline at end of file +} + +*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs b/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs index 53fc29d..26b35c6 100644 --- a/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs +++ b/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs @@ -1,3 +1,5 @@ +/* TODO: Integration test currently not necessary + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // These tests are integration tests and need an up-and-running keyring-api // Use config.yml to configure the urls correctly. @@ -84,4 +86,6 @@ fn test_decrypt_keys() -> Result<()> { delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; Ok(()) -} \ No newline at end of file +} + +*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/main.rs b/clearing-house-app/core-lib/tests/integration/main.rs index 69e3b9a..81e5e7d 100644 --- a/clearing-house-app/core-lib/tests/integration/main.rs +++ b/clearing-house-app/core-lib/tests/integration/main.rs @@ -1,4 +1,5 @@ -use reqwest::{Client, StatusCode}; +use reqwest::blocking::{Client}; +use reqwest::StatusCode; use reqwest::header::{CONTENT_TYPE, HeaderValue}; use core_lib::constants::ROCKET_DOC_TYPE_API; @@ -9,12 +10,14 @@ use core_lib::model::document::{Document, DocumentPart}; pub const TOKEN: &'static str = "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzUyNDEyNzgsImlhdCI6MTYzNTI0MTI3OCwianRpIjoiT0RBNE5EazRNemsxT0RZMU16TXlOamN4TlE9PSIsImV4cCI6MTYzNTI0NDg3OCwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpUUlVTVF9TRUNVUklUWV9QUk9GSUxFIiwicmVmZXJyaW5nQ29ubmVjdG9yIjoiaHR0cDovL2NvbnN1bWVyLWNvcmUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiYzE1ZTY1NTgwODhkYmZlZjIxNWE0M2QyNTA3YmJkMTI0ZjQ0ZmI4ZmFjZDU2MWMxNDU2MWEyYzFhNjY5ZDBlMCIsInN1YiI6IkE1OjBDOkE1OkYwOjg0OkQ5OjkwOkJCOkJDOkQ5OjU3OjNBOjA0OkM4OjdGOjkzOkVEOjk3OkEyOjUyOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.iemDKZXE_RXFKkffqpweTAXBb6YX0spU0b5Ez1ncQzEyDNkJ5UtsZkwZz8WqfWOdPqMA74ShzLMwfEtao3DoO4DfWrvXFAYh8Y6hHJjHO44kPm4rUdcymUsVLXxcWd8Jszi6HjRHLaJ1-466s1akDQ7yQB0l8g9PP7BOlYr2I00HZ_b5wQOWtwT2PQxeWjkBzTgP8iycF7kIT6jgTHYDkOAwIdiMgNH_dPaxOPfxupz5vJQPuC1o9-IAyXtk-yC9GNI18YtjYpqizB-Nm5QGlUSSYMrB7tUKEc46471QaC4tR_LkYDrGnDtJHrH_fq0eEe6wIKoUcdt_VnI9Km-Hpw"; pub const TEST_CONFIG: &'static str = "config.yml"; - +/* TODO: Disable all integration tests for now mod document_api_client; mod keyring_api_client; mod daps_api_client; mod token_validation; + */ + fn create_test_document(pid: &String, dt_id: &String, tc: i64) -> Document{ let p1 = DocumentPart::new(String::from("name"), Some(String::from("This is document part name."))); let p2 = DocumentPart::new(String::from("payload"), Some(String::from("This is document part payload."))); diff --git a/clearing-house-app/core-lib/tests/integration/token_validation.rs b/clearing-house-app/core-lib/tests/integration/token_validation.rs index 4f2d92a..305ab0f 100644 --- a/clearing-house-app/core-lib/tests/integration/token_validation.rs +++ b/clearing-house-app/core-lib/tests/integration/token_validation.rs @@ -1,3 +1,5 @@ +/* TODO: Integration test currently not necessary + use biscuit::jwa::SignatureAlgorithm; use biscuit::jwk::JWKSet; use biscuit::{CompactJson, Empty}; @@ -54,3 +56,4 @@ fn test_invalid_claims() -> Result<()>{ assert!(jwt.is_err(), "Token is valid. this should not happen, really!"); Ok(()) } +*/ \ No newline at end of file diff --git a/clearing-house-app/document-api/src/db/mod.rs b/clearing-house-app/document-api/src/db/mod.rs index e2afa40..5f682c7 100644 --- a/clearing-house-app/document-api/src/db/mod.rs +++ b/clearing-house-app/document-api/src/db/mod.rs @@ -14,7 +14,9 @@ use core_lib::model::SortingOrder; use crate::db::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; mod bucket; -#[cfg(test)] mod tests; + +// TODO: Disabled integration tests with database +// #[cfg(test)] mod tests; #[derive(Clone, Debug)] pub struct DatastoreConfigurator; @@ -145,10 +147,10 @@ impl DataStoreApi for DataStore { impl DataStore { - pub async fn add_document(&self, doc: &EncryptedDocument) -> Result{ + pub async fn add_document(&self, doc: EncryptedDocument) -> Result{ debug!("add_document to bucket"); let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - let bucket_update = DocumentBucketUpdate::from(doc); + let bucket_update = DocumentBucketUpdate::from(&doc); let mut update_options = UpdateOptions::default(); update_options.upsert = Some(true); let id = format!("^{}_", doc.pid.clone()); diff --git a/clearing-house-app/document-api/src/db/tests.rs b/clearing-house-app/document-api/src/db/tests.rs index 4fa5b7e..c7bc226 100644 --- a/clearing-house-app/document-api/src/db/tests.rs +++ b/clearing-house-app/document-api/src/db/tests.rs @@ -95,7 +95,7 @@ async fn test_delete_document_doc_is_deleted() -> Result<()>{ assert_eq!(db.exists_document(&id).await?, true); // run the test - assert!(db.delete_document(&id).await?); + //assert!(db.delete_document(&id).await?); // db should not find document anymore assert_eq!(db.exists_document(&id).await?, false); @@ -127,7 +127,7 @@ async fn test_delete_document_check_others() -> Result<()>{ assert_eq!(db.exists_document(&id2).await?, true); // run the test - assert!(db.delete_document(&id1).await?); + //assert!(db.delete_document(&id1).await?); // db should still find the other document assert_eq!(db.exists_document(&id2).await?, true); @@ -153,7 +153,7 @@ async fn test_delete_document_on_not_existing_doc() -> Result<()>{ db.add_document(doc.clone()).await?; // run the test - assert_eq!(db.delete_document(&id2).await?, false); + // assert_eq!(db.delete_document(&id2).await?, false); // clean up tear_down(db).await; diff --git a/clearing-house-app/document-api/src/doc_api.rs b/clearing-house-app/document-api/src/doc_api.rs index eb5edc9..ab182d0 100644 --- a/clearing-house-app/document-api/src/doc_api.rs +++ b/clearing-house-app/document-api/src/doc_api.rs @@ -108,7 +108,7 @@ async fn create_enc_document( debug!("storing document ...."); // store document - match db.add_document(&enc_doc).await { + match db.add_document(enc_doc).await { Ok(_b) => ApiResponse::SuccessCreate(json!(receipt)), Err(e) => { error!("Error while adding: {:?}", e); diff --git a/clearing-house-app/keyring-api/src/db/mod.rs b/clearing-house-app/keyring-api/src/db/mod.rs index 9145161..002d67b 100644 --- a/clearing-house-app/keyring-api/src/db/mod.rs +++ b/clearing-house-app/keyring-api/src/db/mod.rs @@ -13,7 +13,8 @@ use crate::model::doc_type::DocumentType; pub(crate) mod doc_type; -#[cfg(test)] mod tests; +// TODO: Disabled integration tests with database +// #[cfg(test)] mod tests; #[derive(Clone, Debug)] pub struct KeyStore { diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 6a953a5..7c5c713 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -1,7 +1,6 @@ #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; -use std::env; use std::path::Path; use core_lib::api::client::{ApiClientConfigurator, ApiClientEnum}; use core_lib::util::{add_service_config, setup_logger}; diff --git a/clearing-house-app/logging-service/src/model/ids/mod.rs b/clearing-house-app/logging-service/src/model/ids/mod.rs index 4edd625..e143889 100644 --- a/clearing-house-app/logging-service/src/model/ids/mod.rs +++ b/clearing-house-app/logging-service/src/model/ids/mod.rs @@ -187,10 +187,18 @@ pub struct IdsQueryResult{ impl IdsQueryResult{ pub fn new(date_from: i64, date_to: i64, page: Option, size: Option, order: String, documents: Vec) -> IdsQueryResult{ + let date_from = NaiveDateTime::from_timestamp_opt(date_from, 0) + .expect("Invalid date_from seconds") + .format("%Y-%m-%d %H:%M:%S") + .to_string(); + let date_to = NaiveDateTime::from_timestamp_opt(date_to, 0) + .expect("Invalid date_to seconds") + .format("%Y-%m-%d %H:%M:%S") + .to_string(); IdsQueryResult{ - date_from: NaiveDateTime::from_timestamp(date_from, 0).format("%Y-%m-%d %H:%M:%S").to_string(), - date_to: NaiveDateTime::from_timestamp(date_to, 0).format("%Y-%m-%d %H:%M:%S").to_string(), + date_from, + date_to, page: page.unwrap_or(-1), size: size.unwrap_or(-1), order, From 89f39f784180b4bd26813f33e7787d0744fe975c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 10 Aug 2023 20:28:56 +0200 Subject: [PATCH 007/183] fix(app): Fix warnings and build on development branch --- .github/workflows/rust.yml | 6 ++++-- clearing-house-app/core-lib/src/db/public_db.rs | 4 ++-- clearing-house-app/core-lib/tests/integration/main.rs | 4 ++-- clearing-house-app/logging-service/src/logging_api.rs | 8 ++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index dac798e..839693d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,8 +4,10 @@ on: push: branches: - master + - development pull_request: - branches: [ master ] + branches: + - master env: CARGO_TERM_COLOR: always @@ -16,7 +18,7 @@ env: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install minimal stable diff --git a/clearing-house-app/core-lib/src/db/public_db.rs b/clearing-house-app/core-lib/src/db/public_db.rs index 0babcf2..864ec45 100644 --- a/clearing-house-app/core-lib/src/db/public_db.rs +++ b/clearing-house-app/core-lib/src/db/public_db.rs @@ -1,5 +1,5 @@ -use crate::mongodb::{ - Bson, +use mongodb::{ + bson::Bson, db::ThreadedDatabase, doc, coll::options::FindOneAndUpdateOptions diff --git a/clearing-house-app/core-lib/tests/integration/main.rs b/clearing-house-app/core-lib/tests/integration/main.rs index 81e5e7d..799d36a 100644 --- a/clearing-house-app/core-lib/tests/integration/main.rs +++ b/clearing-house-app/core-lib/tests/integration/main.rs @@ -49,7 +49,7 @@ fn insert_test_doc_type_into_keyring(token: &String, pid: &String, dt_id: &Strin println!("json_data: {}", json_data); println!("calling {}", &dt_url); - let mut response = client + let response = client .post(dt_url.as_str()) .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) .bearer_auth(token) @@ -72,7 +72,7 @@ fn delete_test_doc_type_from_keyring(token: &String, pid: &String, dt_id: &Strin let dt_url = format!("http://localhost:8002{}/{}/{}", ROCKET_DOC_TYPE_API, pid, dt_id); println!("calling {}", &dt_url); - let mut response = client + let response = client .delete(dt_url.as_str()) .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) .bearer_auth(token) diff --git a/clearing-house-app/logging-service/src/logging_api.rs b/clearing-house-app/logging-service/src/logging_api.rs index 0704793..c5e5de5 100644 --- a/clearing-house-app/logging-service/src/logging_api.rs +++ b/clearing-house-app/logging-service/src/logging_api.rs @@ -243,7 +243,7 @@ async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/?&&&&", format = "json", data = "")] +#[post("/?&&&&", format = "json", data = "<_message>")] async fn query_pid( ch_claims: ChClaims, db: &State, @@ -254,7 +254,7 @@ async fn query_pid( date_from: Option, doc_api: &State, pid: String, - message: Json + _message: Json ) -> ApiResponse { debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); @@ -336,8 +336,8 @@ async fn query_pid( } } -#[post("//", format = "json", data = "")] -async fn query_id(ch_claims: ChClaims, db: &State, doc_api: &State, pid: String, id: String, message: Json) -> ApiResponse { +#[post("//", format = "json", data = "<_message>")] +async fn query_id(ch_claims: ChClaims, db: &State, doc_api: &State, pid: String, id: String, _message: Json) -> ApiResponse { trace!("...user '{:?}'", &ch_claims.client_id); let user = &ch_claims.client_id; From 851146eb3c546f6813d3209beee367b84ee1ffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 10 Aug 2023 20:33:07 +0200 Subject: [PATCH 008/183] fix(app): Fix build on development branch --- .github/workflows/rust.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 839693d..67f5334 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,11 +29,8 @@ jobs: # TODO: do not use caching for actual release builds, aka ones that start with v* - uses: Swatinem/rust-cache@v2 - name: Build clearing-house-api - run: | - cd clearing-house-app - eval "$(ssh-agent -s)" - ssh-add - <<< "${{ secrets.IDS_CLEARING_HOUSE_CORE_TOKEN }}" - cargo build --release + working-directory: ./clearing-house-app + run: cargo build --release - name: Build build images run: | From 453ce8810ddd5970f0d7c349f142ea5f24db8b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 10 Aug 2023 20:43:51 +0200 Subject: [PATCH 009/183] fix(ci): Fix unauthorized push --- .github/workflows/rust.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 67f5334..ab67af7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,6 +14,7 @@ env: IMAGE_NAME_LS: ids-ch-logging-service IMAGE_NAME_DA: ids-ch-document-api IMAGE_NAME_KA: ids-ch-keyring-api + IMAGE_BASE: ghcr.io/truzzt/ids-basecamp-clearinghouse jobs: @@ -21,11 +22,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install minimal stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true # TODO: do not use caching for actual release builds, aka ones that start with v* - uses: Swatinem/rust-cache@v2 - name: Build clearing-house-api @@ -43,9 +39,9 @@ jobs: - name: Push image run: | - IMAGE_ID_LS=ghcr.io/Fraunhofer-AISEC/$IMAGE_NAME_LS - IMAGE_ID_DA=ghcr.io/Fraunhofer-AISEC/$IMAGE_NAME_DA - IMAGE_ID_KA=ghcr.io/Fraunhofer-AISEC/$IMAGE_NAME_KA + IMAGE_ID_LS=$IMAGE_BASE/$IMAGE_NAME_LS + IMAGE_ID_DA=$IMAGE_BASE/$IMAGE_NAME_DA + IMAGE_ID_KA=$IMAGE_BASE/$IMAGE_NAME_KA # Change all uppercase to lowercase IMAGE_ID_LS=$(echo $IMAGE_ID_LS | tr '[A-Z]' '[a-z]') From f1a8e5969006156c931ce39a7225b8e3acea56a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 15 Aug 2023 13:43:19 +0200 Subject: [PATCH 010/183] feat(ch-app): Created services for Keyring- and Document-Service inside logging service and adjusted the handlers --- clearing-house-app/Cargo.lock | 148 ++++++-- clearing-house-app/logging-service/Cargo.toml | 10 +- .../logging-service/src/crypto.rs | 161 +++++++++ .../logging-service/src/db/docstore.rs | 322 ++++++++++++++++++ .../logging-service/src/db/keystore.rs | 146 ++++++++ .../logging-service/src/db/mod.rs | 3 + .../logging-service/src/logging_api.rs | 5 +- .../logging-service/src/main.rs | 2 + .../logging-service/src/model/crypto.rs | 29 ++ .../logging-service/src/model/doc_type.rs | 29 ++ .../logging-service/src/model/mod.rs | 2 + .../src/services/document_service.rs | 281 +++++++++++++++ .../src/services/keyring_service.rs | 169 +++++++++ .../logging-service/src/services/mod.rs | 2 + 14 files changed, 1285 insertions(+), 24 deletions(-) create mode 100644 clearing-house-app/logging-service/src/crypto.rs create mode 100644 clearing-house-app/logging-service/src/db/docstore.rs create mode 100644 clearing-house-app/logging-service/src/db/keystore.rs create mode 100644 clearing-house-app/logging-service/src/model/crypto.rs create mode 100644 clearing-house-app/logging-service/src/model/doc_type.rs create mode 100644 clearing-house-app/logging-service/src/services/document_service.rs create mode 100644 clearing-house-app/logging-service/src/services/keyring_service.rs create mode 100644 clearing-house-app/logging-service/src/services/mod.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 3382892..673600d 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -26,6 +26,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.6.0" @@ -34,7 +44,18 @@ checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" dependencies = [ "aes-soft", "aesni", - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", ] [[package]] @@ -43,11 +64,26 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202a43562bc3e159554b7590f5fd1f432d9e8de0cc2c2ce4bb8d194a34b3b0f3" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "polyval", + "aead 0.3.2", + "aes 0.6.0", + "cipher 0.2.5", + "ctr 0.6.0", + "polyval 0.4.5", + "subtle", + "zeroize", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead 0.5.2", + "aes 0.8.3", + "cipher 0.4.4", + "ctr 0.9.2", + "polyval 0.6.1", "subtle", "zeroize", ] @@ -58,7 +94,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -68,7 +104,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -107,6 +143,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f768393e7fabd388fe8409b13faa4d93ab0fef35db1508438dfdb066918bcf38" + [[package]] name = "arrayvec" version = "0.4.12" @@ -347,6 +389,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -390,8 +442,8 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" name = "core-lib" version = "0.10.0" dependencies = [ - "aes", - "aes-gcm-siv", + "aes 0.6.0", + "aes-gcm-siv 0.9.0", "base64 0.9.3", "biscuit", "blake2-rfc", @@ -437,6 +489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -456,7 +509,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" dependencies = [ - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", ] [[package]] @@ -924,6 +986,15 @@ dependencies = [ "hmac 0.10.1", ] +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.10.1" @@ -1102,6 +1173,15 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -1150,8 +1230,8 @@ dependencies = [ name = "keyring-api" version = "0.10.0" dependencies = [ - "aes", - "aes-gcm-siv", + "aes 0.6.0", + "aes-gcm-siv 0.9.0", "base64 0.9.3", "biscuit", "chrono", @@ -1160,7 +1240,7 @@ dependencies = [ "fern", "generic-array", "hex", - "hkdf", + "hkdf 0.10.0", "log", "mongodb", "openssl", @@ -1218,18 +1298,26 @@ checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" name = "logging-service" version = "0.10.0" dependencies = [ + "aes 0.8.3", + "aes-gcm-siv 0.11.1", + "anyhow", "biscuit", "chrono", "core-lib", "error-chain", "fern", + "generic-array", + "hex", + "hkdf 0.12.3", "log", "mongodb", + "openssl", "percent-encoding", "rocket", "serde", "serde_derive", "serde_json", + "sha2 0.10.7", ] [[package]] @@ -1506,9 +1594,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -1538,9 +1626,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -1641,7 +1729,19 @@ checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" dependencies = [ "cpuid-bool", "opaque-debug", - "universal-hash", + "universal-hash 0.4.1", +] + +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash 0.5.1", ] [[package]] @@ -2819,6 +2919,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.9" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 19b1333..afb441e 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -19,4 +19,12 @@ percent-encoding = "2.1.0" rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0" serde_derive = "1.0" -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" +anyhow = "1.0.73" +hex = "0.4.3" +aes = "0.8.3" +aes-gcm-siv = "0.11.1" +hkdf = "0.12.3" +sha2 = "0.10.7" +generic-array = "0.14.7" +openssl = "0.10.56" diff --git a/clearing-house-app/logging-service/src/crypto.rs b/clearing-house-app/logging-service/src/crypto.rs new file mode 100644 index 0000000..a361176 --- /dev/null +++ b/clearing-house-app/logging-service/src/crypto.rs @@ -0,0 +1,161 @@ +use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; +use aes_gcm_siv::aead::Aead; +use core_lib::model::crypto::{KeyEntry, KeyMap}; +use generic_array::GenericArray; +use hkdf::Hkdf; +use openssl::rand::rand_bytes; +use sha2::Sha256; +use std::collections::HashMap; +use anyhow::anyhow; +use crate::model::doc_type::DocumentType; +use crate::model::crypto::MasterKey; + +const EXP_KEY_SIZE: usize = 32; +const EXP_NONCE_SIZE: usize = 12; +const EXP_BUFF_SIZE: usize = 44; + +fn initialize_kdf() -> (String, Hkdf) { + let salt = generate_random_seed(); + let ikm = generate_random_seed(); + let (master_key, kdf) = Hkdf::::extract(Some(&salt), &ikm); + (hex::encode_upper(master_key), kdf) +} + +pub fn generate_random_seed() -> Vec{ + let mut buf = [0u8; 256]; + rand_bytes(&mut buf).unwrap(); // TODO: Replace with some other cryptographically secure random number generator + buf.to_vec() +} + +fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap{ + let mut key_map = HashMap::new(); + let mut okm = [0u8; EXP_BUFF_SIZE]; + let mut i = 0; + dt.parts.iter() + .for_each( |p| { + if kdf.expand(p.name.clone().as_bytes(), &mut okm).is_ok() { + let map_key = match enc{ + true => p.name.clone(), + false => i.to_string() + }; + key_map.insert(map_key, KeyEntry::new(i.to_string(), okm[..EXP_KEY_SIZE].to_vec(), okm[EXP_KEY_SIZE..].to_vec())); + } + i = i +1; + }); + key_map +} + +pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result{ + debug!("generating encryption key_map for doc type: '{}'", &dt.id); + let (secret, doc_kdf) = initialize_kdf(); + let key_map = derive_key_map(doc_kdf, dt, true); + + debug!("encrypting the key seed"); + let kdf = restore_kdf(&mkey.key)?; + let mut okm = [0u8; EXP_BUFF_SIZE]; + if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err(){ + return Err(anyhow!("Error while generating key")); + } + match encrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], secret){ + Ok(ct) => Ok(KeyMap::new(true, key_map, Some(ct))), + Err(e) => { + error!("Error while encrypting key seed: {:?}", e); + Err(anyhow!("Error while encrypting key seed!")) + } + } +} + +pub fn restore_key_map(mkey: MasterKey, dt: DocumentType, keys_ct: Vec) -> anyhow::Result{ + debug!("decrypting the key seed"); + let kdf = restore_kdf(&mkey.key)?; + let mut okm = [0u8; EXP_BUFF_SIZE]; + if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err(){ + return Err(anyhow!("Error while generating key")); + } + + match decrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], &keys_ct){ + Ok(key_seed) => { + // generate new random key map + restore_keys(&key_seed, dt) + } + Err(e) => { + error!("Error while decrypting key ciphertext: {}", e); + Err(anyhow!("Error while decrypting keys")) + } + } +} + +pub fn restore_keys(secret: &String, dt: DocumentType) -> anyhow::Result{ + debug!("restoring decryption key_map for doc type: '{}'", &dt.id); + let kdf = restore_kdf(secret)?; + let key_map = derive_key_map(kdf, dt, false); + + Ok(KeyMap::new(false, key_map, None)) +} + +fn restore_kdf(secret: &String) -> anyhow::Result>{ + debug!("restoring kdf from secret"); + let prk = match hex::decode(secret){ + Ok(key) => key, + Err(e) => { + error!("Error while decoding master key: {}", e); + return Err(anyhow!("Error while encrypting key seed!")); + } + }; + + match Hkdf::::from_prk(prk.as_slice()){ + Ok(kdf) => Ok(kdf), + Err(e) => { + error!("Error while instantiating hkdf: {}", e); + Err(anyhow!("Error while encrypting key seed!")) + } + } +} + +pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Result>{ + // check key size + if key.len() != EXP_KEY_SIZE { + error!("Given key has size {} but expected {} bytes", key.len(), EXP_KEY_SIZE); + Err(anyhow!("Incorrect key size")) + } + // check nonce size + else if nonce.len() != EXP_NONCE_SIZE { + error!("Given nonce has size {} but expected {} bytes", nonce.len(), EXP_NONCE_SIZE); + Err(anyhow!("Incorrect nonce size")) + } + else{ + let key = GenericArray::from_slice(key); + let nonce = GenericArray::from_slice(nonce); + let cipher = Aes256GcmSiv::new(key); + + match cipher.encrypt(nonce, secret.as_bytes()){ + Ok(ct) => { + Ok(ct) + } + Err(e) => Err(anyhow!("Error while encrypting {}", e)) + } + } +} + +pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result{ + debug!("key len = {}", key.len()); + debug!("ct len = {}", ct.len()); + let key = GenericArray::from_slice(key); + let nonce = GenericArray::from_slice(nonce); + let cipher = Aes256GcmSiv::new(key); + + debug!("key: {}", hex::encode_upper(key)); + debug!("nonce: {}", hex::encode_upper(nonce)); + + debug!("ct len = {}", ct.len()); + debug!("nonce len = {}", nonce.len()); + match cipher.decrypt(nonce, ct){ + Ok(pt) => { + let pt = String::from_utf8(pt)?; + Ok(pt) + }, + Err(e) => { + Err(anyhow!("Error while decrypting: {}", e)) + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/docstore.rs b/clearing-house-app/logging-service/src/db/docstore.rs new file mode 100644 index 0000000..4a22827 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/docstore.rs @@ -0,0 +1,322 @@ +use mongodb::bson; +use mongodb::bson::doc; +use mongodb::options::{AggregateOptions, UpdateOptions}; +use rocket::futures::StreamExt; +use core_lib::constants::{ + MAX_NUM_RESPONSE_ENTRIES, + MONGO_COLL_DOCUMENT_BUCKET, + MONGO_ID, + MONGO_PID, + MONGO_COUNTER, + MONGO_DOC_ARRAY, + MONGO_DT_ID, + MONGO_FROM_TS, + MONGO_TO_TS, + MONGO_TC, + MONGO_TS, +}; +use core_lib::model::document::EncryptedDocument; +use core_lib::errors::*; +use core_lib::model::SortingOrder; +use crate::db::docstore::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; + +#[derive(Clone)] +pub struct DataStore { + client: mongodb::Client, + database: mongodb::Database, +} + +impl DataStore { + pub async fn add_document(&self, doc: EncryptedDocument) -> Result { + debug!("add_document to bucket"); + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + let bucket_update = DocumentBucketUpdate::from(&doc); + let mut update_options = UpdateOptions::default(); + update_options.upsert = Some(true); + let id = format!("^{}_", doc.pid.clone()); + let re = mongodb::bson::Regex { + pattern: id, + options: String::new(), + }; + + let query = doc! {"_id": re, MONGO_PID: doc.pid.clone(), MONGO_COUNTER: mongodb::bson::bson!({"$lt": MAX_NUM_RESPONSE_ENTRIES as i64})}; + + match coll.update_one(query, + doc! { + "$push": { + MONGO_DOC_ARRAY: mongodb::bson::to_bson(&bucket_update).unwrap(), + }, + "$inc": {"counter": 1}, + "$setOnInsert": { "_id": format!("{}_{}", doc.pid.clone(), doc.ts), MONGO_DT_ID: doc.dt_id.clone(), MONGO_FROM_TS: doc.ts}, + "$set": {MONGO_TO_TS: doc.ts}, + }, update_options).await { + Ok(_r) => { + debug!("added new document: {:#?}", &_r.upserted_id); + Ok(true) + } + Err(e) => { + error!("failed to store document: {:#?}", &e); + Err(Error::from(e)) + } + } + } + + /// checks if the document exists + /// document ids are globally unique + pub async fn exists_document(&self, id: &String) -> Result { + debug!("Check if document with id '{}' exists...", id); + let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone()}; + + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + match coll.count_documents(Some(query), None).await? { + 0 => { + debug!("Document with id '{}' does not exist!", &id); + Ok(false) + } + _ => { + debug!("... found."); + Ok(true) + } + } + } + + /// gets the model from the db + pub async fn get_document(&self, id: &String, pid: &String) -> Result> { + debug!("Trying to get doc with id {}...", id); + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + + let pipeline = vec![doc! {"$match":{ + MONGO_PID: pid.clone(), + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone() + }}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, + doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, + doc! {"$match":{ MONGO_ID: id.clone()}}]; + + let mut results = coll.aggregate(pipeline, None).await?; + + if let Some(result) = results.next().await { + let doc: EncryptedDocument = bson::from_document(result?)?; + return Ok(Some(doc)); + } + + return Ok(None); + } + + /// gets documents for a single process from the db + pub async fn get_document_with_previous_tc(&self, tc: i64) -> Result> { + let previous_tc = tc - 1; + debug!("Trying to get document for tc {} ...", previous_tc); + if previous_tc < 0 { + info!("... not entry exists."); + Ok(None) + } else { + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + + let pipeline = vec![doc! {"$match":{ + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TC): previous_tc + }}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, + doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, + doc! {"$match":{ MONGO_TC: previous_tc}}]; + + let mut results = coll.aggregate(pipeline, None).await?; + + return if let Some(result) = results.next().await { + debug!("Found {:#?}", &result); + let doc: EncryptedDocument = bson::from_document(result?)?; + Ok(Some(doc)) + } else { + warn!("Document with tc {} not found!", previous_tc); + Ok(None) + }; + } + } + + /// gets a page of documents of a specific document type for a single process from the db defined by parameters page, size and sort + pub async fn get_documents_for_pid(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> Result> { + debug!("...trying to get page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); + + match self.get_start_bucket_size(dt_id, pid, page, size, sort, date_from, date_to).await { + Ok(bucket_size) => { + let offset = DataStore::get_offset(&bucket_size); + let start_bucket = DataStore::get_start_bucket(page, size, &bucket_size, offset); + trace!("...working with start_bucket {} and offset {} ...", start_bucket, offset); + let start_entry = DataStore::get_start_entry(page, size, start_bucket, &bucket_size, offset); + + trace!("...working with start_entry {} in start_bucket {} and offset {} ...", start_entry, start_bucket, offset); + + let skip_buckets = (start_bucket - 1) as i32; + let sort_order = match sort { + SortingOrder::Ascending => 1, + SortingOrder::Descending => -1, + }; + + let pipeline = vec![doc! {"$match":{ + MONGO_PID: pid.clone(), + MONGO_DT_ID: dt_id.clone(), + MONGO_FROM_TS: {"$lte": date_to.timestamp()}, + MONGO_TO_TS: {"$gte": date_from.timestamp()} + }}, + doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, + doc! {"$skip" : skip_buckets}, + // worst case: overlap between two buckets. + doc! {"$limit" : 2}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$replaceRoot": { "newRoot": "$documents"}}, + doc! {"$match":{ + MONGO_TS: {"$gte": date_from.timestamp(), "$lte": date_to.timestamp()} + }}, + doc! {"$sort" : {MONGO_TS: sort_order}}, + doc! {"$skip" : start_entry as i32}, + doc! { "$limit": size as i32}]; + + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + + let mut options = AggregateOptions::default(); + options.allow_disk_use = Some(true); + let mut results = coll.aggregate(pipeline, options).await?; + + let mut docs = vec!(); + while let Some(result) = results.next().await { + let doc: DocumentBucketUpdate = bson::from_document(result?)?; + docs.push(restore_from_bucket(pid, dt_id, doc)); + } + + return Ok(docs); + } + Err(e) => { + error!("Error while getting bucket offset!"); + Err(Error::from(e)) + } + } + } + + /// offset is necessary for duration queries. There, start_entries of bucket depend on timestamps which usually creates an offset in the bucket + async fn get_start_bucket_size(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> Result { + debug!("...trying to get the offset for page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); + let sort_order = match sort { + SortingOrder::Ascending => { + 1 + } + SortingOrder::Descending => { + -1 + } + }; + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + + debug!("... match with pid: {}, dt_it: {}, to_ts <= {}, from_ts >= {} ...", pid, dt_id, date_from.timestamp(), date_to.timestamp()); + let pipeline = vec![doc! {"$match":{ + MONGO_PID: pid.clone(), + MONGO_DT_ID: dt_id.clone(), + MONGO_FROM_TS: {"$lte": date_to.timestamp()}, + MONGO_TO_TS: {"$gte": date_from.timestamp()} + }}, + // sorting according to sorting order, so we get either the start or end + doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, + doc! {"$limit" : 1}, + // count all relevant documents in the target bucket + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$match":{ + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TS): {"$lte": date_to.timestamp(), "$gte": date_from.timestamp()} + }}, + // modify result to return total number of docs in bucket and number of relevant docs in bucket + doc! { "$group": { "_id": {"total": "$counter"}, "size": { "$sum": 1 } } }, + doc! { "$project": {"_id":0, "capacity": "$_id.total", "size":true}}]; + + let mut options = AggregateOptions::default(); + options.allow_disk_use = Some(true); + let mut results = coll.aggregate(pipeline, options).await?; + let mut bucket_size = DocumentBucketSize { + capacity: MAX_NUM_RESPONSE_ENTRIES as i32, + size: 0, + }; + while let Some(result) = results.next().await { + debug!("... retrieved: {:#?}", &result); + let result_bucket: DocumentBucketSize = bson::from_document(result?)?; + bucket_size = result_bucket; + } + debug!("... sending offset: {:?}", bucket_size); + Ok(bucket_size) + } + + fn get_offset(bucket_size: &DocumentBucketSize) -> u64 { + return (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES; + } + + fn get_start_bucket(page: u64, size: u64, bucket_size: &DocumentBucketSize, offset: u64) -> u64 { + let docs_to_skip = (page - 1) * size + offset + MAX_NUM_RESPONSE_ENTRIES - bucket_size.capacity as u64; + return (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1; + } + + fn get_start_entry(page: u64, size: u64, start_bucket: u64, bucket_size: &DocumentBucketSize, offset: u64) -> u64 { + // docs to skip calculated by page * size + let docs_to_skip = (page - 1) * size + offset; + let mut start_entry = 0; + if start_bucket > 1 { + start_entry = docs_to_skip - bucket_size.capacity as u64; + if start_entry > 2 { + start_entry = start_entry - (start_bucket - 2) * MAX_NUM_RESPONSE_ENTRIES + } + } + return start_entry; + } +} + +mod bucket { + use core_lib::model::document::EncryptedDocument; + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct DocumentBucket { + pub counter: u64, + pub pid: String, + pub dt_id: String, + pub from_ts: i64, + pub to_ts: i64, + pub documents: Vec, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct DocumentBucketSize { + pub capacity: i32, + pub size: i32, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct DocumentBucketUpdate { + pub id: String, + pub ts: i64, + pub tc: i64, + pub hash: String, + pub keys_ct: String, + pub cts: Vec, + } + + impl From<&EncryptedDocument> for DocumentBucketUpdate { + fn from(doc: &EncryptedDocument) -> Self { + DocumentBucketUpdate { + id: doc.id.clone(), + ts: doc.ts, + tc: doc.tc, + hash: doc.hash.clone(), + keys_ct: doc.keys_ct.clone(), + cts: doc.cts.to_vec(), + } + } + } + + pub fn restore_from_bucket(pid: &String, dt_id: &String, bucket_update: DocumentBucketUpdate) -> EncryptedDocument { + EncryptedDocument { + id: bucket_update.id.clone(), + dt_id: dt_id.clone(), + pid: pid.clone(), + ts: bucket_update.ts, + tc: bucket_update.tc, + hash: bucket_update.hash.clone(), + keys_ct: bucket_update.keys_ct.clone(), + cts: bucket_update.cts.to_vec(), + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/keystore.rs b/clearing-house-app/logging-service/src/db/keystore.rs new file mode 100644 index 0000000..7705b01 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/keystore.rs @@ -0,0 +1,146 @@ +use std::process::exit; +use core_lib::errors::*; +use mongodb::bson::doc; +use rocket::futures::TryStreamExt; +use core_lib::constants::{MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; +use crate::model::crypto::MasterKey; +use crate::model::doc_type::DocumentType; + +#[derive(Clone, Debug)] +pub struct KeyStore { + client: mongodb::Client, + database: mongodb::Database +} + +impl KeyStore { + + /// Only one master key may exist in the database. + pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result{ + debug!("Storing new master key..."); + let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); + debug!("... but first check if there's already one."); + let result= coll.find(None, None).await + .expect("Error retrieving the master keys") + .try_collect().await.unwrap_or_else(|_| vec![]); + + if result.len() > 1{ + error!("Master Key table corrupted!"); + exit(1); + } + if result.len() == 1{ + error!("Master key already exists!"); + Ok(false) + } + else{ + //let db_key = bson::to_bson(&key) + // .expect("failed to serialize master key for database"); + match coll.insert_one(key, None).await{ + Ok(_r) => { + Ok(true) + }, + Err(e) => { + error!("master key could not be stored: {:?}", &e); + panic!("master key could not be stored") + } + } + } + } + + /// Only one master key may exist in the database. + pub async fn get_msk(&self) -> anyhow::Result { + let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); + let result= coll.find(None, None).await + .expect("Error retrieving the master keys") + .try_collect().await.unwrap_or_else(|_| vec![]); + + if result.len() > 1{ + error!("Master Key table corrupted!"); + exit(1); + } + if result.len() == 1{ + Ok(result[0].clone()) + } + else { + error!("Master Key missing!"); + exit(1); + } + } + + // DOCTYPE + pub async fn add_document_type(&self, doc_type: DocumentType) -> Result<()> { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + match coll.insert_one(doc_type.clone(), None).await { + Ok(_r) => { + debug!("added new document type: {}", &_r.inserted_id); + Ok(()) + }, + Err(e) => { + error!("failed to log document type {}", &doc_type.id); + Err(Error::from(e)) + } + } + } + + //TODO: Do we need to check that no documents of this type exist before we remove it from the db? + pub async fn delete_document_type(&self, id: &String, pid: &String) -> Result { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + let result = coll.delete_many(doc! { MONGO_ID: id, MONGO_PID: pid }, None).await?; + if result.deleted_count >= 1 { + Ok(true) + } else { + Ok(false) + } + } + + + /// checks if the model exits + pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> Result { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + let result = coll.find_one(Some(doc! { MONGO_ID: dt_id, MONGO_PID: pid }), None).await?; + match result { + Some(_r) => Ok(true), + None => { + debug!("document type with id {} and pid {:?} does not exist!", &dt_id, &pid); + Ok(false) + } + } + } + + pub async fn get_all_document_types(&self) -> Result> { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + let result = coll.find(None, None).await? + .try_collect().await.unwrap_or_else(|_| vec![]); + Ok(result) + } + + pub async fn get_document_type(&self, dt_id: &String) -> Result> { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + debug!("get_document_type for dt_id: '{}'", dt_id); + match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await{ + Ok(result) => Ok(result), + Err(e) => { + error!("error while getting document type with id {}!", dt_id); + Err(Error::from(e)) + } + } + } + + pub async fn update_document_type(&self, doc_type: DocumentType, id: &String) -> Result { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await{ + Ok(r) => { + if r.matched_count != 1 || r.modified_count != 1{ + warn!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + } + else{ + debug!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + } + Ok(true) + }, + Err(e) => { + error!("error while updating document type with id {}: {:#?}", id, e); + Ok(false) + } + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/logging-service/src/db/mod.rs index 468fa5b..3451022 100644 --- a/clearing-house-app/logging-service/src/db/mod.rs +++ b/clearing-house-app/logging-service/src/db/mod.rs @@ -1,3 +1,6 @@ +pub(crate) mod keystore; +pub(crate) mod docstore; + use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, DATABASE_URL, CLEAR_DB, PROCESS_DB, PROCESS_DB_CLIENT, MONGO_COLL_TRANSACTIONS, MONGO_TC}; use core_lib::db::{DataStoreApi, init_database_client}; use core_lib::errors::*; diff --git a/clearing-house-app/logging-service/src/logging_api.rs b/clearing-house-app/logging-service/src/logging_api.rs index c5e5de5..254b1f0 100644 --- a/clearing-house-app/logging-service/src/logging_api.rs +++ b/clearing-house-app/logging-service/src/logging_api.rs @@ -317,10 +317,7 @@ async fn query_pid( None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() }; - let sanitized_sort = match sort { - Some(s) => s, - None => Descending - }; + let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); match doc_api.get_documents(&user, &pid, sanitized_page, sanitized_size, sanitized_sort, date_from, date_to).await { Ok(r) => { diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 7c5c713..fdbbbcb 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -14,6 +14,8 @@ use model::constants::SIGNING_KEY; pub mod logging_api; pub mod db; pub mod model; +mod services; +mod crypto; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { diff --git a/clearing-house-app/logging-service/src/model/crypto.rs b/clearing-house-app/logging-service/src/model/crypto.rs new file mode 100644 index 0000000..a700c76 --- /dev/null +++ b/clearing-house-app/logging-service/src/model/crypto.rs @@ -0,0 +1,29 @@ +use crate::crypto::generate_random_seed; +use hkdf::Hkdf; +use sha2::Sha256; +use core_lib::model::new_uuid; + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct MasterKey { + pub id: String, + pub key: String, + pub salt: String +} + +impl MasterKey{ + pub fn new(id: String, key: String, salt: String)-> MasterKey{ + MasterKey{ + id, + key, + salt + } + } + + pub fn new_random() -> MasterKey{ + let key_salt = generate_random_seed(); + let ikm = generate_random_seed(); + let (master_key, _) = Hkdf::::extract(Some(&key_salt), &ikm); + + MasterKey::new(new_uuid(), hex::encode_upper(master_key), hex::encode_upper(generate_random_seed())) + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/model/doc_type.rs b/clearing-house-app/logging-service/src/model/doc_type.rs new file mode 100644 index 0000000..fe53c2a --- /dev/null +++ b/clearing-house-app/logging-service/src/model/doc_type.rs @@ -0,0 +1,29 @@ +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct DocumentType { + pub id: String, + pub pid: String, + pub parts: Vec, +} + +impl DocumentType { + pub fn new(id: String, pid: String, parts: Vec) -> DocumentType { + DocumentType{ + id, + pid, + parts, + } + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct DocumentTypePart { + pub name: String, +} + +impl DocumentTypePart { + pub fn new(name: String) -> DocumentTypePart{ + DocumentTypePart{ + name + } + } +} diff --git a/clearing-house-app/logging-service/src/model/mod.rs b/clearing-house-app/logging-service/src/model/mod.rs index f7a208b..53a8ef5 100644 --- a/clearing-house-app/logging-service/src/model/mod.rs +++ b/clearing-house-app/logging-service/src/model/mod.rs @@ -5,6 +5,8 @@ use core_lib::api::crypto::get_fingerprint; pub mod constants; pub mod ids; +pub(crate) mod crypto; +pub(crate) mod doc_type; #[derive(Serialize, Deserialize)] pub struct TransactionCounter{ diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs new file mode 100644 index 0000000..47acc93 --- /dev/null +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -0,0 +1,281 @@ +use std::convert::TryFrom; +use anyhow::anyhow; +use core_lib::api::crypto::{ChClaims, create_service_token}; +use core_lib::api::{DocumentReceipt, QueryResult}; +use core_lib::constants::{DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART}; +use core_lib::model::document::Document; +use core_lib::model::{parse_date, sanitize_dates, SortingOrder, validate_dates}; +use core_lib::model::crypto::{KeyCt, KeyCtList}; +use crate::services::keyring_service::KeyringService; +use crate::db::docstore::DataStore; + +pub struct DocumentService { + db: DataStore, + key_api: KeyringService, +} + +impl DocumentService { + async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { + trace!("...user '{:?}'", &ch_claims.client_id); + // data validation + let payload: Vec = doc.parts.iter() + .filter(|p| String::from(PAYLOAD_PART) == p.name) + .map(|p| p.content.as_ref().unwrap().clone()).collect(); + if payload.len() > 1 { + return Err(anyhow!("Document contains two payloads!")); // BadRequest + } else if payload.len() == 0 { + return Err(anyhow!("Document contains no payload!")); // BadRequest + } + + // check if doc id already exists + match self.db.exists_document(&doc.id).await { + Ok(true) => { + warn!("Document exists already!"); + Err(anyhow!("Document exists already!")) // BadRequest + } + _ => { + debug!("Document does not exists!"); + debug!("getting keys"); + let keys; + + // TODO: This needs some attention, because keyring api called `create_service_token` on `ch_claims` + match self.key_api.generate_keys(ch_claims, doc.pid.clone(), doc.dt_id.clone()).await { + Ok(key_map) => { + keys = key_map; + debug!("got keys"); + } + Err(e) => { + error!("Error while retrieving keys: {:?}", e); + return Err(anyhow!("Error while retrieving keys!")); // InternalError + } + }; + + debug!("start encryption"); + let mut enc_doc; + match doc.encrypt(keys) { + Ok(ct) => { + debug!("got ct"); + enc_doc = ct + } + Err(e) => { + error!("Error while encrypting: {:?}", e); + return Err(anyhow!("Error while encrypting!")); // InternalError + } + }; + + // chain the document to previous documents + debug!("add the chain hash..."); + // get the document with the previous tc + match self.db.get_document_with_previous_tc(doc.tc).await { + Ok(Some(previous_doc)) => { + enc_doc.hash = previous_doc.hash(); + } + Ok(None) => { + if doc.tc == 0 { + info!("No entries found for pid {}. Beginning new chain!", doc.pid); + } else { + // If this happens, db didn't find a tc entry that should exist. + return Err(anyhow!("Error while creating the chain hash!")); // InternalError + } + } + Err(e) => { + error!("Error while creating the chain hash: {:?}", e); + return Err(anyhow!("Error while creating the chain hash!")); + } + } + + // prepare the success result message + + + let receipt = DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id, &enc_doc.hash); + + debug!("storing document ...."); + // store document + match self.db.add_document(enc_doc).await { + Ok(_b) => Ok(receipt), + Err(e) => { + error!("Error while adding: {:?}", e); + Err(anyhow!("Error while storing document!")) + } + } + } + } + } + + async fn get_enc_documents_for_pid(&self, + ch_claims: ChClaims, + doc_type: Option, + page: Option, // TODO: Why i32? This should be and unsinged int + size: Option, // TODO: Why i32? This should be and unsinged int + sort: Option, + date_from: Option, + date_to: Option, + pid: String) -> anyhow::Result { + debug!("Trying to retrieve documents for pid '{}'...", &pid); + trace!("...user '{:?}'", &ch_claims.client_id); + debug!("...page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); + + // Parameter validation for pagination: + // Valid pages start from 1 + // Max page number as of yet unknown + let sanitized_page = match page{ + Some(p) => { + if p > 0{ + u64::try_from(p).unwrap() + } + else{ + warn!("...invalid page requested. Falling back to 1."); + 1 + } + }, + None => 1 + }; + + // Valid sizes are between 1 and MAX_NUM_RESPONSE_ENTRIES (1000) + let sanitized_size = match size{ + Some(s) => { + if s > 0 && s <= i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap() { + u64::try_from(s).unwrap() + } + else{ + warn!("...invalid size requested. Falling back to default."); + DEFAULT_NUM_RESPONSE_ENTRIES + } + }, + None => DEFAULT_NUM_RESPONSE_ENTRIES + }; + + // Sorting order is already validated and defaults to descending + let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); + + // Parsing the dates for duration queries + let parsed_date_from = parse_date(date_from, false); + let parsed_date_to = parse_date(date_to, true); + + // Validation of dates with various checks. If none given dates default to date_now (date_to) and (date_now - 2 weeks) (date_from) + if !validate_dates(parsed_date_from, parsed_date_to){ + debug!("date validation failed!"); + return Err(anyhow!("Invalid date parameter!")); // BadRequest + } + let (sanitized_date_from, sanitized_date_to) = sanitize_dates(parsed_date_from, parsed_date_to); + + //new behavior: if pages are "invalid" return {}. Do not adjust page + //either call db with type filter or without to get cts + let start = chrono::Local::now(); + debug!("... using pagination with page: {}, size:{} and sort:{:#?}", sanitized_page, sanitized_size, &sanitized_sort); + + let dt_id = match doc_type{ + Some(dt) => dt, + None => String::from(DEFAULT_DOC_TYPE), + }; + let cts = match self.db.get_documents_for_pid(&dt_id, &pid, sanitized_page, sanitized_size, &sanitized_sort, &sanitized_date_from, &sanitized_date_to).await{ + Ok(cts) => cts, + Err(e) => { + error!("Error while retrieving document: {:?}", e); + return Err(anyhow!("Error while retrieving document for {}", &pid)) + }, + }; + + let result_size = i32::try_from(sanitized_size).ok(); + let result_page = i32::try_from(sanitized_page).ok(); + let result_sort = match sanitized_sort{ + SortingOrder::Ascending => String::from("asc"), + SortingOrder::Descending => String::from("desc"), + }; + + let mut result = QueryResult::new(sanitized_date_from.timestamp(), sanitized_date_to.timestamp(), result_page, result_size, result_sort, vec!()); + + // The db might contain no documents in which case we get an empty vector + if cts.is_empty(){ + debug!("Queried empty pid: {}", &pid); + Ok(result) + } + else{ + // Documents found for pid, now decrypting them + debug!("Found {} documents. Getting keys from keyring...", cts.len()); + let key_cts: Vec = cts.iter() + .map(|e| KeyCt::new(e.id.clone(), e.keys_ct.clone())).collect(); + // caution! we currently only support a single dt per call, so we use the first dt we found + let key_cts_list = KeyCtList::new(cts[0].dt_id.clone(), key_cts); + // decrypt cts + // TODO: This method needs some attention, because keyring api called `create_service_token` on `ch_claims` + let key_maps = match self.key_api.decrypt_multiple_keys(ch_claims, Some(pid),&key_cts_list).await{ + Ok(key_map) => { + key_map + } + Err(e) => { + error!("Error while retrieving keys from keyring: {:?}", e); + return Err(anyhow!("Error while retrieving keys from keyring")); // InternalError + } + }; + debug!("... keys received. Starting decryption..."); + let pts_bulk : Vec = cts.iter().zip(key_maps.iter()) + .filter_map(|(ct,key_map)|{ + if ct.id != key_map.id{ + error!("Document and map don't match"); + }; + match ct.decrypt(key_map.map.keys.clone()){ + Ok(d) => Some(d), + Err(e) => { + warn!("Got empty document from decryption! {:?}", e); + None + } + } + }).collect(); + debug!("...done."); + let end = chrono::Local::now(); + let diff = end - start; + info!("Total time taken to run in ms: {}", diff.num_milliseconds()); + result.documents = pts_bulk; + Ok(result) + } + } + + async fn get_enc_document(&self, ch_claims: ChClaims, pid: String, id: String, hash: Option) -> anyhow::Result { + trace!("...user '{:?}'", &ch_claims.client_id); + trace!("trying to retrieve document with id '{}' for pid '{}'", &id, &pid); + if hash.is_some(){ + debug!("integrity check with hash: {}", hash.as_ref().unwrap()); + } + + match self.db.get_document(&id, &pid).await{ + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(Some(ct)) => { + match hex::decode(&ct.keys_ct){ + Ok(key_ct) => { + // TODO: This method needs some attention, because keyring api called `create_service_token` on `ch_claims` + match self.key_api.decrypt_key_map(ch_claims, hex::encode_upper(key_ct), Some(pid), ct.dt_id.clone()).await{ + Ok(key_map) => { + //TODO check the hash + match ct.decrypt(key_map.keys){ + Ok(d) => Ok(d), + Err(e) => { + warn!("Got empty document from decryption! {:?}", e); + return Err(anyhow!("Document {} not found!", &id)); // NotFound + } + } + } + Err(e) => { + error!("Error while retrieving keys from keyring: {:?}", e); + return Err(anyhow!("Error while retrieving keys")) // InternalError + } + } + + }, + Err(e) => { + error!("Error while decoding ciphertext: {:?}", e); + return Err(anyhow!("Key Ciphertext corrupted")) // InternalError + } + } + }, + Ok(None) => { + debug!("Nothing found in db!"); + return Err(anyhow!("Document {} not found!", &id)) // NotFound + } + Err(e) => { + error!("Error while retrieving document: {:?}", e); + return Err(anyhow!("Error while retrieving document {}", &id)) // InternalError + } + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs new file mode 100644 index 0000000..f15e973 --- /dev/null +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -0,0 +1,169 @@ +use std::process::exit; +use anyhow::anyhow; +use rocket::futures::TryStreamExt; +use core_lib::api::crypto::ChClaims; +use core_lib::constants::MONGO_COLL_MASTER_KEY; +use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; +use crate::crypto; +use crate::crypto::restore_key_map; +use crate::db::keystore::KeyStore; + +pub struct KeyringService { + db: KeyStore, +} + +impl KeyringService { + pub async fn generate_keys(&self, ch_claims: ChClaims, _pid: String, dt_id: String) -> anyhow::Result { + trace!("generate_keys"); + trace!("...user '{:?}'", &ch_claims.client_id); + match self.db.get_msk().await { + Ok(key) => { + // check that doc type exists for pid + match self.db.get_document_type(&dt_id).await { + Ok(Some(dt)) => { + // generate new random key map + match crypto::generate_key_map(key, dt) { + Ok(key_map) => { + trace!("response: {:?}", &key_map); + return Ok(key_map); + } + Err(e) => { + error!("Error while generating key map: {}", e); + return Err(anyhow!("Error while generating keys")); // InternalError + } + } + } + Ok(None) => { + warn!("document type {} not found", &dt_id); + return Err(anyhow!("Document type not found!")); // BadRequest + } + Err(e) => { + warn!("Error while retrieving document type: {}", e); + return Err(anyhow!("Error while retrieving document type")); // InternalError + } + } + } + Err(e) => { + error!("Error while retrieving master key: {}", e); + return Err(anyhow!("Error while generating keys")); // InternalError + } + } + } + + pub(crate) async fn decrypt_keys(&self, ch_claims: ChClaims, _pid: Option, key_cts: &KeyCtList) -> anyhow::Result> { + trace!("decrypt_keys"); + trace!("...user '{:?}'", &ch_claims.client_id); + debug!("number of cts to decrypt: {}", &key_cts.cts.len()); + + // get master key + match self.db.get_msk().await { + Ok(m_key) => { + // check that doc type exists for pid + match self.db.get_document_type(&key_cts.dt).await { + Ok(Some(dt)) => { + let mut dec_error_count = 0; + let mut map_error_count = 0; + // validate keys_ct input + let key_maps: Vec = key_cts.cts.iter().filter_map( + |key_ct| { + match hex::decode(key_ct.ct.clone()) { + Ok(key) => Some((key_ct.id.clone(), key)), + Err(e) => { + error!("Error while decoding key ciphertext: {}", e); + dec_error_count = dec_error_count + 1; + None + } + } + } + ).filter_map( + |(id, key)| { + match restore_key_map(m_key.clone(), dt.clone(), key) { + Ok(key_map) => { + Some(KeyMapListItem::new(id, key_map)) + } + Err(e) => { + error!("Error while generating key map: {}", e); + map_error_count = map_error_count + 1; + None + } + } + } + ) + .collect(); + + let error_count = map_error_count + dec_error_count; + + // Currently, we don't tolerate errors while decrypting keys + if error_count > 0 { + return Err(anyhow!("Error while decrypting keys")); // InternalError + } else { + return Ok(key_maps); + } + } + Ok(None) => { + warn!("document type {} not found", &key_cts.dt); + return Err(anyhow!("Document type not found!")); // BadRequest + } + Err(e) => { + warn!("Error while retrieving document type: {}", e); + return Err(anyhow!("Document type not found!")); // NotFound + } + } + } + Err(e) => { + error!("Error while retrieving master key: {}", e); + return Err(anyhow!("Error while decrypting keys")); // InternalError + } + } + } + + pub async fn decrypt_key_map(&self, ch_claims: ChClaims, keys_ct: String, _pid: Option, dt_id: String) -> anyhow::Result { + trace!("decrypt_key_map"); + trace!("...user '{:?}'", &ch_claims.client_id); + trace!("ct: {}", &keys_ct); + // get master key + match self.db.get_msk().await { + Ok(key) => { + // check that doc type exists for pid + match self.db.get_document_type(&dt_id).await { + Ok(Some(dt)) => { + // validate keys_ct input + let keys_ct = match hex::decode(keys_ct) { + Ok(key) => key, + Err(e) => { + error!("Error while decoding key ciphertext: {}", e); + return Err(anyhow!("Error while decrypting keys")); // InternalError + } + }; + + match restore_key_map(key, dt, keys_ct) { + Ok(key_map) => { + return Ok(key_map); + }, + Err(e) => { + error!("Error while generating key map: {}", e); + return Err(anyhow!("Error while restoring keys")); // InternalError + } + } + } + Ok(None) => { + warn!("document type {} not found", &dt_id); + return Err(anyhow!("Document type not found!")); // BadRequest + } + Err(e) => { + warn!("Error while retrieving document type: {}", e); + return Err(anyhow!("Document type not found!")); // NotFound + } + } + } + Err(e) => { + error!("Error while retrieving master key: {}", e); + return Err(anyhow!("Error while decrypting keys")); // InternalError + } + } + } + + pub(crate) async fn decrypt_multiple_keys(&self, ch_claims: ChClaims, pid: Option, cts: &KeyCtList) -> anyhow::Result> { + self.decrypt_keys(ch_claims, pid, cts).await + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs new file mode 100644 index 0000000..01df54e --- /dev/null +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -0,0 +1,2 @@ +mod keyring_service; +mod document_service; \ No newline at end of file From f1beee0bd6ed48277d02a385b25d232f7ee5740a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 10:05:09 +0200 Subject: [PATCH 011/183] feat(ch-app): Refactor logging-api to use a service as well - Separated rocket-handler and LoggingService method, so that a http server replacement becomes easier and the architecture becomes more modular. - Added ports module, where the rocket-handler code will be moved in the next commits - Added forbid(unsafe_code) in every crate --- clearing-house-app/core-lib/src/lib.rs | 2 + clearing-house-app/document-api/src/main.rs | 2 + clearing-house-app/keyring-api/src/main.rs | 2 + .../logging-service/src/logging_api.rs | 366 ++---------------- .../logging-service/src/main.rs | 3 + .../logging-service/src/ports/mod.rs | 5 + .../src/services/document_service.rs | 18 +- .../src/services/logging_service.rs | 365 +++++++++++++++++ .../logging-service/src/services/mod.rs | 9 +- 9 files changed, 436 insertions(+), 336 deletions(-) create mode 100644 clearing-house-app/logging-service/src/ports/mod.rs create mode 100644 clearing-house-app/logging-service/src/services/logging_service.rs diff --git a/clearing-house-app/core-lib/src/lib.rs b/clearing-house-app/core-lib/src/lib.rs index e14ed64..815fed7 100644 --- a/clearing-house-app/core-lib/src/lib.rs +++ b/clearing-house-app/core-lib/src/lib.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + extern crate biscuit; extern crate chrono; extern crate fern; diff --git a/clearing-house-app/document-api/src/main.rs b/clearing-house-app/document-api/src/main.rs index 8da3956..59e38d4 100644 --- a/clearing-house-app/document-api/src/main.rs +++ b/clearing-house-app/document-api/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; diff --git a/clearing-house-app/keyring-api/src/main.rs b/clearing-house-app/keyring-api/src/main.rs index 5074728..ac4bf78 100644 --- a/clearing-house-app/keyring-api/src/main.rs +++ b/clearing-house-app/keyring-api/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + #[macro_use] extern crate error_chain; #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; diff --git a/clearing-house-app/logging-service/src/logging_api.rs b/clearing-house-app/logging-service/src/logging_api.rs index 254b1f0..f38b316 100644 --- a/clearing-house-app/logging-service/src/logging_api.rs +++ b/clearing-house-app/logging-service/src/logging_api.rs @@ -1,389 +1,103 @@ use core_lib::{ api::{ ApiResponse, - client::document_api::DocumentApiClient, crypto::{ChClaims, get_jwks}, }, - constants::{DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, DEFAULT_PROCESS_ID}, - model::{ - document::Document, - process::Process, - SortingOrder, - SortingOrder::Descending - } + model::SortingOrder, }; use rocket::serde::json::{json, Json}; use rocket::fairing::AdHoc; -use rocket::form::validate::Contains; use rocket::State; use std::convert::TryFrom; -use crate::model::{ids::{ - message::IdsMessage, - IdsQueryResult, - request::ClearingHouseMessage, -}, OwnerList, DataTransaction}; -use crate::db::ProcessStore; +use crate::model::ids::request::ClearingHouseMessage; use crate::model::constants::{ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_QUERY_API, ROCKET_PROCESS_API, ROCKET_PK_API}; +use crate::services::logging_service::LoggingService; -#[post( "/", format = "json", data = "")] +#[post("/", format = "json", data = "")] async fn log( ch_claims: ChClaims, - db: &State, - doc_api: &State, + logging_api: &State, key_path: &State, message: Json, - pid: String + pid: String, ) -> ApiResponse { - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - // Add non-InfoModel information to IdsMessage - let msg = message.into_inner(); - let mut m = msg.header; - m.payload = msg.payload; - m.payload_type = msg.payload_type; - m.pid = Some(pid.clone()); - - // validate that there is a payload - if m.payload.is_none() || (m.payload.is_some() && m.payload.as_ref().unwrap().trim().is_empty()){ - error!("Trying to log an empty payload!"); - return ApiResponse::BadRequest(String::from("No payload received for logging!")) - } - - // filter out calls for default process id and call application logic - match DEFAULT_PROCESS_ID.eq(pid.as_str()){ - true => { - warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); - ApiResponse::BadRequest(String::from("Document already exists")) - }, - false => { - // convenience: if process does not exist, we create it but only if no error occurred before - match db.get_process(&pid).await { - Ok(Some(_p)) => { - debug!("Requested pid '{}' exists. Nothing to create.", &pid); - } - Ok(None) => { - info!("Requested pid '{}' does not exist. Creating...", &pid); - // create a new process - let new_process = Process::new(pid.clone(), vec!(user.clone())); - - if db.store_process(new_process).await.is_err() { - error!("Error while creating process '{}'", &pid); - return ApiResponse::InternalError(String::from("Error while creating process")) - } - } - Err(_) => { - error!("Error while getting process '{}'", &pid); - return ApiResponse::InternalError(String::from("Error while getting process")) - } - } - - // now check if user is authorized to write to pid - match db.is_authorized(&user, &pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - warn!("This is the forbidden branch"); - return ApiResponse::Forbidden(String::from("User not authorized!")) - } - Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); - return ApiResponse::InternalError(String::from("Error during authorization")) - } - } - - debug!("logging message for pid {}", &pid); - log_message(db, user, doc_api, key_path.inner().as_str(), m.clone()).await + match logging_api.inner().log(ch_claims, key_path, message.into_inner(), pid).await { + Ok(id) => ApiResponse::SuccessCreate(json!(id)), + Err(e) => { + error!("Error while logging: {:?}", e); + ApiResponse::InternalError(String::from("Error while logging!")) } } } -#[post( "/", format = "json", data = "")] +#[post("/", format = "json", data = "")] async fn create_process( ch_claims: ChClaims, - db: &State, + logging_api: &State, message: Json, - pid: String -) -> ApiResponse { - let msg = message.into_inner(); - let mut m = msg.header; - m.payload = msg.payload; - m.payload_type = msg.payload_type; - - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - - // validate payload - let mut owners = vec!(user.clone()); - let payload = m.payload.clone().unwrap_or(String::new()); - if !payload.is_empty() { - trace!("OwnerList: '{:#?}'", &payload); - match serde_json::from_str::(&payload){ - Ok(owner_list) => { - for o in owner_list.owners{ - if !owners.contains(&o){ - owners.push(o); - } - } - }, - Err(e) => { - error!("Error while creating process '{}': {}", &pid, e); - return ApiResponse::BadRequest(String::from("Invalid owner list!")) - } - }; - }; - - // check if the pid already exists - match db.get_process(&pid).await{ - Ok(Some(p)) => { - warn!("Requested pid '{}' already exists.", &p.id); - if !p.owners.contains(user) { - ApiResponse::Forbidden(String::from("User not authorized!")) - } - else { - ApiResponse::BadRequest(String::from("Process already exists!")) - } - } - _ => { - // filter out calls for default process id - match DEFAULT_PROCESS_ID.eq(pid.as_str()) { - true => { - warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); - ApiResponse::BadRequest(String::from("Document already exists")) - }, - false => { - info!("Requested pid '{}' will have {} owners", &pid, owners.len()); - - // create process - info!("Requested pid '{}' does not exist. Creating...", &pid); - let new_process = Process::new(pid.clone(), owners); - - match db.store_process(new_process).await{ - Ok(_) => { - ApiResponse::SuccessCreate(json!(pid.clone())) - } - Err(e) => { - error!("Error while creating process '{}': {}", &pid, e); - ApiResponse::InternalError(String::from("Error while creating process")) - } - } - } - } - } - } -} - -async fn log_message( - db: &State, - user: &String, - doc_api: &State, - key_path: &str, - message: IdsMessage + pid: String, ) -> ApiResponse { - - debug!("transforming message to document..."); - let payload = message.payload.as_ref().unwrap().clone(); - // transform message to document - let mut doc = Document::from(message); - match db.get_transaction_counter().await{ - Ok(Some(tid)) => { - debug!("Storing document..."); - doc.tc = tid; - return match doc_api.create_document(&user, &doc).await{ - Ok(doc_receipt) => { - debug!("Increase transaction counter"); - match db.increment_transaction_counter().await{ - Ok(Some(_tid)) => { - debug!("Creating receipt..."); - let transaction = DataTransaction{ - transaction_id: doc.get_formatted_tc(), - timestamp: doc_receipt.timestamp, - process_id: doc_receipt.pid, - document_id: doc_receipt.doc_id, - payload, - chain_hash: doc_receipt.chain_hash, - client_id: user.clone(), - clearing_house_version: env!("CARGO_PKG_VERSION").to_string(), - }; - debug!("...done. Signing receipt..."); - ApiResponse::SuccessCreate(json!(transaction.sign(key_path))) - } - _ => { - error!("Error while incrementing transaction id!"); - ApiResponse::InternalError(String::from("Internal error while preparing transaction data")) - } - } - - }, - Err(e) => { - error!("Error while creating document: {:?}", e); - ApiResponse::BadRequest(String::from("Document already exists")) - } - } - }, - Ok(None) => { - println!("None!"); - ApiResponse::InternalError(String::from("Internal error while preparing transaction data")) - } + match logging_api.inner().create_process(ch_claims, message.into_inner(), pid).await { + Ok(id) => ApiResponse::SuccessCreate(json!(id)), Err(e) => { - error!("Error while getting transaction id!"); - println!("{}", e); - ApiResponse::InternalError(String::from("Internal error while preparing transaction data")) + error!("Error while creating process: {:?}", e); + ApiResponse::InternalError(String::from("Error while creating process!")) } } } -#[post("/<_pid>", format = "json", rank=50)] +#[post("/<_pid>", format = "json", rank = 50)] async fn unauth(_pid: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/<_pid>/<_id>", format = "json", rank=50)] +#[post("/<_pid>/<_id>", format = "json", rank = 50)] async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/?&&&&", format = "json", data = "<_message>")] +#[post("/?&&&&", format = "json", data = "")] async fn query_pid( ch_claims: ChClaims, - db: &State, + logging_api: &State, page: Option, size: Option, sort: Option, date_to: Option, date_from: Option, - doc_api: &State, pid: String, - _message: Json + message: Json, ) -> ApiResponse { - debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); - - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - - // check if process exists - match db.exists_process(&pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => return ApiResponse::NotFound(String::from("Process does not exist!")), - Err(_e) => { - error!("Error while checking process '{}' for user '{}'", &pid, &user); - return ApiResponse::InternalError(String::from("Cannot authorize user!")) - } - }; - - // now check if user is authorized to read infos in pid - match db.is_authorized(&user, &pid).await { - Ok(true) => { - info!("User authorized."); - }, - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - return ApiResponse::Forbidden(String::from("User not authorized!")) - } - Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); - return ApiResponse::InternalError(String::from("Cannot authorize user!")) - } - } - - // sanity check for pagination - let sanitized_page = match page { - Some(p) => { - if p >= 0 { - p - } else { - warn!("...invalid page requested. Falling back to 0."); - 1 - } - }, - None => 1 - }; - - let sanitized_size = match size { - Some(s) => { - let converted_max = i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap(); - if s > converted_max { - warn!("...invalid size requested. Falling back to default."); - converted_max - } else { - if s > 0 { - s - } else { - warn!("...invalid size requested. Falling back to default."); - i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() - } - } - }, - None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() - }; - - let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); - - match doc_api.get_documents(&user, &pid, sanitized_page, sanitized_size, sanitized_sort, date_from, date_to).await { - Ok(r) => { - let messages: Vec = r.documents.iter().map(|d| IdsMessage::from(d.clone())).collect(); - let result = IdsQueryResult::new(r.date_from, r.date_to, r.page, r.size, r.order, messages); - ApiResponse::SuccessOk(json!(result)) - - }, + match logging_api.inner().query_pid(ch_claims, page, size, sort, date_to, date_from, pid, message.into_inner()).await { + Ok(result) => ApiResponse::SuccessOk(json!(result)), Err(e) => { - error!("Error while retrieving message: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving messages for pid {}!", &pid)) + error!("Error while querying: {:?}", e); + ApiResponse::InternalError(String::from("Error while querying!")) } } } -#[post("//", format = "json", data = "<_message>")] -async fn query_id(ch_claims: ChClaims, db: &State, doc_api: &State, pid: String, id: String, _message: Json) -> ApiResponse { - - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - - // check if process exists - match db.exists_process(&pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => return ApiResponse::NotFound(String::from("Process does not exist!")), - Err(_e) => { - error!("Error while checking process '{}' for user '{}'", &pid, &user); - return ApiResponse::InternalError(String::from("Cannot authorize user!")) - } - }; - - // now check if user is authorized to read infos in pid - match db.is_authorized(&user, &pid).await { - Ok(true) => { - info!("User authorized."); - }, - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - return ApiResponse::Forbidden(String::from("User not authorized!")) - } - Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); - return ApiResponse::InternalError(String::from("Cannot authorize user!")) - } - } - - match doc_api.get_document(&user, &pid, &id).await { - Ok(Some(doc)) => { - // transform document to IDS message - let queried_message = IdsMessage::from(doc); - ApiResponse::SuccessOk(json!(queried_message)) - }, - Ok(None) => { - debug!("Queried a non-existing document: {}", &id); - ApiResponse::NotFound(format!("No message found with id {}!", &id)) - }, +#[post("//", format = "json", data = "")] +async fn query_id( + ch_claims: ChClaims, + logging_api: &State, + pid: String, + id: String, + message: Json, +) -> ApiResponse { + match logging_api.inner().query_id(ch_claims, pid, id, message.into_inner()).await { + Ok(result) => ApiResponse::SuccessOk(json!(result)), Err(e) => { - error!("Error while retrieving message: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving message with id {}!", &id)) + error!("Error while querying: {:?}", e); + ApiResponse::InternalError(String::from("Error while querying!")) } } } #[get("/.well-known/jwks.json", format = "json")] async fn get_public_sign_key(key_path: &State) -> ApiResponse { - match get_jwks(key_path.as_str()){ + match get_jwks(key_path.as_str()) { Some(jwks) => ApiResponse::SuccessOk(json!(jwks)), None => ApiResponse::InternalError(String::from("Error reading signing key")) } diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index fdbbbcb..6dfd988 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; @@ -16,6 +18,7 @@ pub mod db; pub mod model; mod services; mod crypto; +mod ports; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/logging-service/src/ports/mod.rs new file mode 100644 index 0000000..7c2432b --- /dev/null +++ b/clearing-house-app/logging-service/src/ports/mod.rs @@ -0,0 +1,5 @@ +//! # Ports +//! +//! This module contains the ports of the logging service. Ports are used to communicate with other +//! services. In this case, the logging service implements REST-API endpoints to provide access to +//! the logging service. diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 47acc93..4cacc31 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -15,7 +15,7 @@ pub struct DocumentService { } impl DocumentService { - async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { + pub(crate) async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); // data validation let payload: Vec = doc.parts.iter() @@ -102,15 +102,15 @@ impl DocumentService { } } - async fn get_enc_documents_for_pid(&self, - ch_claims: ChClaims, - doc_type: Option, - page: Option, // TODO: Why i32? This should be and unsinged int + pub(crate) async fn get_enc_documents_for_pid(&self, + ch_claims: ChClaims, + doc_type: Option, + page: Option, // TODO: Why i32? This should be and unsinged int size: Option, // TODO: Why i32? This should be and unsinged int sort: Option, - date_from: Option, - date_to: Option, - pid: String) -> anyhow::Result { + date_from: Option, + date_to: Option, + pid: String) -> anyhow::Result { debug!("Trying to retrieve documents for pid '{}'...", &pid); trace!("...user '{:?}'", &ch_claims.client_id); debug!("...page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); @@ -231,7 +231,7 @@ impl DocumentService { } } - async fn get_enc_document(&self, ch_claims: ChClaims, pid: String, id: String, hash: Option) -> anyhow::Result { + pub(crate) async fn get_enc_document(&self, ch_claims: ChClaims, pid: String, id: String, hash: Option) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); trace!("trying to retrieve document with id '{}' for pid '{}'", &id, &pid); if hash.is_some(){ diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/logging-service/src/services/logging_service.rs new file mode 100644 index 0000000..1a75f0e --- /dev/null +++ b/clearing-house-app/logging-service/src/services/logging_service.rs @@ -0,0 +1,365 @@ +use core_lib::{ + api::crypto::ChClaims, + constants::{DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, DEFAULT_PROCESS_ID}, + model::{ + document::Document, + process::Process, + SortingOrder, + }, +}; +use rocket::form::validate::Contains; +use rocket::State; +use std::convert::TryFrom; +use anyhow::anyhow; + +use crate::model::{ids::{ + message::IdsMessage, + IdsQueryResult, + request::ClearingHouseMessage, +}, OwnerList, DataTransaction, Receipt}; +use crate::db::ProcessStore; +use crate::services::document_service::DocumentService; + +pub struct LoggingService { + db: ProcessStore, + doc_api: DocumentService, +} + +impl LoggingService { + pub async fn log( + &self, + ch_claims: ChClaims, + key_path: &State, + msg: ClearingHouseMessage, + pid: String, + ) -> anyhow::Result { + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + // Add non-InfoModel information to IdsMessage + let mut m = msg.header; + m.payload = msg.payload; + m.payload_type = msg.payload_type; + m.pid = Some(pid.clone()); + + // validate that there is a payload + if m.payload.is_none() || (m.payload.is_some() && m.payload.as_ref().unwrap().trim().is_empty()) { + error!("Trying to log an empty payload!"); + return Err(anyhow!("No payload received for logging!")); // BadRequest + } + + // filter out calls for default process id and call application logic + match DEFAULT_PROCESS_ID.eq(pid.as_str()) { + true => { + warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); + Err(anyhow!("Document already exists")) // BadRequest + } + false => { + // convenience: if process does not exist, we create it but only if no error occurred before + match self.db.get_process(&pid).await { + Ok(Some(_p)) => { + debug!("Requested pid '{}' exists. Nothing to create.", &pid); + } + Ok(None) => { + info!("Requested pid '{}' does not exist. Creating...", &pid); + // create a new process + let new_process = Process::new(pid.clone(), vec!(user.clone())); + + if self.db.store_process(new_process).await.is_err() { + error!("Error while creating process '{}'", &pid); + return Err(anyhow!("Error while creating process")); // InternalError + } + } + Err(_) => { + error!("Error while getting process '{}'", &pid); + return Err(anyhow!("Error while getting process")); // InternalError + } + } + + // now check if user is authorized to write to pid + match self.db.is_authorized(&user, &pid).await { + Ok(true) => info!("User authorized."), + Ok(false) => { + warn!("User is not authorized to write to pid '{}'", &pid); + warn!("This is the forbidden branch"); + return Err(anyhow!("User not authorized!")); // Forbidden + } + Err(_) => { + error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + return Err(anyhow!("Error during authorization")); + } + } + + debug!("logging message for pid {}", &pid); + self.log_message(user, key_path.inner().as_str(), m.clone()).await + } + } + } + + pub(crate) async fn create_process( + &self, + ch_claims: ChClaims, + msg: ClearingHouseMessage, + pid: String, + ) -> anyhow::Result { + let mut m = msg.header; + m.payload = msg.payload; + m.payload_type = msg.payload_type; + + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + + // validate payload + let mut owners = vec!(user.clone()); + let payload = m.payload.clone().unwrap_or(String::new()); + if !payload.is_empty() { + trace!("OwnerList: '{:#?}'", &payload); + match serde_json::from_str::(&payload) { + Ok(owner_list) => { + for o in owner_list.owners { + if !owners.contains(&o) { + owners.push(o); + } + } + } + Err(e) => { + error!("Error while creating process '{}': {}", &pid, e); + return Err(anyhow!("Invalid owner list!")); // BadRequest + } + }; + }; + + // check if the pid already exists + match self.db.get_process(&pid).await { + Ok(Some(p)) => { + warn!("Requested pid '{}' already exists.", &p.id); + if !p.owners.contains(user) { + Err(anyhow!("User not authorized!")) // Forbidden + } else { + Err(anyhow!("Process already exists!")) // BadRequest + } + } + _ => { + // filter out calls for default process id + match DEFAULT_PROCESS_ID.eq(pid.as_str()) { + true => { + warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); + Err(anyhow!("Document already exists")) // BadRequest + } + false => { + info!("Requested pid '{}' will have {} owners", &pid, owners.len()); + + // create process + info!("Requested pid '{}' does not exist. Creating...", &pid); + let new_process = Process::new(pid.clone(), owners); + + match self.db.store_process(new_process).await { + Ok(_) => { + Ok(pid.clone()) + } + Err(e) => { + error!("Error while creating process '{}': {}", &pid, e); + Err(anyhow!("Error while creating process")) // InternalError + } + } + } + } + } + } + } + + async fn log_message( + &self, + user: &String, + key_path: &str, + message: IdsMessage, + ) -> anyhow::Result { + debug!("transforming message to document..."); + let payload = message.payload.as_ref().unwrap().clone(); + // transform message to document + let mut doc = Document::from(message); + match self.db.get_transaction_counter().await { + Ok(Some(tid)) => { + debug!("Storing document..."); + doc.tc = tid; + // TODO: ChClaims usage check + match self.doc_api.create_enc_document(ChClaims::new(&user), doc.clone()).await { + Ok(doc_receipt) => { + debug!("Increase transaction counter"); + match self.db.increment_transaction_counter().await { + Ok(Some(_tid)) => { + debug!("Creating receipt..."); + let transaction = DataTransaction { + transaction_id: doc.get_formatted_tc(), + timestamp: doc_receipt.timestamp, + process_id: doc_receipt.pid, + document_id: doc_receipt.doc_id, + payload, + chain_hash: doc_receipt.chain_hash, + client_id: user.clone(), + clearing_house_version: env!("CARGO_PKG_VERSION").to_string(), + }; + debug!("...done. Signing receipt..."); + Ok(transaction.sign(key_path)) + } + _ => { + error!("Error while incrementing transaction id!"); + Err(anyhow!("Internal error while preparing transaction data")) // InternalError + } + } + } + Err(e) => { + error!("Error while creating document: {:?}", e); + Err(anyhow!("Document already exists")) // BadRequest + } + } + } + Ok(None) => { + println!("None!"); + Err(anyhow!("Internal error while preparing transaction data")) // InternalError + } + Err(e) => { + error!("Error while getting transaction id!"); + println!("{}", e); + Err(anyhow!("Internal error while preparing transaction data")) // InternalError + } + } + } + + pub(crate) async fn query_pid( + &self, + ch_claims: ChClaims, + page: Option, + size: Option, + sort: Option, + date_to: Option, + date_from: Option, + pid: String, + message: ClearingHouseMessage, + ) -> anyhow::Result { + debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); + + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + + // check if process exists + match self.db.exists_process(&pid).await { + Ok(true) => info!("User authorized."), + Ok(false) => return Err(anyhow!("Process does not exist!")), // NotFound + Err(_e) => { + error!("Error while checking process '{}' for user '{}'", &pid, &user); + return Err(anyhow!("Cannot authorize user!")); // InternalError + } + }; + + // now check if user is authorized to read infos in pid + match self.db.is_authorized(&user, &pid).await { + Ok(true) => { + info!("User authorized."); + } + Ok(false) => { + warn!("User is not authorized to write to pid '{}'", &pid); + return Err(anyhow!("User not authorized!")); // Forbidden + } + Err(_) => { + error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + return Err(anyhow!("Cannot authorize user!")); // InternalError + } + } + + // sanity check for pagination + let sanitized_page = match page { + Some(p) => { + if p >= 0 { + p + } else { + warn!("...invalid page requested. Falling back to 0."); + 1 + } + } + None => 1 + }; + + let sanitized_size = match size { + Some(s) => { + let converted_max = i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap(); + if s > converted_max { + warn!("...invalid size requested. Falling back to default."); + converted_max + } else { + if s > 0 { + s + } else { + warn!("...invalid size requested. Falling back to default."); + i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() + } + } + } + None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() + }; + + let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); + + match self.doc_api.get_enc_documents_for_pid(ChClaims::new(&user), None, Some(sanitized_page), Some(sanitized_size), Some(sanitized_sort), date_from, date_to, pid.clone()).await { + Ok(r) => { + let messages: Vec = r.documents.iter().map(|d| IdsMessage::from(d.clone())).collect(); + let result = IdsQueryResult::new(r.date_from, r.date_to, r.page, r.size, r.order, messages); + Ok(result) + } + Err(e) => { + error!("Error while retrieving message: {:?}", e); + Err(anyhow!("Error while retrieving messages for pid {}!", &pid)) // InternalError + } + } + } + + pub(crate) async fn query_id(&self, + ch_claims: ChClaims, + pid: String, + id: String, + message: ClearingHouseMessage, + ) -> anyhow::Result { + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + + // check if process exists + match self.db.exists_process(&pid).await { + Ok(true) => info!("User authorized."), + Ok(false) => return Err(anyhow!("Process does not exist!")), // NotFound + Err(_e) => { + error!("Error while checking process '{}' for user '{}'", &pid, &user); + return Err(anyhow!("Cannot authorize user!")); // InternalError + } + }; + + // now check if user is authorized to read infos in pid + match self.db.is_authorized(&user, &pid).await { + Ok(true) => { + info!("User authorized."); + } + Ok(false) => { + warn!("User is not authorized to write to pid '{}'", &pid); + return Err(anyhow!("User not authorized!")); // Forbidden + } + Err(_) => { + error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + return Err(anyhow!("Cannot authorize user!")); // InternalError + } + } + + match self.doc_api.get_enc_document(ChClaims::new(&user), pid.clone(), id.clone(), None).await { + Ok(doc) => { + // transform document to IDS message + let queried_message = IdsMessage::from(doc); + Ok(queried_message) + } + /*Result::Ok(None) => { + debug!("Queried a non-existing document: {}", &id); + ApiResponse::NotFound(format!("No message found with id {}!", &id)) + }*/ + Err(e) => { + error!("Error while retrieving message: {:?}", e); + Err(anyhow!("Error while retrieving message with id {}!", &id)) // InternalError + } + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs index 01df54e..a1b5a37 100644 --- a/clearing-house-app/logging-service/src/services/mod.rs +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -1,2 +1,9 @@ +//! # Services +//! +//! This module contains the Application Services that are used by the API Controllers. It is +//! responsible for the business logic of the application. The services are used by the API +//! Controllers to handle the requests and responses. +//! mod keyring_service; -mod document_service; \ No newline at end of file +mod document_service; +pub(crate) mod logging_service; \ No newline at end of file From 71d51ad90cdff372c5ceb205778308f6aa368b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 10:14:36 +0200 Subject: [PATCH 012/183] refactor(ch-app): Refactor rocket-handlers into the ports module --- clearing-house-app/logging-service/src/main.rs | 3 +-- .../logging-service/src/{ => ports}/logging_api.rs | 0 clearing-house-app/logging-service/src/ports/mod.rs | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) rename clearing-house-app/logging-service/src/{ => ports}/logging_api.rs (100%) diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 6dfd988..62fcf31 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -13,7 +13,6 @@ use core_lib::constants::ENV_LOGGING_SERVICE_ID; use db::ProcessStoreConfigurator; use model::constants::SIGNING_KEY; -pub mod logging_api; pub mod db; pub mod model; mod services; @@ -44,5 +43,5 @@ fn rocket() -> Rocket { .attach(add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) .attach(ApiClientConfigurator::new(ApiClientEnum::Document)) .attach(ApiClientConfigurator::new(ApiClientEnum::Keyring)) - .attach(logging_api::mount_api()) + .attach(ports::logging_api::mount_api()) } diff --git a/clearing-house-app/logging-service/src/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs similarity index 100% rename from clearing-house-app/logging-service/src/logging_api.rs rename to clearing-house-app/logging-service/src/ports/logging_api.rs diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/logging-service/src/ports/mod.rs index 7c2432b..9e62cb4 100644 --- a/clearing-house-app/logging-service/src/ports/mod.rs +++ b/clearing-house-app/logging-service/src/ports/mod.rs @@ -3,3 +3,4 @@ //! This module contains the ports of the logging service. Ports are used to communicate with other //! services. In this case, the logging service implements REST-API endpoints to provide access to //! the logging service. +pub mod logging_api; From 6a3934e089f775bf434821d0e672e63daf34676c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 10:29:30 +0200 Subject: [PATCH 013/183] feat(ch-app): Bump Cargo edition to 2021 and remove unused imports --- clearing-house-app/core-lib/Cargo.toml | 2 +- clearing-house-app/document-api/Cargo.toml | 2 +- clearing-house-app/keyring-api/Cargo.toml | 2 +- clearing-house-app/logging-service/Cargo.toml | 2 +- clearing-house-app/logging-service/src/ports/logging_api.rs | 1 - .../logging-service/src/services/document_service.rs | 2 +- .../logging-service/src/services/keyring_service.rs | 3 --- 7 files changed, 5 insertions(+), 9 deletions(-) diff --git a/clearing-house-app/core-lib/Cargo.toml b/clearing-house-app/core-lib/Cargo.toml index 140385c..1a993c7 100644 --- a/clearing-house-app/core-lib/Cargo.toml +++ b/clearing-house-app/core-lib/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Mark Gall ", "Georg Bramm ", ] -edition = "2018" +edition = "2021" [dependencies] aes = "0.6.0" diff --git a/clearing-house-app/document-api/Cargo.toml b/clearing-house-app/document-api/Cargo.toml index 3cb0918..f844f62 100644 --- a/clearing-house-app/document-api/Cargo.toml +++ b/clearing-house-app/document-api/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Mark Gall ", "Georg Bramm ", ] -edition = "2018" +edition = "2021" [dependencies] biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } diff --git a/clearing-house-app/keyring-api/Cargo.toml b/clearing-house-app/keyring-api/Cargo.toml index 7d3eda4..37718a3 100644 --- a/clearing-house-app/keyring-api/Cargo.toml +++ b/clearing-house-app/keyring-api/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Mark Gall ", "Georg Bramm " ] -edition = "2018" +edition = "2021" [dependencies] aes = "0.6.0" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index afb441e..ccd6294 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Mark Gall ", "Georg Bramm ", ] -edition = "2018" +edition = "2021" [dependencies] biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } diff --git a/clearing-house-app/logging-service/src/ports/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs index f38b316..8ac0bac 100644 --- a/clearing-house-app/logging-service/src/ports/logging_api.rs +++ b/clearing-house-app/logging-service/src/ports/logging_api.rs @@ -8,7 +8,6 @@ use core_lib::{ use rocket::serde::json::{json, Json}; use rocket::fairing::AdHoc; use rocket::State; -use std::convert::TryFrom; use crate::model::ids::request::ClearingHouseMessage; use crate::model::constants::{ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_QUERY_API, ROCKET_PROCESS_API, ROCKET_PK_API}; diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 4cacc31..1261d25 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; use anyhow::anyhow; -use core_lib::api::crypto::{ChClaims, create_service_token}; +use core_lib::api::crypto::ChClaims; use core_lib::api::{DocumentReceipt, QueryResult}; use core_lib::constants::{DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART}; use core_lib::model::document::Document; diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index f15e973..0937beb 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -1,8 +1,5 @@ -use std::process::exit; use anyhow::anyhow; -use rocket::futures::TryStreamExt; use core_lib::api::crypto::ChClaims; -use core_lib::constants::MONGO_COLL_MASTER_KEY; use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::crypto; use crate::crypto::restore_key_map; From de2fe53c98e5e9e619807779ed9c4e9904a0ffb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 11:39:24 +0200 Subject: [PATCH 014/183] refactor(ch-app): Moved rocket fairings in separate module in logging service, restructured keystore fairing --- .../src/db/config/doc_store.rs | 119 ++++++++++++++++++ .../src/db/config/keyring_store.rs | 113 +++++++++++++++++ .../logging-service/src/db/config/mod.rs | 3 + .../src/db/config/process_store.rs | 81 ++++++++++++ .../src/db/{docstore.rs => doc_store.rs} | 30 +++-- .../src/db/{keystore.rs => key_store.rs} | 15 ++- .../logging-service/src/db/mod.rs | 80 +----------- .../logging-service/src/main.rs | 6 +- .../logging-service/src/ports/doc_type_api.rs | 106 ++++++++++++++++ .../logging-service/src/ports/mod.rs | 3 +- .../src/services/document_service.rs | 2 +- .../src/services/keyring_service.rs | 2 +- 12 files changed, 458 insertions(+), 102 deletions(-) create mode 100644 clearing-house-app/logging-service/src/db/config/doc_store.rs create mode 100644 clearing-house-app/logging-service/src/db/config/keyring_store.rs create mode 100644 clearing-house-app/logging-service/src/db/config/mod.rs create mode 100644 clearing-house-app/logging-service/src/db/config/process_store.rs rename clearing-house-app/logging-service/src/db/{docstore.rs => doc_store.rs} (95%) rename clearing-house-app/logging-service/src/db/{keystore.rs => key_store.rs} (93%) create mode 100644 clearing-house-app/logging-service/src/ports/doc_type_api.rs diff --git a/clearing-house-app/logging-service/src/db/config/doc_store.rs b/clearing-house-app/logging-service/src/db/config/doc_store.rs new file mode 100644 index 0000000..58e132b --- /dev/null +++ b/clearing-house-app/logging-service/src/db/config/doc_store.rs @@ -0,0 +1,119 @@ +use mongodb::bson::doc; +use mongodb::IndexModel; +use mongodb::options::{CreateCollectionOptions, IndexOptions, WriteConcern}; +use rocket::{Build, fairing, Rocket}; +use core_lib::constants::{CLEAR_DB, DATABASE_URL, DOCUMENT_DB, DOCUMENT_DB_CLIENT, MONGO_COLL_DOCUMENT_BUCKET, MONGO_DOC_ARRAY, MONGO_PID, MONGO_TS, MONGO_TC}; +use core_lib::db::init_database_client; +use core_lib::model::document::Document; +use crate::db::doc_store::DataStore; + +#[derive(Clone, Debug)] +pub struct DatastoreConfigurator; + +#[rocket::async_trait] +impl fairing::Fairing for DatastoreConfigurator { + fn info(&self) -> fairing::Info { + fairing::Info { + name: "Configuring Document Database", + kind: fairing::Kind::Ignite + } + } + async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { + let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); + let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ + Ok(value) => { + debug!("clear_db: '{}' found.", &value); + value + }, + Err(_) => { + false + } + }; + debug!("Using mongodb url: '{:#?}'", &db_url); + match init_database_client::(&db_url.as_str(), Some(DOCUMENT_DB_CLIENT.to_string())).await{ + Ok(datastore) => { + debug!("Check if database is empty..."); + match datastore.client.database(DOCUMENT_DB) + .list_collection_names(None) + .await{ + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + let number_of_colls = match colls.contains(&MONGO_COLL_DOCUMENT_BUCKET.to_string()){ + true => colls.len(), + false => 0 + }; + + if number_of_colls > 0 && clear_db{ + debug!("Database not empty and clear_db == true. Dropping database..."); + match datastore.client.database(DOCUMENT_DB).drop(None).await{ + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + }; + } + if number_of_colls == 0 || clear_db{ + debug!("Database empty. Need to initialize..."); + let mut write_concern = WriteConcern::default(); + write_concern.journal = Some(true); + let mut options = CreateCollectionOptions::default(); + options.write_concern = Some(write_concern); + debug!("Create collection {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore.client.database(DOCUMENT_DB).create_collection(MONGO_COLL_DOCUMENT_BUCKET, options).await{ + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + }; + + // This purpose of this index is to ensure that the transaction counter is unique + let mut index_options = IndexOptions::default(); + index_options.unique = Some(true); + let mut index_model = IndexModel::default(); + index_model.keys = doc!{format!("{}.{}",MONGO_DOC_ARRAY, MONGO_TC): 1}; + index_model.options = Some(index_options); + + debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore.client.database(DOCUMENT_DB).collection::(MONGO_COLL_DOCUMENT_BUCKET).create_index(index_model, None).await{ + Ok(result) => { + debug!("... index {} created", result.index_name); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + } + + // This creates a compound index over pid and the timestamp to enable paging using buckets + let mut compound_index_model = IndexModel::default(); + compound_index_model.keys = doc!{MONGO_PID: 1, MONGO_TS: 1}; + + debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore.client.database(DOCUMENT_DB).collection::(MONGO_COLL_DOCUMENT_BUCKET).create_index(compound_index_model, None).await{ + Ok(result) => { + debug!("... index {} created", result.index_name); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + } + } + debug!("... database initialized."); + Ok(rocket.manage(datastore)) + } + Err(_) => { + Err(rocket) + } + } + }, + Err(_) => Err(rocket) + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/config/keyring_store.rs b/clearing-house-app/logging-service/src/db/config/keyring_store.rs new file mode 100644 index 0000000..ce459c1 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/config/keyring_store.rs @@ -0,0 +1,113 @@ +use anyhow::anyhow; +use rocket::fairing::Kind; +use rocket::{Build, fairing, Rocket}; +use core_lib::constants::{CLEAR_DB, DATABASE_URL, FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT}; +use core_lib::db::init_database_client; +use core_lib::util::read_file; +use crate::db::key_store::KeyStore; +use crate::model::crypto::MasterKey; +use crate::model::doc_type::DocumentType; + +#[derive(Clone, Debug)] +pub struct KeyringDbConfigurator; + +#[rocket::async_trait] +impl fairing::Fairing for KeyringDbConfigurator { + fn info(&self) -> fairing::Info { + fairing::Info { + name: "Configuring Keyring Database", + kind: Kind::Ignite + } + } + async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { + let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); + let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { + Ok(value) => { + debug!("clear_db: '{}' found.", &value); + value + }, + Err(_) => { + false + } + }; + debug!("Using database url: '{:#?}'", &db_url); + + match Self::init_keystore(db_url, clear_db).await { + Ok(keystore) => { + Ok(rocket.manage(keystore)) + }, + Err(_) => { + Err(rocket) + } + } + } +} + +impl KeyringDbConfigurator { + pub async fn init_keystore(db_url: String, clear_db: bool) -> anyhow::Result { + debug!("Using database url: '{:#?}'", &db_url); + + match init_database_client::(&db_url.as_str(), Some(KEYRING_DB_CLIENT.to_string())).await { + Ok(keystore) => { + debug!("Check if database is empty..."); + match keystore.client.database(KEYRING_DB) + .list_collection_names(None) + .await { + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + if colls.len() > 0 && clear_db { + debug!("Database not empty and clear_db == true. Dropping database..."); + match keystore.client.database(KEYRING_DB).drop(None).await { + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to drop database")); + } + }; + } + if colls.len() == 0 || clear_db { + debug!("Database empty. Need to initialize..."); + debug!("Adding initial document type..."); + match serde_json::from_str::(&read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or(String::new())) { + Ok(dt) => { + match keystore.add_document_type(dt).await { + Ok(_) => { + debug!("... done."); + }, + Err(e) => { + error!("Error while adding initial document type: {:#?}", e); + return Err(anyhow!("Error while adding initial document type")); + } + } + } + _ => { + error!("Error while loading initial document type"); + return Err(anyhow!("Error while loading initial document type")); + } + }; + debug!("Creating master key..."); + // create master key + match keystore.store_master_key(MasterKey::new_random()).await { + Ok(true) => { + debug!("... done."); + }, + _ => { + error!("... failed to create master key"); + return Err(anyhow!("Failed to create master key")); + } + }; + } + debug!("... database initialized."); + Ok(keystore) + } + Err(_) => { + Err(anyhow!("Failed to list collections")) + } + } + }, + Err(_) => Err(anyhow!("Failed to initialize database client")) + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/config/mod.rs b/clearing-house-app/logging-service/src/db/config/mod.rs new file mode 100644 index 0000000..5e2b055 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/config/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod process_store; +pub(crate) mod keyring_store; +pub(crate) mod doc_store; \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/config/process_store.rs b/clearing-house-app/logging-service/src/db/config/process_store.rs new file mode 100644 index 0000000..2e16c63 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/config/process_store.rs @@ -0,0 +1,81 @@ +use mongodb::options::{CreateCollectionOptions, WriteConcern}; +use rocket::{Build, Rocket}; +use rocket::fairing::Kind; +use core_lib::constants::{CLEAR_DB, DATABASE_URL, MONGO_COLL_TRANSACTIONS, PROCESS_DB, PROCESS_DB_CLIENT}; +use core_lib::db::init_database_client; +use crate::db::ProcessStore; + +#[derive(Clone, Debug)] +pub struct ProcessStoreConfigurator; + +#[rocket::async_trait] +impl rocket::fairing::Fairing for ProcessStoreConfigurator { + fn info(&self) -> rocket::fairing::Info { + rocket::fairing::Info { + name: "Configuring Process Database", + kind: Kind::Ignite + } + } + async fn on_ignite(&self, rocket: Rocket) -> rocket::fairing::Result { + debug!("Preparing to initialize database..."); + let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); + let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ + Ok(value) => { + debug!("...clear_db: {} found. ", &value); + value + }, + Err(_) => { + false + } + }; + debug!("...using database url: '{:#?}'", &db_url); + + match init_database_client::(&db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string())).await{ + Ok(process_store) => { + debug!("...check if database is empty..."); + match process_store.client.database(PROCESS_DB) + .list_collection_names(None) + .await{ + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + if colls.len() > 0 && clear_db{ + debug!("...database not empty and clear_db == true. Dropping database..."); + match process_store.client.database(PROCESS_DB).drop(None).await{ + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + }; + } + if colls.len() == 0 || clear_db{ + debug!("..database empty. Need to initialize..."); + let mut write_concern = WriteConcern::default(); + write_concern.journal = Some(true); + let mut options = CreateCollectionOptions::default(); + options.write_concern = Some(write_concern); + debug!("...create collection {} ...", MONGO_COLL_TRANSACTIONS); + match process_store.client.database(PROCESS_DB).create_collection(MONGO_COLL_TRANSACTIONS, options).await{ + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + }; + } + debug!("... database initialized."); + Ok(rocket.manage(process_store)) + } + Err(_) => { + Err(rocket) + } + } + }, + Err(_) => Err(rocket) + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/docstore.rs b/clearing-house-app/logging-service/src/db/doc_store.rs similarity index 95% rename from clearing-house-app/logging-service/src/db/docstore.rs rename to clearing-house-app/logging-service/src/db/doc_store.rs index 4a22827..7d39f21 100644 --- a/clearing-house-app/logging-service/src/db/docstore.rs +++ b/clearing-house-app/logging-service/src/db/doc_store.rs @@ -1,31 +1,29 @@ -use mongodb::bson; +use mongodb::{bson, Client}; use mongodb::bson::doc; use mongodb::options::{AggregateOptions, UpdateOptions}; use rocket::futures::StreamExt; -use core_lib::constants::{ - MAX_NUM_RESPONSE_ENTRIES, - MONGO_COLL_DOCUMENT_BUCKET, - MONGO_ID, - MONGO_PID, - MONGO_COUNTER, - MONGO_DOC_ARRAY, - MONGO_DT_ID, - MONGO_FROM_TS, - MONGO_TO_TS, - MONGO_TC, - MONGO_TS, -}; +use core_lib::constants::{MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_ID, MONGO_PID, MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_TO_TS, MONGO_TC, MONGO_TS, DOCUMENT_DB}; +use core_lib::db::DataStoreApi; use core_lib::model::document::EncryptedDocument; use core_lib::errors::*; use core_lib::model::SortingOrder; -use crate::db::docstore::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; +use crate::db::doc_store::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; #[derive(Clone)] pub struct DataStore { - client: mongodb::Client, + pub(crate) client: mongodb::Client, database: mongodb::Database, } +impl DataStoreApi for DataStore { + fn new(client: Client) -> DataStore{ + DataStore { + client: client.clone(), + database: client.database(DOCUMENT_DB) + } + } +} + impl DataStore { pub async fn add_document(&self, doc: EncryptedDocument) -> Result { debug!("add_document to bucket"); diff --git a/clearing-house-app/logging-service/src/db/keystore.rs b/clearing-house-app/logging-service/src/db/key_store.rs similarity index 93% rename from clearing-house-app/logging-service/src/db/keystore.rs rename to clearing-house-app/logging-service/src/db/key_store.rs index 7705b01..231f635 100644 --- a/clearing-house-app/logging-service/src/db/keystore.rs +++ b/clearing-house-app/logging-service/src/db/key_store.rs @@ -1,17 +1,28 @@ use std::process::exit; use core_lib::errors::*; use mongodb::bson::doc; +use mongodb::Client; use rocket::futures::TryStreamExt; -use core_lib::constants::{MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; +use core_lib::constants::{KEYRING_DB, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; +use core_lib::db::DataStoreApi; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; #[derive(Clone, Debug)] pub struct KeyStore { - client: mongodb::Client, + pub(crate) client: mongodb::Client, database: mongodb::Database } +impl DataStoreApi for KeyStore { + fn new(client: Client) -> KeyStore{ + KeyStore { + client: client.clone(), + database: client.database(KEYRING_DB) + } + } +} + impl KeyStore { /// Only one master key may exist in the database. diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/logging-service/src/db/mod.rs index 3451022..2c6752d 100644 --- a/clearing-house-app/logging-service/src/db/mod.rs +++ b/clearing-house-app/logging-service/src/db/mod.rs @@ -1,5 +1,6 @@ -pub(crate) mod keystore; -pub(crate) mod docstore; +pub(crate) mod key_store; +pub(crate) mod doc_store; +pub(crate) mod config; use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, DATABASE_URL, CLEAR_DB, PROCESS_DB, PROCESS_DB_CLIENT, MONGO_COLL_TRANSACTIONS, MONGO_TC}; use core_lib::db::{DataStoreApi, init_database_client}; @@ -13,81 +14,6 @@ use rocket::{Rocket, Build}; use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions, WriteConcern, CreateCollectionOptions}; use crate::model::TransactionCounter; -#[derive(Clone, Debug)] -pub struct ProcessStoreConfigurator; - -#[rocket::async_trait] -impl Fairing for ProcessStoreConfigurator { - fn info(&self) -> Info { - Info { - name: "Configuring Process Database", - kind: Kind::Ignite - } - } - async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - debug!("Preparing to initialize database..."); - let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ - Ok(value) => { - debug!("...clear_db: {} found. ", &value); - value - }, - Err(_) => { - false - } - }; - debug!("...using database url: '{:#?}'", &db_url); - - match init_database_client::(&db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string())).await{ - Ok(process_store) => { - debug!("...check if database is empty..."); - match process_store.client.database(PROCESS_DB) - .list_collection_names(None) - .await{ - Ok(colls) => { - debug!("... found collections: {:#?}", &colls); - if colls.len() > 0 && clear_db{ - debug!("...database not empty and clear_db == true. Dropping database..."); - match process_store.client.database(PROCESS_DB).drop(None).await{ - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - }; - } - if colls.len() == 0 || clear_db{ - debug!("..database empty. Need to initialize..."); - let mut write_concern = WriteConcern::default(); - write_concern.journal = Some(true); - let mut options = CreateCollectionOptions::default(); - options.write_concern = Some(write_concern); - debug!("...create collection {} ...", MONGO_COLL_TRANSACTIONS); - match process_store.client.database(PROCESS_DB).create_collection(MONGO_COLL_TRANSACTIONS, options).await{ - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - }; - } - debug!("... database initialized."); - Ok(rocket.manage(process_store)) - } - Err(_) => { - Err(rocket) - } - } - }, - Err(_) => Err(rocket) - } - } -} - #[derive(Clone)] pub struct ProcessStore { client: Client, diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 62fcf31..207b36f 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -4,13 +4,12 @@ #[macro_use] extern crate serde_derive; use std::path::Path; -use core_lib::api::client::{ApiClientConfigurator, ApiClientEnum}; use core_lib::util::{add_service_config, setup_logger}; use rocket::{Build, Rocket}; use rocket::fairing::AdHoc; use core_lib::constants::ENV_LOGGING_SERVICE_ID; -use db::ProcessStoreConfigurator; +use db::config::process_store::ProcessStoreConfigurator; use model::constants::SIGNING_KEY; pub mod db; @@ -41,7 +40,6 @@ fn rocket() -> Rocket { .attach(ProcessStoreConfigurator) .attach(add_signing_key()) .attach(add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) - .attach(ApiClientConfigurator::new(ApiClientEnum::Document)) - .attach(ApiClientConfigurator::new(ApiClientEnum::Keyring)) .attach(ports::logging_api::mount_api()) + .attach(ports::doc_type_api::mount_api()) } diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs new file mode 100644 index 0000000..af5a082 --- /dev/null +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -0,0 +1,106 @@ +use core_lib::api::ApiResponse; +use core_lib::constants::{ROCKET_DOC_TYPE_API, DEFAULT_PROCESS_ID}; +use rocket::fairing::AdHoc; +use rocket::State; +use rocket::serde::json::{json,Json}; + +use crate::db::key_store::KeyStore; +use crate::model::doc_type::DocumentType; + +#[post("/", format = "json", data = "")] +async fn create_doc_type(db: &State, doc_type: Json) -> ApiResponse { + let doc_type: DocumentType = doc_type.into_inner(); + debug!("adding doctype: {:?}", &doc_type); + match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + Ok(true) => ApiResponse::BadRequest(String::from("doctype already exists!")), + Ok(false) => { + match db.add_document_type(doc_type.clone()).await{ + Ok(()) => ApiResponse::SuccessCreate(json!(doc_type)), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + return ApiResponse::InternalError(String::from("Error while adding document type!")) + } + } + }, + Err(e) => { + error!("Error while adding document type: {:?}", e); + return ApiResponse::InternalError(String::from("Error while checking database!")) + } + } +} + +#[post("/", format = "json", data = "")] +async fn update_doc_type(db: &State, id: String, doc_type: Json) -> ApiResponse { + let doc_type: DocumentType = doc_type.into_inner(); + match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + Ok(true) => ApiResponse::BadRequest(String::from("Doctype already exists!")), + Ok(false) => { + match db.update_document_type(doc_type, &id).await{ + Ok(id) => ApiResponse::SuccessOk(json!(id)), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + return ApiResponse::InternalError(String::from("Error while storing document type!")) + } + } + }, + Err(e) => { + error!("Error while adding document type: {:?}", e); + return ApiResponse::InternalError(String::from("Error while checking database!")) + } + } +} + +#[delete("/", format = "json")] +async fn delete_default_doc_type(db: &State, id: String) -> ApiResponse{ + delete_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await +} + +#[delete("//", format = "json")] +async fn delete_doc_type(db: &State, id: String, pid: String) -> ApiResponse{ + match db.delete_document_type(&id, &pid).await{ + Ok(true) => ApiResponse::SuccessNoContent(String::from("Document type deleted!")), + Ok(false) => ApiResponse::NotFound(String::from("Document type does not exist!")), + Err(e) => { + error!("Error while deleting doctype: {:?}", e); + ApiResponse::InternalError(format!("Error while deleting document type with id {}!", id)) + } + } +} + +#[get("/", format = "json")] +async fn get_default_doc_type(db: &State, id: String) -> ApiResponse { + get_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await +} + +#[get("//", format = "json")] +async fn get_doc_type(db: &State, id: String, pid: String) -> ApiResponse { + match db.get_document_type(&id).await{ + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(dt) => ApiResponse::SuccessOk(json!(dt)), + Err(e) => { + error!("Error while retrieving doctype: {:?}", e); + ApiResponse::InternalError(format!("Error while retrieving document type with id {} and pid {}!", id, pid)) + } + } +} + +#[get("/", format = "json")] +async fn get_doc_types(db: &State) -> ApiResponse { + match db.get_all_document_types().await { + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(dt) => ApiResponse::SuccessOk(json!(dt)), + Err(e) => { + error!("Error while retrieving default doctypes: {:?}", e); + ApiResponse::InternalError(format!("Error while retrieving all document types")) + } + } +} + +pub fn mount_api() -> AdHoc { + AdHoc::on_ignite("Mounting Document Type API", |rocket| async { + rocket + .mount(ROCKET_DOC_TYPE_API, routes![create_doc_type, + update_doc_type, delete_default_doc_type, delete_doc_type, + get_default_doc_type, get_doc_type , get_doc_types]) + }) +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/logging-service/src/ports/mod.rs index 9e62cb4..9948361 100644 --- a/clearing-house-app/logging-service/src/ports/mod.rs +++ b/clearing-house-app/logging-service/src/ports/mod.rs @@ -3,4 +3,5 @@ //! This module contains the ports of the logging service. Ports are used to communicate with other //! services. In this case, the logging service implements REST-API endpoints to provide access to //! the logging service. -pub mod logging_api; +pub(crate) mod logging_api; +pub(crate) mod doc_type_api; diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 1261d25..8abda1c 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -7,7 +7,7 @@ use core_lib::model::document::Document; use core_lib::model::{parse_date, sanitize_dates, SortingOrder, validate_dates}; use core_lib::model::crypto::{KeyCt, KeyCtList}; use crate::services::keyring_service::KeyringService; -use crate::db::docstore::DataStore; +use crate::db::doc_store::DataStore; pub struct DocumentService { db: DataStore, diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index 0937beb..ff3962b 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -3,7 +3,7 @@ use core_lib::api::crypto::ChClaims; use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::crypto; use crate::crypto::restore_key_map; -use crate::db::keystore::KeyStore; +use crate::db::key_store::KeyStore; pub struct KeyringService { db: KeyStore, From 63af464946d8be78c4aff9f2e1f3e5c04870f30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 11:55:32 +0200 Subject: [PATCH 015/183] refactor(ch-app): Separated the remaining rocket fairings logic into separate function --- .../src/db/config/doc_store.rs | 27 ++++++++++++++----- .../src/db/config/keyring_store.rs | 1 - .../src/db/config/process_store.rs | 25 +++++++++++++---- .../logging-service/src/db/mod.rs | 9 +++---- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/clearing-house-app/logging-service/src/db/config/doc_store.rs b/clearing-house-app/logging-service/src/db/config/doc_store.rs index 58e132b..81c51e9 100644 --- a/clearing-house-app/logging-service/src/db/config/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/config/doc_store.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use mongodb::bson::doc; use mongodb::IndexModel; use mongodb::options::{CreateCollectionOptions, IndexOptions, WriteConcern}; @@ -29,6 +30,18 @@ impl fairing::Fairing for DatastoreConfigurator { false } }; + + match Self::init_datastore(db_url, clear_db).await { + Ok(datastore) => { + Ok(rocket.manage(datastore)) + }, + Err(_) => Err(rocket) + } + } +} + +impl DatastoreConfigurator { + pub async fn init_datastore(db_url: String, clear_db: bool) -> anyhow::Result { debug!("Using mongodb url: '{:#?}'", &db_url); match init_database_client::(&db_url.as_str(), Some(DOCUMENT_DB_CLIENT.to_string())).await{ Ok(datastore) => { @@ -51,7 +64,7 @@ impl fairing::Fairing for DatastoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to drop database")); } }; } @@ -68,7 +81,7 @@ impl fairing::Fairing for DatastoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to create collection")); } }; @@ -86,7 +99,7 @@ impl fairing::Fairing for DatastoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to create index")); } } @@ -101,19 +114,19 @@ impl fairing::Fairing for DatastoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to create compound index")); } } } debug!("... database initialized."); - Ok(rocket.manage(datastore)) + Ok(datastore) } Err(_) => { - Err(rocket) + Err(anyhow!("Failed to list collections")) } } }, - Err(_) => Err(rocket) + Err(_) => Err(anyhow!("Failed to initialize database client")) } } } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/config/keyring_store.rs b/clearing-house-app/logging-service/src/db/config/keyring_store.rs index ce459c1..cf83a77 100644 --- a/clearing-house-app/logging-service/src/db/config/keyring_store.rs +++ b/clearing-house-app/logging-service/src/db/config/keyring_store.rs @@ -30,7 +30,6 @@ impl fairing::Fairing for KeyringDbConfigurator { false } }; - debug!("Using database url: '{:#?}'", &db_url); match Self::init_keystore(db_url, clear_db).await { Ok(keystore) => { diff --git a/clearing-house-app/logging-service/src/db/config/process_store.rs b/clearing-house-app/logging-service/src/db/config/process_store.rs index 2e16c63..e0fb2e7 100644 --- a/clearing-house-app/logging-service/src/db/config/process_store.rs +++ b/clearing-house-app/logging-service/src/db/config/process_store.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use mongodb::options::{CreateCollectionOptions, WriteConcern}; use rocket::{Build, Rocket}; use rocket::fairing::Kind; @@ -28,6 +29,19 @@ impl rocket::fairing::Fairing for ProcessStoreConfigurator { false } }; + + match Self::init_process_store(db_url, clear_db).await { + Ok(process_store) => { + debug!("...done."); + Ok(rocket.manage(process_store)) + }, + Err(_) => Err(rocket) + } + } +} + +impl ProcessStoreConfigurator { + pub async fn init_process_store(db_url: String, clear_db: bool) -> anyhow::Result { debug!("...using database url: '{:#?}'", &db_url); match init_database_client::(&db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string())).await{ @@ -46,7 +60,7 @@ impl rocket::fairing::Fairing for ProcessStoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to drop database")); } }; } @@ -63,19 +77,20 @@ impl rocket::fairing::Fairing for ProcessStoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to create collection")); } }; } debug!("... database initialized."); - Ok(rocket.manage(process_store)) + Ok(process_store) } Err(_) => { - Err(rocket) + Err(anyhow!("Failed to list collections")) } } }, - Err(_) => Err(rocket) + Err(_) => Err(anyhow!("Failed to initialize database client")) } } + } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/logging-service/src/db/mod.rs index 2c6752d..8276c20 100644 --- a/clearing-house-app/logging-service/src/db/mod.rs +++ b/clearing-house-app/logging-service/src/db/mod.rs @@ -2,16 +2,15 @@ pub(crate) mod key_store; pub(crate) mod doc_store; pub(crate) mod config; -use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, DATABASE_URL, CLEAR_DB, PROCESS_DB, PROCESS_DB_CLIENT, MONGO_COLL_TRANSACTIONS, MONGO_TC}; -use core_lib::db::{DataStoreApi, init_database_client}; +use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, PROCESS_DB, MONGO_COLL_TRANSACTIONS, MONGO_TC}; +use core_lib::db::DataStoreApi; use core_lib::errors::*; use core_lib::model::process::Process; use mongodb::bson::doc; use mongodb::{Client, Database}; -use rocket::fairing::{self, Fairing, Info, Kind}; +use rocket::fairing::Fairing; use rocket::futures::TryStreamExt; -use rocket::{Rocket, Build}; -use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions, WriteConcern, CreateCollectionOptions}; +use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions}; use crate::model::TransactionCounter; #[derive(Clone)] From 675fce3b7298cb83ac3629e0a33d518c6d0ed7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 12:26:08 +0200 Subject: [PATCH 016/183] refactor(ch-app): Refactor doc_api and separated it likewise over ports and services --- .../logging-service/src/ports/doc_type_api.rs | 83 +++++++------------ .../src/services/keyring_service.rs | 73 ++++++++++++++++ .../logging-service/src/services/mod.rs | 2 +- 3 files changed, 106 insertions(+), 52 deletions(-) diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs index af5a082..4f4d5bb 100644 --- a/clearing-house-app/logging-service/src/ports/doc_type_api.rs +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -4,94 +4,75 @@ use rocket::fairing::AdHoc; use rocket::State; use rocket::serde::json::{json,Json}; -use crate::db::key_store::KeyStore; +use crate::services::keyring_service::KeyringService; use crate::model::doc_type::DocumentType; #[post("/", format = "json", data = "")] -async fn create_doc_type(db: &State, doc_type: Json) -> ApiResponse { - let doc_type: DocumentType = doc_type.into_inner(); - debug!("adding doctype: {:?}", &doc_type); - match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ - Ok(true) => ApiResponse::BadRequest(String::from("doctype already exists!")), - Ok(false) => { - match db.add_document_type(doc_type.clone()).await{ - Ok(()) => ApiResponse::SuccessCreate(json!(doc_type)), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(String::from("Error while adding document type!")) - } - } - }, +async fn create_doc_type(key_api: &State, doc_type: Json) -> ApiResponse { + match key_api.inner().create_doc_type(doc_type.into_inner()).await{ + Ok(dt) => ApiResponse::SuccessCreate(json!(dt)), Err(e) => { - error!("Error while adding document type: {:?}", e); - return ApiResponse::InternalError(String::from("Error while checking database!")) + error!("Error while adding doctype: {:?}", e); + return ApiResponse::InternalError(e.to_string()) } } } #[post("/", format = "json", data = "")] -async fn update_doc_type(db: &State, id: String, doc_type: Json) -> ApiResponse { - let doc_type: DocumentType = doc_type.into_inner(); - match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ - Ok(true) => ApiResponse::BadRequest(String::from("Doctype already exists!")), - Ok(false) => { - match db.update_document_type(doc_type, &id).await{ - Ok(id) => ApiResponse::SuccessOk(json!(id)), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(String::from("Error while storing document type!")) - } - } - }, +async fn update_doc_type(key_api: &State, id: String, doc_type: Json) -> ApiResponse { + match key_api.inner().update_doc_type(id, doc_type.into_inner()).await{ + Ok(id) => ApiResponse::SuccessOk(json!(id)), Err(e) => { - error!("Error while adding document type: {:?}", e); - return ApiResponse::InternalError(String::from("Error while checking database!")) + error!("Error while adding doctype: {:?}", e); + return ApiResponse::InternalError(e.to_string()) } } } #[delete("/", format = "json")] -async fn delete_default_doc_type(db: &State, id: String) -> ApiResponse{ - delete_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await +async fn delete_default_doc_type(key_api: &State, id: String) -> ApiResponse{ + delete_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } #[delete("//", format = "json")] -async fn delete_doc_type(db: &State, id: String, pid: String) -> ApiResponse{ - match db.delete_document_type(&id, &pid).await{ - Ok(true) => ApiResponse::SuccessNoContent(String::from("Document type deleted!")), - Ok(false) => ApiResponse::NotFound(String::from("Document type does not exist!")), +async fn delete_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse{ + match key_api.inner().delete_doc_type(id, pid).await{ + Ok(id) => ApiResponse::SuccessOk(json!(id)), Err(e) => { error!("Error while deleting doctype: {:?}", e); - ApiResponse::InternalError(format!("Error while deleting document type with id {}!", id)) + return ApiResponse::InternalError(e.to_string()) } } } #[get("/", format = "json")] -async fn get_default_doc_type(db: &State, id: String) -> ApiResponse { - get_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await +async fn get_default_doc_type(key_api: &State, id: String) -> ApiResponse { + get_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } #[get("//", format = "json")] -async fn get_doc_type(db: &State, id: String, pid: String) -> ApiResponse { - match db.get_document_type(&id).await{ - //TODO: would like to send "{}" instead of "null" when dt is not found - Ok(dt) => ApiResponse::SuccessOk(json!(dt)), +async fn get_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse { + match key_api.inner().get_doc_type(id, pid).await{ + Ok(dt) => { + match dt{ + Some(dt) => ApiResponse::SuccessOk(json!(dt)), + None => ApiResponse::SuccessOk(json!(null)) + } + }, Err(e) => { error!("Error while retrieving doctype: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving document type with id {} and pid {}!", id, pid)) + return ApiResponse::InternalError(e.to_string()) } } } #[get("/", format = "json")] -async fn get_doc_types(db: &State) -> ApiResponse { - match db.get_all_document_types().await { - //TODO: would like to send "{}" instead of "null" when dt is not found +async fn get_doc_types(key_api: &State) -> ApiResponse { + match key_api.inner().get_doc_types().await{ Ok(dt) => ApiResponse::SuccessOk(json!(dt)), Err(e) => { - error!("Error while retrieving default doctypes: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving all document types")) + error!("Error while retrieving doctypes: {:?}", e); + return ApiResponse::InternalError(e.to_string()) } } } diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index ff3962b..fd250e2 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -4,6 +4,7 @@ use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::crypto; use crate::crypto::restore_key_map; use crate::db::key_store::KeyStore; +use crate::model::doc_type::DocumentType; pub struct KeyringService { db: KeyStore, @@ -163,4 +164,76 @@ impl KeyringService { pub(crate) async fn decrypt_multiple_keys(&self, ch_claims: ChClaims, pid: Option, cts: &KeyCtList) -> anyhow::Result> { self.decrypt_keys(ch_claims, pid, cts).await } + + pub(crate) async fn create_doc_type(&self, doc_type: DocumentType) -> anyhow::Result { + debug!("adding doctype: {:?}", &doc_type); + match self.db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + Ok(true) => Err(anyhow!("doctype already exists!")), // BadRequest + Ok(false) => { + match self.db.add_document_type(doc_type.clone()).await{ + Ok(()) => Ok(doc_type), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + return Err(anyhow!("Error while adding document type!")) // InternalError + } + } + }, + Err(e) => { + error!("Error while adding document type: {:?}", e); + return Err(anyhow!("Error while checking database!")) // InternalError + } + } + } + + pub(crate) async fn update_doc_type(&self, id: String, doc_type: DocumentType) -> anyhow::Result { + match self.db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + Ok(true) => Err(anyhow!("Doctype already exists!")), // BadRequest + Ok(false) => { + match self.db.update_document_type(doc_type, &id).await{ + Ok(id) => Ok(id), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + return Err(anyhow!("Error while storing document type!")) // InternalError + } + } + }, + Err(e) => { + error!("Error while adding document type: {:?}", e); + return Err(anyhow!("Error while checking database!")) // InternalError + } + } + } + + pub(crate) async fn delete_doc_type(&self, id: String, pid: String) -> anyhow::Result{ + match self.db.delete_document_type(&id, &pid).await{ + Ok(true) => Ok(String::from("Document type deleted!")), // NoContent + Ok(false) => Err(anyhow!("Document type does not exist!")), // NotFound + Err(e) => { + error!("Error while deleting doctype: {:?}", e); + Err(anyhow!("Error while deleting document type with id {}!", id)) // InternalError + } + } + } + + pub(crate) async fn get_doc_type(&self, id: String, pid: String) -> anyhow::Result> { + match self.db.get_document_type(&id).await{ + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(dt) => Ok(dt), + Err(e) => { + error!("Error while retrieving doctype: {:?}", e); + Err(anyhow!("Error while retrieving document type with id {} and pid {}!", id, pid)) // InternalError + } + } + } + + pub(crate) async fn get_doc_types(&self) -> anyhow::Result> { + match self.db.get_all_document_types().await { + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(dt) => Ok(dt), + Err(e) => { + error!("Error while retrieving default doctypes: {:?}", e); + Err(anyhow!("Error while retrieving all document types")) // InternalError + } + } + } } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs index a1b5a37..cb72058 100644 --- a/clearing-house-app/logging-service/src/services/mod.rs +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -4,6 +4,6 @@ //! responsible for the business logic of the application. The services are used by the API //! Controllers to handle the requests and responses. //! -mod keyring_service; +pub(crate) mod keyring_service; mod document_service; pub(crate) mod logging_service; \ No newline at end of file From f2bf55f05687bfcca627206f9657b593a32d7f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 14:30:09 +0200 Subject: [PATCH 017/183] refactor(ch-app): Refactor config from rocket to config-rs --- clearing-house-app/Cargo.lock | 48 +++++++++++- clearing-house-app/logging-service/Cargo.toml | 3 +- .../logging-service/config.toml | 4 + .../init_db/default_doc_type.json | 42 ++++++++++ .../logging-service/src/db/doc_store.rs | 6 +- .../logging-service/src/main.rs | 76 +++++++++++++++---- .../logging-service/src/model/crypto.rs | 2 +- .../logging-service/src/model/ids/message.rs | 2 +- .../logging-service/src/model/ids/mod.rs | 14 ++-- .../logging-service/src/model/ids/request.rs | 2 +- .../logging-service/src/model/mod.rs | 8 +- .../src/services/document_service.rs | 6 ++ .../src/services/keyring_service.rs | 7 ++ .../src/services/logging_service.rs | 8 ++ .../logging-service/src/services/mod.rs | 2 +- 15 files changed, 194 insertions(+), 36 deletions(-) create mode 100644 clearing-house-app/logging-service/config.toml create mode 100644 clearing-house-app/logging-service/init_db/default_doc_type.json diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 673600d..e9395ea 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -399,6 +399,20 @@ dependencies = [ "inout", ] +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "lazy_static", + "nom", + "pathdiff", + "serde", + "toml 0.5.11", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -751,7 +765,7 @@ dependencies = [ "pear", "serde", "serde_yaml", - "toml", + "toml 0.7.6", "uncased", "version_check", ] @@ -1303,6 +1317,7 @@ dependencies = [ "anyhow", "biscuit", "chrono", + "config", "core-lib", "error-chain", "fern", @@ -1397,6 +1412,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1508,6 +1529,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1665,6 +1696,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -2674,6 +2711,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.7.6" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index ccd6294..0e9ebf9 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -20,7 +20,7 @@ rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0" serde_derive = "1.0" serde_json = "1.0" -anyhow = "1.0.73" +anyhow = "1" hex = "0.4.3" aes = "0.8.3" aes-gcm-siv = "0.11.1" @@ -28,3 +28,4 @@ hkdf = "0.12.3" sha2 = "0.10.7" generic-array = "0.14.7" openssl = "0.10.56" +config = { version = "0.13.3", default-features = false, features = ["toml"] } diff --git a/clearing-house-app/logging-service/config.toml b/clearing-house-app/logging-service/config.toml new file mode 100644 index 0000000..5e89afe --- /dev/null +++ b/clearing-house-app/logging-service/config.toml @@ -0,0 +1,4 @@ +document_database_url= "mongodb://localhost:27017" +process_database_url= "mongodb://localhost:27017" +keyring_database_url= "mongodb://localhost:27017" +clear_db = true \ No newline at end of file diff --git a/clearing-house-app/logging-service/init_db/default_doc_type.json b/clearing-house-app/logging-service/init_db/default_doc_type.json new file mode 100644 index 0000000..5e1f843 --- /dev/null +++ b/clearing-house-app/logging-service/init_db/default_doc_type.json @@ -0,0 +1,42 @@ +{ + "id": "IDS_MESSAGE", + "pid": "default", + "parts": [ + { + "name": "model_version" + }, + { + "name": "correlation_message" + }, + { + "name": "transfer_contract" + }, + { + "name": "issued" + }, + { + "name": "issuer_connector" + }, + { + "name": "content_version" + }, + { + "name": "recipient_connector" + }, + { + "name": "sender_agent" + }, + { + "name": "recipient_agent" + }, + { + "name": "payload" + }, + { + "name": "payload_type" + }, + { + "name": "message_id" + } + ] +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/doc_store.rs b/clearing-house-app/logging-service/src/db/doc_store.rs index 7d39f21..7babbf0 100644 --- a/clearing-house-app/logging-service/src/db/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/doc_store.rs @@ -266,7 +266,7 @@ impl DataStore { mod bucket { use core_lib::model::document::EncryptedDocument; - #[derive(Clone, Debug, Serialize, Deserialize)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentBucket { pub counter: u64, pub pid: String, @@ -276,13 +276,13 @@ mod bucket { pub documents: Vec, } - #[derive(Clone, Debug, Serialize, Deserialize)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentBucketSize { pub capacity: i32, pub size: i32, } - #[derive(Debug, Clone, Serialize, Deserialize)] + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DocumentBucketUpdate { pub id: String, pub ts: i64, diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 207b36f..c3fe97e 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -1,19 +1,19 @@ #![forbid(unsafe_code)] -#[macro_use] extern crate rocket; -#[macro_use] extern crate serde_derive; +#[macro_use] +extern crate rocket; use std::path::Path; -use core_lib::util::{add_service_config, setup_logger}; -use rocket::{Build, Rocket}; +use core_lib::util::{add_service_config}; use rocket::fairing::AdHoc; use core_lib::constants::ENV_LOGGING_SERVICE_ID; - +use db::config::doc_store::DatastoreConfigurator; +use db::config::keyring_store::KeyringDbConfigurator; use db::config::process_store::ProcessStoreConfigurator; use model::constants::SIGNING_KEY; -pub mod db; -pub mod model; +mod db; +mod model; mod services; mod crypto; mod ports; @@ -21,25 +21,69 @@ mod ports; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { let private_key_path = rocket.figment().extract_inner(SIGNING_KEY).unwrap_or(String::from("keys/private_key.der")); - if Path::new(&private_key_path).exists(){ + if Path::new(&private_key_path).exists() { Ok(rocket.manage(private_key_path)) - } - else{ + } else { error!("Signing key not found! Aborting startup! Please configure signing_key!"); - return Err(rocket) + return Err(rocket); } }) } -#[launch] -fn rocket() -> Rocket { +#[derive(Debug, serde::Deserialize)] +struct CHConfig { + process_database_url: String, + keyring_database_url: String, + document_database_url: String, + clear_db: bool, +} + +#[rocket::main] +async fn main() -> Result<(), rocket::Error> { + // Read configuration + let conf = config::Config::builder() + .add_source(config::File::with_name("config.toml")) + .add_source(config::Environment::with_prefix("CH_APP_")) + .build() + .expect("Failure to read configuration! Exiting..."); + // setup logging - setup_logger().expect("Failure to set up the logger! Exiting..."); + // TODO: Setup tracing_subscriber - rocket::build() - .attach(ProcessStoreConfigurator) + let conf: CHConfig = conf.try_deserialize().expect("Failure to read configuration! Exiting..."); + println!("Config: {:?}", conf); + + + let process_store = + ProcessStoreConfigurator::init_process_store(String::from(conf.process_database_url), conf.clear_db) + .await + .expect("Failure to initialize process store! Exiting..."); + let keyring_store = + KeyringDbConfigurator::init_keystore(String::from(conf.keyring_database_url), conf.clear_db) + .await + .expect("Failure to initialize keyring store! Exiting..."); + let doc_store = + DatastoreConfigurator::init_datastore(String::from(conf.document_database_url), conf.clear_db) + .await + .expect("Failure to initialize document store! Exiting..."); + + let keyring_service = services::keyring_service::KeyringService::new(keyring_store); + let doc_service = services::document_service::DocumentService::new(doc_store, keyring_service.clone()); + let logging_service = services::logging_service::LoggingService::new( + process_store, + doc_service.clone(), + ); + + let _rocket = rocket::build() + .manage(keyring_service) + .manage(doc_service) + .manage(logging_service) .attach(add_signing_key()) .attach(add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) .attach(ports::logging_api::mount_api()) .attach(ports::doc_type_api::mount_api()) + .ignite().await? + .launch().await?; + + Ok(()) } diff --git a/clearing-house-app/logging-service/src/model/crypto.rs b/clearing-house-app/logging-service/src/model/crypto.rs index a700c76..f45b9a6 100644 --- a/clearing-house-app/logging-service/src/model/crypto.rs +++ b/clearing-house-app/logging-service/src/model/crypto.rs @@ -3,7 +3,7 @@ use hkdf::Hkdf; use sha2::Sha256; use core_lib::model::new_uuid; -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct MasterKey { pub id: String, pub key: String, diff --git a/clearing-house-app/logging-service/src/model/ids/message.rs b/clearing-house-app/logging-service/src/model/ids/message.rs index 0c47465..4f02b0a 100644 --- a/clearing-house-app/logging-service/src/model/ids/message.rs +++ b/clearing-house-app/logging-service/src/model/ids/message.rs @@ -20,7 +20,7 @@ pub const RESULT_MESSAGE: &'static str = "ResultMessage"; pub const REJECTION_MESSAGE: &'static str = "RejectionMessage"; pub const MESSAGE_PROC_NOTIFICATION_MESSAGE: &'static str = "MessageProcessedNotificationMessage"; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct IdsMessage { //IDS name #[serde(rename = "@context")] diff --git a/clearing-house-app/logging-service/src/model/ids/mod.rs b/clearing-house-app/logging-service/src/model/ids/mod.rs index e143889..fa211bb 100644 --- a/clearing-house-app/logging-service/src/model/ids/mod.rs +++ b/clearing-house-app/logging-service/src/model/ids/mod.rs @@ -6,7 +6,7 @@ use crate::model::ids::message::IdsMessage; pub mod message; pub mod request; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct InfoModelComplexId { //IDS name #[serde(rename = "@id", alias="id", skip_serializing_if = "Option::is_none")] @@ -36,7 +36,7 @@ impl From for InfoModelComplexId { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(untagged)] pub enum InfoModelId { SimpleId(String), @@ -67,7 +67,7 @@ impl From for InfoModelId { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(untagged)] pub enum InfoModelDateTime { ComplexTime(InfoModelTimeStamp), @@ -93,7 +93,7 @@ impl fmt::Display for InfoModelDateTime { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct InfoModelTimeStamp { //IDS name #[serde(rename = "@type", alias="type", skip_serializing_if = "Option::is_none")] @@ -123,7 +123,7 @@ impl Display for InfoModelTimeStamp { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub enum MessageType { #[serde(rename = "ids:Message")] Message, @@ -147,7 +147,7 @@ pub enum MessageType { Other, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SecurityToken { //IDS name #[serde(rename = "@type")] @@ -175,7 +175,7 @@ impl SecurityToken { } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct IdsQueryResult{ pub date_from: String, pub date_to: String, diff --git a/clearing-house-app/logging-service/src/model/ids/request.rs b/clearing-house-app/logging-service/src/model/ids/request.rs index e56067d..e332c2d 100644 --- a/clearing-house-app/logging-service/src/model/ids/request.rs +++ b/clearing-house-app/logging-service/src/model/ids/request.rs @@ -1,6 +1,6 @@ use crate::model::ids::message::IdsMessage; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ClearingHouseMessage { pub header: IdsMessage, pub payload: Option, diff --git a/clearing-house-app/logging-service/src/model/mod.rs b/clearing-house-app/logging-service/src/model/mod.rs index 53a8ef5..e510986 100644 --- a/clearing-house-app/logging-service/src/model/mod.rs +++ b/clearing-house-app/logging-service/src/model/mod.rs @@ -8,12 +8,12 @@ pub mod ids; pub(crate) mod crypto; pub(crate) mod doc_type; -#[derive(Serialize, Deserialize)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct TransactionCounter{ pub tc: i64 } -#[derive(Serialize, Deserialize)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct OwnerList{ pub owners: Vec } @@ -26,12 +26,12 @@ impl OwnerList{ } } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Receipt { pub data: Compact } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct DataTransaction { pub transaction_id: String, pub timestamp: i64, diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 8abda1c..317be49 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -9,12 +9,18 @@ use core_lib::model::crypto::{KeyCt, KeyCtList}; use crate::services::keyring_service::KeyringService; use crate::db::doc_store::DataStore; + +#[derive(Clone)] pub struct DocumentService { db: DataStore, key_api: KeyringService, } impl DocumentService { + pub fn new(db: DataStore, key_api: KeyringService) -> Self { + Self { db, key_api } + } + pub(crate) async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); // data validation diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index fd250e2..63f2ab1 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -6,11 +6,18 @@ use crate::crypto::restore_key_map; use crate::db::key_store::KeyStore; use crate::model::doc_type::DocumentType; +#[derive(Clone)] pub struct KeyringService { db: KeyStore, } impl KeyringService { + pub fn new(db: KeyStore) -> KeyringService { + KeyringService { + db + } + } + pub async fn generate_keys(&self, ch_claims: ChClaims, _pid: String, dt_id: String) -> anyhow::Result { trace!("generate_keys"); trace!("...user '{:?}'", &ch_claims.client_id); diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/logging-service/src/services/logging_service.rs index 1a75f0e..c81c01c 100644 --- a/clearing-house-app/logging-service/src/services/logging_service.rs +++ b/clearing-house-app/logging-service/src/services/logging_service.rs @@ -20,12 +20,20 @@ use crate::model::{ids::{ use crate::db::ProcessStore; use crate::services::document_service::DocumentService; +#[derive(Clone)] pub struct LoggingService { db: ProcessStore, doc_api: DocumentService, } impl LoggingService { + pub fn new(db: ProcessStore, doc_api: DocumentService) -> LoggingService { + LoggingService { + db, + doc_api, + } + } + pub async fn log( &self, ch_claims: ChClaims, diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs index cb72058..400f075 100644 --- a/clearing-house-app/logging-service/src/services/mod.rs +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -5,5 +5,5 @@ //! Controllers to handle the requests and responses. //! pub(crate) mod keyring_service; -mod document_service; +pub(crate) mod document_service; pub(crate) mod logging_service; \ No newline at end of file From 356665a46bd6de165b0fd227b845d10d6e1fcb0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 15:14:29 +0200 Subject: [PATCH 018/183] feat(ch-app): Setup tracing as logger and replace rocket as logger; setup config --- clearing-house-app/Cargo.lock | 2 + clearing-house-app/logging-service/Cargo.toml | 2 + .../logging-service/config.toml | 1 + .../logging-service/src/config.rs | 69 +++++++++++++++++++ .../logging-service/src/db/key_store.rs | 30 ++++---- .../logging-service/src/main.rs | 28 ++------ .../logging-service/src/ports/doc_type_api.rs | 16 ++--- .../logging-service/src/ports/logging_api.rs | 22 +++--- 8 files changed, 114 insertions(+), 56 deletions(-) create mode 100644 clearing-house-app/logging-service/src/config.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index e9395ea..afafe41 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -1333,6 +1333,8 @@ dependencies = [ "serde_derive", "serde_json", "sha2 0.10.7", + "tracing", + "tracing-subscriber", ] [[package]] diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 0e9ebf9..64733e4 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -29,3 +29,5 @@ sha2 = "0.10.7" generic-array = "0.14.7" openssl = "0.10.56" config = { version = "0.13.3", default-features = false, features = ["toml"] } +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/clearing-house-app/logging-service/config.toml b/clearing-house-app/logging-service/config.toml index 5e89afe..fe0d348 100644 --- a/clearing-house-app/logging-service/config.toml +++ b/clearing-house-app/logging-service/config.toml @@ -1,3 +1,4 @@ +log_level = "DEBUG" # TRACE, DEBUG, INFO, WARN, ERROR document_database_url= "mongodb://localhost:27017" process_database_url= "mongodb://localhost:27017" keyring_database_url= "mongodb://localhost:27017" diff --git a/clearing-house-app/logging-service/src/config.rs b/clearing-house-app/logging-service/src/config.rs new file mode 100644 index 0000000..6657a21 --- /dev/null +++ b/clearing-house-app/logging-service/src/config.rs @@ -0,0 +1,69 @@ +#[derive(Debug, serde::Deserialize)] +pub(crate) struct CHConfig { + pub(crate) process_database_url: String, + pub(crate) keyring_database_url: String, + pub(crate) document_database_url: String, + pub(crate) clear_db: bool, + #[serde(default)] + pub(crate) log_level: Option, +} + +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub(crate) enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +impl Into for LogLevel { + fn into(self) -> tracing::Level { + match self { + LogLevel::Trace => tracing::Level::TRACE, + LogLevel::Debug => tracing::Level::DEBUG, + LogLevel::Info => tracing::Level::INFO, + LogLevel::Warn => tracing::Level::WARN, + LogLevel::Error => tracing::Level::ERROR, + } + } +} + +impl ToString for LogLevel { + fn to_string(&self) -> String { + match self { + LogLevel::Trace => String::from("TRACE"), + LogLevel::Debug => String::from("DEBUG"), + LogLevel::Info => String::from("INFO"), + LogLevel::Warn => String::from("WARN"), + LogLevel::Error => String::from("ERROR"), + } + } +} + +pub(crate) fn read_config() -> CHConfig { + let conf = config::Config::builder() + .add_source(config::File::with_name("config.toml")) + .add_source(config::Environment::with_prefix("CH_APP_")) + .build() + .expect("Failure to read configuration! Exiting..."); + + let conf: CHConfig = conf.try_deserialize().expect("Failure to read configuration! Exiting..."); + tracing::trace!(config = ?conf, "Config read"); + + conf +} + +pub(crate) fn configure_logging(log_level: Option) { + if std::env::var("RUST_LOG").is_err() { + if let Some(level) = log_level { + std::env::set_var("RUST_LOG", level.to_string()); + } + } + + // setup logging + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/key_store.rs b/clearing-house-app/logging-service/src/db/key_store.rs index 231f635..1abbbb8 100644 --- a/clearing-house-app/logging-service/src/db/key_store.rs +++ b/clearing-house-app/logging-service/src/db/key_store.rs @@ -27,19 +27,19 @@ impl KeyStore { /// Only one master key may exist in the database. pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result{ - debug!("Storing new master key..."); + tracing::debug!("Storing new master key..."); let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); - debug!("... but first check if there's already one."); + tracing::debug!("... but first check if there's already one."); let result= coll.find(None, None).await .expect("Error retrieving the master keys") .try_collect().await.unwrap_or_else(|_| vec![]); if result.len() > 1{ - error!("Master Key table corrupted!"); + tracing::error!("Master Key table corrupted!"); exit(1); } if result.len() == 1{ - error!("Master key already exists!"); + tracing::error!("Master key already exists!"); Ok(false) } else{ @@ -50,7 +50,7 @@ impl KeyStore { Ok(true) }, Err(e) => { - error!("master key could not be stored: {:?}", &e); + tracing::error!("master key could not be stored: {:?}", &e); panic!("master key could not be stored") } } @@ -65,14 +65,14 @@ impl KeyStore { .try_collect().await.unwrap_or_else(|_| vec![]); if result.len() > 1{ - error!("Master Key table corrupted!"); + tracing::error!("Master Key table corrupted!"); exit(1); } if result.len() == 1{ Ok(result[0].clone()) } else { - error!("Master Key missing!"); + tracing::error!("Master Key missing!"); exit(1); } } @@ -82,11 +82,11 @@ impl KeyStore { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); match coll.insert_one(doc_type.clone(), None).await { Ok(_r) => { - debug!("added new document type: {}", &_r.inserted_id); + tracing::debug!("added new document type: {}", &_r.inserted_id); Ok(()) }, Err(e) => { - error!("failed to log document type {}", &doc_type.id); + tracing::error!("failed to log document type {}", &doc_type.id); Err(Error::from(e)) } } @@ -111,7 +111,7 @@ impl KeyStore { match result { Some(_r) => Ok(true), None => { - debug!("document type with id {} and pid {:?} does not exist!", &dt_id, &pid); + tracing::debug!("document type with id {} and pid {:?} does not exist!", &dt_id, &pid); Ok(false) } } @@ -126,11 +126,11 @@ impl KeyStore { pub async fn get_document_type(&self, dt_id: &String) -> Result> { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - debug!("get_document_type for dt_id: '{}'", dt_id); + tracing::debug!("get_document_type for dt_id: '{}'", dt_id); match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await{ Ok(result) => Ok(result), Err(e) => { - error!("error while getting document type with id {}!", dt_id); + tracing::error!("error while getting document type with id {}!", dt_id); Err(Error::from(e)) } } @@ -141,15 +141,15 @@ impl KeyStore { match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await{ Ok(r) => { if r.matched_count != 1 || r.modified_count != 1{ - warn!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + tracing::warn!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); } else{ - debug!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + tracing::debug!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); } Ok(true) }, Err(e) => { - error!("error while updating document type with id {}: {:#?}", id, e); + tracing::error!("error while updating document type with id {}: {:#?}", id, e); Ok(false) } } diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index c3fe97e..4a3a356 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -1,11 +1,12 @@ #![forbid(unsafe_code)] #[macro_use] -extern crate rocket; +extern crate tracing; use std::path::Path; use core_lib::util::{add_service_config}; use rocket::fairing::AdHoc; +use tracing::subscriber; use core_lib::constants::ENV_LOGGING_SERVICE_ID; use db::config::doc_store::DatastoreConfigurator; use db::config::keyring_store::KeyringDbConfigurator; @@ -17,6 +18,7 @@ mod model; mod services; mod crypto; mod ports; +mod config; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { @@ -24,35 +26,17 @@ pub fn add_signing_key() -> AdHoc { if Path::new(&private_key_path).exists() { Ok(rocket.manage(private_key_path)) } else { - error!("Signing key not found! Aborting startup! Please configure signing_key!"); + tracing::error!("Signing key not found! Aborting startup! Please configure signing_key!"); return Err(rocket); } }) } -#[derive(Debug, serde::Deserialize)] -struct CHConfig { - process_database_url: String, - keyring_database_url: String, - document_database_url: String, - clear_db: bool, -} - #[rocket::main] async fn main() -> Result<(), rocket::Error> { // Read configuration - let conf = config::Config::builder() - .add_source(config::File::with_name("config.toml")) - .add_source(config::Environment::with_prefix("CH_APP_")) - .build() - .expect("Failure to read configuration! Exiting..."); - - // setup logging - // TODO: Setup tracing_subscriber - - let conf: CHConfig = conf.try_deserialize().expect("Failure to read configuration! Exiting..."); - println!("Config: {:?}", conf); - + let conf = config::read_config(); + config::configure_logging(conf.log_level); let process_store = ProcessStoreConfigurator::init_process_store(String::from(conf.process_database_url), conf.clear_db) diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs index 4f4d5bb..9b0bcb8 100644 --- a/clearing-house-app/logging-service/src/ports/doc_type_api.rs +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -7,7 +7,7 @@ use rocket::serde::json::{json,Json}; use crate::services::keyring_service::KeyringService; use crate::model::doc_type::DocumentType; -#[post("/", format = "json", data = "")] +#[rocket::post("/", format = "json", data = "")] async fn create_doc_type(key_api: &State, doc_type: Json) -> ApiResponse { match key_api.inner().create_doc_type(doc_type.into_inner()).await{ Ok(dt) => ApiResponse::SuccessCreate(json!(dt)), @@ -18,7 +18,7 @@ async fn create_doc_type(key_api: &State, doc_type: Json", format = "json", data = "")] +#[rocket::post("/", format = "json", data = "")] async fn update_doc_type(key_api: &State, id: String, doc_type: Json) -> ApiResponse { match key_api.inner().update_doc_type(id, doc_type.into_inner()).await{ Ok(id) => ApiResponse::SuccessOk(json!(id)), @@ -29,12 +29,12 @@ async fn update_doc_type(key_api: &State, id: String, doc_type: } } -#[delete("/", format = "json")] +#[rocket::delete("/", format = "json")] async fn delete_default_doc_type(key_api: &State, id: String) -> ApiResponse{ delete_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } -#[delete("//", format = "json")] +#[rocket::delete("//", format = "json")] async fn delete_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse{ match key_api.inner().delete_doc_type(id, pid).await{ Ok(id) => ApiResponse::SuccessOk(json!(id)), @@ -45,12 +45,12 @@ async fn delete_doc_type(key_api: &State, id: String, pid: Strin } } -#[get("/", format = "json")] +#[rocket::get("/", format = "json")] async fn get_default_doc_type(key_api: &State, id: String) -> ApiResponse { get_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } -#[get("//", format = "json")] +#[rocket::get("//", format = "json")] async fn get_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse { match key_api.inner().get_doc_type(id, pid).await{ Ok(dt) => { @@ -66,7 +66,7 @@ async fn get_doc_type(key_api: &State, id: String, pid: String) } } -#[get("/", format = "json")] +#[rocket::get("/", format = "json")] async fn get_doc_types(key_api: &State) -> ApiResponse { match key_api.inner().get_doc_types().await{ Ok(dt) => ApiResponse::SuccessOk(json!(dt)), @@ -80,7 +80,7 @@ async fn get_doc_types(key_api: &State) -> ApiResponse { pub fn mount_api() -> AdHoc { AdHoc::on_ignite("Mounting Document Type API", |rocket| async { rocket - .mount(ROCKET_DOC_TYPE_API, routes![create_doc_type, + .mount(ROCKET_DOC_TYPE_API, rocket::routes![create_doc_type, update_doc_type, delete_default_doc_type, delete_doc_type, get_default_doc_type, get_doc_type , get_doc_types]) }) diff --git a/clearing-house-app/logging-service/src/ports/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs index 8ac0bac..8151caa 100644 --- a/clearing-house-app/logging-service/src/ports/logging_api.rs +++ b/clearing-house-app/logging-service/src/ports/logging_api.rs @@ -13,7 +13,7 @@ use crate::model::ids::request::ClearingHouseMessage; use crate::model::constants::{ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_QUERY_API, ROCKET_PROCESS_API, ROCKET_PK_API}; use crate::services::logging_service::LoggingService; -#[post("/", format = "json", data = "")] +#[rocket::post("/", format = "json", data = "")] async fn log( ch_claims: ChClaims, logging_api: &State, @@ -30,7 +30,7 @@ async fn log( } } -#[post("/", format = "json", data = "")] +#[rocket::post("/", format = "json", data = "")] async fn create_process( ch_claims: ChClaims, logging_api: &State, @@ -46,17 +46,17 @@ async fn create_process( } } -#[post("/<_pid>", format = "json", rank = 50)] +#[rocket::post("/<_pid>", format = "json", rank = 50)] async fn unauth(_pid: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/<_pid>/<_id>", format = "json", rank = 50)] +#[rocket::post("/<_pid>/<_id>", format = "json", rank = 50)] async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/?&&&&", format = "json", data = "")] +#[rocket::post("/?&&&&", format = "json", data = "")] async fn query_pid( ch_claims: ChClaims, logging_api: &State, @@ -77,7 +77,7 @@ async fn query_pid( } } -#[post("//", format = "json", data = "")] +#[rocket::post("//", format = "json", data = "")] async fn query_id( ch_claims: ChClaims, logging_api: &State, @@ -94,7 +94,7 @@ async fn query_id( } } -#[get("/.well-known/jwks.json", format = "json")] +#[rocket::get("/.well-known/jwks.json", format = "json")] async fn get_public_sign_key(key_path: &State) -> ApiResponse { match get_jwks(key_path.as_str()) { Some(jwks) => ApiResponse::SuccessOk(json!(jwks)), @@ -105,10 +105,10 @@ async fn get_public_sign_key(key_path: &State) -> ApiResponse { pub fn mount_api() -> AdHoc { AdHoc::on_ignite("Mounting Clearing House API", |rocket| async { rocket - .mount(format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API).as_str(), routes![log, unauth]) - .mount(format!("{}", ROCKET_PROCESS_API).as_str(), routes![create_process, unauth]) + .mount(format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API).as_str(), rocket::routes![log, unauth]) + .mount(format!("{}", ROCKET_PROCESS_API).as_str(), rocket::routes![create_process, unauth]) .mount(format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_QUERY_API).as_str(), - routes![query_id, query_pid, unauth, unauth_id]) - .mount(format!("{}", ROCKET_PK_API).as_str(), routes![get_public_sign_key]) + rocket::routes![query_id, query_pid, unauth, unauth_id]) + .mount(format!("{}", ROCKET_PK_API).as_str(), rocket::routes![get_public_sign_key]) }) } \ No newline at end of file From 98f1448795003bf6fc823fccda7f0e14fe8b7cb0 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 26 Jul 2023 11:46:33 +0200 Subject: [PATCH 019/183] feat: release action --- .github/workflows/release.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..950d799 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,26 @@ +name: Release +on: + push: + branches: + - master + - alpha + - beta +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release From a2dd150f9770e4ed44877eef238fb636c0cbb6f4 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 26 Jul 2023 13:04:49 +0200 Subject: [PATCH 020/183] chore: pr and issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 39 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 2 ++ .github/ISSUE_TEMPLATE/feature_request.md | 19 +++++++++++ .github/PULL_REQUEST_TEMPLATE.md | 15 +++++++++ 4 files changed, 75 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..0f38032 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +# Bug Report + +## Describe the Bug +_A clear and concise description of the bug._ + +### Expected Behavior +_A clear and concise description of what you expected to happen._ + +### Observed Behavior +_A clear and concise description of what happened instead._ + +## Steps to Reproduce +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +## Context Information +_Add any other context about the problem here._ + +- Used version [e.g. EDC v1.0.0] +- OS: [e.g. iOS, Windows] +- ... + +## Detailed Description +_If applicable, add screenshots and logs to help explain your problem._ + +## Possible Implementation +_You already know the root cause of the erroneous state and how to fix it? Feel free to share your thoughts._ diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..bd9dfe4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +--- +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..292266b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature Request +about: Help us with new ideas +title: '' +labels: '' +assignees: '' + +--- + +# Feature Request + +## Which Areas Would Be Affected? +_e.g., DPF, CI, build, transfer, etc._ + +## Why Is the Feature Desired? +_Are there any requirements?_ + +## Solution Proposal +_If possible, provide a (brief!) solution proposal._ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..55a8bd9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +## What this PR changes/adds + +_Briefly describe WHAT your pr changes, which features it adds/modifies._ + +## Why it does that + +_Briefly state why the change was necessary._ + +## Further notes + +_List other areas of code that have changed but are not necessarily linked to the main feature. This could be method signature changes, package declarations, bugs that were encountered and were fixed inline, etc._ + +## Linked Issue(s) + +Closes # <-- _insert Issue number if one exists_ \ No newline at end of file From ecd3078b92d8061588f58537133c5b56074b91f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 10 Aug 2023 19:58:35 +0200 Subject: [PATCH 021/183] fix(core): Disable integration tests, fix warnings and make the build reproducible --- .gitignore | 2 +- clearing-house-app/Cargo.lock | 3164 +++++++++++++++++ clearing-house-app/core-lib/Cargo.toml | 2 +- clearing-house-app/core-lib/src/model/mod.rs | 59 +- .../tests/integration/daps_api_client.rs | 6 +- .../tests/integration/document_api_client.rs | 6 +- .../tests/integration/keyring_api_client.rs | 6 +- .../core-lib/tests/integration/main.rs | 7 +- .../tests/integration/token_validation.rs | 3 + clearing-house-app/document-api/src/db/mod.rs | 8 +- .../document-api/src/db/tests.rs | 6 +- .../document-api/src/doc_api.rs | 2 +- clearing-house-app/keyring-api/src/db/mod.rs | 3 +- .../logging-service/src/main.rs | 1 - .../logging-service/src/model/ids/mod.rs | 12 +- 15 files changed, 3235 insertions(+), 52 deletions(-) create mode 100644 clearing-house-app/Cargo.lock diff --git a/.gitignore b/.gitignore index 9d70ebc..87d2bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ data .idea/ **/*.lock **/*.iml -node_modules/ \ No newline at end of file +node_modules/ diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock new file mode 100644 index 0000000..3382892 --- /dev/null +++ b/clearing-house-app/Cargo.lock @@ -0,0 +1,3164 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" +dependencies = [ + "aes-soft", + "aesni", + "cipher", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202a43562bc3e159554b7590f5fd1f432d9e8de0cc2c2ce4bb8d194a34b3b0f3" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "aes-soft" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "aesni" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" +dependencies = [ + "cipher", + "opaque-debug", +] + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "async-trait" +version = "0.1.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "binascii" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" + +[[package]] +name = "biscuit" +version = "0.6.0" +source = "git+https://github.com/lawliet89/biscuit?branch=master#16d5c91c0576ec40ec655b7f107b4df19fe4186f" +dependencies = [ + "chrono", + "data-encoding", + "num-bigint", + "num-traits", + "once_cell", + "ring", + "serde", + "serde_json", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2-rfc" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +dependencies = [ + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bson" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aeb8bae494e49dbc330dd23cf78f6f7accee22f640ce3ab17841badaa4ce232" +dependencies = [ + "ahash", + "base64 0.13.1", + "bitvec", + "hex", + "indexmap 1.9.3", + "js-sys", + "lazy_static", + "rand", + "serde", + "serde_bytes", + "serde_json", + "time 0.3.23", + "uuid 1.4.1", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "time 0.1.45", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" +dependencies = [ + "generic-array", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time 0.3.23", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "core-lib" +version = "0.10.0" +dependencies = [ + "aes", + "aes-gcm-siv", + "base64 0.9.3", + "biscuit", + "blake2-rfc", + "chrono", + "error-chain", + "fern", + "figment", + "generic-array", + "hex", + "log", + "mongodb", + "num-bigint", + "openssh-keys", + "percent-encoding", + "reqwest", + "ring", + "rocket", + "serde", + "serde_derive", + "serde_json", + "uuid 0.8.2", +] + +[[package]] +name = "cpufeatures" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +dependencies = [ + "libc", +] + +[[package]] +name = "cpuid-bool" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "data-encoding" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn 1.0.109", +] + +[[package]] +name = "devise" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" +dependencies = [ + "devise_core", + "quote", +] + +[[package]] +name = "devise_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" +dependencies = [ + "bitflags 2.3.3", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "document-api" +version = "0.10.0" +dependencies = [ + "biscuit", + "chrono", + "core-lib", + "error-chain", + "fern", + "futures", + "hex", + "log", + "mongodb", + "rocket", + "rocket_cors", + "serde", + "serde_derive", + "serde_json", + "tokio", + "tokio-test", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "backtrace", + "version_check", +] + +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + +[[package]] +name = "fern" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e69ab0d5aca163e388c3a49d284fed6c3d0810700e77c5ae2756a50ec1a4daaa" +dependencies = [ + "chrono", + "log", +] + +[[package]] +name = "figment" +version = "0.10.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" +dependencies = [ + "atomic", + "pear", + "serde", + "serde_yaml", + "toml", + "uncased", + "version_check", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-executor" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "futures-sink" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" + +[[package]] +name = "futures-task" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" + +[[package]] +name = "futures-util" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" +dependencies = [ + "digest 0.9.0", + "hmac 0.10.1", +] + +[[package]] +name = "hmac" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.9", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.3", + "widestring", + "windows-sys", + "winreg 0.50.0", +] + +[[package]] +name = "ipnet" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "keyring-api" +version = "0.10.0" +dependencies = [ + "aes", + "aes-gcm-siv", + "base64 0.9.3", + "biscuit", + "chrono", + "core-lib", + "error-chain", + "fern", + "generic-array", + "hex", + "hkdf", + "log", + "mongodb", + "openssl", + "rocket", + "serde", + "serde_derive", + "serde_json", + "sha2 0.9.9", + "tokio", + "tokio-test", + "yaml-rust", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "logging-service" +version = "0.10.0" +dependencies = [ + "biscuit", + "chrono", + "core-lib", + "error-chain", + "fern", + "log", + "mongodb", + "percent-encoding", + "rocket", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md-5" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "md-5" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "mongodb" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcd85ec209a5b84fd9f54b9e381f6fa17462bc74160d018fc94fd8b9f61faa8" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bitflags 1.3.2", + "bson", + "chrono", + "derivative", + "derive_more", + "futures-core", + "futures-executor", + "futures-io", + "futures-util", + "hex", + "hmac 0.12.1", + "lazy_static", + "md-5 0.10.5", + "pbkdf2", + "percent-encoding", + "rand", + "rustc_version_runtime", + "rustls", + "rustls-pemfile", + "serde", + "serde_bytes", + "serde_with", + "sha-1", + "sha2 0.10.7", + "socket2 0.4.9", + "stringprep", + "strsim", + "take_mut", + "thiserror", + "tokio", + "tokio-rustls", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", + "typed-builder", + "uuid 1.4.1", + "webpki-roots", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.8", + "tokio", + "tokio-util", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssh-keys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7249a699cdeea261ac73f1bf9350777cb867324f44373aafb5a287365bf1771" +dependencies = [ + "base64 0.13.1", + "byteorder", + "md-5 0.9.1", + "sha2 0.9.9", + "thiserror", +] + +[[package]] +name = "openssl" +version = "0.10.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pear" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi 1.0.0-rc", +] + +[[package]] +name = "pear_codegen" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "polyval" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" +dependencies = [ + "cpuid-bool", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", + "version_check", + "yansi 1.0.0-rc", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "ref-cast" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ef7e18e8841942ddb1cf845054f8008410030a3997875d9e49b7a363063df1" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfaf0c85b766276c797f3791f5bc6d5bd116b41d53049af2789666b0c0bc9fa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "regex" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.3.3", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.4", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" + +[[package]] +name = "reqwest" +version = "0.11.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +dependencies = [ + "base64 0.21.2", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.10.1", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rocket" +version = "0.5.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58734f7401ae5cfd129685b48f61182331745b357b96f2367f01aebaf1cc9cc9" +dependencies = [ + "async-stream", + "async-trait", + "atomic", + "binascii", + "bytes", + "either", + "figment", + "futures", + "indexmap 1.9.3", + "is-terminal", + "log", + "memchr", + "multer", + "num_cpus", + "parking_lot", + "pin-project-lite", + "rand", + "ref-cast", + "rocket_codegen", + "rocket_http", + "serde", + "serde_json", + "state", + "tempfile", + "time 0.3.23", + "tokio", + "tokio-stream", + "tokio-util", + "ubyte", + "version_check", + "yansi 0.5.1", +] + +[[package]] +name = "rocket_codegen" +version = "0.5.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7093353f14228c744982e409259fb54878ba9563d08214f2d880d59ff2fc508b" +dependencies = [ + "devise", + "glob", + "indexmap 1.9.3", + "proc-macro2", + "quote", + "rocket_http", + "syn 2.0.27", + "unicode-xid", +] + +[[package]] +name = "rocket_cors" +version = "0.6.0-alpha2" +source = "git+https://github.com/lawliet89/rocket_cors?branch=master#985098dd8f3b052716111eaa872d184cc21a1a68" +dependencies = [ + "http", + "log", + "regex", + "rocket", + "serde", + "serde_derive", + "unicase", + "unicase_serde", + "url", +] + +[[package]] +name = "rocket_http" +version = "0.5.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936012c99162a03a67f37f9836d5f938f662e26f2717809761a9ac46432090f4" +dependencies = [ + "cookie", + "either", + "futures", + "http", + "hyper", + "indexmap 1.9.3", + "log", + "memchr", + "pear", + "percent-encoding", + "pin-project-lite", + "ref-cast", + "serde", + "smallvec", + "stable-pattern", + "state", + "time 0.3.23", + "tokio", + "uncased", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.18", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31b7153270ebf48bf91c65ae5b0c00e749c4cfad505f66530ac74950249582f" +dependencies = [ + "rustc_version 0.2.3", + "semver 0.9.0", +] + +[[package]] +name = "rustix" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "serde_json" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +dependencies = [ + "indexmap 2.0.0", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.0.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable-pattern" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" +dependencies = [ + "memchr", +] + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "stringprep" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +dependencies = [ + "autocfg", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.4.9", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap 2.0.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "tracing-core" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trust-dns-proto" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "log", + "rand", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "typed-builder" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ubyte" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c81f0dae7d286ad0d9366d7679a77934cfc3cf3a8d67e82669794412b2368fe6" +dependencies = [ + "serde", +] + +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "serde", + "version_check", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicase_serde" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef53697679d874d69f3160af80bc28de12730a985d57bdf2b47456ccb8b11f1" +dependencies = [ + "serde", + "unicase", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +dependencies = [ + "form_urlencoded", + "idna 0.4.0", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "uuid" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" +dependencies = [ + "webpki", +] + +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "winnow" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "yansi" +version = "1.0.0-rc" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee746ad3851dd3bc40e4a028ab3b00b99278d929e48957bcb2d111874a7e43e" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/clearing-house-app/core-lib/Cargo.toml b/clearing-house-app/core-lib/Cargo.toml index e3977e6..140385c 100644 --- a/clearing-house-app/core-lib/Cargo.toml +++ b/clearing-house-app/core-lib/Cargo.toml @@ -25,7 +25,7 @@ mongodb ="2.3.0" num-bigint = "0.4.3" openssh-keys = "0.5.0" percent-encoding = "2.1.0" -reqwest = { version="0.11.11", features = ["default", "json"]} +reqwest = { version="0.11.11", features = ["default", "json", "blocking"]} ring = "0.16.20" rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0" diff --git a/clearing-house-app/core-lib/src/model/mod.rs b/clearing-house-app/core-lib/src/model/mod.rs index f6e2124..ab2124e 100644 --- a/clearing-house-app/core-lib/src/model/mod.rs +++ b/clearing-house-app/core-lib/src/model/mod.rs @@ -1,10 +1,9 @@ -use chrono::{Datelike, Duration, Local, NaiveDate, NaiveDateTime, NaiveTime}; - pub mod crypto; pub mod document; pub mod process; -#[cfg(test)] mod tests; +#[cfg(test)] +mod tests; pub fn new_uuid() -> String { use uuid::Uuid; @@ -12,34 +11,33 @@ pub fn new_uuid() -> String { } #[derive(Debug, Clone, Serialize, Deserialize, FromFormField)] -pub enum SortingOrder{ +pub enum SortingOrder { #[field(value = "asc")] #[serde(rename = "asc")] Ascending, #[field(value = "desc")] #[serde(rename = "desc")] - Descending + Descending, } -pub fn parse_date(date: Option, to_date: bool) -> Option{ +pub fn parse_date(date: Option, to_date: bool) -> Option { let time_format; - if to_date{ + if to_date { time_format = "23:59:59" - } - else{ + } else { time_format = "00:00:00" } - match date{ + match date { Some(d) => { debug!("Parsing date: {}", &d); - match NaiveDateTime::parse_from_str(format!("{} {}",&d, &time_format).as_str(), "%Y-%m-%d %H:%M:%S"){ + match chrono::NaiveDateTime::parse_from_str(format!("{} {}", &d, &time_format).as_str(), "%Y-%m-%d %H:%M:%S") { Ok(date) => { Some(date) } Err(e) => { error!("Error occurred: {:#?}", e); - return None + return None; } } } @@ -47,11 +45,11 @@ pub fn parse_date(date: Option, to_date: bool) -> Option{ } } -pub fn sanitize_dates(date_from: Option, date_to: Option) -> (NaiveDateTime, NaiveDateTime){ - let default_to_date = Local::now().naive_local(); - let d = NaiveDate::from_ymd(default_to_date.year(), default_to_date.month(), default_to_date.day()); - let t = NaiveTime::from_hms(0, 0, 0); - let default_from_date = NaiveDateTime::new(d,t) - Duration::weeks(2); +pub fn sanitize_dates(date_from: Option, date_to: Option) -> (chrono::NaiveDateTime, chrono::NaiveDateTime) { + let default_to_date = chrono::Local::now().naive_local(); + let default_from_date = default_to_date.date() + .and_hms_opt(0, 0, 0) + .expect("00:00:00 is a valid time") - chrono::Duration::weeks(2); println!("date_to: {:#?}", date_to); println!("date_from: {:#?}", date_from); @@ -59,26 +57,19 @@ pub fn sanitize_dates(date_from: Option, date_to: Option date_to - if date_from.is_some() && date_to.is_some(){ - return (date_from.unwrap(), date_to.unwrap()) + match (date_from, date_to) { + (Some(from), Some(to)) => (from, to), // validate already checked that date_from > date_to + (Some(from), None) => (from, default_to_date), // if to_date is missing, default to now + (None, Some(_to)) => todo!("Not defined yet; check"), + (None, None) => (default_from_date, default_to_date), // if both dates are none (case to_date is none and from_date is_some should be catched by validation); return dates for default duration (last 2 weeks) } - - // if to_date is missing, default to now - if date_from.is_some() && date_to.is_none(){ - return (date_from.unwrap(), default_to_date) - } - - // if both dates are none (case to_date is none and from_date is_some should be catched by validation) - // return dates for default duration (last 2 weeks) - return (default_from_date, default_to_date) } -pub fn validate_dates(date_from: Option, date_to: Option) -> bool{ - let date_now = Local::now().naive_local(); +pub fn validate_dates(date_from: Option, date_to: Option) -> bool { + let date_now = chrono::Local::now().naive_local(); debug!("... validating dates: now: {:#?} , from: {:#?} , to: {:#?}", &date_now, &date_from, &date_to); // date_from before now - if date_from.is_some() && date_from.as_ref().unwrap().clone() > date_now{ + if date_from.is_some() && date_from.as_ref().unwrap().clone() > date_now { debug!("oh no, date_from {:#?} is in the future! date_now is {:#?}", &date_from, &date_now); return false; } @@ -89,13 +80,13 @@ pub fn validate_dates(date_from: Option, date_to: Option= date_now{ + if date_to.is_some() && date_to.as_ref().unwrap().clone() >= date_now { debug!("oh no, date_to {:#?} is in the future! date_now is {:#?}", &date_to, &date_now); return false; } // date_from before date_to - if date_from.is_some() && date_to.is_some(){ + if date_from.is_some() && date_to.is_some() { if date_from.unwrap() > date_to.unwrap() { debug!("oh no, date_from {:#?} is before date_to {:#?}", &date_from, &date_to); return false; diff --git a/clearing-house-app/core-lib/tests/integration/daps_api_client.rs b/clearing-house-app/core-lib/tests/integration/daps_api_client.rs index f0fe59a..afe38bf 100644 --- a/clearing-house-app/core-lib/tests/integration/daps_api_client.rs +++ b/clearing-house-app/core-lib/tests/integration/daps_api_client.rs @@ -1,3 +1,5 @@ +/* TODO: Integration test currently not necessary + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // These tests are integration tests and need an up-and-running keyring-api // Use config.yml to configure the urls correctly. @@ -24,4 +26,6 @@ fn test_get_jwks() -> Result<()>{ assert_eq!(KeyType::RSA, jwk.algorithm.key_type()); assert_eq!(DAPS_KID, jwk.common.key_id.unwrap()); Ok(()) -} \ No newline at end of file +} + +*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/document_api_client.rs b/clearing-house-app/core-lib/tests/integration/document_api_client.rs index 69f0670..50dbe56 100644 --- a/clearing-house-app/core-lib/tests/integration/document_api_client.rs +++ b/clearing-house-app/core-lib/tests/integration/document_api_client.rs @@ -1,3 +1,5 @@ +/* TODO: Integration test currently not necessary + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // These tests are integration tests and need an up-and-running keyring-api and // document-api. Use config.yml to configure the urls correctly. @@ -221,4 +223,6 @@ fn test_create_document_url_encoded_id() -> Result<()>{ delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; Ok(()) -} \ No newline at end of file +} + +*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs b/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs index 53fc29d..26b35c6 100644 --- a/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs +++ b/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs @@ -1,3 +1,5 @@ +/* TODO: Integration test currently not necessary + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // These tests are integration tests and need an up-and-running keyring-api // Use config.yml to configure the urls correctly. @@ -84,4 +86,6 @@ fn test_decrypt_keys() -> Result<()> { delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; Ok(()) -} \ No newline at end of file +} + +*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/main.rs b/clearing-house-app/core-lib/tests/integration/main.rs index 69e3b9a..81e5e7d 100644 --- a/clearing-house-app/core-lib/tests/integration/main.rs +++ b/clearing-house-app/core-lib/tests/integration/main.rs @@ -1,4 +1,5 @@ -use reqwest::{Client, StatusCode}; +use reqwest::blocking::{Client}; +use reqwest::StatusCode; use reqwest::header::{CONTENT_TYPE, HeaderValue}; use core_lib::constants::ROCKET_DOC_TYPE_API; @@ -9,12 +10,14 @@ use core_lib::model::document::{Document, DocumentPart}; pub const TOKEN: &'static str = "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzUyNDEyNzgsImlhdCI6MTYzNTI0MTI3OCwianRpIjoiT0RBNE5EazRNemsxT0RZMU16TXlOamN4TlE9PSIsImV4cCI6MTYzNTI0NDg3OCwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpUUlVTVF9TRUNVUklUWV9QUk9GSUxFIiwicmVmZXJyaW5nQ29ubmVjdG9yIjoiaHR0cDovL2NvbnN1bWVyLWNvcmUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiYzE1ZTY1NTgwODhkYmZlZjIxNWE0M2QyNTA3YmJkMTI0ZjQ0ZmI4ZmFjZDU2MWMxNDU2MWEyYzFhNjY5ZDBlMCIsInN1YiI6IkE1OjBDOkE1OkYwOjg0OkQ5OjkwOkJCOkJDOkQ5OjU3OjNBOjA0OkM4OjdGOjkzOkVEOjk3OkEyOjUyOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.iemDKZXE_RXFKkffqpweTAXBb6YX0spU0b5Ez1ncQzEyDNkJ5UtsZkwZz8WqfWOdPqMA74ShzLMwfEtao3DoO4DfWrvXFAYh8Y6hHJjHO44kPm4rUdcymUsVLXxcWd8Jszi6HjRHLaJ1-466s1akDQ7yQB0l8g9PP7BOlYr2I00HZ_b5wQOWtwT2PQxeWjkBzTgP8iycF7kIT6jgTHYDkOAwIdiMgNH_dPaxOPfxupz5vJQPuC1o9-IAyXtk-yC9GNI18YtjYpqizB-Nm5QGlUSSYMrB7tUKEc46471QaC4tR_LkYDrGnDtJHrH_fq0eEe6wIKoUcdt_VnI9Km-Hpw"; pub const TEST_CONFIG: &'static str = "config.yml"; - +/* TODO: Disable all integration tests for now mod document_api_client; mod keyring_api_client; mod daps_api_client; mod token_validation; + */ + fn create_test_document(pid: &String, dt_id: &String, tc: i64) -> Document{ let p1 = DocumentPart::new(String::from("name"), Some(String::from("This is document part name."))); let p2 = DocumentPart::new(String::from("payload"), Some(String::from("This is document part payload."))); diff --git a/clearing-house-app/core-lib/tests/integration/token_validation.rs b/clearing-house-app/core-lib/tests/integration/token_validation.rs index 4f2d92a..305ab0f 100644 --- a/clearing-house-app/core-lib/tests/integration/token_validation.rs +++ b/clearing-house-app/core-lib/tests/integration/token_validation.rs @@ -1,3 +1,5 @@ +/* TODO: Integration test currently not necessary + use biscuit::jwa::SignatureAlgorithm; use biscuit::jwk::JWKSet; use biscuit::{CompactJson, Empty}; @@ -54,3 +56,4 @@ fn test_invalid_claims() -> Result<()>{ assert!(jwt.is_err(), "Token is valid. this should not happen, really!"); Ok(()) } +*/ \ No newline at end of file diff --git a/clearing-house-app/document-api/src/db/mod.rs b/clearing-house-app/document-api/src/db/mod.rs index e2afa40..5f682c7 100644 --- a/clearing-house-app/document-api/src/db/mod.rs +++ b/clearing-house-app/document-api/src/db/mod.rs @@ -14,7 +14,9 @@ use core_lib::model::SortingOrder; use crate::db::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; mod bucket; -#[cfg(test)] mod tests; + +// TODO: Disabled integration tests with database +// #[cfg(test)] mod tests; #[derive(Clone, Debug)] pub struct DatastoreConfigurator; @@ -145,10 +147,10 @@ impl DataStoreApi for DataStore { impl DataStore { - pub async fn add_document(&self, doc: &EncryptedDocument) -> Result{ + pub async fn add_document(&self, doc: EncryptedDocument) -> Result{ debug!("add_document to bucket"); let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - let bucket_update = DocumentBucketUpdate::from(doc); + let bucket_update = DocumentBucketUpdate::from(&doc); let mut update_options = UpdateOptions::default(); update_options.upsert = Some(true); let id = format!("^{}_", doc.pid.clone()); diff --git a/clearing-house-app/document-api/src/db/tests.rs b/clearing-house-app/document-api/src/db/tests.rs index 4fa5b7e..c7bc226 100644 --- a/clearing-house-app/document-api/src/db/tests.rs +++ b/clearing-house-app/document-api/src/db/tests.rs @@ -95,7 +95,7 @@ async fn test_delete_document_doc_is_deleted() -> Result<()>{ assert_eq!(db.exists_document(&id).await?, true); // run the test - assert!(db.delete_document(&id).await?); + //assert!(db.delete_document(&id).await?); // db should not find document anymore assert_eq!(db.exists_document(&id).await?, false); @@ -127,7 +127,7 @@ async fn test_delete_document_check_others() -> Result<()>{ assert_eq!(db.exists_document(&id2).await?, true); // run the test - assert!(db.delete_document(&id1).await?); + //assert!(db.delete_document(&id1).await?); // db should still find the other document assert_eq!(db.exists_document(&id2).await?, true); @@ -153,7 +153,7 @@ async fn test_delete_document_on_not_existing_doc() -> Result<()>{ db.add_document(doc.clone()).await?; // run the test - assert_eq!(db.delete_document(&id2).await?, false); + // assert_eq!(db.delete_document(&id2).await?, false); // clean up tear_down(db).await; diff --git a/clearing-house-app/document-api/src/doc_api.rs b/clearing-house-app/document-api/src/doc_api.rs index eb5edc9..ab182d0 100644 --- a/clearing-house-app/document-api/src/doc_api.rs +++ b/clearing-house-app/document-api/src/doc_api.rs @@ -108,7 +108,7 @@ async fn create_enc_document( debug!("storing document ...."); // store document - match db.add_document(&enc_doc).await { + match db.add_document(enc_doc).await { Ok(_b) => ApiResponse::SuccessCreate(json!(receipt)), Err(e) => { error!("Error while adding: {:?}", e); diff --git a/clearing-house-app/keyring-api/src/db/mod.rs b/clearing-house-app/keyring-api/src/db/mod.rs index 9145161..002d67b 100644 --- a/clearing-house-app/keyring-api/src/db/mod.rs +++ b/clearing-house-app/keyring-api/src/db/mod.rs @@ -13,7 +13,8 @@ use crate::model::doc_type::DocumentType; pub(crate) mod doc_type; -#[cfg(test)] mod tests; +// TODO: Disabled integration tests with database +// #[cfg(test)] mod tests; #[derive(Clone, Debug)] pub struct KeyStore { diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 6a953a5..7c5c713 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -1,7 +1,6 @@ #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; -use std::env; use std::path::Path; use core_lib::api::client::{ApiClientConfigurator, ApiClientEnum}; use core_lib::util::{add_service_config, setup_logger}; diff --git a/clearing-house-app/logging-service/src/model/ids/mod.rs b/clearing-house-app/logging-service/src/model/ids/mod.rs index 4edd625..e143889 100644 --- a/clearing-house-app/logging-service/src/model/ids/mod.rs +++ b/clearing-house-app/logging-service/src/model/ids/mod.rs @@ -187,10 +187,18 @@ pub struct IdsQueryResult{ impl IdsQueryResult{ pub fn new(date_from: i64, date_to: i64, page: Option, size: Option, order: String, documents: Vec) -> IdsQueryResult{ + let date_from = NaiveDateTime::from_timestamp_opt(date_from, 0) + .expect("Invalid date_from seconds") + .format("%Y-%m-%d %H:%M:%S") + .to_string(); + let date_to = NaiveDateTime::from_timestamp_opt(date_to, 0) + .expect("Invalid date_to seconds") + .format("%Y-%m-%d %H:%M:%S") + .to_string(); IdsQueryResult{ - date_from: NaiveDateTime::from_timestamp(date_from, 0).format("%Y-%m-%d %H:%M:%S").to_string(), - date_to: NaiveDateTime::from_timestamp(date_to, 0).format("%Y-%m-%d %H:%M:%S").to_string(), + date_from, + date_to, page: page.unwrap_or(-1), size: size.unwrap_or(-1), order, From ef8bf76e772b0b23076f6e5a633281ecc12a6e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 10 Aug 2023 20:28:56 +0200 Subject: [PATCH 022/183] fix(app): Fix warnings and build on development branch --- .github/workflows/rust.yml | 6 ++++-- clearing-house-app/core-lib/src/db/public_db.rs | 4 ++-- clearing-house-app/core-lib/tests/integration/main.rs | 4 ++-- clearing-house-app/logging-service/src/logging_api.rs | 8 ++++---- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index dac798e..839693d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -4,8 +4,10 @@ on: push: branches: - master + - development pull_request: - branches: [ master ] + branches: + - master env: CARGO_TERM_COLOR: always @@ -16,7 +18,7 @@ env: jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install minimal stable diff --git a/clearing-house-app/core-lib/src/db/public_db.rs b/clearing-house-app/core-lib/src/db/public_db.rs index 0babcf2..864ec45 100644 --- a/clearing-house-app/core-lib/src/db/public_db.rs +++ b/clearing-house-app/core-lib/src/db/public_db.rs @@ -1,5 +1,5 @@ -use crate::mongodb::{ - Bson, +use mongodb::{ + bson::Bson, db::ThreadedDatabase, doc, coll::options::FindOneAndUpdateOptions diff --git a/clearing-house-app/core-lib/tests/integration/main.rs b/clearing-house-app/core-lib/tests/integration/main.rs index 81e5e7d..799d36a 100644 --- a/clearing-house-app/core-lib/tests/integration/main.rs +++ b/clearing-house-app/core-lib/tests/integration/main.rs @@ -49,7 +49,7 @@ fn insert_test_doc_type_into_keyring(token: &String, pid: &String, dt_id: &Strin println!("json_data: {}", json_data); println!("calling {}", &dt_url); - let mut response = client + let response = client .post(dt_url.as_str()) .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) .bearer_auth(token) @@ -72,7 +72,7 @@ fn delete_test_doc_type_from_keyring(token: &String, pid: &String, dt_id: &Strin let dt_url = format!("http://localhost:8002{}/{}/{}", ROCKET_DOC_TYPE_API, pid, dt_id); println!("calling {}", &dt_url); - let mut response = client + let response = client .delete(dt_url.as_str()) .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) .bearer_auth(token) diff --git a/clearing-house-app/logging-service/src/logging_api.rs b/clearing-house-app/logging-service/src/logging_api.rs index 0704793..c5e5de5 100644 --- a/clearing-house-app/logging-service/src/logging_api.rs +++ b/clearing-house-app/logging-service/src/logging_api.rs @@ -243,7 +243,7 @@ async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/?&&&&", format = "json", data = "")] +#[post("/?&&&&", format = "json", data = "<_message>")] async fn query_pid( ch_claims: ChClaims, db: &State, @@ -254,7 +254,7 @@ async fn query_pid( date_from: Option, doc_api: &State, pid: String, - message: Json + _message: Json ) -> ApiResponse { debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); @@ -336,8 +336,8 @@ async fn query_pid( } } -#[post("//", format = "json", data = "")] -async fn query_id(ch_claims: ChClaims, db: &State, doc_api: &State, pid: String, id: String, message: Json) -> ApiResponse { +#[post("//", format = "json", data = "<_message>")] +async fn query_id(ch_claims: ChClaims, db: &State, doc_api: &State, pid: String, id: String, _message: Json) -> ApiResponse { trace!("...user '{:?}'", &ch_claims.client_id); let user = &ch_claims.client_id; From 32bfea389a3f0f43907f3c5e7afa66105f25cf60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 10 Aug 2023 20:33:07 +0200 Subject: [PATCH 023/183] fix(app): Fix build on development branch --- .github/workflows/rust.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 839693d..67f5334 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -29,11 +29,8 @@ jobs: # TODO: do not use caching for actual release builds, aka ones that start with v* - uses: Swatinem/rust-cache@v2 - name: Build clearing-house-api - run: | - cd clearing-house-app - eval "$(ssh-agent -s)" - ssh-add - <<< "${{ secrets.IDS_CLEARING_HOUSE_CORE_TOKEN }}" - cargo build --release + working-directory: ./clearing-house-app + run: cargo build --release - name: Build build images run: | From 57d4e02ebee80c04f359d577fd87af2a70e0b7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 10 Aug 2023 20:43:51 +0200 Subject: [PATCH 024/183] fix(ci): Fix unauthorized push --- .github/workflows/rust.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 67f5334..ab67af7 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,6 +14,7 @@ env: IMAGE_NAME_LS: ids-ch-logging-service IMAGE_NAME_DA: ids-ch-document-api IMAGE_NAME_KA: ids-ch-keyring-api + IMAGE_BASE: ghcr.io/truzzt/ids-basecamp-clearinghouse jobs: @@ -21,11 +22,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Install minimal stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true # TODO: do not use caching for actual release builds, aka ones that start with v* - uses: Swatinem/rust-cache@v2 - name: Build clearing-house-api @@ -43,9 +39,9 @@ jobs: - name: Push image run: | - IMAGE_ID_LS=ghcr.io/Fraunhofer-AISEC/$IMAGE_NAME_LS - IMAGE_ID_DA=ghcr.io/Fraunhofer-AISEC/$IMAGE_NAME_DA - IMAGE_ID_KA=ghcr.io/Fraunhofer-AISEC/$IMAGE_NAME_KA + IMAGE_ID_LS=$IMAGE_BASE/$IMAGE_NAME_LS + IMAGE_ID_DA=$IMAGE_BASE/$IMAGE_NAME_DA + IMAGE_ID_KA=$IMAGE_BASE/$IMAGE_NAME_KA # Change all uppercase to lowercase IMAGE_ID_LS=$(echo $IMAGE_ID_LS | tr '[A-Z]' '[a-z]') From 4bb512f68f1137a3c89cca7bbd4ee6055525b1ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 15 Aug 2023 13:43:19 +0200 Subject: [PATCH 025/183] feat(ch-app): Created services for Keyring- and Document-Service inside logging service and adjusted the handlers --- clearing-house-app/Cargo.lock | 148 ++++++-- clearing-house-app/logging-service/Cargo.toml | 10 +- .../logging-service/src/crypto.rs | 161 +++++++++ .../logging-service/src/db/docstore.rs | 322 ++++++++++++++++++ .../logging-service/src/db/keystore.rs | 146 ++++++++ .../logging-service/src/db/mod.rs | 3 + .../logging-service/src/logging_api.rs | 5 +- .../logging-service/src/main.rs | 2 + .../logging-service/src/model/crypto.rs | 29 ++ .../logging-service/src/model/doc_type.rs | 29 ++ .../logging-service/src/model/mod.rs | 2 + .../src/services/document_service.rs | 281 +++++++++++++++ .../src/services/keyring_service.rs | 169 +++++++++ .../logging-service/src/services/mod.rs | 2 + 14 files changed, 1285 insertions(+), 24 deletions(-) create mode 100644 clearing-house-app/logging-service/src/crypto.rs create mode 100644 clearing-house-app/logging-service/src/db/docstore.rs create mode 100644 clearing-house-app/logging-service/src/db/keystore.rs create mode 100644 clearing-house-app/logging-service/src/model/crypto.rs create mode 100644 clearing-house-app/logging-service/src/model/doc_type.rs create mode 100644 clearing-house-app/logging-service/src/services/document_service.rs create mode 100644 clearing-house-app/logging-service/src/services/keyring_service.rs create mode 100644 clearing-house-app/logging-service/src/services/mod.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 3382892..673600d 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -26,6 +26,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.6.0" @@ -34,7 +44,18 @@ checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" dependencies = [ "aes-soft", "aesni", - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures", ] [[package]] @@ -43,11 +64,26 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202a43562bc3e159554b7590f5fd1f432d9e8de0cc2c2ce4bb8d194a34b3b0f3" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "polyval", + "aead 0.3.2", + "aes 0.6.0", + "cipher 0.2.5", + "ctr 0.6.0", + "polyval 0.4.5", + "subtle", + "zeroize", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead 0.5.2", + "aes 0.8.3", + "cipher 0.4.4", + "ctr 0.9.2", + "polyval 0.6.1", "subtle", "zeroize", ] @@ -58,7 +94,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -68,7 +104,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" dependencies = [ - "cipher", + "cipher 0.2.5", "opaque-debug", ] @@ -107,6 +143,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f768393e7fabd388fe8409b13faa4d93ab0fef35db1508438dfdb066918bcf38" + [[package]] name = "arrayvec" version = "0.4.12" @@ -347,6 +389,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -390,8 +442,8 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" name = "core-lib" version = "0.10.0" dependencies = [ - "aes", - "aes-gcm-siv", + "aes 0.6.0", + "aes-gcm-siv 0.9.0", "base64 0.9.3", "biscuit", "blake2-rfc", @@ -437,6 +489,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] @@ -456,7 +509,16 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" dependencies = [ - "cipher", + "cipher 0.2.5", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", ] [[package]] @@ -924,6 +986,15 @@ dependencies = [ "hmac 0.10.1", ] +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.10.1" @@ -1102,6 +1173,15 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -1150,8 +1230,8 @@ dependencies = [ name = "keyring-api" version = "0.10.0" dependencies = [ - "aes", - "aes-gcm-siv", + "aes 0.6.0", + "aes-gcm-siv 0.9.0", "base64 0.9.3", "biscuit", "chrono", @@ -1160,7 +1240,7 @@ dependencies = [ "fern", "generic-array", "hex", - "hkdf", + "hkdf 0.10.0", "log", "mongodb", "openssl", @@ -1218,18 +1298,26 @@ checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" name = "logging-service" version = "0.10.0" dependencies = [ + "aes 0.8.3", + "aes-gcm-siv 0.11.1", + "anyhow", "biscuit", "chrono", "core-lib", "error-chain", "fern", + "generic-array", + "hex", + "hkdf 0.12.3", "log", "mongodb", + "openssl", "percent-encoding", "rocket", "serde", "serde_derive", "serde_json", + "sha2 0.10.7", ] [[package]] @@ -1506,9 +1594,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -1538,9 +1626,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -1641,7 +1729,19 @@ checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" dependencies = [ "cpuid-bool", "opaque-debug", - "universal-hash", + "universal-hash 0.4.1", +] + +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash 0.5.1", ] [[package]] @@ -2819,6 +2919,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.9" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 19b1333..afb441e 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -19,4 +19,12 @@ percent-encoding = "2.1.0" rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0" serde_derive = "1.0" -serde_json = "1.0" \ No newline at end of file +serde_json = "1.0" +anyhow = "1.0.73" +hex = "0.4.3" +aes = "0.8.3" +aes-gcm-siv = "0.11.1" +hkdf = "0.12.3" +sha2 = "0.10.7" +generic-array = "0.14.7" +openssl = "0.10.56" diff --git a/clearing-house-app/logging-service/src/crypto.rs b/clearing-house-app/logging-service/src/crypto.rs new file mode 100644 index 0000000..a361176 --- /dev/null +++ b/clearing-house-app/logging-service/src/crypto.rs @@ -0,0 +1,161 @@ +use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; +use aes_gcm_siv::aead::Aead; +use core_lib::model::crypto::{KeyEntry, KeyMap}; +use generic_array::GenericArray; +use hkdf::Hkdf; +use openssl::rand::rand_bytes; +use sha2::Sha256; +use std::collections::HashMap; +use anyhow::anyhow; +use crate::model::doc_type::DocumentType; +use crate::model::crypto::MasterKey; + +const EXP_KEY_SIZE: usize = 32; +const EXP_NONCE_SIZE: usize = 12; +const EXP_BUFF_SIZE: usize = 44; + +fn initialize_kdf() -> (String, Hkdf) { + let salt = generate_random_seed(); + let ikm = generate_random_seed(); + let (master_key, kdf) = Hkdf::::extract(Some(&salt), &ikm); + (hex::encode_upper(master_key), kdf) +} + +pub fn generate_random_seed() -> Vec{ + let mut buf = [0u8; 256]; + rand_bytes(&mut buf).unwrap(); // TODO: Replace with some other cryptographically secure random number generator + buf.to_vec() +} + +fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap{ + let mut key_map = HashMap::new(); + let mut okm = [0u8; EXP_BUFF_SIZE]; + let mut i = 0; + dt.parts.iter() + .for_each( |p| { + if kdf.expand(p.name.clone().as_bytes(), &mut okm).is_ok() { + let map_key = match enc{ + true => p.name.clone(), + false => i.to_string() + }; + key_map.insert(map_key, KeyEntry::new(i.to_string(), okm[..EXP_KEY_SIZE].to_vec(), okm[EXP_KEY_SIZE..].to_vec())); + } + i = i +1; + }); + key_map +} + +pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result{ + debug!("generating encryption key_map for doc type: '{}'", &dt.id); + let (secret, doc_kdf) = initialize_kdf(); + let key_map = derive_key_map(doc_kdf, dt, true); + + debug!("encrypting the key seed"); + let kdf = restore_kdf(&mkey.key)?; + let mut okm = [0u8; EXP_BUFF_SIZE]; + if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err(){ + return Err(anyhow!("Error while generating key")); + } + match encrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], secret){ + Ok(ct) => Ok(KeyMap::new(true, key_map, Some(ct))), + Err(e) => { + error!("Error while encrypting key seed: {:?}", e); + Err(anyhow!("Error while encrypting key seed!")) + } + } +} + +pub fn restore_key_map(mkey: MasterKey, dt: DocumentType, keys_ct: Vec) -> anyhow::Result{ + debug!("decrypting the key seed"); + let kdf = restore_kdf(&mkey.key)?; + let mut okm = [0u8; EXP_BUFF_SIZE]; + if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err(){ + return Err(anyhow!("Error while generating key")); + } + + match decrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], &keys_ct){ + Ok(key_seed) => { + // generate new random key map + restore_keys(&key_seed, dt) + } + Err(e) => { + error!("Error while decrypting key ciphertext: {}", e); + Err(anyhow!("Error while decrypting keys")) + } + } +} + +pub fn restore_keys(secret: &String, dt: DocumentType) -> anyhow::Result{ + debug!("restoring decryption key_map for doc type: '{}'", &dt.id); + let kdf = restore_kdf(secret)?; + let key_map = derive_key_map(kdf, dt, false); + + Ok(KeyMap::new(false, key_map, None)) +} + +fn restore_kdf(secret: &String) -> anyhow::Result>{ + debug!("restoring kdf from secret"); + let prk = match hex::decode(secret){ + Ok(key) => key, + Err(e) => { + error!("Error while decoding master key: {}", e); + return Err(anyhow!("Error while encrypting key seed!")); + } + }; + + match Hkdf::::from_prk(prk.as_slice()){ + Ok(kdf) => Ok(kdf), + Err(e) => { + error!("Error while instantiating hkdf: {}", e); + Err(anyhow!("Error while encrypting key seed!")) + } + } +} + +pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Result>{ + // check key size + if key.len() != EXP_KEY_SIZE { + error!("Given key has size {} but expected {} bytes", key.len(), EXP_KEY_SIZE); + Err(anyhow!("Incorrect key size")) + } + // check nonce size + else if nonce.len() != EXP_NONCE_SIZE { + error!("Given nonce has size {} but expected {} bytes", nonce.len(), EXP_NONCE_SIZE); + Err(anyhow!("Incorrect nonce size")) + } + else{ + let key = GenericArray::from_slice(key); + let nonce = GenericArray::from_slice(nonce); + let cipher = Aes256GcmSiv::new(key); + + match cipher.encrypt(nonce, secret.as_bytes()){ + Ok(ct) => { + Ok(ct) + } + Err(e) => Err(anyhow!("Error while encrypting {}", e)) + } + } +} + +pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result{ + debug!("key len = {}", key.len()); + debug!("ct len = {}", ct.len()); + let key = GenericArray::from_slice(key); + let nonce = GenericArray::from_slice(nonce); + let cipher = Aes256GcmSiv::new(key); + + debug!("key: {}", hex::encode_upper(key)); + debug!("nonce: {}", hex::encode_upper(nonce)); + + debug!("ct len = {}", ct.len()); + debug!("nonce len = {}", nonce.len()); + match cipher.decrypt(nonce, ct){ + Ok(pt) => { + let pt = String::from_utf8(pt)?; + Ok(pt) + }, + Err(e) => { + Err(anyhow!("Error while decrypting: {}", e)) + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/docstore.rs b/clearing-house-app/logging-service/src/db/docstore.rs new file mode 100644 index 0000000..4a22827 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/docstore.rs @@ -0,0 +1,322 @@ +use mongodb::bson; +use mongodb::bson::doc; +use mongodb::options::{AggregateOptions, UpdateOptions}; +use rocket::futures::StreamExt; +use core_lib::constants::{ + MAX_NUM_RESPONSE_ENTRIES, + MONGO_COLL_DOCUMENT_BUCKET, + MONGO_ID, + MONGO_PID, + MONGO_COUNTER, + MONGO_DOC_ARRAY, + MONGO_DT_ID, + MONGO_FROM_TS, + MONGO_TO_TS, + MONGO_TC, + MONGO_TS, +}; +use core_lib::model::document::EncryptedDocument; +use core_lib::errors::*; +use core_lib::model::SortingOrder; +use crate::db::docstore::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; + +#[derive(Clone)] +pub struct DataStore { + client: mongodb::Client, + database: mongodb::Database, +} + +impl DataStore { + pub async fn add_document(&self, doc: EncryptedDocument) -> Result { + debug!("add_document to bucket"); + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + let bucket_update = DocumentBucketUpdate::from(&doc); + let mut update_options = UpdateOptions::default(); + update_options.upsert = Some(true); + let id = format!("^{}_", doc.pid.clone()); + let re = mongodb::bson::Regex { + pattern: id, + options: String::new(), + }; + + let query = doc! {"_id": re, MONGO_PID: doc.pid.clone(), MONGO_COUNTER: mongodb::bson::bson!({"$lt": MAX_NUM_RESPONSE_ENTRIES as i64})}; + + match coll.update_one(query, + doc! { + "$push": { + MONGO_DOC_ARRAY: mongodb::bson::to_bson(&bucket_update).unwrap(), + }, + "$inc": {"counter": 1}, + "$setOnInsert": { "_id": format!("{}_{}", doc.pid.clone(), doc.ts), MONGO_DT_ID: doc.dt_id.clone(), MONGO_FROM_TS: doc.ts}, + "$set": {MONGO_TO_TS: doc.ts}, + }, update_options).await { + Ok(_r) => { + debug!("added new document: {:#?}", &_r.upserted_id); + Ok(true) + } + Err(e) => { + error!("failed to store document: {:#?}", &e); + Err(Error::from(e)) + } + } + } + + /// checks if the document exists + /// document ids are globally unique + pub async fn exists_document(&self, id: &String) -> Result { + debug!("Check if document with id '{}' exists...", id); + let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone()}; + + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + match coll.count_documents(Some(query), None).await? { + 0 => { + debug!("Document with id '{}' does not exist!", &id); + Ok(false) + } + _ => { + debug!("... found."); + Ok(true) + } + } + } + + /// gets the model from the db + pub async fn get_document(&self, id: &String, pid: &String) -> Result> { + debug!("Trying to get doc with id {}...", id); + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + + let pipeline = vec![doc! {"$match":{ + MONGO_PID: pid.clone(), + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone() + }}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, + doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, + doc! {"$match":{ MONGO_ID: id.clone()}}]; + + let mut results = coll.aggregate(pipeline, None).await?; + + if let Some(result) = results.next().await { + let doc: EncryptedDocument = bson::from_document(result?)?; + return Ok(Some(doc)); + } + + return Ok(None); + } + + /// gets documents for a single process from the db + pub async fn get_document_with_previous_tc(&self, tc: i64) -> Result> { + let previous_tc = tc - 1; + debug!("Trying to get document for tc {} ...", previous_tc); + if previous_tc < 0 { + info!("... not entry exists."); + Ok(None) + } else { + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + + let pipeline = vec![doc! {"$match":{ + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TC): previous_tc + }}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, + doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, + doc! {"$match":{ MONGO_TC: previous_tc}}]; + + let mut results = coll.aggregate(pipeline, None).await?; + + return if let Some(result) = results.next().await { + debug!("Found {:#?}", &result); + let doc: EncryptedDocument = bson::from_document(result?)?; + Ok(Some(doc)) + } else { + warn!("Document with tc {} not found!", previous_tc); + Ok(None) + }; + } + } + + /// gets a page of documents of a specific document type for a single process from the db defined by parameters page, size and sort + pub async fn get_documents_for_pid(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> Result> { + debug!("...trying to get page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); + + match self.get_start_bucket_size(dt_id, pid, page, size, sort, date_from, date_to).await { + Ok(bucket_size) => { + let offset = DataStore::get_offset(&bucket_size); + let start_bucket = DataStore::get_start_bucket(page, size, &bucket_size, offset); + trace!("...working with start_bucket {} and offset {} ...", start_bucket, offset); + let start_entry = DataStore::get_start_entry(page, size, start_bucket, &bucket_size, offset); + + trace!("...working with start_entry {} in start_bucket {} and offset {} ...", start_entry, start_bucket, offset); + + let skip_buckets = (start_bucket - 1) as i32; + let sort_order = match sort { + SortingOrder::Ascending => 1, + SortingOrder::Descending => -1, + }; + + let pipeline = vec![doc! {"$match":{ + MONGO_PID: pid.clone(), + MONGO_DT_ID: dt_id.clone(), + MONGO_FROM_TS: {"$lte": date_to.timestamp()}, + MONGO_TO_TS: {"$gte": date_from.timestamp()} + }}, + doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, + doc! {"$skip" : skip_buckets}, + // worst case: overlap between two buckets. + doc! {"$limit" : 2}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$replaceRoot": { "newRoot": "$documents"}}, + doc! {"$match":{ + MONGO_TS: {"$gte": date_from.timestamp(), "$lte": date_to.timestamp()} + }}, + doc! {"$sort" : {MONGO_TS: sort_order}}, + doc! {"$skip" : start_entry as i32}, + doc! { "$limit": size as i32}]; + + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + + let mut options = AggregateOptions::default(); + options.allow_disk_use = Some(true); + let mut results = coll.aggregate(pipeline, options).await?; + + let mut docs = vec!(); + while let Some(result) = results.next().await { + let doc: DocumentBucketUpdate = bson::from_document(result?)?; + docs.push(restore_from_bucket(pid, dt_id, doc)); + } + + return Ok(docs); + } + Err(e) => { + error!("Error while getting bucket offset!"); + Err(Error::from(e)) + } + } + } + + /// offset is necessary for duration queries. There, start_entries of bucket depend on timestamps which usually creates an offset in the bucket + async fn get_start_bucket_size(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> Result { + debug!("...trying to get the offset for page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); + let sort_order = match sort { + SortingOrder::Ascending => { + 1 + } + SortingOrder::Descending => { + -1 + } + }; + let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + + debug!("... match with pid: {}, dt_it: {}, to_ts <= {}, from_ts >= {} ...", pid, dt_id, date_from.timestamp(), date_to.timestamp()); + let pipeline = vec![doc! {"$match":{ + MONGO_PID: pid.clone(), + MONGO_DT_ID: dt_id.clone(), + MONGO_FROM_TS: {"$lte": date_to.timestamp()}, + MONGO_TO_TS: {"$gte": date_from.timestamp()} + }}, + // sorting according to sorting order, so we get either the start or end + doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, + doc! {"$limit" : 1}, + // count all relevant documents in the target bucket + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$match":{ + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TS): {"$lte": date_to.timestamp(), "$gte": date_from.timestamp()} + }}, + // modify result to return total number of docs in bucket and number of relevant docs in bucket + doc! { "$group": { "_id": {"total": "$counter"}, "size": { "$sum": 1 } } }, + doc! { "$project": {"_id":0, "capacity": "$_id.total", "size":true}}]; + + let mut options = AggregateOptions::default(); + options.allow_disk_use = Some(true); + let mut results = coll.aggregate(pipeline, options).await?; + let mut bucket_size = DocumentBucketSize { + capacity: MAX_NUM_RESPONSE_ENTRIES as i32, + size: 0, + }; + while let Some(result) = results.next().await { + debug!("... retrieved: {:#?}", &result); + let result_bucket: DocumentBucketSize = bson::from_document(result?)?; + bucket_size = result_bucket; + } + debug!("... sending offset: {:?}", bucket_size); + Ok(bucket_size) + } + + fn get_offset(bucket_size: &DocumentBucketSize) -> u64 { + return (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES; + } + + fn get_start_bucket(page: u64, size: u64, bucket_size: &DocumentBucketSize, offset: u64) -> u64 { + let docs_to_skip = (page - 1) * size + offset + MAX_NUM_RESPONSE_ENTRIES - bucket_size.capacity as u64; + return (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1; + } + + fn get_start_entry(page: u64, size: u64, start_bucket: u64, bucket_size: &DocumentBucketSize, offset: u64) -> u64 { + // docs to skip calculated by page * size + let docs_to_skip = (page - 1) * size + offset; + let mut start_entry = 0; + if start_bucket > 1 { + start_entry = docs_to_skip - bucket_size.capacity as u64; + if start_entry > 2 { + start_entry = start_entry - (start_bucket - 2) * MAX_NUM_RESPONSE_ENTRIES + } + } + return start_entry; + } +} + +mod bucket { + use core_lib::model::document::EncryptedDocument; + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct DocumentBucket { + pub counter: u64, + pub pid: String, + pub dt_id: String, + pub from_ts: i64, + pub to_ts: i64, + pub documents: Vec, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + pub struct DocumentBucketSize { + pub capacity: i32, + pub size: i32, + } + + #[derive(Debug, Clone, Serialize, Deserialize)] + pub struct DocumentBucketUpdate { + pub id: String, + pub ts: i64, + pub tc: i64, + pub hash: String, + pub keys_ct: String, + pub cts: Vec, + } + + impl From<&EncryptedDocument> for DocumentBucketUpdate { + fn from(doc: &EncryptedDocument) -> Self { + DocumentBucketUpdate { + id: doc.id.clone(), + ts: doc.ts, + tc: doc.tc, + hash: doc.hash.clone(), + keys_ct: doc.keys_ct.clone(), + cts: doc.cts.to_vec(), + } + } + } + + pub fn restore_from_bucket(pid: &String, dt_id: &String, bucket_update: DocumentBucketUpdate) -> EncryptedDocument { + EncryptedDocument { + id: bucket_update.id.clone(), + dt_id: dt_id.clone(), + pid: pid.clone(), + ts: bucket_update.ts, + tc: bucket_update.tc, + hash: bucket_update.hash.clone(), + keys_ct: bucket_update.keys_ct.clone(), + cts: bucket_update.cts.to_vec(), + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/keystore.rs b/clearing-house-app/logging-service/src/db/keystore.rs new file mode 100644 index 0000000..7705b01 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/keystore.rs @@ -0,0 +1,146 @@ +use std::process::exit; +use core_lib::errors::*; +use mongodb::bson::doc; +use rocket::futures::TryStreamExt; +use core_lib::constants::{MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; +use crate::model::crypto::MasterKey; +use crate::model::doc_type::DocumentType; + +#[derive(Clone, Debug)] +pub struct KeyStore { + client: mongodb::Client, + database: mongodb::Database +} + +impl KeyStore { + + /// Only one master key may exist in the database. + pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result{ + debug!("Storing new master key..."); + let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); + debug!("... but first check if there's already one."); + let result= coll.find(None, None).await + .expect("Error retrieving the master keys") + .try_collect().await.unwrap_or_else(|_| vec![]); + + if result.len() > 1{ + error!("Master Key table corrupted!"); + exit(1); + } + if result.len() == 1{ + error!("Master key already exists!"); + Ok(false) + } + else{ + //let db_key = bson::to_bson(&key) + // .expect("failed to serialize master key for database"); + match coll.insert_one(key, None).await{ + Ok(_r) => { + Ok(true) + }, + Err(e) => { + error!("master key could not be stored: {:?}", &e); + panic!("master key could not be stored") + } + } + } + } + + /// Only one master key may exist in the database. + pub async fn get_msk(&self) -> anyhow::Result { + let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); + let result= coll.find(None, None).await + .expect("Error retrieving the master keys") + .try_collect().await.unwrap_or_else(|_| vec![]); + + if result.len() > 1{ + error!("Master Key table corrupted!"); + exit(1); + } + if result.len() == 1{ + Ok(result[0].clone()) + } + else { + error!("Master Key missing!"); + exit(1); + } + } + + // DOCTYPE + pub async fn add_document_type(&self, doc_type: DocumentType) -> Result<()> { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + match coll.insert_one(doc_type.clone(), None).await { + Ok(_r) => { + debug!("added new document type: {}", &_r.inserted_id); + Ok(()) + }, + Err(e) => { + error!("failed to log document type {}", &doc_type.id); + Err(Error::from(e)) + } + } + } + + //TODO: Do we need to check that no documents of this type exist before we remove it from the db? + pub async fn delete_document_type(&self, id: &String, pid: &String) -> Result { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + let result = coll.delete_many(doc! { MONGO_ID: id, MONGO_PID: pid }, None).await?; + if result.deleted_count >= 1 { + Ok(true) + } else { + Ok(false) + } + } + + + /// checks if the model exits + pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> Result { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + let result = coll.find_one(Some(doc! { MONGO_ID: dt_id, MONGO_PID: pid }), None).await?; + match result { + Some(_r) => Ok(true), + None => { + debug!("document type with id {} and pid {:?} does not exist!", &dt_id, &pid); + Ok(false) + } + } + } + + pub async fn get_all_document_types(&self) -> Result> { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + let result = coll.find(None, None).await? + .try_collect().await.unwrap_or_else(|_| vec![]); + Ok(result) + } + + pub async fn get_document_type(&self, dt_id: &String) -> Result> { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + debug!("get_document_type for dt_id: '{}'", dt_id); + match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await{ + Ok(result) => Ok(result), + Err(e) => { + error!("error while getting document type with id {}!", dt_id); + Err(Error::from(e)) + } + } + } + + pub async fn update_document_type(&self, doc_type: DocumentType, id: &String) -> Result { + let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await{ + Ok(r) => { + if r.matched_count != 1 || r.modified_count != 1{ + warn!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + } + else{ + debug!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + } + Ok(true) + }, + Err(e) => { + error!("error while updating document type with id {}: {:#?}", id, e); + Ok(false) + } + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/logging-service/src/db/mod.rs index 468fa5b..3451022 100644 --- a/clearing-house-app/logging-service/src/db/mod.rs +++ b/clearing-house-app/logging-service/src/db/mod.rs @@ -1,3 +1,6 @@ +pub(crate) mod keystore; +pub(crate) mod docstore; + use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, DATABASE_URL, CLEAR_DB, PROCESS_DB, PROCESS_DB_CLIENT, MONGO_COLL_TRANSACTIONS, MONGO_TC}; use core_lib::db::{DataStoreApi, init_database_client}; use core_lib::errors::*; diff --git a/clearing-house-app/logging-service/src/logging_api.rs b/clearing-house-app/logging-service/src/logging_api.rs index c5e5de5..254b1f0 100644 --- a/clearing-house-app/logging-service/src/logging_api.rs +++ b/clearing-house-app/logging-service/src/logging_api.rs @@ -317,10 +317,7 @@ async fn query_pid( None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() }; - let sanitized_sort = match sort { - Some(s) => s, - None => Descending - }; + let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); match doc_api.get_documents(&user, &pid, sanitized_page, sanitized_size, sanitized_sort, date_from, date_to).await { Ok(r) => { diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 7c5c713..fdbbbcb 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -14,6 +14,8 @@ use model::constants::SIGNING_KEY; pub mod logging_api; pub mod db; pub mod model; +mod services; +mod crypto; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { diff --git a/clearing-house-app/logging-service/src/model/crypto.rs b/clearing-house-app/logging-service/src/model/crypto.rs new file mode 100644 index 0000000..a700c76 --- /dev/null +++ b/clearing-house-app/logging-service/src/model/crypto.rs @@ -0,0 +1,29 @@ +use crate::crypto::generate_random_seed; +use hkdf::Hkdf; +use sha2::Sha256; +use core_lib::model::new_uuid; + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct MasterKey { + pub id: String, + pub key: String, + pub salt: String +} + +impl MasterKey{ + pub fn new(id: String, key: String, salt: String)-> MasterKey{ + MasterKey{ + id, + key, + salt + } + } + + pub fn new_random() -> MasterKey{ + let key_salt = generate_random_seed(); + let ikm = generate_random_seed(); + let (master_key, _) = Hkdf::::extract(Some(&key_salt), &ikm); + + MasterKey::new(new_uuid(), hex::encode_upper(master_key), hex::encode_upper(generate_random_seed())) + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/model/doc_type.rs b/clearing-house-app/logging-service/src/model/doc_type.rs new file mode 100644 index 0000000..fe53c2a --- /dev/null +++ b/clearing-house-app/logging-service/src/model/doc_type.rs @@ -0,0 +1,29 @@ +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct DocumentType { + pub id: String, + pub pid: String, + pub parts: Vec, +} + +impl DocumentType { + pub fn new(id: String, pid: String, parts: Vec) -> DocumentType { + DocumentType{ + id, + pid, + parts, + } + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct DocumentTypePart { + pub name: String, +} + +impl DocumentTypePart { + pub fn new(name: String) -> DocumentTypePart{ + DocumentTypePart{ + name + } + } +} diff --git a/clearing-house-app/logging-service/src/model/mod.rs b/clearing-house-app/logging-service/src/model/mod.rs index f7a208b..53a8ef5 100644 --- a/clearing-house-app/logging-service/src/model/mod.rs +++ b/clearing-house-app/logging-service/src/model/mod.rs @@ -5,6 +5,8 @@ use core_lib::api::crypto::get_fingerprint; pub mod constants; pub mod ids; +pub(crate) mod crypto; +pub(crate) mod doc_type; #[derive(Serialize, Deserialize)] pub struct TransactionCounter{ diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs new file mode 100644 index 0000000..47acc93 --- /dev/null +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -0,0 +1,281 @@ +use std::convert::TryFrom; +use anyhow::anyhow; +use core_lib::api::crypto::{ChClaims, create_service_token}; +use core_lib::api::{DocumentReceipt, QueryResult}; +use core_lib::constants::{DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART}; +use core_lib::model::document::Document; +use core_lib::model::{parse_date, sanitize_dates, SortingOrder, validate_dates}; +use core_lib::model::crypto::{KeyCt, KeyCtList}; +use crate::services::keyring_service::KeyringService; +use crate::db::docstore::DataStore; + +pub struct DocumentService { + db: DataStore, + key_api: KeyringService, +} + +impl DocumentService { + async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { + trace!("...user '{:?}'", &ch_claims.client_id); + // data validation + let payload: Vec = doc.parts.iter() + .filter(|p| String::from(PAYLOAD_PART) == p.name) + .map(|p| p.content.as_ref().unwrap().clone()).collect(); + if payload.len() > 1 { + return Err(anyhow!("Document contains two payloads!")); // BadRequest + } else if payload.len() == 0 { + return Err(anyhow!("Document contains no payload!")); // BadRequest + } + + // check if doc id already exists + match self.db.exists_document(&doc.id).await { + Ok(true) => { + warn!("Document exists already!"); + Err(anyhow!("Document exists already!")) // BadRequest + } + _ => { + debug!("Document does not exists!"); + debug!("getting keys"); + let keys; + + // TODO: This needs some attention, because keyring api called `create_service_token` on `ch_claims` + match self.key_api.generate_keys(ch_claims, doc.pid.clone(), doc.dt_id.clone()).await { + Ok(key_map) => { + keys = key_map; + debug!("got keys"); + } + Err(e) => { + error!("Error while retrieving keys: {:?}", e); + return Err(anyhow!("Error while retrieving keys!")); // InternalError + } + }; + + debug!("start encryption"); + let mut enc_doc; + match doc.encrypt(keys) { + Ok(ct) => { + debug!("got ct"); + enc_doc = ct + } + Err(e) => { + error!("Error while encrypting: {:?}", e); + return Err(anyhow!("Error while encrypting!")); // InternalError + } + }; + + // chain the document to previous documents + debug!("add the chain hash..."); + // get the document with the previous tc + match self.db.get_document_with_previous_tc(doc.tc).await { + Ok(Some(previous_doc)) => { + enc_doc.hash = previous_doc.hash(); + } + Ok(None) => { + if doc.tc == 0 { + info!("No entries found for pid {}. Beginning new chain!", doc.pid); + } else { + // If this happens, db didn't find a tc entry that should exist. + return Err(anyhow!("Error while creating the chain hash!")); // InternalError + } + } + Err(e) => { + error!("Error while creating the chain hash: {:?}", e); + return Err(anyhow!("Error while creating the chain hash!")); + } + } + + // prepare the success result message + + + let receipt = DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id, &enc_doc.hash); + + debug!("storing document ...."); + // store document + match self.db.add_document(enc_doc).await { + Ok(_b) => Ok(receipt), + Err(e) => { + error!("Error while adding: {:?}", e); + Err(anyhow!("Error while storing document!")) + } + } + } + } + } + + async fn get_enc_documents_for_pid(&self, + ch_claims: ChClaims, + doc_type: Option, + page: Option, // TODO: Why i32? This should be and unsinged int + size: Option, // TODO: Why i32? This should be and unsinged int + sort: Option, + date_from: Option, + date_to: Option, + pid: String) -> anyhow::Result { + debug!("Trying to retrieve documents for pid '{}'...", &pid); + trace!("...user '{:?}'", &ch_claims.client_id); + debug!("...page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); + + // Parameter validation for pagination: + // Valid pages start from 1 + // Max page number as of yet unknown + let sanitized_page = match page{ + Some(p) => { + if p > 0{ + u64::try_from(p).unwrap() + } + else{ + warn!("...invalid page requested. Falling back to 1."); + 1 + } + }, + None => 1 + }; + + // Valid sizes are between 1 and MAX_NUM_RESPONSE_ENTRIES (1000) + let sanitized_size = match size{ + Some(s) => { + if s > 0 && s <= i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap() { + u64::try_from(s).unwrap() + } + else{ + warn!("...invalid size requested. Falling back to default."); + DEFAULT_NUM_RESPONSE_ENTRIES + } + }, + None => DEFAULT_NUM_RESPONSE_ENTRIES + }; + + // Sorting order is already validated and defaults to descending + let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); + + // Parsing the dates for duration queries + let parsed_date_from = parse_date(date_from, false); + let parsed_date_to = parse_date(date_to, true); + + // Validation of dates with various checks. If none given dates default to date_now (date_to) and (date_now - 2 weeks) (date_from) + if !validate_dates(parsed_date_from, parsed_date_to){ + debug!("date validation failed!"); + return Err(anyhow!("Invalid date parameter!")); // BadRequest + } + let (sanitized_date_from, sanitized_date_to) = sanitize_dates(parsed_date_from, parsed_date_to); + + //new behavior: if pages are "invalid" return {}. Do not adjust page + //either call db with type filter or without to get cts + let start = chrono::Local::now(); + debug!("... using pagination with page: {}, size:{} and sort:{:#?}", sanitized_page, sanitized_size, &sanitized_sort); + + let dt_id = match doc_type{ + Some(dt) => dt, + None => String::from(DEFAULT_DOC_TYPE), + }; + let cts = match self.db.get_documents_for_pid(&dt_id, &pid, sanitized_page, sanitized_size, &sanitized_sort, &sanitized_date_from, &sanitized_date_to).await{ + Ok(cts) => cts, + Err(e) => { + error!("Error while retrieving document: {:?}", e); + return Err(anyhow!("Error while retrieving document for {}", &pid)) + }, + }; + + let result_size = i32::try_from(sanitized_size).ok(); + let result_page = i32::try_from(sanitized_page).ok(); + let result_sort = match sanitized_sort{ + SortingOrder::Ascending => String::from("asc"), + SortingOrder::Descending => String::from("desc"), + }; + + let mut result = QueryResult::new(sanitized_date_from.timestamp(), sanitized_date_to.timestamp(), result_page, result_size, result_sort, vec!()); + + // The db might contain no documents in which case we get an empty vector + if cts.is_empty(){ + debug!("Queried empty pid: {}", &pid); + Ok(result) + } + else{ + // Documents found for pid, now decrypting them + debug!("Found {} documents. Getting keys from keyring...", cts.len()); + let key_cts: Vec = cts.iter() + .map(|e| KeyCt::new(e.id.clone(), e.keys_ct.clone())).collect(); + // caution! we currently only support a single dt per call, so we use the first dt we found + let key_cts_list = KeyCtList::new(cts[0].dt_id.clone(), key_cts); + // decrypt cts + // TODO: This method needs some attention, because keyring api called `create_service_token` on `ch_claims` + let key_maps = match self.key_api.decrypt_multiple_keys(ch_claims, Some(pid),&key_cts_list).await{ + Ok(key_map) => { + key_map + } + Err(e) => { + error!("Error while retrieving keys from keyring: {:?}", e); + return Err(anyhow!("Error while retrieving keys from keyring")); // InternalError + } + }; + debug!("... keys received. Starting decryption..."); + let pts_bulk : Vec = cts.iter().zip(key_maps.iter()) + .filter_map(|(ct,key_map)|{ + if ct.id != key_map.id{ + error!("Document and map don't match"); + }; + match ct.decrypt(key_map.map.keys.clone()){ + Ok(d) => Some(d), + Err(e) => { + warn!("Got empty document from decryption! {:?}", e); + None + } + } + }).collect(); + debug!("...done."); + let end = chrono::Local::now(); + let diff = end - start; + info!("Total time taken to run in ms: {}", diff.num_milliseconds()); + result.documents = pts_bulk; + Ok(result) + } + } + + async fn get_enc_document(&self, ch_claims: ChClaims, pid: String, id: String, hash: Option) -> anyhow::Result { + trace!("...user '{:?}'", &ch_claims.client_id); + trace!("trying to retrieve document with id '{}' for pid '{}'", &id, &pid); + if hash.is_some(){ + debug!("integrity check with hash: {}", hash.as_ref().unwrap()); + } + + match self.db.get_document(&id, &pid).await{ + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(Some(ct)) => { + match hex::decode(&ct.keys_ct){ + Ok(key_ct) => { + // TODO: This method needs some attention, because keyring api called `create_service_token` on `ch_claims` + match self.key_api.decrypt_key_map(ch_claims, hex::encode_upper(key_ct), Some(pid), ct.dt_id.clone()).await{ + Ok(key_map) => { + //TODO check the hash + match ct.decrypt(key_map.keys){ + Ok(d) => Ok(d), + Err(e) => { + warn!("Got empty document from decryption! {:?}", e); + return Err(anyhow!("Document {} not found!", &id)); // NotFound + } + } + } + Err(e) => { + error!("Error while retrieving keys from keyring: {:?}", e); + return Err(anyhow!("Error while retrieving keys")) // InternalError + } + } + + }, + Err(e) => { + error!("Error while decoding ciphertext: {:?}", e); + return Err(anyhow!("Key Ciphertext corrupted")) // InternalError + } + } + }, + Ok(None) => { + debug!("Nothing found in db!"); + return Err(anyhow!("Document {} not found!", &id)) // NotFound + } + Err(e) => { + error!("Error while retrieving document: {:?}", e); + return Err(anyhow!("Error while retrieving document {}", &id)) // InternalError + } + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs new file mode 100644 index 0000000..f15e973 --- /dev/null +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -0,0 +1,169 @@ +use std::process::exit; +use anyhow::anyhow; +use rocket::futures::TryStreamExt; +use core_lib::api::crypto::ChClaims; +use core_lib::constants::MONGO_COLL_MASTER_KEY; +use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; +use crate::crypto; +use crate::crypto::restore_key_map; +use crate::db::keystore::KeyStore; + +pub struct KeyringService { + db: KeyStore, +} + +impl KeyringService { + pub async fn generate_keys(&self, ch_claims: ChClaims, _pid: String, dt_id: String) -> anyhow::Result { + trace!("generate_keys"); + trace!("...user '{:?}'", &ch_claims.client_id); + match self.db.get_msk().await { + Ok(key) => { + // check that doc type exists for pid + match self.db.get_document_type(&dt_id).await { + Ok(Some(dt)) => { + // generate new random key map + match crypto::generate_key_map(key, dt) { + Ok(key_map) => { + trace!("response: {:?}", &key_map); + return Ok(key_map); + } + Err(e) => { + error!("Error while generating key map: {}", e); + return Err(anyhow!("Error while generating keys")); // InternalError + } + } + } + Ok(None) => { + warn!("document type {} not found", &dt_id); + return Err(anyhow!("Document type not found!")); // BadRequest + } + Err(e) => { + warn!("Error while retrieving document type: {}", e); + return Err(anyhow!("Error while retrieving document type")); // InternalError + } + } + } + Err(e) => { + error!("Error while retrieving master key: {}", e); + return Err(anyhow!("Error while generating keys")); // InternalError + } + } + } + + pub(crate) async fn decrypt_keys(&self, ch_claims: ChClaims, _pid: Option, key_cts: &KeyCtList) -> anyhow::Result> { + trace!("decrypt_keys"); + trace!("...user '{:?}'", &ch_claims.client_id); + debug!("number of cts to decrypt: {}", &key_cts.cts.len()); + + // get master key + match self.db.get_msk().await { + Ok(m_key) => { + // check that doc type exists for pid + match self.db.get_document_type(&key_cts.dt).await { + Ok(Some(dt)) => { + let mut dec_error_count = 0; + let mut map_error_count = 0; + // validate keys_ct input + let key_maps: Vec = key_cts.cts.iter().filter_map( + |key_ct| { + match hex::decode(key_ct.ct.clone()) { + Ok(key) => Some((key_ct.id.clone(), key)), + Err(e) => { + error!("Error while decoding key ciphertext: {}", e); + dec_error_count = dec_error_count + 1; + None + } + } + } + ).filter_map( + |(id, key)| { + match restore_key_map(m_key.clone(), dt.clone(), key) { + Ok(key_map) => { + Some(KeyMapListItem::new(id, key_map)) + } + Err(e) => { + error!("Error while generating key map: {}", e); + map_error_count = map_error_count + 1; + None + } + } + } + ) + .collect(); + + let error_count = map_error_count + dec_error_count; + + // Currently, we don't tolerate errors while decrypting keys + if error_count > 0 { + return Err(anyhow!("Error while decrypting keys")); // InternalError + } else { + return Ok(key_maps); + } + } + Ok(None) => { + warn!("document type {} not found", &key_cts.dt); + return Err(anyhow!("Document type not found!")); // BadRequest + } + Err(e) => { + warn!("Error while retrieving document type: {}", e); + return Err(anyhow!("Document type not found!")); // NotFound + } + } + } + Err(e) => { + error!("Error while retrieving master key: {}", e); + return Err(anyhow!("Error while decrypting keys")); // InternalError + } + } + } + + pub async fn decrypt_key_map(&self, ch_claims: ChClaims, keys_ct: String, _pid: Option, dt_id: String) -> anyhow::Result { + trace!("decrypt_key_map"); + trace!("...user '{:?}'", &ch_claims.client_id); + trace!("ct: {}", &keys_ct); + // get master key + match self.db.get_msk().await { + Ok(key) => { + // check that doc type exists for pid + match self.db.get_document_type(&dt_id).await { + Ok(Some(dt)) => { + // validate keys_ct input + let keys_ct = match hex::decode(keys_ct) { + Ok(key) => key, + Err(e) => { + error!("Error while decoding key ciphertext: {}", e); + return Err(anyhow!("Error while decrypting keys")); // InternalError + } + }; + + match restore_key_map(key, dt, keys_ct) { + Ok(key_map) => { + return Ok(key_map); + }, + Err(e) => { + error!("Error while generating key map: {}", e); + return Err(anyhow!("Error while restoring keys")); // InternalError + } + } + } + Ok(None) => { + warn!("document type {} not found", &dt_id); + return Err(anyhow!("Document type not found!")); // BadRequest + } + Err(e) => { + warn!("Error while retrieving document type: {}", e); + return Err(anyhow!("Document type not found!")); // NotFound + } + } + } + Err(e) => { + error!("Error while retrieving master key: {}", e); + return Err(anyhow!("Error while decrypting keys")); // InternalError + } + } + } + + pub(crate) async fn decrypt_multiple_keys(&self, ch_claims: ChClaims, pid: Option, cts: &KeyCtList) -> anyhow::Result> { + self.decrypt_keys(ch_claims, pid, cts).await + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs new file mode 100644 index 0000000..01df54e --- /dev/null +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -0,0 +1,2 @@ +mod keyring_service; +mod document_service; \ No newline at end of file From 4259c65cfca978f3ad77c8d37fec85bd3fbaa90f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 10:05:09 +0200 Subject: [PATCH 026/183] feat(ch-app): Refactor logging-api to use a service as well - Separated rocket-handler and LoggingService method, so that a http server replacement becomes easier and the architecture becomes more modular. - Added ports module, where the rocket-handler code will be moved in the next commits - Added forbid(unsafe_code) in every crate --- clearing-house-app/core-lib/src/lib.rs | 2 + clearing-house-app/document-api/src/main.rs | 2 + clearing-house-app/keyring-api/src/main.rs | 2 + .../logging-service/src/logging_api.rs | 366 ++---------------- .../logging-service/src/main.rs | 3 + .../logging-service/src/ports/mod.rs | 5 + .../src/services/document_service.rs | 18 +- .../src/services/logging_service.rs | 365 +++++++++++++++++ .../logging-service/src/services/mod.rs | 9 +- 9 files changed, 436 insertions(+), 336 deletions(-) create mode 100644 clearing-house-app/logging-service/src/ports/mod.rs create mode 100644 clearing-house-app/logging-service/src/services/logging_service.rs diff --git a/clearing-house-app/core-lib/src/lib.rs b/clearing-house-app/core-lib/src/lib.rs index e14ed64..815fed7 100644 --- a/clearing-house-app/core-lib/src/lib.rs +++ b/clearing-house-app/core-lib/src/lib.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + extern crate biscuit; extern crate chrono; extern crate fern; diff --git a/clearing-house-app/document-api/src/main.rs b/clearing-house-app/document-api/src/main.rs index 8da3956..59e38d4 100644 --- a/clearing-house-app/document-api/src/main.rs +++ b/clearing-house-app/document-api/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; diff --git a/clearing-house-app/keyring-api/src/main.rs b/clearing-house-app/keyring-api/src/main.rs index 5074728..ac4bf78 100644 --- a/clearing-house-app/keyring-api/src/main.rs +++ b/clearing-house-app/keyring-api/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + #[macro_use] extern crate error_chain; #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; diff --git a/clearing-house-app/logging-service/src/logging_api.rs b/clearing-house-app/logging-service/src/logging_api.rs index 254b1f0..f38b316 100644 --- a/clearing-house-app/logging-service/src/logging_api.rs +++ b/clearing-house-app/logging-service/src/logging_api.rs @@ -1,389 +1,103 @@ use core_lib::{ api::{ ApiResponse, - client::document_api::DocumentApiClient, crypto::{ChClaims, get_jwks}, }, - constants::{DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, DEFAULT_PROCESS_ID}, - model::{ - document::Document, - process::Process, - SortingOrder, - SortingOrder::Descending - } + model::SortingOrder, }; use rocket::serde::json::{json, Json}; use rocket::fairing::AdHoc; -use rocket::form::validate::Contains; use rocket::State; use std::convert::TryFrom; -use crate::model::{ids::{ - message::IdsMessage, - IdsQueryResult, - request::ClearingHouseMessage, -}, OwnerList, DataTransaction}; -use crate::db::ProcessStore; +use crate::model::ids::request::ClearingHouseMessage; use crate::model::constants::{ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_QUERY_API, ROCKET_PROCESS_API, ROCKET_PK_API}; +use crate::services::logging_service::LoggingService; -#[post( "/", format = "json", data = "")] +#[post("/", format = "json", data = "")] async fn log( ch_claims: ChClaims, - db: &State, - doc_api: &State, + logging_api: &State, key_path: &State, message: Json, - pid: String + pid: String, ) -> ApiResponse { - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - // Add non-InfoModel information to IdsMessage - let msg = message.into_inner(); - let mut m = msg.header; - m.payload = msg.payload; - m.payload_type = msg.payload_type; - m.pid = Some(pid.clone()); - - // validate that there is a payload - if m.payload.is_none() || (m.payload.is_some() && m.payload.as_ref().unwrap().trim().is_empty()){ - error!("Trying to log an empty payload!"); - return ApiResponse::BadRequest(String::from("No payload received for logging!")) - } - - // filter out calls for default process id and call application logic - match DEFAULT_PROCESS_ID.eq(pid.as_str()){ - true => { - warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); - ApiResponse::BadRequest(String::from("Document already exists")) - }, - false => { - // convenience: if process does not exist, we create it but only if no error occurred before - match db.get_process(&pid).await { - Ok(Some(_p)) => { - debug!("Requested pid '{}' exists. Nothing to create.", &pid); - } - Ok(None) => { - info!("Requested pid '{}' does not exist. Creating...", &pid); - // create a new process - let new_process = Process::new(pid.clone(), vec!(user.clone())); - - if db.store_process(new_process).await.is_err() { - error!("Error while creating process '{}'", &pid); - return ApiResponse::InternalError(String::from("Error while creating process")) - } - } - Err(_) => { - error!("Error while getting process '{}'", &pid); - return ApiResponse::InternalError(String::from("Error while getting process")) - } - } - - // now check if user is authorized to write to pid - match db.is_authorized(&user, &pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - warn!("This is the forbidden branch"); - return ApiResponse::Forbidden(String::from("User not authorized!")) - } - Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); - return ApiResponse::InternalError(String::from("Error during authorization")) - } - } - - debug!("logging message for pid {}", &pid); - log_message(db, user, doc_api, key_path.inner().as_str(), m.clone()).await + match logging_api.inner().log(ch_claims, key_path, message.into_inner(), pid).await { + Ok(id) => ApiResponse::SuccessCreate(json!(id)), + Err(e) => { + error!("Error while logging: {:?}", e); + ApiResponse::InternalError(String::from("Error while logging!")) } } } -#[post( "/", format = "json", data = "")] +#[post("/", format = "json", data = "")] async fn create_process( ch_claims: ChClaims, - db: &State, + logging_api: &State, message: Json, - pid: String -) -> ApiResponse { - let msg = message.into_inner(); - let mut m = msg.header; - m.payload = msg.payload; - m.payload_type = msg.payload_type; - - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - - // validate payload - let mut owners = vec!(user.clone()); - let payload = m.payload.clone().unwrap_or(String::new()); - if !payload.is_empty() { - trace!("OwnerList: '{:#?}'", &payload); - match serde_json::from_str::(&payload){ - Ok(owner_list) => { - for o in owner_list.owners{ - if !owners.contains(&o){ - owners.push(o); - } - } - }, - Err(e) => { - error!("Error while creating process '{}': {}", &pid, e); - return ApiResponse::BadRequest(String::from("Invalid owner list!")) - } - }; - }; - - // check if the pid already exists - match db.get_process(&pid).await{ - Ok(Some(p)) => { - warn!("Requested pid '{}' already exists.", &p.id); - if !p.owners.contains(user) { - ApiResponse::Forbidden(String::from("User not authorized!")) - } - else { - ApiResponse::BadRequest(String::from("Process already exists!")) - } - } - _ => { - // filter out calls for default process id - match DEFAULT_PROCESS_ID.eq(pid.as_str()) { - true => { - warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); - ApiResponse::BadRequest(String::from("Document already exists")) - }, - false => { - info!("Requested pid '{}' will have {} owners", &pid, owners.len()); - - // create process - info!("Requested pid '{}' does not exist. Creating...", &pid); - let new_process = Process::new(pid.clone(), owners); - - match db.store_process(new_process).await{ - Ok(_) => { - ApiResponse::SuccessCreate(json!(pid.clone())) - } - Err(e) => { - error!("Error while creating process '{}': {}", &pid, e); - ApiResponse::InternalError(String::from("Error while creating process")) - } - } - } - } - } - } -} - -async fn log_message( - db: &State, - user: &String, - doc_api: &State, - key_path: &str, - message: IdsMessage + pid: String, ) -> ApiResponse { - - debug!("transforming message to document..."); - let payload = message.payload.as_ref().unwrap().clone(); - // transform message to document - let mut doc = Document::from(message); - match db.get_transaction_counter().await{ - Ok(Some(tid)) => { - debug!("Storing document..."); - doc.tc = tid; - return match doc_api.create_document(&user, &doc).await{ - Ok(doc_receipt) => { - debug!("Increase transaction counter"); - match db.increment_transaction_counter().await{ - Ok(Some(_tid)) => { - debug!("Creating receipt..."); - let transaction = DataTransaction{ - transaction_id: doc.get_formatted_tc(), - timestamp: doc_receipt.timestamp, - process_id: doc_receipt.pid, - document_id: doc_receipt.doc_id, - payload, - chain_hash: doc_receipt.chain_hash, - client_id: user.clone(), - clearing_house_version: env!("CARGO_PKG_VERSION").to_string(), - }; - debug!("...done. Signing receipt..."); - ApiResponse::SuccessCreate(json!(transaction.sign(key_path))) - } - _ => { - error!("Error while incrementing transaction id!"); - ApiResponse::InternalError(String::from("Internal error while preparing transaction data")) - } - } - - }, - Err(e) => { - error!("Error while creating document: {:?}", e); - ApiResponse::BadRequest(String::from("Document already exists")) - } - } - }, - Ok(None) => { - println!("None!"); - ApiResponse::InternalError(String::from("Internal error while preparing transaction data")) - } + match logging_api.inner().create_process(ch_claims, message.into_inner(), pid).await { + Ok(id) => ApiResponse::SuccessCreate(json!(id)), Err(e) => { - error!("Error while getting transaction id!"); - println!("{}", e); - ApiResponse::InternalError(String::from("Internal error while preparing transaction data")) + error!("Error while creating process: {:?}", e); + ApiResponse::InternalError(String::from("Error while creating process!")) } } } -#[post("/<_pid>", format = "json", rank=50)] +#[post("/<_pid>", format = "json", rank = 50)] async fn unauth(_pid: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/<_pid>/<_id>", format = "json", rank=50)] +#[post("/<_pid>/<_id>", format = "json", rank = 50)] async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/?&&&&", format = "json", data = "<_message>")] +#[post("/?&&&&", format = "json", data = "")] async fn query_pid( ch_claims: ChClaims, - db: &State, + logging_api: &State, page: Option, size: Option, sort: Option, date_to: Option, date_from: Option, - doc_api: &State, pid: String, - _message: Json + message: Json, ) -> ApiResponse { - debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); - - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - - // check if process exists - match db.exists_process(&pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => return ApiResponse::NotFound(String::from("Process does not exist!")), - Err(_e) => { - error!("Error while checking process '{}' for user '{}'", &pid, &user); - return ApiResponse::InternalError(String::from("Cannot authorize user!")) - } - }; - - // now check if user is authorized to read infos in pid - match db.is_authorized(&user, &pid).await { - Ok(true) => { - info!("User authorized."); - }, - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - return ApiResponse::Forbidden(String::from("User not authorized!")) - } - Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); - return ApiResponse::InternalError(String::from("Cannot authorize user!")) - } - } - - // sanity check for pagination - let sanitized_page = match page { - Some(p) => { - if p >= 0 { - p - } else { - warn!("...invalid page requested. Falling back to 0."); - 1 - } - }, - None => 1 - }; - - let sanitized_size = match size { - Some(s) => { - let converted_max = i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap(); - if s > converted_max { - warn!("...invalid size requested. Falling back to default."); - converted_max - } else { - if s > 0 { - s - } else { - warn!("...invalid size requested. Falling back to default."); - i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() - } - } - }, - None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() - }; - - let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); - - match doc_api.get_documents(&user, &pid, sanitized_page, sanitized_size, sanitized_sort, date_from, date_to).await { - Ok(r) => { - let messages: Vec = r.documents.iter().map(|d| IdsMessage::from(d.clone())).collect(); - let result = IdsQueryResult::new(r.date_from, r.date_to, r.page, r.size, r.order, messages); - ApiResponse::SuccessOk(json!(result)) - - }, + match logging_api.inner().query_pid(ch_claims, page, size, sort, date_to, date_from, pid, message.into_inner()).await { + Ok(result) => ApiResponse::SuccessOk(json!(result)), Err(e) => { - error!("Error while retrieving message: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving messages for pid {}!", &pid)) + error!("Error while querying: {:?}", e); + ApiResponse::InternalError(String::from("Error while querying!")) } } } -#[post("//", format = "json", data = "<_message>")] -async fn query_id(ch_claims: ChClaims, db: &State, doc_api: &State, pid: String, id: String, _message: Json) -> ApiResponse { - - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - - // check if process exists - match db.exists_process(&pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => return ApiResponse::NotFound(String::from("Process does not exist!")), - Err(_e) => { - error!("Error while checking process '{}' for user '{}'", &pid, &user); - return ApiResponse::InternalError(String::from("Cannot authorize user!")) - } - }; - - // now check if user is authorized to read infos in pid - match db.is_authorized(&user, &pid).await { - Ok(true) => { - info!("User authorized."); - }, - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - return ApiResponse::Forbidden(String::from("User not authorized!")) - } - Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); - return ApiResponse::InternalError(String::from("Cannot authorize user!")) - } - } - - match doc_api.get_document(&user, &pid, &id).await { - Ok(Some(doc)) => { - // transform document to IDS message - let queried_message = IdsMessage::from(doc); - ApiResponse::SuccessOk(json!(queried_message)) - }, - Ok(None) => { - debug!("Queried a non-existing document: {}", &id); - ApiResponse::NotFound(format!("No message found with id {}!", &id)) - }, +#[post("//", format = "json", data = "")] +async fn query_id( + ch_claims: ChClaims, + logging_api: &State, + pid: String, + id: String, + message: Json, +) -> ApiResponse { + match logging_api.inner().query_id(ch_claims, pid, id, message.into_inner()).await { + Ok(result) => ApiResponse::SuccessOk(json!(result)), Err(e) => { - error!("Error while retrieving message: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving message with id {}!", &id)) + error!("Error while querying: {:?}", e); + ApiResponse::InternalError(String::from("Error while querying!")) } } } #[get("/.well-known/jwks.json", format = "json")] async fn get_public_sign_key(key_path: &State) -> ApiResponse { - match get_jwks(key_path.as_str()){ + match get_jwks(key_path.as_str()) { Some(jwks) => ApiResponse::SuccessOk(json!(jwks)), None => ApiResponse::InternalError(String::from("Error reading signing key")) } diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index fdbbbcb..6dfd988 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -1,3 +1,5 @@ +#![forbid(unsafe_code)] + #[macro_use] extern crate rocket; #[macro_use] extern crate serde_derive; @@ -16,6 +18,7 @@ pub mod db; pub mod model; mod services; mod crypto; +mod ports; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/logging-service/src/ports/mod.rs new file mode 100644 index 0000000..7c2432b --- /dev/null +++ b/clearing-house-app/logging-service/src/ports/mod.rs @@ -0,0 +1,5 @@ +//! # Ports +//! +//! This module contains the ports of the logging service. Ports are used to communicate with other +//! services. In this case, the logging service implements REST-API endpoints to provide access to +//! the logging service. diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 47acc93..4cacc31 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -15,7 +15,7 @@ pub struct DocumentService { } impl DocumentService { - async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { + pub(crate) async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); // data validation let payload: Vec = doc.parts.iter() @@ -102,15 +102,15 @@ impl DocumentService { } } - async fn get_enc_documents_for_pid(&self, - ch_claims: ChClaims, - doc_type: Option, - page: Option, // TODO: Why i32? This should be and unsinged int + pub(crate) async fn get_enc_documents_for_pid(&self, + ch_claims: ChClaims, + doc_type: Option, + page: Option, // TODO: Why i32? This should be and unsinged int size: Option, // TODO: Why i32? This should be and unsinged int sort: Option, - date_from: Option, - date_to: Option, - pid: String) -> anyhow::Result { + date_from: Option, + date_to: Option, + pid: String) -> anyhow::Result { debug!("Trying to retrieve documents for pid '{}'...", &pid); trace!("...user '{:?}'", &ch_claims.client_id); debug!("...page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); @@ -231,7 +231,7 @@ impl DocumentService { } } - async fn get_enc_document(&self, ch_claims: ChClaims, pid: String, id: String, hash: Option) -> anyhow::Result { + pub(crate) async fn get_enc_document(&self, ch_claims: ChClaims, pid: String, id: String, hash: Option) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); trace!("trying to retrieve document with id '{}' for pid '{}'", &id, &pid); if hash.is_some(){ diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/logging-service/src/services/logging_service.rs new file mode 100644 index 0000000..1a75f0e --- /dev/null +++ b/clearing-house-app/logging-service/src/services/logging_service.rs @@ -0,0 +1,365 @@ +use core_lib::{ + api::crypto::ChClaims, + constants::{DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, DEFAULT_PROCESS_ID}, + model::{ + document::Document, + process::Process, + SortingOrder, + }, +}; +use rocket::form::validate::Contains; +use rocket::State; +use std::convert::TryFrom; +use anyhow::anyhow; + +use crate::model::{ids::{ + message::IdsMessage, + IdsQueryResult, + request::ClearingHouseMessage, +}, OwnerList, DataTransaction, Receipt}; +use crate::db::ProcessStore; +use crate::services::document_service::DocumentService; + +pub struct LoggingService { + db: ProcessStore, + doc_api: DocumentService, +} + +impl LoggingService { + pub async fn log( + &self, + ch_claims: ChClaims, + key_path: &State, + msg: ClearingHouseMessage, + pid: String, + ) -> anyhow::Result { + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + // Add non-InfoModel information to IdsMessage + let mut m = msg.header; + m.payload = msg.payload; + m.payload_type = msg.payload_type; + m.pid = Some(pid.clone()); + + // validate that there is a payload + if m.payload.is_none() || (m.payload.is_some() && m.payload.as_ref().unwrap().trim().is_empty()) { + error!("Trying to log an empty payload!"); + return Err(anyhow!("No payload received for logging!")); // BadRequest + } + + // filter out calls for default process id and call application logic + match DEFAULT_PROCESS_ID.eq(pid.as_str()) { + true => { + warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); + Err(anyhow!("Document already exists")) // BadRequest + } + false => { + // convenience: if process does not exist, we create it but only if no error occurred before + match self.db.get_process(&pid).await { + Ok(Some(_p)) => { + debug!("Requested pid '{}' exists. Nothing to create.", &pid); + } + Ok(None) => { + info!("Requested pid '{}' does not exist. Creating...", &pid); + // create a new process + let new_process = Process::new(pid.clone(), vec!(user.clone())); + + if self.db.store_process(new_process).await.is_err() { + error!("Error while creating process '{}'", &pid); + return Err(anyhow!("Error while creating process")); // InternalError + } + } + Err(_) => { + error!("Error while getting process '{}'", &pid); + return Err(anyhow!("Error while getting process")); // InternalError + } + } + + // now check if user is authorized to write to pid + match self.db.is_authorized(&user, &pid).await { + Ok(true) => info!("User authorized."), + Ok(false) => { + warn!("User is not authorized to write to pid '{}'", &pid); + warn!("This is the forbidden branch"); + return Err(anyhow!("User not authorized!")); // Forbidden + } + Err(_) => { + error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + return Err(anyhow!("Error during authorization")); + } + } + + debug!("logging message for pid {}", &pid); + self.log_message(user, key_path.inner().as_str(), m.clone()).await + } + } + } + + pub(crate) async fn create_process( + &self, + ch_claims: ChClaims, + msg: ClearingHouseMessage, + pid: String, + ) -> anyhow::Result { + let mut m = msg.header; + m.payload = msg.payload; + m.payload_type = msg.payload_type; + + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + + // validate payload + let mut owners = vec!(user.clone()); + let payload = m.payload.clone().unwrap_or(String::new()); + if !payload.is_empty() { + trace!("OwnerList: '{:#?}'", &payload); + match serde_json::from_str::(&payload) { + Ok(owner_list) => { + for o in owner_list.owners { + if !owners.contains(&o) { + owners.push(o); + } + } + } + Err(e) => { + error!("Error while creating process '{}': {}", &pid, e); + return Err(anyhow!("Invalid owner list!")); // BadRequest + } + }; + }; + + // check if the pid already exists + match self.db.get_process(&pid).await { + Ok(Some(p)) => { + warn!("Requested pid '{}' already exists.", &p.id); + if !p.owners.contains(user) { + Err(anyhow!("User not authorized!")) // Forbidden + } else { + Err(anyhow!("Process already exists!")) // BadRequest + } + } + _ => { + // filter out calls for default process id + match DEFAULT_PROCESS_ID.eq(pid.as_str()) { + true => { + warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); + Err(anyhow!("Document already exists")) // BadRequest + } + false => { + info!("Requested pid '{}' will have {} owners", &pid, owners.len()); + + // create process + info!("Requested pid '{}' does not exist. Creating...", &pid); + let new_process = Process::new(pid.clone(), owners); + + match self.db.store_process(new_process).await { + Ok(_) => { + Ok(pid.clone()) + } + Err(e) => { + error!("Error while creating process '{}': {}", &pid, e); + Err(anyhow!("Error while creating process")) // InternalError + } + } + } + } + } + } + } + + async fn log_message( + &self, + user: &String, + key_path: &str, + message: IdsMessage, + ) -> anyhow::Result { + debug!("transforming message to document..."); + let payload = message.payload.as_ref().unwrap().clone(); + // transform message to document + let mut doc = Document::from(message); + match self.db.get_transaction_counter().await { + Ok(Some(tid)) => { + debug!("Storing document..."); + doc.tc = tid; + // TODO: ChClaims usage check + match self.doc_api.create_enc_document(ChClaims::new(&user), doc.clone()).await { + Ok(doc_receipt) => { + debug!("Increase transaction counter"); + match self.db.increment_transaction_counter().await { + Ok(Some(_tid)) => { + debug!("Creating receipt..."); + let transaction = DataTransaction { + transaction_id: doc.get_formatted_tc(), + timestamp: doc_receipt.timestamp, + process_id: doc_receipt.pid, + document_id: doc_receipt.doc_id, + payload, + chain_hash: doc_receipt.chain_hash, + client_id: user.clone(), + clearing_house_version: env!("CARGO_PKG_VERSION").to_string(), + }; + debug!("...done. Signing receipt..."); + Ok(transaction.sign(key_path)) + } + _ => { + error!("Error while incrementing transaction id!"); + Err(anyhow!("Internal error while preparing transaction data")) // InternalError + } + } + } + Err(e) => { + error!("Error while creating document: {:?}", e); + Err(anyhow!("Document already exists")) // BadRequest + } + } + } + Ok(None) => { + println!("None!"); + Err(anyhow!("Internal error while preparing transaction data")) // InternalError + } + Err(e) => { + error!("Error while getting transaction id!"); + println!("{}", e); + Err(anyhow!("Internal error while preparing transaction data")) // InternalError + } + } + } + + pub(crate) async fn query_pid( + &self, + ch_claims: ChClaims, + page: Option, + size: Option, + sort: Option, + date_to: Option, + date_from: Option, + pid: String, + message: ClearingHouseMessage, + ) -> anyhow::Result { + debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); + + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + + // check if process exists + match self.db.exists_process(&pid).await { + Ok(true) => info!("User authorized."), + Ok(false) => return Err(anyhow!("Process does not exist!")), // NotFound + Err(_e) => { + error!("Error while checking process '{}' for user '{}'", &pid, &user); + return Err(anyhow!("Cannot authorize user!")); // InternalError + } + }; + + // now check if user is authorized to read infos in pid + match self.db.is_authorized(&user, &pid).await { + Ok(true) => { + info!("User authorized."); + } + Ok(false) => { + warn!("User is not authorized to write to pid '{}'", &pid); + return Err(anyhow!("User not authorized!")); // Forbidden + } + Err(_) => { + error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + return Err(anyhow!("Cannot authorize user!")); // InternalError + } + } + + // sanity check for pagination + let sanitized_page = match page { + Some(p) => { + if p >= 0 { + p + } else { + warn!("...invalid page requested. Falling back to 0."); + 1 + } + } + None => 1 + }; + + let sanitized_size = match size { + Some(s) => { + let converted_max = i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap(); + if s > converted_max { + warn!("...invalid size requested. Falling back to default."); + converted_max + } else { + if s > 0 { + s + } else { + warn!("...invalid size requested. Falling back to default."); + i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() + } + } + } + None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() + }; + + let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); + + match self.doc_api.get_enc_documents_for_pid(ChClaims::new(&user), None, Some(sanitized_page), Some(sanitized_size), Some(sanitized_sort), date_from, date_to, pid.clone()).await { + Ok(r) => { + let messages: Vec = r.documents.iter().map(|d| IdsMessage::from(d.clone())).collect(); + let result = IdsQueryResult::new(r.date_from, r.date_to, r.page, r.size, r.order, messages); + Ok(result) + } + Err(e) => { + error!("Error while retrieving message: {:?}", e); + Err(anyhow!("Error while retrieving messages for pid {}!", &pid)) // InternalError + } + } + } + + pub(crate) async fn query_id(&self, + ch_claims: ChClaims, + pid: String, + id: String, + message: ClearingHouseMessage, + ) -> anyhow::Result { + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + + // check if process exists + match self.db.exists_process(&pid).await { + Ok(true) => info!("User authorized."), + Ok(false) => return Err(anyhow!("Process does not exist!")), // NotFound + Err(_e) => { + error!("Error while checking process '{}' for user '{}'", &pid, &user); + return Err(anyhow!("Cannot authorize user!")); // InternalError + } + }; + + // now check if user is authorized to read infos in pid + match self.db.is_authorized(&user, &pid).await { + Ok(true) => { + info!("User authorized."); + } + Ok(false) => { + warn!("User is not authorized to write to pid '{}'", &pid); + return Err(anyhow!("User not authorized!")); // Forbidden + } + Err(_) => { + error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + return Err(anyhow!("Cannot authorize user!")); // InternalError + } + } + + match self.doc_api.get_enc_document(ChClaims::new(&user), pid.clone(), id.clone(), None).await { + Ok(doc) => { + // transform document to IDS message + let queried_message = IdsMessage::from(doc); + Ok(queried_message) + } + /*Result::Ok(None) => { + debug!("Queried a non-existing document: {}", &id); + ApiResponse::NotFound(format!("No message found with id {}!", &id)) + }*/ + Err(e) => { + error!("Error while retrieving message: {:?}", e); + Err(anyhow!("Error while retrieving message with id {}!", &id)) // InternalError + } + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs index 01df54e..a1b5a37 100644 --- a/clearing-house-app/logging-service/src/services/mod.rs +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -1,2 +1,9 @@ +//! # Services +//! +//! This module contains the Application Services that are used by the API Controllers. It is +//! responsible for the business logic of the application. The services are used by the API +//! Controllers to handle the requests and responses. +//! mod keyring_service; -mod document_service; \ No newline at end of file +mod document_service; +pub(crate) mod logging_service; \ No newline at end of file From c64b85fb0c1b44bbd233f73ec38323caaa24aff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 10:14:36 +0200 Subject: [PATCH 027/183] refactor(ch-app): Refactor rocket-handlers into the ports module --- clearing-house-app/logging-service/src/main.rs | 3 +-- .../logging-service/src/{ => ports}/logging_api.rs | 0 clearing-house-app/logging-service/src/ports/mod.rs | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) rename clearing-house-app/logging-service/src/{ => ports}/logging_api.rs (100%) diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 6dfd988..62fcf31 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -13,7 +13,6 @@ use core_lib::constants::ENV_LOGGING_SERVICE_ID; use db::ProcessStoreConfigurator; use model::constants::SIGNING_KEY; -pub mod logging_api; pub mod db; pub mod model; mod services; @@ -44,5 +43,5 @@ fn rocket() -> Rocket { .attach(add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) .attach(ApiClientConfigurator::new(ApiClientEnum::Document)) .attach(ApiClientConfigurator::new(ApiClientEnum::Keyring)) - .attach(logging_api::mount_api()) + .attach(ports::logging_api::mount_api()) } diff --git a/clearing-house-app/logging-service/src/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs similarity index 100% rename from clearing-house-app/logging-service/src/logging_api.rs rename to clearing-house-app/logging-service/src/ports/logging_api.rs diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/logging-service/src/ports/mod.rs index 7c2432b..9e62cb4 100644 --- a/clearing-house-app/logging-service/src/ports/mod.rs +++ b/clearing-house-app/logging-service/src/ports/mod.rs @@ -3,3 +3,4 @@ //! This module contains the ports of the logging service. Ports are used to communicate with other //! services. In this case, the logging service implements REST-API endpoints to provide access to //! the logging service. +pub mod logging_api; From 007281f3e7f436606c04c41edab917c432e7e0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 10:29:30 +0200 Subject: [PATCH 028/183] feat(ch-app): Bump Cargo edition to 2021 and remove unused imports --- clearing-house-app/core-lib/Cargo.toml | 2 +- clearing-house-app/document-api/Cargo.toml | 2 +- clearing-house-app/keyring-api/Cargo.toml | 2 +- clearing-house-app/logging-service/Cargo.toml | 2 +- clearing-house-app/logging-service/src/ports/logging_api.rs | 1 - .../logging-service/src/services/document_service.rs | 2 +- .../logging-service/src/services/keyring_service.rs | 3 --- 7 files changed, 5 insertions(+), 9 deletions(-) diff --git a/clearing-house-app/core-lib/Cargo.toml b/clearing-house-app/core-lib/Cargo.toml index 140385c..1a993c7 100644 --- a/clearing-house-app/core-lib/Cargo.toml +++ b/clearing-house-app/core-lib/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Mark Gall ", "Georg Bramm ", ] -edition = "2018" +edition = "2021" [dependencies] aes = "0.6.0" diff --git a/clearing-house-app/document-api/Cargo.toml b/clearing-house-app/document-api/Cargo.toml index 3cb0918..f844f62 100644 --- a/clearing-house-app/document-api/Cargo.toml +++ b/clearing-house-app/document-api/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Mark Gall ", "Georg Bramm ", ] -edition = "2018" +edition = "2021" [dependencies] biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } diff --git a/clearing-house-app/keyring-api/Cargo.toml b/clearing-house-app/keyring-api/Cargo.toml index 7d3eda4..37718a3 100644 --- a/clearing-house-app/keyring-api/Cargo.toml +++ b/clearing-house-app/keyring-api/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Mark Gall ", "Georg Bramm " ] -edition = "2018" +edition = "2021" [dependencies] aes = "0.6.0" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index afb441e..ccd6294 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -5,7 +5,7 @@ authors = [ "Mark Gall ", "Georg Bramm ", ] -edition = "2018" +edition = "2021" [dependencies] biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } diff --git a/clearing-house-app/logging-service/src/ports/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs index f38b316..8ac0bac 100644 --- a/clearing-house-app/logging-service/src/ports/logging_api.rs +++ b/clearing-house-app/logging-service/src/ports/logging_api.rs @@ -8,7 +8,6 @@ use core_lib::{ use rocket::serde::json::{json, Json}; use rocket::fairing::AdHoc; use rocket::State; -use std::convert::TryFrom; use crate::model::ids::request::ClearingHouseMessage; use crate::model::constants::{ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_QUERY_API, ROCKET_PROCESS_API, ROCKET_PK_API}; diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 4cacc31..1261d25 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -1,6 +1,6 @@ use std::convert::TryFrom; use anyhow::anyhow; -use core_lib::api::crypto::{ChClaims, create_service_token}; +use core_lib::api::crypto::ChClaims; use core_lib::api::{DocumentReceipt, QueryResult}; use core_lib::constants::{DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART}; use core_lib::model::document::Document; diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index f15e973..0937beb 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -1,8 +1,5 @@ -use std::process::exit; use anyhow::anyhow; -use rocket::futures::TryStreamExt; use core_lib::api::crypto::ChClaims; -use core_lib::constants::MONGO_COLL_MASTER_KEY; use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::crypto; use crate::crypto::restore_key_map; From c1931284ace72c7d2602527896096dff234a1d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 11:39:24 +0200 Subject: [PATCH 029/183] refactor(ch-app): Moved rocket fairings in separate module in logging service, restructured keystore fairing --- .../src/db/config/doc_store.rs | 119 ++++++++++++++++++ .../src/db/config/keyring_store.rs | 113 +++++++++++++++++ .../logging-service/src/db/config/mod.rs | 3 + .../src/db/config/process_store.rs | 81 ++++++++++++ .../src/db/{docstore.rs => doc_store.rs} | 30 +++-- .../src/db/{keystore.rs => key_store.rs} | 15 ++- .../logging-service/src/db/mod.rs | 80 +----------- .../logging-service/src/main.rs | 6 +- .../logging-service/src/ports/doc_type_api.rs | 106 ++++++++++++++++ .../logging-service/src/ports/mod.rs | 3 +- .../src/services/document_service.rs | 2 +- .../src/services/keyring_service.rs | 2 +- 12 files changed, 458 insertions(+), 102 deletions(-) create mode 100644 clearing-house-app/logging-service/src/db/config/doc_store.rs create mode 100644 clearing-house-app/logging-service/src/db/config/keyring_store.rs create mode 100644 clearing-house-app/logging-service/src/db/config/mod.rs create mode 100644 clearing-house-app/logging-service/src/db/config/process_store.rs rename clearing-house-app/logging-service/src/db/{docstore.rs => doc_store.rs} (95%) rename clearing-house-app/logging-service/src/db/{keystore.rs => key_store.rs} (93%) create mode 100644 clearing-house-app/logging-service/src/ports/doc_type_api.rs diff --git a/clearing-house-app/logging-service/src/db/config/doc_store.rs b/clearing-house-app/logging-service/src/db/config/doc_store.rs new file mode 100644 index 0000000..58e132b --- /dev/null +++ b/clearing-house-app/logging-service/src/db/config/doc_store.rs @@ -0,0 +1,119 @@ +use mongodb::bson::doc; +use mongodb::IndexModel; +use mongodb::options::{CreateCollectionOptions, IndexOptions, WriteConcern}; +use rocket::{Build, fairing, Rocket}; +use core_lib::constants::{CLEAR_DB, DATABASE_URL, DOCUMENT_DB, DOCUMENT_DB_CLIENT, MONGO_COLL_DOCUMENT_BUCKET, MONGO_DOC_ARRAY, MONGO_PID, MONGO_TS, MONGO_TC}; +use core_lib::db::init_database_client; +use core_lib::model::document::Document; +use crate::db::doc_store::DataStore; + +#[derive(Clone, Debug)] +pub struct DatastoreConfigurator; + +#[rocket::async_trait] +impl fairing::Fairing for DatastoreConfigurator { + fn info(&self) -> fairing::Info { + fairing::Info { + name: "Configuring Document Database", + kind: fairing::Kind::Ignite + } + } + async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { + let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); + let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ + Ok(value) => { + debug!("clear_db: '{}' found.", &value); + value + }, + Err(_) => { + false + } + }; + debug!("Using mongodb url: '{:#?}'", &db_url); + match init_database_client::(&db_url.as_str(), Some(DOCUMENT_DB_CLIENT.to_string())).await{ + Ok(datastore) => { + debug!("Check if database is empty..."); + match datastore.client.database(DOCUMENT_DB) + .list_collection_names(None) + .await{ + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + let number_of_colls = match colls.contains(&MONGO_COLL_DOCUMENT_BUCKET.to_string()){ + true => colls.len(), + false => 0 + }; + + if number_of_colls > 0 && clear_db{ + debug!("Database not empty and clear_db == true. Dropping database..."); + match datastore.client.database(DOCUMENT_DB).drop(None).await{ + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + }; + } + if number_of_colls == 0 || clear_db{ + debug!("Database empty. Need to initialize..."); + let mut write_concern = WriteConcern::default(); + write_concern.journal = Some(true); + let mut options = CreateCollectionOptions::default(); + options.write_concern = Some(write_concern); + debug!("Create collection {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore.client.database(DOCUMENT_DB).create_collection(MONGO_COLL_DOCUMENT_BUCKET, options).await{ + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + }; + + // This purpose of this index is to ensure that the transaction counter is unique + let mut index_options = IndexOptions::default(); + index_options.unique = Some(true); + let mut index_model = IndexModel::default(); + index_model.keys = doc!{format!("{}.{}",MONGO_DOC_ARRAY, MONGO_TC): 1}; + index_model.options = Some(index_options); + + debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore.client.database(DOCUMENT_DB).collection::(MONGO_COLL_DOCUMENT_BUCKET).create_index(index_model, None).await{ + Ok(result) => { + debug!("... index {} created", result.index_name); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + } + + // This creates a compound index over pid and the timestamp to enable paging using buckets + let mut compound_index_model = IndexModel::default(); + compound_index_model.keys = doc!{MONGO_PID: 1, MONGO_TS: 1}; + + debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore.client.database(DOCUMENT_DB).collection::(MONGO_COLL_DOCUMENT_BUCKET).create_index(compound_index_model, None).await{ + Ok(result) => { + debug!("... index {} created", result.index_name); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + } + } + debug!("... database initialized."); + Ok(rocket.manage(datastore)) + } + Err(_) => { + Err(rocket) + } + } + }, + Err(_) => Err(rocket) + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/config/keyring_store.rs b/clearing-house-app/logging-service/src/db/config/keyring_store.rs new file mode 100644 index 0000000..ce459c1 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/config/keyring_store.rs @@ -0,0 +1,113 @@ +use anyhow::anyhow; +use rocket::fairing::Kind; +use rocket::{Build, fairing, Rocket}; +use core_lib::constants::{CLEAR_DB, DATABASE_URL, FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT}; +use core_lib::db::init_database_client; +use core_lib::util::read_file; +use crate::db::key_store::KeyStore; +use crate::model::crypto::MasterKey; +use crate::model::doc_type::DocumentType; + +#[derive(Clone, Debug)] +pub struct KeyringDbConfigurator; + +#[rocket::async_trait] +impl fairing::Fairing for KeyringDbConfigurator { + fn info(&self) -> fairing::Info { + fairing::Info { + name: "Configuring Keyring Database", + kind: Kind::Ignite + } + } + async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { + let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); + let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { + Ok(value) => { + debug!("clear_db: '{}' found.", &value); + value + }, + Err(_) => { + false + } + }; + debug!("Using database url: '{:#?}'", &db_url); + + match Self::init_keystore(db_url, clear_db).await { + Ok(keystore) => { + Ok(rocket.manage(keystore)) + }, + Err(_) => { + Err(rocket) + } + } + } +} + +impl KeyringDbConfigurator { + pub async fn init_keystore(db_url: String, clear_db: bool) -> anyhow::Result { + debug!("Using database url: '{:#?}'", &db_url); + + match init_database_client::(&db_url.as_str(), Some(KEYRING_DB_CLIENT.to_string())).await { + Ok(keystore) => { + debug!("Check if database is empty..."); + match keystore.client.database(KEYRING_DB) + .list_collection_names(None) + .await { + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + if colls.len() > 0 && clear_db { + debug!("Database not empty and clear_db == true. Dropping database..."); + match keystore.client.database(KEYRING_DB).drop(None).await { + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to drop database")); + } + }; + } + if colls.len() == 0 || clear_db { + debug!("Database empty. Need to initialize..."); + debug!("Adding initial document type..."); + match serde_json::from_str::(&read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or(String::new())) { + Ok(dt) => { + match keystore.add_document_type(dt).await { + Ok(_) => { + debug!("... done."); + }, + Err(e) => { + error!("Error while adding initial document type: {:#?}", e); + return Err(anyhow!("Error while adding initial document type")); + } + } + } + _ => { + error!("Error while loading initial document type"); + return Err(anyhow!("Error while loading initial document type")); + } + }; + debug!("Creating master key..."); + // create master key + match keystore.store_master_key(MasterKey::new_random()).await { + Ok(true) => { + debug!("... done."); + }, + _ => { + error!("... failed to create master key"); + return Err(anyhow!("Failed to create master key")); + } + }; + } + debug!("... database initialized."); + Ok(keystore) + } + Err(_) => { + Err(anyhow!("Failed to list collections")) + } + } + }, + Err(_) => Err(anyhow!("Failed to initialize database client")) + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/config/mod.rs b/clearing-house-app/logging-service/src/db/config/mod.rs new file mode 100644 index 0000000..5e2b055 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/config/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod process_store; +pub(crate) mod keyring_store; +pub(crate) mod doc_store; \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/config/process_store.rs b/clearing-house-app/logging-service/src/db/config/process_store.rs new file mode 100644 index 0000000..2e16c63 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/config/process_store.rs @@ -0,0 +1,81 @@ +use mongodb::options::{CreateCollectionOptions, WriteConcern}; +use rocket::{Build, Rocket}; +use rocket::fairing::Kind; +use core_lib::constants::{CLEAR_DB, DATABASE_URL, MONGO_COLL_TRANSACTIONS, PROCESS_DB, PROCESS_DB_CLIENT}; +use core_lib::db::init_database_client; +use crate::db::ProcessStore; + +#[derive(Clone, Debug)] +pub struct ProcessStoreConfigurator; + +#[rocket::async_trait] +impl rocket::fairing::Fairing for ProcessStoreConfigurator { + fn info(&self) -> rocket::fairing::Info { + rocket::fairing::Info { + name: "Configuring Process Database", + kind: Kind::Ignite + } + } + async fn on_ignite(&self, rocket: Rocket) -> rocket::fairing::Result { + debug!("Preparing to initialize database..."); + let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); + let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ + Ok(value) => { + debug!("...clear_db: {} found. ", &value); + value + }, + Err(_) => { + false + } + }; + debug!("...using database url: '{:#?}'", &db_url); + + match init_database_client::(&db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string())).await{ + Ok(process_store) => { + debug!("...check if database is empty..."); + match process_store.client.database(PROCESS_DB) + .list_collection_names(None) + .await{ + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + if colls.len() > 0 && clear_db{ + debug!("...database not empty and clear_db == true. Dropping database..."); + match process_store.client.database(PROCESS_DB).drop(None).await{ + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + }; + } + if colls.len() == 0 || clear_db{ + debug!("..database empty. Need to initialize..."); + let mut write_concern = WriteConcern::default(); + write_concern.journal = Some(true); + let mut options = CreateCollectionOptions::default(); + options.write_concern = Some(write_concern); + debug!("...create collection {} ...", MONGO_COLL_TRANSACTIONS); + match process_store.client.database(PROCESS_DB).create_collection(MONGO_COLL_TRANSACTIONS, options).await{ + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(rocket); + } + }; + } + debug!("... database initialized."); + Ok(rocket.manage(process_store)) + } + Err(_) => { + Err(rocket) + } + } + }, + Err(_) => Err(rocket) + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/docstore.rs b/clearing-house-app/logging-service/src/db/doc_store.rs similarity index 95% rename from clearing-house-app/logging-service/src/db/docstore.rs rename to clearing-house-app/logging-service/src/db/doc_store.rs index 4a22827..7d39f21 100644 --- a/clearing-house-app/logging-service/src/db/docstore.rs +++ b/clearing-house-app/logging-service/src/db/doc_store.rs @@ -1,31 +1,29 @@ -use mongodb::bson; +use mongodb::{bson, Client}; use mongodb::bson::doc; use mongodb::options::{AggregateOptions, UpdateOptions}; use rocket::futures::StreamExt; -use core_lib::constants::{ - MAX_NUM_RESPONSE_ENTRIES, - MONGO_COLL_DOCUMENT_BUCKET, - MONGO_ID, - MONGO_PID, - MONGO_COUNTER, - MONGO_DOC_ARRAY, - MONGO_DT_ID, - MONGO_FROM_TS, - MONGO_TO_TS, - MONGO_TC, - MONGO_TS, -}; +use core_lib::constants::{MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_ID, MONGO_PID, MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_TO_TS, MONGO_TC, MONGO_TS, DOCUMENT_DB}; +use core_lib::db::DataStoreApi; use core_lib::model::document::EncryptedDocument; use core_lib::errors::*; use core_lib::model::SortingOrder; -use crate::db::docstore::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; +use crate::db::doc_store::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; #[derive(Clone)] pub struct DataStore { - client: mongodb::Client, + pub(crate) client: mongodb::Client, database: mongodb::Database, } +impl DataStoreApi for DataStore { + fn new(client: Client) -> DataStore{ + DataStore { + client: client.clone(), + database: client.database(DOCUMENT_DB) + } + } +} + impl DataStore { pub async fn add_document(&self, doc: EncryptedDocument) -> Result { debug!("add_document to bucket"); diff --git a/clearing-house-app/logging-service/src/db/keystore.rs b/clearing-house-app/logging-service/src/db/key_store.rs similarity index 93% rename from clearing-house-app/logging-service/src/db/keystore.rs rename to clearing-house-app/logging-service/src/db/key_store.rs index 7705b01..231f635 100644 --- a/clearing-house-app/logging-service/src/db/keystore.rs +++ b/clearing-house-app/logging-service/src/db/key_store.rs @@ -1,17 +1,28 @@ use std::process::exit; use core_lib::errors::*; use mongodb::bson::doc; +use mongodb::Client; use rocket::futures::TryStreamExt; -use core_lib::constants::{MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; +use core_lib::constants::{KEYRING_DB, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; +use core_lib::db::DataStoreApi; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; #[derive(Clone, Debug)] pub struct KeyStore { - client: mongodb::Client, + pub(crate) client: mongodb::Client, database: mongodb::Database } +impl DataStoreApi for KeyStore { + fn new(client: Client) -> KeyStore{ + KeyStore { + client: client.clone(), + database: client.database(KEYRING_DB) + } + } +} + impl KeyStore { /// Only one master key may exist in the database. diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/logging-service/src/db/mod.rs index 3451022..2c6752d 100644 --- a/clearing-house-app/logging-service/src/db/mod.rs +++ b/clearing-house-app/logging-service/src/db/mod.rs @@ -1,5 +1,6 @@ -pub(crate) mod keystore; -pub(crate) mod docstore; +pub(crate) mod key_store; +pub(crate) mod doc_store; +pub(crate) mod config; use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, DATABASE_URL, CLEAR_DB, PROCESS_DB, PROCESS_DB_CLIENT, MONGO_COLL_TRANSACTIONS, MONGO_TC}; use core_lib::db::{DataStoreApi, init_database_client}; @@ -13,81 +14,6 @@ use rocket::{Rocket, Build}; use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions, WriteConcern, CreateCollectionOptions}; use crate::model::TransactionCounter; -#[derive(Clone, Debug)] -pub struct ProcessStoreConfigurator; - -#[rocket::async_trait] -impl Fairing for ProcessStoreConfigurator { - fn info(&self) -> Info { - Info { - name: "Configuring Process Database", - kind: Kind::Ignite - } - } - async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - debug!("Preparing to initialize database..."); - let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ - Ok(value) => { - debug!("...clear_db: {} found. ", &value); - value - }, - Err(_) => { - false - } - }; - debug!("...using database url: '{:#?}'", &db_url); - - match init_database_client::(&db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string())).await{ - Ok(process_store) => { - debug!("...check if database is empty..."); - match process_store.client.database(PROCESS_DB) - .list_collection_names(None) - .await{ - Ok(colls) => { - debug!("... found collections: {:#?}", &colls); - if colls.len() > 0 && clear_db{ - debug!("...database not empty and clear_db == true. Dropping database..."); - match process_store.client.database(PROCESS_DB).drop(None).await{ - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - }; - } - if colls.len() == 0 || clear_db{ - debug!("..database empty. Need to initialize..."); - let mut write_concern = WriteConcern::default(); - write_concern.journal = Some(true); - let mut options = CreateCollectionOptions::default(); - options.write_concern = Some(write_concern); - debug!("...create collection {} ...", MONGO_COLL_TRANSACTIONS); - match process_store.client.database(PROCESS_DB).create_collection(MONGO_COLL_TRANSACTIONS, options).await{ - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - }; - } - debug!("... database initialized."); - Ok(rocket.manage(process_store)) - } - Err(_) => { - Err(rocket) - } - } - }, - Err(_) => Err(rocket) - } - } -} - #[derive(Clone)] pub struct ProcessStore { client: Client, diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 62fcf31..207b36f 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -4,13 +4,12 @@ #[macro_use] extern crate serde_derive; use std::path::Path; -use core_lib::api::client::{ApiClientConfigurator, ApiClientEnum}; use core_lib::util::{add_service_config, setup_logger}; use rocket::{Build, Rocket}; use rocket::fairing::AdHoc; use core_lib::constants::ENV_LOGGING_SERVICE_ID; -use db::ProcessStoreConfigurator; +use db::config::process_store::ProcessStoreConfigurator; use model::constants::SIGNING_KEY; pub mod db; @@ -41,7 +40,6 @@ fn rocket() -> Rocket { .attach(ProcessStoreConfigurator) .attach(add_signing_key()) .attach(add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) - .attach(ApiClientConfigurator::new(ApiClientEnum::Document)) - .attach(ApiClientConfigurator::new(ApiClientEnum::Keyring)) .attach(ports::logging_api::mount_api()) + .attach(ports::doc_type_api::mount_api()) } diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs new file mode 100644 index 0000000..af5a082 --- /dev/null +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -0,0 +1,106 @@ +use core_lib::api::ApiResponse; +use core_lib::constants::{ROCKET_DOC_TYPE_API, DEFAULT_PROCESS_ID}; +use rocket::fairing::AdHoc; +use rocket::State; +use rocket::serde::json::{json,Json}; + +use crate::db::key_store::KeyStore; +use crate::model::doc_type::DocumentType; + +#[post("/", format = "json", data = "")] +async fn create_doc_type(db: &State, doc_type: Json) -> ApiResponse { + let doc_type: DocumentType = doc_type.into_inner(); + debug!("adding doctype: {:?}", &doc_type); + match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + Ok(true) => ApiResponse::BadRequest(String::from("doctype already exists!")), + Ok(false) => { + match db.add_document_type(doc_type.clone()).await{ + Ok(()) => ApiResponse::SuccessCreate(json!(doc_type)), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + return ApiResponse::InternalError(String::from("Error while adding document type!")) + } + } + }, + Err(e) => { + error!("Error while adding document type: {:?}", e); + return ApiResponse::InternalError(String::from("Error while checking database!")) + } + } +} + +#[post("/", format = "json", data = "")] +async fn update_doc_type(db: &State, id: String, doc_type: Json) -> ApiResponse { + let doc_type: DocumentType = doc_type.into_inner(); + match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + Ok(true) => ApiResponse::BadRequest(String::from("Doctype already exists!")), + Ok(false) => { + match db.update_document_type(doc_type, &id).await{ + Ok(id) => ApiResponse::SuccessOk(json!(id)), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + return ApiResponse::InternalError(String::from("Error while storing document type!")) + } + } + }, + Err(e) => { + error!("Error while adding document type: {:?}", e); + return ApiResponse::InternalError(String::from("Error while checking database!")) + } + } +} + +#[delete("/", format = "json")] +async fn delete_default_doc_type(db: &State, id: String) -> ApiResponse{ + delete_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await +} + +#[delete("//", format = "json")] +async fn delete_doc_type(db: &State, id: String, pid: String) -> ApiResponse{ + match db.delete_document_type(&id, &pid).await{ + Ok(true) => ApiResponse::SuccessNoContent(String::from("Document type deleted!")), + Ok(false) => ApiResponse::NotFound(String::from("Document type does not exist!")), + Err(e) => { + error!("Error while deleting doctype: {:?}", e); + ApiResponse::InternalError(format!("Error while deleting document type with id {}!", id)) + } + } +} + +#[get("/", format = "json")] +async fn get_default_doc_type(db: &State, id: String) -> ApiResponse { + get_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await +} + +#[get("//", format = "json")] +async fn get_doc_type(db: &State, id: String, pid: String) -> ApiResponse { + match db.get_document_type(&id).await{ + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(dt) => ApiResponse::SuccessOk(json!(dt)), + Err(e) => { + error!("Error while retrieving doctype: {:?}", e); + ApiResponse::InternalError(format!("Error while retrieving document type with id {} and pid {}!", id, pid)) + } + } +} + +#[get("/", format = "json")] +async fn get_doc_types(db: &State) -> ApiResponse { + match db.get_all_document_types().await { + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(dt) => ApiResponse::SuccessOk(json!(dt)), + Err(e) => { + error!("Error while retrieving default doctypes: {:?}", e); + ApiResponse::InternalError(format!("Error while retrieving all document types")) + } + } +} + +pub fn mount_api() -> AdHoc { + AdHoc::on_ignite("Mounting Document Type API", |rocket| async { + rocket + .mount(ROCKET_DOC_TYPE_API, routes![create_doc_type, + update_doc_type, delete_default_doc_type, delete_doc_type, + get_default_doc_type, get_doc_type , get_doc_types]) + }) +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/logging-service/src/ports/mod.rs index 9e62cb4..9948361 100644 --- a/clearing-house-app/logging-service/src/ports/mod.rs +++ b/clearing-house-app/logging-service/src/ports/mod.rs @@ -3,4 +3,5 @@ //! This module contains the ports of the logging service. Ports are used to communicate with other //! services. In this case, the logging service implements REST-API endpoints to provide access to //! the logging service. -pub mod logging_api; +pub(crate) mod logging_api; +pub(crate) mod doc_type_api; diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 1261d25..8abda1c 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -7,7 +7,7 @@ use core_lib::model::document::Document; use core_lib::model::{parse_date, sanitize_dates, SortingOrder, validate_dates}; use core_lib::model::crypto::{KeyCt, KeyCtList}; use crate::services::keyring_service::KeyringService; -use crate::db::docstore::DataStore; +use crate::db::doc_store::DataStore; pub struct DocumentService { db: DataStore, diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index 0937beb..ff3962b 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -3,7 +3,7 @@ use core_lib::api::crypto::ChClaims; use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::crypto; use crate::crypto::restore_key_map; -use crate::db::keystore::KeyStore; +use crate::db::key_store::KeyStore; pub struct KeyringService { db: KeyStore, From 729494543971b8650ee72942c67ca48a658bf62f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 11:55:32 +0200 Subject: [PATCH 030/183] refactor(ch-app): Separated the remaining rocket fairings logic into separate function --- .../src/db/config/doc_store.rs | 27 ++++++++++++++----- .../src/db/config/keyring_store.rs | 1 - .../src/db/config/process_store.rs | 25 +++++++++++++---- .../logging-service/src/db/mod.rs | 9 +++---- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/clearing-house-app/logging-service/src/db/config/doc_store.rs b/clearing-house-app/logging-service/src/db/config/doc_store.rs index 58e132b..81c51e9 100644 --- a/clearing-house-app/logging-service/src/db/config/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/config/doc_store.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use mongodb::bson::doc; use mongodb::IndexModel; use mongodb::options::{CreateCollectionOptions, IndexOptions, WriteConcern}; @@ -29,6 +30,18 @@ impl fairing::Fairing for DatastoreConfigurator { false } }; + + match Self::init_datastore(db_url, clear_db).await { + Ok(datastore) => { + Ok(rocket.manage(datastore)) + }, + Err(_) => Err(rocket) + } + } +} + +impl DatastoreConfigurator { + pub async fn init_datastore(db_url: String, clear_db: bool) -> anyhow::Result { debug!("Using mongodb url: '{:#?}'", &db_url); match init_database_client::(&db_url.as_str(), Some(DOCUMENT_DB_CLIENT.to_string())).await{ Ok(datastore) => { @@ -51,7 +64,7 @@ impl fairing::Fairing for DatastoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to drop database")); } }; } @@ -68,7 +81,7 @@ impl fairing::Fairing for DatastoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to create collection")); } }; @@ -86,7 +99,7 @@ impl fairing::Fairing for DatastoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to create index")); } } @@ -101,19 +114,19 @@ impl fairing::Fairing for DatastoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to create compound index")); } } } debug!("... database initialized."); - Ok(rocket.manage(datastore)) + Ok(datastore) } Err(_) => { - Err(rocket) + Err(anyhow!("Failed to list collections")) } } }, - Err(_) => Err(rocket) + Err(_) => Err(anyhow!("Failed to initialize database client")) } } } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/config/keyring_store.rs b/clearing-house-app/logging-service/src/db/config/keyring_store.rs index ce459c1..cf83a77 100644 --- a/clearing-house-app/logging-service/src/db/config/keyring_store.rs +++ b/clearing-house-app/logging-service/src/db/config/keyring_store.rs @@ -30,7 +30,6 @@ impl fairing::Fairing for KeyringDbConfigurator { false } }; - debug!("Using database url: '{:#?}'", &db_url); match Self::init_keystore(db_url, clear_db).await { Ok(keystore) => { diff --git a/clearing-house-app/logging-service/src/db/config/process_store.rs b/clearing-house-app/logging-service/src/db/config/process_store.rs index 2e16c63..e0fb2e7 100644 --- a/clearing-house-app/logging-service/src/db/config/process_store.rs +++ b/clearing-house-app/logging-service/src/db/config/process_store.rs @@ -1,3 +1,4 @@ +use anyhow::anyhow; use mongodb::options::{CreateCollectionOptions, WriteConcern}; use rocket::{Build, Rocket}; use rocket::fairing::Kind; @@ -28,6 +29,19 @@ impl rocket::fairing::Fairing for ProcessStoreConfigurator { false } }; + + match Self::init_process_store(db_url, clear_db).await { + Ok(process_store) => { + debug!("...done."); + Ok(rocket.manage(process_store)) + }, + Err(_) => Err(rocket) + } + } +} + +impl ProcessStoreConfigurator { + pub async fn init_process_store(db_url: String, clear_db: bool) -> anyhow::Result { debug!("...using database url: '{:#?}'", &db_url); match init_database_client::(&db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string())).await{ @@ -46,7 +60,7 @@ impl rocket::fairing::Fairing for ProcessStoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to drop database")); } }; } @@ -63,19 +77,20 @@ impl rocket::fairing::Fairing for ProcessStoreConfigurator { } Err(_) => { debug!("... failed."); - return Err(rocket); + return Err(anyhow!("Failed to create collection")); } }; } debug!("... database initialized."); - Ok(rocket.manage(process_store)) + Ok(process_store) } Err(_) => { - Err(rocket) + Err(anyhow!("Failed to list collections")) } } }, - Err(_) => Err(rocket) + Err(_) => Err(anyhow!("Failed to initialize database client")) } } + } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/logging-service/src/db/mod.rs index 2c6752d..8276c20 100644 --- a/clearing-house-app/logging-service/src/db/mod.rs +++ b/clearing-house-app/logging-service/src/db/mod.rs @@ -2,16 +2,15 @@ pub(crate) mod key_store; pub(crate) mod doc_store; pub(crate) mod config; -use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, DATABASE_URL, CLEAR_DB, PROCESS_DB, PROCESS_DB_CLIENT, MONGO_COLL_TRANSACTIONS, MONGO_TC}; -use core_lib::db::{DataStoreApi, init_database_client}; +use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, PROCESS_DB, MONGO_COLL_TRANSACTIONS, MONGO_TC}; +use core_lib::db::DataStoreApi; use core_lib::errors::*; use core_lib::model::process::Process; use mongodb::bson::doc; use mongodb::{Client, Database}; -use rocket::fairing::{self, Fairing, Info, Kind}; +use rocket::fairing::Fairing; use rocket::futures::TryStreamExt; -use rocket::{Rocket, Build}; -use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions, WriteConcern, CreateCollectionOptions}; +use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions}; use crate::model::TransactionCounter; #[derive(Clone)] From d2c9adeabfe38c36c77be7d7f12157fffc76a4dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 12:26:08 +0200 Subject: [PATCH 031/183] refactor(ch-app): Refactor doc_api and separated it likewise over ports and services --- .../logging-service/src/ports/doc_type_api.rs | 83 +++++++------------ .../src/services/keyring_service.rs | 73 ++++++++++++++++ .../logging-service/src/services/mod.rs | 2 +- 3 files changed, 106 insertions(+), 52 deletions(-) diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs index af5a082..4f4d5bb 100644 --- a/clearing-house-app/logging-service/src/ports/doc_type_api.rs +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -4,94 +4,75 @@ use rocket::fairing::AdHoc; use rocket::State; use rocket::serde::json::{json,Json}; -use crate::db::key_store::KeyStore; +use crate::services::keyring_service::KeyringService; use crate::model::doc_type::DocumentType; #[post("/", format = "json", data = "")] -async fn create_doc_type(db: &State, doc_type: Json) -> ApiResponse { - let doc_type: DocumentType = doc_type.into_inner(); - debug!("adding doctype: {:?}", &doc_type); - match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ - Ok(true) => ApiResponse::BadRequest(String::from("doctype already exists!")), - Ok(false) => { - match db.add_document_type(doc_type.clone()).await{ - Ok(()) => ApiResponse::SuccessCreate(json!(doc_type)), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(String::from("Error while adding document type!")) - } - } - }, +async fn create_doc_type(key_api: &State, doc_type: Json) -> ApiResponse { + match key_api.inner().create_doc_type(doc_type.into_inner()).await{ + Ok(dt) => ApiResponse::SuccessCreate(json!(dt)), Err(e) => { - error!("Error while adding document type: {:?}", e); - return ApiResponse::InternalError(String::from("Error while checking database!")) + error!("Error while adding doctype: {:?}", e); + return ApiResponse::InternalError(e.to_string()) } } } #[post("/", format = "json", data = "")] -async fn update_doc_type(db: &State, id: String, doc_type: Json) -> ApiResponse { - let doc_type: DocumentType = doc_type.into_inner(); - match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ - Ok(true) => ApiResponse::BadRequest(String::from("Doctype already exists!")), - Ok(false) => { - match db.update_document_type(doc_type, &id).await{ - Ok(id) => ApiResponse::SuccessOk(json!(id)), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(String::from("Error while storing document type!")) - } - } - }, +async fn update_doc_type(key_api: &State, id: String, doc_type: Json) -> ApiResponse { + match key_api.inner().update_doc_type(id, doc_type.into_inner()).await{ + Ok(id) => ApiResponse::SuccessOk(json!(id)), Err(e) => { - error!("Error while adding document type: {:?}", e); - return ApiResponse::InternalError(String::from("Error while checking database!")) + error!("Error while adding doctype: {:?}", e); + return ApiResponse::InternalError(e.to_string()) } } } #[delete("/", format = "json")] -async fn delete_default_doc_type(db: &State, id: String) -> ApiResponse{ - delete_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await +async fn delete_default_doc_type(key_api: &State, id: String) -> ApiResponse{ + delete_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } #[delete("//", format = "json")] -async fn delete_doc_type(db: &State, id: String, pid: String) -> ApiResponse{ - match db.delete_document_type(&id, &pid).await{ - Ok(true) => ApiResponse::SuccessNoContent(String::from("Document type deleted!")), - Ok(false) => ApiResponse::NotFound(String::from("Document type does not exist!")), +async fn delete_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse{ + match key_api.inner().delete_doc_type(id, pid).await{ + Ok(id) => ApiResponse::SuccessOk(json!(id)), Err(e) => { error!("Error while deleting doctype: {:?}", e); - ApiResponse::InternalError(format!("Error while deleting document type with id {}!", id)) + return ApiResponse::InternalError(e.to_string()) } } } #[get("/", format = "json")] -async fn get_default_doc_type(db: &State, id: String) -> ApiResponse { - get_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await +async fn get_default_doc_type(key_api: &State, id: String) -> ApiResponse { + get_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } #[get("//", format = "json")] -async fn get_doc_type(db: &State, id: String, pid: String) -> ApiResponse { - match db.get_document_type(&id).await{ - //TODO: would like to send "{}" instead of "null" when dt is not found - Ok(dt) => ApiResponse::SuccessOk(json!(dt)), +async fn get_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse { + match key_api.inner().get_doc_type(id, pid).await{ + Ok(dt) => { + match dt{ + Some(dt) => ApiResponse::SuccessOk(json!(dt)), + None => ApiResponse::SuccessOk(json!(null)) + } + }, Err(e) => { error!("Error while retrieving doctype: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving document type with id {} and pid {}!", id, pid)) + return ApiResponse::InternalError(e.to_string()) } } } #[get("/", format = "json")] -async fn get_doc_types(db: &State) -> ApiResponse { - match db.get_all_document_types().await { - //TODO: would like to send "{}" instead of "null" when dt is not found +async fn get_doc_types(key_api: &State) -> ApiResponse { + match key_api.inner().get_doc_types().await{ Ok(dt) => ApiResponse::SuccessOk(json!(dt)), Err(e) => { - error!("Error while retrieving default doctypes: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving all document types")) + error!("Error while retrieving doctypes: {:?}", e); + return ApiResponse::InternalError(e.to_string()) } } } diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index ff3962b..fd250e2 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -4,6 +4,7 @@ use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::crypto; use crate::crypto::restore_key_map; use crate::db::key_store::KeyStore; +use crate::model::doc_type::DocumentType; pub struct KeyringService { db: KeyStore, @@ -163,4 +164,76 @@ impl KeyringService { pub(crate) async fn decrypt_multiple_keys(&self, ch_claims: ChClaims, pid: Option, cts: &KeyCtList) -> anyhow::Result> { self.decrypt_keys(ch_claims, pid, cts).await } + + pub(crate) async fn create_doc_type(&self, doc_type: DocumentType) -> anyhow::Result { + debug!("adding doctype: {:?}", &doc_type); + match self.db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + Ok(true) => Err(anyhow!("doctype already exists!")), // BadRequest + Ok(false) => { + match self.db.add_document_type(doc_type.clone()).await{ + Ok(()) => Ok(doc_type), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + return Err(anyhow!("Error while adding document type!")) // InternalError + } + } + }, + Err(e) => { + error!("Error while adding document type: {:?}", e); + return Err(anyhow!("Error while checking database!")) // InternalError + } + } + } + + pub(crate) async fn update_doc_type(&self, id: String, doc_type: DocumentType) -> anyhow::Result { + match self.db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + Ok(true) => Err(anyhow!("Doctype already exists!")), // BadRequest + Ok(false) => { + match self.db.update_document_type(doc_type, &id).await{ + Ok(id) => Ok(id), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + return Err(anyhow!("Error while storing document type!")) // InternalError + } + } + }, + Err(e) => { + error!("Error while adding document type: {:?}", e); + return Err(anyhow!("Error while checking database!")) // InternalError + } + } + } + + pub(crate) async fn delete_doc_type(&self, id: String, pid: String) -> anyhow::Result{ + match self.db.delete_document_type(&id, &pid).await{ + Ok(true) => Ok(String::from("Document type deleted!")), // NoContent + Ok(false) => Err(anyhow!("Document type does not exist!")), // NotFound + Err(e) => { + error!("Error while deleting doctype: {:?}", e); + Err(anyhow!("Error while deleting document type with id {}!", id)) // InternalError + } + } + } + + pub(crate) async fn get_doc_type(&self, id: String, pid: String) -> anyhow::Result> { + match self.db.get_document_type(&id).await{ + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(dt) => Ok(dt), + Err(e) => { + error!("Error while retrieving doctype: {:?}", e); + Err(anyhow!("Error while retrieving document type with id {} and pid {}!", id, pid)) // InternalError + } + } + } + + pub(crate) async fn get_doc_types(&self) -> anyhow::Result> { + match self.db.get_all_document_types().await { + //TODO: would like to send "{}" instead of "null" when dt is not found + Ok(dt) => Ok(dt), + Err(e) => { + error!("Error while retrieving default doctypes: {:?}", e); + Err(anyhow!("Error while retrieving all document types")) // InternalError + } + } + } } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs index a1b5a37..cb72058 100644 --- a/clearing-house-app/logging-service/src/services/mod.rs +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -4,6 +4,6 @@ //! responsible for the business logic of the application. The services are used by the API //! Controllers to handle the requests and responses. //! -mod keyring_service; +pub(crate) mod keyring_service; mod document_service; pub(crate) mod logging_service; \ No newline at end of file From fa5c6f24b8ca2b3ed8ac9291d294ed67ce0911f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 14:30:09 +0200 Subject: [PATCH 032/183] refactor(ch-app): Refactor config from rocket to config-rs --- clearing-house-app/Cargo.lock | 48 +++++++++++- clearing-house-app/logging-service/Cargo.toml | 3 +- .../logging-service/config.toml | 4 + .../init_db/default_doc_type.json | 42 ++++++++++ .../logging-service/src/db/doc_store.rs | 6 +- .../logging-service/src/main.rs | 76 +++++++++++++++---- .../logging-service/src/model/crypto.rs | 2 +- .../logging-service/src/model/ids/message.rs | 2 +- .../logging-service/src/model/ids/mod.rs | 14 ++-- .../logging-service/src/model/ids/request.rs | 2 +- .../logging-service/src/model/mod.rs | 8 +- .../src/services/document_service.rs | 6 ++ .../src/services/keyring_service.rs | 7 ++ .../src/services/logging_service.rs | 8 ++ .../logging-service/src/services/mod.rs | 2 +- 15 files changed, 194 insertions(+), 36 deletions(-) create mode 100644 clearing-house-app/logging-service/config.toml create mode 100644 clearing-house-app/logging-service/init_db/default_doc_type.json diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 673600d..e9395ea 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -399,6 +399,20 @@ dependencies = [ "inout", ] +[[package]] +name = "config" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +dependencies = [ + "async-trait", + "lazy_static", + "nom", + "pathdiff", + "serde", + "toml 0.5.11", +] + [[package]] name = "constant_time_eq" version = "0.1.5" @@ -751,7 +765,7 @@ dependencies = [ "pear", "serde", "serde_yaml", - "toml", + "toml 0.7.6", "uncased", "version_check", ] @@ -1303,6 +1317,7 @@ dependencies = [ "anyhow", "biscuit", "chrono", + "config", "core-lib", "error-chain", "fern", @@ -1397,6 +1412,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1508,6 +1529,16 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1665,6 +1696,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pbkdf2" version = "0.11.0" @@ -2674,6 +2711,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.7.6" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index ccd6294..0e9ebf9 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -20,7 +20,7 @@ rocket = { version = "0.5.0-rc.1", features = ["json"] } serde = "1.0" serde_derive = "1.0" serde_json = "1.0" -anyhow = "1.0.73" +anyhow = "1" hex = "0.4.3" aes = "0.8.3" aes-gcm-siv = "0.11.1" @@ -28,3 +28,4 @@ hkdf = "0.12.3" sha2 = "0.10.7" generic-array = "0.14.7" openssl = "0.10.56" +config = { version = "0.13.3", default-features = false, features = ["toml"] } diff --git a/clearing-house-app/logging-service/config.toml b/clearing-house-app/logging-service/config.toml new file mode 100644 index 0000000..5e89afe --- /dev/null +++ b/clearing-house-app/logging-service/config.toml @@ -0,0 +1,4 @@ +document_database_url= "mongodb://localhost:27017" +process_database_url= "mongodb://localhost:27017" +keyring_database_url= "mongodb://localhost:27017" +clear_db = true \ No newline at end of file diff --git a/clearing-house-app/logging-service/init_db/default_doc_type.json b/clearing-house-app/logging-service/init_db/default_doc_type.json new file mode 100644 index 0000000..5e1f843 --- /dev/null +++ b/clearing-house-app/logging-service/init_db/default_doc_type.json @@ -0,0 +1,42 @@ +{ + "id": "IDS_MESSAGE", + "pid": "default", + "parts": [ + { + "name": "model_version" + }, + { + "name": "correlation_message" + }, + { + "name": "transfer_contract" + }, + { + "name": "issued" + }, + { + "name": "issuer_connector" + }, + { + "name": "content_version" + }, + { + "name": "recipient_connector" + }, + { + "name": "sender_agent" + }, + { + "name": "recipient_agent" + }, + { + "name": "payload" + }, + { + "name": "payload_type" + }, + { + "name": "message_id" + } + ] +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/doc_store.rs b/clearing-house-app/logging-service/src/db/doc_store.rs index 7d39f21..7babbf0 100644 --- a/clearing-house-app/logging-service/src/db/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/doc_store.rs @@ -266,7 +266,7 @@ impl DataStore { mod bucket { use core_lib::model::document::EncryptedDocument; - #[derive(Clone, Debug, Serialize, Deserialize)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentBucket { pub counter: u64, pub pid: String, @@ -276,13 +276,13 @@ mod bucket { pub documents: Vec, } - #[derive(Clone, Debug, Serialize, Deserialize)] + #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentBucketSize { pub capacity: i32, pub size: i32, } - #[derive(Debug, Clone, Serialize, Deserialize)] + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DocumentBucketUpdate { pub id: String, pub ts: i64, diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 207b36f..c3fe97e 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -1,19 +1,19 @@ #![forbid(unsafe_code)] -#[macro_use] extern crate rocket; -#[macro_use] extern crate serde_derive; +#[macro_use] +extern crate rocket; use std::path::Path; -use core_lib::util::{add_service_config, setup_logger}; -use rocket::{Build, Rocket}; +use core_lib::util::{add_service_config}; use rocket::fairing::AdHoc; use core_lib::constants::ENV_LOGGING_SERVICE_ID; - +use db::config::doc_store::DatastoreConfigurator; +use db::config::keyring_store::KeyringDbConfigurator; use db::config::process_store::ProcessStoreConfigurator; use model::constants::SIGNING_KEY; -pub mod db; -pub mod model; +mod db; +mod model; mod services; mod crypto; mod ports; @@ -21,25 +21,69 @@ mod ports; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { let private_key_path = rocket.figment().extract_inner(SIGNING_KEY).unwrap_or(String::from("keys/private_key.der")); - if Path::new(&private_key_path).exists(){ + if Path::new(&private_key_path).exists() { Ok(rocket.manage(private_key_path)) - } - else{ + } else { error!("Signing key not found! Aborting startup! Please configure signing_key!"); - return Err(rocket) + return Err(rocket); } }) } -#[launch] -fn rocket() -> Rocket { +#[derive(Debug, serde::Deserialize)] +struct CHConfig { + process_database_url: String, + keyring_database_url: String, + document_database_url: String, + clear_db: bool, +} + +#[rocket::main] +async fn main() -> Result<(), rocket::Error> { + // Read configuration + let conf = config::Config::builder() + .add_source(config::File::with_name("config.toml")) + .add_source(config::Environment::with_prefix("CH_APP_")) + .build() + .expect("Failure to read configuration! Exiting..."); + // setup logging - setup_logger().expect("Failure to set up the logger! Exiting..."); + // TODO: Setup tracing_subscriber - rocket::build() - .attach(ProcessStoreConfigurator) + let conf: CHConfig = conf.try_deserialize().expect("Failure to read configuration! Exiting..."); + println!("Config: {:?}", conf); + + + let process_store = + ProcessStoreConfigurator::init_process_store(String::from(conf.process_database_url), conf.clear_db) + .await + .expect("Failure to initialize process store! Exiting..."); + let keyring_store = + KeyringDbConfigurator::init_keystore(String::from(conf.keyring_database_url), conf.clear_db) + .await + .expect("Failure to initialize keyring store! Exiting..."); + let doc_store = + DatastoreConfigurator::init_datastore(String::from(conf.document_database_url), conf.clear_db) + .await + .expect("Failure to initialize document store! Exiting..."); + + let keyring_service = services::keyring_service::KeyringService::new(keyring_store); + let doc_service = services::document_service::DocumentService::new(doc_store, keyring_service.clone()); + let logging_service = services::logging_service::LoggingService::new( + process_store, + doc_service.clone(), + ); + + let _rocket = rocket::build() + .manage(keyring_service) + .manage(doc_service) + .manage(logging_service) .attach(add_signing_key()) .attach(add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) .attach(ports::logging_api::mount_api()) .attach(ports::doc_type_api::mount_api()) + .ignite().await? + .launch().await?; + + Ok(()) } diff --git a/clearing-house-app/logging-service/src/model/crypto.rs b/clearing-house-app/logging-service/src/model/crypto.rs index a700c76..f45b9a6 100644 --- a/clearing-house-app/logging-service/src/model/crypto.rs +++ b/clearing-house-app/logging-service/src/model/crypto.rs @@ -3,7 +3,7 @@ use hkdf::Hkdf; use sha2::Sha256; use core_lib::model::new_uuid; -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct MasterKey { pub id: String, pub key: String, diff --git a/clearing-house-app/logging-service/src/model/ids/message.rs b/clearing-house-app/logging-service/src/model/ids/message.rs index 0c47465..4f02b0a 100644 --- a/clearing-house-app/logging-service/src/model/ids/message.rs +++ b/clearing-house-app/logging-service/src/model/ids/message.rs @@ -20,7 +20,7 @@ pub const RESULT_MESSAGE: &'static str = "ResultMessage"; pub const REJECTION_MESSAGE: &'static str = "RejectionMessage"; pub const MESSAGE_PROC_NOTIFICATION_MESSAGE: &'static str = "MessageProcessedNotificationMessage"; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct IdsMessage { //IDS name #[serde(rename = "@context")] diff --git a/clearing-house-app/logging-service/src/model/ids/mod.rs b/clearing-house-app/logging-service/src/model/ids/mod.rs index e143889..fa211bb 100644 --- a/clearing-house-app/logging-service/src/model/ids/mod.rs +++ b/clearing-house-app/logging-service/src/model/ids/mod.rs @@ -6,7 +6,7 @@ use crate::model::ids::message::IdsMessage; pub mod message; pub mod request; -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct InfoModelComplexId { //IDS name #[serde(rename = "@id", alias="id", skip_serializing_if = "Option::is_none")] @@ -36,7 +36,7 @@ impl From for InfoModelComplexId { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(untagged)] pub enum InfoModelId { SimpleId(String), @@ -67,7 +67,7 @@ impl From for InfoModelId { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] #[serde(untagged)] pub enum InfoModelDateTime { ComplexTime(InfoModelTimeStamp), @@ -93,7 +93,7 @@ impl fmt::Display for InfoModelDateTime { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct InfoModelTimeStamp { //IDS name #[serde(rename = "@type", alias="type", skip_serializing_if = "Option::is_none")] @@ -123,7 +123,7 @@ impl Display for InfoModelTimeStamp { } } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub enum MessageType { #[serde(rename = "ids:Message")] Message, @@ -147,7 +147,7 @@ pub enum MessageType { Other, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SecurityToken { //IDS name #[serde(rename = "@type")] @@ -175,7 +175,7 @@ impl SecurityToken { } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct IdsQueryResult{ pub date_from: String, pub date_to: String, diff --git a/clearing-house-app/logging-service/src/model/ids/request.rs b/clearing-house-app/logging-service/src/model/ids/request.rs index e56067d..e332c2d 100644 --- a/clearing-house-app/logging-service/src/model/ids/request.rs +++ b/clearing-house-app/logging-service/src/model/ids/request.rs @@ -1,6 +1,6 @@ use crate::model::ids::message::IdsMessage; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ClearingHouseMessage { pub header: IdsMessage, pub payload: Option, diff --git a/clearing-house-app/logging-service/src/model/mod.rs b/clearing-house-app/logging-service/src/model/mod.rs index 53a8ef5..e510986 100644 --- a/clearing-house-app/logging-service/src/model/mod.rs +++ b/clearing-house-app/logging-service/src/model/mod.rs @@ -8,12 +8,12 @@ pub mod ids; pub(crate) mod crypto; pub(crate) mod doc_type; -#[derive(Serialize, Deserialize)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct TransactionCounter{ pub tc: i64 } -#[derive(Serialize, Deserialize)] +#[derive(serde::Serialize, serde::Deserialize)] pub struct OwnerList{ pub owners: Vec } @@ -26,12 +26,12 @@ impl OwnerList{ } } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Receipt { pub data: Compact } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct DataTransaction { pub transaction_id: String, pub timestamp: i64, diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 8abda1c..317be49 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -9,12 +9,18 @@ use core_lib::model::crypto::{KeyCt, KeyCtList}; use crate::services::keyring_service::KeyringService; use crate::db::doc_store::DataStore; + +#[derive(Clone)] pub struct DocumentService { db: DataStore, key_api: KeyringService, } impl DocumentService { + pub fn new(db: DataStore, key_api: KeyringService) -> Self { + Self { db, key_api } + } + pub(crate) async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); // data validation diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index fd250e2..63f2ab1 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -6,11 +6,18 @@ use crate::crypto::restore_key_map; use crate::db::key_store::KeyStore; use crate::model::doc_type::DocumentType; +#[derive(Clone)] pub struct KeyringService { db: KeyStore, } impl KeyringService { + pub fn new(db: KeyStore) -> KeyringService { + KeyringService { + db + } + } + pub async fn generate_keys(&self, ch_claims: ChClaims, _pid: String, dt_id: String) -> anyhow::Result { trace!("generate_keys"); trace!("...user '{:?}'", &ch_claims.client_id); diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/logging-service/src/services/logging_service.rs index 1a75f0e..c81c01c 100644 --- a/clearing-house-app/logging-service/src/services/logging_service.rs +++ b/clearing-house-app/logging-service/src/services/logging_service.rs @@ -20,12 +20,20 @@ use crate::model::{ids::{ use crate::db::ProcessStore; use crate::services::document_service::DocumentService; +#[derive(Clone)] pub struct LoggingService { db: ProcessStore, doc_api: DocumentService, } impl LoggingService { + pub fn new(db: ProcessStore, doc_api: DocumentService) -> LoggingService { + LoggingService { + db, + doc_api, + } + } + pub async fn log( &self, ch_claims: ChClaims, diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs index cb72058..400f075 100644 --- a/clearing-house-app/logging-service/src/services/mod.rs +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -5,5 +5,5 @@ //! Controllers to handle the requests and responses. //! pub(crate) mod keyring_service; -mod document_service; +pub(crate) mod document_service; pub(crate) mod logging_service; \ No newline at end of file From c9d8e6f99fba95ab83816911293cc1885f866fae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 15:14:29 +0200 Subject: [PATCH 033/183] feat(ch-app): Setup tracing as logger and replace rocket as logger; setup config --- clearing-house-app/Cargo.lock | 2 + clearing-house-app/logging-service/Cargo.toml | 2 + .../logging-service/config.toml | 1 + .../logging-service/src/config.rs | 69 +++++++++++++++++++ .../logging-service/src/db/key_store.rs | 30 ++++---- .../logging-service/src/main.rs | 28 ++------ .../logging-service/src/ports/doc_type_api.rs | 16 ++--- .../logging-service/src/ports/logging_api.rs | 22 +++--- 8 files changed, 114 insertions(+), 56 deletions(-) create mode 100644 clearing-house-app/logging-service/src/config.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index e9395ea..afafe41 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -1333,6 +1333,8 @@ dependencies = [ "serde_derive", "serde_json", "sha2 0.10.7", + "tracing", + "tracing-subscriber", ] [[package]] diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 0e9ebf9..64733e4 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -29,3 +29,5 @@ sha2 = "0.10.7" generic-array = "0.14.7" openssl = "0.10.56" config = { version = "0.13.3", default-features = false, features = ["toml"] } +tracing = "0.1.37" +tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } diff --git a/clearing-house-app/logging-service/config.toml b/clearing-house-app/logging-service/config.toml index 5e89afe..fe0d348 100644 --- a/clearing-house-app/logging-service/config.toml +++ b/clearing-house-app/logging-service/config.toml @@ -1,3 +1,4 @@ +log_level = "DEBUG" # TRACE, DEBUG, INFO, WARN, ERROR document_database_url= "mongodb://localhost:27017" process_database_url= "mongodb://localhost:27017" keyring_database_url= "mongodb://localhost:27017" diff --git a/clearing-house-app/logging-service/src/config.rs b/clearing-house-app/logging-service/src/config.rs new file mode 100644 index 0000000..6657a21 --- /dev/null +++ b/clearing-house-app/logging-service/src/config.rs @@ -0,0 +1,69 @@ +#[derive(Debug, serde::Deserialize)] +pub(crate) struct CHConfig { + pub(crate) process_database_url: String, + pub(crate) keyring_database_url: String, + pub(crate) document_database_url: String, + pub(crate) clear_db: bool, + #[serde(default)] + pub(crate) log_level: Option, +} + +#[derive(Debug, serde::Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub(crate) enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +impl Into for LogLevel { + fn into(self) -> tracing::Level { + match self { + LogLevel::Trace => tracing::Level::TRACE, + LogLevel::Debug => tracing::Level::DEBUG, + LogLevel::Info => tracing::Level::INFO, + LogLevel::Warn => tracing::Level::WARN, + LogLevel::Error => tracing::Level::ERROR, + } + } +} + +impl ToString for LogLevel { + fn to_string(&self) -> String { + match self { + LogLevel::Trace => String::from("TRACE"), + LogLevel::Debug => String::from("DEBUG"), + LogLevel::Info => String::from("INFO"), + LogLevel::Warn => String::from("WARN"), + LogLevel::Error => String::from("ERROR"), + } + } +} + +pub(crate) fn read_config() -> CHConfig { + let conf = config::Config::builder() + .add_source(config::File::with_name("config.toml")) + .add_source(config::Environment::with_prefix("CH_APP_")) + .build() + .expect("Failure to read configuration! Exiting..."); + + let conf: CHConfig = conf.try_deserialize().expect("Failure to read configuration! Exiting..."); + tracing::trace!(config = ?conf, "Config read"); + + conf +} + +pub(crate) fn configure_logging(log_level: Option) { + if std::env::var("RUST_LOG").is_err() { + if let Some(level) = log_level { + std::env::set_var("RUST_LOG", level.to_string()); + } + } + + // setup logging + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .init(); +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/key_store.rs b/clearing-house-app/logging-service/src/db/key_store.rs index 231f635..1abbbb8 100644 --- a/clearing-house-app/logging-service/src/db/key_store.rs +++ b/clearing-house-app/logging-service/src/db/key_store.rs @@ -27,19 +27,19 @@ impl KeyStore { /// Only one master key may exist in the database. pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result{ - debug!("Storing new master key..."); + tracing::debug!("Storing new master key..."); let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); - debug!("... but first check if there's already one."); + tracing::debug!("... but first check if there's already one."); let result= coll.find(None, None).await .expect("Error retrieving the master keys") .try_collect().await.unwrap_or_else(|_| vec![]); if result.len() > 1{ - error!("Master Key table corrupted!"); + tracing::error!("Master Key table corrupted!"); exit(1); } if result.len() == 1{ - error!("Master key already exists!"); + tracing::error!("Master key already exists!"); Ok(false) } else{ @@ -50,7 +50,7 @@ impl KeyStore { Ok(true) }, Err(e) => { - error!("master key could not be stored: {:?}", &e); + tracing::error!("master key could not be stored: {:?}", &e); panic!("master key could not be stored") } } @@ -65,14 +65,14 @@ impl KeyStore { .try_collect().await.unwrap_or_else(|_| vec![]); if result.len() > 1{ - error!("Master Key table corrupted!"); + tracing::error!("Master Key table corrupted!"); exit(1); } if result.len() == 1{ Ok(result[0].clone()) } else { - error!("Master Key missing!"); + tracing::error!("Master Key missing!"); exit(1); } } @@ -82,11 +82,11 @@ impl KeyStore { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); match coll.insert_one(doc_type.clone(), None).await { Ok(_r) => { - debug!("added new document type: {}", &_r.inserted_id); + tracing::debug!("added new document type: {}", &_r.inserted_id); Ok(()) }, Err(e) => { - error!("failed to log document type {}", &doc_type.id); + tracing::error!("failed to log document type {}", &doc_type.id); Err(Error::from(e)) } } @@ -111,7 +111,7 @@ impl KeyStore { match result { Some(_r) => Ok(true), None => { - debug!("document type with id {} and pid {:?} does not exist!", &dt_id, &pid); + tracing::debug!("document type with id {} and pid {:?} does not exist!", &dt_id, &pid); Ok(false) } } @@ -126,11 +126,11 @@ impl KeyStore { pub async fn get_document_type(&self, dt_id: &String) -> Result> { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - debug!("get_document_type for dt_id: '{}'", dt_id); + tracing::debug!("get_document_type for dt_id: '{}'", dt_id); match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await{ Ok(result) => Ok(result), Err(e) => { - error!("error while getting document type with id {}!", dt_id); + tracing::error!("error while getting document type with id {}!", dt_id); Err(Error::from(e)) } } @@ -141,15 +141,15 @@ impl KeyStore { match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await{ Ok(r) => { if r.matched_count != 1 || r.modified_count != 1{ - warn!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + tracing::warn!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); } else{ - debug!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + tracing::debug!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); } Ok(true) }, Err(e) => { - error!("error while updating document type with id {}: {:#?}", id, e); + tracing::error!("error while updating document type with id {}: {:#?}", id, e); Ok(false) } } diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index c3fe97e..4a3a356 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -1,11 +1,12 @@ #![forbid(unsafe_code)] #[macro_use] -extern crate rocket; +extern crate tracing; use std::path::Path; use core_lib::util::{add_service_config}; use rocket::fairing::AdHoc; +use tracing::subscriber; use core_lib::constants::ENV_LOGGING_SERVICE_ID; use db::config::doc_store::DatastoreConfigurator; use db::config::keyring_store::KeyringDbConfigurator; @@ -17,6 +18,7 @@ mod model; mod services; mod crypto; mod ports; +mod config; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { @@ -24,35 +26,17 @@ pub fn add_signing_key() -> AdHoc { if Path::new(&private_key_path).exists() { Ok(rocket.manage(private_key_path)) } else { - error!("Signing key not found! Aborting startup! Please configure signing_key!"); + tracing::error!("Signing key not found! Aborting startup! Please configure signing_key!"); return Err(rocket); } }) } -#[derive(Debug, serde::Deserialize)] -struct CHConfig { - process_database_url: String, - keyring_database_url: String, - document_database_url: String, - clear_db: bool, -} - #[rocket::main] async fn main() -> Result<(), rocket::Error> { // Read configuration - let conf = config::Config::builder() - .add_source(config::File::with_name("config.toml")) - .add_source(config::Environment::with_prefix("CH_APP_")) - .build() - .expect("Failure to read configuration! Exiting..."); - - // setup logging - // TODO: Setup tracing_subscriber - - let conf: CHConfig = conf.try_deserialize().expect("Failure to read configuration! Exiting..."); - println!("Config: {:?}", conf); - + let conf = config::read_config(); + config::configure_logging(conf.log_level); let process_store = ProcessStoreConfigurator::init_process_store(String::from(conf.process_database_url), conf.clear_db) diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs index 4f4d5bb..9b0bcb8 100644 --- a/clearing-house-app/logging-service/src/ports/doc_type_api.rs +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -7,7 +7,7 @@ use rocket::serde::json::{json,Json}; use crate::services::keyring_service::KeyringService; use crate::model::doc_type::DocumentType; -#[post("/", format = "json", data = "")] +#[rocket::post("/", format = "json", data = "")] async fn create_doc_type(key_api: &State, doc_type: Json) -> ApiResponse { match key_api.inner().create_doc_type(doc_type.into_inner()).await{ Ok(dt) => ApiResponse::SuccessCreate(json!(dt)), @@ -18,7 +18,7 @@ async fn create_doc_type(key_api: &State, doc_type: Json", format = "json", data = "")] +#[rocket::post("/", format = "json", data = "")] async fn update_doc_type(key_api: &State, id: String, doc_type: Json) -> ApiResponse { match key_api.inner().update_doc_type(id, doc_type.into_inner()).await{ Ok(id) => ApiResponse::SuccessOk(json!(id)), @@ -29,12 +29,12 @@ async fn update_doc_type(key_api: &State, id: String, doc_type: } } -#[delete("/", format = "json")] +#[rocket::delete("/", format = "json")] async fn delete_default_doc_type(key_api: &State, id: String) -> ApiResponse{ delete_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } -#[delete("//", format = "json")] +#[rocket::delete("//", format = "json")] async fn delete_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse{ match key_api.inner().delete_doc_type(id, pid).await{ Ok(id) => ApiResponse::SuccessOk(json!(id)), @@ -45,12 +45,12 @@ async fn delete_doc_type(key_api: &State, id: String, pid: Strin } } -#[get("/", format = "json")] +#[rocket::get("/", format = "json")] async fn get_default_doc_type(key_api: &State, id: String) -> ApiResponse { get_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } -#[get("//", format = "json")] +#[rocket::get("//", format = "json")] async fn get_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse { match key_api.inner().get_doc_type(id, pid).await{ Ok(dt) => { @@ -66,7 +66,7 @@ async fn get_doc_type(key_api: &State, id: String, pid: String) } } -#[get("/", format = "json")] +#[rocket::get("/", format = "json")] async fn get_doc_types(key_api: &State) -> ApiResponse { match key_api.inner().get_doc_types().await{ Ok(dt) => ApiResponse::SuccessOk(json!(dt)), @@ -80,7 +80,7 @@ async fn get_doc_types(key_api: &State) -> ApiResponse { pub fn mount_api() -> AdHoc { AdHoc::on_ignite("Mounting Document Type API", |rocket| async { rocket - .mount(ROCKET_DOC_TYPE_API, routes![create_doc_type, + .mount(ROCKET_DOC_TYPE_API, rocket::routes![create_doc_type, update_doc_type, delete_default_doc_type, delete_doc_type, get_default_doc_type, get_doc_type , get_doc_types]) }) diff --git a/clearing-house-app/logging-service/src/ports/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs index 8ac0bac..8151caa 100644 --- a/clearing-house-app/logging-service/src/ports/logging_api.rs +++ b/clearing-house-app/logging-service/src/ports/logging_api.rs @@ -13,7 +13,7 @@ use crate::model::ids::request::ClearingHouseMessage; use crate::model::constants::{ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_QUERY_API, ROCKET_PROCESS_API, ROCKET_PK_API}; use crate::services::logging_service::LoggingService; -#[post("/", format = "json", data = "")] +#[rocket::post("/", format = "json", data = "")] async fn log( ch_claims: ChClaims, logging_api: &State, @@ -30,7 +30,7 @@ async fn log( } } -#[post("/", format = "json", data = "")] +#[rocket::post("/", format = "json", data = "")] async fn create_process( ch_claims: ChClaims, logging_api: &State, @@ -46,17 +46,17 @@ async fn create_process( } } -#[post("/<_pid>", format = "json", rank = 50)] +#[rocket::post("/<_pid>", format = "json", rank = 50)] async fn unauth(_pid: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/<_pid>/<_id>", format = "json", rank = 50)] +#[rocket::post("/<_pid>/<_id>", format = "json", rank = 50)] async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[post("/?&&&&", format = "json", data = "")] +#[rocket::post("/?&&&&", format = "json", data = "")] async fn query_pid( ch_claims: ChClaims, logging_api: &State, @@ -77,7 +77,7 @@ async fn query_pid( } } -#[post("//", format = "json", data = "")] +#[rocket::post("//", format = "json", data = "")] async fn query_id( ch_claims: ChClaims, logging_api: &State, @@ -94,7 +94,7 @@ async fn query_id( } } -#[get("/.well-known/jwks.json", format = "json")] +#[rocket::get("/.well-known/jwks.json", format = "json")] async fn get_public_sign_key(key_path: &State) -> ApiResponse { match get_jwks(key_path.as_str()) { Some(jwks) => ApiResponse::SuccessOk(json!(jwks)), @@ -105,10 +105,10 @@ async fn get_public_sign_key(key_path: &State) -> ApiResponse { pub fn mount_api() -> AdHoc { AdHoc::on_ignite("Mounting Clearing House API", |rocket| async { rocket - .mount(format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API).as_str(), routes![log, unauth]) - .mount(format!("{}", ROCKET_PROCESS_API).as_str(), routes![create_process, unauth]) + .mount(format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API).as_str(), rocket::routes![log, unauth]) + .mount(format!("{}", ROCKET_PROCESS_API).as_str(), rocket::routes![create_process, unauth]) .mount(format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_QUERY_API).as_str(), - routes![query_id, query_pid, unauth, unauth_id]) - .mount(format!("{}", ROCKET_PK_API).as_str(), routes![get_public_sign_key]) + rocket::routes![query_id, query_pid, unauth, unauth_id]) + .mount(format!("{}", ROCKET_PK_API).as_str(), rocket::routes![get_public_sign_key]) }) } \ No newline at end of file From 04cecce30c0c787847ca199788d40e1daf07092f Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 16 Aug 2023 18:25:13 +0200 Subject: [PATCH 034/183] fix(ci): updated test job to run from root --- .github/workflows/test.yml | 41 +++----------------------------------- 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5f6935e..9b73abf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,10 +6,11 @@ on: - master - beta - alpha + - development jobs: - test-keyring: + unit-tests: runs-on: ubuntu-latest steps: @@ -23,42 +24,6 @@ jobs: - name: Build and Test run: | - cd clearing-house-app/keyring-api + cd clearing-house-app cargo build --verbose cargo test --verbose - - test-document: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - name: Build and Test - run: | - cd clearing-house-app/document-api - cargo build --verbose - cargo test --verbose - - test-logging: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - - - name: Build and Test - run: | - cd clearing-house-app/logging-service - cargo build --verbose - cargo test --verbose \ No newline at end of file From a61ba68d2993b52aa288327fe0de36cd7c1aeeaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 16 Aug 2023 18:48:38 +0200 Subject: [PATCH 035/183] refactor(ch-app): Remove openssl as cryptographical random number generator --- clearing-house-app/Cargo.lock | 3 +- clearing-house-app/logging-service/Cargo.toml | 3 +- .../logging-service/src/crypto.rs | 31 +++++++++++++++++-- .../logging-service/src/main.rs | 1 - 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index afafe41..4abcb96 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -1326,8 +1326,9 @@ dependencies = [ "hkdf 0.12.3", "log", "mongodb", - "openssl", + "once_cell", "percent-encoding", + "rand", "rocket", "serde", "serde_derive", diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 64733e4..5d42cc3 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -27,7 +27,8 @@ aes-gcm-siv = "0.11.1" hkdf = "0.12.3" sha2 = "0.10.7" generic-array = "0.14.7" -openssl = "0.10.56" config = { version = "0.13.3", default-features = false, features = ["toml"] } tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +rand = "0.8.5" +once_cell = "1.18.0" diff --git a/clearing-house-app/logging-service/src/crypto.rs b/clearing-house-app/logging-service/src/crypto.rs index a361176..688cf7f 100644 --- a/clearing-house-app/logging-service/src/crypto.rs +++ b/clearing-house-app/logging-service/src/crypto.rs @@ -3,12 +3,14 @@ use aes_gcm_siv::aead::Aead; use core_lib::model::crypto::{KeyEntry, KeyMap}; use generic_array::GenericArray; use hkdf::Hkdf; -use openssl::rand::rand_bytes; use sha2::Sha256; use std::collections::HashMap; +use std::sync::Mutex; use anyhow::anyhow; +use once_cell::sync::Lazy; use crate::model::doc_type::DocumentType; use crate::model::crypto::MasterKey; +use rand::{RngCore, SeedableRng}; const EXP_KEY_SIZE: usize = 32; const EXP_NONCE_SIZE: usize = 12; @@ -21,9 +23,20 @@ fn initialize_kdf() -> (String, Hkdf) { (hex::encode_upper(master_key), kdf) } +/// Generates a random seed with 256 bytes. pub fn generate_random_seed() -> Vec{ + // Init crypto RNG once lazy + static RNG: Lazy> = Lazy::new(|| Mutex::new(rand::rngs::StdRng::from_entropy())); + // Create a buffer to fill with random bytes let mut buf = [0u8; 256]; - rand_bytes(&mut buf).unwrap(); // TODO: Replace with some other cryptographically secure random number generator + + // Fill buffer with random bytes in a block, so the mutex is locked for a short time. + { + RNG.lock() + .expect("This mutex locking is fine, because it will be release immediately after use and this is the only place of usage ") + .fill_bytes(&mut buf); + } + buf.to_vec() } @@ -158,4 +171,18 @@ pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result()); + } + } } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 4a3a356..eaf6491 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -6,7 +6,6 @@ extern crate tracing; use std::path::Path; use core_lib::util::{add_service_config}; use rocket::fairing::AdHoc; -use tracing::subscriber; use core_lib::constants::ENV_LOGGING_SERVICE_ID; use db::config::doc_store::DatastoreConfigurator; use db::config::keyring_store::KeyringDbConfigurator; From dca03ce88839a8baa1b60932aa9269a623d27987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 17 Aug 2023 12:28:37 +0200 Subject: [PATCH 036/183] refactor(ch-app-core-lib): Cleanup dependencies, no global serde macros --- clearing-house-app/Cargo.lock | 41 +++++++++++-------- clearing-house-app/core-lib/Cargo.toml | 12 +++--- clearing-house-app/core-lib/src/api/crypto.rs | 7 ++-- clearing-house-app/core-lib/src/api/mod.rs | 4 +- clearing-house-app/core-lib/src/lib.rs | 1 - .../core-lib/src/model/crypto.rs | 12 +++--- .../core-lib/src/model/document.rs | 8 ++-- clearing-house-app/core-lib/src/model/mod.rs | 4 +- .../core-lib/src/model/process.rs | 2 +- clearing-house-app/core-lib/src/util.rs | 2 +- 10 files changed, 47 insertions(+), 46 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 4abcb96..7dcc817 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -246,6 +246,22 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" +[[package]] +name = "biscuit" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28865439fc81744500265d96c920985ceb6b612ef8564d43f1cc78e7a6c89e26" +dependencies = [ + "chrono", + "data-encoding", + "num-bigint", + "num-traits", + "once_cell", + "ring", + "serde", + "serde_json", +] + [[package]] name = "biscuit" version = "0.6.0" @@ -331,7 +347,7 @@ dependencies = [ "serde_bytes", "serde_json", "time 0.3.23", - "uuid 1.4.1", + "uuid", ] [[package]] @@ -459,7 +475,7 @@ dependencies = [ "aes 0.6.0", "aes-gcm-siv 0.9.0", "base64 0.9.3", - "biscuit", + "biscuit 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "blake2-rfc", "chrono", "error-chain", @@ -476,9 +492,8 @@ dependencies = [ "ring", "rocket", "serde", - "serde_derive", "serde_json", - "uuid 0.8.2", + "uuid", ] [[package]] @@ -657,7 +672,7 @@ dependencies = [ name = "document-api" version = "0.10.0" dependencies = [ - "biscuit", + "biscuit 0.6.0 (git+https://github.com/lawliet89/biscuit?branch=master)", "chrono", "core-lib", "error-chain", @@ -1247,7 +1262,7 @@ dependencies = [ "aes 0.6.0", "aes-gcm-siv 0.9.0", "base64 0.9.3", - "biscuit", + "biscuit 0.6.0 (git+https://github.com/lawliet89/biscuit?branch=master)", "chrono", "core-lib", "error-chain", @@ -1315,7 +1330,7 @@ dependencies = [ "aes 0.8.3", "aes-gcm-siv 0.11.1", "anyhow", - "biscuit", + "biscuit 0.6.0 (git+https://github.com/lawliet89/biscuit?branch=master)", "chrono", "config", "core-lib", @@ -1484,7 +1499,7 @@ dependencies = [ "trust-dns-proto", "trust-dns-resolver", "typed-builder", - "uuid 1.4.1", + "uuid", "webpki-roots", ] @@ -3001,16 +3016,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom", - "serde", -] - [[package]] name = "uuid" version = "1.4.1" diff --git a/clearing-house-app/core-lib/Cargo.toml b/clearing-house-app/core-lib/Cargo.toml index 1a993c7..fb96dd9 100644 --- a/clearing-house-app/core-lib/Cargo.toml +++ b/clearing-house-app/core-lib/Cargo.toml @@ -10,8 +10,7 @@ edition = "2021" [dependencies] aes = "0.6.0" aes-gcm-siv = "0.9.0" -# As of now there is no release of biscuit after 04.03.2022 which introduces Clone to JWKS -biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } +biscuit = "0.6.0" base64 = "0.9.3" blake2-rfc = "0.2.18" chrono = { version = "0.4", features = ["serde"] } @@ -25,10 +24,9 @@ mongodb ="2.3.0" num-bigint = "0.4.3" openssh-keys = "0.5.0" percent-encoding = "2.1.0" -reqwest = { version="0.11.11", features = ["default", "json", "blocking"]} +reqwest = { version="0.11", features = ["default", "json", "blocking"]} ring = "0.16.20" rocket = { version = "0.5.0-rc.1", features = ["json"] } -serde = "1.0" -serde_derive = "1.0" -serde_json = "1.0" -uuid = { version = "0.8", features = ["serde", "v4"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +uuid = { version = "1.4.1", features = ["serde", "v4"] } diff --git a/clearing-house-app/core-lib/src/api/crypto.rs b/clearing-house-app/core-lib/src/api/crypto.rs index c433d44..f62cc09 100644 --- a/clearing-house-app/core-lib/src/api/crypto.rs +++ b/clearing-house-app/core-lib/src/api/crypto.rs @@ -10,12 +10,11 @@ use num_bigint::BigUint; use ring::signature::KeyPair; use rocket::http::Status; use rocket::request::{Request, FromRequest, Outcome}; -use serde::{Deserialize,Serialize}; use crate::errors::*; use crate::constants::{ENV_SHARED_SECRET, SERVICE_HEADER}; use crate::util::ServiceConfig; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ChClaims{ pub client_id: String, } @@ -109,7 +108,7 @@ pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> St create_token(issuer, audience, &private_claims) } -pub fn create_token Deserialize<'de>> (issuer: &str, audience: &str, private_claims: &T) -> String{ +pub fn create_token serde::Deserialize<'de>> (issuer: &str, audience: &str, private_claims: &T) -> String{ let signing_secret = match env::var(ENV_SHARED_SECRET){ Ok(secret) => { Secret::Bytes(secret.to_string().into_bytes()) @@ -143,7 +142,7 @@ pub fn create_token Deserialize<'de>> jwt.into_encoded(&signing_secret).unwrap().unwrap_encoded().to_string() } -pub fn decode_token Deserialize<'de>>(token: &str, audience: &str) -> Result{ +pub fn decode_token serde::Deserialize<'de>>(token: &str, audience: &str) -> Result{ let signing_secret = match env::var(ENV_SHARED_SECRET){ Ok(secret) => { Secret::Bytes(secret.to_string().into_bytes()) diff --git a/clearing-house-app/core-lib/src/api/mod.rs b/clearing-house-app/core-lib/src/api/mod.rs index 6ed026d..97460a3 100644 --- a/clearing-house-app/core-lib/src/api/mod.rs +++ b/clearing-house-app/core-lib/src/api/mod.rs @@ -32,7 +32,7 @@ pub enum ApiResponse { InternalError(String), } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct DocumentReceipt{ pub timestamp: i64, pub pid: String, @@ -51,7 +51,7 @@ impl DocumentReceipt{ } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct QueryResult{ pub date_from: i64, pub date_to: i64, diff --git a/clearing-house-app/core-lib/src/lib.rs b/clearing-house-app/core-lib/src/lib.rs index 815fed7..5fbc789 100644 --- a/clearing-house-app/core-lib/src/lib.rs +++ b/clearing-house-app/core-lib/src/lib.rs @@ -5,7 +5,6 @@ extern crate chrono; extern crate fern; extern crate mongodb; #[macro_use] extern crate rocket; -#[macro_use] extern crate serde_derive; #[macro_use] extern crate error_chain; pub mod errors { diff --git a/clearing-house-app/core-lib/src/model/crypto.rs b/clearing-house-app/core-lib/src/model/crypto.rs index ef269de..baaea10 100644 --- a/clearing-house-app/core-lib/src/model/crypto.rs +++ b/clearing-house-app/core-lib/src/model/crypto.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct KeyEntry { pub id: String, pub key: Vec, @@ -17,7 +17,7 @@ impl KeyEntry{ } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct KeyMap { pub enc: bool, pub keys: HashMap, @@ -32,9 +32,9 @@ impl KeyMap{ keys_enc } } - } +} -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct KeyCt{ pub id: String, pub ct: String @@ -49,7 +49,7 @@ impl KeyCt{ } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct KeyCtList { pub dt: String, pub cts: Vec @@ -64,7 +64,7 @@ impl KeyCtList{ } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct KeyMapListItem { pub id: String, pub map: KeyMap diff --git a/clearing-house-app/core-lib/src/model/document.rs b/clearing-house-app/core-lib/src/model/document.rs index 6357e7d..59fd2e2 100644 --- a/clearing-house-app/core-lib/src/model/document.rs +++ b/clearing-house-app/core-lib/src/model/document.rs @@ -10,7 +10,7 @@ use crate::model::new_uuid; use crate::model::crypto::{KeyEntry, KeyMap}; use chrono::Local; -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct DocumentPart { pub name: String, pub content: Option, @@ -76,7 +76,7 @@ impl DocumentPart{ } } -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct Document { #[serde(default = "new_uuid")] pub id: String, @@ -90,7 +90,7 @@ pub struct Document { /// Documents should have a globally unique id, setting the id manually is discouraged. impl Document{ pub fn create_uuid() -> String{ - Uuid::new_v4().to_hyphenated().to_string() + Uuid::new_v4().hyphenated().to_string() } // each part is encrypted using the part specific key from the key map @@ -172,7 +172,7 @@ impl Document{ } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct EncryptedDocument { pub id: String, pub pid: String, diff --git a/clearing-house-app/core-lib/src/model/mod.rs b/clearing-house-app/core-lib/src/model/mod.rs index ab2124e..b31d90c 100644 --- a/clearing-house-app/core-lib/src/model/mod.rs +++ b/clearing-house-app/core-lib/src/model/mod.rs @@ -7,10 +7,10 @@ mod tests; pub fn new_uuid() -> String { use uuid::Uuid; - Uuid::new_v4().to_hyphenated().to_string() + Uuid::new_v4().hyphenated().to_string() } -#[derive(Debug, Clone, Serialize, Deserialize, FromFormField)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, FromFormField)] pub enum SortingOrder { #[field(value = "asc")] #[serde(rename = "asc")] diff --git a/clearing-house-app/core-lib/src/model/process.rs b/clearing-house-app/core-lib/src/model/process.rs index 12bc571..7fda6aa 100644 --- a/clearing-house-app/core-lib/src/model/process.rs +++ b/clearing-house-app/core-lib/src/model/process.rs @@ -1,4 +1,4 @@ -#[derive(Clone, Serialize, Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct Process { pub id: String, pub owners: Vec, diff --git a/clearing-house-app/core-lib/src/util.rs b/clearing-house-app/core-lib/src/util.rs index f9ddd22..c655d87 100644 --- a/clearing-house-app/core-lib/src/util.rs +++ b/clearing-house-app/core-lib/src/util.rs @@ -9,7 +9,7 @@ use crate::errors::*; use figment::{Figment, providers::{Format, Yaml}}; use rocket::fairing::AdHoc; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ServiceConfig{ pub service_id: String } From ce7080aa584d73eb94cc456eab4bffa9232f74cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 17 Aug 2023 12:28:37 +0200 Subject: [PATCH 037/183] refactor(ch-app-core-lib): Cleanup dependencies, no global serde macros --- clearing-house-app/Cargo.lock | 239 +++++++++--------- clearing-house-app/document-api/Cargo.toml | 4 +- clearing-house-app/keyring-api/Cargo.toml | 2 +- clearing-house-app/logging-service/Cargo.toml | 14 +- 4 files changed, 123 insertions(+), 136 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 7dcc817..8fa23d4 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -145,9 +145,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.73" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f768393e7fabd388fe8409b13faa4d93ab0fef35db1508438dfdb066918bcf38" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayvec" @@ -177,18 +177,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -262,21 +262,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "biscuit" -version = "0.6.0" -source = "git+https://github.com/lawliet89/biscuit?branch=master#16d5c91c0576ec40ec655b7f107b4df19fe4186f" -dependencies = [ - "chrono", - "data-encoding", - "num-bigint", - "num-traits", - "once_cell", - "ring", - "serde", - "serde_json", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -285,9 +270,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "bitvec" @@ -346,7 +331,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "time 0.3.23", + "time 0.3.25", "uuid", ] @@ -370,9 +355,12 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +dependencies = [ + "libc", +] [[package]] name = "cfg-if" @@ -448,7 +436,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" dependencies = [ "percent-encoding", - "time 0.3.23", + "time 0.3.25", "version_check", ] @@ -475,7 +463,7 @@ dependencies = [ "aes 0.6.0", "aes-gcm-siv 0.9.0", "base64 0.9.3", - "biscuit 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "biscuit", "blake2-rfc", "chrono", "error-chain", @@ -591,6 +579,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" + [[package]] name = "derivative" version = "2.2.0" @@ -641,11 +635,11 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -672,7 +666,7 @@ dependencies = [ name = "document-api" version = "0.10.0" dependencies = [ - "biscuit 0.6.0 (git+https://github.com/lawliet89/biscuit?branch=master)", + "biscuit", "chrono", "core-lib", "error-chain", @@ -725,9 +719,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -877,7 +871,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -1084,9 +1078,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" @@ -1262,7 +1256,7 @@ dependencies = [ "aes 0.6.0", "aes-gcm-siv 0.9.0", "base64 0.9.3", - "biscuit 0.6.0 (git+https://github.com/lawliet89/biscuit?branch=master)", + "biscuit", "chrono", "core-lib", "error-chain", @@ -1303,9 +1297,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" @@ -1319,9 +1313,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "logging-service" @@ -1330,23 +1324,19 @@ dependencies = [ "aes 0.8.3", "aes-gcm-siv 0.11.1", "anyhow", - "biscuit 0.6.0 (git+https://github.com/lawliet89/biscuit?branch=master)", + "biscuit", "chrono", "config", "core-lib", - "error-chain", - "fern", "generic-array", "hex", "hkdf 0.12.3", - "log", "mongodb", "once_cell", "percent-encoding", "rand", "rocket", "serde", - "serde_derive", "serde_json", "sha2 0.10.7", "tracing", @@ -1458,9 +1448,9 @@ dependencies = [ [[package]] name = "mongodb" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebcd85ec209a5b84fd9f54b9e381f6fa17462bc74160d018fc94fd8b9f61faa8" +checksum = "16928502631c0db72214720aa479c722397fe5aed6bf1c740a3830b3fe4bfcfe" dependencies = [ "async-trait", "base64 0.13.1", @@ -1664,7 +1654,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -1737,7 +1727,7 @@ checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" dependencies = [ "inlinable_string", "pear_codegen", - "yansi 1.0.0-rc", + "yansi 1.0.0-rc.1", ] [[package]] @@ -1749,7 +1739,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -1760,9 +1750,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -1822,9 +1812,9 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", "version_check", - "yansi 1.0.0-rc", + "yansi 1.0.0-rc.1", ] [[package]] @@ -1835,9 +1825,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -1889,33 +1879,33 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ef7e18e8841942ddb1cf845054f8008410030a3997875d9e49b7a363063df1" +checksum = "acde58d073e9c79da00f2b5b84eed919c8326832648a5b109b3fce1bb1175280" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfaf0c85b766276c797f3791f5bc6d5bd116b41d53049af2789666b0c0bc9fa" +checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.3", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -1930,9 +1920,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -2043,7 +2033,7 @@ dependencies = [ "serde_json", "state", "tempfile", - "time 0.3.23", + "time 0.3.25", "tokio", "tokio-stream", "tokio-util", @@ -2064,14 +2054,15 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.27", + "syn 2.0.29", "unicode-xid", ] [[package]] name = "rocket_cors" version = "0.6.0-alpha2" -source = "git+https://github.com/lawliet89/rocket_cors?branch=master#985098dd8f3b052716111eaa872d184cc21a1a68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12771b47f52e34d5d0e0e444aeba382863e73263cb9e18847e7d5b74aa2cbd0" dependencies = [ "http", "log", @@ -2106,7 +2097,7 @@ dependencies = [ "smallvec", "stable-pattern", "state", - "time 0.3.23", + "time 0.3.25", "tokio", "uncased", ] @@ -2147,11 +2138,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno", "libc", "linux-raw-sys", @@ -2274,9 +2265,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.175" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" +checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" dependencies = [ "serde_derive", ] @@ -2292,20 +2283,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.175" +version = "1.0.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" +checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] name = "serde_json" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "indexmap 2.0.0", "itoa", @@ -2522,9 +2513,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.27" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -2545,9 +2536,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" dependencies = [ "cfg-if", "fastrand", @@ -2558,22 +2549,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -2599,10 +2590,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.23" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" dependencies = [ + "deranged", "itoa", "serde", "time-core", @@ -2617,9 +2609,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" dependencies = [ "time-core", ] @@ -2641,11 +2633,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -2653,7 +2644,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.4.9", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -2666,7 +2657,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -2798,7 +2789,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", ] [[package]] @@ -3086,7 +3077,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -3120,7 +3111,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.27", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3208,9 +3199,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -3223,51 +3214,51 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" [[package]] name = "winnow" -version = "0.5.0" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7" +checksum = "83817bbecf72c73bad717ee86820ebf286203d2e04c3951f3cd538869c897364" dependencies = [ "memchr", ] @@ -3317,9 +3308,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "yansi" -version = "1.0.0-rc" +version = "1.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee746ad3851dd3bc40e4a028ab3b00b99278d929e48957bcb2d111874a7e43e" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" [[package]] name = "zeroize" diff --git a/clearing-house-app/document-api/Cargo.toml b/clearing-house-app/document-api/Cargo.toml index f844f62..4fbdedc 100644 --- a/clearing-house-app/document-api/Cargo.toml +++ b/clearing-house-app/document-api/Cargo.toml @@ -8,7 +8,7 @@ authors = [ edition = "2021" [dependencies] -biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } +biscuit = "0.6.0" chrono = { version = "0.4", features = ["serde"] } core-lib = {path = "../core-lib" } error-chain = "0.12.4" @@ -18,7 +18,7 @@ hex = "0.4.3" log = "0.4.14" mongodb ="2.3.0" rocket = { version = "0.5.0-rc.1", features = ["json"] } -rocket_cors = { git = "https://github.com/lawliet89/rocket_cors", branch = "master" } +rocket_cors = "0.6.0-alpha2" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" diff --git a/clearing-house-app/keyring-api/Cargo.toml b/clearing-house-app/keyring-api/Cargo.toml index 37718a3..dc9c3d0 100644 --- a/clearing-house-app/keyring-api/Cargo.toml +++ b/clearing-house-app/keyring-api/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" aes = "0.6.0" aes-gcm-siv = "0.9.0" base64 = "0.9.3" -biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } +biscuit = "0.6.0" chrono = { version = "0.4", features = ["serde"] } core-lib = {path = "../core-lib" } error-chain = "0.12.4" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 5d42cc3..664e8f8 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -8,18 +8,14 @@ authors = [ edition = "2021" [dependencies] -biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } +biscuit = "0.6.0" core-lib = { path = "../core-lib" } chrono = { version = "0.4", features = ["serde"] } -error-chain = "0.12.1" -fern = "0.5" -log = "0.4" -mongodb ="2.3.0" +mongodb ="2" percent-encoding = "2.1.0" rocket = { version = "0.5.0-rc.1", features = ["json"] } -serde = "1.0" -serde_derive = "1.0" -serde_json = "1.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" anyhow = "1" hex = "0.4.3" aes = "0.8.3" @@ -28,7 +24,7 @@ hkdf = "0.12.3" sha2 = "0.10.7" generic-array = "0.14.7" config = { version = "0.13.3", default-features = false, features = ["toml"] } -tracing = "0.1.37" +tracing = "0.1" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } rand = "0.8.5" once_cell = "1.18.0" From ed5f949e7d839d4c289866de375268c8626105c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 17 Aug 2023 13:32:30 +0200 Subject: [PATCH 038/183] refactor(ch-app): Refactored core-lib into logging-service and disabled all microservices except the logging service --- clearing-house-app/Cargo.lock | 167 ++-------- clearing-house-app/Cargo.toml | 6 +- clearing-house-app/core-lib/src/util.rs | 10 +- clearing-house-app/logging-service/Cargo.toml | 7 + .../logging-service/src/config.rs | 8 +- .../logging-service/src/crypto.rs | 52 ++-- .../src/db/config/doc_store.rs | 6 +- .../src/db/config/keyring_store.rs | 6 +- .../src/db/config/process_store.rs | 6 +- .../logging-service/src/db/doc_store.rs | 28 +- .../logging-service/src/db/key_store.rs | 22 +- .../logging-service/src/db/mod.rs | 147 ++------- .../logging-service/src/db/process_store.rs | 135 +++++++++ .../logging-service/src/main.rs | 5 +- .../logging-service/src/model/claims.rs | 169 +++++++++++ .../logging-service/src/model/constants.rs | 91 +++++- .../logging-service/src/model/crypto.rs | 82 +++++ .../logging-service/src/model/document.rs | 286 ++++++++++++++++++ .../logging-service/src/model/errors.rs | 18 ++ .../logging-service/src/model/ids/message.rs | 4 +- .../logging-service/src/model/mod.rs | 133 ++++---- .../logging-service/src/model/process.rs | 92 ++++++ .../logging-service/src/model/util.rs | 4 + .../logging-service/src/ports/doc_type_api.rs | 4 +- .../logging-service/src/ports/logging_api.rs | 6 +- .../logging-service/src/ports/mod.rs | 24 ++ .../src/services/document_service.rs | 12 +- .../src/services/keyring_service.rs | 4 +- .../src/services/logging_service.rs | 10 +- .../logging-service/src/services/mod.rs | 49 ++- .../logging-service/src/util.rs | 26 ++ 31 files changed, 1188 insertions(+), 431 deletions(-) create mode 100644 clearing-house-app/logging-service/src/db/process_store.rs create mode 100644 clearing-house-app/logging-service/src/model/claims.rs create mode 100644 clearing-house-app/logging-service/src/model/document.rs create mode 100644 clearing-house-app/logging-service/src/model/errors.rs create mode 100644 clearing-house-app/logging-service/src/model/process.rs create mode 100644 clearing-house-app/logging-service/src/model/util.rs create mode 100644 clearing-house-app/logging-service/src/util.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 8fa23d4..96f677d 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -474,7 +474,7 @@ dependencies = [ "log", "mongodb", "num-bigint", - "openssh-keys", + "openssh-keys 0.5.0", "percent-encoding", "reqwest", "ring", @@ -510,16 +510,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff07008ec701e8028e2ceb8f83f0e4274ee62bd2dbdc4fefff2e9a91824081a" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "ctr" version = "0.6.0" @@ -662,28 +652,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "document-api" -version = "0.10.0" -dependencies = [ - "biscuit", - "chrono", - "core-lib", - "error-chain", - "fern", - "futures", - "hex", - "log", - "mongodb", - "rocket", - "rocket_cors", - "serde", - "serde_derive", - "serde_json", - "tokio", - "tokio-test", -] - [[package]] name = "either" version = "1.9.0" @@ -823,7 +791,6 @@ checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", - "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -999,33 +966,13 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hkdf" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" -dependencies = [ - "digest 0.9.0", - "hmac 0.10.1", -] - [[package]] name = "hkdf" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" -dependencies = [ - "crypto-mac", - "digest 0.9.0", + "hmac", ] [[package]] @@ -1249,34 +1196,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "keyring-api" -version = "0.10.0" -dependencies = [ - "aes 0.6.0", - "aes-gcm-siv 0.9.0", - "base64 0.9.3", - "biscuit", - "chrono", - "core-lib", - "error-chain", - "fern", - "generic-array", - "hex", - "hkdf 0.10.0", - "log", - "mongodb", - "openssl", - "rocket", - "serde", - "serde_derive", - "serde_json", - "sha2 0.9.9", - "tokio", - "tokio-test", - "yaml-rust", -] - [[package]] name = "lazy_static" version = "1.4.0" @@ -1324,23 +1243,30 @@ dependencies = [ "aes 0.8.3", "aes-gcm-siv 0.11.1", "anyhow", + "base64 0.21.2", "biscuit", + "blake2-rfc", "chrono", "config", "core-lib", + "error-chain", "generic-array", "hex", - "hkdf 0.12.3", + "hkdf", "mongodb", + "num-bigint", "once_cell", + "openssh-keys 0.6.2", "percent-encoding", "rand", + "ring", "rocket", "serde", "serde_json", "sha2 0.10.7", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -1464,7 +1390,7 @@ dependencies = [ "futures-io", "futures-util", "hex", - "hmac 0.12.1", + "hmac", "lazy_static", "md-5 0.10.5", "pbkdf2", @@ -1631,6 +1557,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "openssh-keys" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75a0ec2d1b302412fb503224289325fcc0e44600176864804c7211b055cfd58" +dependencies = [ + "base64 0.21.2", + "byteorder", + "md-5 0.10.5", + "sha2 0.10.7", + "thiserror", +] + [[package]] name = "openssl" version = "0.10.56" @@ -2058,23 +1997,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "rocket_cors" -version = "0.6.0-alpha2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12771b47f52e34d5d0e0e444aeba382863e73263cb9e18847e7d5b74aa2cbd0" -dependencies = [ - "http", - "log", - "regex", - "rocket", - "serde", - "serde_derive", - "unicase", - "unicase_serde", - "url", -] - [[package]] name = "rocket_http" version = "0.5.0-rc.3" @@ -2692,19 +2614,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-test" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53474327ae5e166530d17f2d956afcb4f8a004de581b3cae10f12006bc8163e3" -dependencies = [ - "async-stream", - "bytes", - "futures-core", - "tokio", - "tokio-stream", -] - [[package]] name = "tokio-util" version = "0.7.8" @@ -2918,25 +2827,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "unicase" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicase_serde" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ef53697679d874d69f3160af80bc28de12730a985d57bdf2b47456ccb8b11f1" -dependencies = [ - "serde", - "unicase", -] - [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3291,15 +3181,6 @@ dependencies = [ "tap", ] -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yansi" version = "0.5.1" diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index c9bdecc..dadd69d 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -1,8 +1,8 @@ [workspace] members = [ - "core-lib", - "document-api", - "keyring-api", +# "core-lib", +# "document-api", +# "keyring-api", "logging-service" ] diff --git a/clearing-house-app/core-lib/src/util.rs b/clearing-house-app/core-lib/src/util.rs index c655d87..604e095 100644 --- a/clearing-house-app/core-lib/src/util.rs +++ b/clearing-house-app/core-lib/src/util.rs @@ -1,10 +1,9 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; use std::env; -use std::fs::File; -use std::io::prelude::*; use std::str::FromStr; use crate::constants::ENV_API_LOG_LEVEL; +use crate::errors; use crate::errors::*; use figment::{Figment, providers::{Format, Yaml}}; use rocket::fairing::AdHoc; @@ -71,11 +70,8 @@ pub fn setup_logger() -> Result<()> { } pub fn read_file(file: &str) -> Result { - let mut f = File::open(file)?; - let mut data = String::new(); - f.read_to_string(&mut data)?; - drop(f); - Ok(data) + std::fs::read_to_string(file) + .map_err(|e| errors::Error::from(e)) } pub fn url_encode(id: &str) -> String{ diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 664e8f8..4f9416a 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -28,3 +28,10 @@ tracing = "0.1" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } rand = "0.8.5" once_cell = "1.18.0" +blake2-rfc = "0.2.18" +base64 = "0.21.2" +uuid = { version = "1.4.1", features = ["serde"] } +error-chain = "0.12.4" +num-bigint = "0.4.3" +ring = "0.16.20" +openssh-keys = "0.6.2" diff --git a/clearing-house-app/logging-service/src/config.rs b/clearing-house-app/logging-service/src/config.rs index 6657a21..c41bf95 100644 --- a/clearing-house-app/logging-service/src/config.rs +++ b/clearing-house-app/logging-service/src/config.rs @@ -42,6 +42,7 @@ impl ToString for LogLevel { } } +/// Read configuration from `config.toml` and environment variables pub(crate) fn read_config() -> CHConfig { let conf = config::Config::builder() .add_source(config::File::with_name("config.toml")) @@ -49,12 +50,11 @@ pub(crate) fn read_config() -> CHConfig { .build() .expect("Failure to read configuration! Exiting..."); - let conf: CHConfig = conf.try_deserialize().expect("Failure to read configuration! Exiting..."); - tracing::trace!(config = ?conf, "Config read"); - - conf + conf.try_deserialize::() + .expect("Failure to parse configuration! Exiting...") } +/// Configure logging based on environment variable `RUST_LOG` pub(crate) fn configure_logging(log_level: Option) { if std::env::var("RUST_LOG").is_err() { if let Some(level) = log_level { diff --git a/clearing-house-app/logging-service/src/crypto.rs b/clearing-house-app/logging-service/src/crypto.rs index 688cf7f..ee79f6f 100644 --- a/clearing-house-app/logging-service/src/crypto.rs +++ b/clearing-house-app/logging-service/src/crypto.rs @@ -1,13 +1,12 @@ use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; use aes_gcm_siv::aead::Aead; -use core_lib::model::crypto::{KeyEntry, KeyMap}; +use crate::model::crypto::{KeyEntry, KeyMap}; use generic_array::GenericArray; use hkdf::Hkdf; use sha2::Sha256; use std::collections::HashMap; use std::sync::Mutex; use anyhow::anyhow; -use once_cell::sync::Lazy; use crate::model::doc_type::DocumentType; use crate::model::crypto::MasterKey; use rand::{RngCore, SeedableRng}; @@ -24,41 +23,43 @@ fn initialize_kdf() -> (String, Hkdf) { } /// Generates a random seed with 256 bytes. -pub fn generate_random_seed() -> Vec{ +pub fn generate_random_seed() -> Vec { // Init crypto RNG once lazy - static RNG: Lazy> = Lazy::new(|| Mutex::new(rand::rngs::StdRng::from_entropy())); + static RNG: once_cell::sync::Lazy> = once_cell::sync::Lazy::new( + || Mutex::new(rand::rngs::StdRng::from_entropy()) + ); // Create a buffer to fill with random bytes let mut buf = [0u8; 256]; // Fill buffer with random bytes in a block, so the mutex is locked for a short time. { RNG.lock() - .expect("This mutex locking is fine, because it will be release immediately after use and this is the only place of usage ") + .expect("This mutex locking is fine, because it will be released immediately after use and this is the only place of usage. So no deadlock possible.") .fill_bytes(&mut buf); } buf.to_vec() } -fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap{ +fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap { let mut key_map = HashMap::new(); let mut okm = [0u8; EXP_BUFF_SIZE]; let mut i = 0; dt.parts.iter() - .for_each( |p| { + .for_each(|p| { if kdf.expand(p.name.clone().as_bytes(), &mut okm).is_ok() { - let map_key = match enc{ + let map_key = match enc { true => p.name.clone(), false => i.to_string() }; key_map.insert(map_key, KeyEntry::new(i.to_string(), okm[..EXP_KEY_SIZE].to_vec(), okm[EXP_KEY_SIZE..].to_vec())); } - i = i +1; + i = i + 1; }); key_map } -pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result{ +pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result { debug!("generating encryption key_map for doc type: '{}'", &dt.id); let (secret, doc_kdf) = initialize_kdf(); let key_map = derive_key_map(doc_kdf, dt, true); @@ -66,10 +67,10 @@ pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result Ok(KeyMap::new(true, key_map, Some(ct))), Err(e) => { error!("Error while encrypting key seed: {:?}", e); @@ -78,15 +79,15 @@ pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result) -> anyhow::Result{ +pub fn restore_key_map(mkey: MasterKey, dt: DocumentType, keys_ct: Vec) -> anyhow::Result { debug!("decrypting the key seed"); let kdf = restore_kdf(&mkey.key)?; let mut okm = [0u8; EXP_BUFF_SIZE]; - if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err(){ + if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err() { return Err(anyhow!("Error while generating key")); } - match decrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], &keys_ct){ + match decrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], &keys_ct) { Ok(key_seed) => { // generate new random key map restore_keys(&key_seed, dt) @@ -98,7 +99,7 @@ pub fn restore_key_map(mkey: MasterKey, dt: DocumentType, keys_ct: Vec) -> a } } -pub fn restore_keys(secret: &String, dt: DocumentType) -> anyhow::Result{ +pub fn restore_keys(secret: &String, dt: DocumentType) -> anyhow::Result { debug!("restoring decryption key_map for doc type: '{}'", &dt.id); let kdf = restore_kdf(secret)?; let key_map = derive_key_map(kdf, dt, false); @@ -106,9 +107,9 @@ pub fn restore_keys(secret: &String, dt: DocumentType) -> anyhow::Result Ok(KeyMap::new(false, key_map, None)) } -fn restore_kdf(secret: &String) -> anyhow::Result>{ +fn restore_kdf(secret: &String) -> anyhow::Result> { debug!("restoring kdf from secret"); - let prk = match hex::decode(secret){ + let prk = match hex::decode(secret) { Ok(key) => key, Err(e) => { error!("Error while decoding master key: {}", e); @@ -116,7 +117,7 @@ fn restore_kdf(secret: &String) -> anyhow::Result>{ } }; - match Hkdf::::from_prk(prk.as_slice()){ + match Hkdf::::from_prk(prk.as_slice()) { Ok(kdf) => Ok(kdf), Err(e) => { error!("Error while instantiating hkdf: {}", e); @@ -125,7 +126,7 @@ fn restore_kdf(secret: &String) -> anyhow::Result>{ } } -pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Result>{ +pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Result> { // check key size if key.len() != EXP_KEY_SIZE { error!("Given key has size {} but expected {} bytes", key.len(), EXP_KEY_SIZE); @@ -135,13 +136,12 @@ pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Resul else if nonce.len() != EXP_NONCE_SIZE { error!("Given nonce has size {} but expected {} bytes", nonce.len(), EXP_NONCE_SIZE); Err(anyhow!("Incorrect nonce size")) - } - else{ + } else { let key = GenericArray::from_slice(key); let nonce = GenericArray::from_slice(nonce); let cipher = Aes256GcmSiv::new(key); - match cipher.encrypt(nonce, secret.as_bytes()){ + match cipher.encrypt(nonce, secret.as_bytes()) { Ok(ct) => { Ok(ct) } @@ -150,7 +150,7 @@ pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Resul } } -pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result{ +pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result { debug!("key len = {}", key.len()); debug!("ct len = {}", ct.len()); let key = GenericArray::from_slice(key); @@ -162,11 +162,11 @@ pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result { let pt = String::from_utf8(pt)?; Ok(pt) - }, + } Err(e) => { Err(anyhow!("Error while decrypting: {}", e)) } diff --git a/clearing-house-app/logging-service/src/db/config/doc_store.rs b/clearing-house-app/logging-service/src/db/config/doc_store.rs index 81c51e9..5c16598 100644 --- a/clearing-house-app/logging-service/src/db/config/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/config/doc_store.rs @@ -3,9 +3,9 @@ use mongodb::bson::doc; use mongodb::IndexModel; use mongodb::options::{CreateCollectionOptions, IndexOptions, WriteConcern}; use rocket::{Build, fairing, Rocket}; -use core_lib::constants::{CLEAR_DB, DATABASE_URL, DOCUMENT_DB, DOCUMENT_DB_CLIENT, MONGO_COLL_DOCUMENT_BUCKET, MONGO_DOC_ARRAY, MONGO_PID, MONGO_TS, MONGO_TC}; -use core_lib::db::init_database_client; -use core_lib::model::document::Document; +use crate::model::constants::{CLEAR_DB, DATABASE_URL, DOCUMENT_DB, DOCUMENT_DB_CLIENT, MONGO_COLL_DOCUMENT_BUCKET, MONGO_DOC_ARRAY, MONGO_PID, MONGO_TS, MONGO_TC}; +use crate::db::init_database_client; +use crate::model::document::Document; use crate::db::doc_store::DataStore; #[derive(Clone, Debug)] diff --git a/clearing-house-app/logging-service/src/db/config/keyring_store.rs b/clearing-house-app/logging-service/src/db/config/keyring_store.rs index cf83a77..508e949 100644 --- a/clearing-house-app/logging-service/src/db/config/keyring_store.rs +++ b/clearing-house-app/logging-service/src/db/config/keyring_store.rs @@ -1,9 +1,9 @@ use anyhow::anyhow; use rocket::fairing::Kind; use rocket::{Build, fairing, Rocket}; -use core_lib::constants::{CLEAR_DB, DATABASE_URL, FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT}; -use core_lib::db::init_database_client; -use core_lib::util::read_file; +use crate::model::constants::{CLEAR_DB, DATABASE_URL, FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT}; +use crate::db::init_database_client; +use crate::util::read_file; use crate::db::key_store::KeyStore; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; diff --git a/clearing-house-app/logging-service/src/db/config/process_store.rs b/clearing-house-app/logging-service/src/db/config/process_store.rs index e0fb2e7..1aef713 100644 --- a/clearing-house-app/logging-service/src/db/config/process_store.rs +++ b/clearing-house-app/logging-service/src/db/config/process_store.rs @@ -2,9 +2,9 @@ use anyhow::anyhow; use mongodb::options::{CreateCollectionOptions, WriteConcern}; use rocket::{Build, Rocket}; use rocket::fairing::Kind; -use core_lib::constants::{CLEAR_DB, DATABASE_URL, MONGO_COLL_TRANSACTIONS, PROCESS_DB, PROCESS_DB_CLIENT}; -use core_lib::db::init_database_client; -use crate::db::ProcessStore; +use crate::model::constants::{CLEAR_DB, DATABASE_URL, MONGO_COLL_TRANSACTIONS, PROCESS_DB, PROCESS_DB_CLIENT}; +use crate::db::init_database_client; +use crate::db::process_store::ProcessStore; #[derive(Clone, Debug)] pub struct ProcessStoreConfigurator; diff --git a/clearing-house-app/logging-service/src/db/doc_store.rs b/clearing-house-app/logging-service/src/db/doc_store.rs index 7babbf0..591353f 100644 --- a/clearing-house-app/logging-service/src/db/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/doc_store.rs @@ -2,12 +2,12 @@ use mongodb::{bson, Client}; use mongodb::bson::doc; use mongodb::options::{AggregateOptions, UpdateOptions}; use rocket::futures::StreamExt; -use core_lib::constants::{MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_ID, MONGO_PID, MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_TO_TS, MONGO_TC, MONGO_TS, DOCUMENT_DB}; -use core_lib::db::DataStoreApi; -use core_lib::model::document::EncryptedDocument; -use core_lib::errors::*; -use core_lib::model::SortingOrder; +use crate::model::constants::{MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_ID, MONGO_PID, MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_TO_TS, MONGO_TC, MONGO_TS, DOCUMENT_DB}; +use crate::db::DataStoreApi; +use crate::model::errors::*; use crate::db::doc_store::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; +use crate::model::document::EncryptedDocument; +use crate::model::SortingOrder; #[derive(Clone)] pub struct DataStore { @@ -25,7 +25,7 @@ impl DataStoreApi for DataStore { } impl DataStore { - pub async fn add_document(&self, doc: EncryptedDocument) -> Result { + pub async fn add_document(&self, doc: EncryptedDocument) -> errors::Result { debug!("add_document to bucket"); let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); let bucket_update = DocumentBucketUpdate::from(&doc); @@ -54,14 +54,14 @@ impl DataStore { } Err(e) => { error!("failed to store document: {:#?}", &e); - Err(Error::from(e)) + Err(errors::Error::from(e)) } } } /// checks if the document exists /// document ids are globally unique - pub async fn exists_document(&self, id: &String) -> Result { + pub async fn exists_document(&self, id: &String) -> errors::Result { debug!("Check if document with id '{}' exists...", id); let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone()}; @@ -79,7 +79,7 @@ impl DataStore { } /// gets the model from the db - pub async fn get_document(&self, id: &String, pid: &String) -> Result> { + pub async fn get_document(&self, id: &String, pid: &String) -> errors::Result> { debug!("Trying to get doc with id {}...", id); let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); @@ -103,7 +103,7 @@ impl DataStore { } /// gets documents for a single process from the db - pub async fn get_document_with_previous_tc(&self, tc: i64) -> Result> { + pub async fn get_document_with_previous_tc(&self, tc: i64) -> errors::Result> { let previous_tc = tc - 1; debug!("Trying to get document for tc {} ...", previous_tc); if previous_tc < 0 { @@ -134,7 +134,7 @@ impl DataStore { } /// gets a page of documents of a specific document type for a single process from the db defined by parameters page, size and sort - pub async fn get_documents_for_pid(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> Result> { + pub async fn get_documents_for_pid(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> errors::Result> { debug!("...trying to get page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); match self.get_start_bucket_size(dt_id, pid, page, size, sort, date_from, date_to).await { @@ -187,13 +187,13 @@ impl DataStore { } Err(e) => { error!("Error while getting bucket offset!"); - Err(Error::from(e)) + Err(errors::Error::from(e)) } } } /// offset is necessary for duration queries. There, start_entries of bucket depend on timestamps which usually creates an offset in the bucket - async fn get_start_bucket_size(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> Result { + async fn get_start_bucket_size(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> errors::Result { debug!("...trying to get the offset for page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); let sort_order = match sort { SortingOrder::Ascending => { @@ -264,7 +264,7 @@ impl DataStore { } mod bucket { - use core_lib::model::document::EncryptedDocument; + use super::{EncryptedDocument}; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentBucket { diff --git a/clearing-house-app/logging-service/src/db/key_store.rs b/clearing-house-app/logging-service/src/db/key_store.rs index 1abbbb8..bc3e185 100644 --- a/clearing-house-app/logging-service/src/db/key_store.rs +++ b/clearing-house-app/logging-service/src/db/key_store.rs @@ -1,10 +1,10 @@ use std::process::exit; -use core_lib::errors::*; +use crate::model::errors::*; use mongodb::bson::doc; use mongodb::Client; use rocket::futures::TryStreamExt; -use core_lib::constants::{KEYRING_DB, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; -use core_lib::db::DataStoreApi; +use crate::model::constants::{KEYRING_DB, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; +use super::DataStoreApi; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; @@ -78,7 +78,7 @@ impl KeyStore { } // DOCTYPE - pub async fn add_document_type(&self, doc_type: DocumentType) -> Result<()> { + pub async fn add_document_type(&self, doc_type: DocumentType) -> errors::Result<()> { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); match coll.insert_one(doc_type.clone(), None).await { Ok(_r) => { @@ -87,13 +87,13 @@ impl KeyStore { }, Err(e) => { tracing::error!("failed to log document type {}", &doc_type.id); - Err(Error::from(e)) + Err(errors::Error::from(e)) } } } //TODO: Do we need to check that no documents of this type exist before we remove it from the db? - pub async fn delete_document_type(&self, id: &String, pid: &String) -> Result { + pub async fn delete_document_type(&self, id: &String, pid: &String) -> errors::Result { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); let result = coll.delete_many(doc! { MONGO_ID: id, MONGO_PID: pid }, None).await?; if result.deleted_count >= 1 { @@ -105,7 +105,7 @@ impl KeyStore { /// checks if the model exits - pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> Result { + pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> errors::Result { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); let result = coll.find_one(Some(doc! { MONGO_ID: dt_id, MONGO_PID: pid }), None).await?; match result { @@ -117,26 +117,26 @@ impl KeyStore { } } - pub async fn get_all_document_types(&self) -> Result> { + pub async fn get_all_document_types(&self) -> errors::Result> { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); let result = coll.find(None, None).await? .try_collect().await.unwrap_or_else(|_| vec![]); Ok(result) } - pub async fn get_document_type(&self, dt_id: &String) -> Result> { + pub async fn get_document_type(&self, dt_id: &String) -> errors::Result> { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); tracing::debug!("get_document_type for dt_id: '{}'", dt_id); match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await{ Ok(result) => Ok(result), Err(e) => { tracing::error!("error while getting document type with id {}!", dt_id); - Err(Error::from(e)) + Err(errors::Error::from(e)) } } } - pub async fn update_document_type(&self, doc_type: DocumentType, id: &String) -> Result { + pub async fn update_document_type(&self, doc_type: DocumentType, id: &String) -> errors::Result { let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await{ Ok(r) => { diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/logging-service/src/db/mod.rs index 8276c20..16a5a57 100644 --- a/clearing-house-app/logging-service/src/db/mod.rs +++ b/clearing-house-app/logging-service/src/db/mod.rs @@ -1,141 +1,28 @@ pub(crate) mod key_store; pub(crate) mod doc_store; pub(crate) mod config; +pub(crate) mod process_store; -use core_lib::constants::{MONGO_ID, MONGO_COLL_PROCESSES, PROCESS_DB, MONGO_COLL_TRANSACTIONS, MONGO_TC}; -use core_lib::db::DataStoreApi; -use core_lib::errors::*; -use core_lib::model::process::Process; -use mongodb::bson::doc; -use mongodb::{Client, Database}; -use rocket::fairing::Fairing; -use rocket::futures::TryStreamExt; -use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions}; -use crate::model::TransactionCounter; +use error_chain::error_chain; +use mongodb::Client; +use mongodb::options::ClientOptions; +use crate::model::errors::*; -#[derive(Clone)] -pub struct ProcessStore { - client: Client, - database: Database +pub trait DataStoreApi{ + fn new(client: Client) -> Self; } -impl DataStoreApi for ProcessStore { - fn new(client: Client) -> ProcessStore{ - ProcessStore { - client: client.clone(), - database: client.database(PROCESS_DB) - } - } -} - -impl ProcessStore { - pub async fn get_transaction_counter(&self) -> Result>{ - debug!("Getting transaction counter..."); - let coll = self.database.collection::(MONGO_COLL_TRANSACTIONS); - match coll.find_one(None, None).await?{ - Some(t) => Ok(Some(t.tc)), - None => Ok(Some(0)) - } - } +pub async fn init_database_client(db_url: &str, client_name: Option) -> errors::Result{ + let mut client_options; - pub async fn increment_transaction_counter(&self) -> Result>{ - debug!("Getting transaction counter..."); - let coll = self.database.collection::(MONGO_COLL_TRANSACTIONS); - let mods = UpdateModifications::Document(doc!{"$inc": {MONGO_TC: 1 }}); - let mut opts = FindOneAndUpdateOptions::default(); - opts.upsert = Some(true); - match coll.find_one_and_update(doc!{}, mods, opts).await?{ - Some(t) => Ok(Some(t.tc)), - None => Ok(Some(0)) - } - } - - pub async fn get_processes(&self) -> Result> { - debug!("Trying to get all processes..."); - let coll = self.database.collection::(MONGO_COLL_PROCESSES); - let result = coll.find(None, None).await? - .try_collect().await.unwrap_or_else(|_| vec![]); - Ok(result) - } - - pub async fn delete_process(&self, pid: &String) -> Result { - debug!("Trying to delete process with pid '{}'...", pid); - let coll = self.database.collection::(MONGO_COLL_PROCESSES); - let result = coll.delete_one(doc! { MONGO_ID: pid }, None).await?; - if result.deleted_count == 1{ - debug!("... deleted one process."); - Ok(true) - } - else{ - warn!("deleted_count={}", result.deleted_count); - Ok(false) + match ClientOptions::parse(&format!("{}", db_url)).await{ + Ok(co) => {client_options = co;} + Err(_) => { + error_chain::bail!("Can't parse database connection string"); } - } + }; - /// checks if the id exits - pub async fn exists_process(&self, pid: &String) -> Result { - debug!("Check if process with pid '{}' exists...", pid); - let coll = self.database.collection::(MONGO_COLL_PROCESSES); - let result = coll.find_one(Some(doc! { MONGO_ID: pid }), None).await?; - match result { - Some(_r) => { - debug!("... found."); - Ok(true) - }, - None => { - debug!("Process with pid '{}' does not exist!", pid); - Ok(false) - } - } - } - - pub async fn get_process(&self, pid: &String) -> Result> { - debug!("Trying to get process with id {}...", pid); - let coll = self.database.collection::(MONGO_COLL_PROCESSES); - match coll.find_one(Some(doc!{ MONGO_ID: pid }), None).await{ - Ok(process) => { - Ok(process) - }, - Err(e) => { - error!("Error while getting process: {:#?}!", &e); - Err(Error::from(e)) - } - } - } - - pub async fn is_authorized(&self, user: &String, pid: &String) -> Result{ - debug!("checking if user '{}' is authorized to access '{}'", user, pid); - return match self.get_process(&pid).await{ - Ok(Some(process)) => { - let authorized = process.owners.iter().any(|o| { - trace!("found owner {}", o); - user.eq(o) - }); - Ok(authorized) - } - Ok(None) => { - trace!("didn't find process"); - Ok(false) - }, - _ => { - Err(format!("User '{}' could not be authorized", &user).into()) - } - } - } - - // store process in db - pub async fn store_process(&self, process: Process) -> Result { - debug!("Storing process with pid {:#?}...", &process.id); - let coll = self.database.collection::(MONGO_COLL_PROCESSES); - match coll.insert_one(process, None).await { - Ok(_r) => { - debug!("...added new process: {}", &_r.inserted_id); - Ok(true) - }, - Err(e) => { - error!("...failed to store process: {:#?}", &e); - Err(Error::from(e)) - } - } - } + client_options.app_name = client_name; + let client = Client::with_options(client_options)?; + Ok(T::new(client)) } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/db/process_store.rs b/clearing-house-app/logging-service/src/db/process_store.rs new file mode 100644 index 0000000..e703577 --- /dev/null +++ b/clearing-house-app/logging-service/src/db/process_store.rs @@ -0,0 +1,135 @@ +use crate::model::constants::{MONGO_ID, MONGO_COLL_PROCESSES, PROCESS_DB, MONGO_COLL_TRANSACTIONS, MONGO_TC}; +use crate::db::DataStoreApi; +use crate::model::errors::*; +use crate::model::process::Process; +use mongodb::bson::doc; +use mongodb::{Client, Database}; +use rocket::futures::TryStreamExt; +use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions}; +use crate::model::process::TransactionCounter; + +#[derive(Clone)] +pub struct ProcessStore { + pub(crate) client: Client, + database: Database, +} + +impl DataStoreApi for ProcessStore { + fn new(client: Client) -> ProcessStore { + ProcessStore { + client: client.clone(), + database: client.database(PROCESS_DB), + } + } +} + +impl ProcessStore { + pub async fn get_transaction_counter(&self) -> errors::Result> { + debug!("Getting transaction counter..."); + let coll = self.database.collection::(MONGO_COLL_TRANSACTIONS); + match coll.find_one(None, None).await? { + Some(t) => Ok(Some(t.tc)), + None => Ok(Some(0)) + } + } + + pub async fn increment_transaction_counter(&self) -> errors::Result> { + debug!("Getting transaction counter..."); + let coll = self.database.collection::(MONGO_COLL_TRANSACTIONS); + let mods = UpdateModifications::Document(doc! {"$inc": {MONGO_TC: 1 }}); + let mut opts = FindOneAndUpdateOptions::default(); + opts.upsert = Some(true); + match coll.find_one_and_update(doc! {}, mods, opts).await? { + Some(t) => Ok(Some(t.tc)), + None => Ok(Some(0)) + } + } + + pub async fn get_processes(&self) -> errors::Result> { + debug!("Trying to get all processes..."); + let coll = self.database.collection::(MONGO_COLL_PROCESSES); + let result = coll.find(None, None).await? + .try_collect().await.unwrap_or_else(|_| vec![]); + Ok(result) + } + + pub async fn delete_process(&self, pid: &String) -> errors::Result { + debug!("Trying to delete process with pid '{}'...", pid); + let coll = self.database.collection::(MONGO_COLL_PROCESSES); + let result = coll.delete_one(doc! { MONGO_ID: pid }, None).await?; + if result.deleted_count == 1 { + debug!("... deleted one process."); + Ok(true) + } else { + warn!("deleted_count={}", result.deleted_count); + Ok(false) + } + } + + /// checks if the id exits + pub async fn exists_process(&self, pid: &String) -> errors::Result { + debug!("Check if process with pid '{}' exists...", pid); + let coll = self.database.collection::(MONGO_COLL_PROCESSES); + let result = coll.find_one(Some(doc! { MONGO_ID: pid }), None).await?; + match result { + Some(_r) => { + debug!("... found."); + Ok(true) + } + None => { + debug!("Process with pid '{}' does not exist!", pid); + Ok(false) + } + } + } + + pub async fn get_process(&self, pid: &String) -> errors::Result> { + debug!("Trying to get process with id {}...", pid); + let coll = self.database.collection::(MONGO_COLL_PROCESSES); + match coll.find_one(Some(doc! { MONGO_ID: pid }), None).await { + Ok(process) => { + Ok(process) + } + Err(e) => { + error!("Error while getting process: {:#?}!", &e); + Err(errors::Error::from(e)) + } + } + } + + pub async fn is_authorized(&self, user: &String, pid: &String) -> errors::Result { + debug!("checking if user '{}' is authorized to access '{}'", user, pid); + return match self.get_process(&pid).await { + Ok(Some(process)) => { + let authorized = process.owners.iter().any(|o| { + trace!("found owner {}", o); + user.eq(o) + }); + Ok(authorized) + } + Ok(None) => { + trace!("didn't find process"); + Ok(false) + } + _ => { + Err(format!("User '{}' could not be authorized", &user).into()) + } + }; + } + + // store process in db + pub async fn store_process(&self, process: Process) -> errors::Result { + debug!("Storing process with pid {:#?}...", &process.id); + let coll = self.database.collection::(MONGO_COLL_PROCESSES); + match coll.insert_one(process, None).await { + Ok(_r) => { + debug!("...added new process: {}", &_r.inserted_id); + Ok(true) + } + Err(e) => { + error!("...failed to store process: {:#?}", &e); + Err(errors::Error::from(e)) + } + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index eaf6491..bb2478c 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -4,13 +4,13 @@ extern crate tracing; use std::path::Path; -use core_lib::util::{add_service_config}; use rocket::fairing::AdHoc; -use core_lib::constants::ENV_LOGGING_SERVICE_ID; +use crate::model::constants::ENV_LOGGING_SERVICE_ID; use db::config::doc_store::DatastoreConfigurator; use db::config::keyring_store::KeyringDbConfigurator; use db::config::process_store::ProcessStoreConfigurator; use model::constants::SIGNING_KEY; +use crate::util::add_service_config; mod db; mod model; @@ -18,6 +18,7 @@ mod services; mod crypto; mod ports; mod config; +mod util; pub fn add_signing_key() -> AdHoc { AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { diff --git a/clearing-house-app/logging-service/src/model/claims.rs b/clearing-house-app/logging-service/src/model/claims.rs new file mode 100644 index 0000000..f9a8921 --- /dev/null +++ b/clearing-house-app/logging-service/src/model/claims.rs @@ -0,0 +1,169 @@ +use std::env; +use std::fmt::{Display, Formatter}; +use biscuit::{ClaimPresenceOptions, ClaimsSet, Empty, jwa::SignatureAlgorithm, JWT, RegisteredClaims, SingleOrMultiple, Timestamp, ValidationOptions}; +use biscuit::jwk::{AlgorithmParameters, CommonParameters, JWKSet}; +use biscuit::{jws, jws::Secret}; +use biscuit::Presence::Required; +use biscuit::Validation::Validate; +use chrono::{Duration, Utc}; +use num_bigint::BigUint; +use ring::signature::KeyPair; +use rocket::http::Status; +use rocket::request::{Request, FromRequest, Outcome}; +use crate::model::errors::*; +use crate::model::constants::{ENV_SHARED_SECRET, SERVICE_HEADER}; +use crate::util::ServiceConfig; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ChClaims{ + pub client_id: String, +} + +impl ChClaims{ + pub fn new(client_id: &str) -> ChClaims{ + ChClaims{ + client_id: client_id.to_string(), + } + } +} + +impl Display for ChClaims{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "<{}>", self.client_id) + } +} + +#[derive(Debug)] +pub enum ChClaimsError { + Missing, + Invalid, +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for ChClaims { + type Error = ChClaimsError; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + match request.headers().get_one(&SERVICE_HEADER) { + None => Outcome::Failure((Status::BadRequest, ChClaimsError::Missing)), + Some(token) => { + debug!("...received service header: {:?}", token); + let service_config = request.rocket().state::().unwrap(); + match decode_token::(token, service_config.service_id.as_str()){ + Ok(claims) => { + debug!("...retrieved claims and succeed"); + Outcome::Success(claims) + }, + Err(_) => Outcome::Failure((Status::BadRequest, ChClaimsError::Invalid)) + } + } + } + } +} + +pub fn get_jwks(key_path: &str) -> Option>{ + let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); + + if let biscuit::jws::Secret::RsaKeyPair(a) = keypair{ + let pk_modulus = BigUint::from_bytes_be(a.as_ref().public_key().modulus().big_endian_without_leading_zero()); + let pk_e = BigUint::from_bytes_be(a.as_ref().public_key().exponent().big_endian_without_leading_zero()); + + let params = biscuit::jwk::RSAKeyParameters{ + n: pk_modulus, + e: pk_e, + ..Default::default() + }; + + let mut common = CommonParameters::default(); + common.key_id = get_fingerprint(key_path); + + let jwk = biscuit::jwk::JWK::{ + common, + algorithm: AlgorithmParameters::RSA(params), + additional: Empty::default(), + }; + + let jwks = biscuit::jwk::JWKSet::{ + keys: vec!(jwk) + }; + return Some(jwks) + } + None +} + +pub fn get_fingerprint(key_path: &str) -> Option{ + let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); + if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { + let pk_modulus = a.as_ref().public_key().modulus().big_endian_without_leading_zero().to_vec(); + let pk_e = a.as_ref().public_key().exponent().big_endian_without_leading_zero().to_vec(); + + let pk = openssh_keys::PublicKey::from_rsa(pk_e, pk_modulus); + return Some(pk.fingerprint()) + } + None +} + +pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> String{ + let private_claims = ChClaims::new(client_id); + create_token(issuer, audience, &private_claims) +} + +pub fn create_token serde::Deserialize<'de>> (issuer: &str, audience: &str, private_claims: &T) -> String{ + let signing_secret = match env::var(ENV_SHARED_SECRET){ + Ok(secret) => { + Secret::Bytes(secret.to_string().into_bytes()) + }, + Err(_) => { + panic!("Shared Secret not configured. Please configure environment variable {}", ENV_SHARED_SECRET); + } + }; + let expiration_date = Utc::now() + Duration::minutes(5); + + let claims = ClaimsSet::{ + registered: RegisteredClaims{ + issuer: Some(issuer.to_string()), + issued_at: Some(Timestamp::from(Utc::now())), + audience: Some(SingleOrMultiple::Single(audience.to_string())), + expiry: Some(Timestamp::from(expiration_date)), + ..Default::default() + }, + private: private_claims.clone() + }; + + // Construct the JWT + let jwt = jws::Compact::new_decoded( + From::from(jws::RegisteredHeader { + algorithm: SignatureAlgorithm::HS256, + ..Default::default() + }), + claims.clone() + ); + + jwt.into_encoded(&signing_secret).unwrap().unwrap_encoded().to_string() +} + +pub fn decode_token serde::Deserialize<'de>>(token: &str, audience: &str) -> errors::Result{ + let signing_secret = match env::var(ENV_SHARED_SECRET){ + Ok(secret) => { + Secret::Bytes(secret.to_string().into_bytes()) + }, + Err(e) => { + error!("Shared Secret not configured. Please configure environment variable {}", ENV_SHARED_SECRET); + return Err(errors::Error::from(e)) + } + }; + let jwt: jws::Compact, Empty> = JWT::<_, Empty>::new_encoded(token); + let decoded_jwt = jwt.decode(&signing_secret,SignatureAlgorithm::HS256)?; + let mut val_options = ValidationOptions::default(); + let mut claim_presence_options = ClaimPresenceOptions::default(); + claim_presence_options.expiry = Required; + claim_presence_options.issuer = Required; + claim_presence_options.audience = Required; + claim_presence_options.issued_at = Required; + val_options.claim_presence_options = claim_presence_options; + val_options.issued_at = Validate(Duration::minutes(5)); + // Issuer is not validated. Wouldn't make much of a difference if we did + val_options.audience = Validate(audience.to_string()); + assert!(decoded_jwt.validate(val_options).is_ok()); + Ok(decoded_jwt.payload().unwrap().private.clone()) +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/model/constants.rs b/clearing-house-app/logging-service/src/model/constants.rs index 780cd30..2b33c76 100644 --- a/clearing-house-app/logging-service/src/model/constants.rs +++ b/clearing-house-app/logging-service/src/model/constants.rs @@ -5,7 +5,96 @@ pub const SIGNING_KEY: &'static str = "signing_key"; pub const CLEARING_HOUSE_URL: &'static str = "clearing_house_url"; pub const ROCKET_CLEARING_HOUSE_BASE_API: &'static str = "/messages"; pub const ROCKET_PK_API: &'static str = "/"; -pub const ROCKET_PROCESS_API: &'static str = "/process"; pub const ROCKET_QUERY_API: &'static str = "/query"; pub const ROCKET_LOG_API: &'static str = "/log"; pub const ROCKET_BLOCKCHAIN_BASE_API: &'static str = "/blockchain"; + +// From core_lib + +// definition of daps constants +pub const DAPS_AUD: &'static str = "idsc:IDS_CONNECTORS_ALL"; +pub const DAPS_JWKS: &'static str = ".well-known/jwks.json"; +pub const DAPS_KID: &'static str = "default"; +pub const DAPS_AUTHHEADER: &'static str = "Authorization"; +pub const DAPS_AUTHBEARER: &'static str = "Bearer"; +pub const DAPS_CERTIFICATES: &'static str = "certs"; + +// definition of custom headers +pub const SERVICE_HEADER: &'static str = "CH-SERVICE"; + +// definition of config parameters (in config files) +pub const DATABASE_URL: &'static str = "database_url"; +pub const DOCUMENT_API_URL: &'static str = "document_api_url"; +pub const KEYRING_API_URL: &'static str = "keyring_api_url"; +pub const DAPS_API_URL: &'static str = "daps_api_url"; +pub const CLEAR_DB: &'static str = "clear_db"; + +// define here the config options from environment variables +pub const ENV_API_LOG_LEVEL: &'static str = "API_LOG_LEVEL"; +pub const ENV_SHARED_SECRET: &'static str = "SHARED_SECRET"; +pub const ENV_DOCUMENT_SERVICE_ID: &'static str = "SERVICE_ID_DOC"; +pub const ENV_KEYRING_SERVICE_ID: &'static str = "SERVICE_ID_KEY"; +pub const ENV_LOGGING_SERVICE_ID: &'static str = "SERVICE_ID_LOG"; + +// definition of rocket mount points +pub const ROCKET_DOC_API: &'static str = "/doc"; +pub const ROCKET_DOC_TYPE_API: &'static str = "/doctype"; +pub const ROCKET_POLICY_API: &'static str = "/policy"; +pub const ROCKET_STATISTICS: &'static str = "/statistics"; +pub const ROCKET_PROCESS_API: &'static str = "/process"; +pub const ROCKET_KEYRING_API: &'static str = "/keyring"; +pub const ROCKET_USER_API: &'static str = "/users"; + +// definition of service names +pub const DOCUMENT_DB_CLIENT: &'static str = "document-api"; +pub const KEYRING_DB_CLIENT: &'static str = "keyring-api"; +pub const PROCESS_DB_CLIENT: &'static str = "logging-service"; + +// definition of table names +pub const MONGO_DB: &'static str = "ch_ids"; +pub const DOCUMENT_DB: &'static str = "document"; +pub const KEYRING_DB: &'static str = "keyring"; +pub const PROCESS_DB: &'static str = "process"; +pub const MONGO_COLL_DOCUMENTS: &'static str = "documents"; +pub const MONGO_COLL_DOCUMENT_BUCKET: &'static str = "document_bucket"; +pub const MONGO_COLL_DOC_TYPES: &'static str = "doc_types"; +pub const MONGO_COLL_DOC_PARTS: &'static str = "parts"; +pub const MONGO_COLL_PROCESSES: &'static str = "processes"; +pub const MONGO_COLL_TRANSACTIONS: &'static str = "transactions"; +pub const MONGO_COLL_MASTER_KEY: &'static str = "keys"; + +// definition of database fields +pub const MONGO_ID: &'static str = "id"; +pub const MONGO_MKEY: &'static str = "msk"; +pub const MONGO_PID: &'static str = "pid"; +pub const MONGO_DT_ID: &'static str = "dt_id"; +pub const MONGO_NAME: &'static str = "name"; +pub const MONGO_OWNER: &'static str = "owner"; +pub const MONGO_TS: &'static str = "ts"; +pub const MONGO_TC: &'static str = "tc"; + +pub const MONGO_DOC_ARRAY: &'static str = "documents"; +pub const MONGO_COUNTER: &'static str = "counter"; +pub const MONGO_FROM_TS: &'static str = "from_ts"; +pub const MONGO_TO_TS: &'static str = "to_ts"; + +// definition of default database values +pub const DEFAULT_PROCESS_ID: &'static str = "default"; +pub const MAX_NUM_RESPONSE_ENTRIES: u64 = 1000; +pub const DEFAULT_NUM_RESPONSE_ENTRIES: u64 = 100; + +pub const DEFAULT_DOC_TYPE: &'static str = "IDS_MESSAGE"; + +// split string symbols for vec_to_string and string_to_vec +pub const SPLIT_QUOTE: &'static str = "'"; +pub const SPLIT_SIGN: &'static str = "~"; +pub const SPLIT_CT: &'static str = "::"; + +// definition of file names and folders +pub const FOLDER_DB: &'static str = "db_init"; +pub const FOLDER_DATA: &'static str = "data"; +pub const FILE_DOC: &'static str = "document.json"; +pub const FILE_DEFAULT_DOC_TYPE: &'static str = "init_db/default_doc_type.json"; + +// definition of special document parts +pub const PAYLOAD_PART: &'static str = "payload"; diff --git a/clearing-house-app/logging-service/src/model/crypto.rs b/clearing-house-app/logging-service/src/model/crypto.rs index f45b9a6..0f091a5 100644 --- a/clearing-house-app/logging-service/src/model/crypto.rs +++ b/clearing-house-app/logging-service/src/model/crypto.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use crate::crypto::generate_random_seed; use hkdf::Hkdf; use sha2::Sha256; @@ -26,4 +27,85 @@ impl MasterKey{ MasterKey::new(new_uuid(), hex::encode_upper(master_key), hex::encode_upper(generate_random_seed())) } +} + + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct KeyEntry { + pub id: String, + pub key: Vec, + pub nonce: Vec, +} + +impl KeyEntry{ + pub fn new(id: String, key: Vec, nonce: Vec)-> KeyEntry{ + KeyEntry{ + id, + key, + nonce + } + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct KeyMap { + pub enc: bool, + pub keys: HashMap, + pub keys_enc: Option>, +} + +impl KeyMap{ + pub fn new(enc: bool, keys: HashMap, keys_enc: Option>) -> KeyMap{ + KeyMap{ + enc, + keys, + keys_enc + } + } +} + + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct KeyCt{ + pub id: String, + pub ct: String +} + +impl KeyCt{ + pub fn new(id: String, ct: String) -> KeyCt{ + KeyCt{ + id, + ct + } + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct KeyCtList { + pub dt: String, + pub cts: Vec +} + +impl KeyCtList{ + pub fn new(dt: String, cts: Vec) -> KeyCtList{ + KeyCtList{ + dt, + cts + } + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct KeyMapListItem { + pub id: String, + pub map: KeyMap +} + +impl KeyMapListItem{ + pub fn new(id: String, map: KeyMap) -> KeyMapListItem{ + KeyMapListItem{ + id, + map + } + } } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/model/document.rs b/clearing-house-app/logging-service/src/model/document.rs new file mode 100644 index 0000000..15decf0 --- /dev/null +++ b/clearing-house-app/logging-service/src/model/document.rs @@ -0,0 +1,286 @@ +use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; +use aes_gcm_siv::aead::{Aead}; +use blake2_rfc::blake2b::Blake2b; +use generic_array::GenericArray; +use std::collections::HashMap; +use uuid::Uuid; +use crate::model::errors::*; +use crate::model::constants::{SPLIT_CT}; +use crate::model::util::new_uuid; +use crate::model::crypto::{KeyEntry, KeyMap}; +use chrono::Local; + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct DocumentPart { + pub name: String, + pub content: Option, +} + +impl DocumentPart { + pub fn new(name: String, content: Option) -> DocumentPart { + DocumentPart { + name, + content, + } + } + + pub fn encrypt(&self, key: &[u8], nonce: &[u8]) -> errors::Result> { + const EXP_KEY_SIZE: usize = 32; + const EXP_NONCE_SIZE: usize = 12; + // check key size + if key.len() != EXP_KEY_SIZE { + error!("Given key has size {} but expected {} bytes", key.len(), EXP_KEY_SIZE); + error_chain::bail!("Incorrect key size") + } + // check nonce size + else if nonce.len() != EXP_NONCE_SIZE { + error!("Given nonce has size {} but expected {} bytes", nonce.len(), EXP_NONCE_SIZE); + error_chain::bail!("Incorrect nonce size") + } else { + let key = GenericArray::from_slice(key); + let nonce = GenericArray::from_slice(nonce); + let cipher = Aes256GcmSiv::new(key); + + match &self.content { + Some(pt) => { + let pt = format_pt_for_storage(&self.name, pt); + match cipher.encrypt(nonce, pt.as_bytes()) { + Ok(ct) => Ok(ct), + Err(e) => error_chain::bail!("Error while encrypting {}", e) + } + } + None => { + error!("Tried to encrypt empty document part."); + error_chain::bail!("Nothing to encrypt"); + } + } + } + } + + pub fn decrypt(key: &[u8], nonce: &[u8], ct: &[u8]) -> errors::Result { + let key = GenericArray::from_slice(key); + let nonce = GenericArray::from_slice(nonce); + let cipher = Aes256GcmSiv::new(key); + + match cipher.decrypt(nonce, ct) { + Ok(pt) => { + let pt = String::from_utf8(pt)?; + let (name, content) = restore_pt_no_dt(&pt)?; + Ok(DocumentPart::new(name, Some(content))) + } + Err(e) => { + error_chain::bail!("Error while decrypting: {}", e) + } + } + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct Document { + #[serde(default = "new_uuid")] + pub id: String, + pub dt_id: String, + pub pid: String, + pub ts: i64, + pub tc: i64, + pub parts: Vec, +} + +/// Documents should have a globally unique id, setting the id manually is discouraged. +impl Document { + pub fn create_uuid() -> String { + Uuid::new_v4().hyphenated().to_string() + } + + // each part is encrypted using the part specific key from the key map + // the hash is set to "0". Chaining is not done here. + pub fn encrypt(&self, key_map: KeyMap) -> errors::Result { + debug!("encrypting document of doc_type {}", self.dt_id); + let mut cts = vec!(); + + let keys = key_map.keys; + let key_ct; + match key_map.keys_enc { + Some(ct) => { + key_ct = hex::encode(ct); + } + None => { + error_chain::bail!("Missing key ct"); + } + } + + for part in self.parts.iter() { + if part.content.is_none() { + // no content, so we skip this one + continue; + } + // check if there's a key for this part + if !keys.contains_key(&part.name) { + error!("Missing key for part '{}'", &part.name); + error_chain::bail!("Missing key for part '{}'", &part.name); + } + // get the key for this part + let key_entry = keys.get(&part.name).unwrap(); + let ct = part.encrypt(key_entry.key.as_slice(), key_entry.nonce.as_slice()); + if ct.is_err() { + warn!("Encryption error. No ct received!"); + error_chain::bail!("Encryption error. No ct received!"); + } + let ct_string = hex::encode_upper(ct.unwrap()); + + // key entry id is needed for decryption + cts.push(format!("{}::{}", key_entry.id, ct_string)); + } + cts.sort(); + + Ok(EncryptedDocument::new(self.id.clone(), self.pid.clone(), self.dt_id.clone(), self.ts, self.tc, key_ct, cts)) + } + + pub fn get_formatted_tc(&self) -> String { + format_tc(self.tc) + } + + pub fn get_parts_map(&self) -> HashMap> { + let mut p_map = HashMap::new(); + for part in self.parts.iter() { + p_map.insert(part.name.clone(), part.content.clone()); + } + p_map + } + + pub fn new(pid: String, dt_id: String, tc: i64, parts: Vec) -> Document { + Document { + id: Document::create_uuid(), + dt_id, + pid, + ts: Local::now().timestamp(), + tc, + parts, + } + } + + fn restore(id: String, pid: String, dt_id: String, ts: i64, tc: i64, parts: Vec) -> Document { + Document { + id, + dt_id, + pid, + ts, + tc, + parts, + } + } +} + +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] +pub struct EncryptedDocument { + pub id: String, + pub pid: String, + pub dt_id: String, + pub ts: i64, + pub tc: i64, + pub hash: String, + pub keys_ct: String, + pub cts: Vec, +} + +impl EncryptedDocument { + /// Note: KeyMap keys need to be KeyEntry.ids in this case + // Decryption is done without checking the hashes. Do this before calling this method + pub fn decrypt(&self, keys: HashMap) -> errors::Result { + let mut pts = vec!(); + for ct in self.cts.iter() { + let ct_parts = ct.split(SPLIT_CT).collect::>(); + if ct_parts.len() != 2 { + error_chain::bail!("Integrity violation! Ciphertexts modified"); + } + // get key and nonce + let key_entry = keys.get(ct_parts[0]); + if key_entry.is_none() { + error_chain::bail!("Key for id '{}' does not exist!", ct_parts[0]); + } + let key = key_entry.unwrap().key.as_slice(); + let nonce = key_entry.unwrap().nonce.as_slice(); + + // get ciphertext + //TODO: use error_chain? + let ct = hex::decode(ct_parts[1]).unwrap(); + + // decrypt + match DocumentPart::decrypt(key, nonce, ct.as_slice()) { + Ok(part) => pts.push(part), + Err(e) => { + error_chain::bail!("Error while decrypting: {}", e); + } + } + } + + Ok(Document::restore(self.id.clone(), self.pid.clone(), self.dt_id.clone(), self.ts, self.tc, pts)) + } + + pub fn get_formatted_tc(&self) -> String { + format_tc(self.tc) + } + + pub fn hash(&self) -> String { + let mut hasher = Blake2b::new(64); + + hasher.update(self.id.as_bytes()); + hasher.update(self.pid.as_bytes()); + hasher.update(self.dt_id.as_bytes()); + hasher.update(self.get_formatted_tc().as_bytes()); + hasher.update(self.ts.to_string().as_bytes()); + hasher.update(self.hash.as_bytes()); + hasher.update(self.keys_ct.as_bytes()); + let mut cts = self.cts.clone(); + cts.sort(); + for ct in cts.iter() { + hasher.update(ct.as_bytes()); + } + + let res = base64::encode(&hasher.finalize()); + debug!("hashed cts: '{}'", &res); + res + } + + pub fn new(id: String, pid: String, dt_id: String, ts: i64, tc: i64, keys_ct: String, cts: Vec) -> EncryptedDocument { + EncryptedDocument { + id, + pid, + dt_id, + ts, + tc, + hash: String::from("0"), + keys_ct, + cts, + } + } +} + +/// companion to format_pt_for_storage +pub fn restore_pt(pt: &str) -> errors::Result<(String, String, String)> { + trace!("Trying to restore plain text"); + let vec: Vec<&str> = pt.split(SPLIT_CT).collect(); + if vec.len() != 3 { + error_chain::bail!("Could not restore plaintext"); + } + Ok((String::from(vec[0]), String::from(vec[1]), String::from(vec[2]))) +} + +/// companion to format_pt_for_storage_no_dt +pub fn restore_pt_no_dt(pt: &str) -> errors::Result<(String, String)> { + trace!("Trying to restore plain text"); + let vec: Vec<&str> = pt.split(SPLIT_CT).collect(); + if vec.len() != 2 { + error_chain::bail!("Could not restore plaintext"); + } + Ok((String::from(vec[0]), String::from(vec[1]))) +} + +/// formats the pt before encryption +fn format_pt_for_storage(field_name: &str, pt: &str) -> String { + format!("{}{}{}", field_name, SPLIT_CT, pt) +} + +fn format_tc(tc: i64) -> String { + format!("{:08}", tc) +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/model/errors.rs b/clearing-house-app/logging-service/src/model/errors.rs new file mode 100644 index 0000000..4e9f661 --- /dev/null +++ b/clearing-house-app/logging-service/src/model/errors.rs @@ -0,0 +1,18 @@ +pub mod errors { + use error_chain::error_chain; + // Create the Error, ErrorKind, ResultExt, and Result types + error_chain!{ + foreign_links { + Conversion(std::num::TryFromIntError); + Figment(rocket::figment::Error); + HexError(hex::FromHexError); + Io(::std::io::Error) #[cfg(unix)]; + Mongodb(mongodb::error::Error); + MongodbBson(mongodb::bson::de::Error); + SerdeJson(serde_json::error::Error); + Uft8Error(std::string::FromUtf8Error); + BiscuitError(biscuit::errors::Error); + EnvVariable(::std::env::VarError); + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/model/ids/message.rs b/clearing-house-app/logging-service/src/model/ids/message.rs index 4f02b0a..08d5ee1 100644 --- a/clearing-house-app/logging-service/src/model/ids/message.rs +++ b/clearing-house-app/logging-service/src/model/ids/message.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use core_lib::constants::DEFAULT_DOC_TYPE; -use core_lib::model::document::{Document, DocumentPart}; +use crate::model::constants::DEFAULT_DOC_TYPE; +use crate::model::document::{Document, DocumentPart}; use crate::model::ids::{InfoModelDateTime, InfoModelId, SecurityToken, MessageType}; const MESSAGE_ID: &'static str = "message_id"; diff --git a/clearing-house-app/logging-service/src/model/mod.rs b/clearing-house-app/logging-service/src/model/mod.rs index e510986..f957540 100644 --- a/clearing-house-app/logging-service/src/model/mod.rs +++ b/clearing-house-app/logging-service/src/model/mod.rs @@ -1,85 +1,100 @@ use biscuit::{Empty, CompactJson}; use biscuit::jws::{Compact, Header}; use biscuit::jwa::SignatureAlgorithm; -use core_lib::api::crypto::get_fingerprint; pub mod constants; pub mod ids; pub(crate) mod crypto; pub(crate) mod doc_type; +pub(crate) mod errors; +pub(crate) mod document; +pub(crate) mod util; +pub(crate) mod process; +pub(crate) mod claims; -#[derive(serde::Serialize, serde::Deserialize)] -pub struct TransactionCounter{ - pub tc: i64 -} -#[derive(serde::Serialize, serde::Deserialize)] -pub struct OwnerList{ - pub owners: Vec +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rocket::FromFormField)] +pub enum SortingOrder { + #[field(value = "asc")] + #[serde(rename = "asc")] + Ascending, + #[field(value = "desc")] + #[serde(rename = "desc")] + Descending, } -impl OwnerList{ - pub fn new(owners: Vec) -> OwnerList{ - OwnerList{ - owners, - } + +pub fn parse_date(date: Option, to_date: bool) -> Option { + let time_format; + if to_date { + time_format = "23:59:59" + } else { + time_format = "00:00:00" } -} -#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] -pub struct Receipt { - pub data: Compact + match date { + Some(d) => { + debug!("Parsing date: {}", &d); + match chrono::NaiveDateTime::parse_from_str(format!("{} {}", &d, &time_format).as_str(), "%Y-%m-%d %H:%M:%S") { + Ok(date) => { + Some(date) + } + Err(e) => { + error!("Error occurred: {:#?}", e); + return None; + } + } + } + None => None + } } -#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] -pub struct DataTransaction { - pub transaction_id: String, - pub timestamp: i64, - pub process_id: String, - pub document_id: String, - pub payload: String, - pub chain_hash: String, - pub client_id: String, - pub clearing_house_version: String, -} +pub fn sanitize_dates(date_from: Option, date_to: Option) -> (chrono::NaiveDateTime, chrono::NaiveDateTime) { + let default_to_date = chrono::Local::now().naive_local(); + let default_from_date = default_to_date.date() + .and_hms_opt(0, 0, 0) + .expect("00:00:00 is a valid time") - chrono::Duration::weeks(2); -impl CompactJson for DataTransaction{} + println!("date_to: {:#?}", date_to); + println!("date_from: {:#?}", date_from); -impl DataTransaction{ - pub fn sign(&self, key_path: &str) -> Receipt{ - let jws = biscuit::jws::Compact::new_decoded(Header::from_registered_header(biscuit::jws::RegisteredHeader{ - algorithm: SignatureAlgorithm::PS512, - media_type: None, - key_id: get_fingerprint(key_path), - ..Default::default()}), self.clone()); + println!("Default date_to: {:#?}", default_to_date); + println!("Default date_from: {:#?}", default_from_date); - let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); - println!("decoded JWS:{:#?}", &jws); - Receipt{ - data: jws.into_encoded(&keypair).unwrap() - } + match (date_from, date_to) { + (Some(from), Some(to)) => (from, to), // validate already checked that date_from > date_to + (Some(from), None) => (from, default_to_date), // if to_date is missing, default to now + (None, Some(_to)) => todo!("Not defined yet; check"), + (None, None) => (default_from_date, default_to_date), // if both dates are none (case to_date is none and from_date is_some should be catched by validation); return dates for default duration (last 2 weeks) } } -// convenience method for testing -impl From for DataTransaction{ - fn from(r: Receipt) -> Self { - match r.data.unverified_payload(){ - Ok(d) => d.clone(), - Err(e) => { - println!("Error occured: {:#?}", e); - DataTransaction{ +pub fn validate_dates(date_from: Option, date_to: Option) -> bool { + let date_now = chrono::Local::now().naive_local(); + debug!("... validating dates: now: {:#?} , from: {:#?} , to: {:#?}", &date_now, &date_from, &date_to); + // date_from before now + if date_from.is_some() && date_from.as_ref().unwrap().clone() > date_now { + debug!("oh no, date_from {:#?} is in the future! date_now is {:#?}", &date_from, &date_now); + return false; + } - transaction_id: "error".to_string(), - timestamp: 0, - process_id: "error".to_string(), - document_id: "error".to_string(), - payload: "error".to_string(), - chain_hash: "error".to_string(), - client_id: "error".to_string(), - clearing_house_version: "error".to_string(), - } - } + // date_to only if there is also date_from + if date_from.is_none() && date_to.is_some() { + return false; + } + + // date_to before or equals now + if date_to.is_some() && date_to.as_ref().unwrap().clone() >= date_now { + debug!("oh no, date_to {:#?} is in the future! date_now is {:#?}", &date_to, &date_now); + return false; + } + + // date_from before date_to + if date_from.is_some() && date_to.is_some() { + if date_from.unwrap() > date_to.unwrap() { + debug!("oh no, date_from {:#?} is before date_to {:#?}", &date_from, &date_to); + return false; } } + return true; } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/model/process.rs b/clearing-house-app/logging-service/src/model/process.rs new file mode 100644 index 0000000..767a31d --- /dev/null +++ b/clearing-house-app/logging-service/src/model/process.rs @@ -0,0 +1,92 @@ +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct Process { + pub id: String, + pub owners: Vec, +} + +impl Process { + pub fn new(id: String, owners: Vec) -> Process { + Process { + id, + owners + } + } +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct TransactionCounter{ + pub tc: i64 +} + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct OwnerList{ + pub owners: Vec +} + +impl OwnerList{ + pub fn new(owners: Vec) -> OwnerList{ + OwnerList{ + owners, + } + } +} + +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] +pub struct Receipt { + pub data: biscuit::jws::Compact +} + +#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] +pub struct DataTransaction { + pub transaction_id: String, + pub timestamp: i64, + pub process_id: String, + pub document_id: String, + pub payload: String, + pub chain_hash: String, + pub client_id: String, + pub clearing_house_version: String, +} + +impl biscuit::CompactJson for DataTransaction{} + +impl DataTransaction{ + pub fn sign(&self, key_path: &str) -> Receipt{ + use crate::model::claims::get_fingerprint; + + let jws = biscuit::jws::Compact::new_decoded(biscuit::jws::Header::from_registered_header(biscuit::jws::RegisteredHeader{ + algorithm: biscuit::jwa::SignatureAlgorithm::PS512, + media_type: None, + key_id: get_fingerprint(key_path), + ..Default::default()}), self.clone()); + + let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); + println!("decoded JWS:{:#?}", &jws); + Receipt{ + data: jws.into_encoded(&keypair).unwrap() + } + } +} + +// convenience method for testing +impl From for DataTransaction{ + fn from(r: Receipt) -> Self { + match r.data.unverified_payload(){ + Ok(d) => d.clone(), + Err(e) => { + println!("Error occured: {:#?}", e); + DataTransaction{ + + transaction_id: "error".to_string(), + timestamp: 0, + process_id: "error".to_string(), + document_id: "error".to_string(), + payload: "error".to_string(), + chain_hash: "error".to_string(), + client_id: "error".to_string(), + clearing_house_version: "error".to_string(), + } + } + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/model/util.rs b/clearing-house-app/logging-service/src/model/util.rs new file mode 100644 index 0000000..1396d9b --- /dev/null +++ b/clearing-house-app/logging-service/src/model/util.rs @@ -0,0 +1,4 @@ +pub fn new_uuid() -> String { + use uuid::Uuid; + Uuid::new_v4().hyphenated().to_string() +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs index 9b0bcb8..6d088d5 100644 --- a/clearing-house-app/logging-service/src/ports/doc_type_api.rs +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -1,5 +1,5 @@ -use core_lib::api::ApiResponse; -use core_lib::constants::{ROCKET_DOC_TYPE_API, DEFAULT_PROCESS_ID}; +use crate::ports::ApiResponse; +use crate::model::constants::{ROCKET_DOC_TYPE_API, DEFAULT_PROCESS_ID}; use rocket::fairing::AdHoc; use rocket::State; use rocket::serde::json::{json,Json}; diff --git a/clearing-house-app/logging-service/src/ports/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs index 8151caa..35bb2e2 100644 --- a/clearing-house-app/logging-service/src/ports/logging_api.rs +++ b/clearing-house-app/logging-service/src/ports/logging_api.rs @@ -1,8 +1,8 @@ -use core_lib::{ - api::{ +use crate::{ + ports::{ ApiResponse, - crypto::{ChClaims, get_jwks}, }, + model::claims::{ChClaims, get_jwks}, model::SortingOrder, }; use rocket::serde::json::{json, Json}; diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/logging-service/src/ports/mod.rs index 9948361..71bc119 100644 --- a/clearing-house-app/logging-service/src/ports/mod.rs +++ b/clearing-house-app/logging-service/src/ports/mod.rs @@ -5,3 +5,27 @@ //! the logging service. pub(crate) mod logging_api; pub(crate) mod doc_type_api; + + + +#[derive(rocket::Responder, Debug)] +pub enum ApiResponse { + #[response(status = 200)] + PreFlight(()), + #[response(status = 400, content_type = "text/plain")] + BadRequest(String), + #[response(status = 201, content_type = "json")] + SuccessCreate(rocket::serde::json::Value), + #[response(status = 200, content_type = "json")] + SuccessOk(rocket::serde::json::Value), + #[response(status = 204, content_type = "text/plain")] + SuccessNoContent(String), + #[response(status = 401, content_type = "text/plain")] + Unauthorized(String), + #[response(status = 403, content_type = "text/plain")] + Forbidden(String), + #[response(status = 404, content_type = "text/plain")] + NotFound(String), + #[response(status = 500, content_type = "text/plain")] + InternalError(String), +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 317be49..ca8032d 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -1,11 +1,11 @@ use std::convert::TryFrom; use anyhow::anyhow; -use core_lib::api::crypto::ChClaims; -use core_lib::api::{DocumentReceipt, QueryResult}; -use core_lib::constants::{DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART}; -use core_lib::model::document::Document; -use core_lib::model::{parse_date, sanitize_dates, SortingOrder, validate_dates}; -use core_lib::model::crypto::{KeyCt, KeyCtList}; +use crate::model::claims::ChClaims; +use crate::services::{DocumentReceipt, QueryResult}; +use crate::model::constants::{DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART}; +use crate::model::document::Document; +use crate::model::{parse_date, sanitize_dates, SortingOrder, validate_dates}; +use crate::model::crypto::{KeyCt, KeyCtList}; use crate::services::keyring_service::KeyringService; use crate::db::doc_store::DataStore; diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index 63f2ab1..6fb2456 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; -use core_lib::api::crypto::ChClaims; -use core_lib::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; +use crate::model::claims::ChClaims; +use crate::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::crypto; use crate::crypto::restore_key_map; use crate::db::key_store::KeyStore; diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/logging-service/src/services/logging_service.rs index c81c01c..d97fb63 100644 --- a/clearing-house-app/logging-service/src/services/logging_service.rs +++ b/clearing-house-app/logging-service/src/services/logging_service.rs @@ -1,7 +1,7 @@ -use core_lib::{ - api::crypto::ChClaims, +use crate::model::{ + claims::ChClaims, constants::{DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, DEFAULT_PROCESS_ID}, - model::{ + { document::Document, process::Process, SortingOrder, @@ -16,8 +16,8 @@ use crate::model::{ids::{ message::IdsMessage, IdsQueryResult, request::ClearingHouseMessage, -}, OwnerList, DataTransaction, Receipt}; -use crate::db::ProcessStore; +}, process::{OwnerList, DataTransaction, Receipt}}; +use crate::db::process_store::ProcessStore; use crate::services::document_service::DocumentService; #[derive(Clone)] diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs index 400f075..e304b9c 100644 --- a/clearing-house-app/logging-service/src/services/mod.rs +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -3,7 +3,52 @@ //! This module contains the Application Services that are used by the API Controllers. It is //! responsible for the business logic of the application. The services are used by the API //! Controllers to handle the requests and responses. -//! +//! +use crate::model::document::Document; + pub(crate) mod keyring_service; pub(crate) mod document_service; -pub(crate) mod logging_service; \ No newline at end of file +pub(crate) mod logging_service; + + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct DocumentReceipt{ + pub timestamp: i64, + pub pid: String, + pub doc_id: String, + pub chain_hash: String, +} + +impl DocumentReceipt{ + pub fn new(timestamp: i64, pid: &str, doc_id: &str, chain_hash: &str) -> DocumentReceipt{ + DocumentReceipt{ + timestamp, + pid: pid.to_string(), + doc_id: doc_id.to_string(), + chain_hash: chain_hash.to_string(), + } + } +} + +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +pub struct QueryResult{ + pub date_from: i64, + pub date_to: i64, + pub page: Option, + pub size: Option, + pub order: String, + pub documents: Vec +} + +impl QueryResult{ + pub fn new(date_from: i64, date_to: i64, page: Option, size: Option, order: String, documents: Vec) -> QueryResult{ + QueryResult{ + date_from, + date_to, + page, + size, + order, + documents + } + } +} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/util.rs b/clearing-house-app/logging-service/src/util.rs new file mode 100644 index 0000000..94e3183 --- /dev/null +++ b/clearing-house-app/logging-service/src/util.rs @@ -0,0 +1,26 @@ +use crate::model::errors::errors; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ServiceConfig{ + pub service_id: String +} + +pub fn add_service_config(service_id: String) -> rocket::fairing::AdHoc { + rocket::fairing::AdHoc::try_on_ignite("Adding Service Config", move |rocket| async move { + match std::env::var(&service_id){ + Ok(id) => { + Ok(rocket.manage(ServiceConfig {service_id: id})) + }, + Err(_e) => { + error!("Service ID not configured. Please configure environment variable {}", &service_id); + return Err(rocket) + } + } + }) +} + +/// Reads a file into a string +pub fn read_file(file: &str) -> errors::Result { + std::fs::read_to_string(file) + .map_err(|e| errors::Error::from(e)) +} \ No newline at end of file From 0a474c0904a74f258978b1bd0ed2278edd8c8db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 17 Aug 2023 13:46:39 +0200 Subject: [PATCH 039/183] fix(ci): Fix rust.yml workflow --- .github/workflows/rust.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ab67af7..9881ac3 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -12,8 +12,6 @@ on: env: CARGO_TERM_COLOR: always IMAGE_NAME_LS: ids-ch-logging-service - IMAGE_NAME_DA: ids-ch-document-api - IMAGE_NAME_KA: ids-ch-keyring-api IMAGE_BASE: ghcr.io/truzzt/ids-basecamp-clearinghouse @@ -31,8 +29,6 @@ jobs: - name: Build build images run: | docker build . --file docker/logging-service.Dockerfile --tag $IMAGE_NAME_LS - docker build . --file docker/document-api.Dockerfile --tag $IMAGE_NAME_DA - docker build . --file docker/keyring-api.Dockerfile --tag $IMAGE_NAME_KA - name: Log into registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin @@ -40,13 +36,9 @@ jobs: - name: Push image run: | IMAGE_ID_LS=$IMAGE_BASE/$IMAGE_NAME_LS - IMAGE_ID_DA=$IMAGE_BASE/$IMAGE_NAME_DA - IMAGE_ID_KA=$IMAGE_BASE/$IMAGE_NAME_KA # Change all uppercase to lowercase IMAGE_ID_LS=$(echo $IMAGE_ID_LS | tr '[A-Z]' '[a-z]') - IMAGE_ID_DA=$(echo $IMAGE_ID_DA | tr '[A-Z]' '[a-z]') - IMAGE_ID_KA=$(echo $IMAGE_ID_KA | tr '[A-Z]' '[a-z]') # Strip git ref prefix from version VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') @@ -60,10 +52,3 @@ jobs: docker tag $IMAGE_NAME_LS $IMAGE_ID_LS:$VERSION docker push $IMAGE_ID_LS:$VERSION - docker tag $IMAGE_NAME_DA $IMAGE_ID_DA:$VERSION - docker push $IMAGE_ID_DA:$VERSION - - docker tag $IMAGE_NAME_KA $IMAGE_ID_KA:$VERSION - docker push $IMAGE_ID_KA:$VERSION - - From 76765e687c3cac025f33fd902d28a6caec764e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Mon, 21 Aug 2023 11:33:53 +0200 Subject: [PATCH 040/183] fix(config): Fixed config and added unit test to verify correct functionality --- clearing-house-app/Cargo.lock | 63 +++++++++--- clearing-house-app/logging-service/Cargo.toml | 10 +- .../logging-service/src/config.rs | 95 +++++++++++++++++-- .../logging-service/src/main.rs | 2 +- .../src/services/document_service.rs | 9 +- 5 files changed, 151 insertions(+), 28 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 96f677d..9afcaba 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -563,6 +563,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "dashmap" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +dependencies = [ + "cfg-if", + "hashbrown 0.14.0", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.4.0" @@ -791,6 +804,7 @@ checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -1263,7 +1277,9 @@ dependencies = [ "rocket", "serde", "serde_json", + "serial_test", "sha2 0.10.7", + "tempfile", "tracing", "tracing-subscriber", "uuid", @@ -1713,7 +1729,7 @@ checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" dependencies = [ "cpuid-bool", "opaque-debug", - "universal-hash 0.4.1", + "universal-hash 0.4.0", ] [[package]] @@ -2187,9 +2203,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.183" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32ac8da02677876d532745a130fc9d8e6edfa81a269b107c5b00829b91d8eb3c" +checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" dependencies = [ "serde_derive", ] @@ -2205,9 +2221,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.183" +version = "1.0.171" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafe972d60b0b9bee71a91b92fee2d4fb3c9d7e8f6b179aa99f27203d99a4816" +checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" dependencies = [ "proc-macro2", "quote", @@ -2282,6 +2298,31 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "sha-1" version = "0.10.1" @@ -2418,9 +2459,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -2458,9 +2499,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc02fddf48964c42031a0b3fe0428320ecf3a73c401040fc0096f97794310651" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", @@ -2856,9 +2897,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "universal-hash" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" dependencies = [ "generic-array", "subtle", diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 4f9416a..6ba992f 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -10,11 +10,11 @@ edition = "2021" [dependencies] biscuit = "0.6.0" core-lib = { path = "../core-lib" } -chrono = { version = "0.4", features = ["serde"] } +chrono = { version = "0.4.26", features = ["serde", "clock", "std"], default-features = false } mongodb ="2" percent-encoding = "2.1.0" rocket = { version = "0.5.0-rc.1", features = ["json"] } -serde = { version = "1", features = ["derive"] } +serde = { version = "= 1.0.171", features = ["derive"] } serde_json = "1" anyhow = "1" hex = "0.4.3" @@ -35,3 +35,9 @@ error-chain = "0.12.4" num-bigint = "0.4.3" ring = "0.16.20" openssh-keys = "0.6.2" + +[dev-dependencies] +# Controlling execution of unit test cases, which could interfere with each other +serial_test = "2.0.0" +# Tempfile creation for testing +tempfile = "3.8.0" diff --git a/clearing-house-app/logging-service/src/config.rs b/clearing-house-app/logging-service/src/config.rs index c41bf95..ed9d6bd 100644 --- a/clearing-house-app/logging-service/src/config.rs +++ b/clearing-house-app/logging-service/src/config.rs @@ -8,7 +8,7 @@ pub(crate) struct CHConfig { pub(crate) log_level: Option, } -#[derive(Debug, serde::Deserialize)] +#[derive(Debug, PartialEq, serde::Deserialize)] #[serde(rename_all = "UPPERCASE")] pub(crate) enum LogLevel { Trace, @@ -42,15 +42,32 @@ impl ToString for LogLevel { } } -/// Read configuration from `config.toml` and environment variables -pub(crate) fn read_config() -> CHConfig { - let conf = config::Config::builder() - .add_source(config::File::with_name("config.toml")) - .add_source(config::Environment::with_prefix("CH_APP_")) - .build() - .expect("Failure to read configuration! Exiting..."); +/// Read configuration from `config.toml` and environment variables. `config_file_override` can be +/// used to override the default config file, mainly for testing purposes. +pub(crate) fn read_config(config_file_override: Option<&std::path::Path>) -> CHConfig { + // Create config builder + let mut conf_builder = config::Config::builder(); + + // Override config file override path + conf_builder = if let Some(config_file) = config_file_override { + conf_builder + .add_source(config::File::from(config_file)) + } else { + conf_builder + .add_source(config::File::with_name("config.toml")) + }; + + // Add environment variables and finish + conf_builder = conf_builder + .add_source( + config::Environment::with_prefix("CH_APP") + .prefix_separator("_") + ); - conf.try_deserialize::() + conf_builder + .build() + .expect("Failure to read configuration! Exiting...") + .try_deserialize::() .expect("Failure to parse configuration! Exiting...") } @@ -66,4 +83,64 @@ pub(crate) fn configure_logging(log_level: Option) { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); +} + +#[cfg(test)] +mod test { + use serial_test::serial; + #[test] + #[serial] + fn test_read_config_from_env() { + std::env::set_var("CH_APP_PROCESS_DATABASE_URL", "mongodb://localhost:27117"); + std::env::set_var("CH_APP_KEYRING_DATABASE_URL", "mongodb://localhost:27118"); + std::env::set_var("CH_APP_DOCUMENT_DATABASE_URL", "mongodb://localhost:27119"); + std::env::set_var("CH_APP_CLEAR_DB", "true"); + std::env::set_var("CH_APP_LOG_LEVEL", "INFO"); + + let conf = super::read_config(None); + assert_eq!(conf.process_database_url, "mongodb://localhost:27117"); + assert_eq!(conf.keyring_database_url, "mongodb://localhost:27118"); + assert_eq!(conf.document_database_url, "mongodb://localhost:27119"); + assert_eq!(conf.clear_db, true); + assert_eq!(conf.log_level, Some(super::LogLevel::Info)); + + // Cleanup + std::env::remove_var("CH_APP_PROCESS_DATABASE_URL"); + std::env::remove_var("CH_APP_KEYRING_DATABASE_URL"); + std::env::remove_var("CH_APP_DOCUMENT_DATABASE_URL"); + std::env::remove_var("CH_APP_CLEAR_DB"); + std::env::remove_var("CH_APP_LOG_LEVEL"); + } + + #[test] + #[serial] + fn test_read_config_from_toml() { + // Create tempfile + let file = tempfile::Builder::new() + .suffix(".toml") + .tempfile() + .unwrap(); + + // Write config to file + let toml = + r#"process_database_url = "mongodb://localhost:27019" +keyring_database_url = "mongodb://localhost:27020" +document_database_url = "mongodb://localhost:27017" +clear_db = true +log_level = "ERROR" +"#; + + // Write to file + std::fs::write(file.path(), toml).expect("Failure to write config file!"); + + // Read config + let conf = super::read_config(Some(file.path())); + + // Test + assert_eq!(conf.process_database_url, "mongodb://localhost:27019"); + assert_eq!(conf.keyring_database_url, "mongodb://localhost:27020"); + assert_eq!(conf.document_database_url, "mongodb://localhost:27017"); + assert_eq!(conf.clear_db, true); + assert_eq!(conf.log_level, Some(super::LogLevel::Error)); + } } \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index bb2478c..487dd7d 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -35,7 +35,7 @@ pub fn add_signing_key() -> AdHoc { #[rocket::main] async fn main() -> Result<(), rocket::Error> { // Read configuration - let conf = config::read_config(); + let conf = config::read_config(None); config::configure_logging(conf.log_level); let process_store = diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index ca8032d..b2e2180 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -57,17 +57,16 @@ impl DocumentService { }; debug!("start encryption"); - let mut enc_doc; - match doc.encrypt(keys) { + let mut enc_doc = match doc.encrypt(keys) { Ok(ct) => { debug!("got ct"); - enc_doc = ct + Ok(ct) } Err(e) => { error!("Error while encrypting: {:?}", e); - return Err(anyhow!("Error while encrypting!")); // InternalError + Err(anyhow!("Error while encrypting!")) // InternalError } - }; + }?; // chain the document to previous documents debug!("add the chain hash..."); From 15b618cf89a8a94131ee7ee2717398ea464e3aaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Mon, 21 Aug 2023 13:31:32 +0200 Subject: [PATCH 041/183] refactor(ch-app): Refactor datetime functions and format whole project --- clearing-house-app/logging-service/Cargo.toml | 2 + .../logging-service/src/config.rs | 29 +- .../logging-service/src/crypto.rs | 85 +++--- .../src/db/config/doc_store.rs | 101 ++++--- .../src/db/config/keyring_store.rs | 89 +++--- .../logging-service/src/db/config/mod.rs | 4 +- .../src/db/config/process_store.rs | 75 +++-- .../logging-service/src/db/doc_store.rs | 271 ++++++++++++------ .../logging-service/src/db/key_store.rs | 145 ++++++---- .../logging-service/src/db/mod.rs | 24 +- .../logging-service/src/db/process_store.rs | 43 +-- .../logging-service/src/main.rs | 47 ++- .../logging-service/src/model/claims.rs | 137 +++++---- .../logging-service/src/model/crypto.rs | 81 +++--- .../logging-service/src/model/doc_type.rs | 12 +- .../logging-service/src/model/document.rs | 81 ++++-- .../logging-service/src/model/errors.rs | 4 +- .../logging-service/src/model/ids/message.rs | 104 ++++--- .../logging-service/src/model/ids/mod.rs | 55 ++-- .../logging-service/src/model/ids/request.rs | 12 +- .../logging-service/src/model/mod.rs | 145 ++++++---- .../logging-service/src/model/process.rs | 56 ++-- .../logging-service/src/model/util.rs | 2 +- .../logging-service/src/ports/doc_type_api.rs | 73 +++-- .../logging-service/src/ports/logging_api.rs | 77 +++-- .../logging-service/src/ports/mod.rs | 6 +- .../src/services/document_service.rs | 230 +++++++++------ .../src/services/keyring_service.rs | 149 ++++++---- .../src/services/logging_service.rs | 126 +++++--- .../logging-service/src/services/mod.rs | 32 ++- .../logging-service/src/util.rs | 41 ++- 31 files changed, 1449 insertions(+), 889 deletions(-) diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 6ba992f..8b30e03 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -14,6 +14,8 @@ chrono = { version = "0.4.26", features = ["serde", "clock", "std"], default-fea mongodb ="2" percent-encoding = "2.1.0" rocket = { version = "0.5.0-rc.1", features = ["json"] } +# Restricted version to 1.0.171, because of a change in derive macro for serde. It introduced precompiled binary files +# for code generation, which is by many developers not considered a secure codeing practice. serde = { version = "= 1.0.171", features = ["derive"] } serde_json = "1" anyhow = "1" diff --git a/clearing-house-app/logging-service/src/config.rs b/clearing-house-app/logging-service/src/config.rs index ed9d6bd..0093b5d 100644 --- a/clearing-house-app/logging-service/src/config.rs +++ b/clearing-house-app/logging-service/src/config.rs @@ -1,3 +1,4 @@ +/// Represents the configuration for the application #[derive(Debug, serde::Deserialize)] pub(crate) struct CHConfig { pub(crate) process_database_url: String, @@ -8,6 +9,7 @@ pub(crate) struct CHConfig { pub(crate) log_level: Option, } +/// Contains the log level for the application #[derive(Debug, PartialEq, serde::Deserialize)] #[serde(rename_all = "UPPERCASE")] pub(crate) enum LogLevel { @@ -50,20 +52,16 @@ pub(crate) fn read_config(config_file_override: Option<&std::path::Path>) -> CHC // Override config file override path conf_builder = if let Some(config_file) = config_file_override { - conf_builder - .add_source(config::File::from(config_file)) + conf_builder.add_source(config::File::from(config_file)) } else { - conf_builder - .add_source(config::File::with_name("config.toml")) + conf_builder.add_source(config::File::with_name("config.toml")) }; // Add environment variables and finish - conf_builder = conf_builder - .add_source( - config::Environment::with_prefix("CH_APP") - .prefix_separator("_") - ); + conf_builder = + conf_builder.add_source(config::Environment::with_prefix("CH_APP").prefix_separator("_")); + // Finalize and deserialize conf_builder .build() .expect("Failure to read configuration! Exiting...") @@ -88,6 +86,8 @@ pub(crate) fn configure_logging(log_level: Option) { #[cfg(test)] mod test { use serial_test::serial; + + /// Test reading config from environment variables #[test] #[serial] fn test_read_config_from_env() { @@ -112,18 +112,15 @@ mod test { std::env::remove_var("CH_APP_LOG_LEVEL"); } + /// Test reading config from toml file #[test] #[serial] fn test_read_config_from_toml() { // Create tempfile - let file = tempfile::Builder::new() - .suffix(".toml") - .tempfile() - .unwrap(); + let file = tempfile::Builder::new().suffix(".toml").tempfile().unwrap(); // Write config to file - let toml = - r#"process_database_url = "mongodb://localhost:27019" + let toml = r#"process_database_url = "mongodb://localhost:27019" keyring_database_url = "mongodb://localhost:27020" document_database_url = "mongodb://localhost:27017" clear_db = true @@ -143,4 +140,4 @@ log_level = "ERROR" assert_eq!(conf.clear_db, true); assert_eq!(conf.log_level, Some(super::LogLevel::Error)); } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/crypto.rs b/clearing-house-app/logging-service/src/crypto.rs index ee79f6f..329c40d 100644 --- a/clearing-house-app/logging-service/src/crypto.rs +++ b/clearing-house-app/logging-service/src/crypto.rs @@ -1,15 +1,15 @@ -use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; -use aes_gcm_siv::aead::Aead; +use crate::model::crypto::MasterKey; use crate::model::crypto::{KeyEntry, KeyMap}; +use crate::model::doc_type::DocumentType; +use aes_gcm_siv::aead::Aead; +use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; +use anyhow::anyhow; use generic_array::GenericArray; use hkdf::Hkdf; +use rand::{RngCore, SeedableRng}; use sha2::Sha256; use std::collections::HashMap; use std::sync::Mutex; -use anyhow::anyhow; -use crate::model::doc_type::DocumentType; -use crate::model::crypto::MasterKey; -use rand::{RngCore, SeedableRng}; const EXP_KEY_SIZE: usize = 32; const EXP_NONCE_SIZE: usize = 12; @@ -25,9 +25,8 @@ fn initialize_kdf() -> (String, Hkdf) { /// Generates a random seed with 256 bytes. pub fn generate_random_seed() -> Vec { // Init crypto RNG once lazy - static RNG: once_cell::sync::Lazy> = once_cell::sync::Lazy::new( - || Mutex::new(rand::rngs::StdRng::from_entropy()) - ); + static RNG: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| Mutex::new(rand::rngs::StdRng::from_entropy())); // Create a buffer to fill with random bytes let mut buf = [0u8; 256]; @@ -45,17 +44,23 @@ fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap p.name.clone(), - false => i.to_string() - }; - key_map.insert(map_key, KeyEntry::new(i.to_string(), okm[..EXP_KEY_SIZE].to_vec(), okm[EXP_KEY_SIZE..].to_vec())); - } - i = i + 1; - }); + dt.parts.iter().for_each(|p| { + if kdf.expand(p.name.clone().as_bytes(), &mut okm).is_ok() { + let map_key = match enc { + true => p.name.clone(), + false => i.to_string(), + }; + key_map.insert( + map_key, + KeyEntry::new( + i.to_string(), + okm[..EXP_KEY_SIZE].to_vec(), + okm[EXP_KEY_SIZE..].to_vec(), + ), + ); + } + i = i + 1; + }); key_map } @@ -67,7 +72,10 @@ pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result anyhow::Result) -> anyhow::Result { +pub fn restore_key_map( + mkey: MasterKey, + dt: DocumentType, + keys_ct: Vec, +) -> anyhow::Result { debug!("decrypting the key seed"); let kdf = restore_kdf(&mkey.key)?; let mut okm = [0u8; EXP_BUFF_SIZE]; - if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err() { + if kdf + .expand(hex::decode(mkey.salt)?.as_slice(), &mut okm) + .is_err() + { return Err(anyhow!("Error while generating key")); } @@ -129,12 +144,20 @@ fn restore_kdf(secret: &String) -> anyhow::Result> { pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Result> { // check key size if key.len() != EXP_KEY_SIZE { - error!("Given key has size {} but expected {} bytes", key.len(), EXP_KEY_SIZE); + error!( + "Given key has size {} but expected {} bytes", + key.len(), + EXP_KEY_SIZE + ); Err(anyhow!("Incorrect key size")) } // check nonce size else if nonce.len() != EXP_NONCE_SIZE { - error!("Given nonce has size {} but expected {} bytes", nonce.len(), EXP_NONCE_SIZE); + error!( + "Given nonce has size {} but expected {} bytes", + nonce.len(), + EXP_NONCE_SIZE + ); Err(anyhow!("Incorrect nonce size")) } else { let key = GenericArray::from_slice(key); @@ -142,10 +165,8 @@ pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Resul let cipher = Aes256GcmSiv::new(key); match cipher.encrypt(nonce, secret.as_bytes()) { - Ok(ct) => { - Ok(ct) - } - Err(e) => Err(anyhow!("Error while encrypting {}", e)) + Ok(ct) => Ok(ct), + Err(e) => Err(anyhow!("Error while encrypting {}", e)), } } } @@ -167,9 +188,7 @@ pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result { - Err(anyhow!("Error while decrypting: {}", e)) - } + Err(e) => Err(anyhow!("Error while decrypting: {}", e)), } } @@ -185,4 +204,4 @@ mod test { assert_ne!(0, seed.iter().map(|b| *b as usize).sum::()); } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/db/config/doc_store.rs b/clearing-house-app/logging-service/src/db/config/doc_store.rs index 5c16598..3ed9dc1 100644 --- a/clearing-house-app/logging-service/src/db/config/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/config/doc_store.rs @@ -1,12 +1,15 @@ +use crate::db::doc_store::DataStore; +use crate::db::init_database_client; +use crate::model::constants::{ + CLEAR_DB, DATABASE_URL, DOCUMENT_DB, DOCUMENT_DB_CLIENT, MONGO_COLL_DOCUMENT_BUCKET, + MONGO_DOC_ARRAY, MONGO_PID, MONGO_TC, MONGO_TS, +}; +use crate::model::document::Document; use anyhow::anyhow; use mongodb::bson::doc; -use mongodb::IndexModel; use mongodb::options::{CreateCollectionOptions, IndexOptions, WriteConcern}; -use rocket::{Build, fairing, Rocket}; -use crate::model::constants::{CLEAR_DB, DATABASE_URL, DOCUMENT_DB, DOCUMENT_DB_CLIENT, MONGO_COLL_DOCUMENT_BUCKET, MONGO_DOC_ARRAY, MONGO_PID, MONGO_TS, MONGO_TC}; -use crate::db::init_database_client; -use crate::model::document::Document; -use crate::db::doc_store::DataStore; +use mongodb::IndexModel; +use rocket::{fairing, Build, Rocket}; #[derive(Clone, Debug)] pub struct DatastoreConfigurator; @@ -16,26 +19,26 @@ impl fairing::Fairing for DatastoreConfigurator { fn info(&self) -> fairing::Info { fairing::Info { name: "Configuring Document Database", - kind: fairing::Kind::Ignite + kind: fairing::Kind::Ignite, } } async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ + let db_url: String = rocket + .figment() + .extract_inner(DATABASE_URL) + .clone() + .unwrap(); + let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { Ok(value) => { debug!("clear_db: '{}' found.", &value); value - }, - Err(_) => { - false } + Err(_) => false, }; match Self::init_datastore(db_url, clear_db).await { - Ok(datastore) => { - Ok(rocket.manage(datastore)) - }, - Err(_) => Err(rocket) + Ok(datastore) => Ok(rocket.manage(datastore)), + Err(_) => Err(rocket), } } } @@ -43,22 +46,31 @@ impl fairing::Fairing for DatastoreConfigurator { impl DatastoreConfigurator { pub async fn init_datastore(db_url: String, clear_db: bool) -> anyhow::Result { debug!("Using mongodb url: '{:#?}'", &db_url); - match init_database_client::(&db_url.as_str(), Some(DOCUMENT_DB_CLIENT.to_string())).await{ + match init_database_client::( + &db_url.as_str(), + Some(DOCUMENT_DB_CLIENT.to_string()), + ) + .await + { Ok(datastore) => { debug!("Check if database is empty..."); - match datastore.client.database(DOCUMENT_DB) + match datastore + .client + .database(DOCUMENT_DB) .list_collection_names(None) - .await{ + .await + { Ok(colls) => { debug!("... found collections: {:#?}", &colls); - let number_of_colls = match colls.contains(&MONGO_COLL_DOCUMENT_BUCKET.to_string()){ - true => colls.len(), - false => 0 - }; + let number_of_colls = + match colls.contains(&MONGO_COLL_DOCUMENT_BUCKET.to_string()) { + true => colls.len(), + false => 0, + }; - if number_of_colls > 0 && clear_db{ + if number_of_colls > 0 && clear_db { debug!("Database not empty and clear_db == true. Dropping database..."); - match datastore.client.database(DOCUMENT_DB).drop(None).await{ + match datastore.client.database(DOCUMENT_DB).drop(None).await { Ok(_) => { debug!("... done."); } @@ -68,14 +80,19 @@ impl DatastoreConfigurator { } }; } - if number_of_colls == 0 || clear_db{ + if number_of_colls == 0 || clear_db { debug!("Database empty. Need to initialize..."); let mut write_concern = WriteConcern::default(); write_concern.journal = Some(true); let mut options = CreateCollectionOptions::default(); options.write_concern = Some(write_concern); debug!("Create collection {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore.client.database(DOCUMENT_DB).create_collection(MONGO_COLL_DOCUMENT_BUCKET, options).await{ + match datastore + .client + .database(DOCUMENT_DB) + .create_collection(MONGO_COLL_DOCUMENT_BUCKET, options) + .await + { Ok(_) => { debug!("... done."); } @@ -89,11 +106,17 @@ impl DatastoreConfigurator { let mut index_options = IndexOptions::default(); index_options.unique = Some(true); let mut index_model = IndexModel::default(); - index_model.keys = doc!{format!("{}.{}",MONGO_DOC_ARRAY, MONGO_TC): 1}; + index_model.keys = doc! {format!("{}.{}",MONGO_DOC_ARRAY, MONGO_TC): 1}; index_model.options = Some(index_options); debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore.client.database(DOCUMENT_DB).collection::(MONGO_COLL_DOCUMENT_BUCKET).create_index(index_model, None).await{ + match datastore + .client + .database(DOCUMENT_DB) + .collection::(MONGO_COLL_DOCUMENT_BUCKET) + .create_index(index_model, None) + .await + { Ok(result) => { debug!("... index {} created", result.index_name); } @@ -105,10 +128,16 @@ impl DatastoreConfigurator { // This creates a compound index over pid and the timestamp to enable paging using buckets let mut compound_index_model = IndexModel::default(); - compound_index_model.keys = doc!{MONGO_PID: 1, MONGO_TS: 1}; + compound_index_model.keys = doc! {MONGO_PID: 1, MONGO_TS: 1}; debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore.client.database(DOCUMENT_DB).collection::(MONGO_COLL_DOCUMENT_BUCKET).create_index(compound_index_model, None).await{ + match datastore + .client + .database(DOCUMENT_DB) + .collection::(MONGO_COLL_DOCUMENT_BUCKET) + .create_index(compound_index_model, None) + .await + { Ok(result) => { debug!("... index {} created", result.index_name); } @@ -121,12 +150,10 @@ impl DatastoreConfigurator { debug!("... database initialized."); Ok(datastore) } - Err(_) => { - Err(anyhow!("Failed to list collections")) - } + Err(_) => Err(anyhow!("Failed to list collections")), } - }, - Err(_) => Err(anyhow!("Failed to initialize database client")) + } + Err(_) => Err(anyhow!("Failed to initialize database client")), } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/db/config/keyring_store.rs b/clearing-house-app/logging-service/src/db/config/keyring_store.rs index 508e949..48eef89 100644 --- a/clearing-house-app/logging-service/src/db/config/keyring_store.rs +++ b/clearing-house-app/logging-service/src/db/config/keyring_store.rs @@ -1,12 +1,14 @@ -use anyhow::anyhow; -use rocket::fairing::Kind; -use rocket::{Build, fairing, Rocket}; -use crate::model::constants::{CLEAR_DB, DATABASE_URL, FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT}; use crate::db::init_database_client; -use crate::util::read_file; use crate::db::key_store::KeyStore; +use crate::model::constants::{ + CLEAR_DB, DATABASE_URL, FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, +}; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; +use crate::util::read_file; +use anyhow::anyhow; +use rocket::fairing::Kind; +use rocket::{fairing, Build, Rocket}; #[derive(Clone, Debug)] pub struct KeyringDbConfigurator; @@ -16,28 +18,26 @@ impl fairing::Fairing for KeyringDbConfigurator { fn info(&self) -> fairing::Info { fairing::Info { name: "Configuring Keyring Database", - kind: Kind::Ignite + kind: Kind::Ignite, } } async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); + let db_url: String = rocket + .figment() + .extract_inner(DATABASE_URL) + .clone() + .unwrap(); let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { Ok(value) => { debug!("clear_db: '{}' found.", &value); value - }, - Err(_) => { - false } + Err(_) => false, }; match Self::init_keystore(db_url, clear_db).await { - Ok(keystore) => { - Ok(rocket.manage(keystore)) - }, - Err(_) => { - Err(rocket) - } + Ok(keystore) => Ok(rocket.manage(keystore)), + Err(_) => Err(rocket), } } } @@ -46,12 +46,20 @@ impl KeyringDbConfigurator { pub async fn init_keystore(db_url: String, clear_db: bool) -> anyhow::Result { debug!("Using database url: '{:#?}'", &db_url); - match init_database_client::(&db_url.as_str(), Some(KEYRING_DB_CLIENT.to_string())).await { + match init_database_client::( + &db_url.as_str(), + Some(KEYRING_DB_CLIENT.to_string()), + ) + .await + { Ok(keystore) => { debug!("Check if database is empty..."); - match keystore.client.database(KEYRING_DB) + match keystore + .client + .database(KEYRING_DB) .list_collection_names(None) - .await { + .await + { Ok(colls) => { debug!("... found collections: {:#?}", &colls); if colls.len() > 0 && clear_db { @@ -69,21 +77,28 @@ impl KeyringDbConfigurator { if colls.len() == 0 || clear_db { debug!("Database empty. Need to initialize..."); debug!("Adding initial document type..."); - match serde_json::from_str::(&read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or(String::new())) { - Ok(dt) => { - match keystore.add_document_type(dt).await { - Ok(_) => { - debug!("... done."); - }, - Err(e) => { - error!("Error while adding initial document type: {:#?}", e); - return Err(anyhow!("Error while adding initial document type")); - } + match serde_json::from_str::( + &read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or(String::new()), + ) { + Ok(dt) => match keystore.add_document_type(dt).await { + Ok(_) => { + debug!("... done."); } - } + Err(e) => { + error!( + "Error while adding initial document type: {:#?}", + e + ); + return Err(anyhow!( + "Error while adding initial document type" + )); + } + }, _ => { error!("Error while loading initial document type"); - return Err(anyhow!("Error while loading initial document type")); + return Err(anyhow!( + "Error while loading initial document type" + )); } }; debug!("Creating master key..."); @@ -91,7 +106,7 @@ impl KeyringDbConfigurator { match keystore.store_master_key(MasterKey::new_random()).await { Ok(true) => { debug!("... done."); - }, + } _ => { error!("... failed to create master key"); return Err(anyhow!("Failed to create master key")); @@ -101,12 +116,10 @@ impl KeyringDbConfigurator { debug!("... database initialized."); Ok(keystore) } - Err(_) => { - Err(anyhow!("Failed to list collections")) - } + Err(_) => Err(anyhow!("Failed to list collections")), } - }, - Err(_) => Err(anyhow!("Failed to initialize database client")) + } + Err(_) => Err(anyhow!("Failed to initialize database client")), } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/db/config/mod.rs b/clearing-house-app/logging-service/src/db/config/mod.rs index 5e2b055..7567b5b 100644 --- a/clearing-house-app/logging-service/src/db/config/mod.rs +++ b/clearing-house-app/logging-service/src/db/config/mod.rs @@ -1,3 +1,3 @@ -pub(crate) mod process_store; +pub(crate) mod doc_store; pub(crate) mod keyring_store; -pub(crate) mod doc_store; \ No newline at end of file +pub(crate) mod process_store; diff --git a/clearing-house-app/logging-service/src/db/config/process_store.rs b/clearing-house-app/logging-service/src/db/config/process_store.rs index 1aef713..bad5120 100644 --- a/clearing-house-app/logging-service/src/db/config/process_store.rs +++ b/clearing-house-app/logging-service/src/db/config/process_store.rs @@ -1,10 +1,12 @@ +use crate::db::init_database_client; +use crate::db::process_store::ProcessStore; +use crate::model::constants::{ + CLEAR_DB, DATABASE_URL, MONGO_COLL_TRANSACTIONS, PROCESS_DB, PROCESS_DB_CLIENT, +}; use anyhow::anyhow; use mongodb::options::{CreateCollectionOptions, WriteConcern}; -use rocket::{Build, Rocket}; use rocket::fairing::Kind; -use crate::model::constants::{CLEAR_DB, DATABASE_URL, MONGO_COLL_TRANSACTIONS, PROCESS_DB, PROCESS_DB_CLIENT}; -use crate::db::init_database_client; -use crate::db::process_store::ProcessStore; +use rocket::{Build, Rocket}; #[derive(Clone, Debug)] pub struct ProcessStoreConfigurator; @@ -14,47 +16,62 @@ impl rocket::fairing::Fairing for ProcessStoreConfigurator { fn info(&self) -> rocket::fairing::Info { rocket::fairing::Info { name: "Configuring Process Database", - kind: Kind::Ignite + kind: Kind::Ignite, } } async fn on_ignite(&self, rocket: Rocket) -> rocket::fairing::Result { debug!("Preparing to initialize database..."); - let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ + let db_url: String = rocket + .figment() + .extract_inner(DATABASE_URL) + .clone() + .unwrap(); + let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { Ok(value) => { debug!("...clear_db: {} found. ", &value); value - }, - Err(_) => { - false } + Err(_) => false, }; match Self::init_process_store(db_url, clear_db).await { Ok(process_store) => { debug!("...done."); Ok(rocket.manage(process_store)) - }, - Err(_) => Err(rocket) + } + Err(_) => Err(rocket), } } } impl ProcessStoreConfigurator { - pub async fn init_process_store(db_url: String, clear_db: bool) -> anyhow::Result { + pub async fn init_process_store( + db_url: String, + clear_db: bool, + ) -> anyhow::Result { debug!("...using database url: '{:#?}'", &db_url); - match init_database_client::(&db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string())).await{ + match init_database_client::( + &db_url.as_str(), + Some(PROCESS_DB_CLIENT.to_string()), + ) + .await + { Ok(process_store) => { debug!("...check if database is empty..."); - match process_store.client.database(PROCESS_DB) + match process_store + .client + .database(PROCESS_DB) .list_collection_names(None) - .await{ + .await + { Ok(colls) => { debug!("... found collections: {:#?}", &colls); - if colls.len() > 0 && clear_db{ - debug!("...database not empty and clear_db == true. Dropping database..."); - match process_store.client.database(PROCESS_DB).drop(None).await{ + if colls.len() > 0 && clear_db { + debug!( + "...database not empty and clear_db == true. Dropping database..." + ); + match process_store.client.database(PROCESS_DB).drop(None).await { Ok(_) => { debug!("... done."); } @@ -64,14 +81,19 @@ impl ProcessStoreConfigurator { } }; } - if colls.len() == 0 || clear_db{ + if colls.len() == 0 || clear_db { debug!("..database empty. Need to initialize..."); let mut write_concern = WriteConcern::default(); write_concern.journal = Some(true); let mut options = CreateCollectionOptions::default(); options.write_concern = Some(write_concern); debug!("...create collection {} ...", MONGO_COLL_TRANSACTIONS); - match process_store.client.database(PROCESS_DB).create_collection(MONGO_COLL_TRANSACTIONS, options).await{ + match process_store + .client + .database(PROCESS_DB) + .create_collection(MONGO_COLL_TRANSACTIONS, options) + .await + { Ok(_) => { debug!("... done."); } @@ -84,13 +106,10 @@ impl ProcessStoreConfigurator { debug!("... database initialized."); Ok(process_store) } - Err(_) => { - Err(anyhow!("Failed to list collections")) - } + Err(_) => Err(anyhow!("Failed to list collections")), } - }, - Err(_) => Err(anyhow!("Failed to initialize database client")) + } + Err(_) => Err(anyhow!("Failed to initialize database client")), } } - -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/db/doc_store.rs b/clearing-house-app/logging-service/src/db/doc_store.rs index 591353f..35f426f 100644 --- a/clearing-house-app/logging-service/src/db/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/doc_store.rs @@ -1,13 +1,17 @@ -use mongodb::{bson, Client}; -use mongodb::bson::doc; -use mongodb::options::{AggregateOptions, UpdateOptions}; -use rocket::futures::StreamExt; -use crate::model::constants::{MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_ID, MONGO_PID, MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_TO_TS, MONGO_TC, MONGO_TS, DOCUMENT_DB}; +use crate::db::doc_store::bucket::{restore_from_bucket, DocumentBucketSize, DocumentBucketUpdate}; use crate::db::DataStoreApi; -use crate::model::errors::*; -use crate::db::doc_store::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; +use crate::model::constants::{ + DOCUMENT_DB, MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_COUNTER, + MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_ID, MONGO_PID, MONGO_TC, MONGO_TO_TS, + MONGO_TS, +}; use crate::model::document::EncryptedDocument; +use crate::model::errors::*; use crate::model::SortingOrder; +use mongodb::bson::doc; +use mongodb::options::{AggregateOptions, UpdateOptions}; +use mongodb::{bson, Client}; +use rocket::futures::StreamExt; #[derive(Clone)] pub struct DataStore { @@ -16,10 +20,10 @@ pub struct DataStore { } impl DataStoreApi for DataStore { - fn new(client: Client) -> DataStore{ + fn new(client: Client) -> DataStore { DataStore { client: client.clone(), - database: client.database(DOCUMENT_DB) + database: client.database(DOCUMENT_DB), } } } @@ -27,7 +31,9 @@ impl DataStoreApi for DataStore { impl DataStore { pub async fn add_document(&self, doc: EncryptedDocument) -> errors::Result { debug!("add_document to bucket"); - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + let coll = self + .database + .collection::(MONGO_COLL_DOCUMENT_BUCKET); let bucket_update = DocumentBucketUpdate::from(&doc); let mut update_options = UpdateOptions::default(); update_options.upsert = Some(true); @@ -65,7 +71,9 @@ impl DataStore { debug!("Check if document with id '{}' exists...", id); let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone()}; - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + let coll = self + .database + .collection::(MONGO_COLL_DOCUMENT_BUCKET); match coll.count_documents(Some(query), None).await? { 0 => { debug!("Document with id '{}' does not exist!", &id); @@ -79,18 +87,26 @@ impl DataStore { } /// gets the model from the db - pub async fn get_document(&self, id: &String, pid: &String) -> errors::Result> { + pub async fn get_document( + &self, + id: &String, + pid: &String, + ) -> errors::Result> { debug!("Trying to get doc with id {}...", id); - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - - let pipeline = vec![doc! {"$match":{ - MONGO_PID: pid.clone(), - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone() - }}, - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, - doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, - doc! {"$match":{ MONGO_ID: id.clone()}}]; + let coll = self + .database + .collection::(MONGO_COLL_DOCUMENT_BUCKET); + + let pipeline = vec![ + doc! {"$match":{ + MONGO_PID: pid.clone(), + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone() + }}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, + doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, + doc! {"$match":{ MONGO_ID: id.clone()}}, + ]; let mut results = coll.aggregate(pipeline, None).await?; @@ -103,22 +119,29 @@ impl DataStore { } /// gets documents for a single process from the db - pub async fn get_document_with_previous_tc(&self, tc: i64) -> errors::Result> { + pub async fn get_document_with_previous_tc( + &self, + tc: i64, + ) -> errors::Result> { let previous_tc = tc - 1; debug!("Trying to get document for tc {} ...", previous_tc); if previous_tc < 0 { info!("... not entry exists."); Ok(None) } else { - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - - let pipeline = vec![doc! {"$match":{ - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TC): previous_tc - }}, - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, - doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, - doc! {"$match":{ MONGO_TC: previous_tc}}]; + let coll = self + .database + .collection::(MONGO_COLL_DOCUMENT_BUCKET); + + let pipeline = vec![ + doc! {"$match":{ + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TC): previous_tc + }}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, + doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, + doc! {"$match":{ MONGO_TC: previous_tc}}, + ]; let mut results = coll.aggregate(pipeline, None).await?; @@ -134,17 +157,42 @@ impl DataStore { } /// gets a page of documents of a specific document type for a single process from the db defined by parameters page, size and sort - pub async fn get_documents_for_pid(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> errors::Result> { - debug!("...trying to get page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); - - match self.get_start_bucket_size(dt_id, pid, page, size, sort, date_from, date_to).await { + pub async fn get_documents_for_pid( + &self, + dt_id: &String, + pid: &String, + page: u64, + size: u64, + sort: &SortingOrder, + date_from: &chrono::NaiveDateTime, + date_to: &chrono::NaiveDateTime, + ) -> errors::Result> { + debug!( + "...trying to get page {} of size {} of documents for pid {} of dt {}...", + pid, dt_id, page, size + ); + + match self + .get_start_bucket_size(dt_id, pid, page, size, sort, date_from, date_to) + .await + { Ok(bucket_size) => { let offset = DataStore::get_offset(&bucket_size); let start_bucket = DataStore::get_start_bucket(page, size, &bucket_size, offset); - trace!("...working with start_bucket {} and offset {} ...", start_bucket, offset); - let start_entry = DataStore::get_start_entry(page, size, start_bucket, &bucket_size, offset); - - trace!("...working with start_entry {} in start_bucket {} and offset {} ...", start_entry, start_bucket, offset); + trace!( + "...working with start_bucket {} and offset {} ...", + start_bucket, + offset + ); + let start_entry = + DataStore::get_start_entry(page, size, start_bucket, &bucket_size, offset); + + trace!( + "...working with start_entry {} in start_bucket {} and offset {} ...", + start_entry, + start_bucket, + offset + ); let skip_buckets = (start_bucket - 1) as i32; let sort_order = match sort { @@ -152,32 +200,36 @@ impl DataStore { SortingOrder::Descending => -1, }; - let pipeline = vec![doc! {"$match":{ - MONGO_PID: pid.clone(), - MONGO_DT_ID: dt_id.clone(), - MONGO_FROM_TS: {"$lte": date_to.timestamp()}, - MONGO_TO_TS: {"$gte": date_from.timestamp()} - }}, - doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, - doc! {"$skip" : skip_buckets}, - // worst case: overlap between two buckets. - doc! {"$limit" : 2}, - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$replaceRoot": { "newRoot": "$documents"}}, - doc! {"$match":{ - MONGO_TS: {"$gte": date_from.timestamp(), "$lte": date_to.timestamp()} - }}, - doc! {"$sort" : {MONGO_TS: sort_order}}, - doc! {"$skip" : start_entry as i32}, - doc! { "$limit": size as i32}]; - - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); + let pipeline = vec![ + doc! {"$match":{ + MONGO_PID: pid.clone(), + MONGO_DT_ID: dt_id.clone(), + MONGO_FROM_TS: {"$lte": date_to.timestamp()}, + MONGO_TO_TS: {"$gte": date_from.timestamp()} + }}, + doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, + doc! {"$skip" : skip_buckets}, + // worst case: overlap between two buckets. + doc! {"$limit" : 2}, + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$replaceRoot": { "newRoot": "$documents"}}, + doc! {"$match":{ + MONGO_TS: {"$gte": date_from.timestamp(), "$lte": date_to.timestamp()} + }}, + doc! {"$sort" : {MONGO_TS: sort_order}}, + doc! {"$skip" : start_entry as i32}, + doc! { "$limit": size as i32}, + ]; + + let coll = self + .database + .collection::(MONGO_COLL_DOCUMENT_BUCKET); let mut options = AggregateOptions::default(); options.allow_disk_use = Some(true); let mut results = coll.aggregate(pipeline, options).await?; - let mut docs = vec!(); + let mut docs = vec![]; while let Some(result) = results.next().await { let doc: DocumentBucketUpdate = bson::from_document(result?)?; docs.push(restore_from_bucket(pid, dt_id, doc)); @@ -193,36 +245,51 @@ impl DataStore { } /// offset is necessary for duration queries. There, start_entries of bucket depend on timestamps which usually creates an offset in the bucket - async fn get_start_bucket_size(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime) -> errors::Result { + async fn get_start_bucket_size( + &self, + dt_id: &String, + pid: &String, + page: u64, + size: u64, + sort: &SortingOrder, + date_from: &chrono::NaiveDateTime, + date_to: &chrono::NaiveDateTime, + ) -> errors::Result { debug!("...trying to get the offset for page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); let sort_order = match sort { - SortingOrder::Ascending => { - 1 - } - SortingOrder::Descending => { - -1 - } + SortingOrder::Ascending => 1, + SortingOrder::Descending => -1, }; - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - - debug!("... match with pid: {}, dt_it: {}, to_ts <= {}, from_ts >= {} ...", pid, dt_id, date_from.timestamp(), date_to.timestamp()); - let pipeline = vec![doc! {"$match":{ - MONGO_PID: pid.clone(), - MONGO_DT_ID: dt_id.clone(), - MONGO_FROM_TS: {"$lte": date_to.timestamp()}, - MONGO_TO_TS: {"$gte": date_from.timestamp()} - }}, - // sorting according to sorting order, so we get either the start or end - doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, - doc! {"$limit" : 1}, - // count all relevant documents in the target bucket - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$match":{ - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TS): {"$lte": date_to.timestamp(), "$gte": date_from.timestamp()} - }}, - // modify result to return total number of docs in bucket and number of relevant docs in bucket - doc! { "$group": { "_id": {"total": "$counter"}, "size": { "$sum": 1 } } }, - doc! { "$project": {"_id":0, "capacity": "$_id.total", "size":true}}]; + let coll = self + .database + .collection::(MONGO_COLL_DOCUMENT_BUCKET); + + debug!( + "... match with pid: {}, dt_it: {}, to_ts <= {}, from_ts >= {} ...", + pid, + dt_id, + date_from.timestamp(), + date_to.timestamp() + ); + let pipeline = vec![ + doc! {"$match":{ + MONGO_PID: pid.clone(), + MONGO_DT_ID: dt_id.clone(), + MONGO_FROM_TS: {"$lte": date_to.timestamp()}, + MONGO_TO_TS: {"$gte": date_from.timestamp()} + }}, + // sorting according to sorting order, so we get either the start or end + doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, + doc! {"$limit" : 1}, + // count all relevant documents in the target bucket + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$match":{ + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TS): {"$lte": date_to.timestamp(), "$gte": date_from.timestamp()} + }}, + // modify result to return total number of docs in bucket and number of relevant docs in bucket + doc! { "$group": { "_id": {"total": "$counter"}, "size": { "$sum": 1 } } }, + doc! { "$project": {"_id":0, "capacity": "$_id.total", "size":true}}, + ]; let mut options = AggregateOptions::default(); options.allow_disk_use = Some(true); @@ -244,12 +311,24 @@ impl DataStore { return (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES; } - fn get_start_bucket(page: u64, size: u64, bucket_size: &DocumentBucketSize, offset: u64) -> u64 { - let docs_to_skip = (page - 1) * size + offset + MAX_NUM_RESPONSE_ENTRIES - bucket_size.capacity as u64; + fn get_start_bucket( + page: u64, + size: u64, + bucket_size: &DocumentBucketSize, + offset: u64, + ) -> u64 { + let docs_to_skip = + (page - 1) * size + offset + MAX_NUM_RESPONSE_ENTRIES - bucket_size.capacity as u64; return (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1; } - fn get_start_entry(page: u64, size: u64, start_bucket: u64, bucket_size: &DocumentBucketSize, offset: u64) -> u64 { + fn get_start_entry( + page: u64, + size: u64, + start_bucket: u64, + bucket_size: &DocumentBucketSize, + offset: u64, + ) -> u64 { // docs to skip calculated by page * size let docs_to_skip = (page - 1) * size + offset; let mut start_entry = 0; @@ -264,7 +343,7 @@ impl DataStore { } mod bucket { - use super::{EncryptedDocument}; + use super::EncryptedDocument; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentBucket { @@ -305,7 +384,11 @@ mod bucket { } } - pub fn restore_from_bucket(pid: &String, dt_id: &String, bucket_update: DocumentBucketUpdate) -> EncryptedDocument { + pub fn restore_from_bucket( + pid: &String, + dt_id: &String, + bucket_update: DocumentBucketUpdate, + ) -> EncryptedDocument { EncryptedDocument { id: bucket_update.id.clone(), dt_id: dt_id.clone(), @@ -317,4 +400,4 @@ mod bucket { cts: bucket_update.cts.to_vec(), } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/db/key_store.rs b/clearing-house-app/logging-service/src/db/key_store.rs index bc3e185..80f603d 100644 --- a/clearing-house-app/logging-service/src/db/key_store.rs +++ b/clearing-house-app/logging-service/src/db/key_store.rs @@ -1,54 +1,56 @@ -use std::process::exit; +use super::DataStoreApi; +use crate::model::constants::{ + KEYRING_DB, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID, +}; +use crate::model::crypto::MasterKey; +use crate::model::doc_type::DocumentType; use crate::model::errors::*; use mongodb::bson::doc; use mongodb::Client; use rocket::futures::TryStreamExt; -use crate::model::constants::{KEYRING_DB, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; -use super::DataStoreApi; -use crate::model::crypto::MasterKey; -use crate::model::doc_type::DocumentType; +use std::process::exit; #[derive(Clone, Debug)] pub struct KeyStore { pub(crate) client: mongodb::Client, - database: mongodb::Database + database: mongodb::Database, } impl DataStoreApi for KeyStore { - fn new(client: Client) -> KeyStore{ + fn new(client: Client) -> KeyStore { KeyStore { client: client.clone(), - database: client.database(KEYRING_DB) + database: client.database(KEYRING_DB), } } } impl KeyStore { - /// Only one master key may exist in the database. - pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result{ + pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result { tracing::debug!("Storing new master key..."); let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); tracing::debug!("... but first check if there's already one."); - let result= coll.find(None, None).await + let result = coll + .find(None, None) + .await .expect("Error retrieving the master keys") - .try_collect().await.unwrap_or_else(|_| vec![]); + .try_collect() + .await + .unwrap_or_else(|_| vec![]); - if result.len() > 1{ + if result.len() > 1 { tracing::error!("Master Key table corrupted!"); exit(1); } - if result.len() == 1{ + if result.len() == 1 { tracing::error!("Master key already exists!"); Ok(false) - } - else{ + } else { //let db_key = bson::to_bson(&key) // .expect("failed to serialize master key for database"); - match coll.insert_one(key, None).await{ - Ok(_r) => { - Ok(true) - }, + match coll.insert_one(key, None).await { + Ok(_r) => Ok(true), Err(e) => { tracing::error!("master key could not be stored: {:?}", &e); panic!("master key could not be stored") @@ -60,18 +62,21 @@ impl KeyStore { /// Only one master key may exist in the database. pub async fn get_msk(&self) -> anyhow::Result { let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); - let result= coll.find(None, None).await + let result = coll + .find(None, None) + .await .expect("Error retrieving the master keys") - .try_collect().await.unwrap_or_else(|_| vec![]); + .try_collect() + .await + .unwrap_or_else(|_| vec![]); - if result.len() > 1{ + if result.len() > 1 { tracing::error!("Master Key table corrupted!"); exit(1); } - if result.len() == 1{ + if result.len() == 1 { Ok(result[0].clone()) - } - else { + } else { tracing::error!("Master Key missing!"); exit(1); } @@ -79,12 +84,14 @@ impl KeyStore { // DOCTYPE pub async fn add_document_type(&self, doc_type: DocumentType) -> errors::Result<()> { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + let coll = self + .database + .collection::(MONGO_COLL_DOC_TYPES); match coll.insert_one(doc_type.clone(), None).await { Ok(_r) => { tracing::debug!("added new document type: {}", &_r.inserted_id); Ok(()) - }, + } Err(e) => { tracing::error!("failed to log document type {}", &doc_type.id); Err(errors::Error::from(e)) @@ -94,8 +101,12 @@ impl KeyStore { //TODO: Do we need to check that no documents of this type exist before we remove it from the db? pub async fn delete_document_type(&self, id: &String, pid: &String) -> errors::Result { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - let result = coll.delete_many(doc! { MONGO_ID: id, MONGO_PID: pid }, None).await?; + let coll = self + .database + .collection::(MONGO_COLL_DOC_TYPES); + let result = coll + .delete_many(doc! { MONGO_ID: id, MONGO_PID: pid }, None) + .await?; if result.deleted_count >= 1 { Ok(true) } else { @@ -103,31 +114,46 @@ impl KeyStore { } } - /// checks if the model exits pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> errors::Result { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - let result = coll.find_one(Some(doc! { MONGO_ID: dt_id, MONGO_PID: pid }), None).await?; + let coll = self + .database + .collection::(MONGO_COLL_DOC_TYPES); + let result = coll + .find_one(Some(doc! { MONGO_ID: dt_id, MONGO_PID: pid }), None) + .await?; match result { Some(_r) => Ok(true), None => { - tracing::debug!("document type with id {} and pid {:?} does not exist!", &dt_id, &pid); + tracing::debug!( + "document type with id {} and pid {:?} does not exist!", + &dt_id, + &pid + ); Ok(false) } } } pub async fn get_all_document_types(&self) -> errors::Result> { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - let result = coll.find(None, None).await? - .try_collect().await.unwrap_or_else(|_| vec![]); + let coll = self + .database + .collection::(MONGO_COLL_DOC_TYPES); + let result = coll + .find(None, None) + .await? + .try_collect() + .await + .unwrap_or_else(|_| vec![]); Ok(result) } pub async fn get_document_type(&self, dt_id: &String) -> errors::Result> { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); + let coll = self + .database + .collection::(MONGO_COLL_DOC_TYPES); tracing::debug!("get_document_type for dt_id: '{}'", dt_id); - match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await{ + match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await { Ok(result) => Ok(result), Err(e) => { tracing::error!("error while getting document type with id {}!", dt_id); @@ -136,22 +162,41 @@ impl KeyStore { } } - pub async fn update_document_type(&self, doc_type: DocumentType, id: &String) -> errors::Result { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await{ + pub async fn update_document_type( + &self, + doc_type: DocumentType, + id: &String, + ) -> errors::Result { + let coll = self + .database + .collection::(MONGO_COLL_DOC_TYPES); + match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await { Ok(r) => { - if r.matched_count != 1 || r.modified_count != 1{ - tracing::warn!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); - } - else{ - tracing::debug!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); + if r.matched_count != 1 || r.modified_count != 1 { + tracing::warn!( + "while replacing doc type {} matched '{}' dts and modified '{}'", + id, + r.matched_count, + r.modified_count + ); + } else { + tracing::debug!( + "while replacing doc type {} matched '{}' dts and modified '{}'", + id, + r.matched_count, + r.modified_count + ); } Ok(true) - }, + } Err(e) => { - tracing::error!("error while updating document type with id {}: {:#?}", id, e); + tracing::error!( + "error while updating document type with id {}: {:#?}", + id, + e + ); Ok(false) } } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/logging-service/src/db/mod.rs index 16a5a57..cc605cf 100644 --- a/clearing-house-app/logging-service/src/db/mod.rs +++ b/clearing-house-app/logging-service/src/db/mod.rs @@ -1,22 +1,26 @@ -pub(crate) mod key_store; -pub(crate) mod doc_store; pub(crate) mod config; +pub(crate) mod doc_store; +pub(crate) mod key_store; pub(crate) mod process_store; -use error_chain::error_chain; -use mongodb::Client; -use mongodb::options::ClientOptions; use crate::model::errors::*; +use mongodb::options::ClientOptions; +use mongodb::Client; -pub trait DataStoreApi{ +pub trait DataStoreApi { fn new(client: Client) -> Self; } -pub async fn init_database_client(db_url: &str, client_name: Option) -> errors::Result{ +pub async fn init_database_client( + db_url: &str, + client_name: Option, +) -> errors::Result { let mut client_options; - match ClientOptions::parse(&format!("{}", db_url)).await{ - Ok(co) => {client_options = co;} + match ClientOptions::parse(&db_url.to_string()).await { + Ok(co) => { + client_options = co; + } Err(_) => { error_chain::bail!("Can't parse database connection string"); } @@ -25,4 +29,4 @@ pub async fn init_database_client(db_url: &str, client_name: Op client_options.app_name = client_name; let client = Client::with_options(client_options)?; Ok(T::new(client)) -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/db/process_store.rs b/clearing-house-app/logging-service/src/db/process_store.rs index e703577..8e8f185 100644 --- a/clearing-house-app/logging-service/src/db/process_store.rs +++ b/clearing-house-app/logging-service/src/db/process_store.rs @@ -1,12 +1,14 @@ -use crate::model::constants::{MONGO_ID, MONGO_COLL_PROCESSES, PROCESS_DB, MONGO_COLL_TRANSACTIONS, MONGO_TC}; use crate::db::DataStoreApi; +use crate::model::constants::{ + MONGO_COLL_PROCESSES, MONGO_COLL_TRANSACTIONS, MONGO_ID, MONGO_TC, PROCESS_DB, +}; use crate::model::errors::*; use crate::model::process::Process; +use crate::model::process::TransactionCounter; use mongodb::bson::doc; +use mongodb::options::{FindOneAndUpdateOptions, UpdateModifications}; use mongodb::{Client, Database}; use rocket::futures::TryStreamExt; -use mongodb::options::{UpdateModifications, FindOneAndUpdateOptions}; -use crate::model::process::TransactionCounter; #[derive(Clone)] pub struct ProcessStore { @@ -26,30 +28,38 @@ impl DataStoreApi for ProcessStore { impl ProcessStore { pub async fn get_transaction_counter(&self) -> errors::Result> { debug!("Getting transaction counter..."); - let coll = self.database.collection::(MONGO_COLL_TRANSACTIONS); + let coll = self + .database + .collection::(MONGO_COLL_TRANSACTIONS); match coll.find_one(None, None).await? { Some(t) => Ok(Some(t.tc)), - None => Ok(Some(0)) + None => Ok(Some(0)), } } pub async fn increment_transaction_counter(&self) -> errors::Result> { debug!("Getting transaction counter..."); - let coll = self.database.collection::(MONGO_COLL_TRANSACTIONS); + let coll = self + .database + .collection::(MONGO_COLL_TRANSACTIONS); let mods = UpdateModifications::Document(doc! {"$inc": {MONGO_TC: 1 }}); let mut opts = FindOneAndUpdateOptions::default(); opts.upsert = Some(true); match coll.find_one_and_update(doc! {}, mods, opts).await? { Some(t) => Ok(Some(t.tc)), - None => Ok(Some(0)) + None => Ok(Some(0)), } } pub async fn get_processes(&self) -> errors::Result> { debug!("Trying to get all processes..."); let coll = self.database.collection::(MONGO_COLL_PROCESSES); - let result = coll.find(None, None).await? - .try_collect().await.unwrap_or_else(|_| vec![]); + let result = coll + .find(None, None) + .await? + .try_collect() + .await + .unwrap_or_else(|_| vec![]); Ok(result) } @@ -87,9 +97,7 @@ impl ProcessStore { debug!("Trying to get process with id {}...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); match coll.find_one(Some(doc! { MONGO_ID: pid }), None).await { - Ok(process) => { - Ok(process) - } + Ok(process) => Ok(process), Err(e) => { error!("Error while getting process: {:#?}!", &e); Err(errors::Error::from(e)) @@ -98,7 +106,10 @@ impl ProcessStore { } pub async fn is_authorized(&self, user: &String, pid: &String) -> errors::Result { - debug!("checking if user '{}' is authorized to access '{}'", user, pid); + debug!( + "checking if user '{}' is authorized to access '{}'", + user, pid + ); return match self.get_process(&pid).await { Ok(Some(process)) => { let authorized = process.owners.iter().any(|o| { @@ -111,9 +122,7 @@ impl ProcessStore { trace!("didn't find process"); Ok(false) } - _ => { - Err(format!("User '{}' could not be authorized", &user).into()) - } + _ => Err(format!("User '{}' could not be authorized", &user).into()), }; } @@ -132,4 +141,4 @@ impl ProcessStore { } } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/logging-service/src/main.rs index 487dd7d..186d8a8 100644 --- a/clearing-house-app/logging-service/src/main.rs +++ b/clearing-house-app/logging-service/src/main.rs @@ -3,35 +3,19 @@ #[macro_use] extern crate tracing; -use std::path::Path; -use rocket::fairing::AdHoc; use crate::model::constants::ENV_LOGGING_SERVICE_ID; use db::config::doc_store::DatastoreConfigurator; use db::config::keyring_store::KeyringDbConfigurator; use db::config::process_store::ProcessStoreConfigurator; -use model::constants::SIGNING_KEY; -use crate::util::add_service_config; +mod config; +mod crypto; mod db; mod model; -mod services; -mod crypto; mod ports; -mod config; +mod services; mod util; -pub fn add_signing_key() -> AdHoc { - AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { - let private_key_path = rocket.figment().extract_inner(SIGNING_KEY).unwrap_or(String::from("keys/private_key.der")); - if Path::new(&private_key_path).exists() { - Ok(rocket.manage(private_key_path)) - } else { - tracing::error!("Signing key not found! Aborting startup! Please configure signing_key!"); - return Err(rocket); - } - }) -} - #[rocket::main] async fn main() -> Result<(), rocket::Error> { // Read configuration @@ -39,35 +23,36 @@ async fn main() -> Result<(), rocket::Error> { config::configure_logging(conf.log_level); let process_store = - ProcessStoreConfigurator::init_process_store(String::from(conf.process_database_url), conf.clear_db) + ProcessStoreConfigurator::init_process_store(conf.process_database_url, conf.clear_db) .await .expect("Failure to initialize process store! Exiting..."); let keyring_store = - KeyringDbConfigurator::init_keystore(String::from(conf.keyring_database_url), conf.clear_db) + KeyringDbConfigurator::init_keystore(conf.keyring_database_url, conf.clear_db) .await .expect("Failure to initialize keyring store! Exiting..."); let doc_store = - DatastoreConfigurator::init_datastore(String::from(conf.document_database_url), conf.clear_db) + DatastoreConfigurator::init_datastore(conf.document_database_url, conf.clear_db) .await .expect("Failure to initialize document store! Exiting..."); let keyring_service = services::keyring_service::KeyringService::new(keyring_store); - let doc_service = services::document_service::DocumentService::new(doc_store, keyring_service.clone()); - let logging_service = services::logging_service::LoggingService::new( - process_store, - doc_service.clone(), - ); + let doc_service = + services::document_service::DocumentService::new(doc_store, keyring_service.clone()); + let logging_service = + services::logging_service::LoggingService::new(process_store, doc_service.clone()); let _rocket = rocket::build() .manage(keyring_service) .manage(doc_service) .manage(logging_service) - .attach(add_signing_key()) - .attach(add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) + .attach(util::add_signing_key()) + .attach(util::add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) .attach(ports::logging_api::mount_api()) .attach(ports::doc_type_api::mount_api()) - .ignite().await? - .launch().await?; + .ignite() + .await? + .launch() + .await?; Ok(()) } diff --git a/clearing-house-app/logging-service/src/model/claims.rs b/clearing-house-app/logging-service/src/model/claims.rs index f9a8921..bd52dc9 100644 --- a/clearing-house-app/logging-service/src/model/claims.rs +++ b/clearing-house-app/logging-service/src/model/claims.rs @@ -1,33 +1,36 @@ -use std::env; -use std::fmt::{Display, Formatter}; -use biscuit::{ClaimPresenceOptions, ClaimsSet, Empty, jwa::SignatureAlgorithm, JWT, RegisteredClaims, SingleOrMultiple, Timestamp, ValidationOptions}; +use crate::model::constants::{ENV_SHARED_SECRET, SERVICE_HEADER}; +use crate::model::errors::*; +use crate::util::ServiceConfig; use biscuit::jwk::{AlgorithmParameters, CommonParameters, JWKSet}; -use biscuit::{jws, jws::Secret}; use biscuit::Presence::Required; use biscuit::Validation::Validate; +use biscuit::{ + jwa::SignatureAlgorithm, ClaimPresenceOptions, ClaimsSet, Empty, RegisteredClaims, + SingleOrMultiple, Timestamp, ValidationOptions, JWT, +}; +use biscuit::{jws, jws::Secret}; use chrono::{Duration, Utc}; use num_bigint::BigUint; use ring::signature::KeyPair; use rocket::http::Status; -use rocket::request::{Request, FromRequest, Outcome}; -use crate::model::errors::*; -use crate::model::constants::{ENV_SHARED_SECRET, SERVICE_HEADER}; -use crate::util::ServiceConfig; +use rocket::request::{FromRequest, Outcome, Request}; +use std::env; +use std::fmt::{Display, Formatter}; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct ChClaims{ +pub struct ChClaims { pub client_id: String, } -impl ChClaims{ - pub fn new(client_id: &str) -> ChClaims{ - ChClaims{ +impl ChClaims { + pub fn new(client_id: &str) -> ChClaims { + ChClaims { client_id: client_id.to_string(), } } } -impl Display for ChClaims{ +impl Display for ChClaims { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "<{}>", self.client_id) } @@ -49,26 +52,36 @@ impl<'r> FromRequest<'r> for ChClaims { Some(token) => { debug!("...received service header: {:?}", token); let service_config = request.rocket().state::().unwrap(); - match decode_token::(token, service_config.service_id.as_str()){ + match decode_token::(token, service_config.service_id.as_str()) { Ok(claims) => { debug!("...retrieved claims and succeed"); Outcome::Success(claims) - }, - Err(_) => Outcome::Failure((Status::BadRequest, ChClaimsError::Invalid)) + } + Err(_) => Outcome::Failure((Status::BadRequest, ChClaimsError::Invalid)), } } } } } -pub fn get_jwks(key_path: &str) -> Option>{ +pub fn get_jwks(key_path: &str) -> Option> { let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); - if let biscuit::jws::Secret::RsaKeyPair(a) = keypair{ - let pk_modulus = BigUint::from_bytes_be(a.as_ref().public_key().modulus().big_endian_without_leading_zero()); - let pk_e = BigUint::from_bytes_be(a.as_ref().public_key().exponent().big_endian_without_leading_zero()); - - let params = biscuit::jwk::RSAKeyParameters{ + if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { + let pk_modulus = BigUint::from_bytes_be( + a.as_ref() + .public_key() + .modulus() + .big_endian_without_leading_zero(), + ); + let pk_e = BigUint::from_bytes_be( + a.as_ref() + .public_key() + .exponent() + .big_endian_without_leading_zero(), + ); + + let params = biscuit::jwk::RSAKeyParameters { n: pk_modulus, e: pk_e, ..Default::default() @@ -77,57 +90,70 @@ pub fn get_jwks(key_path: &str) -> Option>{ let mut common = CommonParameters::default(); common.key_id = get_fingerprint(key_path); - let jwk = biscuit::jwk::JWK::{ + let jwk = biscuit::jwk::JWK:: { common, algorithm: AlgorithmParameters::RSA(params), additional: Empty::default(), }; - let jwks = biscuit::jwk::JWKSet::{ - keys: vec!(jwk) - }; - return Some(jwks) + let jwks = biscuit::jwk::JWKSet:: { keys: vec![jwk] }; + return Some(jwks); } None } -pub fn get_fingerprint(key_path: &str) -> Option{ +pub fn get_fingerprint(key_path: &str) -> Option { let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { - let pk_modulus = a.as_ref().public_key().modulus().big_endian_without_leading_zero().to_vec(); - let pk_e = a.as_ref().public_key().exponent().big_endian_without_leading_zero().to_vec(); + let pk_modulus = a + .as_ref() + .public_key() + .modulus() + .big_endian_without_leading_zero() + .to_vec(); + let pk_e = a + .as_ref() + .public_key() + .exponent() + .big_endian_without_leading_zero() + .to_vec(); let pk = openssh_keys::PublicKey::from_rsa(pk_e, pk_modulus); - return Some(pk.fingerprint()) + return Some(pk.fingerprint()); } None } -pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> String{ +pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> String { let private_claims = ChClaims::new(client_id); create_token(issuer, audience, &private_claims) } -pub fn create_token serde::Deserialize<'de>> (issuer: &str, audience: &str, private_claims: &T) -> String{ - let signing_secret = match env::var(ENV_SHARED_SECRET){ - Ok(secret) => { - Secret::Bytes(secret.to_string().into_bytes()) - }, +pub fn create_token serde::Deserialize<'de>>( + issuer: &str, + audience: &str, + private_claims: &T, +) -> String { + let signing_secret = match env::var(ENV_SHARED_SECRET) { + Ok(secret) => Secret::Bytes(secret.to_string().into_bytes()), Err(_) => { - panic!("Shared Secret not configured. Please configure environment variable {}", ENV_SHARED_SECRET); + panic!( + "Shared Secret not configured. Please configure environment variable {}", + ENV_SHARED_SECRET + ); } }; let expiration_date = Utc::now() + Duration::minutes(5); - let claims = ClaimsSet::{ - registered: RegisteredClaims{ + let claims = ClaimsSet:: { + registered: RegisteredClaims { issuer: Some(issuer.to_string()), issued_at: Some(Timestamp::from(Utc::now())), audience: Some(SingleOrMultiple::Single(audience.to_string())), expiry: Some(Timestamp::from(expiration_date)), ..Default::default() }, - private: private_claims.clone() + private: private_claims.clone(), }; // Construct the JWT @@ -136,24 +162,31 @@ pub fn create_token serde::Dese algorithm: SignatureAlgorithm::HS256, ..Default::default() }), - claims.clone() + claims.clone(), ); - jwt.into_encoded(&signing_secret).unwrap().unwrap_encoded().to_string() + jwt.into_encoded(&signing_secret) + .unwrap() + .unwrap_encoded() + .to_string() } -pub fn decode_token serde::Deserialize<'de>>(token: &str, audience: &str) -> errors::Result{ - let signing_secret = match env::var(ENV_SHARED_SECRET){ - Ok(secret) => { - Secret::Bytes(secret.to_string().into_bytes()) - }, +pub fn decode_token serde::Deserialize<'de>>( + token: &str, + audience: &str, +) -> errors::Result { + let signing_secret = match env::var(ENV_SHARED_SECRET) { + Ok(secret) => Secret::Bytes(secret.to_string().into_bytes()), Err(e) => { - error!("Shared Secret not configured. Please configure environment variable {}", ENV_SHARED_SECRET); - return Err(errors::Error::from(e)) + error!( + "Shared Secret not configured. Please configure environment variable {}", + ENV_SHARED_SECRET + ); + return Err(errors::Error::from(e)); } }; let jwt: jws::Compact, Empty> = JWT::<_, Empty>::new_encoded(token); - let decoded_jwt = jwt.decode(&signing_secret,SignatureAlgorithm::HS256)?; + let decoded_jwt = jwt.decode(&signing_secret, SignatureAlgorithm::HS256)?; let mut val_options = ValidationOptions::default(); let mut claim_presence_options = ClaimPresenceOptions::default(); claim_presence_options.expiry = Required; @@ -166,4 +199,4 @@ pub fn decode_token serde::Deserialize<'d val_options.audience = Validate(audience.to_string()); assert!(decoded_jwt.validate(val_options).is_ok()); Ok(decoded_jwt.payload().unwrap().private.clone()) -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/model/crypto.rs b/clearing-house-app/logging-service/src/model/crypto.rs index 0f091a5..9ae092c 100644 --- a/clearing-house-app/logging-service/src/model/crypto.rs +++ b/clearing-house-app/logging-service/src/model/crypto.rs @@ -1,35 +1,34 @@ -use std::collections::HashMap; use crate::crypto::generate_random_seed; +use core_lib::model::new_uuid; use hkdf::Hkdf; use sha2::Sha256; -use core_lib::model::new_uuid; +use std::collections::HashMap; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct MasterKey { pub id: String, pub key: String, - pub salt: String + pub salt: String, } -impl MasterKey{ - pub fn new(id: String, key: String, salt: String)-> MasterKey{ - MasterKey{ - id, - key, - salt - } +impl MasterKey { + pub fn new(id: String, key: String, salt: String) -> MasterKey { + MasterKey { id, key, salt } } - pub fn new_random() -> MasterKey{ + pub fn new_random() -> MasterKey { let key_salt = generate_random_seed(); let ikm = generate_random_seed(); let (master_key, _) = Hkdf::::extract(Some(&key_salt), &ikm); - MasterKey::new(new_uuid(), hex::encode_upper(master_key), hex::encode_upper(generate_random_seed())) + MasterKey::new( + new_uuid(), + hex::encode_upper(master_key), + hex::encode_upper(generate_random_seed()), + ) } } - #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct KeyEntry { pub id: String, @@ -37,13 +36,9 @@ pub struct KeyEntry { pub nonce: Vec, } -impl KeyEntry{ - pub fn new(id: String, key: Vec, nonce: Vec)-> KeyEntry{ - KeyEntry{ - id, - key, - nonce - } +impl KeyEntry { + pub fn new(id: String, key: Vec, nonce: Vec) -> KeyEntry { + KeyEntry { id, key, nonce } } } @@ -54,58 +49,48 @@ pub struct KeyMap { pub keys_enc: Option>, } -impl KeyMap{ - pub fn new(enc: bool, keys: HashMap, keys_enc: Option>) -> KeyMap{ - KeyMap{ +impl KeyMap { + pub fn new(enc: bool, keys: HashMap, keys_enc: Option>) -> KeyMap { + KeyMap { enc, keys, - keys_enc + keys_enc, } } } - #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyCt{ +pub struct KeyCt { pub id: String, - pub ct: String + pub ct: String, } -impl KeyCt{ - pub fn new(id: String, ct: String) -> KeyCt{ - KeyCt{ - id, - ct - } +impl KeyCt { + pub fn new(id: String, ct: String) -> KeyCt { + KeyCt { id, ct } } } #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct KeyCtList { pub dt: String, - pub cts: Vec + pub cts: Vec, } -impl KeyCtList{ - pub fn new(dt: String, cts: Vec) -> KeyCtList{ - KeyCtList{ - dt, - cts - } +impl KeyCtList { + pub fn new(dt: String, cts: Vec) -> KeyCtList { + KeyCtList { dt, cts } } } #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct KeyMapListItem { pub id: String, - pub map: KeyMap + pub map: KeyMap, } -impl KeyMapListItem{ - pub fn new(id: String, map: KeyMap) -> KeyMapListItem{ - KeyMapListItem{ - id, - map - } +impl KeyMapListItem { + pub fn new(id: String, map: KeyMap) -> KeyMapListItem { + KeyMapListItem { id, map } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/model/doc_type.rs b/clearing-house-app/logging-service/src/model/doc_type.rs index fe53c2a..2cff304 100644 --- a/clearing-house-app/logging-service/src/model/doc_type.rs +++ b/clearing-house-app/logging-service/src/model/doc_type.rs @@ -7,11 +7,7 @@ pub struct DocumentType { impl DocumentType { pub fn new(id: String, pid: String, parts: Vec) -> DocumentType { - DocumentType{ - id, - pid, - parts, - } + DocumentType { id, pid, parts } } } @@ -21,9 +17,7 @@ pub struct DocumentTypePart { } impl DocumentTypePart { - pub fn new(name: String) -> DocumentTypePart{ - DocumentTypePart{ - name - } + pub fn new(name: String) -> DocumentTypePart { + DocumentTypePart { name } } } diff --git a/clearing-house-app/logging-service/src/model/document.rs b/clearing-house-app/logging-service/src/model/document.rs index 15decf0..0c6a4d7 100644 --- a/clearing-house-app/logging-service/src/model/document.rs +++ b/clearing-house-app/logging-service/src/model/document.rs @@ -1,14 +1,14 @@ +use crate::model::constants::SPLIT_CT; +use crate::model::crypto::{KeyEntry, KeyMap}; +use crate::model::errors::*; +use crate::model::util::new_uuid; +use aes_gcm_siv::aead::Aead; use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; -use aes_gcm_siv::aead::{Aead}; use blake2_rfc::blake2b::Blake2b; +use chrono::Local; use generic_array::GenericArray; use std::collections::HashMap; use uuid::Uuid; -use crate::model::errors::*; -use crate::model::constants::{SPLIT_CT}; -use crate::model::util::new_uuid; -use crate::model::crypto::{KeyEntry, KeyMap}; -use chrono::Local; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct DocumentPart { @@ -18,10 +18,7 @@ pub struct DocumentPart { impl DocumentPart { pub fn new(name: String, content: Option) -> DocumentPart { - DocumentPart { - name, - content, - } + DocumentPart { name, content } } pub fn encrypt(&self, key: &[u8], nonce: &[u8]) -> errors::Result> { @@ -29,12 +26,20 @@ impl DocumentPart { const EXP_NONCE_SIZE: usize = 12; // check key size if key.len() != EXP_KEY_SIZE { - error!("Given key has size {} but expected {} bytes", key.len(), EXP_KEY_SIZE); + error!( + "Given key has size {} but expected {} bytes", + key.len(), + EXP_KEY_SIZE + ); error_chain::bail!("Incorrect key size") } // check nonce size else if nonce.len() != EXP_NONCE_SIZE { - error!("Given nonce has size {} but expected {} bytes", nonce.len(), EXP_NONCE_SIZE); + error!( + "Given nonce has size {} but expected {} bytes", + nonce.len(), + EXP_NONCE_SIZE + ); error_chain::bail!("Incorrect nonce size") } else { let key = GenericArray::from_slice(key); @@ -46,7 +51,7 @@ impl DocumentPart { let pt = format_pt_for_storage(&self.name, pt); match cipher.encrypt(nonce, pt.as_bytes()) { Ok(ct) => Ok(ct), - Err(e) => error_chain::bail!("Error while encrypting {}", e) + Err(e) => error_chain::bail!("Error while encrypting {}", e), } } None => { @@ -96,7 +101,7 @@ impl Document { // the hash is set to "0". Chaining is not done here. pub fn encrypt(&self, key_map: KeyMap) -> errors::Result { debug!("encrypting document of doc_type {}", self.dt_id); - let mut cts = vec!(); + let mut cts = vec![]; let keys = key_map.keys; let key_ct; @@ -133,7 +138,15 @@ impl Document { } cts.sort(); - Ok(EncryptedDocument::new(self.id.clone(), self.pid.clone(), self.dt_id.clone(), self.ts, self.tc, key_ct, cts)) + Ok(EncryptedDocument::new( + self.id.clone(), + self.pid.clone(), + self.dt_id.clone(), + self.ts, + self.tc, + key_ct, + cts, + )) } pub fn get_formatted_tc(&self) -> String { @@ -159,7 +172,14 @@ impl Document { } } - fn restore(id: String, pid: String, dt_id: String, ts: i64, tc: i64, parts: Vec) -> Document { + fn restore( + id: String, + pid: String, + dt_id: String, + ts: i64, + tc: i64, + parts: Vec, + ) -> Document { Document { id, dt_id, @@ -187,7 +207,7 @@ impl EncryptedDocument { /// Note: KeyMap keys need to be KeyEntry.ids in this case // Decryption is done without checking the hashes. Do this before calling this method pub fn decrypt(&self, keys: HashMap) -> errors::Result { - let mut pts = vec!(); + let mut pts = vec![]; for ct in self.cts.iter() { let ct_parts = ct.split(SPLIT_CT).collect::>(); if ct_parts.len() != 2 { @@ -214,7 +234,14 @@ impl EncryptedDocument { } } - Ok(Document::restore(self.id.clone(), self.pid.clone(), self.dt_id.clone(), self.ts, self.tc, pts)) + Ok(Document::restore( + self.id.clone(), + self.pid.clone(), + self.dt_id.clone(), + self.ts, + self.tc, + pts, + )) } pub fn get_formatted_tc(&self) -> String { @@ -242,7 +269,15 @@ impl EncryptedDocument { res } - pub fn new(id: String, pid: String, dt_id: String, ts: i64, tc: i64, keys_ct: String, cts: Vec) -> EncryptedDocument { + pub fn new( + id: String, + pid: String, + dt_id: String, + ts: i64, + tc: i64, + keys_ct: String, + cts: Vec, + ) -> EncryptedDocument { EncryptedDocument { id, pid, @@ -263,7 +298,11 @@ pub fn restore_pt(pt: &str) -> errors::Result<(String, String, String)> { if vec.len() != 3 { error_chain::bail!("Could not restore plaintext"); } - Ok((String::from(vec[0]), String::from(vec[1]), String::from(vec[2]))) + Ok(( + String::from(vec[0]), + String::from(vec[1]), + String::from(vec[2]), + )) } /// companion to format_pt_for_storage_no_dt @@ -283,4 +322,4 @@ fn format_pt_for_storage(field_name: &str, pt: &str) -> String { fn format_tc(tc: i64) -> String { format!("{:08}", tc) -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/model/errors.rs b/clearing-house-app/logging-service/src/model/errors.rs index 4e9f661..73646ec 100644 --- a/clearing-house-app/logging-service/src/model/errors.rs +++ b/clearing-house-app/logging-service/src/model/errors.rs @@ -1,7 +1,7 @@ pub mod errors { use error_chain::error_chain; // Create the Error, ErrorKind, ResultExt, and Result types - error_chain!{ + error_chain! { foreign_links { Conversion(std::num::TryFromIntError); Figment(rocket::figment::Error); @@ -15,4 +15,4 @@ pub mod errors { EnvVariable(::std::env::VarError); } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/model/ids/message.rs b/clearing-house-app/logging-service/src/model/ids/message.rs index 08d5ee1..dafa0b1 100644 --- a/clearing-house-app/logging-service/src/model/ids/message.rs +++ b/clearing-house-app/logging-service/src/model/ids/message.rs @@ -1,7 +1,7 @@ -use std::collections::HashMap; use crate::model::constants::DEFAULT_DOC_TYPE; use crate::model::document::{Document, DocumentPart}; -use crate::model::ids::{InfoModelDateTime, InfoModelId, SecurityToken, MessageType}; +use crate::model::ids::{InfoModelDateTime, InfoModelId, MessageType, SecurityToken}; +use std::collections::HashMap; const MESSAGE_ID: &'static str = "message_id"; const MODEL_VERSION: &'static str = "model_version"; @@ -39,47 +39,75 @@ pub struct IdsMessage { // process id pub pid: Option, //IDS name - #[serde(rename = "ids:modelVersion", alias="modelVersion")] + #[serde(rename = "ids:modelVersion", alias = "modelVersion")] // Version of the Information Model against which the Message should be interpreted pub model_version: String, //IDS name - #[serde(rename = "ids:correlationMessage", alias="correlationMessage", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "ids:correlationMessage", + alias = "correlationMessage", + skip_serializing_if = "Option::is_none" + )] // Correlated message, e.g. a response to a previous request pub correlation_message: Option, //IDS name - #[serde(rename = "ids:issued", alias="issued")] + #[serde(rename = "ids:issued", alias = "issued")] // Date of issuing the Message pub issued: InfoModelDateTime, //IDS name - #[serde(rename = "ids:issuerConnector", alias="issuerConnector")] + #[serde(rename = "ids:issuerConnector", alias = "issuerConnector")] // The Connector which is the origin of the message pub issuer_connector: InfoModelId, //IDS name - #[serde(rename = "ids:senderAgent", alias="senderAgent")] + #[serde(rename = "ids:senderAgent", alias = "senderAgent")] // The Agent which initiated the Message pub sender_agent: String, //IDS name - #[serde(rename = "ids:recipientConnector", alias="recipientConnector", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "ids:recipientConnector", + alias = "recipientConnector", + skip_serializing_if = "Option::is_none" + )] // The Connector which is the recipient of the message pub recipient_connector: Option>, //IDS name - #[serde(rename = "ids:recipientAgent", alias="recipientAgent", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "ids:recipientAgent", + alias = "recipientAgent", + skip_serializing_if = "Option::is_none" + )] // The Agent for which the Message is intended pub recipient_agent: Option>, //IDS name - #[serde(rename = "ids:transferContract", alias="transferContract", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "ids:transferContract", + alias = "transferContract", + skip_serializing_if = "Option::is_none" + )] // The contract which is (or will be) the legal basis of the data transfer pub transfer_contract: Option, //IDS name - #[serde(rename = "ids:contentVersion", alias="contentVersion", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "ids:contentVersion", + alias = "contentVersion", + skip_serializing_if = "Option::is_none" + )] // The contract which is (or will be) the legal basis of the data transfer pub content_version: Option, //IDS name - #[serde(rename = "ids:securityToken", alias="securityToken", skip_serializing)] + #[serde( + rename = "ids:securityToken", + alias = "securityToken", + skip_serializing + )] // Authorization pub security_token: Option, //IDS name - #[serde(rename = "ids:authorizationToken", alias="authorizationToken", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "ids:authorizationToken", + alias = "authorizationToken", + skip_serializing_if = "Option::is_none" + )] // Authorization pub authorization_token: Option, //IDS name @@ -92,7 +120,6 @@ pub struct IdsMessage { pub payload_type: Option, } - macro_rules! hashmap { ($( $key: expr => $val: expr ),*) => {{ let mut map = ::std::collections::HashMap::new(); @@ -105,10 +132,9 @@ impl Default for IdsMessage { fn default() -> Self { IdsMessage { context: Some(hashmap![ - "ids".to_string() => "https://w3id.org/idsa/core/".to_string(), - "idsc".to_string() => "https://w3id.org/idsa/code/".to_string() - ] - ), + "ids".to_string() => "https://w3id.org/idsa/core/".to_string(), + "idsc".to_string() => "https://w3id.org/idsa/code/".to_string() + ]), type_message: MessageType::Message, id: Some(autogen("MessageProcessedNotification")), pid: None, @@ -169,11 +195,11 @@ impl IdsMessage { authorization_token: msg.authorization_token.clone(), payload: msg.payload.clone(), content_version: msg.content_version.clone(), - payload_type: msg.payload.clone() + payload_type: msg.payload.clone(), } } - pub fn restore() -> IdsMessage{ + pub fn restore() -> IdsMessage { IdsMessage { type_message: MessageType::LogMessage, //TODO recipient_agent CH @@ -204,7 +230,6 @@ impl IdsMessage { /// - payload /// - payload_type impl From for IdsMessage { - fn from(doc: Document) -> Self { let mut m = IdsMessage::restore(); // pid @@ -234,9 +259,12 @@ impl From for IdsMessage { match serde_json::from_str(v.as_ref().unwrap()) { Ok(date_time) => { m.issued = date_time; - }, + } Err(e) => { - error!("Error while converting DateTimeStamp (field 'issued') from database: {}", e); + error!( + "Error while converting DateTimeStamp (field 'issued') from database: {}", + e + ); } } } @@ -274,10 +302,10 @@ impl From for IdsMessage { } /// Conversion from IdsMessage to Document -/// +/// /// most important part to store: /// payload and payload type -/// +/// /// meta data that we also need to store /// - message_id /// - pid @@ -302,10 +330,7 @@ impl From for Document { None => autogen("Message"), }; - doc_parts.push(DocumentPart::new( - MESSAGE_ID.to_string(), - Some(id), - )); + doc_parts.push(DocumentPart::new(MESSAGE_ID.to_string(), Some(id))); // model_version doc_parts.push(DocumentPart::new( @@ -322,7 +347,7 @@ impl From for Document { // issued doc_parts.push(DocumentPart::new( ISSUED.to_string(), - serde_json::to_string(&m.issued).ok() + serde_json::to_string(&m.issued).ok(), )); // issuer_connector @@ -334,7 +359,7 @@ impl From for Document { // sender_agent doc_parts.push(DocumentPart::new( SENDER_AGENT.to_string(), - Some(m.sender_agent.to_string()) + Some(m.sender_agent.to_string()), )); // transfer_contract @@ -356,15 +381,12 @@ impl From for Document { //TODO // payload - doc_parts.push(DocumentPart::new( - PAYLOAD.to_string(), - m.payload.clone() - )); + doc_parts.push(DocumentPart::new(PAYLOAD.to_string(), m.payload.clone())); // payload_type doc_parts.push(DocumentPart::new( PAYLOAD_TYPE.to_string(), - m.payload_type.clone() + m.payload_type.clone(), )); // pid @@ -373,5 +395,11 @@ impl From for Document { } fn autogen(message: &str) -> String { - ["https://w3id.org/idsa/autogen/", message, "/", &Document::create_uuid()].concat() -} \ No newline at end of file + [ + "https://w3id.org/idsa/autogen/", + message, + "/", + &Document::create_uuid(), + ] + .concat() +} diff --git a/clearing-house-app/logging-service/src/model/ids/mod.rs b/clearing-house-app/logging-service/src/model/ids/mod.rs index fa211bb..25c98b7 100644 --- a/clearing-house-app/logging-service/src/model/ids/mod.rs +++ b/clearing-house-app/logging-service/src/model/ids/mod.rs @@ -1,7 +1,7 @@ +use crate::model::ids::message::IdsMessage; use chrono::prelude::*; use std::fmt; use std::fmt::{Display, Formatter, Result}; -use crate::model::ids::message::IdsMessage; pub mod message; pub mod request; @@ -9,25 +9,23 @@ pub mod request; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct InfoModelComplexId { //IDS name - #[serde(rename = "@id", alias="id", skip_serializing_if = "Option::is_none")] + #[serde(rename = "@id", alias = "id", skip_serializing_if = "Option::is_none")] // Correlated message, e.g. a response to a previous request - pub id: Option + pub id: Option, } impl Display for InfoModelComplexId { fn fmt(&self, f: &mut Formatter<'_>) -> Result { match &self.id { Some(id) => write!(f, "{}", serde_json::to_string(id).unwrap()), - None => write!(f, "") + None => write!(f, ""), } } } impl InfoModelComplexId { pub fn new(id: String) -> InfoModelComplexId { - InfoModelComplexId { - id: Some(id) - } + InfoModelComplexId { id: Some(id) } } } impl From for InfoModelComplexId { @@ -40,7 +38,7 @@ impl From for InfoModelComplexId { #[serde(untagged)] pub enum InfoModelId { SimpleId(String), - ComplexId(InfoModelComplexId) + ComplexId(InfoModelComplexId), } impl InfoModelId { @@ -56,7 +54,7 @@ impl fmt::Display for InfoModelId { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match self { InfoModelId::SimpleId(id) => fmt.write_str(&id)?, - InfoModelId::ComplexId(id) => fmt.write_str(&id.to_string())? + InfoModelId::ComplexId(id) => fmt.write_str(&id.to_string())?, } Ok(()) } @@ -71,7 +69,7 @@ impl From for InfoModelId { #[serde(untagged)] pub enum InfoModelDateTime { ComplexTime(InfoModelTimeStamp), - Time(DateTime) + Time(DateTime), } impl InfoModelDateTime { @@ -87,7 +85,7 @@ impl fmt::Display for InfoModelDateTime { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { match self { InfoModelDateTime::Time(value) => fmt.write_str(&value.to_string())?, - InfoModelDateTime::ComplexTime(value) => fmt.write_str(&value.to_string())? + InfoModelDateTime::ComplexTime(value) => fmt.write_str(&value.to_string())?, } Ok(()) } @@ -96,10 +94,14 @@ impl fmt::Display for InfoModelDateTime { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct InfoModelTimeStamp { //IDS name - #[serde(rename = "@type", alias="type", skip_serializing_if = "Option::is_none")] + #[serde( + rename = "@type", + alias = "type", + skip_serializing_if = "Option::is_none" + )] pub format: Option, //IDS name - #[serde(rename = "@value", alias="value")] + #[serde(rename = "@value", alias = "value")] pub value: DateTime, } @@ -107,7 +109,7 @@ impl Default for InfoModelTimeStamp { fn default() -> Self { InfoModelTimeStamp { format: Some("http://www.w3.org/2001/XMLSchema#dateTimeStamp".to_string()), - value: Local::now() + value: Local::now(), } } } @@ -176,17 +178,24 @@ impl SecurityToken { } #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct IdsQueryResult{ +pub struct IdsQueryResult { pub date_from: String, pub date_to: String, pub page: i32, pub size: i32, pub order: String, - pub documents: Vec -} - -impl IdsQueryResult{ - pub fn new(date_from: i64, date_to: i64, page: Option, size: Option, order: String, documents: Vec) -> IdsQueryResult{ + pub documents: Vec, +} + +impl IdsQueryResult { + pub fn new( + date_from: i64, + date_to: i64, + page: Option, + size: Option, + order: String, + documents: Vec, + ) -> IdsQueryResult { let date_from = NaiveDateTime::from_timestamp_opt(date_from, 0) .expect("Invalid date_from seconds") .format("%Y-%m-%d %H:%M:%S") @@ -196,13 +205,13 @@ impl IdsQueryResult{ .format("%Y-%m-%d %H:%M:%S") .to_string(); - IdsQueryResult{ + IdsQueryResult { date_from, date_to, page: page.unwrap_or(-1), size: size.unwrap_or(-1), order, - documents + documents, } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/model/ids/request.rs b/clearing-house-app/logging-service/src/model/ids/request.rs index e332c2d..fac3d7f 100644 --- a/clearing-house-app/logging-service/src/model/ids/request.rs +++ b/clearing-house-app/logging-service/src/model/ids/request.rs @@ -9,11 +9,15 @@ pub struct ClearingHouseMessage { } impl ClearingHouseMessage { - pub fn new(header: IdsMessage, payload: Option, payload_type: Option) -> ClearingHouseMessage{ - ClearingHouseMessage{ + pub fn new( + header: IdsMessage, + payload: Option, + payload_type: Option, + ) -> ClearingHouseMessage { + ClearingHouseMessage { header, payload, - payload_type + payload_type, } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/model/mod.rs b/clearing-house-app/logging-service/src/model/mod.rs index f957540..26594c2 100644 --- a/clearing-house-app/logging-service/src/model/mod.rs +++ b/clearing-house-app/logging-service/src/model/mod.rs @@ -1,17 +1,12 @@ -use biscuit::{Empty, CompactJson}; -use biscuit::jws::{Compact, Header}; -use biscuit::jwa::SignatureAlgorithm; - +pub(crate) mod claims; pub mod constants; -pub mod ids; pub(crate) mod crypto; pub(crate) mod doc_type; -pub(crate) mod errors; pub(crate) mod document; -pub(crate) mod util; +pub(crate) mod errors; +pub mod ids; pub(crate) mod process; -pub(crate) mod claims; - +pub(crate) mod util; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rocket::FromFormField)] pub enum SortingOrder { @@ -23,37 +18,45 @@ pub enum SortingOrder { Descending, } - pub fn parse_date(date: Option, to_date: bool) -> Option { - let time_format; - if to_date { - time_format = "23:59:59" - } else { - time_format = "00:00:00" - } + let time_format = if to_date { "23:59:59" } else { "00:00:00" }; match date { Some(d) => { debug!("Parsing date: {}", &d); - match chrono::NaiveDateTime::parse_from_str(format!("{} {}", &d, &time_format).as_str(), "%Y-%m-%d %H:%M:%S") { - Ok(date) => { - Some(date) - } + match chrono::NaiveDateTime::parse_from_str( + format!("{} {}", &d, &time_format).as_str(), + "%Y-%m-%d %H:%M:%S", + ) { + Ok(date) => Some(date), Err(e) => { error!("Error occurred: {:#?}", e); - return None; + None } } } - None => None + None => None, } } -pub fn sanitize_dates(date_from: Option, date_to: Option) -> (chrono::NaiveDateTime, chrono::NaiveDateTime) { - let default_to_date = chrono::Local::now().naive_local(); - let default_from_date = default_to_date.date() +/// Validates the provided dates. `date_now` is optional and defaults to `chrono::Local::now().naive_local()`. +pub fn validate_and_sanitize_dates( + date_from: Option, + date_to: Option, + date_now: Option, +) -> anyhow::Result<(chrono::NaiveDateTime, chrono::NaiveDateTime)> { + let now = date_now.unwrap_or(chrono::Local::now().naive_local()); + debug!( + "... validating dates: now: {:#?} , from: {:#?} , to: {:#?}", + &now, &date_from, &date_to + ); + + let default_to_date = now; + let default_from_date = default_to_date + .date() .and_hms_opt(0, 0, 0) - .expect("00:00:00 is a valid time") - chrono::Duration::weeks(2); + .expect("00:00:00 is a valid time") + - chrono::Duration::weeks(2); println!("date_to: {:#?}", date_to); println!("date_from: {:#?}", date_from); @@ -62,39 +65,69 @@ pub fn sanitize_dates(date_from: Option, date_to: Option< println!("Default date_from: {:#?}", default_from_date); match (date_from, date_to) { - (Some(from), Some(to)) => (from, to), // validate already checked that date_from > date_to - (Some(from), None) => (from, default_to_date), // if to_date is missing, default to now - (None, Some(_to)) => todo!("Not defined yet; check"), - (None, None) => (default_from_date, default_to_date), // if both dates are none (case to_date is none and from_date is_some should be catched by validation); return dates for default duration (last 2 weeks) + (Some(from), None) if from < now => Ok((from, default_to_date)), + (Some(from), Some(to)) if from < now && to <= now && from < to => Ok((from, to)), + (None, None) => Ok((default_from_date, default_to_date)), + _ => Err(anyhow::anyhow!("Invalid date parameters")), } } -pub fn validate_dates(date_from: Option, date_to: Option) -> bool { - let date_now = chrono::Local::now().naive_local(); - debug!("... validating dates: now: {:#?} , from: {:#?} , to: {:#?}", &date_now, &date_from, &date_to); - // date_from before now - if date_from.is_some() && date_from.as_ref().unwrap().clone() > date_now { - debug!("oh no, date_from {:#?} is in the future! date_now is {:#?}", &date_from, &date_now); - return false; - } - - // date_to only if there is also date_from - if date_from.is_none() && date_to.is_some() { - return false; - } +#[cfg(test)] +mod test { + #[test] + fn validate_and_sanitize_dates() { + // Setup dates for testing + let date_now = chrono::Local::now().naive_local(); + let date_now_midnight = date_now.date().and_hms_opt(0, 0, 0).unwrap(); + let date_from = date_now_midnight - chrono::Duration::weeks(2); + let date_to = date_now_midnight - chrono::Duration::weeks(1); - // date_to before or equals now - if date_to.is_some() && date_to.as_ref().unwrap().clone() >= date_now { - debug!("oh no, date_to {:#?} is in the future! date_now is {:#?}", &date_to, &date_now); - return false; - } + // # Good cases + assert_eq!( + (date_from, date_now), + super::validate_and_sanitize_dates(None, None, Some(date_now)) + .expect("Should be valid") + ); + assert_eq!( + (date_from, date_now), + super::validate_and_sanitize_dates(Some(date_from), None, Some(date_now)) + .expect("Should be valid") + ); + assert_eq!( + (date_from, date_to), + super::validate_and_sanitize_dates(Some(date_from), Some(date_to), Some(date_now)) + .expect("Should be valid") + ); + assert_eq!( + (date_from, date_to), + super::validate_and_sanitize_dates(Some(date_from), Some(date_to), Some(date_to)) + .expect("Should be valid") + ); - // date_from before date_to - if date_from.is_some() && date_to.is_some() { - if date_from.unwrap() > date_to.unwrap() { - debug!("oh no, date_from {:#?} is before date_to {:#?}", &date_from, &date_to); - return false; - } + // # Bad cases + // no to without from not satisfied + assert!(super::validate_and_sanitize_dates(None, Some(date_to), Some(date_now)).is_err()); + // from < now not satisfied + assert!(super::validate_and_sanitize_dates(Some(date_now), None, Some(date_to)).is_err()); + // from < to not satisfied + assert!( + super::validate_and_sanitize_dates(Some(date_to), Some(date_from), Some(date_now)) + .is_err() + ); + // from < to not satisfied + assert!( + super::validate_and_sanitize_dates(Some(date_to), Some(date_to), Some(date_now)) + .is_err() + ); + // to < now not satisfied + assert!( + super::validate_and_sanitize_dates(Some(date_from), Some(date_now), Some(date_to)) + .is_err() + ); + // from < now && to < now not satisfied + assert!( + super::validate_and_sanitize_dates(Some(date_to), Some(date_now), Some(date_from)) + .is_err() + ); } - return true; -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/model/process.rs b/clearing-house-app/logging-service/src/model/process.rs index 767a31d..3eb2c01 100644 --- a/clearing-house-app/logging-service/src/model/process.rs +++ b/clearing-house-app/logging-service/src/model/process.rs @@ -6,34 +6,29 @@ pub struct Process { impl Process { pub fn new(id: String, owners: Vec) -> Process { - Process { - id, - owners - } + Process { id, owners } } } #[derive(serde::Serialize, serde::Deserialize)] -pub struct TransactionCounter{ - pub tc: i64 +pub struct TransactionCounter { + pub tc: i64, } #[derive(serde::Serialize, serde::Deserialize)] -pub struct OwnerList{ - pub owners: Vec +pub struct OwnerList { + pub owners: Vec, } -impl OwnerList{ - pub fn new(owners: Vec) -> OwnerList{ - OwnerList{ - owners, - } +impl OwnerList { + pub fn new(owners: Vec) -> OwnerList { + OwnerList { owners } } } #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Receipt { - pub data: biscuit::jws::Compact + pub data: biscuit::jws::Compact, } #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] @@ -48,35 +43,38 @@ pub struct DataTransaction { pub clearing_house_version: String, } -impl biscuit::CompactJson for DataTransaction{} +impl biscuit::CompactJson for DataTransaction {} -impl DataTransaction{ - pub fn sign(&self, key_path: &str) -> Receipt{ +impl DataTransaction { + pub fn sign(&self, key_path: &str) -> Receipt { use crate::model::claims::get_fingerprint; - let jws = biscuit::jws::Compact::new_decoded(biscuit::jws::Header::from_registered_header(biscuit::jws::RegisteredHeader{ - algorithm: biscuit::jwa::SignatureAlgorithm::PS512, - media_type: None, - key_id: get_fingerprint(key_path), - ..Default::default()}), self.clone()); + let jws = biscuit::jws::Compact::new_decoded( + biscuit::jws::Header::from_registered_header(biscuit::jws::RegisteredHeader { + algorithm: biscuit::jwa::SignatureAlgorithm::PS512, + media_type: None, + key_id: get_fingerprint(key_path), + ..Default::default() + }), + self.clone(), + ); let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); println!("decoded JWS:{:#?}", &jws); - Receipt{ - data: jws.into_encoded(&keypair).unwrap() + Receipt { + data: jws.into_encoded(&keypair).unwrap(), } } } // convenience method for testing -impl From for DataTransaction{ +impl From for DataTransaction { fn from(r: Receipt) -> Self { - match r.data.unverified_payload(){ + match r.data.unverified_payload() { Ok(d) => d.clone(), Err(e) => { println!("Error occured: {:#?}", e); - DataTransaction{ - + DataTransaction { transaction_id: "error".to_string(), timestamp: 0, process_id: "error".to_string(), @@ -89,4 +87,4 @@ impl From for DataTransaction{ } } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/model/util.rs b/clearing-house-app/logging-service/src/model/util.rs index 1396d9b..367d122 100644 --- a/clearing-house-app/logging-service/src/model/util.rs +++ b/clearing-house-app/logging-service/src/model/util.rs @@ -1,4 +1,4 @@ pub fn new_uuid() -> String { use uuid::Uuid; Uuid::new_v4().hyphenated().to_string() -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs index 6d088d5..2699063 100644 --- a/clearing-house-app/logging-service/src/ports/doc_type_api.rs +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -1,46 +1,57 @@ +use crate::model::constants::{DEFAULT_PROCESS_ID, ROCKET_DOC_TYPE_API}; use crate::ports::ApiResponse; -use crate::model::constants::{ROCKET_DOC_TYPE_API, DEFAULT_PROCESS_ID}; use rocket::fairing::AdHoc; +use rocket::serde::json::{json, Json}; use rocket::State; -use rocket::serde::json::{json,Json}; -use crate::services::keyring_service::KeyringService; use crate::model::doc_type::DocumentType; +use crate::services::keyring_service::KeyringService; #[rocket::post("/", format = "json", data = "")] -async fn create_doc_type(key_api: &State, doc_type: Json) -> ApiResponse { - match key_api.inner().create_doc_type(doc_type.into_inner()).await{ +async fn create_doc_type( + key_api: &State, + doc_type: Json, +) -> ApiResponse { + match key_api.inner().create_doc_type(doc_type.into_inner()).await { Ok(dt) => ApiResponse::SuccessCreate(json!(dt)), Err(e) => { error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(e.to_string()) + return ApiResponse::InternalError(e.to_string()); } } } #[rocket::post("/", format = "json", data = "")] -async fn update_doc_type(key_api: &State, id: String, doc_type: Json) -> ApiResponse { - match key_api.inner().update_doc_type(id, doc_type.into_inner()).await{ +async fn update_doc_type( + key_api: &State, + id: String, + doc_type: Json, +) -> ApiResponse { + match key_api + .inner() + .update_doc_type(id, doc_type.into_inner()) + .await + { Ok(id) => ApiResponse::SuccessOk(json!(id)), Err(e) => { error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(e.to_string()) + return ApiResponse::InternalError(e.to_string()); } } } #[rocket::delete("/", format = "json")] -async fn delete_default_doc_type(key_api: &State, id: String) -> ApiResponse{ - delete_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await +async fn delete_default_doc_type(key_api: &State, id: String) -> ApiResponse { + delete_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await } #[rocket::delete("//", format = "json")] -async fn delete_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse{ - match key_api.inner().delete_doc_type(id, pid).await{ +async fn delete_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse { + match key_api.inner().delete_doc_type(id, pid).await { Ok(id) => ApiResponse::SuccessOk(json!(id)), Err(e) => { error!("Error while deleting doctype: {:?}", e); - return ApiResponse::InternalError(e.to_string()) + return ApiResponse::InternalError(e.to_string()); } } } @@ -52,36 +63,42 @@ async fn get_default_doc_type(key_api: &State, id: String) -> Ap #[rocket::get("//", format = "json")] async fn get_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse { - match key_api.inner().get_doc_type(id, pid).await{ - Ok(dt) => { - match dt{ - Some(dt) => ApiResponse::SuccessOk(json!(dt)), - None => ApiResponse::SuccessOk(json!(null)) - } + match key_api.inner().get_doc_type(id, pid).await { + Ok(dt) => match dt { + Some(dt) => ApiResponse::SuccessOk(json!(dt)), + None => ApiResponse::SuccessOk(json!(null)), }, Err(e) => { error!("Error while retrieving doctype: {:?}", e); - return ApiResponse::InternalError(e.to_string()) + return ApiResponse::InternalError(e.to_string()); } } } #[rocket::get("/", format = "json")] async fn get_doc_types(key_api: &State) -> ApiResponse { - match key_api.inner().get_doc_types().await{ + match key_api.inner().get_doc_types().await { Ok(dt) => ApiResponse::SuccessOk(json!(dt)), Err(e) => { error!("Error while retrieving doctypes: {:?}", e); - return ApiResponse::InternalError(e.to_string()) + return ApiResponse::InternalError(e.to_string()); } } } pub fn mount_api() -> AdHoc { AdHoc::on_ignite("Mounting Document Type API", |rocket| async { - rocket - .mount(ROCKET_DOC_TYPE_API, rocket::routes![create_doc_type, - update_doc_type, delete_default_doc_type, delete_doc_type, - get_default_doc_type, get_doc_type , get_doc_types]) + rocket.mount( + ROCKET_DOC_TYPE_API, + rocket::routes![ + create_doc_type, + update_doc_type, + delete_default_doc_type, + delete_doc_type, + get_default_doc_type, + get_doc_type, + get_doc_types + ], + ) }) -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/ports/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs index 35bb2e2..685bd24 100644 --- a/clearing-house-app/logging-service/src/ports/logging_api.rs +++ b/clearing-house-app/logging-service/src/ports/logging_api.rs @@ -1,16 +1,17 @@ use crate::{ - ports::{ - ApiResponse, - }, - model::claims::{ChClaims, get_jwks}, + model::claims::{get_jwks, ChClaims}, model::SortingOrder, + ports::ApiResponse, }; -use rocket::serde::json::{json, Json}; use rocket::fairing::AdHoc; +use rocket::serde::json::{json, Json}; use rocket::State; +use crate::model::constants::{ + ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_PK_API, ROCKET_PROCESS_API, + ROCKET_QUERY_API, +}; use crate::model::ids::request::ClearingHouseMessage; -use crate::model::constants::{ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_QUERY_API, ROCKET_PROCESS_API, ROCKET_PK_API}; use crate::services::logging_service::LoggingService; #[rocket::post("/", format = "json", data = "")] @@ -21,7 +22,11 @@ async fn log( message: Json, pid: String, ) -> ApiResponse { - match logging_api.inner().log(ch_claims, key_path, message.into_inner(), pid).await { + match logging_api + .inner() + .log(ch_claims, key_path, message.into_inner(), pid) + .await + { Ok(id) => ApiResponse::SuccessCreate(json!(id)), Err(e) => { error!("Error while logging: {:?}", e); @@ -37,7 +42,11 @@ async fn create_process( message: Json, pid: String, ) -> ApiResponse { - match logging_api.inner().create_process(ch_claims, message.into_inner(), pid).await { + match logging_api + .inner() + .create_process(ch_claims, message.into_inner(), pid) + .await + { Ok(id) => ApiResponse::SuccessCreate(json!(id)), Err(e) => { error!("Error while creating process: {:?}", e); @@ -56,7 +65,11 @@ async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) } -#[rocket::post("/?&&&&", format = "json", data = "")] +#[rocket::post( + "/?&&&&", + format = "json", + data = "" +)] async fn query_pid( ch_claims: ChClaims, logging_api: &State, @@ -68,7 +81,20 @@ async fn query_pid( pid: String, message: Json, ) -> ApiResponse { - match logging_api.inner().query_pid(ch_claims, page, size, sort, date_to, date_from, pid, message.into_inner()).await { + match logging_api + .inner() + .query_pid( + ch_claims, + page, + size, + sort, + date_to, + date_from, + pid, + message.into_inner(), + ) + .await + { Ok(result) => ApiResponse::SuccessOk(json!(result)), Err(e) => { error!("Error while querying: {:?}", e); @@ -85,7 +111,11 @@ async fn query_id( id: String, message: Json, ) -> ApiResponse { - match logging_api.inner().query_id(ch_claims, pid, id, message.into_inner()).await { + match logging_api + .inner() + .query_id(ch_claims, pid, id, message.into_inner()) + .await + { Ok(result) => ApiResponse::SuccessOk(json!(result)), Err(e) => { error!("Error while querying: {:?}", e); @@ -98,17 +128,28 @@ async fn query_id( async fn get_public_sign_key(key_path: &State) -> ApiResponse { match get_jwks(key_path.as_str()) { Some(jwks) => ApiResponse::SuccessOk(json!(jwks)), - None => ApiResponse::InternalError(String::from("Error reading signing key")) + None => ApiResponse::InternalError(String::from("Error reading signing key")), } } pub fn mount_api() -> AdHoc { AdHoc::on_ignite("Mounting Clearing House API", |rocket| async { rocket - .mount(format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API).as_str(), rocket::routes![log, unauth]) - .mount(format!("{}", ROCKET_PROCESS_API).as_str(), rocket::routes![create_process, unauth]) - .mount(format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_QUERY_API).as_str(), - rocket::routes![query_id, query_pid, unauth, unauth_id]) - .mount(format!("{}", ROCKET_PK_API).as_str(), rocket::routes![get_public_sign_key]) + .mount( + format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API).as_str(), + rocket::routes![log, unauth], + ) + .mount( + format!("{}", ROCKET_PROCESS_API).as_str(), + rocket::routes![create_process, unauth], + ) + .mount( + format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_QUERY_API).as_str(), + rocket::routes![query_id, query_pid, unauth, unauth_id], + ) + .mount( + format!("{}", ROCKET_PK_API).as_str(), + rocket::routes![get_public_sign_key], + ) }) -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/logging-service/src/ports/mod.rs index 71bc119..04881a1 100644 --- a/clearing-house-app/logging-service/src/ports/mod.rs +++ b/clearing-house-app/logging-service/src/ports/mod.rs @@ -3,10 +3,8 @@ //! This module contains the ports of the logging service. Ports are used to communicate with other //! services. In this case, the logging service implements REST-API endpoints to provide access to //! the logging service. -pub(crate) mod logging_api; pub(crate) mod doc_type_api; - - +pub(crate) mod logging_api; #[derive(rocket::Responder, Debug)] pub enum ApiResponse { @@ -28,4 +26,4 @@ pub enum ApiResponse { NotFound(String), #[response(status = 500, content_type = "text/plain")] InternalError(String), -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index b2e2180..28facb8 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -1,14 +1,17 @@ -use std::convert::TryFrom; -use anyhow::anyhow; +use crate::db::doc_store::DataStore; use crate::model::claims::ChClaims; -use crate::services::{DocumentReceipt, QueryResult}; -use crate::model::constants::{DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART}; -use crate::model::document::Document; -use crate::model::{parse_date, sanitize_dates, SortingOrder, validate_dates}; +use crate::model::constants::{ + DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART, +}; use crate::model::crypto::{KeyCt, KeyCtList}; +use crate::model::document::Document; +use crate::model::{ + parse_date, validate_and_sanitize_dates, SortingOrder, +}; use crate::services::keyring_service::KeyringService; -use crate::db::doc_store::DataStore; - +use crate::services::{DocumentReceipt, QueryResult}; +use anyhow::anyhow; +use std::convert::TryFrom; #[derive(Clone)] pub struct DocumentService { @@ -21,12 +24,19 @@ impl DocumentService { Self { db, key_api } } - pub(crate) async fn create_enc_document(&self, ch_claims: ChClaims, doc: Document) -> anyhow::Result { + pub(crate) async fn create_enc_document( + &self, + ch_claims: ChClaims, + doc: Document, + ) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); // data validation - let payload: Vec = doc.parts.iter() + let payload: Vec = doc + .parts + .iter() .filter(|p| String::from(PAYLOAD_PART) == p.name) - .map(|p| p.content.as_ref().unwrap().clone()).collect(); + .map(|p| p.content.as_ref().unwrap().clone()) + .collect(); if payload.len() > 1 { return Err(anyhow!("Document contains two payloads!")); // BadRequest } else if payload.len() == 0 { @@ -42,19 +52,22 @@ impl DocumentService { _ => { debug!("Document does not exists!"); debug!("getting keys"); - let keys; // TODO: This needs some attention, because keyring api called `create_service_token` on `ch_claims` - match self.key_api.generate_keys(ch_claims, doc.pid.clone(), doc.dt_id.clone()).await { + let keys = match self + .key_api + .generate_keys(ch_claims, doc.pid.clone(), doc.dt_id.clone()) + .await + { Ok(key_map) => { - keys = key_map; debug!("got keys"); + Ok(key_map) } Err(e) => { error!("Error while retrieving keys: {:?}", e); - return Err(anyhow!("Error while retrieving keys!")); // InternalError + Err(anyhow!("Error while retrieving keys!")) // InternalError } - }; + }?; debug!("start encryption"); let mut enc_doc = match doc.encrypt(keys) { @@ -80,7 +93,8 @@ impl DocumentService { info!("No entries found for pid {}. Beginning new chain!", doc.pid); } else { // If this happens, db didn't find a tc entry that should exist. - return Err(anyhow!("Error while creating the chain hash!")); // InternalError + return Err(anyhow!("Error while creating the chain hash!")); + // InternalError } } Err(e) => { @@ -91,8 +105,8 @@ impl DocumentService { // prepare the success result message - - let receipt = DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id, &enc_doc.hash); + let receipt = + DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id, &enc_doc.hash); debug!("storing document ...."); // store document @@ -107,47 +121,50 @@ impl DocumentService { } } - pub(crate) async fn get_enc_documents_for_pid(&self, - ch_claims: ChClaims, - doc_type: Option, - page: Option, // TODO: Why i32? This should be and unsinged int - size: Option, // TODO: Why i32? This should be and unsinged int + pub(crate) async fn get_enc_documents_for_pid( + &self, + ch_claims: ChClaims, + doc_type: Option, + page: Option, // TODO: Why i32? This should be and unsigned int + size: Option, // TODO: Why i32? This should be and unsigned int sort: Option, - date_from: Option, - date_to: Option, - pid: String) -> anyhow::Result { + date_from: Option, + date_to: Option, + pid: String, + ) -> anyhow::Result { debug!("Trying to retrieve documents for pid '{}'...", &pid); trace!("...user '{:?}'", &ch_claims.client_id); - debug!("...page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); + debug!( + "...page: {:#?}, size:{:#?} and sort:{:#?}", + page, size, sort + ); // Parameter validation for pagination: // Valid pages start from 1 // Max page number as of yet unknown - let sanitized_page = match page{ + let sanitized_page = match page { Some(p) => { - if p > 0{ + if p > 0 { u64::try_from(p).unwrap() - } - else{ + } else { warn!("...invalid page requested. Falling back to 1."); 1 } - }, - None => 1 + } + None => 1, }; // Valid sizes are between 1 and MAX_NUM_RESPONSE_ENTRIES (1000) - let sanitized_size = match size{ + let sanitized_size = match size { Some(s) => { if s > 0 && s <= i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap() { u64::try_from(s).unwrap() - } - else{ + } else { warn!("...invalid size requested. Falling back to default."); DEFAULT_NUM_RESPONSE_ENTRIES } - }, - None => DEFAULT_NUM_RESPONSE_ENTRIES + } + None => DEFAULT_NUM_RESPONSE_ENTRIES, }; // Sorting order is already validated and defaults to descending @@ -158,75 +175,106 @@ impl DocumentService { let parsed_date_to = parse_date(date_to, true); // Validation of dates with various checks. If none given dates default to date_now (date_to) and (date_now - 2 weeks) (date_from) - if !validate_dates(parsed_date_from, parsed_date_to){ + let Ok((sanitized_date_from, sanitized_date_to)) = validate_and_sanitize_dates(parsed_date_from, parsed_date_to, None) else { debug!("date validation failed!"); return Err(anyhow!("Invalid date parameter!")); // BadRequest - } - let (sanitized_date_from, sanitized_date_to) = sanitize_dates(parsed_date_from, parsed_date_to); + }; //new behavior: if pages are "invalid" return {}. Do not adjust page //either call db with type filter or without to get cts let start = chrono::Local::now(); - debug!("... using pagination with page: {}, size:{} and sort:{:#?}", sanitized_page, sanitized_size, &sanitized_sort); + debug!( + "... using pagination with page: {}, size:{} and sort:{:#?}", + sanitized_page, sanitized_size, &sanitized_sort + ); - let dt_id = match doc_type{ + let dt_id = match doc_type { Some(dt) => dt, None => String::from(DEFAULT_DOC_TYPE), }; - let cts = match self.db.get_documents_for_pid(&dt_id, &pid, sanitized_page, sanitized_size, &sanitized_sort, &sanitized_date_from, &sanitized_date_to).await{ + let cts = match self + .db + .get_documents_for_pid( + &dt_id, + &pid, + sanitized_page, + sanitized_size, + &sanitized_sort, + &sanitized_date_from, + &sanitized_date_to, + ) + .await + { Ok(cts) => cts, Err(e) => { error!("Error while retrieving document: {:?}", e); - return Err(anyhow!("Error while retrieving document for {}", &pid)) - }, + return Err(anyhow!("Error while retrieving document for {}", &pid)); + } }; let result_size = i32::try_from(sanitized_size).ok(); let result_page = i32::try_from(sanitized_page).ok(); - let result_sort = match sanitized_sort{ + let result_sort = match sanitized_sort { SortingOrder::Ascending => String::from("asc"), SortingOrder::Descending => String::from("desc"), }; - let mut result = QueryResult::new(sanitized_date_from.timestamp(), sanitized_date_to.timestamp(), result_page, result_size, result_sort, vec!()); + let mut result = QueryResult::new( + sanitized_date_from.timestamp(), + sanitized_date_to.timestamp(), + result_page, + result_size, + result_sort, + vec![], + ); // The db might contain no documents in which case we get an empty vector - if cts.is_empty(){ + if cts.is_empty() { debug!("Queried empty pid: {}", &pid); Ok(result) - } - else{ + } else { // Documents found for pid, now decrypting them - debug!("Found {} documents. Getting keys from keyring...", cts.len()); - let key_cts: Vec = cts.iter() - .map(|e| KeyCt::new(e.id.clone(), e.keys_ct.clone())).collect(); + debug!( + "Found {} documents. Getting keys from keyring...", + cts.len() + ); + let key_cts: Vec = cts + .iter() + .map(|e| KeyCt::new(e.id.clone(), e.keys_ct.clone())) + .collect(); // caution! we currently only support a single dt per call, so we use the first dt we found let key_cts_list = KeyCtList::new(cts[0].dt_id.clone(), key_cts); // decrypt cts // TODO: This method needs some attention, because keyring api called `create_service_token` on `ch_claims` - let key_maps = match self.key_api.decrypt_multiple_keys(ch_claims, Some(pid),&key_cts_list).await{ - Ok(key_map) => { - key_map - } + let key_maps = match self + .key_api + .decrypt_multiple_keys(ch_claims, Some(pid), &key_cts_list) + .await + { + Ok(key_map) => key_map, Err(e) => { error!("Error while retrieving keys from keyring: {:?}", e); - return Err(anyhow!("Error while retrieving keys from keyring")); // InternalError + return Err(anyhow!("Error while retrieving keys from keyring")); + // InternalError } }; debug!("... keys received. Starting decryption..."); - let pts_bulk : Vec = cts.iter().zip(key_maps.iter()) - .filter_map(|(ct,key_map)|{ - if ct.id != key_map.id{ + let pts_bulk: Vec = cts + .iter() + .zip(key_maps.iter()) + .filter_map(|(ct, key_map)| { + if ct.id != key_map.id { error!("Document and map don't match"); }; - match ct.decrypt(key_map.map.keys.clone()){ + match ct.decrypt(key_map.map.keys.clone()) { Ok(d) => Some(d), Err(e) => { warn!("Got empty document from decryption! {:?}", e); None } } - }).collect(); + }) + .collect(); debug!("...done."); let end = chrono::Local::now(); let diff = end - start; @@ -236,51 +284,71 @@ impl DocumentService { } } - pub(crate) async fn get_enc_document(&self, ch_claims: ChClaims, pid: String, id: String, hash: Option) -> anyhow::Result { + pub(crate) async fn get_enc_document( + &self, + ch_claims: ChClaims, + pid: String, + id: String, + hash: Option, + ) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); - trace!("trying to retrieve document with id '{}' for pid '{}'", &id, &pid); - if hash.is_some(){ + trace!( + "trying to retrieve document with id '{}' for pid '{}'", + &id, + &pid + ); + if hash.is_some() { debug!("integrity check with hash: {}", hash.as_ref().unwrap()); } - match self.db.get_document(&id, &pid).await{ + match self.db.get_document(&id, &pid).await { //TODO: would like to send "{}" instead of "null" when dt is not found Ok(Some(ct)) => { - match hex::decode(&ct.keys_ct){ + match hex::decode(&ct.keys_ct) { Ok(key_ct) => { // TODO: This method needs some attention, because keyring api called `create_service_token` on `ch_claims` - match self.key_api.decrypt_key_map(ch_claims, hex::encode_upper(key_ct), Some(pid), ct.dt_id.clone()).await{ + match self + .key_api + .decrypt_key_map( + ch_claims, + hex::encode_upper(key_ct), + Some(pid), + ct.dt_id.clone(), + ) + .await + { Ok(key_map) => { //TODO check the hash - match ct.decrypt(key_map.keys){ + match ct.decrypt(key_map.keys) { Ok(d) => Ok(d), Err(e) => { warn!("Got empty document from decryption! {:?}", e); - return Err(anyhow!("Document {} not found!", &id)); // NotFound + return Err(anyhow!("Document {} not found!", &id)); + // NotFound } } } Err(e) => { error!("Error while retrieving keys from keyring: {:?}", e); - return Err(anyhow!("Error while retrieving keys")) // InternalError + return Err(anyhow!("Error while retrieving keys")); + // InternalError } } - - }, + } Err(e) => { error!("Error while decoding ciphertext: {:?}", e); - return Err(anyhow!("Key Ciphertext corrupted")) // InternalError + return Err(anyhow!("Key Ciphertext corrupted")); // InternalError } } - }, + } Ok(None) => { debug!("Nothing found in db!"); - return Err(anyhow!("Document {} not found!", &id)) // NotFound + return Err(anyhow!("Document {} not found!", &id)); // NotFound } Err(e) => { error!("Error while retrieving document: {:?}", e); - return Err(anyhow!("Error while retrieving document {}", &id)) // InternalError + return Err(anyhow!("Error while retrieving document {}", &id)); // InternalError } } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index 6fb2456..8956fec 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -1,10 +1,10 @@ -use anyhow::anyhow; -use crate::model::claims::ChClaims; -use crate::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::crypto; use crate::crypto::restore_key_map; use crate::db::key_store::KeyStore; +use crate::model::claims::ChClaims; +use crate::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::model::doc_type::DocumentType; +use anyhow::anyhow; #[derive(Clone)] pub struct KeyringService { @@ -13,12 +13,15 @@ pub struct KeyringService { impl KeyringService { pub fn new(db: KeyStore) -> KeyringService { - KeyringService { - db - } + KeyringService { db } } - pub async fn generate_keys(&self, ch_claims: ChClaims, _pid: String, dt_id: String) -> anyhow::Result { + pub async fn generate_keys( + &self, + ch_claims: ChClaims, + _pid: String, + dt_id: String, + ) -> anyhow::Result { trace!("generate_keys"); trace!("...user '{:?}'", &ch_claims.client_id); match self.db.get_msk().await { @@ -34,7 +37,8 @@ impl KeyringService { } Err(e) => { error!("Error while generating key map: {}", e); - return Err(anyhow!("Error while generating keys")); // InternalError + return Err(anyhow!("Error while generating keys")); + // InternalError } } } @@ -44,7 +48,8 @@ impl KeyringService { } Err(e) => { warn!("Error while retrieving document type: {}", e); - return Err(anyhow!("Error while retrieving document type")); // InternalError + return Err(anyhow!("Error while retrieving document type")); + // InternalError } } } @@ -55,7 +60,12 @@ impl KeyringService { } } - pub(crate) async fn decrypt_keys(&self, ch_claims: ChClaims, _pid: Option, key_cts: &KeyCtList) -> anyhow::Result> { + pub(crate) async fn decrypt_keys( + &self, + ch_claims: ChClaims, + _pid: Option, + key_cts: &KeyCtList, + ) -> anyhow::Result> { trace!("decrypt_keys"); trace!("...user '{:?}'", &ch_claims.client_id); debug!("number of cts to decrypt: {}", &key_cts.cts.len()); @@ -69,31 +79,27 @@ impl KeyringService { let mut dec_error_count = 0; let mut map_error_count = 0; // validate keys_ct input - let key_maps: Vec = key_cts.cts.iter().filter_map( - |key_ct| { - match hex::decode(key_ct.ct.clone()) { - Ok(key) => Some((key_ct.id.clone(), key)), - Err(e) => { - error!("Error while decoding key ciphertext: {}", e); - dec_error_count = dec_error_count + 1; - None - } + let key_maps: Vec = key_cts + .cts + .iter() + .filter_map(|key_ct| match hex::decode(key_ct.ct.clone()) { + Ok(key) => Some((key_ct.id.clone(), key)), + Err(e) => { + error!("Error while decoding key ciphertext: {}", e); + dec_error_count = dec_error_count + 1; + None } - } - ).filter_map( - |(id, key)| { + }) + .filter_map(|(id, key)| { match restore_key_map(m_key.clone(), dt.clone(), key) { - Ok(key_map) => { - Some(KeyMapListItem::new(id, key_map)) - } + Ok(key_map) => Some(KeyMapListItem::new(id, key_map)), Err(e) => { error!("Error while generating key map: {}", e); map_error_count = map_error_count + 1; None } } - } - ) + }) .collect(); let error_count = map_error_count + dec_error_count; @@ -122,7 +128,13 @@ impl KeyringService { } } - pub async fn decrypt_key_map(&self, ch_claims: ChClaims, keys_ct: String, _pid: Option, dt_id: String) -> anyhow::Result { + pub async fn decrypt_key_map( + &self, + ch_claims: ChClaims, + keys_ct: String, + _pid: Option, + dt_id: String, + ) -> anyhow::Result { trace!("decrypt_key_map"); trace!("...user '{:?}'", &ch_claims.client_id); trace!("ct: {}", &keys_ct); @@ -137,17 +149,19 @@ impl KeyringService { Ok(key) => key, Err(e) => { error!("Error while decoding key ciphertext: {}", e); - return Err(anyhow!("Error while decrypting keys")); // InternalError + return Err(anyhow!("Error while decrypting keys")); + // InternalError } }; match restore_key_map(key, dt, keys_ct) { Ok(key_map) => { return Ok(key_map); - }, + } Err(e) => { error!("Error while generating key map: {}", e); - return Err(anyhow!("Error while restoring keys")); // InternalError + return Err(anyhow!("Error while restoring keys")); + // InternalError } } } @@ -168,67 +182,100 @@ impl KeyringService { } } - pub(crate) async fn decrypt_multiple_keys(&self, ch_claims: ChClaims, pid: Option, cts: &KeyCtList) -> anyhow::Result> { + pub(crate) async fn decrypt_multiple_keys( + &self, + ch_claims: ChClaims, + pid: Option, + cts: &KeyCtList, + ) -> anyhow::Result> { self.decrypt_keys(ch_claims, pid, cts).await } - pub(crate) async fn create_doc_type(&self, doc_type: DocumentType) -> anyhow::Result { + pub(crate) async fn create_doc_type( + &self, + doc_type: DocumentType, + ) -> anyhow::Result { debug!("adding doctype: {:?}", &doc_type); - match self.db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + match self + .db + .exists_document_type(&doc_type.pid, &doc_type.id) + .await + { Ok(true) => Err(anyhow!("doctype already exists!")), // BadRequest Ok(false) => { - match self.db.add_document_type(doc_type.clone()).await{ + match self.db.add_document_type(doc_type.clone()).await { Ok(()) => Ok(doc_type), Err(e) => { error!("Error while adding doctype: {:?}", e); - return Err(anyhow!("Error while adding document type!")) // InternalError + return Err(anyhow!("Error while adding document type!")); + // InternalError } } - }, + } Err(e) => { error!("Error while adding document type: {:?}", e); - return Err(anyhow!("Error while checking database!")) // InternalError + return Err(anyhow!("Error while checking database!")); // InternalError } } } - pub(crate) async fn update_doc_type(&self, id: String, doc_type: DocumentType) -> anyhow::Result { - match self.db.exists_document_type(&doc_type.pid, &doc_type.id).await{ + pub(crate) async fn update_doc_type( + &self, + id: String, + doc_type: DocumentType, + ) -> anyhow::Result { + match self + .db + .exists_document_type(&doc_type.pid, &doc_type.id) + .await + { Ok(true) => Err(anyhow!("Doctype already exists!")), // BadRequest Ok(false) => { - match self.db.update_document_type(doc_type, &id).await{ + match self.db.update_document_type(doc_type, &id).await { Ok(id) => Ok(id), Err(e) => { error!("Error while adding doctype: {:?}", e); - return Err(anyhow!("Error while storing document type!")) // InternalError + return Err(anyhow!("Error while storing document type!")); + // InternalError } } - }, + } Err(e) => { error!("Error while adding document type: {:?}", e); - return Err(anyhow!("Error while checking database!")) // InternalError + return Err(anyhow!("Error while checking database!")); // InternalError } } } - pub(crate) async fn delete_doc_type(&self, id: String, pid: String) -> anyhow::Result{ - match self.db.delete_document_type(&id, &pid).await{ + pub(crate) async fn delete_doc_type(&self, id: String, pid: String) -> anyhow::Result { + match self.db.delete_document_type(&id, &pid).await { Ok(true) => Ok(String::from("Document type deleted!")), // NoContent Ok(false) => Err(anyhow!("Document type does not exist!")), // NotFound Err(e) => { error!("Error while deleting doctype: {:?}", e); - Err(anyhow!("Error while deleting document type with id {}!", id)) // InternalError + Err(anyhow!( + "Error while deleting document type with id {}!", + id + )) // InternalError } } } - pub(crate) async fn get_doc_type(&self, id: String, pid: String) -> anyhow::Result> { - match self.db.get_document_type(&id).await{ + pub(crate) async fn get_doc_type( + &self, + id: String, + pid: String, + ) -> anyhow::Result> { + match self.db.get_document_type(&id).await { //TODO: would like to send "{}" instead of "null" when dt is not found Ok(dt) => Ok(dt), Err(e) => { error!("Error while retrieving doctype: {:?}", e); - Err(anyhow!("Error while retrieving document type with id {} and pid {}!", id, pid)) // InternalError + Err(anyhow!( + "Error while retrieving document type with id {} and pid {}!", + id, + pid + )) // InternalError } } } @@ -243,4 +290,4 @@ impl KeyringService { } } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/logging-service/src/services/logging_service.rs index d97fb63..b536c48 100644 --- a/clearing-house-app/logging-service/src/services/logging_service.rs +++ b/clearing-house-app/logging-service/src/services/logging_service.rs @@ -1,23 +1,18 @@ use crate::model::{ claims::ChClaims, - constants::{DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, DEFAULT_PROCESS_ID}, - { - document::Document, - process::Process, - SortingOrder, - }, + constants::{DEFAULT_NUM_RESPONSE_ENTRIES, DEFAULT_PROCESS_ID, MAX_NUM_RESPONSE_ENTRIES}, + {document::Document, process::Process, SortingOrder}, }; +use anyhow::anyhow; use rocket::form::validate::Contains; use rocket::State; use std::convert::TryFrom; -use anyhow::anyhow; -use crate::model::{ids::{ - message::IdsMessage, - IdsQueryResult, - request::ClearingHouseMessage, -}, process::{OwnerList, DataTransaction, Receipt}}; use crate::db::process_store::ProcessStore; +use crate::model::{ + ids::{message::IdsMessage, request::ClearingHouseMessage, IdsQueryResult}, + process::{DataTransaction, OwnerList, Receipt}, +}; use crate::services::document_service::DocumentService; #[derive(Clone)] @@ -28,10 +23,7 @@ pub struct LoggingService { impl LoggingService { pub fn new(db: ProcessStore, doc_api: DocumentService) -> LoggingService { - LoggingService { - db, - doc_api, - } + LoggingService { db, doc_api } } pub async fn log( @@ -50,7 +42,9 @@ impl LoggingService { m.pid = Some(pid.clone()); // validate that there is a payload - if m.payload.is_none() || (m.payload.is_some() && m.payload.as_ref().unwrap().trim().is_empty()) { + if m.payload.is_none() + || (m.payload.is_some() && m.payload.as_ref().unwrap().trim().is_empty()) + { error!("Trying to log an empty payload!"); return Err(anyhow!("No payload received for logging!")); // BadRequest } @@ -70,11 +64,12 @@ impl LoggingService { Ok(None) => { info!("Requested pid '{}' does not exist. Creating...", &pid); // create a new process - let new_process = Process::new(pid.clone(), vec!(user.clone())); + let new_process = Process::new(pid.clone(), vec![user.clone()]); if self.db.store_process(new_process).await.is_err() { error!("Error while creating process '{}'", &pid); - return Err(anyhow!("Error while creating process")); // InternalError + return Err(anyhow!("Error while creating process")); + // InternalError } } Err(_) => { @@ -92,13 +87,17 @@ impl LoggingService { return Err(anyhow!("User not authorized!")); // Forbidden } Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + error!( + "Error while checking authorization of user '{}' for '{}'", + &user, &pid + ); return Err(anyhow!("Error during authorization")); } } debug!("logging message for pid {}", &pid); - self.log_message(user, key_path.inner().as_str(), m.clone()).await + self.log_message(user, key_path.inner().as_str(), m.clone()) + .await } } } @@ -117,7 +116,7 @@ impl LoggingService { let user = &ch_claims.client_id; // validate payload - let mut owners = vec!(user.clone()); + let mut owners = vec![user.clone()]; let payload = m.payload.clone().unwrap_or(String::new()); if !payload.is_empty() { trace!("OwnerList: '{:#?}'", &payload); @@ -161,9 +160,7 @@ impl LoggingService { let new_process = Process::new(pid.clone(), owners); match self.db.store_process(new_process).await { - Ok(_) => { - Ok(pid.clone()) - } + Ok(_) => Ok(pid.clone()), Err(e) => { error!("Error while creating process '{}': {}", &pid, e); Err(anyhow!("Error while creating process")) // InternalError @@ -190,7 +187,11 @@ impl LoggingService { debug!("Storing document..."); doc.tc = tid; // TODO: ChClaims usage check - match self.doc_api.create_enc_document(ChClaims::new(&user), doc.clone()).await { + match self + .doc_api + .create_enc_document(ChClaims::new(&user), doc.clone()) + .await + { Ok(doc_receipt) => { debug!("Increase transaction counter"); match self.db.increment_transaction_counter().await { @@ -211,7 +212,8 @@ impl LoggingService { } _ => { error!("Error while incrementing transaction id!"); - Err(anyhow!("Internal error while preparing transaction data")) // InternalError + Err(anyhow!("Internal error while preparing transaction data")) + // InternalError } } } @@ -254,7 +256,10 @@ impl LoggingService { Ok(true) => info!("User authorized."), Ok(false) => return Err(anyhow!("Process does not exist!")), // NotFound Err(_e) => { - error!("Error while checking process '{}' for user '{}'", &pid, &user); + error!( + "Error while checking process '{}' for user '{}'", + &pid, &user + ); return Err(anyhow!("Cannot authorize user!")); // InternalError } }; @@ -269,7 +274,10 @@ impl LoggingService { return Err(anyhow!("User not authorized!")); // Forbidden } Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + error!( + "Error while checking authorization of user '{}' for '{}'", + &user, &pid + ); return Err(anyhow!("Cannot authorize user!")); // InternalError } } @@ -284,7 +292,7 @@ impl LoggingService { 1 } } - None => 1 + None => 1, }; let sanitized_size = match size { @@ -302,29 +310,49 @@ impl LoggingService { } } } - None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() + None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap(), }; let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); - match self.doc_api.get_enc_documents_for_pid(ChClaims::new(&user), None, Some(sanitized_page), Some(sanitized_size), Some(sanitized_sort), date_from, date_to, pid.clone()).await { + match self + .doc_api + .get_enc_documents_for_pid( + ChClaims::new(&user), + None, + Some(sanitized_page), + Some(sanitized_size), + Some(sanitized_sort), + date_from, + date_to, + pid.clone(), + ) + .await + { Ok(r) => { - let messages: Vec = r.documents.iter().map(|d| IdsMessage::from(d.clone())).collect(); - let result = IdsQueryResult::new(r.date_from, r.date_to, r.page, r.size, r.order, messages); + let messages: Vec = r + .documents + .iter() + .map(|d| IdsMessage::from(d.clone())) + .collect(); + let result = + IdsQueryResult::new(r.date_from, r.date_to, r.page, r.size, r.order, messages); Ok(result) } Err(e) => { error!("Error while retrieving message: {:?}", e); - Err(anyhow!("Error while retrieving messages for pid {}!", &pid)) // InternalError + Err(anyhow!("Error while retrieving messages for pid {}!", &pid)) + // InternalError } } } - pub(crate) async fn query_id(&self, - ch_claims: ChClaims, - pid: String, - id: String, - message: ClearingHouseMessage, + pub(crate) async fn query_id( + &self, + ch_claims: ChClaims, + pid: String, + id: String, + message: ClearingHouseMessage, ) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); let user = &ch_claims.client_id; @@ -334,7 +362,10 @@ impl LoggingService { Ok(true) => info!("User authorized."), Ok(false) => return Err(anyhow!("Process does not exist!")), // NotFound Err(_e) => { - error!("Error while checking process '{}' for user '{}'", &pid, &user); + error!( + "Error while checking process '{}' for user '{}'", + &pid, &user + ); return Err(anyhow!("Cannot authorize user!")); // InternalError } }; @@ -349,12 +380,19 @@ impl LoggingService { return Err(anyhow!("User not authorized!")); // Forbidden } Err(_) => { - error!("Error while checking authorization of user '{}' for '{}'", &user, &pid); + error!( + "Error while checking authorization of user '{}' for '{}'", + &user, &pid + ); return Err(anyhow!("Cannot authorize user!")); // InternalError } } - match self.doc_api.get_enc_document(ChClaims::new(&user), pid.clone(), id.clone(), None).await { + match self + .doc_api + .get_enc_document(ChClaims::new(&user), pid.clone(), id.clone(), None) + .await + { Ok(doc) => { // transform document to IDS message let queried_message = IdsMessage::from(doc); @@ -370,4 +408,4 @@ impl LoggingService { } } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/logging-service/src/services/mod.rs index e304b9c..a5f1e20 100644 --- a/clearing-house-app/logging-service/src/services/mod.rs +++ b/clearing-house-app/logging-service/src/services/mod.rs @@ -6,22 +6,21 @@ //! use crate::model::document::Document; -pub(crate) mod keyring_service; pub(crate) mod document_service; +pub(crate) mod keyring_service; pub(crate) mod logging_service; - #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct DocumentReceipt{ +pub struct DocumentReceipt { pub timestamp: i64, pub pid: String, pub doc_id: String, pub chain_hash: String, } -impl DocumentReceipt{ - pub fn new(timestamp: i64, pid: &str, doc_id: &str, chain_hash: &str) -> DocumentReceipt{ - DocumentReceipt{ +impl DocumentReceipt { + pub fn new(timestamp: i64, pid: &str, doc_id: &str, chain_hash: &str) -> DocumentReceipt { + DocumentReceipt { timestamp, pid: pid.to_string(), doc_id: doc_id.to_string(), @@ -31,24 +30,31 @@ impl DocumentReceipt{ } #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct QueryResult{ +pub struct QueryResult { pub date_from: i64, pub date_to: i64, pub page: Option, pub size: Option, pub order: String, - pub documents: Vec + pub documents: Vec, } -impl QueryResult{ - pub fn new(date_from: i64, date_to: i64, page: Option, size: Option, order: String, documents: Vec) -> QueryResult{ - QueryResult{ +impl QueryResult { + pub fn new( + date_from: i64, + date_to: i64, + page: Option, + size: Option, + order: String, + documents: Vec, + ) -> QueryResult { + QueryResult { date_from, date_to, page, size, order, - documents + documents, } } -} \ No newline at end of file +} diff --git a/clearing-house-app/logging-service/src/util.rs b/clearing-house-app/logging-service/src/util.rs index 94e3183..9a95e19 100644 --- a/clearing-house-app/logging-service/src/util.rs +++ b/clearing-house-app/logging-service/src/util.rs @@ -1,26 +1,45 @@ +use crate::model::constants::SIGNING_KEY; use crate::model::errors::errors; +use std::path::Path; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct ServiceConfig{ - pub service_id: String +pub struct ServiceConfig { + pub service_id: String, } pub fn add_service_config(service_id: String) -> rocket::fairing::AdHoc { rocket::fairing::AdHoc::try_on_ignite("Adding Service Config", move |rocket| async move { - match std::env::var(&service_id){ - Ok(id) => { - Ok(rocket.manage(ServiceConfig {service_id: id})) - }, + match std::env::var(&service_id) { + Ok(id) => Ok(rocket.manage(ServiceConfig { service_id: id })), Err(_e) => { - error!("Service ID not configured. Please configure environment variable {}", &service_id); - return Err(rocket) + error!( + "Service ID not configured. Please configure environment variable {}", + &service_id + ); + Err(rocket) } } }) } +pub fn add_signing_key() -> rocket::fairing::AdHoc { + rocket::fairing::AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { + let private_key_path = rocket + .figment() + .extract_inner(SIGNING_KEY) + .unwrap_or(String::from("keys/private_key.der")); + if Path::new(&private_key_path).exists() { + Ok(rocket.manage(private_key_path)) + } else { + tracing::error!( + "Signing key not found! Aborting startup! Please configure signing_key!" + ); + Err(rocket) + } + }) +} + /// Reads a file into a string pub fn read_file(file: &str) -> errors::Result { - std::fs::read_to_string(file) - .map_err(|e| errors::Error::from(e)) -} \ No newline at end of file + std::fs::read_to_string(file).map_err(errors::Error::from) +} From 9d05fb31072fdce71a7cabd6b37566de93d59884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 29 Aug 2023 11:01:27 +0200 Subject: [PATCH 042/183] refactor(ch-app): Fix clippy warnings --- .../logging-service/src/crypto.rs | 14 +- .../logging-service/src/db/doc_store.rs | 16 +- .../logging-service/src/db/key_store.rs | 5 +- .../logging-service/src/db/process_store.rs | 4 +- .../logging-service/src/model/claims.rs | 75 +++++----- .../logging-service/src/model/constants.rs | 140 +++++++++--------- .../logging-service/src/model/document.rs | 7 +- .../logging-service/src/model/ids/message.rs | 98 ++++++------ .../logging-service/src/model/ids/mod.rs | 31 ++-- .../logging-service/src/model/process.rs | 4 +- .../logging-service/src/model/util.rs | 1 + .../src/services/document_service.rs | 16 +- .../src/services/keyring_service.rs | 36 ++--- .../src/services/logging_service.rs | 10 +- 14 files changed, 216 insertions(+), 241 deletions(-) diff --git a/clearing-house-app/logging-service/src/crypto.rs b/clearing-house-app/logging-service/src/crypto.rs index 329c40d..fed00c9 100644 --- a/clearing-house-app/logging-service/src/crypto.rs +++ b/clearing-house-app/logging-service/src/crypto.rs @@ -42,9 +42,9 @@ pub fn generate_random_seed() -> Vec { fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap { let mut key_map = HashMap::new(); - let mut okm = [0u8; EXP_BUFF_SIZE]; let mut i = 0; dt.parts.iter().for_each(|p| { + let mut okm = [0u8; EXP_BUFF_SIZE]; if kdf.expand(p.name.clone().as_bytes(), &mut okm).is_ok() { let map_key = match enc { true => p.name.clone(), @@ -59,7 +59,7 @@ fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap anyhow::Result fn restore_kdf(secret: &String) -> anyhow::Result> { debug!("restoring kdf from secret"); - let prk = match hex::decode(secret) { - Ok(key) => key, - Err(e) => { + let prk = hex::decode(secret) + .map_err(|e| { error!("Error while decoding master key: {}", e); - return Err(anyhow!("Error while encrypting key seed!")); - } - }; + anyhow!("Error while encrypting key seed!") + })?; match Hkdf::::from_prk(prk.as_slice()) { Ok(kdf) => Ok(kdf), diff --git a/clearing-house-app/logging-service/src/db/doc_store.rs b/clearing-house-app/logging-service/src/db/doc_store.rs index 35f426f..27cd396 100644 --- a/clearing-house-app/logging-service/src/db/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/doc_store.rs @@ -232,10 +232,10 @@ impl DataStore { let mut docs = vec![]; while let Some(result) = results.next().await { let doc: DocumentBucketUpdate = bson::from_document(result?)?; - docs.push(restore_from_bucket(pid, dt_id, doc)); + docs.push(restore_from_bucket(pid.clone(), dt_id.clone(), doc)); } - return Ok(docs); + Ok(docs) } Err(e) => { error!("Error while getting bucket offset!"); @@ -319,7 +319,7 @@ impl DataStore { ) -> u64 { let docs_to_skip = (page - 1) * size + offset + MAX_NUM_RESPONSE_ENTRIES - bucket_size.capacity as u64; - return (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1; + (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1 } fn get_start_entry( @@ -338,7 +338,7 @@ impl DataStore { start_entry = start_entry - (start_bucket - 2) * MAX_NUM_RESPONSE_ENTRIES } } - return start_entry; + start_entry } } @@ -385,14 +385,14 @@ mod bucket { } pub fn restore_from_bucket( - pid: &String, - dt_id: &String, + pid: String, + dt_id: String, bucket_update: DocumentBucketUpdate, ) -> EncryptedDocument { EncryptedDocument { id: bucket_update.id.clone(), - dt_id: dt_id.clone(), - pid: pid.clone(), + dt_id: dt_id, + pid: pid, ts: bucket_update.ts, tc: bucket_update.tc, hash: bucket_update.hash.clone(), diff --git a/clearing-house-app/logging-service/src/db/key_store.rs b/clearing-house-app/logging-service/src/db/key_store.rs index 80f603d..3ebd8b8 100644 --- a/clearing-house-app/logging-service/src/db/key_store.rs +++ b/clearing-house-app/logging-service/src/db/key_store.rs @@ -6,7 +6,6 @@ use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; use crate::model::errors::*; use mongodb::bson::doc; -use mongodb::Client; use rocket::futures::TryStreamExt; use std::process::exit; @@ -17,7 +16,7 @@ pub struct KeyStore { } impl DataStoreApi for KeyStore { - fn new(client: Client) -> KeyStore { + fn new(client: mongodb::Client) -> KeyStore { KeyStore { client: client.clone(), database: client.database(KEYRING_DB), @@ -144,7 +143,7 @@ impl KeyStore { .await? .try_collect() .await - .unwrap_or_else(|_| vec![]); + .unwrap_or_default(); Ok(result) } diff --git a/clearing-house-app/logging-service/src/db/process_store.rs b/clearing-house-app/logging-service/src/db/process_store.rs index 8e8f185..23997b6 100644 --- a/clearing-house-app/logging-service/src/db/process_store.rs +++ b/clearing-house-app/logging-service/src/db/process_store.rs @@ -110,7 +110,7 @@ impl ProcessStore { "checking if user '{}' is authorized to access '{}'", user, pid ); - return match self.get_process(&pid).await { + match self.get_process(&pid).await { Ok(Some(process)) => { let authorized = process.owners.iter().any(|o| { trace!("found owner {}", o); @@ -123,7 +123,7 @@ impl ProcessStore { Ok(false) } _ => Err(format!("User '{}' could not be authorized", &user).into()), - }; + } } // store process in db diff --git a/clearing-house-app/logging-service/src/model/claims.rs b/clearing-house-app/logging-service/src/model/claims.rs index bd52dc9..7c4a4f8 100644 --- a/clearing-house-app/logging-service/src/model/claims.rs +++ b/clearing-house-app/logging-service/src/model/claims.rs @@ -1,14 +1,6 @@ use crate::model::constants::{ENV_SHARED_SECRET, SERVICE_HEADER}; use crate::model::errors::*; use crate::util::ServiceConfig; -use biscuit::jwk::{AlgorithmParameters, CommonParameters, JWKSet}; -use biscuit::Presence::Required; -use biscuit::Validation::Validate; -use biscuit::{ - jwa::SignatureAlgorithm, ClaimPresenceOptions, ClaimsSet, Empty, RegisteredClaims, - SingleOrMultiple, Timestamp, ValidationOptions, JWT, -}; -use biscuit::{jws, jws::Secret}; use chrono::{Duration, Utc}; use num_bigint::BigUint; use ring::signature::KeyPair; @@ -64,7 +56,7 @@ impl<'r> FromRequest<'r> for ChClaims { } } -pub fn get_jwks(key_path: &str) -> Option> { +pub fn get_jwks(key_path: &str) -> Option> { let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { @@ -87,16 +79,16 @@ pub fn get_jwks(key_path: &str) -> Option> { ..Default::default() }; - let mut common = CommonParameters::default(); + let mut common = biscuit::jwk::CommonParameters::default(); common.key_id = get_fingerprint(key_path); - let jwk = biscuit::jwk::JWK:: { + let jwk = biscuit::jwk::JWK:: { common, - algorithm: AlgorithmParameters::RSA(params), - additional: Empty::default(), + algorithm: biscuit::jwk::AlgorithmParameters::RSA(params), + additional: biscuit::Empty::default(), }; - let jwks = biscuit::jwk::JWKSet:: { keys: vec![jwk] }; + let jwks = biscuit::jwk::JWKSet:: { keys: vec![jwk] }; return Some(jwks); } None @@ -135,7 +127,7 @@ pub fn create_token serde::Dese private_claims: &T, ) -> String { let signing_secret = match env::var(ENV_SHARED_SECRET) { - Ok(secret) => Secret::Bytes(secret.to_string().into_bytes()), + Ok(secret) => biscuit::jws::Secret::Bytes(secret.to_string().into_bytes()), Err(_) => { panic!( "Shared Secret not configured. Please configure environment variable {}", @@ -145,24 +137,24 @@ pub fn create_token serde::Dese }; let expiration_date = Utc::now() + Duration::minutes(5); - let claims = ClaimsSet:: { - registered: RegisteredClaims { + let claims = biscuit::ClaimsSet:: { + registered: biscuit::RegisteredClaims { issuer: Some(issuer.to_string()), - issued_at: Some(Timestamp::from(Utc::now())), - audience: Some(SingleOrMultiple::Single(audience.to_string())), - expiry: Some(Timestamp::from(expiration_date)), + issued_at: Some(biscuit::Timestamp::from(Utc::now())), + audience: Some(biscuit::SingleOrMultiple::Single(audience.to_string())), + expiry: Some(biscuit::Timestamp::from(expiration_date)), ..Default::default() }, private: private_claims.clone(), }; // Construct the JWT - let jwt = jws::Compact::new_decoded( - From::from(jws::RegisteredHeader { - algorithm: SignatureAlgorithm::HS256, + let jwt = biscuit::jws::Compact::new_decoded( + From::from(biscuit::jws::RegisteredHeader { + algorithm: biscuit::jwa::SignatureAlgorithm::HS256, ..Default::default() }), - claims.clone(), + claims, ); jwt.into_encoded(&signing_secret) @@ -175,8 +167,11 @@ pub fn decode_token serde::Deserialize<'d token: &str, audience: &str, ) -> errors::Result { + use biscuit::Presence::Required; + use biscuit::Validation::Validate; + let signing_secret = match env::var(ENV_SHARED_SECRET) { - Ok(secret) => Secret::Bytes(secret.to_string().into_bytes()), + Ok(secret) => biscuit::jws::Secret::Bytes(secret.to_string().into_bytes()), Err(e) => { error!( "Shared Secret not configured. Please configure environment variable {}", @@ -185,18 +180,22 @@ pub fn decode_token serde::Deserialize<'d return Err(errors::Error::from(e)); } }; - let jwt: jws::Compact, Empty> = JWT::<_, Empty>::new_encoded(token); - let decoded_jwt = jwt.decode(&signing_secret, SignatureAlgorithm::HS256)?; - let mut val_options = ValidationOptions::default(); - let mut claim_presence_options = ClaimPresenceOptions::default(); - claim_presence_options.expiry = Required; - claim_presence_options.issuer = Required; - claim_presence_options.audience = Required; - claim_presence_options.issued_at = Required; - val_options.claim_presence_options = claim_presence_options; - val_options.issued_at = Validate(Duration::minutes(5)); - // Issuer is not validated. Wouldn't make much of a difference if we did - val_options.audience = Validate(audience.to_string()); - assert!(decoded_jwt.validate(val_options).is_ok()); + let jwt: biscuit::jws::Compact, biscuit::Empty> = biscuit::JWT::<_, biscuit::Empty>::new_encoded(token); + let decoded_jwt = jwt.decode(&signing_secret, biscuit::jwa::SignatureAlgorithm::HS256)?; + let claim_presence_options = biscuit::ClaimPresenceOptions { + issuer: Required, + audience: Required, + issued_at: Required, + expiry: Required, + ..Default::default() + }; + let val_options = biscuit::ValidationOptions { + claim_presence_options, + issued_at: Validate(Duration::minutes(5)), + // Issuer is not validated. Wouldn't make much of a difference if we did + audience: Validate(audience.to_string()), + ..Default::default() + }; + assert!(decoded_jwt.validate(val_options).is_ok()); // TODO: Handle error Ok(decoded_jwt.payload().unwrap().private.clone()) } diff --git a/clearing-house-app/logging-service/src/model/constants.rs b/clearing-house-app/logging-service/src/model/constants.rs index 2b33c76..a6e027f 100644 --- a/clearing-house-app/logging-service/src/model/constants.rs +++ b/clearing-house-app/logging-service/src/model/constants.rs @@ -1,100 +1,100 @@ -pub const CONTENT_TYPE: &'static str = "Content-Type"; -pub const APPLICATION_JSON: &'static str = "application/json"; -pub const SIGNING_KEY: &'static str = "signing_key"; +pub const CONTENT_TYPE: &str = "Content-Type"; +pub const APPLICATION_JSON: &str = "application/json"; +pub const SIGNING_KEY: &str = "signing_key"; -pub const CLEARING_HOUSE_URL: &'static str = "clearing_house_url"; -pub const ROCKET_CLEARING_HOUSE_BASE_API: &'static str = "/messages"; -pub const ROCKET_PK_API: &'static str = "/"; -pub const ROCKET_QUERY_API: &'static str = "/query"; -pub const ROCKET_LOG_API: &'static str = "/log"; -pub const ROCKET_BLOCKCHAIN_BASE_API: &'static str = "/blockchain"; +pub const CLEARING_HOUSE_URL: &str = "clearing_house_url"; +pub const ROCKET_CLEARING_HOUSE_BASE_API: &str = "/messages"; +pub const ROCKET_PK_API: &str = "/"; +pub const ROCKET_QUERY_API: &str = "/query"; +pub const ROCKET_LOG_API: &str = "/log"; +pub const ROCKET_BLOCKCHAIN_BASE_API: &str = "/blockchain"; // From core_lib // definition of daps constants -pub const DAPS_AUD: &'static str = "idsc:IDS_CONNECTORS_ALL"; -pub const DAPS_JWKS: &'static str = ".well-known/jwks.json"; -pub const DAPS_KID: &'static str = "default"; -pub const DAPS_AUTHHEADER: &'static str = "Authorization"; -pub const DAPS_AUTHBEARER: &'static str = "Bearer"; -pub const DAPS_CERTIFICATES: &'static str = "certs"; +pub const DAPS_AUD: &str = "idsc:IDS_CONNECTORS_ALL"; +pub const DAPS_JWKS: &str = ".well-known/jwks.json"; +pub const DAPS_KID: &str = "default"; +pub const DAPS_AUTHHEADER: &str = "Authorization"; +pub const DAPS_AUTHBEARER: &str = "Bearer"; +pub const DAPS_CERTIFICATES: &str = "certs"; // definition of custom headers -pub const SERVICE_HEADER: &'static str = "CH-SERVICE"; +pub const SERVICE_HEADER: &str = "CH-SERVICE"; // definition of config parameters (in config files) -pub const DATABASE_URL: &'static str = "database_url"; -pub const DOCUMENT_API_URL: &'static str = "document_api_url"; -pub const KEYRING_API_URL: &'static str = "keyring_api_url"; -pub const DAPS_API_URL: &'static str = "daps_api_url"; -pub const CLEAR_DB: &'static str = "clear_db"; +pub const DATABASE_URL: &str = "database_url"; +pub const DOCUMENT_API_URL: &str = "document_api_url"; +pub const KEYRING_API_URL: &str = "keyring_api_url"; +pub const DAPS_API_URL: &str = "daps_api_url"; +pub const CLEAR_DB: &str = "clear_db"; // define here the config options from environment variables -pub const ENV_API_LOG_LEVEL: &'static str = "API_LOG_LEVEL"; -pub const ENV_SHARED_SECRET: &'static str = "SHARED_SECRET"; -pub const ENV_DOCUMENT_SERVICE_ID: &'static str = "SERVICE_ID_DOC"; -pub const ENV_KEYRING_SERVICE_ID: &'static str = "SERVICE_ID_KEY"; -pub const ENV_LOGGING_SERVICE_ID: &'static str = "SERVICE_ID_LOG"; +pub const ENV_API_LOG_LEVEL: &str = "API_LOG_LEVEL"; +pub const ENV_SHARED_SECRET: &str = "SHARED_SECRET"; +pub const ENV_DOCUMENT_SERVICE_ID: &str = "SERVICE_ID_DOC"; +pub const ENV_KEYRING_SERVICE_ID: &str = "SERVICE_ID_KEY"; +pub const ENV_LOGGING_SERVICE_ID: &str = "SERVICE_ID_LOG"; // definition of rocket mount points -pub const ROCKET_DOC_API: &'static str = "/doc"; -pub const ROCKET_DOC_TYPE_API: &'static str = "/doctype"; -pub const ROCKET_POLICY_API: &'static str = "/policy"; -pub const ROCKET_STATISTICS: &'static str = "/statistics"; -pub const ROCKET_PROCESS_API: &'static str = "/process"; -pub const ROCKET_KEYRING_API: &'static str = "/keyring"; -pub const ROCKET_USER_API: &'static str = "/users"; +pub const ROCKET_DOC_API: &str = "/doc"; +pub const ROCKET_DOC_TYPE_API: &str = "/doctype"; +pub const ROCKET_POLICY_API: &str = "/policy"; +pub const ROCKET_STATISTICS: &str = "/statistics"; +pub const ROCKET_PROCESS_API: &str = "/process"; +pub const ROCKET_KEYRING_API: &str = "/keyring"; +pub const ROCKET_USER_API: &str = "/users"; // definition of service names -pub const DOCUMENT_DB_CLIENT: &'static str = "document-api"; -pub const KEYRING_DB_CLIENT: &'static str = "keyring-api"; -pub const PROCESS_DB_CLIENT: &'static str = "logging-service"; +pub const DOCUMENT_DB_CLIENT: &str = "document-api"; +pub const KEYRING_DB_CLIENT: &str = "keyring-api"; +pub const PROCESS_DB_CLIENT: &str = "logging-service"; // definition of table names -pub const MONGO_DB: &'static str = "ch_ids"; -pub const DOCUMENT_DB: &'static str = "document"; -pub const KEYRING_DB: &'static str = "keyring"; -pub const PROCESS_DB: &'static str = "process"; -pub const MONGO_COLL_DOCUMENTS: &'static str = "documents"; -pub const MONGO_COLL_DOCUMENT_BUCKET: &'static str = "document_bucket"; -pub const MONGO_COLL_DOC_TYPES: &'static str = "doc_types"; -pub const MONGO_COLL_DOC_PARTS: &'static str = "parts"; -pub const MONGO_COLL_PROCESSES: &'static str = "processes"; -pub const MONGO_COLL_TRANSACTIONS: &'static str = "transactions"; -pub const MONGO_COLL_MASTER_KEY: &'static str = "keys"; +pub const MONGO_DB: &str = "ch_ids"; +pub const DOCUMENT_DB: &str = "document"; +pub const KEYRING_DB: &str = "keyring"; +pub const PROCESS_DB: &str = "process"; +pub const MONGO_COLL_DOCUMENTS: &str = "documents"; +pub const MONGO_COLL_DOCUMENT_BUCKET: &str = "document_bucket"; +pub const MONGO_COLL_DOC_TYPES: &str = "doc_types"; +pub const MONGO_COLL_DOC_PARTS: &str = "parts"; +pub const MONGO_COLL_PROCESSES: &str = "processes"; +pub const MONGO_COLL_TRANSACTIONS: &str = "transactions"; +pub const MONGO_COLL_MASTER_KEY: &str = "keys"; // definition of database fields -pub const MONGO_ID: &'static str = "id"; -pub const MONGO_MKEY: &'static str = "msk"; -pub const MONGO_PID: &'static str = "pid"; -pub const MONGO_DT_ID: &'static str = "dt_id"; -pub const MONGO_NAME: &'static str = "name"; -pub const MONGO_OWNER: &'static str = "owner"; -pub const MONGO_TS: &'static str = "ts"; -pub const MONGO_TC: &'static str = "tc"; - -pub const MONGO_DOC_ARRAY: &'static str = "documents"; -pub const MONGO_COUNTER: &'static str = "counter"; -pub const MONGO_FROM_TS: &'static str = "from_ts"; -pub const MONGO_TO_TS: &'static str = "to_ts"; +pub const MONGO_ID: &str = "id"; +pub const MONGO_MKEY: &str = "msk"; +pub const MONGO_PID: &str = "pid"; +pub const MONGO_DT_ID: &str = "dt_id"; +pub const MONGO_NAME: &str = "name"; +pub const MONGO_OWNER: &str = "owner"; +pub const MONGO_TS: &str = "ts"; +pub const MONGO_TC: &str = "tc"; + +pub const MONGO_DOC_ARRAY: &str = "documents"; +pub const MONGO_COUNTER: &str = "counter"; +pub const MONGO_FROM_TS: &str = "from_ts"; +pub const MONGO_TO_TS: &str = "to_ts"; // definition of default database values -pub const DEFAULT_PROCESS_ID: &'static str = "default"; +pub const DEFAULT_PROCESS_ID: &str = "default"; pub const MAX_NUM_RESPONSE_ENTRIES: u64 = 1000; pub const DEFAULT_NUM_RESPONSE_ENTRIES: u64 = 100; -pub const DEFAULT_DOC_TYPE: &'static str = "IDS_MESSAGE"; +pub const DEFAULT_DOC_TYPE: &str = "IDS_MESSAGE"; // split string symbols for vec_to_string and string_to_vec -pub const SPLIT_QUOTE: &'static str = "'"; -pub const SPLIT_SIGN: &'static str = "~"; -pub const SPLIT_CT: &'static str = "::"; +pub const SPLIT_QUOTE: &str = "'"; +pub const SPLIT_SIGN: &str = "~"; +pub const SPLIT_CT: &str = "::"; // definition of file names and folders -pub const FOLDER_DB: &'static str = "db_init"; -pub const FOLDER_DATA: &'static str = "data"; -pub const FILE_DOC: &'static str = "document.json"; -pub const FILE_DEFAULT_DOC_TYPE: &'static str = "init_db/default_doc_type.json"; +pub const FOLDER_DB: &str = "db_init"; +pub const FOLDER_DATA: &str = "data"; +pub const FILE_DOC: &str = "document.json"; +pub const FILE_DEFAULT_DOC_TYPE: &str = "init_db/default_doc_type.json"; // definition of special document parts -pub const PAYLOAD_PART: &'static str = "payload"; +pub const PAYLOAD_PART: &str = "payload"; diff --git a/clearing-house-app/logging-service/src/model/document.rs b/clearing-house-app/logging-service/src/model/document.rs index 0c6a4d7..f06044d 100644 --- a/clearing-house-app/logging-service/src/model/document.rs +++ b/clearing-house-app/logging-service/src/model/document.rs @@ -104,15 +104,14 @@ impl Document { let mut cts = vec![]; let keys = key_map.keys; - let key_ct; - match key_map.keys_enc { + let key_ct= match key_map.keys_enc { Some(ct) => { - key_ct = hex::encode(ct); + hex::encode(ct) } None => { error_chain::bail!("Missing key ct"); } - } + }; for part in self.parts.iter() { if part.content.is_none() { diff --git a/clearing-house-app/logging-service/src/model/ids/message.rs b/clearing-house-app/logging-service/src/model/ids/message.rs index dafa0b1..aa950f6 100644 --- a/clearing-house-app/logging-service/src/model/ids/message.rs +++ b/clearing-house-app/logging-service/src/model/ids/message.rs @@ -3,22 +3,22 @@ use crate::model::document::{Document, DocumentPart}; use crate::model::ids::{InfoModelDateTime, InfoModelId, MessageType, SecurityToken}; use std::collections::HashMap; -const MESSAGE_ID: &'static str = "message_id"; -const MODEL_VERSION: &'static str = "model_version"; -const CORRELATION_MESSAGE: &'static str = "correlation_message"; -const TRANSFER_CONTRACT: &'static str = "transfer_contract"; -const ISSUED: &'static str = "issued"; -const ISSUER_CONNECTOR: &'static str = "issuer_connector"; -const CONTENT_VERSION: &'static str = "content_version"; +const MESSAGE_ID: &str = "message_id"; +const MODEL_VERSION: &str = "model_version"; +const CORRELATION_MESSAGE: &str = "correlation_message"; +const TRANSFER_CONTRACT: &str = "transfer_contract"; +const ISSUED: &str = "issued"; +const ISSUER_CONNECTOR: &str = "issuer_connector"; +const CONTENT_VERSION: &str = "content_version"; /// const RECIPIENT_CONNECTOR: &'static str = "recipient_connector"; // all messages should contain the CH connector, so we skip this information -const SENDER_AGENT: &'static str = "sender_agent"; +const SENDER_AGENT: &str = "sender_agent"; ///const RECIPIENT_AGENT: &'static str = "recipient_agent"; // all messages should contain the CH agent, so we skip this information -const PAYLOAD: &'static str = "payload"; -const PAYLOAD_TYPE: &'static str = "payload_type"; +const PAYLOAD: &str = "payload"; +const PAYLOAD_TYPE: &str = "payload_type"; -pub const RESULT_MESSAGE: &'static str = "ResultMessage"; -pub const REJECTION_MESSAGE: &'static str = "RejectionMessage"; -pub const MESSAGE_PROC_NOTIFICATION_MESSAGE: &'static str = "MessageProcessedNotificationMessage"; +pub const RESULT_MESSAGE: &str = "ResultMessage"; +pub const REJECTION_MESSAGE: &str = "RejectionMessage"; +pub const MESSAGE_PROC_NOTIFICATION_MESSAGE: &str = "MessageProcessedNotificationMessage"; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct IdsMessage { @@ -44,9 +44,9 @@ pub struct IdsMessage { pub model_version: String, //IDS name #[serde( - rename = "ids:correlationMessage", - alias = "correlationMessage", - skip_serializing_if = "Option::is_none" + rename = "ids:correlationMessage", + alias = "correlationMessage", + skip_serializing_if = "Option::is_none" )] // Correlated message, e.g. a response to a previous request pub correlation_message: Option, @@ -64,49 +64,49 @@ pub struct IdsMessage { pub sender_agent: String, //IDS name #[serde( - rename = "ids:recipientConnector", - alias = "recipientConnector", - skip_serializing_if = "Option::is_none" + rename = "ids:recipientConnector", + alias = "recipientConnector", + skip_serializing_if = "Option::is_none" )] // The Connector which is the recipient of the message pub recipient_connector: Option>, //IDS name #[serde( - rename = "ids:recipientAgent", - alias = "recipientAgent", - skip_serializing_if = "Option::is_none" + rename = "ids:recipientAgent", + alias = "recipientAgent", + skip_serializing_if = "Option::is_none" )] // The Agent for which the Message is intended pub recipient_agent: Option>, //IDS name #[serde( - rename = "ids:transferContract", - alias = "transferContract", - skip_serializing_if = "Option::is_none" + rename = "ids:transferContract", + alias = "transferContract", + skip_serializing_if = "Option::is_none" )] // The contract which is (or will be) the legal basis of the data transfer pub transfer_contract: Option, //IDS name #[serde( - rename = "ids:contentVersion", - alias = "contentVersion", - skip_serializing_if = "Option::is_none" + rename = "ids:contentVersion", + alias = "contentVersion", + skip_serializing_if = "Option::is_none" )] // The contract which is (or will be) the legal basis of the data transfer pub content_version: Option, //IDS name #[serde( - rename = "ids:securityToken", - alias = "securityToken", - skip_serializing + rename = "ids:securityToken", + alias = "securityToken", + skip_serializing )] // Authorization pub security_token: Option, //IDS name #[serde( - rename = "ids:authorizationToken", - alias = "authorizationToken", - skip_serializing_if = "Option::is_none" + rename = "ids:authorizationToken", + alias = "authorizationToken", + skip_serializing_if = "Option::is_none" )] // Authorization pub authorization_token: Option, @@ -120,21 +120,13 @@ pub struct IdsMessage { pub payload_type: Option, } -macro_rules! hashmap { - ($( $key: expr => $val: expr ),*) => {{ - let mut map = ::std::collections::HashMap::new(); - $( map.insert($key, $val); )* - map - }} -} - impl Default for IdsMessage { fn default() -> Self { IdsMessage { - context: Some(hashmap![ - "ids".to_string() => "https://w3id.org/idsa/core/".to_string(), - "idsc".to_string() => "https://w3id.org/idsa/code/".to_string() - ]), + context: Some(std::collections::HashMap::from([ + ("ids".to_string(), "https://w3id.org/idsa/core/".to_string()), + ("idsc".to_string(), "https://w3id.org/idsa/code/".to_string()) + ])), type_message: MessageType::Message, id: Some(autogen("MessageProcessedNotification")), pid: None, @@ -160,21 +152,21 @@ impl IdsMessage { let mut message = IdsMessage::clone(msg); message.id = Some(autogen(MESSAGE_PROC_NOTIFICATION_MESSAGE)); message.type_message = MessageType::MessageProcessedNotification; - return message; + message } pub fn return_result(msg: IdsMessage) -> IdsMessage { let mut message = IdsMessage::clone(msg); message.id = Some(autogen(RESULT_MESSAGE)); message.type_message = MessageType::ResultMessage; - return message; + message } pub fn error(msg: IdsMessage) -> IdsMessage { let mut message = IdsMessage::clone(msg); message.id = Some(autogen(REJECTION_MESSAGE)); message.type_message = MessageType::RejectionMessage; - return message; + message } fn clone(msg: IdsMessage) -> IdsMessage { @@ -395,11 +387,5 @@ impl From for Document { } fn autogen(message: &str) -> String { - [ - "https://w3id.org/idsa/autogen/", - message, - "/", - &Document::create_uuid(), - ] - .concat() + format!("https://w3id.org/idsa/autogen/{}/{}", message, Document::create_uuid()) } diff --git a/clearing-house-app/logging-service/src/model/ids/mod.rs b/clearing-house-app/logging-service/src/model/ids/mod.rs index 25c98b7..7866aed 100644 --- a/clearing-house-app/logging-service/src/model/ids/mod.rs +++ b/clearing-house-app/logging-service/src/model/ids/mod.rs @@ -1,7 +1,4 @@ use crate::model::ids::message::IdsMessage; -use chrono::prelude::*; -use std::fmt; -use std::fmt::{Display, Formatter, Result}; pub mod message; pub mod request; @@ -14,8 +11,8 @@ pub struct InfoModelComplexId { pub id: Option, } -impl Display for InfoModelComplexId { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { +impl std::fmt::Display for InfoModelComplexId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match &self.id { Some(id) => write!(f, "{}", serde_json::to_string(id).unwrap()), None => write!(f, ""), @@ -50,8 +47,8 @@ impl InfoModelId { } } -impl fmt::Display for InfoModelId { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { +impl std::fmt::Display for InfoModelId { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { match self { InfoModelId::SimpleId(id) => fmt.write_str(&id)?, InfoModelId::ComplexId(id) => fmt.write_str(&id.to_string())?, @@ -69,20 +66,20 @@ impl From for InfoModelId { #[serde(untagged)] pub enum InfoModelDateTime { ComplexTime(InfoModelTimeStamp), - Time(DateTime), + Time(chrono::DateTime), } impl InfoModelDateTime { pub fn new() -> InfoModelDateTime { - InfoModelDateTime::Time(Local::now()) + InfoModelDateTime::Time(chrono::Local::now()) } pub fn complex() -> InfoModelDateTime { InfoModelDateTime::ComplexTime(InfoModelTimeStamp::default()) } } -impl fmt::Display for InfoModelDateTime { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { +impl std::fmt::Display for InfoModelDateTime { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { match self { InfoModelDateTime::Time(value) => fmt.write_str(&value.to_string())?, InfoModelDateTime::ComplexTime(value) => fmt.write_str(&value.to_string())?, @@ -102,19 +99,19 @@ pub struct InfoModelTimeStamp { pub format: Option, //IDS name #[serde(rename = "@value", alias = "value")] - pub value: DateTime, + pub value: chrono::DateTime, } impl Default for InfoModelTimeStamp { fn default() -> Self { InfoModelTimeStamp { format: Some("http://www.w3.org/2001/XMLSchema#dateTimeStamp".to_string()), - value: Local::now(), + value: chrono::Local::now(), } } } -impl Display for InfoModelTimeStamp { - fn fmt(&self, f: &mut Formatter<'_>) -> Result { +impl std::fmt::Display for InfoModelTimeStamp { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match serde_json::to_string(&self) { Ok(result) => write!(f, "{}", result), Err(e) => { @@ -196,11 +193,11 @@ impl IdsQueryResult { order: String, documents: Vec, ) -> IdsQueryResult { - let date_from = NaiveDateTime::from_timestamp_opt(date_from, 0) + let date_from = chrono::NaiveDateTime::from_timestamp_opt(date_from, 0) .expect("Invalid date_from seconds") .format("%Y-%m-%d %H:%M:%S") .to_string(); - let date_to = NaiveDateTime::from_timestamp_opt(date_to, 0) + let date_to = chrono::NaiveDateTime::from_timestamp_opt(date_to, 0) .expect("Invalid date_to seconds") .format("%Y-%m-%d %H:%M:%S") .to_string(); diff --git a/clearing-house-app/logging-service/src/model/process.rs b/clearing-house-app/logging-service/src/model/process.rs index 3eb2c01..7c4bf1d 100644 --- a/clearing-house-app/logging-service/src/model/process.rs +++ b/clearing-house-app/logging-service/src/model/process.rs @@ -68,10 +68,12 @@ impl DataTransaction { } // convenience method for testing +#[cfg(test)] impl From for DataTransaction { + // TODO: It would be better to implement the TryFrom trait instead of this error DataTransaction fn from(r: Receipt) -> Self { match r.data.unverified_payload() { - Ok(d) => d.clone(), + Ok(d) => d, Err(e) => { println!("Error occured: {:#?}", e); DataTransaction { diff --git a/clearing-house-app/logging-service/src/model/util.rs b/clearing-house-app/logging-service/src/model/util.rs index 367d122..60d2efb 100644 --- a/clearing-house-app/logging-service/src/model/util.rs +++ b/clearing-house-app/logging-service/src/model/util.rs @@ -1,3 +1,4 @@ +/// Returns a new UUID as a string with hyphens. pub fn new_uuid() -> String { use uuid::Uuid; Uuid::new_v4().hyphenated().to_string() diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/logging-service/src/services/document_service.rs index 28facb8..1121e8d 100644 --- a/clearing-house-app/logging-service/src/services/document_service.rs +++ b/clearing-house-app/logging-service/src/services/document_service.rs @@ -34,12 +34,12 @@ impl DocumentService { let payload: Vec = doc .parts .iter() - .filter(|p| String::from(PAYLOAD_PART) == p.name) + .filter(|p| *PAYLOAD_PART == p.name) .map(|p| p.content.as_ref().unwrap().clone()) .collect(); if payload.len() > 1 { - return Err(anyhow!("Document contains two payloads!")); // BadRequest - } else if payload.len() == 0 { + return Err(anyhow!("Document contains two or more payloads!")); // BadRequest + } else if payload.is_empty() { return Err(anyhow!("Document contains no payload!")); // BadRequest } @@ -323,31 +323,31 @@ impl DocumentService { Ok(d) => Ok(d), Err(e) => { warn!("Got empty document from decryption! {:?}", e); - return Err(anyhow!("Document {} not found!", &id)); + Err(anyhow!("Document {} not found!", &id)) // NotFound } } } Err(e) => { error!("Error while retrieving keys from keyring: {:?}", e); - return Err(anyhow!("Error while retrieving keys")); + Err(anyhow!("Error while retrieving keys")) // InternalError } } } Err(e) => { error!("Error while decoding ciphertext: {:?}", e); - return Err(anyhow!("Key Ciphertext corrupted")); // InternalError + Err(anyhow!("Key Ciphertext corrupted")) // InternalError } } } Ok(None) => { debug!("Nothing found in db!"); - return Err(anyhow!("Document {} not found!", &id)); // NotFound + Err(anyhow!("Document {} not found!", &id)) // NotFound } Err(e) => { error!("Error while retrieving document: {:?}", e); - return Err(anyhow!("Error while retrieving document {}", &id)); // InternalError + Err(anyhow!("Error while retrieving document {}", &id)) // InternalError } } } diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index 8956fec..569fc2a 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -86,7 +86,7 @@ impl KeyringService { Ok(key) => Some((key_ct.id.clone(), key)), Err(e) => { error!("Error while decoding key ciphertext: {}", e); - dec_error_count = dec_error_count + 1; + dec_error_count += 1; None } }) @@ -95,7 +95,7 @@ impl KeyringService { Ok(key_map) => Some(KeyMapListItem::new(id, key_map)), Err(e) => { error!("Error while generating key map: {}", e); - map_error_count = map_error_count + 1; + map_error_count += 1; None } } @@ -106,24 +106,24 @@ impl KeyringService { // Currently, we don't tolerate errors while decrypting keys if error_count > 0 { - return Err(anyhow!("Error while decrypting keys")); // InternalError + Err(anyhow!("Error while decrypting keys")) // InternalError } else { - return Ok(key_maps); + Ok(key_maps) } } Ok(None) => { warn!("document type {} not found", &key_cts.dt); - return Err(anyhow!("Document type not found!")); // BadRequest + Err(anyhow!("Document type not found!")) // BadRequest } Err(e) => { warn!("Error while retrieving document type: {}", e); - return Err(anyhow!("Document type not found!")); // NotFound + Err(anyhow!("Document type not found!")) // NotFound } } } Err(e) => { error!("Error while retrieving master key: {}", e); - return Err(anyhow!("Error while decrypting keys")); // InternalError + Err(anyhow!("Error while decrypting keys")) // InternalError } } } @@ -145,39 +145,35 @@ impl KeyringService { match self.db.get_document_type(&dt_id).await { Ok(Some(dt)) => { // validate keys_ct input - let keys_ct = match hex::decode(keys_ct) { - Ok(key) => key, - Err(e) => { + let keys_ct = hex::decode(keys_ct) + .map_err(|e| { error!("Error while decoding key ciphertext: {}", e); - return Err(anyhow!("Error while decrypting keys")); - // InternalError - } - }; + anyhow!("Error while decrypting keys") // InternalError + })?; match restore_key_map(key, dt, keys_ct) { Ok(key_map) => { - return Ok(key_map); + Ok(key_map) } Err(e) => { error!("Error while generating key map: {}", e); - return Err(anyhow!("Error while restoring keys")); - // InternalError + Err(anyhow!("Error while restoring keys")) // InternalError } } } Ok(None) => { warn!("document type {} not found", &dt_id); - return Err(anyhow!("Document type not found!")); // BadRequest + Err(anyhow!("Document type not found!")) // BadRequest } Err(e) => { warn!("Error while retrieving document type: {}", e); - return Err(anyhow!("Document type not found!")); // NotFound + Err(anyhow!("Document type not found!")) // NotFound } } } Err(e) => { error!("Error while retrieving master key: {}", e); - return Err(anyhow!("Error while decrypting keys")); // InternalError + Err(anyhow!("Error while decrypting keys")) // InternalError } } } diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/logging-service/src/services/logging_service.rs index b536c48..6cb4460 100644 --- a/clearing-house-app/logging-service/src/services/logging_service.rs +++ b/clearing-house-app/logging-service/src/services/logging_service.rs @@ -301,13 +301,11 @@ impl LoggingService { if s > converted_max { warn!("...invalid size requested. Falling back to default."); converted_max + } else if s > 0 { + s } else { - if s > 0 { - s - } else { - warn!("...invalid size requested. Falling back to default."); - i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() - } + warn!("...invalid size requested. Falling back to default."); + i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() } } None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap(), From 37cbe295e7b60ff079735335ee1d4273a96cd35c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 29 Aug 2023 11:09:56 +0200 Subject: [PATCH 043/183] refactor(ch-app): Autofix clippy warnings --- .../logging-service/src/config.rs | 10 ++++----- .../src/db/config/doc_store.rs | 2 +- .../src/db/config/keyring_store.rs | 6 ++--- .../src/db/config/process_store.rs | 6 ++--- .../logging-service/src/db/doc_store.rs | 16 +++++++------- .../logging-service/src/db/process_store.rs | 2 +- .../logging-service/src/model/claims.rs | 2 +- .../logging-service/src/model/document.rs | 2 +- .../logging-service/src/model/ids/mod.rs | 2 +- .../logging-service/src/ports/doc_type_api.rs | 10 ++++----- .../logging-service/src/ports/logging_api.rs | 4 ++-- .../src/services/keyring_service.rs | 22 ++++++++----------- .../src/services/logging_service.rs | 16 +++++++------- 13 files changed, 48 insertions(+), 52 deletions(-) diff --git a/clearing-house-app/logging-service/src/config.rs b/clearing-house-app/logging-service/src/config.rs index 0093b5d..5bca6a9 100644 --- a/clearing-house-app/logging-service/src/config.rs +++ b/clearing-house-app/logging-service/src/config.rs @@ -20,9 +20,9 @@ pub(crate) enum LogLevel { Error, } -impl Into for LogLevel { - fn into(self) -> tracing::Level { - match self { +impl From for tracing::Level { + fn from(val: LogLevel) -> Self { + match val { LogLevel::Trace => tracing::Level::TRACE, LogLevel::Debug => tracing::Level::DEBUG, LogLevel::Info => tracing::Level::INFO, @@ -101,7 +101,7 @@ mod test { assert_eq!(conf.process_database_url, "mongodb://localhost:27117"); assert_eq!(conf.keyring_database_url, "mongodb://localhost:27118"); assert_eq!(conf.document_database_url, "mongodb://localhost:27119"); - assert_eq!(conf.clear_db, true); + assert!(conf.clear_db); assert_eq!(conf.log_level, Some(super::LogLevel::Info)); // Cleanup @@ -137,7 +137,7 @@ log_level = "ERROR" assert_eq!(conf.process_database_url, "mongodb://localhost:27019"); assert_eq!(conf.keyring_database_url, "mongodb://localhost:27020"); assert_eq!(conf.document_database_url, "mongodb://localhost:27017"); - assert_eq!(conf.clear_db, true); + assert!(conf.clear_db); assert_eq!(conf.log_level, Some(super::LogLevel::Error)); } } diff --git a/clearing-house-app/logging-service/src/db/config/doc_store.rs b/clearing-house-app/logging-service/src/db/config/doc_store.rs index 3ed9dc1..d41b3e0 100644 --- a/clearing-house-app/logging-service/src/db/config/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/config/doc_store.rs @@ -47,7 +47,7 @@ impl DatastoreConfigurator { pub async fn init_datastore(db_url: String, clear_db: bool) -> anyhow::Result { debug!("Using mongodb url: '{:#?}'", &db_url); match init_database_client::( - &db_url.as_str(), + db_url.as_str(), Some(DOCUMENT_DB_CLIENT.to_string()), ) .await diff --git a/clearing-house-app/logging-service/src/db/config/keyring_store.rs b/clearing-house-app/logging-service/src/db/config/keyring_store.rs index 48eef89..79ea1b9 100644 --- a/clearing-house-app/logging-service/src/db/config/keyring_store.rs +++ b/clearing-house-app/logging-service/src/db/config/keyring_store.rs @@ -47,7 +47,7 @@ impl KeyringDbConfigurator { debug!("Using database url: '{:#?}'", &db_url); match init_database_client::( - &db_url.as_str(), + db_url.as_str(), Some(KEYRING_DB_CLIENT.to_string()), ) .await @@ -62,7 +62,7 @@ impl KeyringDbConfigurator { { Ok(colls) => { debug!("... found collections: {:#?}", &colls); - if colls.len() > 0 && clear_db { + if !colls.is_empty() && clear_db { debug!("Database not empty and clear_db == true. Dropping database..."); match keystore.client.database(KEYRING_DB).drop(None).await { Ok(_) => { @@ -74,7 +74,7 @@ impl KeyringDbConfigurator { } }; } - if colls.len() == 0 || clear_db { + if colls.is_empty() || clear_db { debug!("Database empty. Need to initialize..."); debug!("Adding initial document type..."); match serde_json::from_str::( diff --git a/clearing-house-app/logging-service/src/db/config/process_store.rs b/clearing-house-app/logging-service/src/db/config/process_store.rs index bad5120..d8b1a62 100644 --- a/clearing-house-app/logging-service/src/db/config/process_store.rs +++ b/clearing-house-app/logging-service/src/db/config/process_store.rs @@ -52,7 +52,7 @@ impl ProcessStoreConfigurator { debug!("...using database url: '{:#?}'", &db_url); match init_database_client::( - &db_url.as_str(), + db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string()), ) .await @@ -67,7 +67,7 @@ impl ProcessStoreConfigurator { { Ok(colls) => { debug!("... found collections: {:#?}", &colls); - if colls.len() > 0 && clear_db { + if !colls.is_empty() && clear_db { debug!( "...database not empty and clear_db == true. Dropping database..." ); @@ -81,7 +81,7 @@ impl ProcessStoreConfigurator { } }; } - if colls.len() == 0 || clear_db { + if colls.is_empty() || clear_db { debug!("..database empty. Need to initialize..."); let mut write_concern = WriteConcern::default(); write_concern.journal = Some(true); diff --git a/clearing-house-app/logging-service/src/db/doc_store.rs b/clearing-house-app/logging-service/src/db/doc_store.rs index 27cd396..544e932 100644 --- a/clearing-house-app/logging-service/src/db/doc_store.rs +++ b/clearing-house-app/logging-service/src/db/doc_store.rs @@ -115,7 +115,7 @@ impl DataStore { return Ok(Some(doc)); } - return Ok(None); + Ok(None) } /// gets documents for a single process from the db @@ -145,14 +145,14 @@ impl DataStore { let mut results = coll.aggregate(pipeline, None).await?; - return if let Some(result) = results.next().await { + if let Some(result) = results.next().await { debug!("Found {:#?}", &result); let doc: EncryptedDocument = bson::from_document(result?)?; Ok(Some(doc)) } else { warn!("Document with tc {} not found!", previous_tc); Ok(None) - }; + } } } @@ -239,7 +239,7 @@ impl DataStore { } Err(e) => { error!("Error while getting bucket offset!"); - Err(errors::Error::from(e)) + Err(e) } } } @@ -308,7 +308,7 @@ impl DataStore { } fn get_offset(bucket_size: &DocumentBucketSize) -> u64 { - return (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES; + (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES } fn get_start_bucket( @@ -335,7 +335,7 @@ impl DataStore { if start_bucket > 1 { start_entry = docs_to_skip - bucket_size.capacity as u64; if start_entry > 2 { - start_entry = start_entry - (start_bucket - 2) * MAX_NUM_RESPONSE_ENTRIES + start_entry -= (start_bucket - 2) * MAX_NUM_RESPONSE_ENTRIES } } start_entry @@ -391,8 +391,8 @@ mod bucket { ) -> EncryptedDocument { EncryptedDocument { id: bucket_update.id.clone(), - dt_id: dt_id, - pid: pid, + dt_id, + pid, ts: bucket_update.ts, tc: bucket_update.tc, hash: bucket_update.hash.clone(), diff --git a/clearing-house-app/logging-service/src/db/process_store.rs b/clearing-house-app/logging-service/src/db/process_store.rs index 23997b6..5d05617 100644 --- a/clearing-house-app/logging-service/src/db/process_store.rs +++ b/clearing-house-app/logging-service/src/db/process_store.rs @@ -110,7 +110,7 @@ impl ProcessStore { "checking if user '{}' is authorized to access '{}'", user, pid ); - match self.get_process(&pid).await { + match self.get_process(pid).await { Ok(Some(process)) => { let authorized = process.owners.iter().any(|o| { trace!("found owner {}", o); diff --git a/clearing-house-app/logging-service/src/model/claims.rs b/clearing-house-app/logging-service/src/model/claims.rs index 7c4a4f8..427cfec 100644 --- a/clearing-house-app/logging-service/src/model/claims.rs +++ b/clearing-house-app/logging-service/src/model/claims.rs @@ -39,7 +39,7 @@ impl<'r> FromRequest<'r> for ChClaims { type Error = ChClaimsError; async fn from_request(request: &'r Request<'_>) -> Outcome { - match request.headers().get_one(&SERVICE_HEADER) { + match request.headers().get_one(SERVICE_HEADER) { None => Outcome::Failure((Status::BadRequest, ChClaimsError::Missing)), Some(token) => { debug!("...received service header: {:?}", token); diff --git a/clearing-house-app/logging-service/src/model/document.rs b/clearing-house-app/logging-service/src/model/document.rs index f06044d..e0a8cc9 100644 --- a/clearing-house-app/logging-service/src/model/document.rs +++ b/clearing-house-app/logging-service/src/model/document.rs @@ -263,7 +263,7 @@ impl EncryptedDocument { hasher.update(ct.as_bytes()); } - let res = base64::encode(&hasher.finalize()); + let res = base64::encode(hasher.finalize()); debug!("hashed cts: '{}'", &res); res } diff --git a/clearing-house-app/logging-service/src/model/ids/mod.rs b/clearing-house-app/logging-service/src/model/ids/mod.rs index 7866aed..194f183 100644 --- a/clearing-house-app/logging-service/src/model/ids/mod.rs +++ b/clearing-house-app/logging-service/src/model/ids/mod.rs @@ -50,7 +50,7 @@ impl InfoModelId { impl std::fmt::Display for InfoModelId { fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - InfoModelId::SimpleId(id) => fmt.write_str(&id)?, + InfoModelId::SimpleId(id) => fmt.write_str(id)?, InfoModelId::ComplexId(id) => fmt.write_str(&id.to_string())?, } Ok(()) diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/logging-service/src/ports/doc_type_api.rs index 2699063..b02b440 100644 --- a/clearing-house-app/logging-service/src/ports/doc_type_api.rs +++ b/clearing-house-app/logging-service/src/ports/doc_type_api.rs @@ -16,7 +16,7 @@ async fn create_doc_type( Ok(dt) => ApiResponse::SuccessCreate(json!(dt)), Err(e) => { error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(e.to_string()); + ApiResponse::InternalError(e.to_string()) } } } @@ -35,7 +35,7 @@ async fn update_doc_type( Ok(id) => ApiResponse::SuccessOk(json!(id)), Err(e) => { error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(e.to_string()); + ApiResponse::InternalError(e.to_string()) } } } @@ -51,7 +51,7 @@ async fn delete_doc_type(key_api: &State, id: String, pid: Strin Ok(id) => ApiResponse::SuccessOk(json!(id)), Err(e) => { error!("Error while deleting doctype: {:?}", e); - return ApiResponse::InternalError(e.to_string()); + ApiResponse::InternalError(e.to_string()) } } } @@ -70,7 +70,7 @@ async fn get_doc_type(key_api: &State, id: String, pid: String) }, Err(e) => { error!("Error while retrieving doctype: {:?}", e); - return ApiResponse::InternalError(e.to_string()); + ApiResponse::InternalError(e.to_string()) } } } @@ -81,7 +81,7 @@ async fn get_doc_types(key_api: &State) -> ApiResponse { Ok(dt) => ApiResponse::SuccessOk(json!(dt)), Err(e) => { error!("Error while retrieving doctypes: {:?}", e); - return ApiResponse::InternalError(e.to_string()); + ApiResponse::InternalError(e.to_string()) } } } diff --git a/clearing-house-app/logging-service/src/ports/logging_api.rs b/clearing-house-app/logging-service/src/ports/logging_api.rs index 685bd24..6a1453e 100644 --- a/clearing-house-app/logging-service/src/ports/logging_api.rs +++ b/clearing-house-app/logging-service/src/ports/logging_api.rs @@ -140,7 +140,7 @@ pub fn mount_api() -> AdHoc { rocket::routes![log, unauth], ) .mount( - format!("{}", ROCKET_PROCESS_API).as_str(), + ROCKET_PROCESS_API.to_string().as_str(), rocket::routes![create_process, unauth], ) .mount( @@ -148,7 +148,7 @@ pub fn mount_api() -> AdHoc { rocket::routes![query_id, query_pid, unauth, unauth_id], ) .mount( - format!("{}", ROCKET_PK_API).as_str(), + ROCKET_PK_API.to_string().as_str(), rocket::routes![get_public_sign_key], ) }) diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/logging-service/src/services/keyring_service.rs index 569fc2a..65e2f3a 100644 --- a/clearing-house-app/logging-service/src/services/keyring_service.rs +++ b/clearing-house-app/logging-service/src/services/keyring_service.rs @@ -33,29 +33,27 @@ impl KeyringService { match crypto::generate_key_map(key, dt) { Ok(key_map) => { trace!("response: {:?}", &key_map); - return Ok(key_map); + Ok(key_map) } Err(e) => { error!("Error while generating key map: {}", e); - return Err(anyhow!("Error while generating keys")); - // InternalError + Err(anyhow!("Error while generating keys")) // InternalError } } } Ok(None) => { warn!("document type {} not found", &dt_id); - return Err(anyhow!("Document type not found!")); // BadRequest + Err(anyhow!("Document type not found!")) // BadRequest } Err(e) => { warn!("Error while retrieving document type: {}", e); - return Err(anyhow!("Error while retrieving document type")); - // InternalError + Err(anyhow!("Error while retrieving document type")) // InternalError } } } Err(e) => { error!("Error while retrieving master key: {}", e); - return Err(anyhow!("Error while generating keys")); // InternalError + Err(anyhow!("Error while generating keys")) // InternalError } } } @@ -203,14 +201,13 @@ impl KeyringService { Ok(()) => Ok(doc_type), Err(e) => { error!("Error while adding doctype: {:?}", e); - return Err(anyhow!("Error while adding document type!")); - // InternalError + Err(anyhow!("Error while adding document type!")) // InternalError } } } Err(e) => { error!("Error while adding document type: {:?}", e); - return Err(anyhow!("Error while checking database!")); // InternalError + Err(anyhow!("Error while checking database!")) // InternalError } } } @@ -231,14 +228,13 @@ impl KeyringService { Ok(id) => Ok(id), Err(e) => { error!("Error while adding doctype: {:?}", e); - return Err(anyhow!("Error while storing document type!")); - // InternalError + Err(anyhow!("Error while storing document type!")) // InternalError } } } Err(e) => { error!("Error while adding document type: {:?}", e); - return Err(anyhow!("Error while checking database!")); // InternalError + Err(anyhow!("Error while checking database!")) // InternalError } } } diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/logging-service/src/services/logging_service.rs index 6cb4460..ac1d71f 100644 --- a/clearing-house-app/logging-service/src/services/logging_service.rs +++ b/clearing-house-app/logging-service/src/services/logging_service.rs @@ -79,7 +79,7 @@ impl LoggingService { } // now check if user is authorized to write to pid - match self.db.is_authorized(&user, &pid).await { + match self.db.is_authorized(user, &pid).await { Ok(true) => info!("User authorized."), Ok(false) => { warn!("User is not authorized to write to pid '{}'", &pid); @@ -189,7 +189,7 @@ impl LoggingService { // TODO: ChClaims usage check match self .doc_api - .create_enc_document(ChClaims::new(&user), doc.clone()) + .create_enc_document(ChClaims::new(user), doc.clone()) .await { Ok(doc_receipt) => { @@ -244,7 +244,7 @@ impl LoggingService { date_to: Option, date_from: Option, pid: String, - message: ClearingHouseMessage, + _message: ClearingHouseMessage, ) -> anyhow::Result { debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); @@ -265,7 +265,7 @@ impl LoggingService { }; // now check if user is authorized to read infos in pid - match self.db.is_authorized(&user, &pid).await { + match self.db.is_authorized(user, &pid).await { Ok(true) => { info!("User authorized."); } @@ -316,7 +316,7 @@ impl LoggingService { match self .doc_api .get_enc_documents_for_pid( - ChClaims::new(&user), + ChClaims::new(user), None, Some(sanitized_page), Some(sanitized_size), @@ -350,7 +350,7 @@ impl LoggingService { ch_claims: ChClaims, pid: String, id: String, - message: ClearingHouseMessage, + _message: ClearingHouseMessage, ) -> anyhow::Result { trace!("...user '{:?}'", &ch_claims.client_id); let user = &ch_claims.client_id; @@ -369,7 +369,7 @@ impl LoggingService { }; // now check if user is authorized to read infos in pid - match self.db.is_authorized(&user, &pid).await { + match self.db.is_authorized(user, &pid).await { Ok(true) => { info!("User authorized."); } @@ -388,7 +388,7 @@ impl LoggingService { match self .doc_api - .get_enc_document(ChClaims::new(&user), pid.clone(), id.clone(), None) + .get_enc_document(ChClaims::new(user), pid.clone(), id.clone(), None) .await { Ok(doc) => { From fb7abd1254fb23272ad318a402959103882c8ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 29 Aug 2023 11:56:53 +0200 Subject: [PATCH 044/183] refactor(ch-app): Refactor directory structure of CH-App --- clearing-house-app/Cargo.toml | 8 - .../{ => logging-service}/Cargo.lock | 614 ++++-------------- clearing-house-app/logging-service/Cargo.toml | 3 +- .../logging-service/src/model/crypto.rs | 2 +- 4 files changed, 115 insertions(+), 512 deletions(-) delete mode 100644 clearing-house-app/Cargo.toml rename clearing-house-app/{ => logging-service}/Cargo.lock (83%) diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml deleted file mode 100644 index dadd69d..0000000 --- a/clearing-house-app/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[workspace] - -members = [ -# "core-lib", -# "document-api", -# "keyring-api", - "logging-service" -] diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/logging-service/Cargo.lock similarity index 83% rename from clearing-house-app/Cargo.lock rename to clearing-house-app/logging-service/Cargo.lock index 9afcaba..b6378b2 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/logging-service/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -17,15 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aead" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fc95d1bdb8e6666b2b217308eeeb09f2d6728d104be3e31916cc74d15420331" -dependencies = [ - "generic-array", -] - [[package]] name = "aead" version = "0.5.2" @@ -36,17 +27,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "aes" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884391ef1066acaa41e766ba8f596341b96e93ce34f9a43e7d24bf0a0eaf0561" -dependencies = [ - "aes-soft", - "aesni", - "cipher 0.2.5", -] - [[package]] name = "aes" version = "0.8.3" @@ -54,60 +34,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures", ] -[[package]] -name = "aes-gcm-siv" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202a43562bc3e159554b7590f5fd1f432d9e8de0cc2c2ce4bb8d194a34b3b0f3" -dependencies = [ - "aead 0.3.2", - "aes 0.6.0", - "cipher 0.2.5", - "ctr 0.6.0", - "polyval 0.4.5", - "subtle", - "zeroize", -] - [[package]] name = "aes-gcm-siv" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" dependencies = [ - "aead 0.5.2", - "aes 0.8.3", - "cipher 0.4.4", - "ctr 0.9.2", - "polyval 0.6.1", + "aead", + "aes", + "cipher", + "ctr", + "polyval", "subtle", "zeroize", ] -[[package]] -name = "aes-soft" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be14c7498ea50828a38d0e24a765ed2effe92a705885b57d029cd67d45744072" -dependencies = [ - "cipher 0.2.5", - "opaque-debug", -] - -[[package]] -name = "aesni" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2e11f5e94c2f7d386164cc2aa1f97823fed6f259e486940a71c174dd01b0ce" -dependencies = [ - "cipher 0.2.5", - "opaque-debug", -] - [[package]] name = "ahash" version = "0.7.6" @@ -205,9 +150,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -218,16 +163,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" -dependencies = [ - "byteorder", - "safemem", -] - [[package]] name = "base64" version = "0.13.1" @@ -236,9 +171,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" [[package]] name = "binascii" @@ -296,15 +231,6 @@ dependencies = [ "constant_time_eq", ] -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -331,7 +257,7 @@ dependencies = [ "serde", "serde_bytes", "serde_json", - "time 0.3.25", + "time", "uuid", ] @@ -355,9 +281,9 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "305fe645edc1442a0fa8b6726ba61d422798d37a52e12eaecf4b022ebbb88f01" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] @@ -376,23 +302,11 @@ checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", "serde", - "time 0.1.45", - "wasm-bindgen", "winapi", ] -[[package]] -name = "cipher" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f8e7987cbd042a63249497f41aed09f8e65add917ea6566effbc56578d6801" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" @@ -436,54 +350,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" dependencies = [ "percent-encoding", - "time 0.3.25", + "time", "version_check", ] -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" -[[package]] -name = "core-lib" -version = "0.10.0" -dependencies = [ - "aes 0.6.0", - "aes-gcm-siv 0.9.0", - "base64 0.9.3", - "biscuit", - "blake2-rfc", - "chrono", - "error-chain", - "fern", - "figment", - "generic-array", - "hex", - "log", - "mongodb", - "num-bigint", - "openssh-keys 0.5.0", - "percent-encoding", - "reqwest", - "ring", - "rocket", - "serde", - "serde_json", - "uuid", -] - [[package]] name = "cpufeatures" version = "0.2.9" @@ -493,12 +369,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpuid-bool" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" - [[package]] name = "crypto-common" version = "0.1.6" @@ -510,22 +380,13 @@ dependencies = [ "typenum", ] -[[package]] -name = "ctr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4a30d54f7443bf3d6191dcd486aca19e67cb3c49fa7a06a319966346707e7f" -dependencies = [ - "cipher 0.2.5", -] - [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ - "cipher 0.4.4", + "cipher", ] [[package]] @@ -565,9 +426,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.0" +version = "5.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -584,9 +445,9 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "deranged" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" [[package]] name = "derivative" @@ -645,22 +506,13 @@ dependencies = [ "syn 2.0.29", ] -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", + "block-buffer", "crypto-common", "subtle", ] @@ -673,9 +525,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -700,9 +552,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -735,16 +587,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" -[[package]] -name = "fern" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69ab0d5aca163e388c3a49d284fed6c3d0810700e77c5ae2756a50ec1a4daaa" -dependencies = [ - "chrono", - "log", -] - [[package]] name = "figment" version = "0.10.10" @@ -754,7 +596,6 @@ dependencies = [ "atomic", "pear", "serde", - "serde_yaml", "toml 0.7.6", "uncased", "version_check", @@ -916,14 +757,14 @@ checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "glob" @@ -933,9 +774,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -995,7 +836,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -1067,19 +908,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1175,7 +1003,7 @@ dependencies = [ "socket2 0.5.3", "widestring", "windows-sys", - "winreg 0.50.0", + "winreg", ] [[package]] @@ -1254,15 +1082,14 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" name = "logging-service" version = "0.10.0" dependencies = [ - "aes 0.8.3", - "aes-gcm-siv 0.11.1", + "aes", + "aes-gcm-siv", "anyhow", - "base64 0.21.2", + "base64 0.21.3", "biscuit", "blake2-rfc", "chrono", "config", - "core-lib", "error-chain", "generic-array", "hex", @@ -1270,7 +1097,7 @@ dependencies = [ "mongodb", "num-bigint", "once_cell", - "openssh-keys 0.6.2", + "openssh-keys", "percent-encoding", "rand", "ring", @@ -1278,7 +1105,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2 0.10.7", + "sha2", "tempfile", "tracing", "tracing-subscriber", @@ -1330,31 +1157,20 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "md-5" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" -dependencies = [ - "block-buffer 0.9.0", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "md-5" version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" [[package]] name = "mime" @@ -1384,7 +1200,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", "windows-sys", ] @@ -1408,7 +1224,9 @@ dependencies = [ "hex", "hmac", "lazy_static", - "md-5 0.10.5", + "md-5", + "openssl", + "openssl-probe", "pbkdf2", "percent-encoding", "rand", @@ -1419,13 +1237,14 @@ dependencies = [ "serde_bytes", "serde_with", "sha-1", - "sha2 0.10.7", + "sha2", "socket2 0.4.9", "stringprep", "strsim", "take_mut", "thiserror", "tokio", + "tokio-openssl", "tokio-rustls", "tokio-util", "trust-dns-proto", @@ -1455,24 +1274,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -1501,9 +1302,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -1541,9 +1342,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -1560,39 +1361,26 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "openssh-keys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7249a699cdeea261ac73f1bf9350777cb867324f44373aafb5a287365bf1771" -dependencies = [ - "base64 0.13.1", - "byteorder", - "md-5 0.9.1", - "sha2 0.9.9", - "thiserror", -] - [[package]] name = "openssh-keys" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75a0ec2d1b302412fb503224289325fcc0e44600176864804c7211b055cfd58" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "byteorder", - "md-5 0.10.5", - "sha2 0.10.7", + "md-5", + "sha2", "thiserror", ] [[package]] name = "openssl" -version = "0.10.56" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cfg-if", "foreign-types", "libc", @@ -1620,9 +1408,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.91" +version = "0.9.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" +checksum = "db7e971c2c2bba161b2d2fdf37080177eff520b3bc044787c7f1f5f9e78d869b" dependencies = [ "cc", "libc", @@ -1671,7 +1459,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ - "digest 0.10.7", + "digest", ] [[package]] @@ -1705,9 +1493,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -1721,17 +1509,6 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" -[[package]] -name = "polyval" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eebcc4aa140b9abd2bc40d9c3f7ccec842679cd79045ac3a7ac698c1a064b7cd" -dependencies = [ - "cpuid-bool", - "opaque-debug", - "universal-hash 0.4.0", -] - [[package]] name = "polyval" version = "0.6.1" @@ -1741,7 +1518,7 @@ dependencies = [ "cfg-if", "cpufeatures", "opaque-debug", - "universal-hash 0.5.1", + "universal-hash", ] [[package]] @@ -1854,14 +1631,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.3" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.6", - "regex-syntax 0.7.4", + "regex-automata 0.3.7", + "regex-syntax 0.7.5", ] [[package]] @@ -1875,13 +1652,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.4", + "regex-syntax 0.7.5", ] [[package]] @@ -1892,46 +1669,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" - -[[package]] -name = "reqwest" -version = "0.11.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" -dependencies = [ - "base64 0.21.2", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg 0.10.1", -] +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "resolv-conf" @@ -1988,7 +1728,7 @@ dependencies = [ "serde_json", "state", "tempfile", - "time 0.3.25", + "time", "tokio", "tokio-stream", "tokio-util", @@ -2035,7 +1775,7 @@ dependencies = [ "smallvec", "stable-pattern", "state", - "time 0.3.25", + "time", "tokio", "uncased", ] @@ -2076,9 +1816,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.8" +version = "0.38.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" dependencies = [ "bitflags 2.4.0", "errno", @@ -2089,9 +1829,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.8" +version = "0.20.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" dependencies = [ "log", "ring", @@ -2105,7 +1845,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", ] [[package]] @@ -2120,21 +1860,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -2157,29 +1882,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "0.9.0" @@ -2251,18 +1953,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_with" version = "1.14.0" @@ -2285,19 +1975,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "serde_yaml" -version = "0.9.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" -dependencies = [ - "indexmap 2.0.0", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - [[package]] name = "serial_test" version = "2.0.0" @@ -2331,20 +2008,7 @@ checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", + "digest", ] [[package]] @@ -2355,7 +2019,7 @@ checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.7", + "digest", ] [[package]] @@ -2378,9 +2042,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -2542,20 +2206,9 @@ dependencies = [ [[package]] name = "time" -version = "0.1.45" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07" dependencies = [ "deranged", "itoa", @@ -2572,9 +2225,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "75c65469ed6b3a4809d987a41eb1dc918e9bc1d92211cbad7ae82931846f7451" dependencies = [ "time-core", ] @@ -2624,12 +2277,14 @@ dependencies = [ ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" +name = "tokio-openssl" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "c08f9ffb7809f1b20c1b398d92acf4cc719874b3b2b2d9ea2f09b4a80350878a" dependencies = [ - "native-tls", + "futures-util", + "openssl", + "openssl-sys", "tokio", ] @@ -2895,16 +2550,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" -[[package]] -name = "universal-hash" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b2c654932e3e4f9196e69d08fdf7cfd718e1dc6f66b347e6024a0c961402" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "universal-hash" version = "0.5.1" @@ -2915,12 +2560,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "unsafe-libyaml" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" - [[package]] name = "untrusted" version = "0.7.1" @@ -2929,9 +2568,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna 0.4.0", @@ -2975,12 +2614,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3012,18 +2645,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.87" @@ -3130,9 +2751,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1eeca1c172a285ee6c2c84c341ccea837e7c01b12fbb2d0fe3c9e550ce49ec8" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -3145,64 +2766,55 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10d0c968ba7f6166195e13d593af609ec2e3d24f916f081690695cf5eaffb2f" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "571d8d4e62f26d4932099a9efe89660e8bd5087775a2ab5cdd8b747b811f1058" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2229ad223e178db5fbbc8bd8d3835e51e566b8474bfca58d2e6150c48bb723cd" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600956e2d840c194eedfc5d18f8242bc2e17c7775b6684488af3a9fff6fe3287" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea99ff3f8b49fb7a8e0d305e5aec485bd068c2ba691b6e277d29eaeac945868a" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1a05a1ece9a7a0d5a7ccf30ba2c33e3a61a30e042ffd247567d1de1d94120d" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.48.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d419259aba16b663966e29e6d7c6ecfa0bb8425818bb96f6f1f3c3eb71a6e7b9" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.12" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83817bbecf72c73bad717ee86820ebf286203d2e04c3951f3cd538869c897364" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.50.0" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/logging-service/Cargo.toml index 8b30e03..586c25a 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/logging-service/Cargo.toml @@ -9,9 +9,8 @@ edition = "2021" [dependencies] biscuit = "0.6.0" -core-lib = { path = "../core-lib" } chrono = { version = "0.4.26", features = ["serde", "clock", "std"], default-features = false } -mongodb ="2" +mongodb = { version = ">= 2.6.1" , features = ["openssl-tls"]} percent-encoding = "2.1.0" rocket = { version = "0.5.0-rc.1", features = ["json"] } # Restricted version to 1.0.171, because of a change in derive macro for serde. It introduced precompiled binary files diff --git a/clearing-house-app/logging-service/src/model/crypto.rs b/clearing-house-app/logging-service/src/model/crypto.rs index 9ae092c..f191556 100644 --- a/clearing-house-app/logging-service/src/model/crypto.rs +++ b/clearing-house-app/logging-service/src/model/crypto.rs @@ -1,8 +1,8 @@ use crate::crypto::generate_random_seed; -use core_lib::model::new_uuid; use hkdf::Hkdf; use sha2::Sha256; use std::collections::HashMap; +use crate::model::util::new_uuid; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct MasterKey { From 509052b40bc52b5ca571419494dcd0737a36eef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 29 Aug 2023 12:04:24 +0200 Subject: [PATCH 045/183] refactor(ch-app): Clean up old sub-projects --- .../{logging-service => }/Cargo.lock | 68 +-- .../{logging-service => }/Cargo.toml | 2 +- .../{logging-service => }/Rocket.toml | 0 .../{logging-service => }/config.toml | 0 clearing-house-app/core-lib/Cargo.toml | 32 -- clearing-house-app/core-lib/certs | 1 - clearing-house-app/core-lib/config.yml | 6 - .../core-lib/src/api/client/document_api.rs | 143 ------- .../core-lib/src/api/client/keyring_api.rs | 101 ----- .../core-lib/src/api/client/mod.rs | 96 ----- clearing-house-app/core-lib/src/api/crypto.rs | 169 -------- clearing-house-app/core-lib/src/api/mod.rs | 75 ---- clearing-house-app/core-lib/src/constants.rs | 87 ---- clearing-house-app/core-lib/src/db/mod.rs | 22 - .../core-lib/src/db/public_db.rs | 176 -------- clearing-house-app/core-lib/src/lib.rs | 35 -- .../core-lib/src/model/crypto.rs | 80 ---- .../core-lib/src/model/document.rs | 289 ------------- clearing-house-app/core-lib/src/model/mod.rs | 96 ----- .../core-lib/src/model/process.rs | 14 - .../core-lib/src/model/tests.rs | 176 -------- clearing-house-app/core-lib/src/util.rs | 79 ---- .../integration/blockchain_api_client.rs | 42 -- .../tests/integration/daps_api_client.rs | 31 -- .../tests/integration/database_client.rs | 191 --------- .../tests/integration/document_api_client.rs | 228 ---------- .../tests/integration/keyring_api_client.rs | 91 ---- .../core-lib/tests/integration/main.rs | 92 ---- .../tests/integration/token_validation.rs | 59 --- clearing-house-app/document-api/Cargo.toml | 26 -- clearing-house-app/document-api/Rocket.toml | 20 - clearing-house-app/document-api/certs | 1 - .../document-api/src/db/bucket.rs | 53 --- clearing-house-app/document-api/src/db/mod.rs | 393 ------------------ .../document-api/src/db/tests.rs | 193 --------- .../document-api/src/doc_api.rs | 312 -------------- clearing-house-app/document-api/src/main.rs | 76 ---- .../init_db/default_doc_type.json | 0 clearing-house-app/keyring-api/Cargo.toml | 32 -- clearing-house-app/keyring-api/Rocket.toml | 18 - clearing-house-app/keyring-api/certs | 1 - .../keyring-api/src/api/doc_type_api.rs | 106 ----- .../keyring-api/src/api/key_api.rs | 173 -------- clearing-house-app/keyring-api/src/api/mod.rs | 2 - clearing-house-app/keyring-api/src/crypto.rs | 161 ------- .../keyring-api/src/db/crypto.rs | 33 -- .../keyring-api/src/db/doc_type.rs | 87 ---- clearing-house-app/keyring-api/src/db/mod.rs | 176 -------- .../keyring-api/src/db/tests.rs | 110 ----- clearing-house-app/keyring-api/src/main.rs | 28 -- .../keyring-api/src/model/crypto.rs | 29 -- .../keyring-api/src/model/doc_type.rs | 29 -- .../keyring-api/src/model/mod.rs | 2 - clearing-house-app/keyring-api/src/tests.rs | 117 ------ .../{logging-service => }/keys/.DS_Store | Bin .../keys/private_key.der | Bin .../keys/private_key_2048.der | Bin clearing-house-app/logging-service/certs | 1 - .../init_db/default_doc_type.json | 42 -- .../{logging-service => }/src/config.rs | 0 .../{logging-service => }/src/crypto.rs | 0 .../src/db/config/doc_store.rs | 0 .../src/db/config/keyring_store.rs | 0 .../src/db/config/mod.rs | 0 .../src/db/config/process_store.rs | 0 .../{logging-service => }/src/db/doc_store.rs | 0 .../{logging-service => }/src/db/key_store.rs | 0 .../{logging-service => }/src/db/mod.rs | 0 .../src/db/process_store.rs | 0 .../{logging-service => }/src/main.rs | 0 .../{logging-service => }/src/model/claims.rs | 0 .../src/model/constants.rs | 0 .../{logging-service => }/src/model/crypto.rs | 0 .../src/model/doc_type.rs | 0 .../src/model/document.rs | 0 .../{logging-service => }/src/model/errors.rs | 0 .../src/model/ids/message.rs | 0 .../src/model/ids/mod.rs | 0 .../src/model/ids/request.rs | 0 .../{logging-service => }/src/model/mod.rs | 0 .../src/model/process.rs | 0 .../{logging-service => }/src/model/util.rs | 0 .../src/ports/doc_type_api.rs | 0 .../src/ports/logging_api.rs | 0 .../{logging-service => }/src/ports/mod.rs | 0 .../src/services/document_service.rs | 0 .../src/services/keyring_service.rs | 0 .../src/services/logging_service.rs | 0 .../{logging-service => }/src/services/mod.rs | 0 .../{logging-service => }/src/util.rs | 0 90 files changed, 35 insertions(+), 4667 deletions(-) rename clearing-house-app/{logging-service => }/Cargo.lock (99%) rename clearing-house-app/{logging-service => }/Cargo.toml (98%) rename clearing-house-app/{logging-service => }/Rocket.toml (100%) rename clearing-house-app/{logging-service => }/config.toml (100%) delete mode 100644 clearing-house-app/core-lib/Cargo.toml delete mode 120000 clearing-house-app/core-lib/certs delete mode 100644 clearing-house-app/core-lib/config.yml delete mode 100644 clearing-house-app/core-lib/src/api/client/document_api.rs delete mode 100644 clearing-house-app/core-lib/src/api/client/keyring_api.rs delete mode 100644 clearing-house-app/core-lib/src/api/client/mod.rs delete mode 100644 clearing-house-app/core-lib/src/api/crypto.rs delete mode 100644 clearing-house-app/core-lib/src/api/mod.rs delete mode 100644 clearing-house-app/core-lib/src/constants.rs delete mode 100644 clearing-house-app/core-lib/src/db/mod.rs delete mode 100644 clearing-house-app/core-lib/src/db/public_db.rs delete mode 100644 clearing-house-app/core-lib/src/lib.rs delete mode 100644 clearing-house-app/core-lib/src/model/crypto.rs delete mode 100644 clearing-house-app/core-lib/src/model/document.rs delete mode 100644 clearing-house-app/core-lib/src/model/mod.rs delete mode 100644 clearing-house-app/core-lib/src/model/process.rs delete mode 100644 clearing-house-app/core-lib/src/model/tests.rs delete mode 100644 clearing-house-app/core-lib/src/util.rs delete mode 100644 clearing-house-app/core-lib/tests/integration/blockchain_api_client.rs delete mode 100644 clearing-house-app/core-lib/tests/integration/daps_api_client.rs delete mode 100644 clearing-house-app/core-lib/tests/integration/database_client.rs delete mode 100644 clearing-house-app/core-lib/tests/integration/document_api_client.rs delete mode 100644 clearing-house-app/core-lib/tests/integration/keyring_api_client.rs delete mode 100644 clearing-house-app/core-lib/tests/integration/main.rs delete mode 100644 clearing-house-app/core-lib/tests/integration/token_validation.rs delete mode 100644 clearing-house-app/document-api/Cargo.toml delete mode 100644 clearing-house-app/document-api/Rocket.toml delete mode 120000 clearing-house-app/document-api/certs delete mode 100644 clearing-house-app/document-api/src/db/bucket.rs delete mode 100644 clearing-house-app/document-api/src/db/mod.rs delete mode 100644 clearing-house-app/document-api/src/db/tests.rs delete mode 100644 clearing-house-app/document-api/src/doc_api.rs delete mode 100644 clearing-house-app/document-api/src/main.rs rename clearing-house-app/{keyring-api => }/init_db/default_doc_type.json (100%) delete mode 100644 clearing-house-app/keyring-api/Cargo.toml delete mode 100644 clearing-house-app/keyring-api/Rocket.toml delete mode 120000 clearing-house-app/keyring-api/certs delete mode 100644 clearing-house-app/keyring-api/src/api/doc_type_api.rs delete mode 100644 clearing-house-app/keyring-api/src/api/key_api.rs delete mode 100644 clearing-house-app/keyring-api/src/api/mod.rs delete mode 100644 clearing-house-app/keyring-api/src/crypto.rs delete mode 100644 clearing-house-app/keyring-api/src/db/crypto.rs delete mode 100644 clearing-house-app/keyring-api/src/db/doc_type.rs delete mode 100644 clearing-house-app/keyring-api/src/db/mod.rs delete mode 100644 clearing-house-app/keyring-api/src/db/tests.rs delete mode 100644 clearing-house-app/keyring-api/src/main.rs delete mode 100644 clearing-house-app/keyring-api/src/model/crypto.rs delete mode 100644 clearing-house-app/keyring-api/src/model/doc_type.rs delete mode 100644 clearing-house-app/keyring-api/src/model/mod.rs delete mode 100644 clearing-house-app/keyring-api/src/tests.rs rename clearing-house-app/{logging-service => }/keys/.DS_Store (100%) rename clearing-house-app/{logging-service => }/keys/private_key.der (100%) rename clearing-house-app/{logging-service => }/keys/private_key_2048.der (100%) delete mode 120000 clearing-house-app/logging-service/certs delete mode 100644 clearing-house-app/logging-service/init_db/default_doc_type.json rename clearing-house-app/{logging-service => }/src/config.rs (100%) rename clearing-house-app/{logging-service => }/src/crypto.rs (100%) rename clearing-house-app/{logging-service => }/src/db/config/doc_store.rs (100%) rename clearing-house-app/{logging-service => }/src/db/config/keyring_store.rs (100%) rename clearing-house-app/{logging-service => }/src/db/config/mod.rs (100%) rename clearing-house-app/{logging-service => }/src/db/config/process_store.rs (100%) rename clearing-house-app/{logging-service => }/src/db/doc_store.rs (100%) rename clearing-house-app/{logging-service => }/src/db/key_store.rs (100%) rename clearing-house-app/{logging-service => }/src/db/mod.rs (100%) rename clearing-house-app/{logging-service => }/src/db/process_store.rs (100%) rename clearing-house-app/{logging-service => }/src/main.rs (100%) rename clearing-house-app/{logging-service => }/src/model/claims.rs (100%) rename clearing-house-app/{logging-service => }/src/model/constants.rs (100%) rename clearing-house-app/{logging-service => }/src/model/crypto.rs (100%) rename clearing-house-app/{logging-service => }/src/model/doc_type.rs (100%) rename clearing-house-app/{logging-service => }/src/model/document.rs (100%) rename clearing-house-app/{logging-service => }/src/model/errors.rs (100%) rename clearing-house-app/{logging-service => }/src/model/ids/message.rs (100%) rename clearing-house-app/{logging-service => }/src/model/ids/mod.rs (100%) rename clearing-house-app/{logging-service => }/src/model/ids/request.rs (100%) rename clearing-house-app/{logging-service => }/src/model/mod.rs (100%) rename clearing-house-app/{logging-service => }/src/model/process.rs (100%) rename clearing-house-app/{logging-service => }/src/model/util.rs (100%) rename clearing-house-app/{logging-service => }/src/ports/doc_type_api.rs (100%) rename clearing-house-app/{logging-service => }/src/ports/logging_api.rs (100%) rename clearing-house-app/{logging-service => }/src/ports/mod.rs (100%) rename clearing-house-app/{logging-service => }/src/services/document_service.rs (100%) rename clearing-house-app/{logging-service => }/src/services/keyring_service.rs (100%) rename clearing-house-app/{logging-service => }/src/services/logging_service.rs (100%) rename clearing-house-app/{logging-service => }/src/services/mod.rs (100%) rename clearing-house-app/{logging-service => }/src/util.rs (100%) diff --git a/clearing-house-app/logging-service/Cargo.lock b/clearing-house-app/Cargo.lock similarity index 99% rename from clearing-house-app/logging-service/Cargo.lock rename to clearing-house-app/Cargo.lock index b6378b2..97b1b49 100644 --- a/clearing-house-app/logging-service/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -317,6 +317,40 @@ dependencies = [ "inout", ] +[[package]] +name = "clearing-house-app" +version = "0.10.0" +dependencies = [ + "aes", + "aes-gcm-siv", + "anyhow", + "base64 0.21.3", + "biscuit", + "blake2-rfc", + "chrono", + "config", + "error-chain", + "generic-array", + "hex", + "hkdf", + "mongodb", + "num-bigint", + "once_cell", + "openssh-keys", + "percent-encoding", + "rand", + "ring", + "rocket", + "serde", + "serde_json", + "serial_test", + "sha2", + "tempfile", + "tracing", + "tracing-subscriber", + "uuid", +] + [[package]] name = "config" version = "0.13.3" @@ -1078,40 +1112,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "logging-service" -version = "0.10.0" -dependencies = [ - "aes", - "aes-gcm-siv", - "anyhow", - "base64 0.21.3", - "biscuit", - "blake2-rfc", - "chrono", - "config", - "error-chain", - "generic-array", - "hex", - "hkdf", - "mongodb", - "num-bigint", - "once_cell", - "openssh-keys", - "percent-encoding", - "rand", - "ring", - "rocket", - "serde", - "serde_json", - "serial_test", - "sha2", - "tempfile", - "tracing", - "tracing-subscriber", - "uuid", -] - [[package]] name = "loom" version = "0.5.6" diff --git a/clearing-house-app/logging-service/Cargo.toml b/clearing-house-app/Cargo.toml similarity index 98% rename from clearing-house-app/logging-service/Cargo.toml rename to clearing-house-app/Cargo.toml index 586c25a..24a777d 100644 --- a/clearing-house-app/logging-service/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "logging-service" +name = "clearing-house-app" version = "0.10.0" authors = [ "Mark Gall ", diff --git a/clearing-house-app/logging-service/Rocket.toml b/clearing-house-app/Rocket.toml similarity index 100% rename from clearing-house-app/logging-service/Rocket.toml rename to clearing-house-app/Rocket.toml diff --git a/clearing-house-app/logging-service/config.toml b/clearing-house-app/config.toml similarity index 100% rename from clearing-house-app/logging-service/config.toml rename to clearing-house-app/config.toml diff --git a/clearing-house-app/core-lib/Cargo.toml b/clearing-house-app/core-lib/Cargo.toml deleted file mode 100644 index fb96dd9..0000000 --- a/clearing-house-app/core-lib/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "core-lib" -version = "0.10.0" -authors = [ - "Mark Gall ", - "Georg Bramm ", -] -edition = "2021" - -[dependencies] -aes = "0.6.0" -aes-gcm-siv = "0.9.0" -biscuit = "0.6.0" -base64 = "0.9.3" -blake2-rfc = "0.2.18" -chrono = { version = "0.4", features = ["serde"] } -error-chain = "0.12.4" -fern = "0.5" -figment = { version = "0.10", features = ["yaml", "env"] } -generic-array = "0.14.4" -hex = "0.4.2" -log = "0.4" -mongodb ="2.3.0" -num-bigint = "0.4.3" -openssh-keys = "0.5.0" -percent-encoding = "2.1.0" -reqwest = { version="0.11", features = ["default", "json", "blocking"]} -ring = "0.16.20" -rocket = { version = "0.5.0-rc.1", features = ["json"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -uuid = { version = "1.4.1", features = ["serde", "v4"] } diff --git a/clearing-house-app/core-lib/certs b/clearing-house-app/core-lib/certs deleted file mode 120000 index 36343b9..0000000 --- a/clearing-house-app/core-lib/certs +++ /dev/null @@ -1 +0,0 @@ -../certs \ No newline at end of file diff --git a/clearing-house-app/core-lib/config.yml b/clearing-house-app/core-lib/config.yml deleted file mode 100644 index ee371c3..0000000 --- a/clearing-house-app/core-lib/config.yml +++ /dev/null @@ -1,6 +0,0 @@ -database_url: 127.0.0.1 -database_port: 27017 - -document_api_url: http://localhost:8001 -keyring_api_url: http://localhost:8002 -daps_api_url: https://daps.aisec.fraunhofer.de diff --git a/clearing-house-app/core-lib/src/api/client/document_api.rs b/clearing-house-app/core-lib/src/api/client/document_api.rs deleted file mode 100644 index 1207edd..0000000 --- a/clearing-house-app/core-lib/src/api/client/document_api.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::env; -use reqwest::Client; -use reqwest::StatusCode; -use reqwest::header::{HeaderValue, CONTENT_TYPE}; -use serde_json; -use crate::api::{ApiClient, DocumentReceipt, QueryResult}; -use crate::api::crypto::create_service_token; -use crate::constants::{ROCKET_DOC_API, DOCUMENT_API_URL, SERVICE_HEADER, ENV_DOCUMENT_SERVICE_ID}; -use crate::errors::*; -use crate::model::document::Document; -use crate::model::SortingOrder; -use crate::util::url_encode; - -#[derive(Clone)] -pub struct DocumentApiClient { - uri: String, - api_service_id: String, - caller_service_id: String -} - -impl ApiClient for DocumentApiClient { - fn new(uri: &str, service_id: &str) -> DocumentApiClient { - let uri = String::from(uri); - let api_id = match env::var(ENV_DOCUMENT_SERVICE_ID){ - Ok(id) => id, - Err(_e) => { - panic!("Service ID not configured. Please configure {}", ENV_DOCUMENT_SERVICE_ID); - } - }; - DocumentApiClient { - uri, - api_service_id: api_id.to_string(), - caller_service_id: service_id.to_string() - } - } - - fn get_conf_param() -> String { - String::from(DOCUMENT_API_URL) - } -} - -impl DocumentApiClient{ - - pub async fn get_document(&self, client_id: &str, pid: &String, id: &String) -> Result>{ - let document_url = format!("{}{}/{}/{}", self.uri, ROCKET_DOC_API, url_encode(pid), url_encode(id)); - let client = Client::new(); - - let token = create_service_token(self.caller_service_id.as_str(), self.api_service_id.as_str(), client_id); - - debug!("calling {}", &document_url); - let response = client - .get(document_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .header(SERVICE_HEADER, &token) - .send().await?; - - debug!("Status Code: {}", &response.status()); - match response.status(){ - StatusCode::OK => { - let doc: Document = response.json().await?; - Ok(Some(doc)) - } - _ => Ok(None) - } - } - - pub async fn get_document_with_integrity_check(&self, client_id: &str, pid: &String, id: &String, hash: &String) -> Result{ - let document_url = format!("{}{}/{}/{}", self.uri, ROCKET_DOC_API, url_encode(pid), url_encode(id)); - let client = Client::new(); - - let token = create_service_token(self.caller_service_id.as_str(), self.api_service_id.as_str(), client_id); - - debug!("calling {}", &document_url); - let response = client - .get(document_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .header(SERVICE_HEADER, &token) - .query(&[("hash", hash.as_str())]) - .send().await?; - - debug!("Status Code: {}", &response.status()); - let doc: Document = response.json().await?; - Ok(doc) - } - - pub async fn get_documents(&self, client_id: &str, pid: &String, page: i32, size: i32, sort: SortingOrder, date_from: Option, date_to: Option) -> Result{ - let document_url = format!("{}{}/{}", self.uri, ROCKET_DOC_API, url_encode(pid)); - let client = Client::new(); - debug!("calling {}", &document_url); - - let token = create_service_token(self.caller_service_id.as_str(), self.api_service_id.as_str(), client_id); - - let mut request = client - .get(document_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .header(SERVICE_HEADER, &token) - - .query(&[("page", page)]) - .query(&[("size", size)]) - .query(&[("sort", sort)]); - - if date_from.is_some(){ - request = request.query(&[("date_from", date_from.unwrap())]); - } - - if date_to.is_some(){ - request = request.query(&[("date_to", date_to.unwrap())]); - } - - let response = request.send().await?; - - debug!("Status Code: {}", &response.status()); - let result: QueryResult = response.json().await?; - Ok(result) - } - - pub async fn create_document(&self, client_id: &str, doc: &Document) -> Result { - let document_url = format!("{}{}", self.uri, ROCKET_DOC_API); - let client = Client::new(); - - let json_data = serde_json::to_string(doc)?; - let token = create_service_token(self.caller_service_id.as_str(), self.api_service_id.as_str(), client_id); - - debug!("created jwt: {}", &token); - debug!("calling {}", &document_url); - let response = client - .post(document_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .header(SERVICE_HEADER, &token) - .body(json_data).send().await?; - - debug!("Status Code: {}", &response.status()); - match &response.status(){ - &StatusCode::CREATED => { - let receipt = response.json().await?; - println!("Payload: {:?}", receipt); - Ok(receipt) - }, - _ => bail!("Error while calling create_document(): status {} content {:?}", response.status(), response.text().await?) - } - - } - } \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/api/client/keyring_api.rs b/clearing-house-app/core-lib/src/api/client/keyring_api.rs deleted file mode 100644 index 5c946c0..0000000 --- a/clearing-house-app/core-lib/src/api/client/keyring_api.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::env; -use reqwest::Client; -use reqwest::header::{CONTENT_TYPE, HeaderValue}; -use crate::api::ApiClient; -use crate::api::crypto::create_service_token; -use crate::errors::*; -use crate::constants::{ROCKET_KEYRING_API, KEYRING_API_URL, SERVICE_HEADER, ENV_KEYRING_SERVICE_ID}; -use crate::model::crypto::{KeyMap, KeyMapListItem, KeyCtList}; - -#[derive(Clone)] -pub struct KeyringApiClient { - uri: String, - api_service_id: String, - caller_service_id: String -} - -impl ApiClient for KeyringApiClient { - - fn new(uri: &str, service_id: &str) -> KeyringApiClient { - let uri = String::from(uri); - let api_id = match env::var(ENV_KEYRING_SERVICE_ID){ - Ok(id) => id, - Err(_e) => { - panic!("Service ID not configured. Please configure {}", ENV_KEYRING_SERVICE_ID); - } - }; - KeyringApiClient { - uri, - api_service_id: api_id.to_string(), - caller_service_id: service_id.to_string() - } - } - - fn get_conf_param() -> String { - String::from(KEYRING_API_URL) - } -} - -impl KeyringApiClient { - - /// Calls the keyring api to generate new aes keys - pub async fn generate_keys(&self, client_id: &str, pid: &str, dt_id: &str) -> Result { - let keys_url = format!("{}{}/generate_keys/{}", self.uri, ROCKET_KEYRING_API, pid); - let client = Client::new(); - - let token = create_service_token(self.caller_service_id.as_str(), self.api_service_id.as_str(), client_id); - - debug!("calling {}", &keys_url); - let result = client.get(keys_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .header(SERVICE_HEADER, &token) - .query(&[("dt_id", dt_id)]) - .send().await?; - - debug!("Status Code: {}", result.status()); - let key_map: KeyMap = result.json().await?; - trace!("Payload: {:?}", key_map); - Ok(key_map) - } - - /// Calls the keyring api to decrypt aes keys - pub async fn decrypt_keys(&self, client_id: &str, pid: &str, dt_id: &str, ct: &[u8]) -> Result{ - let keys_url = format!("{}{}/decrypt_keys/{}/{}", self.uri, ROCKET_KEYRING_API, pid, hex::encode_upper(ct)); - let client = Client::new(); - - let token = create_service_token(self.caller_service_id.as_str(), self.api_service_id.as_str(), client_id); - - debug!("calling {}", &keys_url); - let result = client.get(keys_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .header(SERVICE_HEADER, &token) - .query(&[("dt_id", dt_id)]) - .send().await?; - - debug!("Status Code: {}", &result.status()); - let key_map: KeyMap = result.json().await?; - trace!("Payload: {:?}", key_map); - Ok(key_map) - } - - /// Calls the keyring api to decrypt aes keys - pub async fn decrypt_multiple_keys(&self, client_id: &str, pid: &str, cts: &KeyCtList) -> Result>{ - let keys_url = format!("{}{}/decrypt_keys/{}", self.uri, ROCKET_KEYRING_API, pid); - let client = Client::new(); - - let json_data = serde_json::to_string(cts)?; - let token = create_service_token(self.caller_service_id.as_str(), self.api_service_id.as_str(), client_id); - - debug!("calling {}", &keys_url); - let result = client.get(keys_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .header(SERVICE_HEADER, &token) - .body(json_data) - .send().await?; - - debug!("Status Code: {}", &result.status()); - let key_maps: Vec = result.json().await?; - trace!("Payload: {:?}", key_maps); - Ok(key_maps) - } -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/api/client/mod.rs b/clearing-house-app/core-lib/src/api/client/mod.rs deleted file mode 100644 index a5bcba0..0000000 --- a/clearing-house-app/core-lib/src/api/client/mod.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::env; -use rocket::fairing::{self, Fairing, Info, Kind}; -use rocket::{Rocket, Build}; -use crate::api::ApiClient; -use crate::api::client::keyring_api::KeyringApiClient; -use crate::api::client::document_api::DocumentApiClient; -use crate::constants::{ENV_DOCUMENT_SERVICE_ID, ENV_KEYRING_SERVICE_ID}; - -pub mod document_api; -pub mod keyring_api; - -#[derive(Clone, Debug)] -pub enum ApiClientEnum{ - Document, - Keyring -} - -#[derive(Clone, Debug)] -pub struct ApiClientConfigurator{ - api: ApiClientEnum, -} - -impl ApiClientConfigurator{ - pub fn new(api: ApiClientEnum) -> Self{ - ApiClientConfigurator{ - api - } - } -} - -#[rocket::async_trait] -impl Fairing for ApiClientConfigurator { - fn info(&self) -> Info { - match self.api { - ApiClientEnum::Document => { - Info { - name: "Configuring Document Api Client", - kind: Kind::Ignite - } - }, - ApiClientEnum::Keyring => { - Info { - name: "Configuring Keyring Api Client", - kind: Kind::Ignite - } - } - } - } - - async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - let config_key = match self.api { - ApiClientEnum::Document => { - debug!("Configuring Document Api Client..."); - DocumentApiClient::get_conf_param() - }, - ApiClientEnum::Keyring => { - debug!("Configuring Keyring Api Client..."); - KeyringApiClient::get_conf_param() - } - }; - let api_url: String = rocket.figment().extract_inner(&config_key).unwrap_or(String::new()); - if api_url.len() > 0 { - debug!("...found api url: {}", &api_url); - match self.api { - ApiClientEnum::Document => { - match env::var(ENV_DOCUMENT_SERVICE_ID){ - Ok(id) => { - let client: DocumentApiClient = ApiClient::new(&api_url, &id); - Ok(rocket.manage(client)) - }, - Err(_e) => { - error!("Service ID not configured. Please configure environment variable {}", ENV_DOCUMENT_SERVICE_ID); - Err(rocket) - } - } - }, - ApiClientEnum::Keyring => { - match env::var(ENV_KEYRING_SERVICE_ID){ - Ok(id) => { - let client: KeyringApiClient = ApiClient::new(&api_url, &id); - Ok(rocket.manage(client)) - }, - Err(_e) => { - error!("Service ID not configured. Please configure environment variable {}", ENV_KEYRING_SERVICE_ID); - Err(rocket) - } - } - } - } - } - else{ - error!("...api url not found in config file."); - Err(rocket) - } - } -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/api/crypto.rs b/clearing-house-app/core-lib/src/api/crypto.rs deleted file mode 100644 index f62cc09..0000000 --- a/clearing-house-app/core-lib/src/api/crypto.rs +++ /dev/null @@ -1,169 +0,0 @@ -use std::env; -use std::fmt::{Display, Formatter}; -use biscuit::{ClaimPresenceOptions, ClaimsSet, Empty, jwa::SignatureAlgorithm, JWT, RegisteredClaims, SingleOrMultiple, Timestamp, ValidationOptions}; -use biscuit::jwk::{AlgorithmParameters, CommonParameters, JWKSet}; -use biscuit::{jws, jws::Secret}; -use biscuit::Presence::Required; -use biscuit::Validation::Validate; -use chrono::{Duration, Utc}; -use num_bigint::BigUint; -use ring::signature::KeyPair; -use rocket::http::Status; -use rocket::request::{Request, FromRequest, Outcome}; -use crate::errors::*; -use crate::constants::{ENV_SHARED_SECRET, SERVICE_HEADER}; -use crate::util::ServiceConfig; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct ChClaims{ - pub client_id: String, -} - -impl ChClaims{ - pub fn new(client_id: &str) -> ChClaims{ - ChClaims{ - client_id: client_id.to_string(), - } - } -} - -impl Display for ChClaims{ - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "<{}>", self.client_id) - } -} - -#[derive(Debug)] -pub enum ChClaimsError { - Missing, - Invalid, -} - -#[rocket::async_trait] -impl<'r> FromRequest<'r> for ChClaims { - type Error = ChClaimsError; - - async fn from_request(request: &'r Request<'_>) -> Outcome { - match request.headers().get_one(&SERVICE_HEADER) { - None => Outcome::Failure((Status::BadRequest, ChClaimsError::Missing)), - Some(token) => { - debug!("...received service header: {:?}", token); - let service_config = request.rocket().state::().unwrap(); - match decode_token::(token, service_config.service_id.as_str()){ - Ok(claims) => { - debug!("...retrieved claims and succeed"); - Outcome::Success(claims) - }, - Err(_) => Outcome::Failure((Status::BadRequest, ChClaimsError::Invalid)) - } - } - } - } -} - -pub fn get_jwks(key_path: &str) -> Option>{ - let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); - - if let biscuit::jws::Secret::RsaKeyPair(a) = keypair{ - let pk_modulus = BigUint::from_bytes_be(a.as_ref().public_key().modulus().big_endian_without_leading_zero()); - let pk_e = BigUint::from_bytes_be(a.as_ref().public_key().exponent().big_endian_without_leading_zero()); - - let params = biscuit::jwk::RSAKeyParameters{ - n: pk_modulus, - e: pk_e, - ..Default::default() - }; - - let mut common = CommonParameters::default(); - common.key_id = get_fingerprint(key_path); - - let jwk = biscuit::jwk::JWK::{ - common, - algorithm: AlgorithmParameters::RSA(params), - additional: Empty::default(), - }; - - let jwks = biscuit::jwk::JWKSet::{ - keys: vec!(jwk) - }; - return Some(jwks) - } - None -} - -pub fn get_fingerprint(key_path: &str) -> Option{ - let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); - if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { - let pk_modulus = a.as_ref().public_key().modulus().big_endian_without_leading_zero().to_vec(); - let pk_e = a.as_ref().public_key().exponent().big_endian_without_leading_zero().to_vec(); - - let pk = openssh_keys::PublicKey::from_rsa(pk_e, pk_modulus); - return Some(pk.fingerprint()) - } - None -} - -pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> String{ - let private_claims = ChClaims::new(client_id); - create_token(issuer, audience, &private_claims) -} - -pub fn create_token serde::Deserialize<'de>> (issuer: &str, audience: &str, private_claims: &T) -> String{ - let signing_secret = match env::var(ENV_SHARED_SECRET){ - Ok(secret) => { - Secret::Bytes(secret.to_string().into_bytes()) - }, - Err(_) => { - panic!("Shared Secret not configured. Please configure environment variable {}", ENV_SHARED_SECRET); - } - }; - let expiration_date = Utc::now() + Duration::minutes(5); - - let claims = ClaimsSet::{ - registered: RegisteredClaims{ - issuer: Some(issuer.to_string()), - issued_at: Some(Timestamp::from(Utc::now())), - audience: Some(SingleOrMultiple::Single(audience.to_string())), - expiry: Some(Timestamp::from(expiration_date)), - ..Default::default() - }, - private: private_claims.clone() - }; - - // Construct the JWT - let jwt = jws::Compact::new_decoded( - From::from(jws::RegisteredHeader { - algorithm: SignatureAlgorithm::HS256, - ..Default::default() - }), - claims.clone() - ); - - jwt.into_encoded(&signing_secret).unwrap().unwrap_encoded().to_string() -} - -pub fn decode_token serde::Deserialize<'de>>(token: &str, audience: &str) -> Result{ - let signing_secret = match env::var(ENV_SHARED_SECRET){ - Ok(secret) => { - Secret::Bytes(secret.to_string().into_bytes()) - }, - Err(e) => { - error!("Shared Secret not configured. Please configure environment variable {}", ENV_SHARED_SECRET); - return Err(Error::from(e)) - } - }; - let jwt: jws::Compact, Empty> = JWT::<_, Empty>::new_encoded(token); - let decoded_jwt = jwt.decode(&signing_secret,SignatureAlgorithm::HS256)?; - let mut val_options = ValidationOptions::default(); - let mut claim_presence_options = ClaimPresenceOptions::default(); - claim_presence_options.expiry = Required; - claim_presence_options.issuer = Required; - claim_presence_options.audience = Required; - claim_presence_options.issued_at = Required; - val_options.claim_presence_options = claim_presence_options; - val_options.issued_at = Validate(Duration::minutes(5)); - // Issuer is not validated. Wouldn't make much of a difference if we did - val_options.audience = Validate(audience.to_string()); - assert!(decoded_jwt.validate(val_options).is_ok()); - Ok(decoded_jwt.payload().unwrap().private.clone()) -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/api/mod.rs b/clearing-house-app/core-lib/src/api/mod.rs deleted file mode 100644 index 97460a3..0000000 --- a/clearing-house-app/core-lib/src/api/mod.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::string::ToString; -use rocket::serde::json::Value; -use crate::model::document::Document; - -pub mod client; -pub mod crypto; - -pub trait ApiClient{ - fn new(url: &str, service_id: &str) -> Self; - fn get_conf_param() -> String; -} - -#[derive(Responder, Debug)] -pub enum ApiResponse { - #[response(status = 200)] - PreFlight(()), - #[response(status = 400, content_type = "text/plain")] - BadRequest(String), - #[response(status = 201, content_type = "json")] - SuccessCreate(Value), - #[response(status = 200, content_type = "json")] - SuccessOk(Value), - #[response(status = 204, content_type = "text/plain")] - SuccessNoContent(String), - #[response(status = 401, content_type = "text/plain")] - Unauthorized(String), - #[response(status = 403, content_type = "text/plain")] - Forbidden(String), - #[response(status = 404, content_type = "text/plain")] - NotFound(String), - #[response(status = 500, content_type = "text/plain")] - InternalError(String), -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct DocumentReceipt{ - pub timestamp: i64, - pub pid: String, - pub doc_id: String, - pub chain_hash: String, -} - -impl DocumentReceipt{ - pub fn new(timestamp: i64, pid: &str, doc_id: &str, chain_hash: &str) -> DocumentReceipt{ - DocumentReceipt{ - timestamp, - pid: pid.to_string(), - doc_id: doc_id.to_string(), - chain_hash: chain_hash.to_string(), - } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct QueryResult{ - pub date_from: i64, - pub date_to: i64, - pub page: Option, - pub size: Option, - pub order: String, - pub documents: Vec -} - -impl QueryResult{ - pub fn new(date_from: i64, date_to: i64, page: Option, size: Option, order: String, documents: Vec) -> QueryResult{ - QueryResult{ - date_from, - date_to, - page, - size, - order, - documents - } - } -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/constants.rs b/clearing-house-app/core-lib/src/constants.rs deleted file mode 100644 index 664477a..0000000 --- a/clearing-house-app/core-lib/src/constants.rs +++ /dev/null @@ -1,87 +0,0 @@ -// definition of daps constants -pub const DAPS_AUD: &'static str = "idsc:IDS_CONNECTORS_ALL"; -pub const DAPS_JWKS: &'static str = ".well-known/jwks.json"; -pub const DAPS_KID: &'static str = "default"; -pub const DAPS_AUTHHEADER: &'static str = "Authorization"; -pub const DAPS_AUTHBEARER: &'static str = "Bearer"; -pub const DAPS_CERTIFICATES: &'static str = "certs"; - -// definition of custom headers -pub const SERVICE_HEADER: &'static str = "CH-SERVICE"; - -// definition of config parameters (in config files) -pub const DATABASE_URL: &'static str = "database_url"; -pub const DOCUMENT_API_URL: &'static str = "document_api_url"; -pub const KEYRING_API_URL: &'static str = "keyring_api_url"; -pub const DAPS_API_URL: &'static str = "daps_api_url"; -pub const CLEAR_DB: &'static str = "clear_db"; - -// define here the config options from environment variables -pub const ENV_API_LOG_LEVEL: &'static str = "API_LOG_LEVEL"; -pub const ENV_SHARED_SECRET: &'static str = "SHARED_SECRET"; -pub const ENV_DOCUMENT_SERVICE_ID: &'static str = "SERVICE_ID_DOC"; -pub const ENV_KEYRING_SERVICE_ID: &'static str = "SERVICE_ID_KEY"; -pub const ENV_LOGGING_SERVICE_ID: &'static str = "SERVICE_ID_LOG"; - -// definition of rocket mount points -pub const ROCKET_DOC_API: &'static str = "/doc"; -pub const ROCKET_DOC_TYPE_API: &'static str = "/doctype"; -pub const ROCKET_POLICY_API: &'static str = "/policy"; -pub const ROCKET_STATISTICS: &'static str = "/statistics"; -pub const ROCKET_PROCESS_API: &'static str = "/process"; -pub const ROCKET_KEYRING_API: &'static str = "/keyring"; -pub const ROCKET_USER_API: &'static str = "/users"; - -// definition of service names -pub const DOCUMENT_DB_CLIENT: &'static str = "document-api"; -pub const KEYRING_DB_CLIENT: &'static str = "keyring-api"; -pub const PROCESS_DB_CLIENT: &'static str = "logging-service"; - -// definition of table names -pub const MONGO_DB: &'static str = "ch_ids"; -pub const DOCUMENT_DB: &'static str = "document"; -pub const KEYRING_DB: &'static str = "keyring"; -pub const PROCESS_DB: &'static str = "process"; -pub const MONGO_COLL_DOCUMENTS: &'static str = "documents"; -pub const MONGO_COLL_DOCUMENT_BUCKET: &'static str = "document_bucket"; -pub const MONGO_COLL_DOC_TYPES: &'static str = "doc_types"; -pub const MONGO_COLL_DOC_PARTS: &'static str = "parts"; -pub const MONGO_COLL_PROCESSES: &'static str = "processes"; -pub const MONGO_COLL_TRANSACTIONS: &'static str = "transactions"; -pub const MONGO_COLL_MASTER_KEY: &'static str = "keys"; - -// definition of database fields -pub const MONGO_ID: &'static str = "id"; -pub const MONGO_MKEY: &'static str = "msk"; -pub const MONGO_PID: &'static str = "pid"; -pub const MONGO_DT_ID: &'static str = "dt_id"; -pub const MONGO_NAME: &'static str = "name"; -pub const MONGO_OWNER: &'static str = "owner"; -pub const MONGO_TS: &'static str = "ts"; -pub const MONGO_TC: &'static str = "tc"; - -pub const MONGO_DOC_ARRAY: &'static str = "documents"; -pub const MONGO_COUNTER: &'static str = "counter"; -pub const MONGO_FROM_TS: &'static str = "from_ts"; -pub const MONGO_TO_TS: &'static str = "to_ts"; - -// definition of default database values -pub const DEFAULT_PROCESS_ID: &'static str = "default"; -pub const MAX_NUM_RESPONSE_ENTRIES: u64 = 1000; -pub const DEFAULT_NUM_RESPONSE_ENTRIES: u64 = 100; - -pub const DEFAULT_DOC_TYPE: &'static str = "IDS_MESSAGE"; - -// split string symbols for vec_to_string and string_to_vec -pub const SPLIT_QUOTE: &'static str = "'"; -pub const SPLIT_SIGN: &'static str = "~"; -pub const SPLIT_CT: &'static str = "::"; - -// definition of file names and folders -pub const FOLDER_DB: &'static str = "db_init"; -pub const FOLDER_DATA: &'static str = "data"; -pub const FILE_DOC: &'static str = "document.json"; -pub const FILE_DEFAULT_DOC_TYPE: &'static str = "init_db/default_doc_type.json"; - -// definition of special document parts -pub const PAYLOAD_PART: &'static str = "payload"; \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/db/mod.rs b/clearing-house-app/core-lib/src/db/mod.rs deleted file mode 100644 index 6d9db45..0000000 --- a/clearing-house-app/core-lib/src/db/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -use mongodb::Client; -use mongodb::options::ClientOptions; -use crate::errors::*; - -pub trait DataStoreApi{ - fn new(client: Client) -> Self; -} - -pub async fn init_database_client(db_url: &str, client_name: Option) -> Result{ - let mut client_options; - - match ClientOptions::parse(&format!("{}", db_url)).await{ - Ok(co) => {client_options = co;} - Err(_) => { - bail!("Can't parse database connection string"); - } - }; - - client_options.app_name = client_name; - let client = Client::with_options(client_options)?; - Ok(T::new(client)) -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/db/public_db.rs b/clearing-house-app/core-lib/src/db/public_db.rs deleted file mode 100644 index 864ec45..0000000 --- a/clearing-house-app/core-lib/src/db/public_db.rs +++ /dev/null @@ -1,176 +0,0 @@ -use mongodb::{ - bson::Bson, - db::ThreadedDatabase, - doc, - coll::options::FindOneAndUpdateOptions -}; -use crate::constants::{MONGO_ID, MONGO_PID, MONGO_DT_ID, MONGO_COLL_DOCUMENTS}; -use crate::db::DataStore; -use crate::errors::*; -use crate::model::document::EncryptedDocument; -use rocket_contrib::json; - -impl DataStore { - - // DOCUMENT - - pub fn add_document(&self, doc: EncryptedDocument) -> Result { - // The model collection - let coll = self.database.collection(MONGO_COLL_DOCUMENTS); - println!("add_document({:?})", json!(doc)); - let serialized_bson = mongodb::to_bson(&doc)?; - match serialized_bson.as_document(){ - Some(document) => { - match coll.insert_one(document.clone(), None) { - Ok(res) => { - println!("inserted document: acknowledged:{:?} inserted_id:{:?}", res.acknowledged, res.inserted_id); - Ok(true) - }, - Err(e) => { - bail!("error_ insertion of document failed: {}", e); - } - } - }, - _ => bail!("conversion to document failed!"), - } - } - - /// deletes model from db - pub fn delete_document(&self, id: &String) -> Result { - // The model collection - debug!("trying to delete entry with id '{}'", id); - let coll = self.database.collection(MONGO_COLL_DOCUMENTS); - let result = coll.delete_one(doc! { MONGO_ID: id }, None)?; - if result.deleted_count == 1{ - Ok(true) - } - else{ - debug!("deleted_count={}", result.deleted_count); - Ok(false) - } - } - - /// checks if the document exists - /// document ids are globally unique - pub fn exists_document(&self, id: &String) -> Result { - // The model collection - let coll = self.database.collection(MONGO_COLL_DOCUMENTS); - let result = coll.find_one(Some(doc! { MONGO_ID: id.clone() }), None)?; - match result { - Some(_r) => Ok(true), - None => { - debug!("document with id '{}' does not exist!", &id); - Ok(false) - } - } - } - - /// gets the model from the db - pub fn get_document(&self, id: &String, pid: &String) -> Result> { - debug!("Looking for doc: {}", &id); - // The model collection - let coll = self.database.collection(MONGO_COLL_DOCUMENTS); - let result = coll.find_one(Some(doc! { MONGO_ID: id.clone(), MONGO_PID: pid.clone() }), None)?; - - match result { - Some(r) => { - let doc = mongodb::from_bson::(Bson::Document(r))?; - Ok(Some(doc)) - }, - None => { - Ok(None) - } - } - } - - /// gets documents for a single process from the db - pub fn get_documents_for_pid(&self, pid: &String) -> Result> { - // The model collection - let coll = self.database.collection(MONGO_COLL_DOCUMENTS); - // Create cursor that finds all documents - let mut cursor = coll.find(Some(doc! { MONGO_PID: pid.clone() }), None)?; - let mut result = vec!(); - - loop{ - if cursor.has_next()?{ - // we checked has_next() so unwrap() is safe to get to the Result - let d = cursor.next().unwrap()?; - let doc = mongodb::from_bson::(Bson::Document(d))?; - result.push(doc); - } - else{ - break; - } - } - Ok(result) - } - - /// gets documents of a specific document type for a single process from the db - pub fn get_documents_of_dt_for_pid(&self, dt_id: &String, pid: &String) -> Result> { - // The model collection - let coll = self.database.collection(MONGO_COLL_DOCUMENTS); - // Create cursor that finds all documents - let mut cursor = coll.find(Some(doc! { MONGO_PID: pid.clone(), MONGO_DT_ID: dt_id.clone() }), None)?; - let mut result = vec!(); - - loop{ - if cursor.has_next()?{ - // we checked has_next() so unwrap() is safe to get to the Result - let d = cursor.next().unwrap()?; - let doc = mongodb::from_bson::(Bson::Document(d))?; - result.push(doc); - } - else{ - break; - } - } - Ok(result) - } - - /// gets all documents from the db - pub fn get_all_documents(&self) -> Result> { - // The model collection - let coll = self.database.collection(MONGO_COLL_DOCUMENTS); - // Create cursor that finds all documents - let mut cursor = coll.find(None, None)?; - let mut result = vec!(); - - loop{ - if cursor.has_next()?{ - // we checked has_next() so unwrap() is safe to get to the Result - let d = cursor.next().unwrap()?; - let doc = mongodb::from_bson::(Bson::Document(d))?; - result.push(doc); - } - else{ - break; - } - } - Ok(result) - } - - /// update existing model in the db - pub fn update_document(&self, doc: EncryptedDocument) -> Result { - // The model collection - let coll = self.database.collection(MONGO_COLL_DOCUMENTS); - let serialized_doc = mongodb::to_bson(&doc).unwrap(); // Serialize - - let mut options = FindOneAndUpdateOptions::new(); - options.upsert = Some(true); - - let result = coll.find_one_and_replace(doc! { MONGO_ID: doc.id.clone() }, - serialized_doc.as_document().unwrap().clone(), - Some(options))?; - match result { - Some(r) => { - let old_doc = mongodb::from_bson::(Bson::Document(r))?; - debug!("old model type was: {}", &old_doc.id); - Ok(true) - }, - None => { - warn!("model type with id {} could not be updated!", &doc.id); - Ok(false) - } - } - } -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/lib.rs b/clearing-house-app/core-lib/src/lib.rs deleted file mode 100644 index 5fbc789..0000000 --- a/clearing-house-app/core-lib/src/lib.rs +++ /dev/null @@ -1,35 +0,0 @@ -#![forbid(unsafe_code)] - -extern crate biscuit; -extern crate chrono; -extern crate fern; -extern crate mongodb; -#[macro_use] extern crate rocket; - -#[macro_use] extern crate error_chain; -pub mod errors { - // Create the Error, ErrorKind, ResultExt, and Result types - error_chain!{ - foreign_links { - Conversion(std::num::TryFromIntError); - Figment(figment::Error); - HexError(hex::FromHexError); - Io(::std::io::Error) #[cfg(unix)]; - Mongodb(mongodb::error::Error); - MongodbBson(mongodb::bson::de::Error); - SetLogger(log::SetLoggerError); - ParseLogLevel(log::ParseLevelError); - Reqwest(reqwest::Error); - SerdeJson(serde_json::error::Error); - Uft8Error(std::string::FromUtf8Error); - BiscuitError(biscuit::errors::Error); - EnvVariable(::std::env::VarError); - } - } -} - -pub mod api; -pub mod constants; -pub mod db; -pub mod model; -pub mod util; diff --git a/clearing-house-app/core-lib/src/model/crypto.rs b/clearing-house-app/core-lib/src/model/crypto.rs deleted file mode 100644 index baaea10..0000000 --- a/clearing-house-app/core-lib/src/model/crypto.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::collections::HashMap; - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyEntry { - pub id: String, - pub key: Vec, - pub nonce: Vec, -} - -impl KeyEntry{ - pub fn new(id: String, key: Vec, nonce: Vec)-> KeyEntry{ - KeyEntry{ - id, - key, - nonce - } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyMap { - pub enc: bool, - pub keys: HashMap, - pub keys_enc: Option>, -} - -impl KeyMap{ - pub fn new(enc: bool, keys: HashMap, keys_enc: Option>) -> KeyMap{ - KeyMap{ - enc, - keys, - keys_enc - } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyCt{ - pub id: String, - pub ct: String -} - -impl KeyCt{ - pub fn new(id: String, ct: String) -> KeyCt{ - KeyCt{ - id, - ct - } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyCtList { - pub dt: String, - pub cts: Vec -} - -impl KeyCtList{ - pub fn new(dt: String, cts: Vec) -> KeyCtList{ - KeyCtList{ - dt, - cts - } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyMapListItem { - pub id: String, - pub map: KeyMap -} - -impl KeyMapListItem{ - pub fn new(id: String, map: KeyMap) -> KeyMapListItem{ - KeyMapListItem{ - id, - map - } - } -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/model/document.rs b/clearing-house-app/core-lib/src/model/document.rs deleted file mode 100644 index 59fd2e2..0000000 --- a/clearing-house-app/core-lib/src/model/document.rs +++ /dev/null @@ -1,289 +0,0 @@ -use aes_gcm_siv::Aes256GcmSiv; -use aes_gcm_siv::aead::{Aead, NewAead}; -use blake2_rfc::blake2b::Blake2b; -use generic_array::GenericArray; -use std::collections::HashMap; -use uuid::Uuid; -use crate::errors::*; -use crate::constants::{SPLIT_CT}; -use crate::model::new_uuid; -use crate::model::crypto::{KeyEntry, KeyMap}; -use chrono::Local; - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct DocumentPart { - pub name: String, - pub content: Option, -} - -impl DocumentPart{ - pub fn new(name: String, content: Option) -> DocumentPart{ - DocumentPart{ - name, - content, - } - } - - pub fn encrypt(&self, key: &[u8], nonce: &[u8]) -> Result>{ - const EXP_KEY_SIZE: usize = 32; - const EXP_NONCE_SIZE: usize = 12; - // check key size - if key.len() != EXP_KEY_SIZE { - error!("Given key has size {} but expected {} bytes", key.len(), EXP_KEY_SIZE); - bail!("Incorrect key size") - } - // check nonce size - else if nonce.len() != EXP_NONCE_SIZE { - error!("Given nonce has size {} but expected {} bytes", nonce.len(), EXP_NONCE_SIZE); - bail!("Incorrect nonce size") - } - else{ - let key = GenericArray::from_slice(key); - let nonce = GenericArray::from_slice(nonce); - let cipher = Aes256GcmSiv::new(key); - - match &self.content{ - Some(pt) => { - let pt = format_pt_for_storage(&self.name, pt); - match cipher.encrypt(nonce, pt.as_bytes()){ - Ok(ct) => Ok(ct), - Err(e) => bail!("Error while encrypting {}", e) - } - }, - None => { - error!("Tried to encrypt empty document part."); - bail!("Nothing to encrypt"); - } - } - } - } - - pub fn decrypt(key: &[u8], nonce: &[u8], ct: &[u8]) -> Result{ - let key = GenericArray::from_slice(key); - let nonce = GenericArray::from_slice(nonce); - let cipher = Aes256GcmSiv::new(key); - - match cipher.decrypt(nonce, ct){ - Ok(pt) => { - let pt = String::from_utf8(pt)?; - let (name, content) = restore_pt_no_dt(&pt)?; - Ok(DocumentPart::new(name, Some(content))) - }, - Err(e) => { - bail!("Error while decrypting: {}", e) - } - } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct Document { - #[serde(default = "new_uuid")] - pub id: String, - pub dt_id: String, - pub pid: String, - pub ts: i64, - pub tc: i64, - pub parts: Vec, -} - -/// Documents should have a globally unique id, setting the id manually is discouraged. -impl Document{ - pub fn create_uuid() -> String{ - Uuid::new_v4().hyphenated().to_string() - } - - // each part is encrypted using the part specific key from the key map - // the hash is set to "0". Chaining is not done here. - pub fn encrypt(&self, key_map: KeyMap) -> Result { - debug!("encrypting document of doc_type {}", self.dt_id); - let mut cts = vec!(); - - let keys = key_map.keys; - let key_ct; - match key_map.keys_enc{ - Some(ct) => { - key_ct = hex::encode(ct); - }, - None => { - bail!("Missing key ct"); - } - } - - for part in self.parts.iter() { - if part.content.is_none(){ - // no content, so we skip this one - continue; - } - // check if there's a key for this part - if !keys.contains_key(&part.name){ - error!("Missing key for part '{}'", &part.name); - bail!("Missing key for part '{}'", &part.name); - } - // get the key for this part - let key_entry = keys.get(&part.name).unwrap(); - let ct = part.encrypt(key_entry.key.as_slice(), key_entry.nonce.as_slice()); - if ct.is_err(){ - warn!("Encryption error. No ct received!"); - bail!("Encryption error. No ct received!"); - } - let ct_string = hex::encode_upper(ct.unwrap()); - - // key entry id is needed for decryption - cts.push(format!("{}::{}", key_entry.id, ct_string)); - } - cts.sort(); - - Ok(EncryptedDocument::new(self.id.clone(), self.pid.clone(), self.dt_id.clone(), self.ts, self.tc, key_ct, cts)) - } - - pub fn get_formatted_tc(&self) -> String{ - format_tc(self.tc) - } - - pub fn get_parts_map(&self) -> HashMap>{ - let mut p_map = HashMap::new(); - for part in self.parts.iter(){ - p_map.insert(part.name.clone(), part.content.clone()); - } - p_map - } - - pub fn new(pid: String, dt_id: String, tc: i64, parts: Vec) -> Document{ - Document{ - id: Document::create_uuid(), - dt_id, - pid, - ts: Local::now().timestamp(), - tc, - parts, - } - } - - fn restore(id: String, pid: String, dt_id: String, ts: i64, tc: i64, parts: Vec) -> Document{ - Document{ - id, - dt_id, - pid, - ts, - tc, - parts, - } - } -} - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct EncryptedDocument { - pub id: String, - pub pid: String, - pub dt_id: String, - pub ts: i64, - pub tc: i64, - pub hash: String, - pub keys_ct: String, - pub cts: Vec, -} - -impl EncryptedDocument{ - - /// Note: KeyMap keys need to be KeyEntry.ids in this case - // Decryption is done without checking the hashes. Do this before calling this method - pub fn decrypt(&self, keys: HashMap) -> Result{ - - let mut pts = vec!(); - for ct in self.cts.iter(){ - let ct_parts = ct.split(SPLIT_CT).collect::>(); - if ct_parts.len() != 2 { - bail!("Integrity violation! Ciphertexts modified"); - } - // get key and nonce - let key_entry = keys.get(ct_parts[0]); - if key_entry.is_none(){ - bail!("Key for id '{}' does not exist!", ct_parts[0]); - } - let key = key_entry.unwrap().key.as_slice(); - let nonce = key_entry.unwrap().nonce.as_slice(); - - // get ciphertext - //TODO: use error_chain? - let ct = hex::decode(ct_parts[1]).unwrap(); - - // decrypt - match DocumentPart::decrypt(key, nonce, ct.as_slice()){ - Ok(part) => pts.push(part), - Err(e) => { - bail!("Error while decrypting: {}", e); - } - } - } - - Ok(Document::restore(self.id.clone(), self.pid.clone(), self.dt_id.clone(), self.ts, self.tc, pts)) - } - - pub fn get_formatted_tc(&self) -> String{ - format_tc(self.tc) - } - - pub fn hash(&self) -> String{ - let mut hasher = Blake2b::new(64); - - hasher.update(self.id.as_bytes()); - hasher.update(self.pid.as_bytes()); - hasher.update(self.dt_id.as_bytes()); - hasher.update(self.get_formatted_tc().as_bytes()); - hasher.update(self.ts.to_string().as_bytes()); - hasher.update(self.hash.as_bytes()); - hasher.update(self.keys_ct.as_bytes()); - let mut cts = self.cts.clone(); - cts.sort(); - for ct in cts.iter() { - hasher.update(ct.as_bytes()); - } - - let res = base64::encode(&hasher.finalize()); - debug!("hashed cts: '{}'", &res); - res - } - - pub fn new(id: String, pid: String, dt_id: String, ts: i64, tc: i64, keys_ct: String, cts: Vec) -> EncryptedDocument { - EncryptedDocument{ - id, - pid, - dt_id, - ts, - tc, - hash: String::from("0"), - keys_ct, - cts, - } - } -} - -/// companion to format_pt_for_storage -pub fn restore_pt(pt: &str) -> Result<(String, String, String)> { - trace!("Trying to restore plain text"); - let vec: Vec<&str> = pt.split(SPLIT_CT).collect(); - if vec.len() != 3{ - bail!("Could not restore plaintext"); - } - Ok((String::from(vec[0]), String::from(vec[1]), String::from(vec[2]))) -} - -/// companion to format_pt_for_storage_no_dt -pub fn restore_pt_no_dt(pt: &str) -> Result<(String, String)> { - trace!("Trying to restore plain text"); - let vec: Vec<&str> = pt.split(SPLIT_CT).collect(); - if vec.len() != 2{ - bail!("Could not restore plaintext"); - } - Ok((String::from(vec[0]), String::from(vec[1]))) -} - -/// formats the pt before encryption -fn format_pt_for_storage(field_name: &str, pt: &str) -> String { - format!("{}{}{}", field_name, SPLIT_CT, pt) -} - -fn format_tc(tc: i64) -> String{ - format!("{:08}", tc) -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/model/mod.rs b/clearing-house-app/core-lib/src/model/mod.rs deleted file mode 100644 index b31d90c..0000000 --- a/clearing-house-app/core-lib/src/model/mod.rs +++ /dev/null @@ -1,96 +0,0 @@ -pub mod crypto; -pub mod document; -pub mod process; - -#[cfg(test)] -mod tests; - -pub fn new_uuid() -> String { - use uuid::Uuid; - Uuid::new_v4().hyphenated().to_string() -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, FromFormField)] -pub enum SortingOrder { - #[field(value = "asc")] - #[serde(rename = "asc")] - Ascending, - #[field(value = "desc")] - #[serde(rename = "desc")] - Descending, -} - -pub fn parse_date(date: Option, to_date: bool) -> Option { - let time_format; - if to_date { - time_format = "23:59:59" - } else { - time_format = "00:00:00" - } - - match date { - Some(d) => { - debug!("Parsing date: {}", &d); - match chrono::NaiveDateTime::parse_from_str(format!("{} {}", &d, &time_format).as_str(), "%Y-%m-%d %H:%M:%S") { - Ok(date) => { - Some(date) - } - Err(e) => { - error!("Error occurred: {:#?}", e); - return None; - } - } - } - None => None - } -} - -pub fn sanitize_dates(date_from: Option, date_to: Option) -> (chrono::NaiveDateTime, chrono::NaiveDateTime) { - let default_to_date = chrono::Local::now().naive_local(); - let default_from_date = default_to_date.date() - .and_hms_opt(0, 0, 0) - .expect("00:00:00 is a valid time") - chrono::Duration::weeks(2); - - println!("date_to: {:#?}", date_to); - println!("date_from: {:#?}", date_from); - - println!("Default date_to: {:#?}", default_to_date); - println!("Default date_from: {:#?}", default_from_date); - - match (date_from, date_to) { - (Some(from), Some(to)) => (from, to), // validate already checked that date_from > date_to - (Some(from), None) => (from, default_to_date), // if to_date is missing, default to now - (None, Some(_to)) => todo!("Not defined yet; check"), - (None, None) => (default_from_date, default_to_date), // if both dates are none (case to_date is none and from_date is_some should be catched by validation); return dates for default duration (last 2 weeks) - } -} - -pub fn validate_dates(date_from: Option, date_to: Option) -> bool { - let date_now = chrono::Local::now().naive_local(); - debug!("... validating dates: now: {:#?} , from: {:#?} , to: {:#?}", &date_now, &date_from, &date_to); - // date_from before now - if date_from.is_some() && date_from.as_ref().unwrap().clone() > date_now { - debug!("oh no, date_from {:#?} is in the future! date_now is {:#?}", &date_from, &date_now); - return false; - } - - // date_to only if there is also date_from - if date_from.is_none() && date_to.is_some() { - return false; - } - - // date_to before or equals now - if date_to.is_some() && date_to.as_ref().unwrap().clone() >= date_now { - debug!("oh no, date_to {:#?} is in the future! date_now is {:#?}", &date_to, &date_now); - return false; - } - - // date_from before date_to - if date_from.is_some() && date_to.is_some() { - if date_from.unwrap() > date_to.unwrap() { - debug!("oh no, date_from {:#?} is before date_to {:#?}", &date_from, &date_to); - return false; - } - } - return true; -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/model/process.rs b/clearing-house-app/core-lib/src/model/process.rs deleted file mode 100644 index 7fda6aa..0000000 --- a/clearing-house-app/core-lib/src/model/process.rs +++ /dev/null @@ -1,14 +0,0 @@ -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct Process { - pub id: String, - pub owners: Vec, -} - -impl Process { - pub fn new(id: String, owners: Vec) -> Process { - Process { - id, - owners - } - } -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/model/tests.rs b/clearing-house-app/core-lib/src/model/tests.rs deleted file mode 100644 index 0c60fb1..0000000 --- a/clearing-house-app/core-lib/src/model/tests.rs +++ /dev/null @@ -1,176 +0,0 @@ -use crate::model::crypto::{KeyEntry, KeyMap}; -use crate::model::document::{Document, DocumentPart, EncryptedDocument}; -use crate::errors::*; -use std::collections::HashMap; -use chrono::Utc; - -fn create_test_doc(dt_id: String) -> Document{ - let mut doc_parts = vec!(); - doc_parts.push(DocumentPart::new(String::from("part1"), Some(String::from("MODEL_VERSION")))); - doc_parts.push(DocumentPart::new(String::from("part2"), Some(String::from("CORRELATION_MESSAGE")))); - Document::new(Document::create_uuid(), dt_id, 3241, doc_parts) -} - -fn create_key_enc_map() -> KeyMap{ - let mut map = HashMap::new(); - let key1 = String::from("an example very very secret key."); - let key2 = String::from("another totally very secret key."); - let nonce1 = String::from("unique nonce"); - let nonce2 = String::from("second nonce"); - let key_ct = String::from("very secure key ct").into_bytes(); - - let e1 = KeyEntry::new(String::from("1"), key1.into_bytes(), nonce1.into_bytes()); - let e2 = KeyEntry::new(String::from("2"), key2.into_bytes(), nonce2.into_bytes()); - map.insert(String::from("part1"), e1); - map.insert(String::from("part2"), e2); - - return KeyMap::new(true, map, Some(key_ct)); -} - -fn create_key_dec_map() -> KeyMap{ - let mut map = HashMap::new(); - let key1 = String::from("an example very very secret key."); - let key2 = String::from("another totally very secret key."); - let nonce1 = String::from("unique nonce"); - let nonce2 = String::from("second nonce"); - - let e1 = KeyEntry::new(String::from("1"), key1.into_bytes(), nonce1.into_bytes()); - let e2 = KeyEntry::new(String::from("2"), key2.into_bytes(), nonce2.into_bytes()); - map.insert(String::from("1"), e1); - map.insert(String::from("2"), e2); - - return KeyMap::new(false, map, None); -} - -#[test] -fn test_document_part_encryption() -> Result<()>{ - - // prepare test data - let part = DocumentPart::new(String::from("model_version"), Some(String::from("MODEL_VERSION"))); - let expected_ct = hex::decode("7F80228F5187DBD7FC6F7DA93510905102D39EF790FB84097EAC541E9DABF3D035FB4E910E6F52E3DB31C935").unwrap(); - - // create key and nonce - let key = String::from("an example very very secret key."); - let nonce = String::from("unique nonce"); - - // encrypt - let ct = part.encrypt(key.as_bytes(), nonce.as_bytes())?; - - // check - assert_eq!(expected_ct, ct, "Ciphertext mismatch"); - Ok(()) -} - -#[test] -fn test_document_part_decryption() -> Result<()>{ - - // prepare test data - let ct = hex::decode("7F80228F5187DBD7FC6F7DA93510905102D39EF790FB84097EAC541E9DABF3D035FB4E910E6F52E3DB31C935").unwrap(); - let expected_part = DocumentPart::new(String::from("model_version"), Some(String::from("MODEL_VERSION"))); - - // create key and nonce - let key = String::from("an example very very secret key."); - let nonce = String::from("unique nonce"); - - // decrypt - let result = DocumentPart::decrypt(key.as_bytes(), nonce.as_bytes(), ct.as_slice())?; - - // check - assert_eq!(expected_part.name, result.name, "Field name mismatch"); - assert_eq!(expected_part.content, result.content, "Content mismatch"); - - Ok(()) -} - -#[test] -fn test_document_encryption() -> Result<()>{ - - // prepare test data - let dt = String::from("ids_message"); - let pid = String::from("test_pid"); - let doc = create_test_doc(dt.clone()); - let ts = Utc::now().timestamp(); - let key_ct = String::from("very secret key ciphertext"); - let mut cts = vec!(); - cts.push(String::from("1::4EBC3F1C2B8CB16C52E41424502FD112015D9C25919C2401514B5DD5B4233B65593CF0A4")); - cts.push(String::from("2::FE2195305E95B9F931660CBA20B4707A1D92123022371CEDD2E70A538A8771EE7540D9F34845BBAEECEC")); - let expected_doc = EncryptedDocument::new(doc.id.clone(), pid, dt, ts, 3241, key_ct, cts); - - // create KeyMap for encryption - let keys = create_key_enc_map(); - - // encrypt - let result = doc.clone().encrypt(keys)?; - - // ids should match - assert_eq!(result.id, expected_doc.id); - - //checking the cts - for i in 0..result.cts.len()-1{ - //println!("cts: {}", &result.cts[i]); - assert_eq!(expected_doc.cts[i], result.cts[i]); - assert_eq!(expected_doc.cts[i], result.cts[i]); - } - - Ok(()) -} - -#[test] -fn test_document_decryption() -> Result<()>{ - - // prepare test data - let mut cts = vec!(); - let ts = Utc::now().timestamp(); - cts.push(String::from("1::4EBC3F1C2B8CB16C52E41424502FD112015D9C25919C2401514B5DD5B4233B65593CF0A4")); - cts.push(String::from("2::FE2195305E95B9F931660CBA20B4707A1D92123022371CEDD2E70A538A8771EE7540D9F34845BBAEECEC")); - let dt = String::from("ids_message"); - let pid = String::from("test_pid"); - let key_ct = String::from("very secure key ct"); - let expected_doc = create_test_doc(dt.clone()); - let enc_doc = EncryptedDocument::new(expected_doc.id.clone(), pid, dt.clone(), ts, 3241, key_ct, cts); - - // create KeyMap for decryption - let dec_keys = create_key_dec_map(); - - // decrypt - let result = enc_doc.decrypt(dec_keys.keys)?; - - // ids should match - assert_eq!(result.id, expected_doc.id); - - //check document type - assert_eq!(result.dt_id, expected_doc.dt_id); - - //checking the parts - for i in 0..result.parts.len()-1{ - //println!("part: {} {}", result.parts[i].name, result.parts[i].content.as_ref().unwrap()); - assert_eq!(expected_doc.parts[i].name, result.parts[i].name); - assert_eq!(expected_doc.parts[i].content, result.parts[i].content); - } - - Ok(()) -} - -#[test] -fn test_encryption_hash() -> Result<()> { - - // prepare test data - let mut cts = vec!(); - let ts_fixed = 1630413850; - let expected_hash = String::from("eIiWaM874V6p3eeGnEEafDvcPJAzACKhXn0yEAVw0pnZNh+Lz7eLuMMtoIQ1mhY3huy0PN5h9ntZf3mBPcZkow=="); - cts.push(String::from("1::4EBC3F1C2B8CB16C52E41424502FD112015D9C25919C2401514B5DD5B4233B65593CF0A4")); - cts.push(String::from("2::FE2195305E95B9F931660CBA20B4707A1D92123022371CEDD2E70A538A8771EE7540D9F34845BBAEECEC")); - let dt = String::from("ids_message"); - let pid = String::from("test_pid"); - let tc = 3241; - let key_ct = String::from("very secure key ct"); - let mut expected_doc = create_test_doc(dt.clone()); - // need to fix otherwise random id - expected_doc.id = String::from("a9a30044-7dfd-476f-a217-db1dc27aeb75"); - - let enc_doc = EncryptedDocument::new(expected_doc.id.clone(), pid, dt.clone(), ts_fixed, tc, key_ct, cts); - let hash = enc_doc.hash(); - assert_eq!(expected_hash, hash); - - Ok(()) -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/src/util.rs b/clearing-house-app/core-lib/src/util.rs deleted file mode 100644 index 604e095..0000000 --- a/clearing-house-app/core-lib/src/util.rs +++ /dev/null @@ -1,79 +0,0 @@ -use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC}; -use std::env; -use std::str::FromStr; - -use crate::constants::ENV_API_LOG_LEVEL; -use crate::errors; -use crate::errors::*; -use figment::{Figment, providers::{Format, Yaml}}; -use rocket::fairing::AdHoc; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct ServiceConfig{ - pub service_id: String -} - -impl ServiceConfig{ - pub fn new(service_id: String) -> ServiceConfig{ - ServiceConfig{ - service_id - } - } -} - -pub fn load_from_test_config(key: &str, file: &str) -> String{ - Figment::new().merge(Yaml::file(file)).extract_inner(key).unwrap_or(String::new()) -} - -pub fn add_service_config(service_id: String) -> AdHoc{ - AdHoc::try_on_ignite("Adding Service Config", move |rocket| async move { - match env::var(&service_id){ - Ok(id) => { - Ok(rocket.manage(ServiceConfig::new(id))) - }, - Err(_e) => { - error!("Service ID not configured. Please configure environment variable {}", &service_id); - return Err(rocket) - } - } - }) -} - - -/// setup the fern logger and set log level to environment variable `ENV_API_LOG_LEVEL` -/// allowed levels: `Off`, `Error`, `Warn`, `Info`, `Debug`, `Trace` -pub fn setup_logger() -> Result<()> { - let log_level; - match env::var(ENV_API_LOG_LEVEL){ - Ok(l) => log_level = l.clone(), - Err(_e) => { - println!("Log level not set correctly. Logging disabled"); - log_level = String::from("Off") - } - }; - - fern::Dispatch::new() - .format(|out, message, record| { - out.finish(format_args!( - "{}[{}][{}] {}", - chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), - record.target(), - record.level(), - message - )) - }) - .level(log::LevelFilter::from_str(&log_level.as_str())?) - .chain(std::io::stdout()) - .chain(fern::log_file("output.log")?) - .apply()?; - Ok(()) -} - -pub fn read_file(file: &str) -> Result { - std::fs::read_to_string(file) - .map_err(|e| errors::Error::from(e)) -} - -pub fn url_encode(id: &str) -> String{ - utf8_percent_encode(id, NON_ALPHANUMERIC).to_string() -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/blockchain_api_client.rs b/clearing-house-app/core-lib/tests/integration/blockchain_api_client.rs deleted file mode 100644 index f4a38a5..0000000 --- a/clearing-house-app/core-lib/tests/integration/blockchain_api_client.rs +++ /dev/null @@ -1,42 +0,0 @@ -use core_lib::constants::{CONFIG_FILE, BLOCKCHAIN_API_URL}; -use core_lib::util; -use core_lib::errors::*; -use core_lib::api::client::blockchain_api::BlockchainApiClient; - -/// before running make sure the blockchain api is available -#[test] -fn test_store_hash() -> Result<()>{ - // configure client_api - let config = util::load_config(CONFIG_FILE); - let bc_api: BlockchainApiClient = util::configure_api(BLOCKCHAIN_API_URL, &config)?; - - let id = String::from("999"); - let cid = String::from("123"); - let hash = String::from("ABCD-EFGH"); - - assert_eq!(bc_api.store_hash(&id, &cid, &hash)?, true); - - Ok(()) -} - -#[test] -fn test_get_hash_list() -> Result<()>{ - // configure client_api - let config = util::load_config(CONFIG_FILE); - let bc_api: BlockchainApiClient = util::configure_api(BLOCKCHAIN_API_URL, &config)?; - - let id = String::from("999"); - let cid1 = String::from("123"); - let hash1 = String::from("ABCD-EFGH"); - let cid2 = String::from("5556"); - let hash2 = String::from("ZAZS-QWEA"); - - assert_eq!(bc_api.store_hash(&id, &cid1, &hash1)?, true); - assert_eq!(bc_api.store_hash(&id, &cid2, &hash2)?, true); - - let result = bc_api.get_hash_list(&id)?; - - assert_eq!(result.len(), 2); - - Ok(()) -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/daps_api_client.rs b/clearing-house-app/core-lib/tests/integration/daps_api_client.rs deleted file mode 100644 index afe38bf..0000000 --- a/clearing-house-app/core-lib/tests/integration/daps_api_client.rs +++ /dev/null @@ -1,31 +0,0 @@ -/* TODO: Integration test currently not necessary - -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// These tests are integration tests and need an up-and-running keyring-api -// Use config.yml to configure the urls correctly. -// Before running the tests make sure that there's a valid token in auth/mod.rs -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -use core_lib::api::ApiClient; -use core_lib::api::client::daps_api::DapsApiClient; -use core_lib::constants::{DAPS_API_URL, DAPS_KID}; -use core_lib::errors::*; -use core_lib::util; -use biscuit::jwk::{JWK, KeyType}; -use biscuit::Empty; -use crate::TEST_CONFIG; - -/// before running make sure the blockchain api is available -#[test] -fn test_get_jwks() -> Result<()>{ - // configure daps_api - let api_url = util::load_from_test_config(DAPS_API_URL, TEST_CONFIG); - let daps_api = DapsApiClient::new(&api_url); - // convert "default" key to HashMap - - let jwk: JWK = daps_api.get_jwks().unwrap().find(DAPS_KID).unwrap().clone(); - assert_eq!(KeyType::RSA, jwk.algorithm.key_type()); - assert_eq!(DAPS_KID, jwk.common.key_id.unwrap()); - Ok(()) -} - -*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/database_client.rs b/clearing-house-app/core-lib/tests/integration/database_client.rs deleted file mode 100644 index b187b7e..0000000 --- a/clearing-house-app/core-lib/tests/integration/database_client.rs +++ /dev/null @@ -1,191 +0,0 @@ -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// These tests all access the db, so if you run the tests use -// cargo test -- --test-threads=1 -// otherwise they will interfere with each other -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -const TEST_CONFIG_FILE: &'static str = "./config.yml"; - -use core_lib::db::{DataStoreApi, DataStore}; -use core_lib::errors::*; -use core_lib::util; - -use crate::create_test_enc_document; - -fn db_setup() -> DataStore{ - let config = util::load_config(TEST_CONFIG_FILE); - - let db: DataStore = util::configure_db(&config).unwrap(); - if let Err(e) = db.clean_db(){ - panic!("Error while cleaning up database {:?}", e); - } - if let Err(e) = db.create_indexes(){ - panic!("Error while setting up database {:?}", e); - }; - db -} - -fn tear_down(db: DataStore){ - if let Err(e) = db.clean_db(){ - panic!("Error while tearing down database {:?}", e); - } -} - -// DOCUMENT -/// Testcase: Document exists in db and is found -#[test] -fn test_document_exists() -> Result<()>{ - // empty db and create tables - let db = db_setup(); - - // prepare test data - let pid = String::from("test_document_exists_pid"); - let dt_id = String::from("test_document_exists_dt"); - let id = String::from("test_document_exists_id"); - let doc = create_test_enc_document(&id, &pid, &dt_id); - db.add_document(doc.clone())?; - - // run the test - assert_eq!(db.exists_document(&id)?, true); - - // clean up - tear_down(db); - - Ok(()) -} - -/// Testcase: Document does not exist and is not found -#[test] -fn test_document_does_not_exist() -> Result<()>{ - // empty db and create tables - let db = db_setup(); - - // prepare test data - let pid = String::from("test_document_does_not_exist_pid"); - let dt_id = String::from("test_document_does_not_exist_dt"); - let id1 = String::from("test_document_does_not_exist_pid_id1"); - let id2 = String::from("test_document_does_not_exist_pid_id2"); - let doc = create_test_enc_document(&id1, &pid, &dt_id); - db.add_document(doc.clone())?; - - // run the test - assert_eq!(db.exists_document(&id2)?, false); - - // clean up - tear_down(db); - - Ok(()) -} - -/// Testcase: Document does not exist after delete -#[test] -fn test_delete_document_doc_is_deleted() -> Result<()>{ - // empty db and create tables - let db = db_setup(); - - // prepare test data - let pid = String::from("test_delete_document_doc_is_deleted_pid"); - let dt_id = String::from("test_delete_document_doc_is_deleted_dt"); - let id = String::from("test_delete_document_doc_is_deleted_id"); - let doc = create_test_enc_document(&id, &pid, &dt_id); - db.add_document(doc.clone())?; - - // db should be able to find the document - assert_eq!(db.exists_document(&id)?, true); - - // run the test - assert!(db.delete_document(&id)?); - - // db should not find document anymore - assert_eq!(db.exists_document(&id)?, false); - - // clean up - tear_down(db); - - Ok(()) -} - -/// Testcase: Other Documents still exist after delete -#[test] -fn test_delete_document_check_others() -> Result<()>{ - // empty db and create tables - let db = db_setup(); - - // prepare test data - let pid = String::from("test_delete_document_check_others_pid"); - let dt_id = String::from("test_delete_document_check_others_dt"); - let id1 = String::from("test_delete_document_check_others_id1"); - let id2 = String::from("test_delete_document_check_others_id2"); - let doc1 = create_test_enc_document(&id1, &pid, &dt_id); - let doc2 = create_test_enc_document(&id2, &pid, &dt_id); - db.add_document(doc1.clone())?; - db.add_document(doc2.clone())?; - - // db should be able to find both documents - assert_eq!(db.exists_document(&id1)?, true); - assert_eq!(db.exists_document(&id2)?, true); - - // run the test - assert!(db.delete_document(&id1)?); - - // db should still find the other document - assert_eq!(db.exists_document(&id2)?, true); - - // clean up - tear_down(db); - - Ok(()) -} - -/// Testcase: Document does not exist before delete -#[test] -fn test_delete_document_on_not_existing_doc() -> Result<()>{ - // empty db and create tables - let db = db_setup(); - - // prepare test data - let pid = String::from("test_delete_document_on_not_existing_doc_pid"); - let dt_id = String::from("test_delete_document_on_not_existing_doc_dt"); - let id1 = String::from("test_delete_document_on_not_existing_doc_id1"); - let id2 = String::from("test_delete_document_on_not_existing_doc_id2"); - let doc = create_test_enc_document(&id1, &pid, &dt_id); - db.add_document(doc.clone())?; - - // run the test - assert_eq!(db.delete_document(&id2)?, false); - - // clean up - tear_down(db); - - Ok(()) -} - -/// Testcase: Find the correct document -#[test] -fn test_get_document() -> Result<()>{ - // empty db and create tables - let db = db_setup(); - - // prepare test data - let pid = String::from("test_get_document_pid"); - let dt_id = String::from("test_get_document_dt"); - let id1 = String::from("test_get_document_id1"); - let id2 = String::from("test_get_document_id2"); - let doc1 = create_test_enc_document(&id1, &pid, &dt_id); - let doc2 = create_test_enc_document(&id2, &pid, &dt_id); - db.add_document(doc1.clone())?; - db.add_document(doc2.clone())?; - - // db should be able to find both documents - assert_eq!(db.exists_document(&id1)?, true); - assert_eq!(db.exists_document(&id2)?, true); - - // the test - let result = db.get_document(&id1, &pid)?; - assert_eq!(result.is_some(), true); - assert_eq!(result.unwrap().id, id1); - - // clean up - tear_down(db); - - Ok(()) -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/document_api_client.rs b/clearing-house-app/core-lib/tests/integration/document_api_client.rs deleted file mode 100644 index 50dbe56..0000000 --- a/clearing-house-app/core-lib/tests/integration/document_api_client.rs +++ /dev/null @@ -1,228 +0,0 @@ -/* TODO: Integration test currently not necessary - -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// These tests are integration tests and need an up-and-running keyring-api and -// document-api. Use config.yml to configure the urls correctly. -// Before running the tests make sure that there's a valid token in auth/mod.rs -// Also note: Clean up will not work if a test fails. -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -use core_lib::api::ApiClient; -use core_lib::constants::DOCUMENT_API_URL; -use core_lib::util; -use core_lib::errors::*; -use core_lib::api::client::document_api::DocumentApiClient; -use crate::{TOKEN, create_test_document, delete_test_doc_type_from_keyring, insert_test_doc_type_into_keyring, TEST_CONFIG}; - -/// Testcase: Standard case: store document as first document for pid -#[test] -fn test_store_first_document() -> Result<()> { - // configure client_api - let api_url = util::load_from_test_config(DOCUMENT_API_URL, TEST_CONFIG); - let doc_api = DocumentApiClient::new(&api_url); - - // prepare test data - let dt_id = String::from("test_store_first_document_dt"); - let pid = String::from("test_store_first_document_pid"); - let expected_doc = create_test_document(&pid, &dt_id, 0); - // clean up doc type (in case of previous test failure) - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - insert_test_doc_type_into_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - // run the test - let result = doc_api.create_document(&TOKEN.to_string(), &expected_doc)?; - assert_eq!(result.chain_hash, String::from("0")); - - // clean up - assert!(doc_api.delete_document(&TOKEN.to_string(), &expected_doc.pid, &expected_doc.id)?); - - // tear down - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - Ok(()) -} - -/// Testcase: Standard case: store document as first document for pid -#[test] -fn test_store_chained_document() -> Result<()> { - // configure client_api - let api_url = util::load_from_test_config(DOCUMENT_API_URL, TEST_CONFIG); - let doc_api = DocumentApiClient::new(&api_url); - - // prepare test data - let dt_id = String::from("test_store_chained_document_dt"); - let pid = String::from("test_store_chained_document_pid"); - let first_doc = create_test_document(&pid, &dt_id, 0); - let second_doc = create_test_document(&pid, &dt_id, 1); - // clean up doc type (in case of previous test failure) - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - insert_test_doc_type_into_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - // create test data in db - doc_api.create_document(&TOKEN.to_string(), &first_doc)?; - - // run the test - let result = doc_api.create_document(&TOKEN.to_string(), &second_doc)?; - assert_ne!(result.chain_hash, String::from("0")); - - // clean up - assert!(doc_api.delete_document(&TOKEN.to_string(), &first_doc.pid, &first_doc.id)?); - assert!(doc_api.delete_document(&TOKEN.to_string(), &second_doc.pid, &second_doc.id)?); - - // tear down - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - Ok(()) -} - -/// Testcase: Standard case: retrieve document. -#[test] -fn test_get_document() -> Result<()>{ - // configure client_api - let api_url = util::load_from_test_config(DOCUMENT_API_URL, TEST_CONFIG); - let doc_api = DocumentApiClient::new(&api_url); - - // prepare test data - let dt_id = String::from("test_get_document_type_1"); - let pid = String::from("test_get_document_process_1"); - let expected_doc = create_test_document(&pid, &dt_id, 0); - // clean up doc type (in case of previous test failure) - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - insert_test_doc_type_into_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - // create test data in db - doc_api.create_document(&TOKEN.to_string(), &expected_doc)?; - - // run test - let result = doc_api.get_document(&TOKEN.to_string(), &pid, &expected_doc.id)?.unwrap(); - println!("Result: {:?}", result); - - // checks - // ids should match - assert_eq!(result.id, expected_doc.id); - - // same document type - assert_eq!(result.dt_id, expected_doc.dt_id); - - // checking the parts - for i in 0..result.parts.len()-1{ - assert_eq!(expected_doc.parts[i].name, result.parts[i].name); - assert_eq!(expected_doc.parts[i].content, result.parts[i].content); - } - - // clean up - assert!(doc_api.delete_document(&TOKEN.to_string(), &expected_doc.pid, &expected_doc.id)?); - - // tear down - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - Ok(()) -} - -/// Testcase: Retrieve all documents for pid, but there are no documents -#[test] -fn test_get_no_documents_for_pid() -> Result<()>{ - // configure client_api - let api_url = util::load_from_test_config(DOCUMENT_API_URL, TEST_CONFIG); - let doc_api = DocumentApiClient::new(&api_url); - - // prepare test data - let dt_id = String::from("test_get_no_documents_for_pid_type"); - let pid_with_doc = String::from("test_get_no_documents_for_pid_pid_1"); - let pid_without_doc = String::from("test_get_no_documents_for_pid_pid_2"); - let expected_doc = create_test_document(&pid_with_doc, &dt_id, 0); - // clean up doc type (in case of previous test failure) - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid_with_doc, &dt_id)?; - insert_test_doc_type_into_keyring(&TOKEN.to_string(), &pid_with_doc, &dt_id)?; - - // create test data in db - doc_api.create_document(&TOKEN.to_string(), &expected_doc)?; - - // run test - let result = doc_api.get_documents_for_pid(&TOKEN.to_string(), &pid_without_doc)?; - println!("Result: {:?}", result); - - // check that there are no documents found - assert_eq!(result.len(), 0); - - // clean up - assert!(doc_api.delete_document(&TOKEN.to_string(), &expected_doc.pid, &expected_doc.id)?); - - // tear down - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid_with_doc, &dt_id)?; - - Ok(()) -} - -/// Testcase: Standard case: Retrieve all documents for pid -//TODO -#[test] -fn test_get_documents_for_pid() -> Result<()>{ - // configure client_api - let api_url = util::load_from_test_config(DOCUMENT_API_URL, TEST_CONFIG); - let doc_api = DocumentApiClient::new(&api_url); - - // prepare test data - let dt_id = String::from("test_get_documents_for_pid_type"); - let pid = String::from("test_get_documents_for_pid_pid"); - let doc1 = create_test_document(&pid, &dt_id, 0); - let doc2 = create_test_document(&pid, &dt_id, 1); - let doc3 = create_test_document(&pid, &dt_id, 2); - // clean up doc type (in case of previous test failure) - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - insert_test_doc_type_into_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - // create test data in db - doc_api.create_document(&TOKEN.to_string(), &doc1)?; - doc_api.create_document(&TOKEN.to_string(), &doc2)?; - doc_api.create_document(&TOKEN.to_string(), &doc3)?; - - // run test - let result = doc_api.get_documents_for_pid(&TOKEN.to_string(), &pid)?; - println!("Result: {:?}", result); - - // check that we got three documents back - assert_eq!(result.len(), 3); - - // tear down - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - assert!(doc_api.delete_document(&TOKEN.to_string(), &pid, &doc1.id)?); - assert!(doc_api.delete_document(&TOKEN.to_string(), &pid, &doc2.id)?); - assert!(doc_api.delete_document(&TOKEN.to_string(), &pid, &doc3.id)?); - - - Ok(()) -} - -/// Testcase: Ensure that IDS ids can be used if they are url_encoded -#[test] -fn test_create_document_url_encoded_id() -> Result<()>{ - // configure client_api - let api_url = util::load_from_test_config(DOCUMENT_API_URL, TEST_CONFIG); - let doc_api = DocumentApiClient::new(&api_url); - - // prepare test data - let dt_id = String::from("test_create_document_url_encoded_id_type_3"); - let pid = String::from("test_create_document_url_encoded_id_process_3"); - let id = String::from("https://w3id.org/idsa/autogen/ResultMessage/71ad9d3a-3743-4966-afa3-f5b02ba91eaa"); - // clean up doc type (in case of previous test failure) - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - insert_test_doc_type_into_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - let mut doc = create_test_document(&pid, &dt_id, 0); - doc.id = id.clone(); - - // run test - let hash = doc_api.create_document(&TOKEN.to_string(), &doc); - - // check that it's not an error - assert!(hash.is_ok()); - - // clean up - assert!(doc_api.delete_document(&TOKEN.to_string(), &doc.pid, &id)?); - - // tear down - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - Ok(()) -} - -*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs b/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs deleted file mode 100644 index 26b35c6..0000000 --- a/clearing-house-app/core-lib/tests/integration/keyring_api_client.rs +++ /dev/null @@ -1,91 +0,0 @@ -/* TODO: Integration test currently not necessary - -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// These tests are integration tests and need an up-and-running keyring-api -// Use config.yml to configure the urls correctly. -// Before running the tests make sure that there's a valid token in auth/mod.rs -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -use core_lib::api::ApiClient; -use core_lib::api::client::keyring_api::KeyringApiClient; -use core_lib::constants::KEYRING_API_URL; -use core_lib::errors::*; -use core_lib::util; -use crate::{TOKEN, delete_test_doc_type_from_keyring, insert_test_doc_type_into_keyring, TEST_CONFIG}; - -/// The tests in this module requires a running key-ring-api -/// Testcase: Generate keys for test document type and check if the key_map is plausible -#[test] -fn test_generate_keys() -> Result<()> { - // configure client_api - let api_url = util::load_from_test_config(KEYRING_API_URL, TEST_CONFIG); - let key_api = KeyringApiClient::new(&api_url); - - // prepare test data - let dt_id = String::from("test_dt"); - let pid = String::from("test_pid"); - // clean up doc type (in case of previous test failure) - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - insert_test_doc_type_into_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - // get the keys from keyring api - let keys = key_api.generate_keys(&TOKEN.to_string(), &pid, &dt_id)?; - - println!("key_ct: {}", hex::encode_upper(keys.keys_enc.as_ref().unwrap())); - - // check that KeyMap is meant for encryption - assert_eq!(keys.enc, true); - - // check that there's a key_ct - assert!(keys.keys_enc.is_some()); - - // check that there are three keys (one for each part in the dt) - assert_eq!(keys.keys.keys().len(), 3); - - // tear down - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - Ok(()) -} - -/// Testcase: Decrypt keys and check that they match the previously generated keys -#[test] -fn test_decrypt_keys() -> Result<()> { - // configure client_api - let api_url = util::load_from_test_config(KEYRING_API_URL, TEST_CONFIG); - let key_api = KeyringApiClient::new(&api_url); - - // prepare test data - let dt_id = String::from("test_dt"); - let pid = String::from("test_pid"); - // clean up doc type (in case of previous test failure) - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - insert_test_doc_type_into_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - // generate keys from keyring api - let keys = key_api.generate_keys(&TOKEN.to_string(), &pid, &dt_id)?; - - // decrypt the keys - let dec_keys = key_api.decrypt_keys(&TOKEN.to_string(), &pid, &dt_id, keys.keys_enc.as_ref().unwrap())?; - - // check that KeyMap is meant for decryption - assert_eq!(dec_keys.enc, false); - - // check that there's no key_ct - assert!(dec_keys.keys_enc.is_none()); - - // check that the keys match the previously generated ones - keys.keys.values().for_each( |entry| { - let dec_entry = dec_keys.keys.get(&entry.id).unwrap(); - assert_eq!(entry.key, dec_entry.key); - assert_eq!(entry.nonce, dec_entry.nonce); - assert_eq!(entry.id, dec_entry.id); - } - ); - - // tear down - delete_test_doc_type_from_keyring(&TOKEN.to_string(), &pid, &dt_id)?; - - Ok(()) -} - -*/ \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/main.rs b/clearing-house-app/core-lib/tests/integration/main.rs deleted file mode 100644 index 799d36a..0000000 --- a/clearing-house-app/core-lib/tests/integration/main.rs +++ /dev/null @@ -1,92 +0,0 @@ -use reqwest::blocking::{Client}; -use reqwest::StatusCode; -use reqwest::header::{CONTENT_TYPE, HeaderValue}; - -use core_lib::constants::ROCKET_DOC_TYPE_API; -use core_lib::errors::*; -use core_lib::model::document::{Document, DocumentPart}; - -/// Update this token to run tests successfully that require authentication -pub const TOKEN: &'static str = "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzUyNDEyNzgsImlhdCI6MTYzNTI0MTI3OCwianRpIjoiT0RBNE5EazRNemsxT0RZMU16TXlOamN4TlE9PSIsImV4cCI6MTYzNTI0NDg3OCwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpUUlVTVF9TRUNVUklUWV9QUk9GSUxFIiwicmVmZXJyaW5nQ29ubmVjdG9yIjoiaHR0cDovL2NvbnN1bWVyLWNvcmUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiYzE1ZTY1NTgwODhkYmZlZjIxNWE0M2QyNTA3YmJkMTI0ZjQ0ZmI4ZmFjZDU2MWMxNDU2MWEyYzFhNjY5ZDBlMCIsInN1YiI6IkE1OjBDOkE1OkYwOjg0OkQ5OjkwOkJCOkJDOkQ5OjU3OjNBOjA0OkM4OjdGOjkzOkVEOjk3OkEyOjUyOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.iemDKZXE_RXFKkffqpweTAXBb6YX0spU0b5Ez1ncQzEyDNkJ5UtsZkwZz8WqfWOdPqMA74ShzLMwfEtao3DoO4DfWrvXFAYh8Y6hHJjHO44kPm4rUdcymUsVLXxcWd8Jszi6HjRHLaJ1-466s1akDQ7yQB0l8g9PP7BOlYr2I00HZ_b5wQOWtwT2PQxeWjkBzTgP8iycF7kIT6jgTHYDkOAwIdiMgNH_dPaxOPfxupz5vJQPuC1o9-IAyXtk-yC9GNI18YtjYpqizB-Nm5QGlUSSYMrB7tUKEc46471QaC4tR_LkYDrGnDtJHrH_fq0eEe6wIKoUcdt_VnI9Km-Hpw"; -pub const TEST_CONFIG: &'static str = "config.yml"; - -/* TODO: Disable all integration tests for now -mod document_api_client; -mod keyring_api_client; -mod daps_api_client; -mod token_validation; - - */ - -fn create_test_document(pid: &String, dt_id: &String, tc: i64) -> Document{ - let p1 = DocumentPart::new(String::from("name"), Some(String::from("This is document part name."))); - let p2 = DocumentPart::new(String::from("payload"), Some(String::from("This is document part payload."))); - let p3 = DocumentPart::new(String::from("connector"), Some(String::from("This is document part connector."))); - let pts = vec!(p1, p2, p3); - let d = Document::new(pid.clone(), dt_id.clone(),tc, pts); - d -} - -fn create_dt_json(dt_id: &String, pid: &String) -> String{ - let begin_dt = r#"{"id":""#; - let begin_pid = r#"","pid":""#; - let rest = r#"","parts":[{"name":"name"},{"name":"payload"},{"name":"connector"}]}"#; - - let mut json = String::from(begin_dt); - json.push_str(dt_id); - json.push_str(begin_pid); - json.push_str(pid); - json.push_str(rest); - return json -} - -fn insert_test_doc_type_into_keyring(token: &String, pid: &String, dt_id: &String) -> Result{ - let client = Client::new(); - let dt_url = format!("http://localhost:8002{}", ROCKET_DOC_TYPE_API); - - let json_data = create_dt_json(dt_id, pid); - - println!("json_data: {}", json_data); - - println!("calling {}", &dt_url); - let response = client - .post(dt_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .bearer_auth(token) - .body(json_data).send()?; - - println!("Status Code: {}", &response.status()); - match response.status(){ - StatusCode::CREATED => { - println!("Response: {}", response.text()?); - Ok(true) - }, - _ => { - panic!("Couldn't prepare doc type for test"); - } - } -} - -fn delete_test_doc_type_from_keyring(token: &String, pid: &String, dt_id: &String) -> Result{ - let client = Client::new(); - let dt_url = format!("http://localhost:8002{}/{}/{}", ROCKET_DOC_TYPE_API, pid, dt_id); - - println!("calling {}", &dt_url); - let response = client - .delete(dt_url.as_str()) - .header(CONTENT_TYPE, HeaderValue::from_static("application/json")) - .bearer_auth(token) - .send()?; - - println!("Status Code: {}", &response.status()); - match response.status(){ - StatusCode::NO_CONTENT => { - println!("Response: {}", response.text()?); - Ok(true) - }, - _ => { - println!("Couldn't delete document type"); - Ok(false) - } - } -} \ No newline at end of file diff --git a/clearing-house-app/core-lib/tests/integration/token_validation.rs b/clearing-house-app/core-lib/tests/integration/token_validation.rs deleted file mode 100644 index 305ab0f..0000000 --- a/clearing-house-app/core-lib/tests/integration/token_validation.rs +++ /dev/null @@ -1,59 +0,0 @@ -/* TODO: Integration test currently not necessary - -use biscuit::jwa::SignatureAlgorithm; -use biscuit::jwk::JWKSet; -use biscuit::{CompactJson, Empty}; -use core_lib::api::ApiClient; -use core_lib::api::auth::{self, ApiKey}; -use core_lib::api::client::daps_api::DapsApiClient; -use core_lib::constants::DAPS_API_URL; -use core_lib::errors::*; -use core_lib::util; -use serde::{Deserialize, Serialize}; -use crate::{TOKEN, TEST_CONFIG}; - -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] -struct CustomClaims { - /// Recipient for which the JWT is intended - scopes: Vec, - #[serde(rename = "securityProfile")] - security_profile: String, - #[serde(rename = "@type")] - claim_type: String, - #[serde(rename = "@context")] - claim_context: String, - #[serde(rename = "transportCertsSha256")] - transport_certs_sha256: String, -} -impl CompactJson for CustomClaims { -} - -#[test] -fn test_valid_claims() -> Result<()>{ - // configure daps_api - let api_url = util::load_from_test_config(DAPS_API_URL, TEST_CONFIG); - let daps_api = DapsApiClient::new(&api_url); - // convert "default" key to HashMap - let jwks = daps_api.get_jwks()?; - let jwt: Result> = auth::validate_token(TOKEN, &jwks, Some(SignatureAlgorithm::RS256)); - assert!(jwt.is_ok(), "Token is invalid. Update test token!"); - let claims = jwt.unwrap().claims(); - assert_eq!(claims.private.scopes, vec!["idsc:IDS_CONNECTOR_ATTRIBUTES_ALL".to_string()]); - assert_eq!(claims.private.security_profile, "idsc:TRUST_SECURITY_PROFILE".to_string()); - assert_eq!(claims.private.transport_certs_sha256, "c15e6558088dbfef215a43d2507bbd124f44fb8facd561c14561a2c1a669d0e0".to_string()); - Ok(()) -} - -#[test] -fn test_invalid_claims() -> Result<()>{ - let invalid_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRlZmF1bHQifQ.eyJpZHMtYXR0cmlidXRlcyI6eyJzZWN1cml0eV9wcm9maWxlIjp7ImF1ZGl0X2xvZ2dpbmciOjJ9fSwiaWRzX21lbWJlcnNoaXAiOnRydWUsImlkcy11cmkiOiJodHRwOi8vc29tZS11cmkiLCJ0cmFuc3BvcnRDZXJ0c1NoYTI1NiI6ImJhY2I4Nzk1NzU3MzBiYjA4M2YyODNmZDViNjdhOGNiODk2OTQ0ZDFiZTI4YzdiMzIxMTdjZmM3NTdjODFlOTYiLCJzY29wZXMiOlsiaWRzX2Nvbm5lY3RvciJdLCJhdWQiOiJJRFNfQ29ubmVjdG9yIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJzdWIiOiJDPURFLE89RnJhdW5ob2ZlcixPVT1BSVNFQyxDTj02ZTYxNThiNC02OWZmLTRkMDQtYTg0Yi1hNDI4NTY0YWU0ZTYiLCJuYmYiOjE1NjI5MjU1MDAsImV4cCI6MTU2MjkyOTEwMH0.V4GZq3ZFnFAULoCiwhXtpno1uLab-mmAwRchhb2w_k4v0VYQYgWsFGf1EJPX-0QJfz4_WtTS_nQMq-MG9fP-Pe9BVXY43Wb9UBrrlaxylwnYbV0BCgUc-T-0uWdtJkRoQDqySnNRzYDMOKxZcOTXLG5d4eOHUulgiHa2muUeWw_c7bV-DKzNxUCzinxCEEVaOpovArJhRHSGgLd-8UI6BA-xehNQu_lmcaQ2ut0_VT-njwkY98haowrvEVcN9yHTm2jrWv-ajrs9phiR24A4wUqPMysDYZzIq_F6RfUBWovuu534nfo5mBXlc1JpT2NydN_dE2FM9nAWPpJ6_BEZxg"; - // configure daps_api - let api_url = util::load_from_test_config(DAPS_API_URL, TEST_CONFIG); - let daps_api = DapsApiClient::new(&api_url); - // convert "default" key to HashMap - let jwks:JWKSet = daps_api.get_jwks()?; - let jwt: Result> = auth::validate_token(invalid_token, &jwks, Some(SignatureAlgorithm::RS256)); - assert!(jwt.is_err(), "Token is valid. this should not happen, really!"); - Ok(()) -} -*/ \ No newline at end of file diff --git a/clearing-house-app/document-api/Cargo.toml b/clearing-house-app/document-api/Cargo.toml deleted file mode 100644 index 4fbdedc..0000000 --- a/clearing-house-app/document-api/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "document-api" -version = "0.10.0" -authors = [ - "Mark Gall ", - "Georg Bramm ", -] -edition = "2021" - -[dependencies] -biscuit = "0.6.0" -chrono = { version = "0.4", features = ["serde"] } -core-lib = {path = "../core-lib" } -error-chain = "0.12.4" -fern = "0.5" -futures = "0.3.24" -hex = "0.4.3" -log = "0.4.14" -mongodb ="2.3.0" -rocket = { version = "0.5.0-rc.1", features = ["json"] } -rocket_cors = "0.6.0-alpha2" -serde = "1.0" -serde_derive = "1.0" -serde_json = "1.0" -tokio = "1.8.1" -tokio-test = "0.4.2" diff --git a/clearing-house-app/document-api/Rocket.toml b/clearing-house-app/document-api/Rocket.toml deleted file mode 100644 index 4df585d..0000000 --- a/clearing-house-app/document-api/Rocket.toml +++ /dev/null @@ -1,20 +0,0 @@ -[global] -limits = { json = 5242880 } - -[debug] -address = "0.0.0.0" -port = 8001 -log_level = "normal" -limits = { forms = 32768 } -database_url = "mongodb://localhost:27017" -keyring_api_url = "http://localhost:8002" -clear_db = true - -[release] -address = "0.0.0.0" -port = 8001 -log_level = "normal" -limits = { forms = 32768 } -database_url = "mongodb://document-mongo:27017" -keyring_api_url = "http://keyring-api:8002" -clear_db = false diff --git a/clearing-house-app/document-api/certs b/clearing-house-app/document-api/certs deleted file mode 120000 index 36343b9..0000000 --- a/clearing-house-app/document-api/certs +++ /dev/null @@ -1 +0,0 @@ -../certs \ No newline at end of file diff --git a/clearing-house-app/document-api/src/db/bucket.rs b/clearing-house-app/document-api/src/db/bucket.rs deleted file mode 100644 index 1f78239..0000000 --- a/clearing-house-app/document-api/src/db/bucket.rs +++ /dev/null @@ -1,53 +0,0 @@ -use core_lib::model::document::EncryptedDocument; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DocumentBucket { - pub counter: u64, - pub pid: String, - pub dt_id: String, - pub from_ts: i64, - pub to_ts: i64, - pub documents: Vec -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DocumentBucketSize { - pub capacity: i32, - pub size: i32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DocumentBucketUpdate { - pub id: String, - pub ts: i64, - pub tc: i64, - pub hash: String, - pub keys_ct: String, - pub cts: Vec -} - -impl From<&EncryptedDocument> for DocumentBucketUpdate{ - fn from(doc: &EncryptedDocument) -> Self { - DocumentBucketUpdate{ - id: doc.id.clone(), - ts: doc.ts, - tc: doc.tc, - hash: doc.hash.clone(), - keys_ct: doc.keys_ct.clone(), - cts: doc.cts.to_vec() - } - } -} - -pub fn restore_from_bucket(pid: &String, dt_id: &String, bucket_update: DocumentBucketUpdate) -> EncryptedDocument{ - EncryptedDocument{ - id: bucket_update.id.clone(), - dt_id: dt_id.clone(), - pid: pid.clone(), - ts: bucket_update.ts, - tc: bucket_update.tc, - hash: bucket_update.hash.clone(), - keys_ct: bucket_update.keys_ct.clone(), - cts: bucket_update.cts.to_vec() - } -} \ No newline at end of file diff --git a/clearing-house-app/document-api/src/db/mod.rs b/clearing-house-app/document-api/src/db/mod.rs deleted file mode 100644 index 5f682c7..0000000 --- a/clearing-house-app/document-api/src/db/mod.rs +++ /dev/null @@ -1,393 +0,0 @@ -use futures::stream::StreamExt; -use mongodb::{bson, Client, Database, IndexModel}; -use mongodb::bson::doc; -use mongodb::options::{AggregateOptions, CreateCollectionOptions, IndexOptions, UpdateOptions, WriteConcern}; -use rocket::{Build, Rocket}; -use rocket::fairing::{self, Fairing, Info, Kind}; -use chrono::NaiveDateTime; - -use core_lib::constants::{DATABASE_URL, DOCUMENT_DB, CLEAR_DB, MAX_NUM_RESPONSE_ENTRIES, MONGO_DT_ID, MONGO_ID, MONGO_PID, DOCUMENT_DB_CLIENT, MONGO_TC, MONGO_TS, MONGO_COLL_DOCUMENT_BUCKET, MONGO_TO_TS, MONGO_FROM_TS, MONGO_DOC_ARRAY, MONGO_COUNTER}; -use core_lib::db::{DataStoreApi, init_database_client}; -use core_lib::errors::*; -use core_lib::model::document::{Document, EncryptedDocument}; -use core_lib::model::SortingOrder; -use crate::db::bucket::{DocumentBucketSize, DocumentBucketUpdate, restore_from_bucket}; - -mod bucket; - -// TODO: Disabled integration tests with database -// #[cfg(test)] mod tests; - -#[derive(Clone, Debug)] -pub struct DatastoreConfigurator; - -#[rocket::async_trait] -impl Fairing for DatastoreConfigurator { - fn info(&self) -> Info { - Info { - name: "Configuring Document Database", - kind: Kind::Ignite - } - } - async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB){ - Ok(value) => { - debug!("clear_db: '{}' found.", &value); - value - }, - Err(_) => { - false - } - }; - debug!("Using mongodb url: '{:#?}'", &db_url); - match init_database_client::(&db_url.as_str(), Some(DOCUMENT_DB_CLIENT.to_string())).await{ - Ok(datastore) => { - debug!("Check if database is empty..."); - match datastore.client.database(DOCUMENT_DB) - .list_collection_names(None) - .await{ - Ok(colls) => { - debug!("... found collections: {:#?}", &colls); - let number_of_colls = match colls.contains(&MONGO_COLL_DOCUMENT_BUCKET.to_string()){ - true => colls.len(), - false => 0 - }; - - if number_of_colls > 0 && clear_db{ - debug!("Database not empty and clear_db == true. Dropping database..."); - match datastore.client.database(DOCUMENT_DB).drop(None).await{ - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - }; - } - if number_of_colls == 0 || clear_db{ - debug!("Database empty. Need to initialize..."); - let mut write_concern = WriteConcern::default(); - write_concern.journal = Some(true); - let mut options = CreateCollectionOptions::default(); - options.write_concern = Some(write_concern); - debug!("Create collection {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore.client.database(DOCUMENT_DB).create_collection(MONGO_COLL_DOCUMENT_BUCKET, options).await{ - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - }; - - // This purpose of this index is to ensure that the transaction counter is unique - let mut index_options = IndexOptions::default(); - index_options.unique = Some(true); - let mut index_model = IndexModel::default(); - index_model.keys = doc!{format!("{}.{}",MONGO_DOC_ARRAY, MONGO_TC): 1}; - index_model.options = Some(index_options); - - debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore.client.database(DOCUMENT_DB).collection::(MONGO_COLL_DOCUMENT_BUCKET).create_index(index_model, None).await{ - Ok(result) => { - debug!("... index {} created", result.index_name); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - } - - // This creates a compound index over pid and the timestamp to enable paging using buckets - let mut compound_index_model = IndexModel::default(); - compound_index_model.keys = doc!{MONGO_PID: 1, MONGO_TS: 1}; - - debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore.client.database(DOCUMENT_DB).collection::(MONGO_COLL_DOCUMENT_BUCKET).create_index(compound_index_model, None).await{ - Ok(result) => { - debug!("... index {} created", result.index_name); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - } - } - debug!("... database initialized."); - Ok(rocket.manage(datastore)) - } - Err(_) => { - Err(rocket) - } - } - }, - Err(_) => Err(rocket) - } - } -} - -#[derive(Clone)] -pub struct DataStore { - client: Client, - database: Database -} - -impl DataStoreApi for DataStore { - fn new(client: Client) -> DataStore{ - DataStore { - client: client.clone(), - database: client.database(DOCUMENT_DB) - } - } -} - - -impl DataStore { - - pub async fn add_document(&self, doc: EncryptedDocument) -> Result{ - debug!("add_document to bucket"); - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - let bucket_update = DocumentBucketUpdate::from(&doc); - let mut update_options = UpdateOptions::default(); - update_options.upsert = Some(true); - let id = format!("^{}_", doc.pid.clone()); - let re = mongodb::bson::Regex{ - pattern: id, - options: String::new() - }; - - let query = doc!{"_id": re, MONGO_PID: doc.pid.clone(), MONGO_COUNTER: mongodb::bson::bson!({"$lt": MAX_NUM_RESPONSE_ENTRIES as i64})}; - - match coll.update_one(query, - doc! { - "$push": { - MONGO_DOC_ARRAY: mongodb::bson::to_bson(&bucket_update).unwrap(), - }, - "$inc": {"counter": 1}, - "$setOnInsert": { "_id": format!("{}_{}", doc.pid.clone(), doc.ts), MONGO_DT_ID: doc.dt_id.clone(), MONGO_FROM_TS: doc.ts}, - "$set": {MONGO_TO_TS: doc.ts}, - }, update_options).await{ - Ok(_r) => { - debug!("added new document: {:#?}", &_r.upserted_id); - Ok(true) - }, - Err(e) => { - error!("failed to store document: {:#?}", &e); - Err(Error::from(e)) - } - } - } - - /// checks if the document exists - /// document ids are globally unique - pub async fn exists_document(&self, id: &String) -> Result { - debug!("Check if document with id '{}' exists...", id); - let query = doc!{format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone()}; - - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - match coll.count_documents(Some(query), None).await? { - 0 => { - debug!("Document with id '{}' does not exist!", &id); - Ok(false) - - }, - _ => { - debug!("... found."); - Ok(true) - } - } - } - - /// gets the model from the db - pub async fn get_document(&self, id: &String, pid: &String) -> Result> { - debug!("Trying to get doc with id {}...", id); - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - - let pipeline = vec![doc! {"$match":{ - MONGO_PID: pid.clone(), - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone() - }}, - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, - doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, - doc! {"$match":{ MONGO_ID: id.clone()}}]; - - let mut results = coll.aggregate(pipeline, None).await?; - - if let Some(result) = results.next().await{ - let doc: EncryptedDocument = bson::from_document(result?)?; - return Ok(Some(doc)) - } - - return Ok(None) - } - - /// gets documents for a single process from the db - pub async fn get_document_with_previous_tc(&self, tc: i64) -> Result> { - let previous_tc = tc - 1; - debug!("Trying to get document for tc {} ...", previous_tc); - if previous_tc < 0 { - info!("... not entry exists."); - Ok(None) - } - else { - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - - let pipeline = vec![doc! {"$match":{ - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TC): previous_tc - }}, - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, - doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, - doc! {"$match":{ MONGO_TC: previous_tc}}]; - - let mut results = coll.aggregate(pipeline, None).await?; - - return if let Some(result) = results.next().await { - debug!("Found {:#?}", &result); - let doc: EncryptedDocument = bson::from_document(result?)?; - Ok(Some(doc)) - } else { - warn!("Document with tc {} not found!", previous_tc); - Ok(None) - } - } - } - - /// gets a page of documents of a specific document type for a single process from the db defined by parameters page, size and sort - pub async fn get_documents_for_pid(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &NaiveDateTime, date_to: &NaiveDateTime) -> Result> { - debug!("...trying to get page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); - - match self.get_start_bucket_size(dt_id, pid, page, size, sort, date_from, date_to).await{ - Ok(bucket_size) => { - let offset = DataStore::get_offset(&bucket_size); - let start_bucket = DataStore::get_start_bucket(page, size, &bucket_size, offset); - trace!("...working with start_bucket {} and offset {} ...", start_bucket, offset); - let start_entry = DataStore::get_start_entry(page, size, start_bucket, &bucket_size, offset); - - trace!("...working with start_entry {} in start_bucket {} and offset {} ...", start_entry, start_bucket, offset); - - let skip_buckets = (start_bucket - 1) as i32; - let sort_order = match sort{ - SortingOrder::Ascending => { - 1 - }, - SortingOrder::Descending => { - - 1 - } - }; - - let pipeline = vec![doc! {"$match":{ - MONGO_PID: pid.clone(), - MONGO_DT_ID: dt_id.clone(), - MONGO_FROM_TS: {"$lte": date_to.timestamp()}, - MONGO_TO_TS: {"$gte": date_from.timestamp()} - }}, - doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, - doc! {"$skip" : skip_buckets}, - // worst case: overlap between two buckets. - doc! {"$limit" : 2}, - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$replaceRoot": { "newRoot": "$documents"}}, - doc! {"$match":{ - MONGO_TS: {"$gte": date_from.timestamp(), "$lte": date_to.timestamp()} - }}, - doc! {"$sort" : {MONGO_TS: sort_order}}, - doc! {"$skip" : start_entry as i32}, - doc! { "$limit": size as i32}]; - - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - - let mut options = AggregateOptions::default(); - options.allow_disk_use = Some(true); - let mut results = coll.aggregate(pipeline, options).await?; - - let mut docs = vec!(); - while let Some(result) = results.next().await{ - let doc: DocumentBucketUpdate = bson::from_document(result?)?; - docs.push(restore_from_bucket(pid, dt_id, doc)); - } - - return Ok(docs) - } - Err(e) => { - error!("Error while getting bucket offset!"); - Err(Error::from(e)) - } - } - } - - /// offset is necessary for duration queries. There, start_entries of bucket depend on timestamps which usually creates an offset in the bucket - async fn get_start_bucket_size(&self, dt_id: &String, pid: &String, page: u64, size: u64, sort: &SortingOrder, date_from: &NaiveDateTime, date_to: &NaiveDateTime) -> Result { - debug!("...trying to get the offset for page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); - let sort_order = match sort{ - SortingOrder::Ascending => { - 1 - }, - SortingOrder::Descending => { - - 1 - } - }; - let coll = self.database.collection::(MONGO_COLL_DOCUMENT_BUCKET); - - debug!("... match with pid: {}, dt_it: {}, to_ts <= {}, from_ts >= {} ...", pid, dt_id, date_from.timestamp(), date_to.timestamp()); - let pipeline = vec![doc! {"$match":{ - MONGO_PID: pid.clone(), - MONGO_DT_ID: dt_id.clone(), - MONGO_FROM_TS: {"$lte": date_to.timestamp()}, - MONGO_TO_TS: {"$gte": date_from.timestamp()} - }}, - // sorting according to sorting order, so we get either the start or end - doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, - doc! {"$limit" : 1}, - // count all relevant documents in the target bucket - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$match":{ - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TS): {"$lte": date_to.timestamp(), "$gte": date_from.timestamp()} - }}, - // modify result to return total number of docs in bucket and number of relevant docs in bucket - doc! { "$group": { "_id": {"total": "$counter"}, "size": { "$sum": 1 } } }, - doc! { "$project": {"_id":0, "capacity": "$_id.total", "size":true}}]; - - let mut options = AggregateOptions::default(); - options.allow_disk_use = Some(true); - let mut results = coll.aggregate(pipeline, options).await?; - let mut bucket_size = DocumentBucketSize{ - capacity: MAX_NUM_RESPONSE_ENTRIES as i32, - size: 0 - }; - while let Some(result) = results.next().await{ - debug!("... retrieved: {:#?}", &result); - let result_bucket: DocumentBucketSize = bson::from_document(result?)?; - bucket_size = result_bucket; - } - debug!("... sending offset: {:?}", bucket_size); - Ok(bucket_size) - } - - fn get_offset(bucket_size: &DocumentBucketSize) -> u64 { - return (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES - } - - fn get_start_bucket(page: u64, size: u64, bucket_size: &DocumentBucketSize, offset: u64) -> u64{ - let docs_to_skip = (page - 1) * size + offset + MAX_NUM_RESPONSE_ENTRIES - bucket_size.capacity as u64; - return (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1 - } - - fn get_start_entry(page: u64, size: u64, start_bucket: u64, bucket_size: &DocumentBucketSize, offset: u64) -> u64{ - // docs to skip calculated by page * size - let docs_to_skip = (page - 1) * size + offset; - let mut start_entry = 0; - if start_bucket > 1 { - start_entry = docs_to_skip - bucket_size.capacity as u64; - if start_entry > 2 { - start_entry = start_entry - (start_bucket - 2) * MAX_NUM_RESPONSE_ENTRIES - } - } - return start_entry - } - -} diff --git a/clearing-house-app/document-api/src/db/tests.rs b/clearing-house-app/document-api/src/db/tests.rs deleted file mode 100644 index c7bc226..0000000 --- a/clearing-house-app/document-api/src/db/tests.rs +++ /dev/null @@ -1,193 +0,0 @@ -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// These tests all access the db, so if you run the tests use -// cargo test -- --test-threads=1 -// otherwise they will interfere with each other -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -use core_lib::db::DataStoreApi; -use core_lib::errors::*; -use core_lib::model::document::EncryptedDocument; -use mongodb::Client; -use crate::db::DataStore; -use chrono::Utc; - -const DATABASE_URL: &'static str = "mongodb://127.0.0.1:27017"; - -async fn db_setup() -> DataStore { - let client = Client::with_uri_str(DATABASE_URL).await.unwrap(); - let db = DataStore::new(client); - db.database.drop(None).await.expect("Database Error"); - db -} - -async fn tear_down(db: DataStore){ - db.database.drop(None).await.expect("Database Error"); -} - -fn create_test_enc_document(id: &String, pid: &String, dt_id: &String) -> EncryptedDocument{ - let mut cts = vec!(); - cts.push(String::from("1::4EBC3F1C2B8CB16C52E41424502FD112015D9C25919C2401514B5DD5B4233B65593CF0A4")); - cts.push(String::from("2::FE2195305E95B9F931660CBA20B4707A1D92123022371CEDD2E70A538A8771EE7540D9F34845BBAEECEC")); - let key_ct = String::from("very secure key ct"); - let ts = Utc::now().timestamp(); - EncryptedDocument::new(id.clone(), pid.clone(), dt_id.clone(), ts, 3241, key_ct, cts) -} - -// DOCUMENT -/// Testcase: Document exists in db and is found -#[tokio::test] -async fn test_document_exists() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data - let pid = String::from("test_document_exists_pid"); - let dt_id = String::from("test_document_exists_dt"); - let id = String::from("test_document_exists_id"); - let doc = create_test_enc_document(&id, &pid, &dt_id); - db.add_document(doc.clone()).await?; - - // run the test - assert_eq!(db.exists_document(&id).await?, true); - - // clean up - tear_down(db).await; - - Ok(()) -} - -/// Testcase: Document does not exist and is not found -#[tokio::test] -async fn test_document_does_not_exist() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data - let pid = String::from("test_document_does_not_exist_pid"); - let dt_id = String::from("test_document_does_not_exist_dt"); - let id1 = String::from("test_document_does_not_exist_pid_id1"); - let id2 = String::from("test_document_does_not_exist_pid_id2"); - let doc = create_test_enc_document(&id1, &pid, &dt_id); - db.add_document(doc.clone()).await?; - - // run the test - assert_eq!(db.exists_document(&id2).await?, false); - - // clean up - tear_down(db).await; - - Ok(()) -} - -/// Testcase: Document does not exist after delete -#[tokio::test] -async fn test_delete_document_doc_is_deleted() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data - let pid = String::from("test_delete_document_doc_is_deleted_pid"); - let dt_id = String::from("test_delete_document_doc_is_deleted_dt"); - let id = String::from("test_delete_document_doc_is_deleted_id"); - let doc = create_test_enc_document(&id, &pid, &dt_id); - db.add_document(doc.clone()).await?; - - // db should be able to find the document - assert_eq!(db.exists_document(&id).await?, true); - - // run the test - //assert!(db.delete_document(&id).await?); - - // db should not find document anymore - assert_eq!(db.exists_document(&id).await?, false); - - // clean up - tear_down(db).await; - - Ok(()) -} - -/// Testcase: Other Documents still exist after delete -#[tokio::test] -async fn test_delete_document_check_others() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data - let pid = String::from("test_delete_document_check_others_pid"); - let dt_id = String::from("test_delete_document_check_others_dt"); - let id1 = String::from("test_delete_document_check_others_id1"); - let id2 = String::from("test_delete_document_check_others_id2"); - let doc1 = create_test_enc_document(&id1, &pid, &dt_id); - let doc2 = create_test_enc_document(&id2, &pid, &dt_id); - db.add_document(doc1.clone()).await?; - db.add_document(doc2.clone()).await?; - - // db should be able to find both documents - assert_eq!(db.exists_document(&id1).await?, true); - assert_eq!(db.exists_document(&id2).await?, true); - - // run the test - //assert!(db.delete_document(&id1).await?); - - // db should still find the other document - assert_eq!(db.exists_document(&id2).await?, true); - - // clean up - tear_down(db).await; - - Ok(()) -} - -/// Testcase: Document does not exist before delete -#[tokio::test] -async fn test_delete_document_on_not_existing_doc() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data - let pid = String::from("test_delete_document_on_not_existing_doc_pid"); - let dt_id = String::from("test_delete_document_on_not_existing_doc_dt"); - let id1 = String::from("test_delete_document_on_not_existing_doc_id1"); - let id2 = String::from("test_delete_document_on_not_existing_doc_id2"); - let doc = create_test_enc_document(&id1, &pid, &dt_id); - db.add_document(doc.clone()).await?; - - // run the test - // assert_eq!(db.delete_document(&id2).await?, false); - - // clean up - tear_down(db).await; - - Ok(()) -} - -/// Testcase: Find the correct document -#[tokio::test] -async fn test_get_document() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data - let pid = String::from("test_get_document_pid"); - let dt_id = String::from("test_get_document_dt"); - let id1 = String::from("test_get_document_id1"); - let id2 = String::from("test_get_document_id2"); - let doc1 = create_test_enc_document(&id1, &pid, &dt_id); - let doc2 = create_test_enc_document(&id2, &pid, &dt_id); - db.add_document(doc1.clone()).await?; - db.add_document(doc2.clone()).await?; - - // db should be able to find both documents - assert_eq!(db.exists_document(&id1).await?, true); - assert_eq!(db.exists_document(&id2).await?, true); - - // the test - let result = db.get_document(&id1, &pid).await?; - assert_eq!(result.is_some(), true); - assert_eq!(result.unwrap().id, id1); - - // clean up - tear_down(db).await; - - Ok(()) -} \ No newline at end of file diff --git a/clearing-house-app/document-api/src/doc_api.rs b/clearing-house-app/document-api/src/doc_api.rs deleted file mode 100644 index ab182d0..0000000 --- a/clearing-house-app/document-api/src/doc_api.rs +++ /dev/null @@ -1,312 +0,0 @@ -use rocket::State; -use chrono::Local; -use core_lib::{ - api::{ - ApiResponse, - client::keyring_api::KeyringApiClient, - crypto::ChClaims, - DocumentReceipt, - QueryResult, - }, - constants::{DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART, ROCKET_DOC_API}, - model::{ - crypto::{KeyCt, KeyCtList}, - document::Document, - parse_date, - sanitize_dates, - SortingOrder, - SortingOrder::{Ascending, Descending}, - validate_dates, - }, -}; -use rocket::fairing::AdHoc; -use rocket::serde::json::{json, Json}; -use std::convert::TryFrom; -use crate::db::DataStore; - - -#[post("/", format = "json", data = "")] -async fn create_enc_document( - ch_claims: ChClaims, - db: &State, - key_api: &State, - document: Json -) -> ApiResponse { - trace!("...user '{:?}'", &ch_claims.client_id); - let doc: Document = document.into_inner(); - // data validation - let payload: Vec = doc.parts.iter() - .filter(|p| String::from(PAYLOAD_PART) == p.name) - .map(|p | p.content.as_ref().unwrap().clone()).collect(); - if payload.len() > 1 { - return ApiResponse::BadRequest(String::from("Document contains two payloads!")); - } - else if payload.len() == 0 { - return ApiResponse::BadRequest(String::from("Document contains no payload!")); - } - - // check if doc id already exists - match db.exists_document(&doc.id).await { - Ok(true) => { - warn!("Document exists already!"); - ApiResponse::BadRequest(String::from("Document exists already!")) - }, - _ => { - debug!("Document does not exists!"); - debug!("getting keys"); - let keys; - match key_api.generate_keys(&ch_claims.client_id, &doc.pid, &doc.dt_id).await { - Ok(key_map) => { - keys = key_map; - debug!("got keys"); - }, - Err(e) => { - error!("Error while retrieving keys: {:?}", e); - return ApiResponse::InternalError(String::from("Error while retrieving keys!")) - }, - }; - - debug!("start encryption"); - let mut enc_doc; - match doc.encrypt(keys) { - Ok(ct) => { - debug!("got ct"); - enc_doc = ct - }, - Err(e) => { - error!("Error while encrypting: {:?}", e); - return ApiResponse::InternalError(String::from("Error while encrypting!")) - }, - }; - - // chain the document to previous documents - debug!("add the chain hash..."); - // get the document with the previous tc - match db.get_document_with_previous_tc(doc.tc).await{ - Ok(Some(previous_doc)) => { - enc_doc.hash = previous_doc.hash(); - }, - Ok(None) => { - if doc.tc == 0{ - info!("No entries found for pid {}. Beginning new chain!", &doc.pid); - } - else{ - // If this happens, db didn't find a tc entry that should exist. - return ApiResponse::InternalError(String::from("Error while creating the chain hash!")) - } - }, - Err(e) => { - error!("Error while creating the chain hash: {:?}", e); - return ApiResponse::InternalError(String::from("Error while creating the chain hash!")) - } - } - - // prepare the success result message - - - let receipt = DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id, &enc_doc.hash); - - debug!("storing document ...."); - // store document - match db.add_document(enc_doc).await { - Ok(_b) => ApiResponse::SuccessCreate(json!(receipt)), - Err(e) => { - error!("Error while adding: {:?}", e); - ApiResponse::InternalError(String::from("Error while storing document!")) - } - } - } - } -} - -#[get("/?&&&&&", format = "json")] -async fn get_enc_documents_for_pid( - ch_claims: ChClaims, - key_api: &State, - db: &State, - doc_type: Option, - page: Option, - size: Option, - sort: Option, - date_from: Option, - date_to: Option, - pid: String) -> ApiResponse { - debug!("Trying to retrieve documents for pid '{}'...", &pid); - trace!("...user '{:?}'", &ch_claims.client_id); - debug!("...page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); - - // Parameter validation for pagination: - // Valid pages start from 1 - // Max page number as of yet unknown - let sanitized_page = match page{ - Some(p) => { - if p > 0{ - u64::try_from(p).unwrap() - } - else{ - warn!("...invalid page requested. Falling back to 1."); - 1 - } - }, - None => 1 - }; - - // Valid sizes are between 1 and MAX_NUM_RESPONSE_ENTRIES (1000) - let sanitized_size = match size{ - Some(s) => { - if s > 0 && s <= i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap() { - u64::try_from(s).unwrap() - } - else{ - warn!("...invalid size requested. Falling back to default."); - DEFAULT_NUM_RESPONSE_ENTRIES - } - }, - None => DEFAULT_NUM_RESPONSE_ENTRIES - }; - - // Sorting order is already validated and defaults to descending - let sanitized_sort = match sort{ - Some(s) => { - s - }, - None => Descending - }; - - // Parsing the dates for duration queries - let parsed_date_from = parse_date(date_from, false); - let parsed_date_to = parse_date(date_to, true); - - // Validation of dates with various checks. If none given dates default to date_now (date_to) and (date_now - 2 weeks) (date_from) - if !validate_dates(parsed_date_from, parsed_date_to){ - debug!("date validation failed!"); - return ApiResponse::BadRequest(String::from("Invalid date parameter!")); - } - let (sanitized_date_from, sanitized_date_to) = sanitize_dates(parsed_date_from, parsed_date_to); - - //new behavior: if pages are "invalid" return {}. Do not adjust page - //either call db with type filter or without to get cts - let start = Local::now(); - debug!("... using pagination with page: {}, size:{} and sort:{:#?}", sanitized_page, sanitized_size, &sanitized_sort); - - let dt_id = match doc_type{ - Some(dt) => dt, - None => String::from(DEFAULT_DOC_TYPE), - }; - let cts = match db.get_documents_for_pid(&dt_id, &pid, sanitized_page, sanitized_size, &sanitized_sort, &sanitized_date_from, &sanitized_date_to).await{ - Ok(cts) => cts, - Err(e) => { - error!("Error while retrieving document: {:?}", e); - return ApiResponse::InternalError(format!("Error while retrieving document for {}", &pid)) - }, - }; - - let result_size = i32::try_from(sanitized_size).ok(); - let result_page = i32::try_from(sanitized_page).ok(); - let result_sort = match sanitized_sort{ - Ascending => String::from("asc"), - Descending => String::from("desc"), - }; - - let mut result = QueryResult::new(sanitized_date_from.timestamp(), sanitized_date_to.timestamp(), result_page, result_size, result_sort, vec!()); - - // The db might contain no documents in which case we get an empty vector - if cts.is_empty(){ - debug!("Queried empty pid: {}", &pid); - ApiResponse::SuccessOk(json!(result)) - } - else{ - // Documents found for pid, now decrypting them - debug!("Found {} documents. Getting keys from keyring...", cts.len()); - let key_cts: Vec = cts.iter() - .map(|e| KeyCt::new(e.id.clone(), e.keys_ct.clone())).collect(); - // caution! we currently only support a single dt per call, so we use the first dt we found - let key_cts_list = KeyCtList::new(cts[0].dt_id.clone(), key_cts); - // decrypt cts - let key_maps = match key_api.decrypt_multiple_keys(&ch_claims.client_id, &pid,&key_cts_list).await{ - Ok(key_map) => { - key_map - } - Err(e) => { - error!("Error while retrieving keys from keyring: {:?}", e); - return ApiResponse::InternalError(format!("Error while retrieving keys from keyring")) - } - }; - debug!("... keys received. Starting decryption..."); - let pts_bulk : Vec = cts.iter().zip(key_maps.iter()) - .filter_map(|(ct,key_map)|{ - if ct.id != key_map.id{ - error!("Document and map don't match"); - }; - match ct.decrypt(key_map.map.keys.clone()){ - Ok(d) => Some(d), - Err(e) => { - warn!("Got empty document from decryption! {:?}", e); - None - } - } - }).collect(); - debug!("...done."); - let end = Local::now(); - let diff = end - start; - info!("Total time taken to run in ms: {}", diff.num_milliseconds()); - result.documents = pts_bulk; - ApiResponse::SuccessOk(json!(result)) - } -} - -/// Retrieve document with id for process with pid -#[get("//?", format = "json")] -async fn get_enc_document(ch_claims: ChClaims, key_api: &State, db: &State, pid: String, id: String, hash: Option) -> ApiResponse { - trace!("...user '{:?}'", &ch_claims.client_id); - trace!("trying to retrieve document with id '{}' for pid '{}'", &id, &pid); - if hash.is_some(){ - debug!("integrity check with hash: {}", hash.as_ref().unwrap()); - } - - match db.get_document(&id, &pid).await{ - //TODO: would like to send "{}" instead of "null" when dt is not found - Ok(Some(ct)) => { - match hex::decode(&ct.keys_ct){ - Ok(key_ct) => { - match key_api.decrypt_keys(&ch_claims.client_id, &pid, &ct.dt_id, &key_ct).await{ - Ok(key_map) => { - //TODO check the hash - match ct.decrypt(key_map.keys){ - Ok(d) => ApiResponse::SuccessOk(json!(d)), - Err(e) => { - warn!("Got empty document from decryption! {:?}", e); - return ApiResponse::NotFound(format!("Document {} not found!", &id)) - } - } - } - Err(e) => { - error!("Error while retrieving keys from keyring: {:?}", e); - return ApiResponse::InternalError(format!("Error while retrieving keys")) - } - } - - }, - Err(e) => { - error!("Error while decoding ciphertext: {:?}", e); - return ApiResponse::InternalError(format!("Key Ciphertext corrupted")) - } - } - }, - Ok(None) => { - debug!("Nothing found in db!"); - return ApiResponse::NotFound(format!("Document {} not found!", &id)) - } - Err(e) => { - error!("Error while retrieving document: {:?}", e); - return ApiResponse::InternalError(format!("Error while retrieving document {}", &id)) - } - } -} - -pub fn mount_api() -> AdHoc { - AdHoc::on_ignite("Mounting Document API", |rocket| async { - rocket - .mount(ROCKET_DOC_API, routes![create_enc_document, get_enc_document, get_enc_documents_for_pid]) - }) -} \ No newline at end of file diff --git a/clearing-house-app/document-api/src/main.rs b/clearing-house-app/document-api/src/main.rs deleted file mode 100644 index 59e38d4..0000000 --- a/clearing-house-app/document-api/src/main.rs +++ /dev/null @@ -1,76 +0,0 @@ -#![forbid(unsafe_code)] - -#[macro_use] extern crate rocket; -#[macro_use] extern crate serde_derive; - -use core_lib::api::client::{ApiClientConfigurator, ApiClientEnum}; -use core_lib::util::{add_service_config, setup_logger}; -use rocket::fairing::AdHoc; -use rocket::http::Method; -use rocket::{Rocket, Build}; -use rocket_cors::{ - AllowedHeaders, AllowedOrigins, - CorsOptions -}; -use core_lib::constants::ENV_DOCUMENT_SERVICE_ID; -use crate::db::DatastoreConfigurator; - -mod doc_api; -mod db; - -fn add_cors_options() -> AdHoc { - AdHoc::on_ignite("Adding CORS rules", |rocket| async { - let allowed_origins = AllowedOrigins::some_exact(&[ - "http://127.0.0.1", - "http://127.0.0.1:4200", - "http://127.0.0.1:8001", - "http://localhost", - "http://localhost:4200", - "http://localhost:8001", - "http://document-gui", - "http://document-gui.local", - "https://127.0.0.1", - "https://127.0.0.1:4200", - "https://127.0.0.1:8001", - "https://localhost", - "https://localhost:4200", - "https://localhost:8001", - "https://document-gui", - "https://document-gui.local" - ]); - - let cors_options = CorsOptions { - allowed_origins, - allowed_methods: vec![Method::Get, Method::Post, Method::Options, Method::Delete].into_iter().map(From::from).collect(), - allowed_headers: AllowedHeaders::some(&[ - "Access-Control-Allow-Origin", - "Access-Control-Allow-Methods", - "Access-Control-Allow-Headers", - "Accept", - "Authorization", - "Content-Type", - "Origin" - ]), - allow_credentials: true, - ..Default::default() - }.to_cors(); - - match cors_options { - Ok(cors) => rocket.attach(cors), - Err(_) => rocket - } - }) -} - -#[launch] -fn rocket() -> Rocket { - // setup logging - setup_logger().expect("Failure to set up the logger! Exiting..."); - - rocket::build() - .attach(doc_api::mount_api()) - .attach(add_cors_options()) - .attach(add_service_config(ENV_DOCUMENT_SERVICE_ID.to_string())) - .attach(DatastoreConfigurator) - .attach(ApiClientConfigurator::new(ApiClientEnum::Keyring)) -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/init_db/default_doc_type.json b/clearing-house-app/init_db/default_doc_type.json similarity index 100% rename from clearing-house-app/keyring-api/init_db/default_doc_type.json rename to clearing-house-app/init_db/default_doc_type.json diff --git a/clearing-house-app/keyring-api/Cargo.toml b/clearing-house-app/keyring-api/Cargo.toml deleted file mode 100644 index dc9c3d0..0000000 --- a/clearing-house-app/keyring-api/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "keyring-api" -version = "0.10.0" -authors = [ - "Mark Gall ", - "Georg Bramm " -] -edition = "2021" - -[dependencies] -aes = "0.6.0" -aes-gcm-siv = "0.9.0" -base64 = "0.9.3" -biscuit = "0.6.0" -chrono = { version = "0.4", features = ["serde"] } -core-lib = {path = "../core-lib" } -error-chain = "0.12.4" -fern = "0.5" -generic-array = "0.14.4" -hex = "0.4.3" -hkdf = "0.10.0" -log = "0.4.14" -mongodb = "2.3.0" -openssl = "0.10.32" -rocket = { version = "0.5.0-rc.1", features = ["json"] } -sha2 = "0.9.3" -serde = "1.0" -serde_derive = "1.0" -serde_json = "1.0" -tokio = "1.8.1" -tokio-test = "0.4.2" -yaml-rust = "0.4" diff --git a/clearing-house-app/keyring-api/Rocket.toml b/clearing-house-app/keyring-api/Rocket.toml deleted file mode 100644 index a2173c7..0000000 --- a/clearing-house-app/keyring-api/Rocket.toml +++ /dev/null @@ -1,18 +0,0 @@ -[global] -limits = { json = 5242880 } - -[debug] -address = "0.0.0.0" -port = 8002 -log_level = "normal" -limits = { forms = 32768 } -database_url = "mongodb://localhost:27018" -clear_db = true - -[release] -address = "0.0.0.0" -port = 8002 -log_level = "normal" -limits = { forms = 32768 } -database_url = "mongodb://keyring-mongo:27017" -clear_db = false diff --git a/clearing-house-app/keyring-api/certs b/clearing-house-app/keyring-api/certs deleted file mode 120000 index 3c23f4b..0000000 --- a/clearing-house-app/keyring-api/certs +++ /dev/null @@ -1 +0,0 @@ -../certs/ \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/api/doc_type_api.rs b/clearing-house-app/keyring-api/src/api/doc_type_api.rs deleted file mode 100644 index ff2cd06..0000000 --- a/clearing-house-app/keyring-api/src/api/doc_type_api.rs +++ /dev/null @@ -1,106 +0,0 @@ -use core_lib::api::ApiResponse; -use core_lib::constants::{ROCKET_DOC_TYPE_API, DEFAULT_PROCESS_ID}; -use rocket::fairing::AdHoc; -use rocket::State; -use rocket::serde::json::{json,Json}; - -use crate::db::KeyStore; -use crate::model::doc_type::DocumentType; - -#[post("/", format = "json", data = "")] -async fn create_doc_type(db: &State, doc_type: Json) -> ApiResponse { - let doc_type: DocumentType = doc_type.into_inner(); - debug!("adding doctype: {:?}", &doc_type); - match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ - Ok(true) => ApiResponse::BadRequest(String::from("doctype already exists!")), - Ok(false) => { - match db.add_document_type(doc_type.clone()).await{ - Ok(()) => ApiResponse::SuccessCreate(json!(doc_type)), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(String::from("Error while adding document type!")) - } - } - }, - Err(e) => { - error!("Error while adding document type: {:?}", e); - return ApiResponse::InternalError(String::from("Error while checking database!")) - } - } -} - -#[post("/", format = "json", data = "")] -async fn update_doc_type(db: &State, id: String, doc_type: Json) -> ApiResponse { - let doc_type: DocumentType = doc_type.into_inner(); - match db.exists_document_type(&doc_type.pid, &doc_type.id).await{ - Ok(true) => ApiResponse::BadRequest(String::from("Doctype already exists!")), - Ok(false) => { - match db.update_document_type(doc_type, &id).await{ - Ok(id) => ApiResponse::SuccessOk(json!(id)), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - return ApiResponse::InternalError(String::from("Error while storing document type!")) - } - } - }, - Err(e) => { - error!("Error while adding document type: {:?}", e); - return ApiResponse::InternalError(String::from("Error while checking database!")) - } - } -} - -#[delete("/", format = "json")] -async fn delete_default_doc_type(db: &State, id: String) -> ApiResponse{ - delete_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await -} - -#[delete("//", format = "json")] -async fn delete_doc_type(db: &State, id: String, pid: String) -> ApiResponse{ - match db.delete_document_type(&id, &pid).await{ - Ok(true) => ApiResponse::SuccessNoContent(String::from("Document type deleted!")), - Ok(false) => ApiResponse::NotFound(String::from("Document type does not exist!")), - Err(e) => { - error!("Error while deleting doctype: {:?}", e); - ApiResponse::InternalError(format!("Error while deleting document type with id {}!", id)) - } - } -} - -#[get("/", format = "json")] -async fn get_default_doc_type(db: &State, id: String) -> ApiResponse { - get_doc_type(db, id, DEFAULT_PROCESS_ID.to_string()).await -} - -#[get("//", format = "json")] -async fn get_doc_type(db: &State, id: String, pid: String) -> ApiResponse { - match db.get_document_type(&id).await{ - //TODO: would like to send "{}" instead of "null" when dt is not found - Ok(dt) => ApiResponse::SuccessOk(json!(dt)), - Err(e) => { - error!("Error while retrieving doctype: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving document type with id {} and pid {}!", id, pid)) - } - } -} - -#[get("/", format = "json")] -async fn get_doc_types(db: &State) -> ApiResponse { - match db.get_all_document_types().await { - //TODO: would like to send "{}" instead of "null" when dt is not found - Ok(dt) => ApiResponse::SuccessOk(json!(dt)), - Err(e) => { - error!("Error while retrieving default doctypes: {:?}", e); - ApiResponse::InternalError(format!("Error while retrieving all document types")) - } - } -} - -pub fn mount_api() -> AdHoc { - AdHoc::on_ignite("Mounting Document Type API", |rocket| async { - rocket - .mount(ROCKET_DOC_TYPE_API, routes![create_doc_type, - update_doc_type, delete_default_doc_type, delete_doc_type, - get_default_doc_type, get_doc_type , get_doc_types]) - }) -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/api/key_api.rs b/clearing-house-app/keyring-api/src/api/key_api.rs deleted file mode 100644 index 50f42dc..0000000 --- a/clearing-house-app/keyring-api/src/api/key_api.rs +++ /dev/null @@ -1,173 +0,0 @@ -use core_lib::api::ApiResponse; -use core_lib::api::crypto::ChClaims; -use core_lib::constants::ROCKET_KEYRING_API; -use core_lib::model::crypto::{KeyCtList, KeyMapListItem}; -use rocket::fairing::AdHoc; -use rocket::State; -use rocket::serde::json::{json, Json}; - -use crate::db::KeyStore; -use crate::crypto::{generate_key_map, restore_key_map}; - -#[get("/generate_keys/<_pid>?", format = "json")] -async fn generate_keys(ch_claims: ChClaims, db: &State, _pid: String, dt_id: String) -> ApiResponse { - trace!("generate_keys"); - trace!("...user '{:?}'", &ch_claims.client_id); - match db.get_msk().await{ - Ok(key) => { - // check that doc type exists for pid - match db.get_document_type(&dt_id).await{ - Ok(Some(dt)) => { - // generate new random key map - match generate_key_map(key, dt) { - Ok(key_map) => { - trace!("response: {:?}", &key_map); - return ApiResponse::SuccessCreate(json!(key_map)); - }, - Err(e) => { - error!("Error while generating key map: {}", e); - return ApiResponse::InternalError(String::from("Error while generating keys")); - } - } - } - Ok(None) =>{ - warn!("document type {} not found", &dt_id); - return ApiResponse::BadRequest(String::from("Document type not found!")); - } - Err(e) => { - warn!("Error while retrieving document type: {}", e); - return ApiResponse::InternalError(String::from("Error while retrieving document type")); - } - } - } - Err(e) => { - error!("Error while retrieving master key: {}", e); - return ApiResponse::InternalError(String::from("Error while generating keys")); - } - } -} - -#[get("/decrypt_keys/<_pid>", format = "json", data = "")] -async fn decrypt_keys(ch_claims: ChClaims, db: &State, _pid: Option, key_cts: Json) -> ApiResponse { - trace!("decrypt_keys"); - trace!("...user '{:?}'", &ch_claims.client_id); - let cts = key_cts.into_inner(); - debug!("number of cts to decrypt: {}", &cts.cts.len()); - - // get master key - match db.get_msk().await{ - Ok(m_key) => { - // check that doc type exists for pid - match db.get_document_type(&cts.dt).await{ - Ok(Some(dt)) => { - let mut dec_error_count = 0; - let mut map_error_count = 0; - // validate keys_ct input - let key_maps : Vec = cts.cts.iter().filter_map( - |key_ct| { - match hex::decode(key_ct.ct.clone()){ - Ok(key) => Some((key_ct.id.clone(), key)), - Err(e) => { - error!("Error while decoding key ciphertext: {}", e); - dec_error_count = dec_error_count + 1; - None - } - } - } - ).filter_map( - |(id, key)| { - match restore_key_map(m_key.clone(), dt.clone(), key){ - Ok(key_map) => { - Some(KeyMapListItem::new(id, key_map)) - }, - Err(e) => { - error!("Error while generating key map: {}", e); - map_error_count = map_error_count + 1; - None - } - } - } - ) - .collect(); - - let error_count = map_error_count + dec_error_count; - - // Currently, we don't tolerate errors while decrypting keys - if error_count > 0 { - return ApiResponse::InternalError(String::from("Error while decrypting keys")); - } - else{ - return ApiResponse::SuccessOk(json!(key_maps)); - } - } - Ok(None) =>{ - warn!("document type {} not found", &cts.dt); - return ApiResponse::BadRequest(String::from("Document type not found!")); - } - Err(e) => { - warn!("Error while retrieving document type: {}", e); - return ApiResponse::NotFound(String::from("Document type not found!")); - } - } - } - Err(e) => { - error!("Error while retrieving master key: {}", e); - return ApiResponse::InternalError(String::from("Error while decrypting keys")); - } - } - -} - -#[get("/decrypt_keys/<_pid>/?", format = "json")] -async fn decrypt_key_map(ch_claims: ChClaims, db: &State, keys_ct: String, _pid: Option, dt_id: String) -> ApiResponse { - trace!("decrypt_key_map"); - trace!("...user '{:?}'", &ch_claims.client_id); - trace!("ct: {}", &keys_ct); - // get master key - match db.get_msk().await{ - Ok(key) => { - // check that doc type exists for pid - match db.get_document_type(&dt_id).await{ - Ok(Some(dt)) => { - // validate keys_ct input - let keys_ct = match hex::decode(keys_ct){ - Ok(key) => key, - Err(e) => { - error!("Error while decoding key ciphertext: {}", e); - return ApiResponse::InternalError(String::from("Error while decrypting keys")); - } - }; - - match restore_key_map(key, dt, keys_ct){ - Ok(key_map) => { - return ApiResponse::SuccessOk(json!(key_map)); - }, - Err(e) => { - error!("Error while generating key map: {}", e); - return ApiResponse::InternalError(String::from("Error while restoring keys")); - } - } - } - Ok(None) =>{ - warn!("document type {} not found", &dt_id); - return ApiResponse::BadRequest(String::from("Document type not found!")); - } - Err(e) => { - warn!("Error while retrieving document type: {}", e); - return ApiResponse::NotFound(String::from("Document type not found!")); - } - } - } - Err(e) => { - error!("Error while retrieving master key: {}", e); - return ApiResponse::InternalError(String::from("Error while decrypting keys")); - } - } -} - -pub fn mount_api() -> AdHoc { - AdHoc::on_ignite("Mounting Keyring API", |rocket| async { - rocket - .mount(ROCKET_KEYRING_API, routes![decrypt_key_map, decrypt_keys, generate_keys]) - }) -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/api/mod.rs b/clearing-house-app/keyring-api/src/api/mod.rs deleted file mode 100644 index 46db62b..0000000 --- a/clearing-house-app/keyring-api/src/api/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod doc_type_api; -pub mod key_api; diff --git a/clearing-house-app/keyring-api/src/crypto.rs b/clearing-house-app/keyring-api/src/crypto.rs deleted file mode 100644 index 584c128..0000000 --- a/clearing-house-app/keyring-api/src/crypto.rs +++ /dev/null @@ -1,161 +0,0 @@ -use aes_gcm_siv::Aes256GcmSiv; -use aes_gcm_siv::aead::{Aead, NewAead}; -use core_lib::errors::*; -use core_lib::model::crypto::{KeyEntry, KeyMap}; -use generic_array::GenericArray; -use hkdf::Hkdf; -use openssl::rand::rand_bytes; -use sha2::Sha256; -use std::collections::HashMap; -use crate::model::doc_type::DocumentType; -use crate::model::crypto::MasterKey; - -const EXP_KEY_SIZE: usize = 32; -const EXP_NONCE_SIZE: usize = 12; -const EXP_BUFF_SIZE: usize = 44; - -fn initialize_kdf() -> (String, Hkdf) { - let salt = generate_random_seed(); - let ikm = generate_random_seed(); - let (master_key, kdf) = Hkdf::::extract(Some(&salt), &ikm); - (hex::encode_upper(master_key), kdf) -} - -pub fn generate_random_seed() -> Vec{ - let mut buf = [0u8; 256]; - rand_bytes(&mut buf).unwrap(); - buf.to_vec() -} - -fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap{ - let mut key_map = HashMap::new(); - let mut okm = [0u8; EXP_BUFF_SIZE]; - let mut i = 0; - dt.parts.iter() - .for_each( |p| { - if kdf.expand(p.name.clone().as_bytes(), &mut okm).is_ok() { - let map_key = match enc{ - true => p.name.clone(), - false => i.to_string() - }; - key_map.insert(map_key, KeyEntry::new(i.to_string(), okm[..EXP_KEY_SIZE].to_vec(), okm[EXP_KEY_SIZE..].to_vec())); - } - i = i +1; - }); - key_map -} - -pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> Result{ - debug!("generating encryption key_map for doc type: '{}'", &dt.id); - let (secret, doc_kdf) = initialize_kdf(); - let key_map = derive_key_map(doc_kdf, dt, true); - - debug!("encrypting the key seed"); - let kdf = restore_kdf(&mkey.key)?; - let mut okm = [0u8; EXP_BUFF_SIZE]; - if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err(){ - bail!("Error while generating key"); - } - match encrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], secret){ - Ok(ct) => Ok(KeyMap::new(true, key_map, Some(ct))), - Err(e) => { - error!("Error while encrypting key seed: {:?}", e); - bail!("Error while encrypting key seed!"); - } - } -} - -pub fn restore_key_map(mkey: MasterKey, dt: DocumentType, keys_ct: Vec) -> Result{ - debug!("decrypting the key seed"); - let kdf = restore_kdf(&mkey.key)?; - let mut okm = [0u8; EXP_BUFF_SIZE]; - if kdf.expand(hex::decode(mkey.salt)?.as_slice(), &mut okm).is_err(){ - bail!("Error while generating key"); - } - - match decrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], &keys_ct){ - Ok(key_seed) => { - // generate new random key map - restore_keys(&key_seed, dt) - } - Err(e) => { - error!("Error while decrypting key ciphertext: {}", e); - bail!("Error while decrypting keys"); - } - } -} - -pub fn restore_keys(secret: &String, dt: DocumentType) -> Result{ - debug!("restoring decryption key_map for doc type: '{}'", &dt.id); - let kdf = restore_kdf(secret)?; - let key_map = derive_key_map(kdf, dt, false); - - Ok(KeyMap::new(false, key_map, None)) -} - -fn restore_kdf(secret: &String) -> Result>{ - debug!("restoring kdf from secret"); - let prk = match hex::decode(secret){ - Ok(key) => key, - Err(e) => { - error!("Error while decoding master key: {}", e); - bail!("Error while encrypting key seed!"); - } - }; - - match Hkdf::::from_prk(prk.as_slice()){ - Ok(kdf) => Ok(kdf), - Err(e) => { - error!("Error while instantiating hkdf: {}", e); - bail!("Error while encrypting key seed!") - } - } -} - -pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> Result>{ - // check key size - if key.len() != EXP_KEY_SIZE { - error!("Given key has size {} but expected {} bytes", key.len(), EXP_KEY_SIZE); - bail!("Incorrect key size") - } - // check nonce size - else if nonce.len() != EXP_NONCE_SIZE { - error!("Given nonce has size {} but expected {} bytes", nonce.len(), EXP_NONCE_SIZE); - bail!("Incorrect nonce size") - } - else{ - let key = GenericArray::from_slice(key); - let nonce = GenericArray::from_slice(nonce); - let cipher = Aes256GcmSiv::new(key); - - match cipher.encrypt(nonce, secret.as_bytes()){ - Ok(ct) => { - Ok(ct) - } - Err(e) => bail!("Error while encrypting {}", e) - } - } -} - -pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> Result{ - debug!("key len = {}", key.len()); - debug!("ct len = {}", ct.len()); - let key = GenericArray::from_slice(key); - let nonce = GenericArray::from_slice(nonce); - let cipher = Aes256GcmSiv::new(key); - - debug!("key: {}", hex::encode_upper(key)); - debug!("nonce: {}", hex::encode_upper(nonce)); - - debug!("ct len = {}", ct.len()); - debug!("nonce len = {}", nonce.len()); - match cipher.decrypt(nonce, ct){ - Ok(pt) => { - let pt = String::from_utf8(pt)?; - Ok(pt) - }, - Err(e) => { - bail!("Error while decrypting: {}", e) - } - } -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/db/crypto.rs b/clearing-house-app/keyring-api/src/db/crypto.rs deleted file mode 100644 index 492ec4c..0000000 --- a/clearing-house-app/keyring-api/src/db/crypto.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::crypto::generate_random_seed; -use hkdf::Hkdf; -use sha2::Sha256; -use core_lib::model::new_uuid; - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct MasterKey { - pub id: String, - pub key: String, - pub salt: Vec -} - -impl MasterKey{ - pub fn new(id: String, key: String, salt: Vec)-> MasterKey{ - MasterKey{ - id, - key, - salt - } - } - - pub fn new_random() -> MasterKey{ - let key_salt = generate_random_seed(); - let ikm = generate_random_seed(); - let (master_key, _) = Hkdf::::extract(Some(&key_salt), &ikm); - - MasterKey{ - id: new_uuid(), - key: hex::encode_upper(master_key), - salt: generate_random_seed() - } - } -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/db/doc_type.rs b/clearing-house-app/keyring-api/src/db/doc_type.rs deleted file mode 100644 index d2944b8..0000000 --- a/clearing-house-app/keyring-api/src/db/doc_type.rs +++ /dev/null @@ -1,87 +0,0 @@ -use core_lib::constants::{MONGO_ID, MONGO_PID, MONGO_COLL_DOC_TYPES}; -use core_lib::errors::*; -use rocket::futures::TryStreamExt; -use mongodb::bson::doc; - -use crate::db::KeyStore; -use crate::model::doc_type::DocumentType; - -impl KeyStore { - // DOCTYPE - pub async fn add_document_type(&self, doc_type: DocumentType) -> Result<()> { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - match coll.insert_one(doc_type.clone(), None).await { - Ok(_r) => { - debug!("added new document type: {}", &_r.inserted_id); - Ok(()) - }, - Err(e) => { - error!("failed to log document type {}", &doc_type.id); - Err(Error::from(e)) - } - } - } - - //TODO: Do we need to check that no documents of this type exist before we remove it from the db? - pub async fn delete_document_type(&self, id: &String, pid: &String) -> Result { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - let result = coll.delete_many(doc! { MONGO_ID: id, MONGO_PID: pid }, None).await?; - if result.deleted_count >= 1 { - Ok(true) - } else { - Ok(false) - } - } - - - /// checks if the model exits - pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> Result { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - let result = coll.find_one(Some(doc! { MONGO_ID: dt_id, MONGO_PID: pid }), None).await?; - match result { - Some(_r) => Ok(true), - None => { - debug!("document type with id {} and pid {:?} does not exist!", &dt_id, &pid); - Ok(false) - } - } - } - - pub async fn get_all_document_types(&self) -> Result> { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - let result = coll.find(None, None).await? - .try_collect().await.unwrap_or_else(|_| vec![]); - Ok(result) - } - - pub async fn get_document_type(&self, dt_id: &String) -> Result> { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - debug!("get_document_type for dt_id: '{}'", dt_id); - match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await{ - Ok(result) => Ok(result), - Err(e) => { - error!("error while getting document type with id {}!", dt_id); - Err(Error::from(e)) - } - } - } - - pub async fn update_document_type(&self, doc_type: DocumentType, id: &String) -> Result { - let coll = self.database.collection::(MONGO_COLL_DOC_TYPES); - match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await{ - Ok(r) => { - if r.matched_count != 1 || r.modified_count != 1{ - warn!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); - } - else{ - debug!("while replacing doc type {} matched '{}' dts and modified '{}'", id, r.matched_count, r.modified_count); - } - Ok(true) - }, - Err(e) => { - error!("error while updating document type with id {}: {:#?}", id, e); - Ok(false) - } - } - } -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/db/mod.rs b/clearing-house-app/keyring-api/src/db/mod.rs deleted file mode 100644 index 002d67b..0000000 --- a/clearing-house-app/keyring-api/src/db/mod.rs +++ /dev/null @@ -1,176 +0,0 @@ -use core_lib::constants::{MONGO_COLL_MASTER_KEY, KEYRING_DB, FILE_DEFAULT_DOC_TYPE, DATABASE_URL, CLEAR_DB, KEYRING_DB_CLIENT}; -use core_lib::db::{DataStoreApi, init_database_client}; -use core_lib::errors::*; -use core_lib::util::read_file; -use mongodb::{Client, Database}; -use rocket::fairing::{self, Fairing, Info, Kind}; -use rocket::futures::TryStreamExt; -use rocket::{Rocket, Build}; -use std::process::exit; - -use crate::model::crypto::MasterKey; -use crate::model::doc_type::DocumentType; - - -pub(crate) mod doc_type; -// TODO: Disabled integration tests with database -// #[cfg(test)] mod tests; - -#[derive(Clone, Debug)] -pub struct KeyStore { - client: Client, - database: Database -} - -impl DataStoreApi for KeyStore { - fn new(client: Client) -> KeyStore{ - KeyStore { - client: client.clone(), - database: client.database(KEYRING_DB) - } - } -} - -#[derive(Clone, Debug)] -pub struct KeyringDbConfigurator; - -#[rocket::async_trait] -impl Fairing for KeyringDbConfigurator { - fn info(&self) -> Info { - Info { - name: "Configuring Keyring Database", - kind: Kind::Ignite - } - } - async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - let db_url: String = rocket.figment().extract_inner(DATABASE_URL).clone().unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { - Ok(value) => { - debug!("clear_db: '{}' found.", &value); - value - }, - Err(_) => { - false - } - }; - debug!("Using database url: '{:#?}'", &db_url); - - match init_database_client::(&db_url.as_str(), Some(KEYRING_DB_CLIENT.to_string())).await { - Ok(keystore) => { - debug!("Check if database is empty..."); - match keystore.client.database(KEYRING_DB) - .list_collection_names(None) - .await { - Ok(colls) => { - debug!("... found collections: {:#?}", &colls); - if colls.len() > 0 && clear_db { - debug!("Database not empty and clear_db == true. Dropping database..."); - match keystore.client.database(KEYRING_DB).drop(None).await { - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(rocket); - } - }; - } - if colls.len() == 0 || clear_db { - debug!("Database empty. Need to initialize..."); - debug!("Adding initial document type..."); - match serde_json::from_str::(&read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or(String::new())) { - Ok(dt) => { - match keystore.add_document_type(dt).await { - Ok(_) => { - debug!("... done."); - }, - Err(e) => { - error!("Error while adding initial document type: {:#?}", e); - return Err(rocket); - } - } - } - _ => { - error!("Error while loading initial document type"); - return Err(rocket); - } - }; - debug!("Creating master key..."); - // create master key - match keystore.store_master_key(MasterKey::new_random()).await { - Ok(true) => { - debug!("... done."); - }, - _ => { - error!("... failed to create master key"); - return Err(rocket); - } - }; - } - debug!("... database initialized."); - Ok(rocket.manage(keystore)) - } - Err(_) => { - Err(rocket) - } - } - }, - Err(_) => Err(rocket) - } - } -} - -impl KeyStore { - - /// Only one master key may exist in the database. - pub async fn store_master_key(&self, key: MasterKey) -> Result{ - debug!("Storing new master key..."); - let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); - debug!("... but first check if there's already one."); - let result= coll.find(None, None).await - .expect("Error retrieving the master keys") - .try_collect().await.unwrap_or_else(|_| vec![]); - - if result.len() > 1{ - error!("Master Key table corrupted!"); - exit(1); - } - if result.len() == 1{ - error!("Master key already exists!"); - Ok(false) - } - else{ - //let db_key = bson::to_bson(&key) - // .expect("failed to serialize master key for database"); - match coll.insert_one(key, None).await{ - Ok(_r) => { - Ok(true) - }, - Err(e) => { - error!("master key could not be stored: {:?}", &e); - panic!("master key could not be stored") - } - } - } - } - - /// Only one master key may exist in the database. - pub async fn get_msk(&self) -> Result { - let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); - let result= coll.find(None, None).await - .expect("Error retrieving the master keys") - .try_collect().await.unwrap_or_else(|_| vec![]); - - if result.len() > 1{ - error!("Master Key table corrupted!"); - exit(1); - } - if result.len() == 1{ - Ok(result[0].clone()) - } - else { - error!("Master Key missing!"); - exit(1); - } - } -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/db/tests.rs b/clearing-house-app/keyring-api/src/db/tests.rs deleted file mode 100644 index af31c01..0000000 --- a/clearing-house-app/keyring-api/src/db/tests.rs +++ /dev/null @@ -1,110 +0,0 @@ -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -// These tests all access the db, so if you run the tests use -// cargo test -- --test-threads=1 -// otherwise they will interfere with each other -// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -use core_lib::errors::*; -use mongodb::Client; - -use crate::db::{DataStoreApi, KeyStore}; -use crate::model::doc_type::DocumentType; - -const DATABASE_URL: &'static str = "mongodb://127.0.0.1:27018"; - -async fn db_setup() -> KeyStore { - let client = Client::with_uri_str(DATABASE_URL).await.unwrap(); - let db = KeyStore::new(client); - db.database.drop(None).await.expect("Database Error"); - db -} - -async fn tear_down(db: KeyStore){ - db.database.drop(None).await.expect("Database Error"); -} - -/// Testcase: Document type exists -#[tokio::test] -async fn test_document_type_exists() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data - let dt = DocumentType::new(String::from("test_document_type_exists_dt_dt"), String::from("test_document_type_exists_dt_pid"), vec!()); - db.add_document_type(dt.clone()).await?; - - // run the test: db should find document type - assert_eq!(db.exists_document_type(&dt.pid, &dt.id).await?, true); - - // clean up - tear_down(db).await; - - Ok(()) -} - - -/// Testcase: Document type exists for other pid and is not found -#[tokio::test] -async fn test_document_type_exists_for_other_pid() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data - let dt = DocumentType::new(String::from("test_document_type_exists_for_other_pid_dt"), String::from("test_document_type_exists_for_other_pid_pid"), vec!()); - let wrong_pid = String::from("the_wrong_pid"); - db.add_document_type(dt.clone()).await?; - - // run the test: db should not find the document type - assert_eq!(db.exists_document_type(&wrong_pid, &dt.id).await?, false); - - // clean up - tear_down(db).await; - - Ok(()) -} - -/// Testcase: Delete on document type with correct pid results in deletion of document type -#[tokio::test] -async fn test_delete_document_type_correct_pid() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data and insert into db - let dt = DocumentType::new(String::from("test_delete_document_type_correct_pid_id"), String::from("test_delete_document_type_correct_pid_pid"), vec!()); - let dt2 = DocumentType::new(String::from("test_delete_document_type_correct_pid_id"), String::from("test_delete_document_type_correct_pid_pid_2"), vec!()); - db.add_document_type(dt.clone()).await?; - db.add_document_type(dt2.clone()).await?; - - // run the test - db.delete_document_type(&dt.id, &dt.pid).await?; - - // db should not find document type - assert_eq!(db.exists_document_type(&dt.pid, &dt.id).await?, false); - - // clean up - tear_down(db).await; - - Ok(()) -} - -/// Testcase: Delete on document type with wrong pid results not in the deletion of document type -#[tokio::test] -async fn test_delete_document_type_wrong_pid() -> Result<()>{ - // empty db and create tables - let db = db_setup().await; - - // prepare test data and insert into db - let dt = DocumentType::new(String::from("test_delete_document_type_correct_pid_id"), String::from("test_delete_document_type_correct_pid_pid"), vec!()); - let wrong_pid = String::from("the_wrong_pid"); - db.add_document_type(dt.clone()).await?; - - // run the test - db.delete_document_type(&dt.id, &wrong_pid).await?; - - // db should still find document type - assert_eq!(db.exists_document_type(&dt.pid, &dt.id).await?, true); - - // clean up - tear_down(db).await; - - Ok(()) -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/main.rs b/clearing-house-app/keyring-api/src/main.rs deleted file mode 100644 index ac4bf78..0000000 --- a/clearing-house-app/keyring-api/src/main.rs +++ /dev/null @@ -1,28 +0,0 @@ -#![forbid(unsafe_code)] - -#[macro_use] extern crate error_chain; -#[macro_use] extern crate rocket; -#[macro_use] extern crate serde_derive; - -use core_lib::util::{add_service_config, setup_logger}; -use rocket::{Build, Rocket}; -use core_lib::constants::ENV_KEYRING_SERVICE_ID; -use crate::db::KeyringDbConfigurator; - -mod api; -mod db; -mod crypto; -mod model; -#[cfg(test)] mod tests; - -#[launch] -fn rocket() -> Rocket { - // setup logging - setup_logger().expect("Failure to set up the logger! Exiting..."); - - rocket::build() - .attach(add_service_config(ENV_KEYRING_SERVICE_ID.to_string())) - .attach(api::key_api::mount_api()) - .attach(api::doc_type_api::mount_api()) - .attach(KeyringDbConfigurator) -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/model/crypto.rs b/clearing-house-app/keyring-api/src/model/crypto.rs deleted file mode 100644 index a700c76..0000000 --- a/clearing-house-app/keyring-api/src/model/crypto.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::crypto::generate_random_seed; -use hkdf::Hkdf; -use sha2::Sha256; -use core_lib::model::new_uuid; - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct MasterKey { - pub id: String, - pub key: String, - pub salt: String -} - -impl MasterKey{ - pub fn new(id: String, key: String, salt: String)-> MasterKey{ - MasterKey{ - id, - key, - salt - } - } - - pub fn new_random() -> MasterKey{ - let key_salt = generate_random_seed(); - let ikm = generate_random_seed(); - let (master_key, _) = Hkdf::::extract(Some(&key_salt), &ikm); - - MasterKey::new(new_uuid(), hex::encode_upper(master_key), hex::encode_upper(generate_random_seed())) - } -} \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/model/doc_type.rs b/clearing-house-app/keyring-api/src/model/doc_type.rs deleted file mode 100644 index 33a7f70..0000000 --- a/clearing-house-app/keyring-api/src/model/doc_type.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct DocumentType { - pub id: String, - pub pid: String, - pub parts: Vec, -} - -impl DocumentType { - pub fn new(id: String, pid: String, parts: Vec) -> DocumentType { - DocumentType{ - id, - pid, - parts, - } - } -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct DocumentTypePart { - pub name: String, -} - -impl DocumentTypePart { - pub fn new(name: String) -> DocumentTypePart{ - DocumentTypePart{ - name - } - } -} diff --git a/clearing-house-app/keyring-api/src/model/mod.rs b/clearing-house-app/keyring-api/src/model/mod.rs deleted file mode 100644 index d1c0859..0000000 --- a/clearing-house-app/keyring-api/src/model/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod doc_type; -pub(crate) mod crypto; \ No newline at end of file diff --git a/clearing-house-app/keyring-api/src/tests.rs b/clearing-house-app/keyring-api/src/tests.rs deleted file mode 100644 index 241c947..0000000 --- a/clearing-house-app/keyring-api/src/tests.rs +++ /dev/null @@ -1,117 +0,0 @@ -use core_lib::errors::*; -use crate::model::doc_type::{DocumentType, DocumentTypePart}; -use crate::crypto::{encrypt_secret, decrypt_secret, generate_key_map, restore_key_map}; -use crate::model::crypto::MasterKey; - -fn create_test_document_type() -> DocumentType{ - let mut parts = vec!(); - parts.push(DocumentTypePart::new(String::from("name"))); - parts.push(DocumentTypePart::new(String::from("message"))); - parts.push(DocumentTypePart::new(String::from("connector"))); - - DocumentType::new(String::from("test_dt_1"), String::from("test_pid_1"), parts) -} - -#[test] -fn test_key_generation() -> Result<()>{ - // prepare test data - let dt = create_test_document_type(); - let k = String::from("C36D50B35B5981C8F1FAD6738848BD5A4F77EF77B56A4E66F7961B9B7A642B2B"); - let salt = String::from("A6E804FF70117E606686EDD8516C95734E239453AB52AC6E3F916D1D861412B574A91B01ECE5F9E4A17B498EDA132792CC9A89C031470950F87AE402B8DDA581410D7E310A5E4204F1467A4E4C240CCB180A84A1B1DE2A06FDB4474C98E78026FDCFB862DE7AC60A4A6772268EE397AF18C28F41DD9A10471E469833EB2092E28AE8D3DD58D98ACC521FC87B99A19912F70376F7E3026C960F903FE7B44F1903A5E36313EE1A8A60B2E317A6443B9408ABBA2763BD3ED42F406F5F19551ED84ADDAD0CD8A652ED72F0040E44CCF3C6CF854D5EA6FBFE9267DB4EBFAD5DE9BA3055049D71CC64A90B081C2A37ED0B5FDDB88AE864436A7D1F14FCA1F969B67F9E"); - let id = String::from("86177e93-29aa-477a-b63f-03ccd9c5679d"); - let mkey = MasterKey::new(id, k, salt); - - // run the test - let keys = generate_key_map(mkey, dt)?; - - // Keymap generated for encryption - assert_eq!(keys.enc, true); - - // there should be 3 items in the hash map - assert_eq!(keys.keys.len(), 3); - - // no key should be the same as another - keys.keys.values().for_each(|i| { - keys.keys.values().for_each(|j|{ - if i.id.ne(&j.id){ - assert!(i.nonce.ne(&j.nonce)); - assert!(i.key.ne(&j.key)); - } - }); - }); - - Ok(()) -} - -#[test] -fn test_restoring_keymap() -> Result<()>{ - // prepare test data - let dt = create_test_document_type(); - let keys_ct = hex::decode("29D816635437C4487DACD93349F6B853EAD8C6F37250901A5BEEF1529E2358BBE634E6D1BD923ED0F2F842DB83139A9786796190DA8DF8F09F0384C8842BA0316079F857C71184C0C4E2A74622D0BED7").unwrap(); - let k = String::from("C36D50B35B5981C8F1FAD6738848BD5A4F77EF77B56A4E66F7961B9B7A642B2B"); - let salt = String::from("A6E804FF70117E606686EDD8516C95734E239453AB52AC6E3F916D1D861412B574A91B01ECE5F9E4A17B498EDA132792CC9A89C031470950F87AE402B8DDA581410D7E310A5E4204F1467A4E4C240CCB180A84A1B1DE2A06FDB4474C98E78026FDCFB862DE7AC60A4A6772268EE397AF18C28F41DD9A10471E469833EB2092E28AE8D3DD58D98ACC521FC87B99A19912F70376F7E3026C960F903FE7B44F1903A5E36313EE1A8A60B2E317A6443B9408ABBA2763BD3ED42F406F5F19551ED84ADDAD0CD8A652ED72F0040E44CCF3C6CF854D5EA6FBFE9267DB4EBFAD5DE9BA3055049D71CC64A90B081C2A37ED0B5FDDB88AE864436A7D1F14FCA1F969B67F9E"); - let id = String::from("86177e93-29aa-477a-b63f-03ccd9c5679d"); - let mkey = MasterKey::new(id, k, salt); - - let mut expected_keys = vec!(); - expected_keys.push(hex::decode("0FCBA316FA47AC0E3EFF4D69B7780925ED22CFF46FC1A731B4E9942FED67BA04").unwrap()); - expected_keys.push(hex::decode("DE888EF80B13390CA76387F18528F3B3948B8C446D70C09F7C2A1D2346CFE917").unwrap()); - expected_keys.push(hex::decode("2E6953A92D081C5189DED6FB9644606257A2839CD2159F77166DF246E236B67C").unwrap()); - - let mut expected_nonces = vec!(); - expected_nonces.push(hex::decode("6A63BE704DC9687FA3FDFF26").unwrap()); - expected_nonces.push(hex::decode("D0E2744835BD2FFECFFA9AE6").unwrap()); - expected_nonces.push(hex::decode("83587A962A24F94D907CF2B7").unwrap()); - - // run the test - let result = restore_key_map(mkey, dt, keys_ct)?; - - // Keymap generated for decryption - assert_eq!(result.enc, false); - - // there should be 3 items in the hash map - assert_eq!(result.keys.len(), 3); - - // check the derived keys and nonces - result.keys.values().for_each(|i| { - let index = i.id.parse::().unwrap(); - assert_eq!(i.key, expected_keys[index]); - assert_eq!(i.nonce, expected_nonces[index]); - }); - - Ok(()) -} - - -#[test] -fn test_encrypting_secret() -> Result<()>{ - // prepare test data - let key = hex::decode("9530D8826CCE9D6CF377B849D63C7155F78343120A303D55F1A9BECAF25E9713").unwrap(); - let nonce = hex::decode("2C0802076377687B9A403120").unwrap(); - let secret = String::from("1EB18B9FC8CBA07F2EA00BC00FBE468AB1D48E2E28F14FAD61EA3A38B41E2586"); - let expected_ct = hex::decode("CAE855AF0FD950A25F2D629A344F2B51530EE98990A77D4B49868C3EB497913A9E936D9DBF9487A77A7B36709C8F1AE43A40D779D7D56351A606675A04FCE5F8B7E80C06B3E9A47083C2E604AD5F681D").unwrap(); - - // run the test - let result = encrypt_secret(key.as_slice(), nonce.as_slice(), secret.clone())?; - - assert_eq!(expected_ct, result); - - Ok(()) -} - -#[test] -fn test_decrypting_secret() -> Result<()>{ - // prepare test data - let key = hex::decode("9530D8826CCE9D6CF377B849D63C7155F78343120A303D55F1A9BECAF25E9713").unwrap(); - let nonce = hex::decode("2C0802076377687B9A403120").unwrap(); - let ct = hex::decode("CAE855AF0FD950A25F2D629A344F2B51530EE98990A77D4B49868C3EB497913A9E936D9DBF9487A77A7B36709C8F1AE43A40D779D7D56351A606675A04FCE5F8B7E80C06B3E9A47083C2E604AD5F681D").unwrap(); - let expected_secret = String::from("1EB18B9FC8CBA07F2EA00BC00FBE468AB1D48E2E28F14FAD61EA3A38B41E2586"); - - // run the test - let result = decrypt_secret(key.as_slice(), nonce.as_slice(), ct.as_slice())?; - - // check the decryption - assert_eq!(expected_secret, result); - - Ok(()) -} diff --git a/clearing-house-app/logging-service/keys/.DS_Store b/clearing-house-app/keys/.DS_Store similarity index 100% rename from clearing-house-app/logging-service/keys/.DS_Store rename to clearing-house-app/keys/.DS_Store diff --git a/clearing-house-app/logging-service/keys/private_key.der b/clearing-house-app/keys/private_key.der similarity index 100% rename from clearing-house-app/logging-service/keys/private_key.der rename to clearing-house-app/keys/private_key.der diff --git a/clearing-house-app/logging-service/keys/private_key_2048.der b/clearing-house-app/keys/private_key_2048.der similarity index 100% rename from clearing-house-app/logging-service/keys/private_key_2048.der rename to clearing-house-app/keys/private_key_2048.der diff --git a/clearing-house-app/logging-service/certs b/clearing-house-app/logging-service/certs deleted file mode 120000 index 36343b9..0000000 --- a/clearing-house-app/logging-service/certs +++ /dev/null @@ -1 +0,0 @@ -../certs \ No newline at end of file diff --git a/clearing-house-app/logging-service/init_db/default_doc_type.json b/clearing-house-app/logging-service/init_db/default_doc_type.json deleted file mode 100644 index 5e1f843..0000000 --- a/clearing-house-app/logging-service/init_db/default_doc_type.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "id": "IDS_MESSAGE", - "pid": "default", - "parts": [ - { - "name": "model_version" - }, - { - "name": "correlation_message" - }, - { - "name": "transfer_contract" - }, - { - "name": "issued" - }, - { - "name": "issuer_connector" - }, - { - "name": "content_version" - }, - { - "name": "recipient_connector" - }, - { - "name": "sender_agent" - }, - { - "name": "recipient_agent" - }, - { - "name": "payload" - }, - { - "name": "payload_type" - }, - { - "name": "message_id" - } - ] -} \ No newline at end of file diff --git a/clearing-house-app/logging-service/src/config.rs b/clearing-house-app/src/config.rs similarity index 100% rename from clearing-house-app/logging-service/src/config.rs rename to clearing-house-app/src/config.rs diff --git a/clearing-house-app/logging-service/src/crypto.rs b/clearing-house-app/src/crypto.rs similarity index 100% rename from clearing-house-app/logging-service/src/crypto.rs rename to clearing-house-app/src/crypto.rs diff --git a/clearing-house-app/logging-service/src/db/config/doc_store.rs b/clearing-house-app/src/db/config/doc_store.rs similarity index 100% rename from clearing-house-app/logging-service/src/db/config/doc_store.rs rename to clearing-house-app/src/db/config/doc_store.rs diff --git a/clearing-house-app/logging-service/src/db/config/keyring_store.rs b/clearing-house-app/src/db/config/keyring_store.rs similarity index 100% rename from clearing-house-app/logging-service/src/db/config/keyring_store.rs rename to clearing-house-app/src/db/config/keyring_store.rs diff --git a/clearing-house-app/logging-service/src/db/config/mod.rs b/clearing-house-app/src/db/config/mod.rs similarity index 100% rename from clearing-house-app/logging-service/src/db/config/mod.rs rename to clearing-house-app/src/db/config/mod.rs diff --git a/clearing-house-app/logging-service/src/db/config/process_store.rs b/clearing-house-app/src/db/config/process_store.rs similarity index 100% rename from clearing-house-app/logging-service/src/db/config/process_store.rs rename to clearing-house-app/src/db/config/process_store.rs diff --git a/clearing-house-app/logging-service/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs similarity index 100% rename from clearing-house-app/logging-service/src/db/doc_store.rs rename to clearing-house-app/src/db/doc_store.rs diff --git a/clearing-house-app/logging-service/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs similarity index 100% rename from clearing-house-app/logging-service/src/db/key_store.rs rename to clearing-house-app/src/db/key_store.rs diff --git a/clearing-house-app/logging-service/src/db/mod.rs b/clearing-house-app/src/db/mod.rs similarity index 100% rename from clearing-house-app/logging-service/src/db/mod.rs rename to clearing-house-app/src/db/mod.rs diff --git a/clearing-house-app/logging-service/src/db/process_store.rs b/clearing-house-app/src/db/process_store.rs similarity index 100% rename from clearing-house-app/logging-service/src/db/process_store.rs rename to clearing-house-app/src/db/process_store.rs diff --git a/clearing-house-app/logging-service/src/main.rs b/clearing-house-app/src/main.rs similarity index 100% rename from clearing-house-app/logging-service/src/main.rs rename to clearing-house-app/src/main.rs diff --git a/clearing-house-app/logging-service/src/model/claims.rs b/clearing-house-app/src/model/claims.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/claims.rs rename to clearing-house-app/src/model/claims.rs diff --git a/clearing-house-app/logging-service/src/model/constants.rs b/clearing-house-app/src/model/constants.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/constants.rs rename to clearing-house-app/src/model/constants.rs diff --git a/clearing-house-app/logging-service/src/model/crypto.rs b/clearing-house-app/src/model/crypto.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/crypto.rs rename to clearing-house-app/src/model/crypto.rs diff --git a/clearing-house-app/logging-service/src/model/doc_type.rs b/clearing-house-app/src/model/doc_type.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/doc_type.rs rename to clearing-house-app/src/model/doc_type.rs diff --git a/clearing-house-app/logging-service/src/model/document.rs b/clearing-house-app/src/model/document.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/document.rs rename to clearing-house-app/src/model/document.rs diff --git a/clearing-house-app/logging-service/src/model/errors.rs b/clearing-house-app/src/model/errors.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/errors.rs rename to clearing-house-app/src/model/errors.rs diff --git a/clearing-house-app/logging-service/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/ids/message.rs rename to clearing-house-app/src/model/ids/message.rs diff --git a/clearing-house-app/logging-service/src/model/ids/mod.rs b/clearing-house-app/src/model/ids/mod.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/ids/mod.rs rename to clearing-house-app/src/model/ids/mod.rs diff --git a/clearing-house-app/logging-service/src/model/ids/request.rs b/clearing-house-app/src/model/ids/request.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/ids/request.rs rename to clearing-house-app/src/model/ids/request.rs diff --git a/clearing-house-app/logging-service/src/model/mod.rs b/clearing-house-app/src/model/mod.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/mod.rs rename to clearing-house-app/src/model/mod.rs diff --git a/clearing-house-app/logging-service/src/model/process.rs b/clearing-house-app/src/model/process.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/process.rs rename to clearing-house-app/src/model/process.rs diff --git a/clearing-house-app/logging-service/src/model/util.rs b/clearing-house-app/src/model/util.rs similarity index 100% rename from clearing-house-app/logging-service/src/model/util.rs rename to clearing-house-app/src/model/util.rs diff --git a/clearing-house-app/logging-service/src/ports/doc_type_api.rs b/clearing-house-app/src/ports/doc_type_api.rs similarity index 100% rename from clearing-house-app/logging-service/src/ports/doc_type_api.rs rename to clearing-house-app/src/ports/doc_type_api.rs diff --git a/clearing-house-app/logging-service/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs similarity index 100% rename from clearing-house-app/logging-service/src/ports/logging_api.rs rename to clearing-house-app/src/ports/logging_api.rs diff --git a/clearing-house-app/logging-service/src/ports/mod.rs b/clearing-house-app/src/ports/mod.rs similarity index 100% rename from clearing-house-app/logging-service/src/ports/mod.rs rename to clearing-house-app/src/ports/mod.rs diff --git a/clearing-house-app/logging-service/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs similarity index 100% rename from clearing-house-app/logging-service/src/services/document_service.rs rename to clearing-house-app/src/services/document_service.rs diff --git a/clearing-house-app/logging-service/src/services/keyring_service.rs b/clearing-house-app/src/services/keyring_service.rs similarity index 100% rename from clearing-house-app/logging-service/src/services/keyring_service.rs rename to clearing-house-app/src/services/keyring_service.rs diff --git a/clearing-house-app/logging-service/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs similarity index 100% rename from clearing-house-app/logging-service/src/services/logging_service.rs rename to clearing-house-app/src/services/logging_service.rs diff --git a/clearing-house-app/logging-service/src/services/mod.rs b/clearing-house-app/src/services/mod.rs similarity index 100% rename from clearing-house-app/logging-service/src/services/mod.rs rename to clearing-house-app/src/services/mod.rs diff --git a/clearing-house-app/logging-service/src/util.rs b/clearing-house-app/src/util.rs similarity index 100% rename from clearing-house-app/logging-service/src/util.rs rename to clearing-house-app/src/util.rs From 9af75cf760173fda5d1fad4bf4ddbefd21224413 Mon Sep 17 00:00:00 2001 From: dhommen Date: Tue, 29 Aug 2023 12:20:25 +0200 Subject: [PATCH 046/183] fix(ci): disable rust workflow (dublicate build) --- .github/workflows/rust.yml | 92 +++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9881ac3..b20439c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,54 +1,54 @@ -name: Rust - -on: - push: - branches: - - master - - development - pull_request: - branches: - - master - -env: - CARGO_TERM_COLOR: always - IMAGE_NAME_LS: ids-ch-logging-service - IMAGE_BASE: ghcr.io/truzzt/ids-basecamp-clearinghouse - - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - # TODO: do not use caching for actual release builds, aka ones that start with v* - - uses: Swatinem/rust-cache@v2 - - name: Build clearing-house-api - working-directory: ./clearing-house-app - run: cargo build --release - - - name: Build build images - run: | - docker build . --file docker/logging-service.Dockerfile --tag $IMAGE_NAME_LS +# name: Rust + +# on: +# push: +# branches: +# - master +# - development +# pull_request: +# branches: +# - master + +# env: +# CARGO_TERM_COLOR: always +# IMAGE_NAME_LS: ids-ch-logging-service +# IMAGE_BASE: ghcr.io/truzzt/ids-basecamp-clearinghouse + + +# jobs: +# build: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v3 +# # TODO: do not use caching for actual release builds, aka ones that start with v* +# - uses: Swatinem/rust-cache@v2 +# - name: Build clearing-house-api +# working-directory: ./clearing-house-app +# run: cargo build --release + +# - name: Build build images +# run: | +# docker build . --file docker/logging-service.Dockerfile --tag $IMAGE_NAME_LS - - name: Log into registry - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin +# - name: Log into registry +# run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Push image - run: | - IMAGE_ID_LS=$IMAGE_BASE/$IMAGE_NAME_LS +# - name: Push image +# run: | +# IMAGE_ID_LS=$IMAGE_BASE/$IMAGE_NAME_LS - # Change all uppercase to lowercase - IMAGE_ID_LS=$(echo $IMAGE_ID_LS | tr '[A-Z]' '[a-z]') +# # Change all uppercase to lowercase +# IMAGE_ID_LS=$(echo $IMAGE_ID_LS | tr '[A-Z]' '[a-z]') - # Strip git ref prefix from version - VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') +# # Strip git ref prefix from version +# VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') - # Strip "v" prefix from tag name - [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') +# # Strip "v" prefix from tag name +# [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - # Use Docker `latest` tag convention - [ "$VERSION" == "master" ] && VERSION=latest +# # Use Docker `latest` tag convention +# [ "$VERSION" == "master" ] && VERSION=latest - docker tag $IMAGE_NAME_LS $IMAGE_ID_LS:$VERSION - docker push $IMAGE_ID_LS:$VERSION +# docker tag $IMAGE_NAME_LS $IMAGE_ID_LS:$VERSION +# docker push $IMAGE_ID_LS:$VERSION From e2784b9b642987cc1ddb9ffa2ca7057cb6382d25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 29 Aug 2023 13:55:44 +0200 Subject: [PATCH 047/183] fix(ch-app): Reenable new serde crates, due to resolved issues with precompiled binaries --- clearing-house-app/Cargo.lock | 20 ++++++++++---------- clearing-house-app/Cargo.toml | 4 +--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 97b1b49..636ced4 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -460,9 +460,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.1" +version = "5.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd72493923899c6f10c641bdbdeddc7183d6396641d99c1a0d1597f37f92e28" +checksum = "9b101bb8960ab42ada6ae98eb82afcea4452294294c45b681295af26610d6d28" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -1905,9 +1905,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] @@ -1923,9 +1923,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", @@ -2206,9 +2206,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.26" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79d09ac6b08c1ab3906a2f7cc2e81a0e27c7ae89c63812df75e52bef0751e07" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ "deranged", "itoa", @@ -2225,9 +2225,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c65469ed6b3a4809d987a41eb1dc918e9bc1d92211cbad7ae82931846f7451" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ "time-core", ] diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 24a777d..a5de761 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -13,9 +13,7 @@ chrono = { version = "0.4.26", features = ["serde", "clock", "std"], default-fea mongodb = { version = ">= 2.6.1" , features = ["openssl-tls"]} percent-encoding = "2.1.0" rocket = { version = "0.5.0-rc.1", features = ["json"] } -# Restricted version to 1.0.171, because of a change in derive macro for serde. It introduced precompiled binary files -# for code generation, which is by many developers not considered a secure codeing practice. -serde = { version = "= 1.0.171", features = ["derive"] } +serde = { version = ">1.0.184", features = ["derive"] } serde_json = "1" anyhow = "1" hex = "0.4.3" From 789404f8139cacc49bc8a1d377d1d3cd01c39411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 29 Aug 2023 14:27:49 +0200 Subject: [PATCH 048/183] refactor(ch-app): Remove separate db/config module.. --- clearing-house-app/src/db/config/doc_store.rs | 159 ------------------ .../src/db/config/keyring_store.rs | 125 -------------- clearing-house-app/src/db/config/mod.rs | 3 - .../src/db/config/process_store.rs | 115 ------------- clearing-house-app/src/db/doc_store.rs | 128 +++++++++++++- clearing-house-app/src/db/key_store.rs | 86 +++++++++- clearing-house-app/src/db/mod.rs | 1 - clearing-house-app/src/db/process_store.rs | 74 +++++++- clearing-house-app/src/main.rs | 12 +- 9 files changed, 277 insertions(+), 426 deletions(-) delete mode 100644 clearing-house-app/src/db/config/doc_store.rs delete mode 100644 clearing-house-app/src/db/config/keyring_store.rs delete mode 100644 clearing-house-app/src/db/config/mod.rs delete mode 100644 clearing-house-app/src/db/config/process_store.rs diff --git a/clearing-house-app/src/db/config/doc_store.rs b/clearing-house-app/src/db/config/doc_store.rs deleted file mode 100644 index d41b3e0..0000000 --- a/clearing-house-app/src/db/config/doc_store.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::db::doc_store::DataStore; -use crate::db::init_database_client; -use crate::model::constants::{ - CLEAR_DB, DATABASE_URL, DOCUMENT_DB, DOCUMENT_DB_CLIENT, MONGO_COLL_DOCUMENT_BUCKET, - MONGO_DOC_ARRAY, MONGO_PID, MONGO_TC, MONGO_TS, -}; -use crate::model::document::Document; -use anyhow::anyhow; -use mongodb::bson::doc; -use mongodb::options::{CreateCollectionOptions, IndexOptions, WriteConcern}; -use mongodb::IndexModel; -use rocket::{fairing, Build, Rocket}; - -#[derive(Clone, Debug)] -pub struct DatastoreConfigurator; - -#[rocket::async_trait] -impl fairing::Fairing for DatastoreConfigurator { - fn info(&self) -> fairing::Info { - fairing::Info { - name: "Configuring Document Database", - kind: fairing::Kind::Ignite, - } - } - async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - let db_url: String = rocket - .figment() - .extract_inner(DATABASE_URL) - .clone() - .unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { - Ok(value) => { - debug!("clear_db: '{}' found.", &value); - value - } - Err(_) => false, - }; - - match Self::init_datastore(db_url, clear_db).await { - Ok(datastore) => Ok(rocket.manage(datastore)), - Err(_) => Err(rocket), - } - } -} - -impl DatastoreConfigurator { - pub async fn init_datastore(db_url: String, clear_db: bool) -> anyhow::Result { - debug!("Using mongodb url: '{:#?}'", &db_url); - match init_database_client::( - db_url.as_str(), - Some(DOCUMENT_DB_CLIENT.to_string()), - ) - .await - { - Ok(datastore) => { - debug!("Check if database is empty..."); - match datastore - .client - .database(DOCUMENT_DB) - .list_collection_names(None) - .await - { - Ok(colls) => { - debug!("... found collections: {:#?}", &colls); - let number_of_colls = - match colls.contains(&MONGO_COLL_DOCUMENT_BUCKET.to_string()) { - true => colls.len(), - false => 0, - }; - - if number_of_colls > 0 && clear_db { - debug!("Database not empty and clear_db == true. Dropping database..."); - match datastore.client.database(DOCUMENT_DB).drop(None).await { - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(anyhow!("Failed to drop database")); - } - }; - } - if number_of_colls == 0 || clear_db { - debug!("Database empty. Need to initialize..."); - let mut write_concern = WriteConcern::default(); - write_concern.journal = Some(true); - let mut options = CreateCollectionOptions::default(); - options.write_concern = Some(write_concern); - debug!("Create collection {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore - .client - .database(DOCUMENT_DB) - .create_collection(MONGO_COLL_DOCUMENT_BUCKET, options) - .await - { - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(anyhow!("Failed to create collection")); - } - }; - - // This purpose of this index is to ensure that the transaction counter is unique - let mut index_options = IndexOptions::default(); - index_options.unique = Some(true); - let mut index_model = IndexModel::default(); - index_model.keys = doc! {format!("{}.{}",MONGO_DOC_ARRAY, MONGO_TC): 1}; - index_model.options = Some(index_options); - - debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore - .client - .database(DOCUMENT_DB) - .collection::(MONGO_COLL_DOCUMENT_BUCKET) - .create_index(index_model, None) - .await - { - Ok(result) => { - debug!("... index {} created", result.index_name); - } - Err(_) => { - debug!("... failed."); - return Err(anyhow!("Failed to create index")); - } - } - - // This creates a compound index over pid and the timestamp to enable paging using buckets - let mut compound_index_model = IndexModel::default(); - compound_index_model.keys = doc! {MONGO_PID: 1, MONGO_TS: 1}; - - debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore - .client - .database(DOCUMENT_DB) - .collection::(MONGO_COLL_DOCUMENT_BUCKET) - .create_index(compound_index_model, None) - .await - { - Ok(result) => { - debug!("... index {} created", result.index_name); - } - Err(_) => { - debug!("... failed."); - return Err(anyhow!("Failed to create compound index")); - } - } - } - debug!("... database initialized."); - Ok(datastore) - } - Err(_) => Err(anyhow!("Failed to list collections")), - } - } - Err(_) => Err(anyhow!("Failed to initialize database client")), - } - } -} diff --git a/clearing-house-app/src/db/config/keyring_store.rs b/clearing-house-app/src/db/config/keyring_store.rs deleted file mode 100644 index 79ea1b9..0000000 --- a/clearing-house-app/src/db/config/keyring_store.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::db::init_database_client; -use crate::db::key_store::KeyStore; -use crate::model::constants::{ - CLEAR_DB, DATABASE_URL, FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, -}; -use crate::model::crypto::MasterKey; -use crate::model::doc_type::DocumentType; -use crate::util::read_file; -use anyhow::anyhow; -use rocket::fairing::Kind; -use rocket::{fairing, Build, Rocket}; - -#[derive(Clone, Debug)] -pub struct KeyringDbConfigurator; - -#[rocket::async_trait] -impl fairing::Fairing for KeyringDbConfigurator { - fn info(&self) -> fairing::Info { - fairing::Info { - name: "Configuring Keyring Database", - kind: Kind::Ignite, - } - } - async fn on_ignite(&self, rocket: Rocket) -> fairing::Result { - let db_url: String = rocket - .figment() - .extract_inner(DATABASE_URL) - .clone() - .unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { - Ok(value) => { - debug!("clear_db: '{}' found.", &value); - value - } - Err(_) => false, - }; - - match Self::init_keystore(db_url, clear_db).await { - Ok(keystore) => Ok(rocket.manage(keystore)), - Err(_) => Err(rocket), - } - } -} - -impl KeyringDbConfigurator { - pub async fn init_keystore(db_url: String, clear_db: bool) -> anyhow::Result { - debug!("Using database url: '{:#?}'", &db_url); - - match init_database_client::( - db_url.as_str(), - Some(KEYRING_DB_CLIENT.to_string()), - ) - .await - { - Ok(keystore) => { - debug!("Check if database is empty..."); - match keystore - .client - .database(KEYRING_DB) - .list_collection_names(None) - .await - { - Ok(colls) => { - debug!("... found collections: {:#?}", &colls); - if !colls.is_empty() && clear_db { - debug!("Database not empty and clear_db == true. Dropping database..."); - match keystore.client.database(KEYRING_DB).drop(None).await { - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(anyhow!("Failed to drop database")); - } - }; - } - if colls.is_empty() || clear_db { - debug!("Database empty. Need to initialize..."); - debug!("Adding initial document type..."); - match serde_json::from_str::( - &read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or(String::new()), - ) { - Ok(dt) => match keystore.add_document_type(dt).await { - Ok(_) => { - debug!("... done."); - } - Err(e) => { - error!( - "Error while adding initial document type: {:#?}", - e - ); - return Err(anyhow!( - "Error while adding initial document type" - )); - } - }, - _ => { - error!("Error while loading initial document type"); - return Err(anyhow!( - "Error while loading initial document type" - )); - } - }; - debug!("Creating master key..."); - // create master key - match keystore.store_master_key(MasterKey::new_random()).await { - Ok(true) => { - debug!("... done."); - } - _ => { - error!("... failed to create master key"); - return Err(anyhow!("Failed to create master key")); - } - }; - } - debug!("... database initialized."); - Ok(keystore) - } - Err(_) => Err(anyhow!("Failed to list collections")), - } - } - Err(_) => Err(anyhow!("Failed to initialize database client")), - } - } -} diff --git a/clearing-house-app/src/db/config/mod.rs b/clearing-house-app/src/db/config/mod.rs deleted file mode 100644 index 7567b5b..0000000 --- a/clearing-house-app/src/db/config/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub(crate) mod doc_store; -pub(crate) mod keyring_store; -pub(crate) mod process_store; diff --git a/clearing-house-app/src/db/config/process_store.rs b/clearing-house-app/src/db/config/process_store.rs deleted file mode 100644 index d8b1a62..0000000 --- a/clearing-house-app/src/db/config/process_store.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::db::init_database_client; -use crate::db::process_store::ProcessStore; -use crate::model::constants::{ - CLEAR_DB, DATABASE_URL, MONGO_COLL_TRANSACTIONS, PROCESS_DB, PROCESS_DB_CLIENT, -}; -use anyhow::anyhow; -use mongodb::options::{CreateCollectionOptions, WriteConcern}; -use rocket::fairing::Kind; -use rocket::{Build, Rocket}; - -#[derive(Clone, Debug)] -pub struct ProcessStoreConfigurator; - -#[rocket::async_trait] -impl rocket::fairing::Fairing for ProcessStoreConfigurator { - fn info(&self) -> rocket::fairing::Info { - rocket::fairing::Info { - name: "Configuring Process Database", - kind: Kind::Ignite, - } - } - async fn on_ignite(&self, rocket: Rocket) -> rocket::fairing::Result { - debug!("Preparing to initialize database..."); - let db_url: String = rocket - .figment() - .extract_inner(DATABASE_URL) - .clone() - .unwrap(); - let clear_db = match rocket.figment().extract_inner(CLEAR_DB) { - Ok(value) => { - debug!("...clear_db: {} found. ", &value); - value - } - Err(_) => false, - }; - - match Self::init_process_store(db_url, clear_db).await { - Ok(process_store) => { - debug!("...done."); - Ok(rocket.manage(process_store)) - } - Err(_) => Err(rocket), - } - } -} - -impl ProcessStoreConfigurator { - pub async fn init_process_store( - db_url: String, - clear_db: bool, - ) -> anyhow::Result { - debug!("...using database url: '{:#?}'", &db_url); - - match init_database_client::( - db_url.as_str(), - Some(PROCESS_DB_CLIENT.to_string()), - ) - .await - { - Ok(process_store) => { - debug!("...check if database is empty..."); - match process_store - .client - .database(PROCESS_DB) - .list_collection_names(None) - .await - { - Ok(colls) => { - debug!("... found collections: {:#?}", &colls); - if !colls.is_empty() && clear_db { - debug!( - "...database not empty and clear_db == true. Dropping database..." - ); - match process_store.client.database(PROCESS_DB).drop(None).await { - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(anyhow!("Failed to drop database")); - } - }; - } - if colls.is_empty() || clear_db { - debug!("..database empty. Need to initialize..."); - let mut write_concern = WriteConcern::default(); - write_concern.journal = Some(true); - let mut options = CreateCollectionOptions::default(); - options.write_concern = Some(write_concern); - debug!("...create collection {} ...", MONGO_COLL_TRANSACTIONS); - match process_store - .client - .database(PROCESS_DB) - .create_collection(MONGO_COLL_TRANSACTIONS, options) - .await - { - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(anyhow!("Failed to create collection")); - } - }; - } - debug!("... database initialized."); - Ok(process_store) - } - Err(_) => Err(anyhow!("Failed to list collections")), - } - } - Err(_) => Err(anyhow!("Failed to initialize database client")), - } - } -} diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 544e932..6beeac9 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -1,16 +1,13 @@ +use anyhow::anyhow; use crate::db::doc_store::bucket::{restore_from_bucket, DocumentBucketSize, DocumentBucketUpdate}; -use crate::db::DataStoreApi; -use crate::model::constants::{ - DOCUMENT_DB, MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_COUNTER, - MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_ID, MONGO_PID, MONGO_TC, MONGO_TO_TS, - MONGO_TS, -}; -use crate::model::document::EncryptedDocument; +use crate::db::{DataStoreApi, init_database_client}; +use crate::model::constants::{DOCUMENT_DB, DOCUMENT_DB_CLIENT, MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_ID, MONGO_PID, MONGO_TC, MONGO_TO_TS, MONGO_TS}; +use crate::model::document::{Document, EncryptedDocument}; use crate::model::errors::*; use crate::model::SortingOrder; use mongodb::bson::doc; -use mongodb::options::{AggregateOptions, UpdateOptions}; -use mongodb::{bson, Client}; +use mongodb::options::{AggregateOptions, CreateCollectionOptions, IndexOptions, UpdateOptions, WriteConcern}; +use mongodb::{bson, Client, IndexModel}; use rocket::futures::StreamExt; #[derive(Clone)] @@ -29,6 +26,119 @@ impl DataStoreApi for DataStore { } impl DataStore { + pub async fn init_datastore(db_url: String, clear_db: bool) -> anyhow::Result { + debug!("Using mongodb url: '{:#?}'", &db_url); + match init_database_client::( + db_url.as_str(), + Some(DOCUMENT_DB_CLIENT.to_string()), + ) + .await + { + Ok(datastore) => { + debug!("Check if database is empty..."); + match datastore + .client + .database(DOCUMENT_DB) + .list_collection_names(None) + .await + { + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + let number_of_colls = + match colls.contains(&MONGO_COLL_DOCUMENT_BUCKET.to_string()) { + true => colls.len(), + false => 0, + }; + + if number_of_colls > 0 && clear_db { + debug!("Database not empty and clear_db == true. Dropping database..."); + match datastore.client.database(DOCUMENT_DB).drop(None).await { + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to drop database")); + } + }; + } + if number_of_colls == 0 || clear_db { + debug!("Database empty. Need to initialize..."); + let mut write_concern = WriteConcern::default(); + write_concern.journal = Some(true); + let mut options = CreateCollectionOptions::default(); + options.write_concern = Some(write_concern); + debug!("Create collection {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore + .client + .database(DOCUMENT_DB) + .create_collection(MONGO_COLL_DOCUMENT_BUCKET, options) + .await + { + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to create collection")); + } + }; + + // This purpose of this index is to ensure that the transaction counter is unique + let mut index_options = IndexOptions::default(); + index_options.unique = Some(true); + let mut index_model = IndexModel::default(); + index_model.keys = doc! {format!("{}.{}",MONGO_DOC_ARRAY, MONGO_TC): 1}; + index_model.options = Some(index_options); + + debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore + .client + .database(DOCUMENT_DB) + .collection::(MONGO_COLL_DOCUMENT_BUCKET) + .create_index(index_model, None) + .await + { + Ok(result) => { + debug!("... index {} created", result.index_name); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to create index")); + } + } + + // This creates a compound index over pid and the timestamp to enable paging using buckets + let mut compound_index_model = IndexModel::default(); + compound_index_model.keys = doc! {MONGO_PID: 1, MONGO_TS: 1}; + + debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); + match datastore + .client + .database(DOCUMENT_DB) + .collection::(MONGO_COLL_DOCUMENT_BUCKET) + .create_index(compound_index_model, None) + .await + { + Ok(result) => { + debug!("... index {} created", result.index_name); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to create compound index")); + } + } + } + debug!("... database initialized."); + Ok(datastore) + } + Err(_) => Err(anyhow!("Failed to list collections")), + } + } + Err(_) => Err(anyhow!("Failed to initialize database client")), + } + } + pub async fn add_document(&self, doc: EncryptedDocument) -> errors::Result { debug!("add_document to bucket"); let coll = self diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index 3ebd8b8..05d54ad 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -1,13 +1,13 @@ use super::DataStoreApi; -use crate::model::constants::{ - KEYRING_DB, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID, -}; +use crate::model::constants::{FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; use crate::model::errors::*; use mongodb::bson::doc; use rocket::futures::TryStreamExt; use std::process::exit; +use anyhow::anyhow; +use crate::db::init_database_client; #[derive(Clone, Debug)] pub struct KeyStore { @@ -25,6 +25,86 @@ impl DataStoreApi for KeyStore { } impl KeyStore { + pub async fn init_keystore(db_url: String, clear_db: bool) -> anyhow::Result { + debug!("Using database url: '{:#?}'", &db_url); + + match init_database_client::( + db_url.as_str(), + Some(KEYRING_DB_CLIENT.to_string()), + ) + .await + { + Ok(keystore) => { + debug!("Check if database is empty..."); + match keystore + .client + .database(KEYRING_DB) + .list_collection_names(None) + .await + { + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + if !colls.is_empty() && clear_db { + debug!("Database not empty and clear_db == true. Dropping database..."); + match keystore.client.database(KEYRING_DB).drop(None).await { + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to drop database")); + } + }; + } + if colls.is_empty() || clear_db { + debug!("Database empty. Need to initialize..."); + debug!("Adding initial document type..."); + match serde_json::from_str::( + &crate::util::read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or(String::new()), + ) { + Ok(dt) => match keystore.add_document_type(dt).await { + Ok(_) => { + debug!("... done."); + } + Err(e) => { + error!( + "Error while adding initial document type: {:#?}", + e + ); + return Err(anyhow!( + "Error while adding initial document type" + )); + } + }, + _ => { + error!("Error while loading initial document type"); + return Err(anyhow!( + "Error while loading initial document type" + )); + } + }; + debug!("Creating master key..."); + // create master key + match keystore.store_master_key(MasterKey::new_random()).await { + Ok(true) => { + debug!("... done."); + } + _ => { + error!("... failed to create master key"); + return Err(anyhow!("Failed to create master key")); + } + }; + } + debug!("... database initialized."); + Ok(keystore) + } + Err(_) => Err(anyhow!("Failed to list collections")), + } + } + Err(_) => Err(anyhow!("Failed to initialize database client")), + } + } + /// Only one master key may exist in the database. pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result { tracing::debug!("Storing new master key..."); diff --git a/clearing-house-app/src/db/mod.rs b/clearing-house-app/src/db/mod.rs index cc605cf..ab419e0 100644 --- a/clearing-house-app/src/db/mod.rs +++ b/clearing-house-app/src/db/mod.rs @@ -1,4 +1,3 @@ -pub(crate) mod config; pub(crate) mod doc_store; pub(crate) mod key_store; pub(crate) mod process_store; diff --git a/clearing-house-app/src/db/process_store.rs b/clearing-house-app/src/db/process_store.rs index 5d05617..b333f3c 100644 --- a/clearing-house-app/src/db/process_store.rs +++ b/clearing-house-app/src/db/process_store.rs @@ -1,12 +1,11 @@ -use crate::db::DataStoreApi; -use crate::model::constants::{ - MONGO_COLL_PROCESSES, MONGO_COLL_TRANSACTIONS, MONGO_ID, MONGO_TC, PROCESS_DB, -}; +use anyhow::anyhow; +use crate::db::{DataStoreApi, init_database_client}; +use crate::model::constants::{MONGO_COLL_PROCESSES, MONGO_COLL_TRANSACTIONS, MONGO_ID, MONGO_TC, PROCESS_DB, PROCESS_DB_CLIENT}; use crate::model::errors::*; use crate::model::process::Process; use crate::model::process::TransactionCounter; use mongodb::bson::doc; -use mongodb::options::{FindOneAndUpdateOptions, UpdateModifications}; +use mongodb::options::{CreateCollectionOptions, FindOneAndUpdateOptions, UpdateModifications, WriteConcern}; use mongodb::{Client, Database}; use rocket::futures::TryStreamExt; @@ -26,6 +25,71 @@ impl DataStoreApi for ProcessStore { } impl ProcessStore { + pub async fn init_process_store(db_url: String, clear_db: bool) -> anyhow::Result { + debug!("...using database url: '{:#?}'", &db_url); + + match init_database_client::( + db_url.as_str(), + Some(PROCESS_DB_CLIENT.to_string()), + ) + .await + { + Ok(process_store) => { + debug!("...check if database is empty..."); + match process_store + .client + .database(PROCESS_DB) + .list_collection_names(None) + .await + { + Ok(colls) => { + debug!("... found collections: {:#?}", &colls); + if !colls.is_empty() && clear_db { + debug!( + "...database not empty and clear_db == true. Dropping database..." + ); + match process_store.client.database(PROCESS_DB).drop(None).await { + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to drop database")); + } + }; + } + if colls.is_empty() || clear_db { + debug!("..database empty. Need to initialize..."); + let mut write_concern = WriteConcern::default(); + write_concern.journal = Some(true); + let mut options = CreateCollectionOptions::default(); + options.write_concern = Some(write_concern); + debug!("...create collection {} ...", MONGO_COLL_TRANSACTIONS); + match process_store + .client + .database(PROCESS_DB) + .create_collection(MONGO_COLL_TRANSACTIONS, options) + .await + { + Ok(_) => { + debug!("... done."); + } + Err(_) => { + debug!("... failed."); + return Err(anyhow!("Failed to create collection")); + } + }; + } + debug!("... database initialized."); + Ok(process_store) + } + Err(_) => Err(anyhow!("Failed to list collections")), + } + } + Err(_) => Err(anyhow!("Failed to initialize database client")), + } + } + pub async fn get_transaction_counter(&self) -> errors::Result> { debug!("Getting transaction counter..."); let coll = self diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index 186d8a8..3d1b66f 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -4,9 +4,9 @@ extern crate tracing; use crate::model::constants::ENV_LOGGING_SERVICE_ID; -use db::config::doc_store::DatastoreConfigurator; -use db::config::keyring_store::KeyringDbConfigurator; -use db::config::process_store::ProcessStoreConfigurator; +use crate::db::doc_store::DataStore; +use crate::db::key_store::KeyStore; +use crate::db::process_store::ProcessStore; mod config; mod crypto; @@ -23,15 +23,15 @@ async fn main() -> Result<(), rocket::Error> { config::configure_logging(conf.log_level); let process_store = - ProcessStoreConfigurator::init_process_store(conf.process_database_url, conf.clear_db) + ProcessStore::init_process_store(conf.process_database_url, conf.clear_db) .await .expect("Failure to initialize process store! Exiting..."); let keyring_store = - KeyringDbConfigurator::init_keystore(conf.keyring_database_url, conf.clear_db) + KeyStore::init_keystore(conf.keyring_database_url, conf.clear_db) .await .expect("Failure to initialize keyring store! Exiting..."); let doc_store = - DatastoreConfigurator::init_datastore(conf.document_database_url, conf.clear_db) + DataStore::init_datastore(conf.document_database_url, conf.clear_db) .await .expect("Failure to initialize document store! Exiting..."); From 4fc1a9af14af9055b11498b27418d2588bdd91e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 29 Aug 2023 15:50:06 +0200 Subject: [PATCH 049/183] Fix(ch-app): Fix more warnings in the CH-App --- clearing-house-app/src/db/doc_store.rs | 10 +++---- clearing-house-app/src/model/claims.rs | 11 +++++--- clearing-house-app/src/model/document.rs | 26 ++++++++++++++++++- .../src/services/logging_service.rs | 4 +-- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 6beeac9..44c6078 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -199,8 +199,8 @@ impl DataStore { /// gets the model from the db pub async fn get_document( &self, - id: &String, - pid: &String, + id: &str, + pid: &str, ) -> errors::Result> { debug!("Trying to get doc with id {}...", id); let coll = self @@ -209,13 +209,13 @@ impl DataStore { let pipeline = vec![ doc! {"$match":{ - MONGO_PID: pid.clone(), - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone() + MONGO_PID: pid.to_owned(), + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.to_owned(), }}, doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, - doc! {"$match":{ MONGO_ID: id.clone()}}, + doc! {"$match":{ MONGO_ID: id.to_owned()}}, ]; let mut results = coll.aggregate(pipeline, None).await?; diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index 427cfec..0965311 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -79,8 +79,10 @@ pub fn get_jwks(key_path: &str) -> Option> ..Default::default() }; - let mut common = biscuit::jwk::CommonParameters::default(); - common.key_id = get_fingerprint(key_path); + let common = biscuit::jwk::CommonParameters { + key_id: get_fingerprint(key_path), + ..Default::default() + }; let jwk = biscuit::jwk::JWK:: { common, @@ -111,9 +113,10 @@ pub fn get_fingerprint(key_path: &str) -> Option { .to_vec(); let pk = openssh_keys::PublicKey::from_rsa(pk_e, pk_modulus); - return Some(pk.fingerprint()); + Some(pk.fingerprint()) + } else { + None } - None } pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> String { diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index e0a8cc9..57776d7 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -8,6 +8,7 @@ use blake2_rfc::blake2b::Blake2b; use chrono::Local; use generic_array::GenericArray; use std::collections::HashMap; +use base64::Engine; use uuid::Uuid; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] @@ -263,7 +264,7 @@ impl EncryptedDocument { hasher.update(ct.as_bytes()); } - let res = base64::encode(hasher.finalize()); + let res = base64::engine::general_purpose::STANDARD.encode(hasher.finalize()); debug!("hashed cts: '{}'", &res); res } @@ -322,3 +323,26 @@ fn format_pt_for_storage(field_name: &str, pt: &str) -> String { fn format_tc(tc: i64) -> String { format!("{:08}", tc) } + + +#[cfg(test)] +mod test { + /// Purpose of this test case: The `base64::encode` function has been deprecated in favor of + /// `base64::engine::Engine::encode`. This test case ensures that the new function works as + /// expected. + #[test] + fn hash() { + let doc = super::EncryptedDocument::new( + String::from("id"), + String::from("pid"), + String::from("dt_id"), + 42, + 12, + String::from("keys_ct"), + vec![String::from("ct1"), String::from("ct2")], + ); + + let hash = doc.hash(); + assert_eq!("X/BsEutzaPbi555duyusiD9z5aUCwE7oNIMteMtdYLEAqJ7FJ0Ln13J3t1Qw8MMJhLCb9rRE8bRbqHtV4mYqRA==", hash); + } +} \ No newline at end of file diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index ac1d71f..a032c72 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -174,7 +174,7 @@ impl LoggingService { async fn log_message( &self, - user: &String, + user: &str, key_path: &str, message: IdsMessage, ) -> anyhow::Result { @@ -204,7 +204,7 @@ impl LoggingService { document_id: doc_receipt.doc_id, payload, chain_hash: doc_receipt.chain_hash, - client_id: user.clone(), + client_id: user.to_owned(), clearing_house_version: env!("CARGO_PKG_VERSION").to_string(), }; debug!("...done. Signing receipt..."); From 18501d9d98abbd6921f395df66fbba8c19c4f9be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 30 Aug 2023 22:12:06 +0200 Subject: [PATCH 050/183] refactor(ch-app): Refactor errors --- clearing-house-app/Cargo.lock | 11 ------ clearing-house-app/Cargo.toml | 1 - clearing-house-app/src/db/doc_store.rs | 15 ++++--- clearing-house-app/src/db/key_store.rs | 17 ++++---- clearing-house-app/src/db/mod.rs | 14 ++----- clearing-house-app/src/db/process_store.rs | 23 ++++++----- clearing-house-app/src/model/claims.rs | 8 ++-- clearing-house-app/src/model/document.rs | 39 +++++++++---------- clearing-house-app/src/model/errors.rs | 18 --------- clearing-house-app/src/model/mod.rs | 1 - clearing-house-app/src/model/process.rs | 8 ++-- .../src/services/logging_service.rs | 3 +- clearing-house-app/src/util.rs | 7 ++-- 13 files changed, 62 insertions(+), 103 deletions(-) delete mode 100644 clearing-house-app/src/model/errors.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 636ced4..5a51476 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -329,7 +329,6 @@ dependencies = [ "blake2-rfc", "chrono", "config", - "error-chain", "generic-array", "hex", "hkdf", @@ -605,16 +604,6 @@ dependencies = [ "libc", ] -[[package]] -name = "error-chain" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "backtrace", - "version_check", -] - [[package]] name = "fastrand" version = "2.0.0" diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index a5de761..8406070 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -30,7 +30,6 @@ once_cell = "1.18.0" blake2-rfc = "0.2.18" base64 = "0.21.2" uuid = { version = "1.4.1", features = ["serde"] } -error-chain = "0.12.4" num-bigint = "0.4.3" ring = "0.16.20" openssh-keys = "0.6.2" diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 44c6078..3642dee 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -3,7 +3,6 @@ use crate::db::doc_store::bucket::{restore_from_bucket, DocumentBucketSize, Docu use crate::db::{DataStoreApi, init_database_client}; use crate::model::constants::{DOCUMENT_DB, DOCUMENT_DB_CLIENT, MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_ID, MONGO_PID, MONGO_TC, MONGO_TO_TS, MONGO_TS}; use crate::model::document::{Document, EncryptedDocument}; -use crate::model::errors::*; use crate::model::SortingOrder; use mongodb::bson::doc; use mongodb::options::{AggregateOptions, CreateCollectionOptions, IndexOptions, UpdateOptions, WriteConcern}; @@ -139,7 +138,7 @@ impl DataStore { } } - pub async fn add_document(&self, doc: EncryptedDocument) -> errors::Result { + pub async fn add_document(&self, doc: EncryptedDocument) -> anyhow::Result { debug!("add_document to bucket"); let coll = self .database @@ -170,14 +169,14 @@ impl DataStore { } Err(e) => { error!("failed to store document: {:#?}", &e); - Err(errors::Error::from(e)) + Err(e.into()) } } } /// checks if the document exists /// document ids are globally unique - pub async fn exists_document(&self, id: &String) -> errors::Result { + pub async fn exists_document(&self, id: &String) -> anyhow::Result { debug!("Check if document with id '{}' exists...", id); let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone()}; @@ -201,7 +200,7 @@ impl DataStore { &self, id: &str, pid: &str, - ) -> errors::Result> { + ) -> anyhow::Result> { debug!("Trying to get doc with id {}...", id); let coll = self .database @@ -232,7 +231,7 @@ impl DataStore { pub async fn get_document_with_previous_tc( &self, tc: i64, - ) -> errors::Result> { + ) -> anyhow::Result> { let previous_tc = tc - 1; debug!("Trying to get document for tc {} ...", previous_tc); if previous_tc < 0 { @@ -276,7 +275,7 @@ impl DataStore { sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime, - ) -> errors::Result> { + ) -> anyhow::Result> { debug!( "...trying to get page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size @@ -364,7 +363,7 @@ impl DataStore { sort: &SortingOrder, date_from: &chrono::NaiveDateTime, date_to: &chrono::NaiveDateTime, - ) -> errors::Result { + ) -> anyhow::Result { debug!("...trying to get the offset for page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); let sort_order = match sort { SortingOrder::Ascending => 1, diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index 05d54ad..f84d8d7 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -2,7 +2,6 @@ use super::DataStoreApi; use crate::model::constants::{FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; -use crate::model::errors::*; use mongodb::bson::doc; use rocket::futures::TryStreamExt; use std::process::exit; @@ -162,7 +161,7 @@ impl KeyStore { } // DOCTYPE - pub async fn add_document_type(&self, doc_type: DocumentType) -> errors::Result<()> { + pub async fn add_document_type(&self, doc_type: DocumentType) -> anyhow::Result<()> { let coll = self .database .collection::(MONGO_COLL_DOC_TYPES); @@ -173,13 +172,13 @@ impl KeyStore { } Err(e) => { tracing::error!("failed to log document type {}", &doc_type.id); - Err(errors::Error::from(e)) + Err(e.into()) } } } //TODO: Do we need to check that no documents of this type exist before we remove it from the db? - pub async fn delete_document_type(&self, id: &String, pid: &String) -> errors::Result { + pub async fn delete_document_type(&self, id: &String, pid: &String) -> anyhow::Result { let coll = self .database .collection::(MONGO_COLL_DOC_TYPES); @@ -194,7 +193,7 @@ impl KeyStore { } /// checks if the model exits - pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> errors::Result { + pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> anyhow::Result { let coll = self .database .collection::(MONGO_COLL_DOC_TYPES); @@ -214,7 +213,7 @@ impl KeyStore { } } - pub async fn get_all_document_types(&self) -> errors::Result> { + pub async fn get_all_document_types(&self) -> anyhow::Result> { let coll = self .database .collection::(MONGO_COLL_DOC_TYPES); @@ -227,7 +226,7 @@ impl KeyStore { Ok(result) } - pub async fn get_document_type(&self, dt_id: &String) -> errors::Result> { + pub async fn get_document_type(&self, dt_id: &String) -> anyhow::Result> { let coll = self .database .collection::(MONGO_COLL_DOC_TYPES); @@ -236,7 +235,7 @@ impl KeyStore { Ok(result) => Ok(result), Err(e) => { tracing::error!("error while getting document type with id {}!", dt_id); - Err(errors::Error::from(e)) + Err(e.into()) } } } @@ -245,7 +244,7 @@ impl KeyStore { &self, doc_type: DocumentType, id: &String, - ) -> errors::Result { + ) -> anyhow::Result { let coll = self .database .collection::(MONGO_COLL_DOC_TYPES); diff --git a/clearing-house-app/src/db/mod.rs b/clearing-house-app/src/db/mod.rs index ab419e0..ff1c505 100644 --- a/clearing-house-app/src/db/mod.rs +++ b/clearing-house-app/src/db/mod.rs @@ -2,7 +2,6 @@ pub(crate) mod doc_store; pub(crate) mod key_store; pub(crate) mod process_store; -use crate::model::errors::*; use mongodb::options::ClientOptions; use mongodb::Client; @@ -10,18 +9,13 @@ pub trait DataStoreApi { fn new(client: Client) -> Self; } -pub async fn init_database_client( - db_url: &str, - client_name: Option, -) -> errors::Result { - let mut client_options; - - match ClientOptions::parse(&db_url.to_string()).await { +pub async fn init_database_client(db_url: &str, client_name: Option) -> anyhow::Result { + let mut client_options = match ClientOptions::parse(&db_url.to_string()).await { Ok(co) => { - client_options = co; + co } Err(_) => { - error_chain::bail!("Can't parse database connection string"); + anyhow::bail!("Can't parse database connection string"); } }; diff --git a/clearing-house-app/src/db/process_store.rs b/clearing-house-app/src/db/process_store.rs index b333f3c..ddd5cb6 100644 --- a/clearing-house-app/src/db/process_store.rs +++ b/clearing-house-app/src/db/process_store.rs @@ -1,7 +1,6 @@ use anyhow::anyhow; use crate::db::{DataStoreApi, init_database_client}; use crate::model::constants::{MONGO_COLL_PROCESSES, MONGO_COLL_TRANSACTIONS, MONGO_ID, MONGO_TC, PROCESS_DB, PROCESS_DB_CLIENT}; -use crate::model::errors::*; use crate::model::process::Process; use crate::model::process::TransactionCounter; use mongodb::bson::doc; @@ -90,7 +89,7 @@ impl ProcessStore { } } - pub async fn get_transaction_counter(&self) -> errors::Result> { + pub async fn get_transaction_counter(&self) -> anyhow::Result> { debug!("Getting transaction counter..."); let coll = self .database @@ -101,7 +100,7 @@ impl ProcessStore { } } - pub async fn increment_transaction_counter(&self) -> errors::Result> { + pub async fn increment_transaction_counter(&self) -> anyhow::Result> { debug!("Getting transaction counter..."); let coll = self .database @@ -115,7 +114,7 @@ impl ProcessStore { } } - pub async fn get_processes(&self) -> errors::Result> { + pub async fn get_processes(&self) -> anyhow::Result> { debug!("Trying to get all processes..."); let coll = self.database.collection::(MONGO_COLL_PROCESSES); let result = coll @@ -127,7 +126,7 @@ impl ProcessStore { Ok(result) } - pub async fn delete_process(&self, pid: &String) -> errors::Result { + pub async fn delete_process(&self, pid: &String) -> anyhow::Result { debug!("Trying to delete process with pid '{}'...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); let result = coll.delete_one(doc! { MONGO_ID: pid }, None).await?; @@ -141,7 +140,7 @@ impl ProcessStore { } /// checks if the id exits - pub async fn exists_process(&self, pid: &String) -> errors::Result { + pub async fn exists_process(&self, pid: &String) -> anyhow::Result { debug!("Check if process with pid '{}' exists...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); let result = coll.find_one(Some(doc! { MONGO_ID: pid }), None).await?; @@ -157,19 +156,19 @@ impl ProcessStore { } } - pub async fn get_process(&self, pid: &String) -> errors::Result> { + pub async fn get_process(&self, pid: &String) -> anyhow::Result> { debug!("Trying to get process with id {}...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); match coll.find_one(Some(doc! { MONGO_ID: pid }), None).await { Ok(process) => Ok(process), Err(e) => { error!("Error while getting process: {:#?}!", &e); - Err(errors::Error::from(e)) + Err(e.into()) } } } - pub async fn is_authorized(&self, user: &String, pid: &String) -> errors::Result { + pub async fn is_authorized(&self, user: &String, pid: &String) -> anyhow::Result { debug!( "checking if user '{}' is authorized to access '{}'", user, pid @@ -186,12 +185,12 @@ impl ProcessStore { trace!("didn't find process"); Ok(false) } - _ => Err(format!("User '{}' could not be authorized", &user).into()), + _ => Err(anyhow!("User '{}' could not be authorized", &user).into()), } } // store process in db - pub async fn store_process(&self, process: Process) -> errors::Result { + pub async fn store_process(&self, process: Process) -> anyhow::Result { debug!("Storing process with pid {:#?}...", &process.id); let coll = self.database.collection::(MONGO_COLL_PROCESSES); match coll.insert_one(process, None).await { @@ -201,7 +200,7 @@ impl ProcessStore { } Err(e) => { error!("...failed to store process: {:#?}", &e); - Err(errors::Error::from(e)) + Err(e.into()) } } } diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index 0965311..c59f5dd 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -1,5 +1,4 @@ use crate::model::constants::{ENV_SHARED_SECRET, SERVICE_HEADER}; -use crate::model::errors::*; use crate::util::ServiceConfig; use chrono::{Duration, Utc}; use num_bigint::BigUint; @@ -8,6 +7,7 @@ use rocket::http::Status; use rocket::request::{FromRequest, Outcome, Request}; use std::env; use std::fmt::{Display, Formatter}; +use anyhow::Context; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ChClaims { @@ -169,7 +169,7 @@ pub fn create_token serde::Dese pub fn decode_token serde::Deserialize<'de>>( token: &str, audience: &str, -) -> errors::Result { +) -> anyhow::Result { use biscuit::Presence::Required; use biscuit::Validation::Validate; @@ -180,11 +180,11 @@ pub fn decode_token serde::Deserialize<'d "Shared Secret not configured. Please configure environment variable {}", ENV_SHARED_SECRET ); - return Err(errors::Error::from(e)); + return Err(e.into()); } }; let jwt: biscuit::jws::Compact, biscuit::Empty> = biscuit::JWT::<_, biscuit::Empty>::new_encoded(token); - let decoded_jwt = jwt.decode(&signing_secret, biscuit::jwa::SignatureAlgorithm::HS256)?; + let decoded_jwt = jwt.decode(&signing_secret, biscuit::jwa::SignatureAlgorithm::HS256).with_context(|| "Failed decoding JWT")?; let claim_presence_options = biscuit::ClaimPresenceOptions { issuer: Required, audience: Required, diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index 57776d7..6897792 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -1,6 +1,5 @@ use crate::model::constants::SPLIT_CT; use crate::model::crypto::{KeyEntry, KeyMap}; -use crate::model::errors::*; use crate::model::util::new_uuid; use aes_gcm_siv::aead::Aead; use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; @@ -22,7 +21,7 @@ impl DocumentPart { DocumentPart { name, content } } - pub fn encrypt(&self, key: &[u8], nonce: &[u8]) -> errors::Result> { + pub fn encrypt(&self, key: &[u8], nonce: &[u8]) -> anyhow::Result> { const EXP_KEY_SIZE: usize = 32; const EXP_NONCE_SIZE: usize = 12; // check key size @@ -32,7 +31,7 @@ impl DocumentPart { key.len(), EXP_KEY_SIZE ); - error_chain::bail!("Incorrect key size") + anyhow::bail!("Incorrect key size") } // check nonce size else if nonce.len() != EXP_NONCE_SIZE { @@ -41,7 +40,7 @@ impl DocumentPart { nonce.len(), EXP_NONCE_SIZE ); - error_chain::bail!("Incorrect nonce size") + anyhow::bail!("Incorrect nonce size") } else { let key = GenericArray::from_slice(key); let nonce = GenericArray::from_slice(nonce); @@ -52,18 +51,18 @@ impl DocumentPart { let pt = format_pt_for_storage(&self.name, pt); match cipher.encrypt(nonce, pt.as_bytes()) { Ok(ct) => Ok(ct), - Err(e) => error_chain::bail!("Error while encrypting {}", e), + Err(e) => anyhow::bail!("Error while encrypting {}", e), } } None => { error!("Tried to encrypt empty document part."); - error_chain::bail!("Nothing to encrypt"); + anyhow::bail!("Nothing to encrypt"); } } } } - pub fn decrypt(key: &[u8], nonce: &[u8], ct: &[u8]) -> errors::Result { + pub fn decrypt(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result { let key = GenericArray::from_slice(key); let nonce = GenericArray::from_slice(nonce); let cipher = Aes256GcmSiv::new(key); @@ -75,7 +74,7 @@ impl DocumentPart { Ok(DocumentPart::new(name, Some(content))) } Err(e) => { - error_chain::bail!("Error while decrypting: {}", e) + anyhow::bail!("Error while decrypting: {}", e) } } } @@ -100,7 +99,7 @@ impl Document { // each part is encrypted using the part specific key from the key map // the hash is set to "0". Chaining is not done here. - pub fn encrypt(&self, key_map: KeyMap) -> errors::Result { + pub fn encrypt(&self, key_map: KeyMap) -> anyhow::Result { debug!("encrypting document of doc_type {}", self.dt_id); let mut cts = vec![]; @@ -110,7 +109,7 @@ impl Document { hex::encode(ct) } None => { - error_chain::bail!("Missing key ct"); + anyhow::bail!("Missing key ct"); } }; @@ -122,14 +121,14 @@ impl Document { // check if there's a key for this part if !keys.contains_key(&part.name) { error!("Missing key for part '{}'", &part.name); - error_chain::bail!("Missing key for part '{}'", &part.name); + anyhow::bail!("Missing key for part '{}'", &part.name); } // get the key for this part let key_entry = keys.get(&part.name).unwrap(); let ct = part.encrypt(key_entry.key.as_slice(), key_entry.nonce.as_slice()); if ct.is_err() { warn!("Encryption error. No ct received!"); - error_chain::bail!("Encryption error. No ct received!"); + anyhow::bail!("Encryption error. No ct received!"); } let ct_string = hex::encode_upper(ct.unwrap()); @@ -206,17 +205,17 @@ pub struct EncryptedDocument { impl EncryptedDocument { /// Note: KeyMap keys need to be KeyEntry.ids in this case // Decryption is done without checking the hashes. Do this before calling this method - pub fn decrypt(&self, keys: HashMap) -> errors::Result { + pub fn decrypt(&self, keys: HashMap) -> anyhow::Result { let mut pts = vec![]; for ct in self.cts.iter() { let ct_parts = ct.split(SPLIT_CT).collect::>(); if ct_parts.len() != 2 { - error_chain::bail!("Integrity violation! Ciphertexts modified"); + anyhow::bail!("Integrity violation! Ciphertexts modified"); } // get key and nonce let key_entry = keys.get(ct_parts[0]); if key_entry.is_none() { - error_chain::bail!("Key for id '{}' does not exist!", ct_parts[0]); + anyhow::bail!("Key for id '{}' does not exist!", ct_parts[0]); } let key = key_entry.unwrap().key.as_slice(); let nonce = key_entry.unwrap().nonce.as_slice(); @@ -229,7 +228,7 @@ impl EncryptedDocument { match DocumentPart::decrypt(key, nonce, ct.as_slice()) { Ok(part) => pts.push(part), Err(e) => { - error_chain::bail!("Error while decrypting: {}", e); + anyhow::bail!("Error while decrypting: {}", e); } } } @@ -292,11 +291,11 @@ impl EncryptedDocument { } /// companion to format_pt_for_storage -pub fn restore_pt(pt: &str) -> errors::Result<(String, String, String)> { +pub fn restore_pt(pt: &str) -> anyhow::Result<(String, String, String)> { trace!("Trying to restore plain text"); let vec: Vec<&str> = pt.split(SPLIT_CT).collect(); if vec.len() != 3 { - error_chain::bail!("Could not restore plaintext"); + anyhow::bail!("Could not restore plaintext"); } Ok(( String::from(vec[0]), @@ -306,11 +305,11 @@ pub fn restore_pt(pt: &str) -> errors::Result<(String, String, String)> { } /// companion to format_pt_for_storage_no_dt -pub fn restore_pt_no_dt(pt: &str) -> errors::Result<(String, String)> { +pub fn restore_pt_no_dt(pt: &str) -> anyhow::Result<(String, String)> { trace!("Trying to restore plain text"); let vec: Vec<&str> = pt.split(SPLIT_CT).collect(); if vec.len() != 2 { - error_chain::bail!("Could not restore plaintext"); + anyhow::bail!("Could not restore plaintext"); } Ok((String::from(vec[0]), String::from(vec[1]))) } diff --git a/clearing-house-app/src/model/errors.rs b/clearing-house-app/src/model/errors.rs deleted file mode 100644 index 73646ec..0000000 --- a/clearing-house-app/src/model/errors.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub mod errors { - use error_chain::error_chain; - // Create the Error, ErrorKind, ResultExt, and Result types - error_chain! { - foreign_links { - Conversion(std::num::TryFromIntError); - Figment(rocket::figment::Error); - HexError(hex::FromHexError); - Io(::std::io::Error) #[cfg(unix)]; - Mongodb(mongodb::error::Error); - MongodbBson(mongodb::bson::de::Error); - SerdeJson(serde_json::error::Error); - Uft8Error(std::string::FromUtf8Error); - BiscuitError(biscuit::errors::Error); - EnvVariable(::std::env::VarError); - } - } -} diff --git a/clearing-house-app/src/model/mod.rs b/clearing-house-app/src/model/mod.rs index 26594c2..43e55b0 100644 --- a/clearing-house-app/src/model/mod.rs +++ b/clearing-house-app/src/model/mod.rs @@ -3,7 +3,6 @@ pub mod constants; pub(crate) mod crypto; pub(crate) mod doc_type; pub(crate) mod document; -pub(crate) mod errors; pub mod ids; pub(crate) mod process; pub(crate) mod util; diff --git a/clearing-house-app/src/model/process.rs b/clearing-house-app/src/model/process.rs index 7c4bf1d..1c48b7d 100644 --- a/clearing-house-app/src/model/process.rs +++ b/clearing-house-app/src/model/process.rs @@ -5,8 +5,8 @@ pub struct Process { } impl Process { - pub fn new(id: String, owners: Vec) -> Process { - Process { id, owners } + pub fn new(id: String, owners: Vec) -> Self { + Self { id, owners } } } @@ -21,8 +21,8 @@ pub struct OwnerList { } impl OwnerList { - pub fn new(owners: Vec) -> OwnerList { - OwnerList { owners } + pub fn new(owners: Vec) -> Self { + Self { owners } } } diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index a032c72..7fdd7ae 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -68,8 +68,7 @@ impl LoggingService { if self.db.store_process(new_process).await.is_err() { error!("Error while creating process '{}'", &pid); - return Err(anyhow!("Error while creating process")); - // InternalError + return Err(anyhow!("Error while creating process")); // InternalError } } Err(_) => { diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index 9a95e19..f5a2ca9 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -1,6 +1,6 @@ use crate::model::constants::SIGNING_KEY; -use crate::model::errors::errors; use std::path::Path; +use anyhow::Context; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ServiceConfig { @@ -40,6 +40,7 @@ pub fn add_signing_key() -> rocket::fairing::AdHoc { } /// Reads a file into a string -pub fn read_file(file: &str) -> errors::Result { - std::fs::read_to_string(file).map_err(errors::Error::from) +pub fn read_file(file: &str) -> anyhow::Result { + std::fs::read_to_string(file) + .with_context(|| format!("Failed to read contents of file '{}'", file)) } From bf3ac5404ef1398c6368ea5f3366ddf0f519e83b Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 30 Aug 2023 22:13:29 +0200 Subject: [PATCH 051/183] init smoke tests --- clearing-house-app/certs/daps-dev.der | Bin 1110 -> 796 bytes clearing-house-app/certs/daps.der | Bin 1106 -> 796 bytes generateJWT.js | 14 ++++ package-lock.json | 95 +++++++++++++++++++++++++- package.json | 1 + tests/smoke.js | 27 ++++++++ 6 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 generateJWT.js create mode 100644 tests/smoke.js diff --git a/clearing-house-app/certs/daps-dev.der b/clearing-house-app/certs/daps-dev.der index 22bf0dfb2650a14467fa67a789cc346d49f78efe..bf5eca335b9710b0ba41a0ba3caf58f5f1c918e6 100644 GIT binary patch literal 796 zcmXqLVwNyyVw$&rnTe5!iILHOi;Y98&EuRc3p0~}iy^lGCmVAp3!5;Li>o2O0WXNd z#lu!oR9aP4V#otj4HV$!VNOXcGZZrr0r8l5cnvL$^bE}`^bGZkj7<&X#CeU34J?c- z4NOffjm)EfTr(io0?IY;BFPpR11X3t!YH;_85$b^K@+1A(2<-#AGCevWn^SzWngY% z7g|VCw^Cc3w7m~eoykqzx?|jk6Am; zyvKU})-vaKN5=A{JwFw;l}dh?_2;;AgI9Aa&r*&PGg}%`l3nbkD$DJi)vm-{p(G%+ zES-Jkgfcl!gF8nVV_7;}7O!(&yKRT*<+>G<9=6-N$y_M%J3T47cKQbygGo)Q>9q$J zu6)h({kX}Y%W~aCf-{@E@;9z->3f)N`&yw&>_gy;|JmCm!na9ApNO5At8hW`zFUaw z&yK@6?JcW%w(d&%r}?$V{ZGBU;j8l@cRy|YymTk`$;V=c9dz88x%@lIG#}Y?2e#N) zmHMxnts*=ne)$h~_QLgleFCdInV1|fpxHm9AGR|Jo?heTWgl%kDLu$P90bG z3K=MYbSQ&VNEnDUi1??~?&WxQ%V0&Pq<%t6WX#!9C(=IX?y~e%*!rV3cXs2ShdJ{ literal 1110 zcmXqLVhJ*6Vzyeq%*4pV#Kh2Jz{|#|)#lOmotKf3o0Y-9!I0a4lZ`o)g-w{r#nn*Q zKoG>?;^A^DN-WLG$WKcxGL$ip0tqtn2z$B&D}sQPijV`s>D7D-_l*n((pAa zVq#wG37rT{ftZOR{cM3a@bORbvvJ$ zm};Kd%f$MQeY>Ny-~Csj_^;Pa#X)jxrvNjl}BA zrngMYj0}v6n;4COA);>}3k+IWJ{B<+kq*@@AqjVw`tIo-ivAFO?`mDv+X4f5khC(3 zgn?Lt$myQr+gdAEC@(uLx>`Iy|5>VY>fcY z80Z=3EYMn@(WaJBQc_^0ub*6$qnDUjoSLkc22WIaDXIFIDaCp~IfD>34sA9@R#tXK zMivhPHv<#J7UkO&zzm>J5?8lK3r7do^QDS>Vs*Am||aLyZS8%@5KJ!DYRX6H18y=Vnl@W(^q$R~Zs!)<-~O)Q5R+7vryGCW_J}Xjg}Rc{ieJ3F z^M6+ryGFsUAHNr|e72GoRbvY|qC2l|`Lg8aL32#cFc`ctTB=a;{_ES6Gx00-D03+K zR{XN5tv8tRX}+CAM$!C*f_C4|>HN5N!%qD9q7UoVw&g#ao2O0WXNd z#lu!oR9aP4V#otj4HV$!VNOXcGZZrr0r8l5cnvL$^bE}`^bGZkj7<&X#CeU34J?c- z4NOffjm)EfTr(io0?IY;BFPpR11X3t!YH;_85$b^K@+1A(2<-#AGCevWn^SzWngY% z7g|VCw^Cc3w7m~eoykqzx?|jk6Am; zyvKU})-vaKN5=A{JwFw;l}dh?_2;;AgI9Aa&r*&PGg}%`l3nbkD$DJi)vm-{p(G%+ zES-Jkgfcl!gF8nVV_7;}7O!(&yKRT*<+>G<9=6-N$y_M%J3T47cKQbygGo)Q>9q$J zu6)h({kX}Y%W~aCf-{@E@;9z->3f)N`&yw&>_gy;|JmCm!na9ApNO5At8hW`zFUaw z&yK@6?JcW%w(d&%r}?$V{ZGBU;j8l@cRy|YymTk`$;V=c9dz88x%@lIG#}Y?2e#N) zmHMxnts*=ne)$h~_QLgleFCdInV1|fpxHm9AGR|Jo?heTWgl%kDLu$P90bG z3K=MYbSQ&VNEnDUi1??~?&WxQ%V0&Pq<%t6WX#!9C(=IX?y~e%*!rV3cXs2ShdJ{ literal 1106 zcmXqLV(~L*Vm4dA%*4pV#Kh2Rz{|#|)#lOmotKf3o0Y-9!I0a4lZ`o)g-w{r#nn*Q zKoG>?;^A^DN-WLG$WKcxGL$ip0tqtn2z$B&D}SvS!n(L&cU$;dLzG||}DAT7zDiBSpJUyQ5_%uS5^3_x)%rY1&4hGVfa)Uvee z_eee0R5|%6l&Qu#IbEOSd2{5->@t^MF^VVNo|e$Fey6b}a^}M~QbC#=MzgXesHIly zKK?7XR4}BMU7&L7)qRN>#VwJ7d$*q~&djSbH+Ojap|eTp?txNMmHQjIzdqE{ow>Tf zcfZUp>-P7x{CSUOJ&zJ8N)|GGVX%=q_JN6nv%ryh_P<-+7N&3R37eO$+ShWl{<&W- zYs~XQstQadS6ozn`FnlPJHJTFE|PCc<+9bS(;~0mv?z<3E3Woe(qU0i1hd5N^CIil z%k2++;@Yidb2#LE{1wrRU((*Q`>yU-_uv2Pyp7RG59obEZkt+jH6^0L#StHlHKpQSp-z5pi|Sz#6?0|oY*1<%Nq>)8o(h zZWEmF`K@BJ$}NL?e_Ow@XD&>P`!|nSdhwZ? txjaFf%UxG1`)?Fh$#eM|8duHut47`M?cPmEm!`1Pg)fM|-ahY8IsmWBi2VQn diff --git a/generateJWT.js b/generateJWT.js new file mode 100644 index 0000000..022f8d6 --- /dev/null +++ b/generateJWT.js @@ -0,0 +1,14 @@ +const jwt = require('jsonwebtoken') + +const payload = { + "sub": "69:F5:9D:B0:DD:A6:9D:30:5F:58:AA:2D:20:4D:B2:39:F0:54:FC:3B:keyid:4F:66:7D:BD:08:EE:C6:4A:D1:96:D8:7C:6C:A2:32:8A:EC:A6:AD:49", + "iss": "69:F5:9D:B0:DD:A6:9D:30:5F:58:AA:2D:20:4D:B2:39:F0:54:FC:3B:keyid:4F:66:7D:BD:08:EE:C6:4A:D1:96:D8:7C:6C:A2:32:8A:EC:A6:AD:49", + "iat": Date.now(), + "nbf": Date.now(), + "exp": Date.now() + 3600, + "aud": "idsc:IDS_CONNECTORS_ALL" +} + +jwt.sign(payload, "123", { algorithm: 'HS256' }, function(err, token) { + console.log(token); +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0e56bb8..c542bc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "ids-basecamp-clearinghouse", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ids-basecamp-clearinghouse", - "version": "0.0.1", - "license": "ISC", + "version": "1.0.0", + "license": "Apache-2.0", "devDependencies": { "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", + "jsonwebtoken": "^9.0.2", "semantic-release": "^21.0.7" } }, @@ -831,6 +832,12 @@ "node": ">=8" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1210,6 +1217,15 @@ "readable-stream": "^2.0.2" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -1916,6 +1932,49 @@ "node": "*" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dev": true, + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -1998,12 +2057,36 @@ "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", @@ -2016,6 +2099,12 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, "node_modules/lodash.uniqby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", diff --git a/package.json b/package.json index 72050fd..7bfc48c 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "devDependencies": { "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", + "jsonwebtoken": "^9.0.2", "semantic-release": "^21.0.7" } } diff --git a/tests/smoke.js b/tests/smoke.js new file mode 100644 index 0000000..c21309c --- /dev/null +++ b/tests/smoke.js @@ -0,0 +1,27 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 1, // Key for Smoke test. Keep it at 2, 3, max 5 VUs, +}; + +const url = 'http://localhost:8000'; + +export default () => { + const jwksRes = http.get(`${url}/.well-known/jwks.json`); + check(jwksRes, { + 'ch-app GET jwks is status 200': (r) => r.status === 200, + }); + + const logMessageResHeader = { + content + } + + const logMessageRes = http.post(`${url}/messages/log/1`, {}. {}); + check(logMessageRes, { + 'ch-app POST jwks is status 200': (r) => r.status === 200, + }); + + + sleep(1); +}; From aba6f756a56a30003c98716e6dfa13c0f8c6c9dd Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 30 Aug 2023 22:45:50 +0200 Subject: [PATCH 052/183] debug claims.rs --- clearing-house-app/src/model/claims.rs | 28 +++++++++++++++++++++----- generateJWT.js | 10 ++++----- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index c59f5dd..a9869e9 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -172,7 +172,7 @@ pub fn decode_token serde::Deserialize<'d ) -> anyhow::Result { use biscuit::Presence::Required; use biscuit::Validation::Validate; - + info!("START Token validated!"); let signing_secret = match env::var(ENV_SHARED_SECRET) { Ok(secret) => biscuit::jws::Secret::Bytes(secret.to_string().into_bytes()), Err(e) => { @@ -183,8 +183,17 @@ pub fn decode_token serde::Deserialize<'d return Err(e.into()); } }; + info!("2 Token validated!"); let jwt: biscuit::jws::Compact, biscuit::Empty> = biscuit::JWT::<_, biscuit::Empty>::new_encoded(token); - let decoded_jwt = jwt.decode(&signing_secret, biscuit::jwa::SignatureAlgorithm::HS256).with_context(|| "Failed decoding JWT")?; + info!("3 Token validated!"); + let decoded_jwt = match jwt.decode(&signing_secret, biscuit::jwa::SignatureAlgorithm::HS256) { + Ok(x) => Ok(x), + Err(e) => { + error!("Failed to decode token {}", e); + Err(e) + } + }?; + info!("4 Token validated!"); let claim_presence_options = biscuit::ClaimPresenceOptions { issuer: Required, audience: Required, @@ -192,13 +201,22 @@ pub fn decode_token serde::Deserialize<'d expiry: Required, ..Default::default() }; + info!("5 Token validated!"); let val_options = biscuit::ValidationOptions { claim_presence_options, - issued_at: Validate(Duration::minutes(5)), + // issued_at: Validate(Duration::minutes(5)), // Issuer is not validated. Wouldn't make much of a difference if we did audience: Validate(audience.to_string()), ..Default::default() }; - assert!(decoded_jwt.validate(val_options).is_ok()); // TODO: Handle error - Ok(decoded_jwt.payload().unwrap().private.clone()) + info!("6 Token validated!"); + match decoded_jwt.validate(val_options) { + Ok(o) => Ok(o), + Err(e) => { + error!("Failed validating JWT token {}", e); + Err(e) + } + }?; + info!("Token validated!"); + Ok(decoded_jwt.payload().expect("If this fails we will see it!!").private.clone()) } diff --git a/generateJWT.js b/generateJWT.js index 022f8d6..9f34a68 100644 --- a/generateJWT.js +++ b/generateJWT.js @@ -1,12 +1,12 @@ const jwt = require('jsonwebtoken') const payload = { - "sub": "69:F5:9D:B0:DD:A6:9D:30:5F:58:AA:2D:20:4D:B2:39:F0:54:FC:3B:keyid:4F:66:7D:BD:08:EE:C6:4A:D1:96:D8:7C:6C:A2:32:8A:EC:A6:AD:49", + "client_id": "69:F5:9D:B0:DD:A6:9D:30:5F:58:AA:2D:20:4D:B2:39:F0:54:FC:3B:keyid:4F:66:7D:BD:08:EE:C6:4A:D1:96:D8:7C:6C:A2:32:8A:EC:A6:AD:49", "iss": "69:F5:9D:B0:DD:A6:9D:30:5F:58:AA:2D:20:4D:B2:39:F0:54:FC:3B:keyid:4F:66:7D:BD:08:EE:C6:4A:D1:96:D8:7C:6C:A2:32:8A:EC:A6:AD:49", - "iat": Date.now(), - "nbf": Date.now(), - "exp": Date.now() + 3600, - "aud": "idsc:IDS_CONNECTORS_ALL" + "iat": Math.floor(Date.now() / 1000), + "nbf": Math.floor(Date.now() / 1000), + "exp": Math.floor(Date.now() / 1000) + 3600, + "aud": "1" } jwt.sign(payload, "123", { algorithm: 'HS256' }, function(err, token) { From 0d07fe55c3a83a2b4d22adde2e7c70ddc44b2c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Fri, 1 Sep 2023 08:46:31 +0200 Subject: [PATCH 053/183] fix(ch-app): Add error log and removed assert --- clearing-house-app/Cargo.lock | 1 - clearing-house-app/Cargo.toml | 24 ++++++++++++++++++++---- clearing-house-app/src/model/claims.rs | 10 +++++++--- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 5a51476..f29bc44 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -336,7 +336,6 @@ dependencies = [ "num-bigint", "once_cell", "openssh-keys", - "percent-encoding", "rand", "ring", "rocket", diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 8406070..efa2edf 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -4,34 +4,50 @@ version = "0.10.0" authors = [ "Mark Gall ", "Georg Bramm ", + "Maximilian Schönenberg " ] edition = "2021" [dependencies] +# JWT biscuit = "0.6.0" -chrono = { version = "0.4.26", features = ["serde", "clock", "std"], default-features = false } +# Database mongodb = { version = ">= 2.6.1" , features = ["openssl-tls"]} -percent-encoding = "2.1.0" +# HTTP-Server rocket = { version = "0.5.0-rc.1", features = ["json"] } +# Serialization serde = { version = ">1.0.184", features = ["derive"] } serde_json = "1" +# Error handling anyhow = "1" +# Time handling +chrono = { version = "0.4.26", features = ["serde", "clock", "std"], default-features = false } hex = "0.4.3" +# Encryption and hashing aes = "0.8.3" aes-gcm-siv = "0.11.1" hkdf = "0.12.3" sha2 = "0.10.7" +blake2-rfc = "0.2.18" +ring = "0.16.20" +# Fixed size arrays generic-array = "0.14.7" +# Config reader config = { version = "0.13.3", default-features = false, features = ["toml"] } +# Logging/Tracing tracing = "0.1" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } +# Random number generation rand = "0.8.5" +# lazy initialization of static variables once_cell = "1.18.0" -blake2-rfc = "0.2.18" +# Base64 encoding base64 = "0.21.2" +# UUID generation uuid = { version = "1.4.1", features = ["serde"] } +# Big integer handling (RSA key modulus and exponent) num-bigint = "0.4.3" -ring = "0.16.20" +# Generating fingerprint of RSA keys openssh-keys = "0.6.2" [dev-dependencies] diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index c59f5dd..e9784d6 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -49,7 +49,10 @@ impl<'r> FromRequest<'r> for ChClaims { debug!("...retrieved claims and succeed"); Outcome::Success(claims) } - Err(_) => Outcome::Failure((Status::BadRequest, ChClaimsError::Invalid)), + Err(e) => { + error!("...failed to retrieve and validate claims: {}", e); + Outcome::Failure((Status::BadRequest, ChClaimsError::Invalid)) + }, } } } @@ -199,6 +202,7 @@ pub fn decode_token serde::Deserialize<'d audience: Validate(audience.to_string()), ..Default::default() }; - assert!(decoded_jwt.validate(val_options).is_ok()); // TODO: Handle error - Ok(decoded_jwt.payload().unwrap().private.clone()) + decoded_jwt.validate(val_options) + .with_context(|| "Failed validating JWT")?; + Ok(decoded_jwt.payload()?.private.clone()) } From e31f8066b08ebac341aa3b081056bbd110b72680 Mon Sep 17 00:00:00 2001 From: dhommen Date: Fri, 1 Sep 2023 10:55:36 +0200 Subject: [PATCH 054/183] feat(tests): add smoke tests --- clearing-house-app/src/model/claims.rs | 7 ---- tests/smoke.js | 45 +++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index a9869e9..7000d87 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -172,7 +172,6 @@ pub fn decode_token serde::Deserialize<'d ) -> anyhow::Result { use biscuit::Presence::Required; use biscuit::Validation::Validate; - info!("START Token validated!"); let signing_secret = match env::var(ENV_SHARED_SECRET) { Ok(secret) => biscuit::jws::Secret::Bytes(secret.to_string().into_bytes()), Err(e) => { @@ -183,9 +182,7 @@ pub fn decode_token serde::Deserialize<'d return Err(e.into()); } }; - info!("2 Token validated!"); let jwt: biscuit::jws::Compact, biscuit::Empty> = biscuit::JWT::<_, biscuit::Empty>::new_encoded(token); - info!("3 Token validated!"); let decoded_jwt = match jwt.decode(&signing_secret, biscuit::jwa::SignatureAlgorithm::HS256) { Ok(x) => Ok(x), Err(e) => { @@ -193,7 +190,6 @@ pub fn decode_token serde::Deserialize<'d Err(e) } }?; - info!("4 Token validated!"); let claim_presence_options = biscuit::ClaimPresenceOptions { issuer: Required, audience: Required, @@ -201,7 +197,6 @@ pub fn decode_token serde::Deserialize<'d expiry: Required, ..Default::default() }; - info!("5 Token validated!"); let val_options = biscuit::ValidationOptions { claim_presence_options, // issued_at: Validate(Duration::minutes(5)), @@ -209,7 +204,6 @@ pub fn decode_token serde::Deserialize<'d audience: Validate(audience.to_string()), ..Default::default() }; - info!("6 Token validated!"); match decoded_jwt.validate(val_options) { Ok(o) => Ok(o), Err(e) => { @@ -217,6 +211,5 @@ pub fn decode_token serde::Deserialize<'d Err(e) } }?; - info!("Token validated!"); Ok(decoded_jwt.payload().expect("If this fails we will see it!!").private.clone()) } diff --git a/tests/smoke.js b/tests/smoke.js index c21309c..2626a70 100644 --- a/tests/smoke.js +++ b/tests/smoke.js @@ -2,7 +2,7 @@ import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { - vus: 1, // Key for Smoke test. Keep it at 2, 3, max 5 VUs, + vus: 1 }; const url = 'http://localhost:8000'; @@ -13,13 +13,48 @@ export default () => { 'ch-app GET jwks is status 200': (r) => r.status === 200, }); - const logMessageResHeader = { - content + const doctypeRes = http.get(`${url}/doctype`); + check(doctypeRes, { + 'ch-app GET doctype is status 200': (r) => r.status === 200, + }); + + const logMessageHeader = { + "Content-Type": "application/json", + "CH-SERVICE": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiI2OTpGNTo5RDpCMDpERDpBNjo5RDozMDo1Rjo1ODpBQToyRDoyMDo0RDpCMjozOTpGMDo1NDpGQzozQjprZXlpZDo0Rjo2Njo3RDpCRDowODpFRTpDNjo0QTpEMTo5NjpEODo3Qzo2QzpBMjozMjo4QTpFQzpBNjpBRDo0OSIsImlzcyI6IjY5OkY1OjlEOkIwOkREOkE2OjlEOjMwOjVGOjU4OkFBOjJEOjIwOjREOkIyOjM5OkYwOjU0OkZDOjNCOmtleWlkOjRGOjY2OjdEOkJEOjA4OkVFOkM2OjRBOkQxOjk2OkQ4OjdDOjZDOkEyOjMyOjhBOkVDOkE2OkFEOjQ5IiwiaWF0IjoxNjkzNTU2NDM2LCJuYmYiOjE2OTM1NTY0MzYsImV4cCI6MTY5MzU2MDAzNiwiYXVkIjoiMSJ9.WGZVbfJqK2bFwE8vEN29VeZzfPC2F_w2_bBkadNm4WM" } - const logMessageRes = http.post(`${url}/messages/log/1`, {}. {}); + const date = new Date(); + const logMessagePayload = { + "header": { + + "@context": { + // ... (HashMap) + }, + "@type": "ids:LogMessage", + "@id": "String", + "modelVersion": "String", + "correlationMessage": "String", + "issued": date.toISOString(), + "issuerConnector": "InfoModelId", + "senderAgent": "String", + "recipientConnector": [ + "test" + ], + "recipientAgent": [ + "test" + ], + "transferContract": "String", + "contentVersion": "String", + "securityToken": null, + "authorizationToken": "String", + "payload": "String", + "payload_type": "String" + }, + payload: "hello world" + } + const logMessageRes = http.post(`${url}/messages/log/6`, JSON.stringify(logMessagePayload, null, 2), { headers: logMessageHeader }); check(logMessageRes, { - 'ch-app POST jwks is status 200': (r) => r.status === 200, + 'ch-app POST logmessage is status 201': (r) => r.status === 201, }); From 3628d2a4a09d17104130a1442836ef4c195ded46 Mon Sep 17 00:00:00 2001 From: dhommen Date: Fri, 1 Sep 2023 11:34:46 +0200 Subject: [PATCH 055/183] =?UTF-8?q?Co-authored-by:=20Maximilian=20Sch?= =?UTF-8?q?=C3=B6nenberg=20=20fix(l?= =?UTF-8?q?ogging):=20add=20mutex=20to=20fix=20parallel=20requests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- clearing-house-app/src/services/logging_service.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 7fdd7ae..c547795 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -19,11 +19,12 @@ use crate::services::document_service::DocumentService; pub struct LoggingService { db: ProcessStore, doc_api: DocumentService, + write_lock: std::sync::Arc>, } impl LoggingService { pub fn new(db: ProcessStore, doc_api: DocumentService) -> LoggingService { - LoggingService { db, doc_api } + LoggingService { db, doc_api, write_lock: std::sync::Arc::new(rocket::tokio::sync::Mutex::new(())) } } pub async fn log( @@ -181,6 +182,7 @@ impl LoggingService { let payload = message.payload.as_ref().unwrap().clone(); // transform message to document let mut doc = Document::from(message); + let _x = self.write_lock.lock().await; match self.db.get_transaction_counter().await { Ok(Some(tid)) => { debug!("Storing document..."); From 573da047c6262bd8042a9601fbdf525160f018ed Mon Sep 17 00:00:00 2001 From: dhommen Date: Fri, 1 Sep 2023 11:43:52 +0200 Subject: [PATCH 056/183] update smoke tests --- tests/smoke.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/smoke.js b/tests/smoke.js index 2626a70..8e2e36b 100644 --- a/tests/smoke.js +++ b/tests/smoke.js @@ -2,10 +2,11 @@ import http from 'k6/http'; import { check, sleep } from 'k6'; export const options = { - vus: 1 + vus: 2 }; const url = 'http://localhost:8000'; +const TOKEN = 'xxx' export default () => { const jwksRes = http.get(`${url}/.well-known/jwks.json`); @@ -20,7 +21,7 @@ export default () => { const logMessageHeader = { "Content-Type": "application/json", - "CH-SERVICE": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRfaWQiOiI2OTpGNTo5RDpCMDpERDpBNjo5RDozMDo1Rjo1ODpBQToyRDoyMDo0RDpCMjozOTpGMDo1NDpGQzozQjprZXlpZDo0Rjo2Njo3RDpCRDowODpFRTpDNjo0QTpEMTo5NjpEODo3Qzo2QzpBMjozMjo4QTpFQzpBNjpBRDo0OSIsImlzcyI6IjY5OkY1OjlEOkIwOkREOkE2OjlEOjMwOjVGOjU4OkFBOjJEOjIwOjREOkIyOjM5OkYwOjU0OkZDOjNCOmtleWlkOjRGOjY2OjdEOkJEOjA4OkVFOkM2OjRBOkQxOjk2OkQ4OjdDOjZDOkEyOjMyOjhBOkVDOkE2OkFEOjQ5IiwiaWF0IjoxNjkzNTU2NDM2LCJuYmYiOjE2OTM1NTY0MzYsImV4cCI6MTY5MzU2MDAzNiwiYXVkIjoiMSJ9.WGZVbfJqK2bFwE8vEN29VeZzfPC2F_w2_bBkadNm4WM" + "CH-SERVICE": TOKEN } const date = new Date(); @@ -57,6 +58,4 @@ export default () => { 'ch-app POST logmessage is status 201': (r) => r.status === 201, }); - - sleep(1); }; From a88175bb083ce0091459e8b47c4c27ac042f782b Mon Sep 17 00:00:00 2001 From: dhommen Date: Fri, 1 Sep 2023 11:44:10 +0200 Subject: [PATCH 057/183] feat(tests): add load tests --- tests/load.js | 52 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/load.js diff --git a/tests/load.js b/tests/load.js new file mode 100644 index 0000000..f2fad22 --- /dev/null +++ b/tests/load.js @@ -0,0 +1,52 @@ +import http from 'k6/http'; +import { check, sleep } from 'k6'; + +export const options = { + vus: 10, + duration: "1m" +}; + +const url = 'http://localhost:8000'; +const TOKEN = 'xxx' + +export default () => { + const logMessageHeader = { + "Content-Type": "application/json", + "CH-SERVICE": TOKEN + } + + const date = new Date(); + const logMessagePayload = { + "header": { + + "@context": { + // ... (HashMap) + }, + "@type": "ids:LogMessage", + "@id": "String", + "modelVersion": "String", + "correlationMessage": "String", + "issued": date.toISOString(), + "issuerConnector": "InfoModelId", + "senderAgent": "String", + "recipientConnector": [ + "test" + ], + "recipientAgent": [ + "test" + ], + "transferContract": "String", + "contentVersion": "String", + "securityToken": null, + "authorizationToken": "String", + "payload": "String", + "payload_type": "String" + }, + payload: "hello world" + } + const logMessageRes = http.post(`${url}/messages/log/6`, JSON.stringify(logMessagePayload, null, 2), { headers: logMessageHeader }); + check(logMessageRes, { + 'ch-app POST logmessage is status 201': (r) => r.status === 201, + }); + +}; From 209244c551e8e9fd4eed5e00b620a271e5fd57e9 Mon Sep 17 00:00:00 2001 From: dhommen Date: Fri, 1 Sep 2023 11:48:54 +0200 Subject: [PATCH 058/183] fix(tests): add __ENV for hostname and token --- tests/load.js | 4 ++-- tests/smoke.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/load.js b/tests/load.js index f2fad22..638f95f 100644 --- a/tests/load.js +++ b/tests/load.js @@ -6,8 +6,8 @@ export const options = { duration: "1m" }; -const url = 'http://localhost:8000'; -const TOKEN = 'xxx' +const url = `http://${__ENV.HOSTNAME}`; +const TOKEN = `${__ENV.TOKEN}` export default () => { const logMessageHeader = { diff --git a/tests/smoke.js b/tests/smoke.js index 8e2e36b..32a1cff 100644 --- a/tests/smoke.js +++ b/tests/smoke.js @@ -5,8 +5,8 @@ export const options = { vus: 2 }; -const url = 'http://localhost:8000'; -const TOKEN = 'xxx' +const url = `http://${__ENV.HOSTNAME}`; +const TOKEN = `${__ENV.TOKEN}` export default () => { const jwksRes = http.get(`${url}/.well-known/jwks.json`); From d385fea1375226fbbc511d5370cdf29f1ade6bac Mon Sep 17 00:00:00 2001 From: dhommen Date: Fri, 1 Sep 2023 12:43:42 +0200 Subject: [PATCH 059/183] refactor(tests): add header and payload module --- tests/load.js | 35 ++++----------------------------- tests/smoke.js | 42 ++++------------------------------------ tests/util/header.js | 6 ++++++ tests/util/logMessage.js | 32 ++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 69 deletions(-) create mode 100644 tests/util/header.js create mode 100644 tests/util/logMessage.js diff --git a/tests/load.js b/tests/load.js index 638f95f..743a22b 100644 --- a/tests/load.js +++ b/tests/load.js @@ -1,5 +1,7 @@ import http from 'k6/http'; -import { check, sleep } from 'k6'; +import { check } from 'k6'; +import logMessage from './util/logMessage.js'; +import header from './util/header.js'; export const options = { vus: 10, @@ -15,36 +17,7 @@ export default () => { "CH-SERVICE": TOKEN } - const date = new Date(); - const logMessagePayload = { - "header": { - - "@context": { - // ... (HashMap) - }, - "@type": "ids:LogMessage", - "@id": "String", - "modelVersion": "String", - "correlationMessage": "String", - "issued": date.toISOString(), - "issuerConnector": "InfoModelId", - "senderAgent": "String", - "recipientConnector": [ - "test" - ], - "recipientAgent": [ - "test" - ], - "transferContract": "String", - "contentVersion": "String", - "securityToken": null, - "authorizationToken": "String", - "payload": "String", - "payload_type": "String" - }, - payload: "hello world" - } - const logMessageRes = http.post(`${url}/messages/log/6`, JSON.stringify(logMessagePayload, null, 2), { headers: logMessageHeader }); + const logMessageRes = http.post(`${url}/messages/log/6`, JSON.stringify(logMessage(), null, 2), { headers: header() }); check(logMessageRes, { 'ch-app POST logmessage is status 201': (r) => r.status === 201, }); diff --git a/tests/smoke.js b/tests/smoke.js index 32a1cff..2c9aaff 100644 --- a/tests/smoke.js +++ b/tests/smoke.js @@ -1,12 +1,13 @@ import http from 'k6/http'; -import { check, sleep } from 'k6'; +import { check } from 'k6'; +import logMessage from './util/logMessage.js'; +import header from './util/header.js' export const options = { vus: 2 }; const url = `http://${__ENV.HOSTNAME}`; -const TOKEN = `${__ENV.TOKEN}` export default () => { const jwksRes = http.get(`${url}/.well-known/jwks.json`); @@ -19,43 +20,8 @@ export default () => { 'ch-app GET doctype is status 200': (r) => r.status === 200, }); - const logMessageHeader = { - "Content-Type": "application/json", - "CH-SERVICE": TOKEN - } - - const date = new Date(); - const logMessagePayload = { - "header": { - - "@context": { - // ... (HashMap) - }, - "@type": "ids:LogMessage", - "@id": "String", - "modelVersion": "String", - "correlationMessage": "String", - "issued": date.toISOString(), - "issuerConnector": "InfoModelId", - "senderAgent": "String", - "recipientConnector": [ - "test" - ], - "recipientAgent": [ - "test" - ], - "transferContract": "String", - "contentVersion": "String", - "securityToken": null, - "authorizationToken": "String", - "payload": "String", - "payload_type": "String" - }, - payload: "hello world" - } - const logMessageRes = http.post(`${url}/messages/log/6`, JSON.stringify(logMessagePayload, null, 2), { headers: logMessageHeader }); + const logMessageRes = http.post(`${url}/messages/log/6`, JSON.stringify(logMessage(), null, 2), { headers: header() }); check(logMessageRes, { 'ch-app POST logmessage is status 201': (r) => r.status === 201, }); - }; diff --git a/tests/util/header.js b/tests/util/header.js new file mode 100644 index 0000000..8f14f0a --- /dev/null +++ b/tests/util/header.js @@ -0,0 +1,6 @@ +export default () => { + return { + "Content-Type": "application/json", + "CH-SERVICE": __ENV.TOKEN + } +} \ No newline at end of file diff --git a/tests/util/logMessage.js b/tests/util/logMessage.js new file mode 100644 index 0000000..48f105f --- /dev/null +++ b/tests/util/logMessage.js @@ -0,0 +1,32 @@ +const date = new Date(); + +export default () => { + return { + "header": { + + "@context": { + // ... (HashMap) + }, + "@type": "ids:LogMessage", + "@id": "String", + "modelVersion": "String", + "correlationMessage": "String", + "issued": date.toISOString(), + "issuerConnector": "InfoModelId", + "senderAgent": "String", + "recipientConnector": [ + "test" + ], + "recipientAgent": [ + "test" + ], + "transferContract": "String", + "contentVersion": "String", + "securityToken": null, + "authorizationToken": "String", + "payload": "String", + "payload_type": "String" + }, + payload: "hello world" + } +} \ No newline at end of file From 1ce073fef0b2e70d97c58d1b14a7dec104bed3a1 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 14 Sep 2023 17:02:55 -0300 Subject: [PATCH 060/183] fix: updating .gitignore to exclude vscode files --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 35303be..b324702 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ data .idea/ node_modules/ **/*.iml +.vscode/ From a8e1cfcaffdf8e2224be7d0c3c47b05cca86713a Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 19 Sep 2023 16:31:58 -0300 Subject: [PATCH 061/183] chore: edc extension base structure --- clearing-house-edc/.gitignore | 4 + clearing-house-edc/build.gradle.kts | 74 ++++++ clearing-house-edc/core/build.gradle.kts | 24 ++ .../extensions/multipart/build.gradle.kts | 24 ++ clearing-house-edc/gradle.properties | 17 ++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63375 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + clearing-house-edc/gradlew | 248 ++++++++++++++++++ clearing-house-edc/gradlew.bat | 92 +++++++ .../launchers/connector/build.gradle.kts | 24 ++ clearing-house-edc/settings.gradle.kts | 75 ++++++ clearing-house-edc/spi/build.gradle.kts | 24 ++ 12 files changed, 613 insertions(+) create mode 100644 clearing-house-edc/.gitignore create mode 100644 clearing-house-edc/build.gradle.kts create mode 100644 clearing-house-edc/core/build.gradle.kts create mode 100644 clearing-house-edc/extensions/multipart/build.gradle.kts create mode 100644 clearing-house-edc/gradle.properties create mode 100644 clearing-house-edc/gradle/wrapper/gradle-wrapper.jar create mode 100644 clearing-house-edc/gradle/wrapper/gradle-wrapper.properties create mode 100755 clearing-house-edc/gradlew create mode 100644 clearing-house-edc/gradlew.bat create mode 100644 clearing-house-edc/launchers/connector/build.gradle.kts create mode 100644 clearing-house-edc/settings.gradle.kts create mode 100644 clearing-house-edc/spi/build.gradle.kts diff --git a/clearing-house-edc/.gitignore b/clearing-house-edc/.gitignore new file mode 100644 index 0000000..cc1fb8e --- /dev/null +++ b/clearing-house-edc/.gitignore @@ -0,0 +1,4 @@ +.gradle/ +.idea/ +.vscode/ +/build diff --git a/clearing-house-edc/build.gradle.kts b/clearing-house-edc/build.gradle.kts new file mode 100644 index 0000000..b8c18b6 --- /dev/null +++ b/clearing-house-edc/build.gradle.kts @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - initial implementation + * + */ + +plugins { + `java-library` +} + +val javaVersion: String by project +val fccScmConnection: String by project +val fccWebsiteUrl: String by project +val fccScmUrl: String by project +val groupId: String by project +val defaultVersion: String by project +val annotationProcessorVersion: String by project +val metaModelVersion: String by project + +var actualVersion: String = (project.findProperty("version") ?: defaultVersion) as String +if (actualVersion == "unspecified") { + actualVersion = defaultVersion +} + +buildscript { + repositories { + mavenLocal() + } + dependencies { + val edcGradlePluginsVersion: String by project + classpath("org.eclipse.edc.edc-build:org.eclipse.edc.edc-build.gradle.plugin:${edcGradlePluginsVersion}") + } +} + +allprojects { + apply(plugin = "${groupId}.edc-build") + + // configure which version of the annotation processor to use. defaults to the same version as the plugin + configure { + processorVersion.set(annotationProcessorVersion) + outputDirectory.set(project.buildDir) + } + + configure { + versions { + // override default dependency versions here + projectVersion.set(actualVersion) + metaModel.set(metaModelVersion) + + } + pom { + projectName.set(project.name) + description.set("edc :: ${project.name}") + projectUrl.set(fccWebsiteUrl) + scmConnection.set(fccScmConnection) + scmUrl.set(fccScmUrl) + } + javaLanguageVersion.set(JavaLanguageVersion.of(javaVersion)) + } + + configure { + configFile = rootProject.file("resources/edc-checkstyle-config.xml") + configDirectory.set(rootProject.file("resources")) + } + +} \ No newline at end of file diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts new file mode 100644 index 0000000..344526d --- /dev/null +++ b/clearing-house-edc/core/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + implementation(edc.util) + implementation(edc.core.connector) + implementation(edc.sql.core) + implementation(edc.spi.core) +} diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts new file mode 100644 index 0000000..344526d --- /dev/null +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + implementation(edc.util) + implementation(edc.core.connector) + implementation(edc.sql.core) + implementation(edc.spi.core) +} diff --git a/clearing-house-edc/gradle.properties b/clearing-house-edc/gradle.properties new file mode 100644 index 0000000..d74cdb2 --- /dev/null +++ b/clearing-house-edc/gradle.properties @@ -0,0 +1,17 @@ +groupId=org.eclipse.edc +defaultVersion=0.0.1-milestone-8 +javaVersion=17 + +# configure the build: +annotationProcessorVersion=0.0.1-milestone-8 +edcGradlePluginsVersion=0.0.1-milestone-8 +metaModelVersion=0.0.1-milestone-8 + +# other dependencies versions +postgresVersion=42.4.0 +flywayVersion=9.0.1 + +# used for publishing artifacts and plugins +fccScmConnection=scm:git:git@github.com:eclipse-dataspaceconnector/FederatedCatalog.git +fccWebsiteUrl=https://github.com/eclipse-dataspaceconnector/FederatedCatalog.git +fccScmUrl=https://github.com/eclipse-dataspaceconnector/FederatedCatalog.git \ No newline at end of file diff --git a/clearing-house-edc/gradle/wrapper/gradle-wrapper.jar b/clearing-house-edc/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..033e24c4cdf41af1ab109bc7f253b2b887023340 GIT binary patch literal 63375 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≫L+crb^Bz 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/clearing-house-edc/gradlew.bat b/clearing-house-edc/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/clearing-house-edc/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/clearing-house-edc/launchers/connector/build.gradle.kts b/clearing-house-edc/launchers/connector/build.gradle.kts new file mode 100644 index 0000000..344526d --- /dev/null +++ b/clearing-house-edc/launchers/connector/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + implementation(edc.util) + implementation(edc.core.connector) + implementation(edc.sql.core) + implementation(edc.spi.core) +} diff --git a/clearing-house-edc/settings.gradle.kts b/clearing-house-edc/settings.gradle.kts new file mode 100644 index 0000000..2c3f95e --- /dev/null +++ b/clearing-house-edc/settings.gradle.kts @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - initial implementation + * + */ + +rootProject.name = "clearing-house-edc" + +include(":spi") +include(":core") +include(":extensions:multipart") +include(":launchers:connector") + +// this is needed to have access to snapshot builds of plugins +pluginManagement { + repositories { + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + maven { + url = uri("https://oss.sonatype.org/content/repositories/snapshots/") + } + mavenCentral() + mavenLocal() + } + versionCatalogs { + create("libs") { + from("org.eclipse.edc:edc-versions:0.0.1-milestone-8") + } + // create version catalog for all EDC modules + create("edc") { + version("edc", "0.0.1-milestone-8") + library("spi-catalog", "org.eclipse.edc", "catalog-spi").versionRef("edc") + library("spi-core", "org.eclipse.edc", "core-spi").versionRef("edc") + library("spi-web", "org.eclipse.edc", "web-spi").versionRef("edc") + library("util", "org.eclipse.edc", "util").versionRef("edc") + library("boot", "org.eclipse.edc", "boot").versionRef("edc") + library("config-filesystem", "org.eclipse.edc", "configuration-filesystem").versionRef("edc") + library("core-controlplane", "org.eclipse.edc", "control-plane-core").versionRef("edc") + library("core-connector", "org.eclipse.edc", "connector-core").versionRef("edc") + library("core-jetty", "org.eclipse.edc", "jetty-core").versionRef("edc") + library("core-jersey", "org.eclipse.edc", "jersey-core").versionRef("edc") + library("junit", "org.eclipse.edc", "junit").versionRef("edc") + library("api-management-config", "org.eclipse.edc", "management-api-configuration").versionRef("edc") + library("api-management", "org.eclipse.edc", "management-api").versionRef("edc") + library("api-observability", "org.eclipse.edc", "api-observability").versionRef("edc") + library("ext-http", "org.eclipse.edc", "http").versionRef("edc") + library("spi-ids", "org.eclipse.edc", "ids-spi").versionRef("edc") + library("ids", "org.eclipse.edc", "ids").versionRef("edc") + library("ids-jsonld-serdes", "org.eclipse.edc", "ids-jsonld-serdes").versionRef("edc") + library("iam-mock", "org.eclipse.edc", "iam-mock").versionRef("edc") + library("oauth2-core", "org.eclipse.edc", "oauth2-core").versionRef("edc") + + bundle( + "connector", + listOf("boot", "core-connector", "core-jersey", "core-controlplane", "api-observability") + ) + } + } +} diff --git a/clearing-house-edc/spi/build.gradle.kts b/clearing-house-edc/spi/build.gradle.kts new file mode 100644 index 0000000..344526d --- /dev/null +++ b/clearing-house-edc/spi/build.gradle.kts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +plugins { + `java-library` +} + +dependencies { + implementation(edc.util) + implementation(edc.core.connector) + implementation(edc.sql.core) + implementation(edc.spi.core) +} From fa47ff8f18feeefd77fdcf6be9cfe266981f358b Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Wed, 20 Sep 2023 16:23:55 -0300 Subject: [PATCH 062/183] feat: create connector and extension modules --- clearing-house-edc/.gitignore | 3 +- clearing-house-edc/build.gradle.kts | 74 ------------------- clearing-house-edc/core/build.gradle.kts | 5 +- .../de/truzzt/clearinghouse/edc/core/.gitkeep | 0 .../extensions/multipart/build.gradle.kts | 12 ++- .../edc/multipart/MultipartController.java | 28 +++++++ .../edc/multipart/MultipartExtension.java | 36 +++++++++ ...rg.eclipse.edc.spi.system.ServiceExtension | 15 ++++ clearing-house-edc/gradle.properties | 18 +---- .../launchers/connector/build.gradle.kts | 13 +++- clearing-house-edc/settings.gradle.kts | 18 ++--- clearing-house-edc/spi/build.gradle.kts | 5 +- .../de/truzzt/clearinghouse/edc/spi/.gitkeep | 0 13 files changed, 109 insertions(+), 118 deletions(-) delete mode 100644 clearing-house-edc/build.gradle.kts create mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/core/.gitkeep create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension create mode 100644 clearing-house-edc/spi/src/main/java/de/truzzt/clearinghouse/edc/spi/.gitkeep diff --git a/clearing-house-edc/.gitignore b/clearing-house-edc/.gitignore index cc1fb8e..e7ae623 100644 --- a/clearing-house-edc/.gitignore +++ b/clearing-house-edc/.gitignore @@ -1,4 +1,5 @@ .gradle/ .idea/ .vscode/ -/build +**/build/ +**/bin/ diff --git a/clearing-house-edc/build.gradle.kts b/clearing-house-edc/build.gradle.kts deleted file mode 100644 index b8c18b6..0000000 --- a/clearing-house-edc/build.gradle.kts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) 2022 Microsoft Corporation - * - * This program and the accompanying materials are made available under the - * terms of the Apache License, Version 2.0 which is available at - * https://www.apache.org/licenses/LICENSE-2.0 - * - * SPDX-License-Identifier: Apache-2.0 - * - * Contributors: - * Microsoft Corporation - initial implementation - * - */ - -plugins { - `java-library` -} - -val javaVersion: String by project -val fccScmConnection: String by project -val fccWebsiteUrl: String by project -val fccScmUrl: String by project -val groupId: String by project -val defaultVersion: String by project -val annotationProcessorVersion: String by project -val metaModelVersion: String by project - -var actualVersion: String = (project.findProperty("version") ?: defaultVersion) as String -if (actualVersion == "unspecified") { - actualVersion = defaultVersion -} - -buildscript { - repositories { - mavenLocal() - } - dependencies { - val edcGradlePluginsVersion: String by project - classpath("org.eclipse.edc.edc-build:org.eclipse.edc.edc-build.gradle.plugin:${edcGradlePluginsVersion}") - } -} - -allprojects { - apply(plugin = "${groupId}.edc-build") - - // configure which version of the annotation processor to use. defaults to the same version as the plugin - configure { - processorVersion.set(annotationProcessorVersion) - outputDirectory.set(project.buildDir) - } - - configure { - versions { - // override default dependency versions here - projectVersion.set(actualVersion) - metaModel.set(metaModelVersion) - - } - pom { - projectName.set(project.name) - description.set("edc :: ${project.name}") - projectUrl.set(fccWebsiteUrl) - scmConnection.set(fccScmConnection) - scmUrl.set(fccScmUrl) - } - javaLanguageVersion.set(JavaLanguageVersion.of(javaVersion)) - } - - configure { - configFile = rootProject.file("resources/edc-checkstyle-config.xml") - configDirectory.set(rootProject.file("resources")) - } - -} \ No newline at end of file diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 344526d..1d90441 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -17,8 +17,5 @@ plugins { } dependencies { - implementation(edc.util) - implementation(edc.core.connector) - implementation(edc.sql.core) - implementation(edc.spi.core) + api(edc.spi.core) } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/core/.gitkeep b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/core/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index 344526d..099e616 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -17,8 +17,12 @@ plugins { } dependencies { - implementation(edc.util) - implementation(edc.core.connector) - implementation(edc.sql.core) - implementation(edc.spi.core) + api(edc.spi.core) + + runtimeOnly(edc.core.connector) + + implementation(edc.spi.web) + implementation(edc.api.management.config) + implementation(libs.jakarta.rsApi) + implementation(libs.jersey.multipart) } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java new file mode 100644 index 0000000..3da26ea --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -0,0 +1,28 @@ +package de.truzzt.clearinghouse.edc.multipart; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; + +import java.io.InputStream; + +@Consumes({MediaType.MULTIPART_FORM_DATA}) +@Produces({MediaType.MULTIPART_FORM_DATA}) +public class MultipartController { + + private static final String HEADER = "header"; + private static final String PAYLOAD = "payload"; + + @POST + @Path("/log") + public FormDataMultiPart request(@FormDataParam(HEADER) InputStream headerInputStream, + @FormDataParam(PAYLOAD) String payload) { + return null; + } + +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java new file mode 100644 index 0000000..c5ecdfc --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java @@ -0,0 +1,36 @@ +package de.truzzt.clearinghouse.edc.multipart; + +import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.runtime.metamodel.annotation.Requires; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.web.spi.WebService; + +@Extension(value = MultipartExtension.NAME) +@Requires(value = { + WebService.class, + ManagementApiConfiguration.class +}) +public class MultipartExtension implements ServiceExtension { + + public static final String NAME = "Clearing House Multipart Extension"; + + @Inject + private WebService webService; + + @Inject + private ManagementApiConfiguration managementApiConfig; + + @Override + public String name() { + return NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + var multipartController = new MultipartController(); + webService.registerResource(managementApiConfig.getContextAlias(), multipartController); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension new file mode 100644 index 0000000..ae7a3a9 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -0,0 +1,15 @@ +# +# Copyright (c) 2022 Microsoft Corporation +# +# This program and the accompanying materials are made available under the +# terms of the Apache License, Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# +# Contributors: +# Microsoft Corporation - initial implementation +# +# + +de.truzzt.clearinghouse.edc.multipart.MultipartExtension \ No newline at end of file diff --git a/clearing-house-edc/gradle.properties b/clearing-house-edc/gradle.properties index d74cdb2..66d3a2d 100644 --- a/clearing-house-edc/gradle.properties +++ b/clearing-house-edc/gradle.properties @@ -1,17 +1,3 @@ -groupId=org.eclipse.edc -defaultVersion=0.0.1-milestone-8 javaVersion=17 - -# configure the build: -annotationProcessorVersion=0.0.1-milestone-8 -edcGradlePluginsVersion=0.0.1-milestone-8 -metaModelVersion=0.0.1-milestone-8 - -# other dependencies versions -postgresVersion=42.4.0 -flywayVersion=9.0.1 - -# used for publishing artifacts and plugins -fccScmConnection=scm:git:git@github.com:eclipse-dataspaceconnector/FederatedCatalog.git -fccWebsiteUrl=https://github.com/eclipse-dataspaceconnector/FederatedCatalog.git -fccScmUrl=https://github.com/eclipse-dataspaceconnector/FederatedCatalog.git \ No newline at end of file +edcGroup=org.eclipse.edc +edcVersion=0.3.0 diff --git a/clearing-house-edc/launchers/connector/build.gradle.kts b/clearing-house-edc/launchers/connector/build.gradle.kts index 344526d..a26afdf 100644 --- a/clearing-house-edc/launchers/connector/build.gradle.kts +++ b/clearing-house-edc/launchers/connector/build.gradle.kts @@ -12,13 +12,18 @@ * */ + plugins { `java-library` + id("application") } dependencies { - implementation(edc.util) - implementation(edc.core.connector) - implementation(edc.sql.core) - implementation(edc.spi.core) + runtimeOnly(project(":extensions:multipart")) + + runtimeOnly(edc.bundles.connector) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") } diff --git a/clearing-house-edc/settings.gradle.kts b/clearing-house-edc/settings.gradle.kts index 2c3f95e..8438ced 100644 --- a/clearing-house-edc/settings.gradle.kts +++ b/clearing-house-edc/settings.gradle.kts @@ -12,14 +12,6 @@ * */ -rootProject.name = "clearing-house-edc" - -include(":spi") -include(":core") -include(":extensions:multipart") -include(":launchers:connector") - -// this is needed to have access to snapshot builds of plugins pluginManagement { repositories { maven { @@ -42,7 +34,6 @@ dependencyResolutionManagement { create("libs") { from("org.eclipse.edc:edc-versions:0.0.1-milestone-8") } - // create version catalog for all EDC modules create("edc") { version("edc", "0.0.1-milestone-8") library("spi-catalog", "org.eclipse.edc", "catalog-spi").versionRef("edc") @@ -67,9 +58,14 @@ dependencyResolutionManagement { library("oauth2-core", "org.eclipse.edc", "oauth2-core").versionRef("edc") bundle( - "connector", - listOf("boot", "core-connector", "core-jersey", "core-controlplane", "api-observability") + "connector", + listOf("boot", "core-connector", "core-jersey", "core-controlplane", "api-observability") ) } } } + +include(":spi") +include(":core") +include(":extensions:multipart") +include(":launchers:connector") diff --git a/clearing-house-edc/spi/build.gradle.kts b/clearing-house-edc/spi/build.gradle.kts index 344526d..1d90441 100644 --- a/clearing-house-edc/spi/build.gradle.kts +++ b/clearing-house-edc/spi/build.gradle.kts @@ -17,8 +17,5 @@ plugins { } dependencies { - implementation(edc.util) - implementation(edc.core.connector) - implementation(edc.sql.core) - implementation(edc.spi.core) + api(edc.spi.core) } diff --git a/clearing-house-edc/spi/src/main/java/de/truzzt/clearinghouse/edc/spi/.gitkeep b/clearing-house-edc/spi/src/main/java/de/truzzt/clearinghouse/edc/spi/.gitkeep new file mode 100644 index 0000000..e69de29 From fe19cdf8c153a1108759a27f689ed3fdc2197ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 21 Sep 2023 18:00:30 +0200 Subject: [PATCH 063/183] fix(ch-app): Updated dependencies to fix security vulnerability --- clearing-house-app/Cargo.lock | 182 ++++++++++++++++++---------------- 1 file changed, 95 insertions(+), 87 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index f29bc44..7e29dde 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -55,10 +55,11 @@ dependencies = [ [[package]] name = "ahash" -version = "0.7.6" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ + "cfg-if", "getrandom", "once_cell", "version_check", @@ -66,9 +67,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -122,7 +123,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -133,7 +134,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -171,9 +172,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.3" +version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" +checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" [[package]] name = "binascii" @@ -242,9 +243,9 @@ dependencies = [ [[package]] name = "bson" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aeb8bae494e49dbc330dd23cf78f6f7accee22f640ce3ab17841badaa4ce232" +checksum = "58da0ae1e701ea752cc46c1bb9f39d5ecefc7395c3ecd526261a566d4f16e0c2" dependencies = [ "ahash", "base64 0.13.1", @@ -252,7 +253,7 @@ dependencies = [ "hex", "indexmap 1.9.3", "js-sys", - "lazy_static", + "once_cell", "rand", "serde", "serde_bytes", @@ -263,9 +264,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" @@ -275,9 +276,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" @@ -296,15 +297,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "winapi", + "windows-targets", ] [[package]] @@ -324,7 +325,7 @@ dependencies = [ "aes", "aes-gcm-siv", "anyhow", - "base64 0.21.3", + "base64 0.21.4", "biscuit", "blake2-rfc", "chrono", @@ -458,9 +459,9 @@ dependencies = [ [[package]] name = "dashmap" -version = "5.5.2" +version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b101bb8960ab42ada6ae98eb82afcea4452294294c45b681295af26610d6d28" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -535,7 +536,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -618,11 +619,17 @@ dependencies = [ "atomic", "pear", "serde", - "toml 0.7.6", + "toml 0.7.8", "uncased", "version_check", ] +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "fnv" version = "1.0.7" @@ -715,7 +722,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -833,9 +840,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1022,7 +1029,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.3", + "socket2 0.5.4", "widestring", "windows-sys", "winreg", @@ -1068,9 +1075,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "linked-hash-map" @@ -1080,9 +1087,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "lock_api" @@ -1156,9 +1163,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "mime" @@ -1330,9 +1337,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -1355,7 +1362,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75a0ec2d1b302412fb503224289325fcc0e44600176864804c7211b055cfd58" dependencies = [ - "base64 0.21.3", + "base64 0.21.4", "byteorder", "md-5", "sha2", @@ -1385,7 +1392,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -1396,9 +1403,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.92" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db7e971c2c2bba161b2d2fdf37080177eff520b3bc044787c7f1f5f9e78d869b" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", @@ -1470,7 +1477,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -1517,9 +1524,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -1532,7 +1539,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "version_check", "yansi 1.0.0-rc.1", ] @@ -1614,18 +1621,18 @@ checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] name = "regex" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.7", + "regex-automata 0.3.8", "regex-syntax 0.7.5", ] @@ -1640,9 +1647,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -1737,7 +1744,7 @@ dependencies = [ "proc-macro2", "quote", "rocket_http", - "syn 2.0.29", + "syn 2.0.37", "unicode-xid", ] @@ -1804,9 +1811,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.10" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", "errno", @@ -1833,7 +1840,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.3", + "base64 0.21.4", ] [[package]] @@ -1917,14 +1924,14 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "indexmap 2.0.0", "itoa", @@ -1985,7 +1992,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2039,9 +2046,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" @@ -2055,9 +2062,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys", @@ -2095,10 +2102,11 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" dependencies = [ + "finl_unicode", "unicode-bidi", "unicode-normalization", ] @@ -2128,9 +2136,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" dependencies = [ "proc-macro2", "quote", @@ -2164,22 +2172,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2248,7 +2256,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.3", + "socket2 0.5.4", "tokio-macros", "windows-sys", ] @@ -2261,7 +2269,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2300,9 +2308,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -2324,9 +2332,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", @@ -2345,9 +2353,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.0.0", "serde", @@ -2382,7 +2390,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", ] [[package]] @@ -2488,9 +2496,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "ubyte" @@ -2519,9 +2527,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -2629,7 +2637,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "wasm-bindgen-shared", ] @@ -2651,7 +2659,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.37", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2674,9 +2682,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" dependencies = [ "ring", "untrusted", From 856c38b8c59060f091cbe6372bd9f63b00bf32e6 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 21 Sep 2023 16:12:50 -0300 Subject: [PATCH 064/183] chore: fix extensions dependencies --- clearing-house-edc/extensions/multipart/build.gradle.kts | 5 ++--- clearing-house-edc/launchers/connector/build.gradle.kts | 1 + clearing-house-edc/settings.gradle.kts | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index 099e616..570361c 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -19,9 +19,8 @@ plugins { dependencies { api(edc.spi.core) - runtimeOnly(edc.core.connector) - - implementation(edc.spi.web) + implementation(edc.ids) + implementation(edc.ids.jsonld.serdes) implementation(edc.api.management.config) implementation(libs.jakarta.rsApi) implementation(libs.jersey.multipart) diff --git a/clearing-house-edc/launchers/connector/build.gradle.kts b/clearing-house-edc/launchers/connector/build.gradle.kts index a26afdf..c97d394 100644 --- a/clearing-house-edc/launchers/connector/build.gradle.kts +++ b/clearing-house-edc/launchers/connector/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { runtimeOnly(project(":extensions:multipart")) runtimeOnly(edc.bundles.connector) + runtimeOnly(edc.iam.mock) } application { diff --git a/clearing-house-edc/settings.gradle.kts b/clearing-house-edc/settings.gradle.kts index 8438ced..6c97c1e 100644 --- a/clearing-house-edc/settings.gradle.kts +++ b/clearing-house-edc/settings.gradle.kts @@ -27,6 +27,9 @@ dependencyResolutionManagement { maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } + maven { + url = uri("https://maven.iais.fraunhofer.de/artifactory/eis-ids-public/") + } mavenCentral() mavenLocal() } From 0b732ebaaa819b234b9bc0dcf620932d5ff813c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Fri, 22 Sep 2023 15:14:16 +0200 Subject: [PATCH 065/183] Feat(ch-app): Added more documentation of fields and added more Message Types --- clearing-house-app/src/model/ids/message.rs | 38 ++-- clearing-house-app/src/model/ids/mod.rs | 198 +++++++++++++++++++- 2 files changed, 206 insertions(+), 30 deletions(-) diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index aa950f6..c4f6ea3 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -20,6 +20,7 @@ pub const RESULT_MESSAGE: &str = "ResultMessage"; pub const REJECTION_MESSAGE: &str = "RejectionMessage"; pub const MESSAGE_PROC_NOTIFICATION_MESSAGE: &str = "MessageProcessedNotificationMessage"; +/// Metadata describing payload exchanged by interacting Connectors. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct IdsMessage { //IDS name @@ -38,77 +39,68 @@ pub struct IdsMessage { #[serde(skip)] // process id pub pid: Option, - //IDS name + /// Version of the Information Model against which the Message should be interpreted #[serde(rename = "ids:modelVersion", alias = "modelVersion")] - // Version of the Information Model against which the Message should be interpreted pub model_version: String, - //IDS name + /// Correlated message, e.g., response to a previous message. Value: URI of the correlatedMessage #[serde( rename = "ids:correlationMessage", alias = "correlationMessage", skip_serializing_if = "Option::is_none" )] - // Correlated message, e.g. a response to a previous request pub correlation_message: Option, - //IDS name + /// Date of issuing the Message #[serde(rename = "ids:issued", alias = "issued")] - // Date of issuing the Message pub issued: InfoModelDateTime, - //IDS name #[serde(rename = "ids:issuerConnector", alias = "issuerConnector")] - // The Connector which is the origin of the message + /// Origin Connector of the message. Value: URI of origin Connector pub issuer_connector: InfoModelId, - //IDS name + /// Agent, which initiated the message. Value: URI of an instance of ids:Agent. #[serde(rename = "ids:senderAgent", alias = "senderAgent")] - // The Agent which initiated the Message pub sender_agent: String, - //IDS name + /// Target Connector. Value: URI of target Connector. Can have multiple values at the same time. #[serde( rename = "ids:recipientConnector", alias = "recipientConnector", skip_serializing_if = "Option::is_none" )] - // The Connector which is the recipient of the message pub recipient_connector: Option>, - //IDS name + /// Agent, for which the message is intended. Value: URI of an instance of ids:Agent. Can have multiple values at the same time #[serde( rename = "ids:recipientAgent", alias = "recipientAgent", skip_serializing_if = "Option::is_none" )] - // The Agent for which the Message is intended pub recipient_agent: Option>, - //IDS name + /// Contract which is (or will be) the legal basis of the data transfer. Value: Instance of class ids:Contract. #[serde( rename = "ids:transferContract", alias = "transferContract", skip_serializing_if = "Option::is_none" )] - // The contract which is (or will be) the legal basis of the data transfer pub transfer_contract: Option, - //IDS name + /// Value describing the version of the content. Value: Version number of the content. #[serde( rename = "ids:contentVersion", alias = "contentVersion", skip_serializing_if = "Option::is_none" )] - // The contract which is (or will be) the legal basis of the data transfer pub content_version: Option, - //IDS name + /// Token representing a claim, that the sender supports a certain security profile. Value: Instance of ids:DynamicAttributeToken. #[serde( rename = "ids:securityToken", alias = "securityToken", skip_serializing )] - // Authorization pub security_token: Option, - //IDS name + /// An authorization token. The token can be issued from the Connector of the Data Provider (A) to the Connector of the + /// Data Consumer (B). Can be used to avoid full authentication via DAPS, if Connector B wants to access the data of + /// Connector A. Value: Instance of ids:Token #[serde( rename = "ids:authorizationToken", alias = "authorizationToken", skip_serializing_if = "Option::is_none" )] - // Authorization pub authorization_token: Option, //IDS name #[serde(skip_serializing_if = "Option::is_none")] @@ -151,7 +143,7 @@ impl IdsMessage { pub fn processed(msg: IdsMessage) -> IdsMessage { let mut message = IdsMessage::clone(msg); message.id = Some(autogen(MESSAGE_PROC_NOTIFICATION_MESSAGE)); - message.type_message = MessageType::MessageProcessedNotification; + message.type_message = MessageType::MessageProcessedNotificationMessage; message } diff --git a/clearing-house-app/src/model/ids/mod.rs b/clearing-house-app/src/model/ids/mod.rs index 194f183..020738e 100644 --- a/clearing-house-app/src/model/ids/mod.rs +++ b/clearing-house-app/src/model/ids/mod.rs @@ -122,28 +122,212 @@ impl std::fmt::Display for InfoModelTimeStamp { } } +/** +There are three Subclasses of the abstract ids:Message class. Namely the ids:RequestMessage, ids:ResponseMessage +and ids:NotificationMessage. Each subclass itself has subclasses that fulfill a specific purpose in the communication process. + +For communication in the IDS, usually the more specific subclasses of the three mentioned ones are used. +The message classes relevant for the Connector to Connector communication are listed below. The entire Collection of Messages +available in the Information Model can be found here. + +Based on [v4.2.0](https://github.com/International-Data-Spaces-Association/InformationModel/blob/v4.2.0/taxonomies/Message.ttl) +*/ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub enum MessageType { #[serde(rename = "ids:Message")] Message, - #[serde(rename = "ids:Query")] - Query, - #[serde(rename = "ids:LogMessage")] - LogMessage, - #[serde(rename = "ids:QueryMessage")] - QueryMessage, + + /// ## Basic Message Types: Request, Response, Notification + /// Client-generated message initiating a communication, motivated by a certain reason and with an answer expected. #[serde(rename = "ids:RequestMessage")] RequestMessage, + /// Response messages hold information about the reaction of a recipient to a formerly sent command or event. They must be correlated to this message. + #[serde(rename = "ids:ResponseMessage")] + ResponseMessage, + /// Event messages are informative and no response is expected by the sender. + #[serde(rename = "ids:NotificationMessage")] + NotificationMessage, + + /// ## Core IDS Messages + /// Command messages are usually sent when a response is expected by the sender. Changes state on the recipient side. Therefore, commands are not 'safe' in the sense of REST. + #[serde(rename = "ids:CommandMessage")] + CommandMessage, + /// Result messages are intended to annotate the results of a query command. #[serde(rename = "ids:ResultMessage")] ResultMessage, + /// Rejection messages are specialized response messages that notify the sender of a message that processing of this message has failed. #[serde(rename = "ids:RejectionMessage")] RejectionMessage, + + + /// ## Self-description + /// Message requesting metadata. If no URI is supplied via the ids:requestedElement field, this messages is treated like a self-description request and the recipient should return its self-description via an ids:DescriptionResponseMessage. However, if a URI is supplied, the Connector should either return metadata about the requested element via an ids:DescriptionResponseMessage, or send an ids:RejectionMessage, e.g., because the element was not found. + #[serde(rename = "ids:DescriptionRequestMessage")] + DescriptionRequestMessage, + /// Message containing the metadata, which a Connector previously requested via the ids:DescriptionRequestMessage, in its payload. + #[serde(rename = "ids:DescriptionResponseMessage")] + DescriptionResponseMessage, + + /// ## Connector-related Messages + /// Superclass of all messages, indicating a change of a connector's conditions. + #[serde(rename = "ids:ConnectorNotificationMessage")] + ConnectorNotificationMessage, + /// Event notifying the recipient(s) about the availability and current configuration of a connector. The payload of the message must contain the updated connector's self-description. + #[serde(rename = "ids:ConnectorUpdateMessage")] + ConnectorUpdateMessage, + /// Event notifying the recipient(s) that a connector will be unavailable. The same connector may be available again in the future. + #[serde(rename = "ids:ConnectorUnavailableMessage")] + ConnectorUnavailableMessage, + /// Whenever a Connector has been successfully certified by the Certification Body, the Identity Provider can use this message to notify Infrastructure Components. + #[serde(rename = "ids:ConnectorCertificateGrantedMessage")] + ConnectorCertificateGrantedMessage, + /// Indicates that a (previously certified) Connector is no more certified. This could happen, for instance, if the Certification Body revokes a granted certificate or if the certificate just expires. + #[serde(rename = "ids:ConnectorCertificateRevokedMessage")] + ConnectorCertificateRevokedMessage, + + /// ## Participant-related Messages + /// Superclass of all messages, indicating a change of a particpants's conditions. + #[serde(rename = "ids:ParticipantNotificationMessage")] + ParticipantNotificationMessage, + /// Event notifying the recipient(s) about the availability and current description of a participant. The payload of the message must contain the participant's self-description. + #[serde(rename = "ids:ParticipantUpdateMessage")] + ParticipantUpdateMessage, + /// Event notifying the recipient(s) that a participant will be unavailable. The same participant may be available again in the future. + #[serde(rename = "ids:ParticipantUnavailableMessage")] + ParticipantUnavailableMessage, + /// Whenever a Participant has been successfully certified by the Certification Body, the Identity Provider can use this message to notify Infrastructure Components. + #[serde(rename = "ids:ParticipantCertificateGrantedMessage")] + ParticipantCertificateGrantedMessage, + /// Indicates that a (previously certified) Participant is no more certified. This could happen, for instance, if the Certification Body revokes a granted certificate or if the certificate just expires. + #[serde(rename = "ids:ParticipantCertificateRevokedMessage")] + ParticipantCertificateRevokedMessage, + + /// ## Query related Messages + /// Query message intended to be consumed by a component. + #[serde(rename = "ids:QueryMessage")] + QueryMessage, + /// Class of query languages in which query strings may be formalized. + #[serde(rename = "ids:QueryLanguage")] + QueryLanguage, + /// Class of recipients of a query message, e.g., BROKER, APPSTORE, ANY. + #[serde(rename = "ids:QueryTarget")] + QueryTarget, + + /// ## Contract Negotiation related Messages + /// Message containing a suggested content contract (as offered by the data consumer to the data provider) in the associated payload (which is an instance of ids:ContractRequest). + #[serde(rename = "ids:ContractRequestMessage")] + ContractRequestMessage, + /// Message containing a response to a contract request (of a data consumer) in form of a counter-proposal of a contract in the associated payload (which is an instance of ids:ContractOffer). + #[serde(rename = "ids:ContractResponseMessage")] + ContractResponseMessage, + /// Message containing a offered content contract (as offered by a data provider to the data consumer) in the associated payload (which is an instance of ids:ContractOffer). In contrast to the ids:ContractResponseMessage, the ids:ContractOfferMessage is not related to a previous contract + #[serde(rename = "ids:ContractOfferMessage")] + ContractOfferMessage, + /// Message containing a contract, as an instance of ids:ContractAgreement, with resource access modalities on which two parties have agreed in the payload. + #[serde(rename = "ids:ContractAgreementMessage")] + ContractAgreementMessage, + /// Message indicating rejection of a contract. + #[serde(rename = "ids:ContractRejectionMessage")] + ContractRejectionMessage, + /// Message containing supplemental information to access resources of a contract (e.g., resource access tokens). + #[serde(rename = "ids:ContractSupplementMessage")] + ContractSupplementMessage, + + /// ## Security-related Messages + /// Message requesting an access token. This is intended for point-to-point communication with, e.g., Brokers. + #[serde(rename = "ids:AccessTokenRequestMessage")] + AccessTokenRequestMessage, + /// Response to an access token request, intended for point-to-point communication. + #[serde(rename = "ids:AccessTokenResponseMessage")] + AccessTokenResponseMessage, + + /// ## Resource related messages + /// Superclass of all messages, indicating a change of a resource. + #[serde(rename = "ids:ResourceNotificationMessage")] + ResourceNotificationMessage, + /// Message indicating the availability and current description of a specific resource. The resource must be present in the payload of this message. + #[serde(rename = "ids:ResourceUpdateMessage")] + ResourceUpdateMessage, + /// Message indicating that a specific resource is unavailable. The same resource may be available again in the future. + #[serde(rename = "ids:ResourceUnavailableMessage")] + ResourceUnavailableMessage, + /// Message requesting the recipient to invoke a specific operation. + #[serde(rename = "ids:OperationInvokeMessage")] + OperationInvokeMessage, + /// Notification that a request has been accepted and is being processed. + #[serde(rename = "ids:RequestInProcessMessage")] + RequestInProcessMessage, + /// Notification that a message has been successfully processed (i.e. not ignored or rejected). #[serde(rename = "ids:MessageProcessedNotificationMessage")] - MessageProcessedNotification, + MessageProcessedNotificationMessage, + /// Message indicating that the result of a former InvokeOperation message is available. May transfer the result data in its associated payload section. + #[serde(rename = "ids:OperationResultMessage")] + OperationResultMessage, + + /// ## Artifact-related Messages + /// Message asking for retrieving the specified Artifact as the payload of an ArtifactResponse message. + #[serde(rename = "ids:ArtifactRequestMessage")] + ArtifactRequestMessage, + /// Message that follows up a RetrieveArtifact Message and contains the Artifact's data in the payload section. + #[serde(rename = "ids:ArtifactResponseMessage")] + ArtifactResponseMessage, + + /// ## Upload Messages + /// Message used to upload a data to a recipient. Payload contains data. + #[serde(rename = "ids:UploadMessage")] + UploadMessage, + /// Message that follows up a UploadMessage and contains the upload confirmation. + #[serde(rename = "ids:UploadResponseMessage")] + UploadResponseMessage, + + /// ## ParIS Messages + /// This class is deprecated. Use ids:DescriptionRequestMessage instead. Message asking for retrieving the specified Participants information as the payload of an ids:ParticipantResponse message. + #[serde(rename = "ids:ParticipantRequestMessage")] + ParticipantRequestMessage, + /// This class is deprecated. Use ids:DescriptionResponseMessage instead. ParticipantResponseMessage follows up a ParticipantRequestMessage and contains the Participant's information in the payload section. + #[serde(rename = "ids:ParticipantResponseMessage")] + ParticipantResponseMessage, + + /// ## Log messaging + /// Log Message which can be used to transfer logs e.g., to the clearing house. + #[serde(rename = "ids:LogMessage")] + LogMessage, + + /// ## App-related Messages + /// Message that asks for registration or update of a data app to the App Store. Payload contains app-related metadata (instance of class ids:AppResource). Message header may contain an app identifier parameter of a prior registered data app. If the app identifier is supplied, the message should be interpreted as a registration for an app update. Otherwise this message is used to register a new app. + #[serde(rename = "ids:AppRegistrationRequestMessage")] + AppRegistrationRequestMessage, + /// Message that follows up an AppRegistrationRequestMessage and contains the app registration confirmation. + #[serde(rename = "ids:AppRegistrationResponseMessage")] + AppRegistrationResponseMessage, + /// Message that usually follows a AppRegistrationResponseMessage and is used to upload a data app to the app store. Payload contains data app. Note that the message must refer to the prior sent, corresponding AppResource instance. The IRI of the ids:appArtifactReference must must match the IRI of the artifact which is the value for the ids:instance property. The ids:instance is specific for each representation. Therefore, if someone wants to upload multiple representations for an app, he has to state them using multiple ids:instance properties inside the AppRepresentation (and therefore inside the AppResource). Otherwise no mapping between payload and app metadata can be achieved. + #[serde(rename = "ids:AppUploadMessage")] + AppUploadMessage, + /// Message that follows up an AppUploadMessage and contains the app upload confimation. + #[serde(rename = "ids:AppUploadResponseMessage")] + AppUploadResponseMessage, + /// Superclass of all messages, indicating a change of a DataApp. Unlike Resource-related Messages, AppNotificationMessages should lead to a state change for an app at the recipient, the AppStore. + #[serde(rename = "ids:AppNotificationMessage")] + AppNotificationMessage, + /// Message indicating that a specific App should be available (again) in the AppStore. + #[serde(rename = "ids:AppAvailableMessage")] + AppAvailableMessage, + /// Message indicating that a specific App should be unavailable in the AppStore. + #[serde(rename = "ids:AppUnavailableMessage")] + AppUnavailableMessage, + /// Message indicating that an App should be deleted from the AppStore. + #[serde(rename = "ids:AppDeleteMessage")] + AppDeleteMessage, + + /// TODO: Not existent in the IDS Information Model #[serde(rename = "ids:DynamicAttributeToken")] DAPSToken, + /* + #[serde(rename = "ids:Query")] + Query, //otherwise Other, + */ } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] From 3a8d5a15c08151ea2d43f70d7a25ecb4f4555424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Fri, 22 Sep 2023 15:16:30 +0200 Subject: [PATCH 066/183] fix(ci): Delete .github/workflows/rust.yml to fix failing CI --- .github/workflows/rust.yml | 54 -------------------------------------- 1 file changed, 54 deletions(-) delete mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index b20439c..0000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,54 +0,0 @@ -# name: Rust - -# on: -# push: -# branches: -# - master -# - development -# pull_request: -# branches: -# - master - -# env: -# CARGO_TERM_COLOR: always -# IMAGE_NAME_LS: ids-ch-logging-service -# IMAGE_BASE: ghcr.io/truzzt/ids-basecamp-clearinghouse - - -# jobs: -# build: -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v3 -# # TODO: do not use caching for actual release builds, aka ones that start with v* -# - uses: Swatinem/rust-cache@v2 -# - name: Build clearing-house-api -# working-directory: ./clearing-house-app -# run: cargo build --release - -# - name: Build build images -# run: | -# docker build . --file docker/logging-service.Dockerfile --tag $IMAGE_NAME_LS - -# - name: Log into registry -# run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - -# - name: Push image -# run: | -# IMAGE_ID_LS=$IMAGE_BASE/$IMAGE_NAME_LS - -# # Change all uppercase to lowercase -# IMAGE_ID_LS=$(echo $IMAGE_ID_LS | tr '[A-Z]' '[a-z]') - -# # Strip git ref prefix from version -# VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,') - -# # Strip "v" prefix from tag name -# [[ "${{ github.ref }}" == "refs/tags/"* ]] && VERSION=$(echo $VERSION | sed -e 's/^v//') - -# # Use Docker `latest` tag convention -# [ "$VERSION" == "master" ] && VERSION=latest - -# docker tag $IMAGE_NAME_LS $IMAGE_ID_LS:$VERSION -# docker push $IMAGE_ID_LS:$VERSION - From f13f15e7e35c866f011a4474bc3bd5722d8a40b9 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Fri, 22 Sep 2023 11:24:24 -0300 Subject: [PATCH 067/183] feat: starting create objects and method --- .../edc/multipart/MultipartController.java | 28 --- .../edc/multipart/MultipartExtension.java | 16 +- .../controller/MultipartController.java | 58 ++++++ .../edc/multipart/types/TypeManagerUtil.java | 62 ++++++ .../edc/multipart/types/ids/Context.java | 36 ++++ .../types/ids/DynamicAttributeToken.java | 74 +++++++ .../edc/multipart/types/ids/Message.java | 193 ++++++++++++++++++ .../multipart/types/ids/RejectionMessage.java | 40 ++++ .../multipart/types/ids/RejectionReason.java | 58 ++++++ .../edc/multipart/types/ids/TokenFormat.java | 49 +++++ .../multipart/types/ids/util/VocabUtil.java | 41 ++++ .../edc/multipart/types/jwt/JwtPayload.java | 34 +++ .../edc/multipart/util/ResponseUtil.java | 66 ++++++ 13 files changed, 726 insertions(+), 29 deletions(-) delete mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/DynamicAttributeToken.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/util/VocabUtil.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/jwt/JwtPayload.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java deleted file mode 100644 index 3da26ea..0000000 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.truzzt.clearinghouse.edc.multipart; - -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; - -import java.io.InputStream; - -@Consumes({MediaType.MULTIPART_FORM_DATA}) -@Produces({MediaType.MULTIPART_FORM_DATA}) -public class MultipartController { - - private static final String HEADER = "header"; - private static final String PAYLOAD = "payload"; - - @POST - @Path("/log") - public FormDataMultiPart request(@FormDataParam(HEADER) InputStream headerInputStream, - @FormDataParam(PAYLOAD) String payload) { - return null; - } - -} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java index c5ecdfc..1ccde37 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java @@ -1,13 +1,19 @@ package de.truzzt.clearinghouse.edc.multipart; +import de.truzzt.clearinghouse.edc.multipart.controller.MultipartController; +import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.protocol.ids.jsonld.JsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Requires; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebService; +import static org.eclipse.edc.protocol.ids.util.ConnectorIdUtil.resolveConnectorId; + @Extension(value = MultipartExtension.NAME) @Requires(value = { WebService.class, @@ -15,6 +21,10 @@ }) public class MultipartExtension implements ServiceExtension { + @Setting + public static final String EDC_IDS_ID = "edc.ids.id"; + public static final String DEFAULT_EDC_IDS_ID = "urn:connector:edc"; + public static final String NAME = "Clearing House Multipart Extension"; @Inject @@ -30,7 +40,11 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { - var multipartController = new MultipartController(); + var connectorId = resolveConnectorId(context); + var typeManagerUtil = new TypeManagerUtil(JsonLd.getObjectMapper()); + + var multipartController = new MultipartController(connectorId, typeManagerUtil); webService.registerResource(managementApiConfig.getContextAlias(), multipartController); } + } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java new file mode 100644 index 0000000..36c756e --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java @@ -0,0 +1,58 @@ +package de.truzzt.clearinghouse.edc.multipart.controller; + +import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataParam; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.jetbrains.annotations.NotNull; + +import java.io.InputStream; + +import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.malformedMessage; + +@Consumes({MediaType.MULTIPART_FORM_DATA}) +@Produces({MediaType.MULTIPART_FORM_DATA}) +@Path("/") +public class MultipartController { + + private static final String HEADER = "header"; + private static final String PAYLOAD = "payload"; + + private final IdsId connectorId; + + private final TypeManagerUtil typeManagerUtil; + + public MultipartController(@NotNull IdsId connectorId, TypeManagerUtil typeManagerUtil) { + this.connectorId = connectorId; + this.typeManagerUtil = typeManagerUtil; + } + + @POST + @Path("log") + public FormDataMultiPart request(@FormDataParam(HEADER) InputStream headerInputStream, + @FormDataParam(PAYLOAD) String payload) { + + if (headerInputStream == null) { + return createFormDataMultiPart(malformedMessage(null, connectorId)); + } + + return null; + } + + private FormDataMultiPart createFormDataMultiPart(Message header) { + var multiPart = new FormDataMultiPart(); + if (header != null) { + multiPart.bodyPart(new FormDataBodyPart(HEADER, typeManagerUtil.toJson(header), MediaType.APPLICATION_JSON_TYPE)); + } + return multiPart; + } + +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java new file mode 100644 index 0000000..07fe3a6 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.multipart.types.ids.DynamicAttributeToken; +import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import de.truzzt.clearinghouse.edc.multipart.types.jwt.JwtPayload; +import org.eclipse.edc.spi.EdcException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Base64; + +public class TypeManagerUtil { + + private final ObjectMapper mapper; + + public TypeManagerUtil(ObjectMapper mapper) { + this.mapper = mapper; + } + + public Message parseMessage(InputStream streamToken) { + try { + return mapper.readValue(streamToken, Message.class); + } catch (IOException e) { + throw new EdcException("Error parsing Header to Message", e); + } + } + + public JwtPayload parseToken(DynamicAttributeToken token) { + try { + Base64.Decoder decoder = Base64.getUrlDecoder(); + String[] chunks = token.getTokenValue().split("\\."); + return mapper.readValue(decoder.decode(chunks[1]), JwtPayload.class); + + } catch (IOException e) { + throw new EdcException("Error parsing Token", e); + } + } + + public byte[] toJson(Object object) { + try { + return mapper.writeValueAsBytes(object); + } catch (JsonProcessingException e) { + throw new EdcException("Error converting to JSON", e); + } + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java new file mode 100644 index 0000000..71bda98 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.ids; + +public class Context { + private String ids; + private String idsc; + + public String getIds() { + return ids; + } + + public void setIds(String ids) { + this.ids = ids; + } + + public String getIdsc() { + return idsc; + } + + public void setIdsc(String idsc) { + this.idsc = idsc; + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/DynamicAttributeToken.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/DynamicAttributeToken.java new file mode 100644 index 0000000..b8770bc --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/DynamicAttributeToken.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.ids; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import de.truzzt.clearinghouse.edc.multipart.types.ids.util.VocabUtil; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type") +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeName("ids:DynamicAttributeToken") +public class DynamicAttributeToken { + + @JsonProperty("@id") + @JsonAlias({"@id", "id"}) + @NotNull + private URI id; + + @NotNull + @JsonAlias({"ids:tokenFormat", "tokenFormat"}) + private TokenFormat tokenFormat; + + @NotNull + @JsonAlias({"ids:tokenValue", "tokenValue"}) + private String tokenValue; + + private DynamicAttributeToken() { + id = VocabUtil.createRandomUrl("dynamicAttributeToken"); + } + + @JsonProperty("@id") + public URI getId() { + return id; + } + + public void setId(URI id) { + this.id = id; + } + + public TokenFormat getTokenFormat() { + return tokenFormat; + } + + public void setTokenFormat(TokenFormat tokenFormat) { + this.tokenFormat = tokenFormat; + } + + public String getTokenValue() { + return tokenValue; + } + + public void setTokenValue(String tokenValue) { + this.tokenValue = tokenValue; + } +} + diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java new file mode 100644 index 0000000..80d0352 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.ids; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +import javax.xml.datatype.XMLGregorianCalendar; +import java.net.URI; +import java.util.List; +import java.util.Map; + +public class Message { + + + @JsonProperty("@context") + @NotNull + private Context context; + + @JsonProperty("@id") + @NotNull + private URI id; + + @JsonProperty("@type") + @NotNull + private String type; + + @NotNull + @JsonProperty("ids:securityToken") + @JsonAlias({"ids:securityToken", "securityToken"}) + private DynamicAttributeToken securityToken; + + @NotNull + @JsonProperty("ids:issuerConnector") + @JsonAlias({"ids:issuerConnector", "issuerConnector"}) + private URI issuerConnector; + + @NotNull + @JsonProperty("ids:modelVersion") + @JsonAlias({"ids:modelVersion", "modelVersion"}) + String modelVersion; + + @JsonProperty("ids:correlationMessage") + @JsonAlias({"ids:correlationMessage", "correlationMessage"}) + URI correlationMessage; + + @JsonProperty("ids:recipientConnector") + @JsonAlias({"ids:recipientConnector", "recipientConnector"}) + List recipientConnector; + + @JsonProperty("ids:recipientAgent") + @JsonAlias({"ids:recipientAgent", "recipientAgent"}) + List recipientAgent; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSzzz") + @NotNull + @JsonProperty("ids:issued") + @JsonAlias({"ids:issued", "issued"}) + XMLGregorianCalendar issued; + + + @NotNull + @JsonProperty("ids:senderAgent") + @JsonAlias({"ids:senderAgent", "senderAgent"}) + private URI senderAgent; + + @JsonProperty("ids:contentVersion") + @JsonAlias({"ids:contentVersion", "contentVersion"}) + String contentVersion; + + // all classes have a generic property array + @JsonIgnore + protected Map properties; + + public Message() { + } + + public Message(URI id) { + this.id = id; + } + + public URI getId() { + return id; + } + + public void setId(URI id) { + this.id = id; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public URI getIssuerConnector() { + return issuerConnector; + } + + public void setIssuerConnector(URI issuerConnector) { + this.issuerConnector = issuerConnector; + } + + public String getModelVersion() { + return modelVersion; + } + + public void setModelVersion(String modelVersion) { + this.modelVersion = modelVersion; + } + + public URI getCorrelationMessage() { + return correlationMessage; + } + + public void setCorrelationMessage(URI correlationMessage) { + this.correlationMessage = correlationMessage; + } + + public List getRecipientConnector() { + return recipientConnector; + } + + public void setRecipientConnector(List recipientConnector) { + this.recipientConnector = recipientConnector; + } + + public List getRecipientAgent() { + return recipientAgent; + } + + public void setRecipientAgent(List recipientAgent) { + this.recipientAgent = recipientAgent; + } + + public XMLGregorianCalendar getIssued() { + return issued; + } + + public void setIssued(XMLGregorianCalendar issued) { + this.issued = issued; + } + + public DynamicAttributeToken getSecurityToken() { + return securityToken; + } + + public void setSecurityToken(DynamicAttributeToken securityToken) { + this.securityToken = securityToken; + } + + public URI getSenderAgent() { + return senderAgent; + } + + public void setSenderAgent(URI senderAgent) { + this.senderAgent = senderAgent; + } + + public String getContentVersion() { + return contentVersion; + } + + public void setContentVersion(String contentVersion) { + this.contentVersion = contentVersion; + } + + public Context getContext() { + return context; + } + + public void setContext(Context context) { + this.context = context; + } +} + diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java new file mode 100644 index 0000000..6f0d1d0 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.ids; + +import com.fasterxml.jackson.annotation.JsonAlias; + +import java.net.URI; + +public class RejectionMessage extends Message { + + @JsonAlias({"https://w3id.org/idsa/core/rejectionReason", "ids:rejectionReason", "rejectionReason"}) + RejectionReason rejectionReason; + + public RejectionMessage() { + } + + public RejectionMessage(URI id) { + super(id); + } + + public RejectionReason getRejectionReason() { + return rejectionReason; + } + + public void setRejectionReason(RejectionReason rejectionReason) { + this.rejectionReason = rejectionReason; + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java new file mode 100644 index 0000000..4960f7d --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.ids; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeName("ids:RejectionReason") +public class RejectionReason { + + @JsonProperty("@id") + @JsonAlias({"@id", "id"}) + @NotNull + private URI id; + + public RejectionReason() { + } + + public RejectionReason(URI id) { + this.id = id; + } + + public static final RejectionReason BAD_PARAMETERS = new RejectionReason(URI.create("https://w3id.org/idsa/code/BAD_PARAMETERS")); + + public static final RejectionReason INTERNAL_RECIPIENT_ERROR = + new RejectionReason(URI.create("https://w3id.org/idsa/code/INTERNAL_RECIPIENT_ERROR")); + + public static final RejectionReason MALFORMED_MESSAGE = + new RejectionReason(URI.create("https://w3id.org/idsa/code/MALFORMED_MESSAGE")); + + public static final RejectionReason MESSAGE_TYPE_NOT_SUPPORTED = + new RejectionReason(URI.create("https://w3id.org/idsa/code/MESSAGE_TYPE_NOT_SUPPORTED")); + + public static final RejectionReason NOT_AUTHENTICATED = + new RejectionReason(URI.create("https://w3id.org/idsa/code/NOT_AUTHENTICATED")); + + public URI getId() { + return id; + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java new file mode 100644 index 0000000..ac377aa --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.ids; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonTypeName("ids:tokenFormat") +public class TokenFormat { + + @JsonProperty("@type") + @NotNull + private String type; + + @JsonProperty("@id") + @JsonAlias({"@id", "id"}) + @NotNull + private URI id; + + public URI getId() { + return id; + } + + public void setId(URI id) { + this.id = id; + } + + public String getType() { + return "ids:tokenFormat"; + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/util/VocabUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/util/VocabUtil.java new file mode 100644 index 0000000..802df1d --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/util/VocabUtil.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.ids.util; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.UUID; + +public class VocabUtil { + public static String randomUrlBase; + + public static URI createRandomUrl(String path) { + try { + if (randomUrlBase != null) { + if (!randomUrlBase.endsWith("/")) { + randomUrlBase = randomUrlBase + "/"; + } + + return (new URL(randomUrlBase + path + "/" + UUID.randomUUID())).toURI(); + } else { + return (new URL("https", "w3id.org", "/idsa/autogen/" + path + "/" + UUID.randomUUID())).toURI(); + } + } catch (URISyntaxException | MalformedURLException var3) { + throw new RuntimeException(var3); + } + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/jwt/JwtPayload.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/jwt/JwtPayload.java new file mode 100644 index 0000000..0de5032 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/jwt/JwtPayload.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * truzzt GmbH - PostgreSQL implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.jwt; + +import com.fasterxml.jackson.annotation.JsonAlias; +import org.jetbrains.annotations.NotNull; + +public class JwtPayload { + + @NotNull + @JsonAlias({"https://w3id.org/idsa/core/sub", "ids:sub", "sub"}) + private String sub; + + public String getSub() { + return sub; + } + + public void setSub(String sub) { + this.sub = sub; + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java new file mode 100644 index 0000000..b1d7252 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java @@ -0,0 +1,66 @@ +package de.truzzt.clearinghouse.edc.multipart.util; + +import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import de.truzzt.clearinghouse.edc.multipart.types.ids.RejectionMessage; +import de.truzzt.clearinghouse.edc.multipart.types.ids.RejectionReason; +import org.eclipse.edc.protocol.ids.spi.domain.IdsConstants; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.protocol.ids.spi.types.IdsType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; +import java.net.URI; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.GregorianCalendar; +import java.util.UUID; + +public class ResponseUtil { + + @NotNull + public static RejectionMessage malformedMessage(@Nullable Message correlationMessage, + @NotNull IdsId connectorId) { + RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); + rejectionMessage.setRejectionReason(RejectionReason.MALFORMED_MESSAGE); + + return rejectionMessage; + } + + @NotNull + private static RejectionMessage createRejectionMessage(@Nullable Message correlationMessage, + @NotNull IdsId connectorId) { + var messageId = getMessageId(); + + var rejectionMessage = new RejectionMessage(messageId); + rejectionMessage.setContentVersion(IdsConstants.INFORMATION_MODEL_VERSION); + rejectionMessage.setModelVersion(IdsConstants.INFORMATION_MODEL_VERSION); + rejectionMessage.setIssued(gregorianNow()); + rejectionMessage.setIssuerConnector(connectorId.toUri()); + rejectionMessage.setSenderAgent(connectorId.toUri()); + + if (correlationMessage != null) { + rejectionMessage.setCorrelationMessage(correlationMessage.getId()); + rejectionMessage.setRecipientAgent(new ArrayList<>(Collections.singletonList(correlationMessage.getSenderAgent()))); + rejectionMessage.setRecipientConnector(new ArrayList<>(Collections.singletonList(correlationMessage.getIssuerConnector()))); + } + + return rejectionMessage; + } + + private static URI getMessageId() { + return IdsId.Builder.newInstance().value(UUID.randomUUID().toString()).type(IdsType.MESSAGE).build().toUri(); + } + + public static XMLGregorianCalendar gregorianNow() { + try { + GregorianCalendar gregorianCalendar = GregorianCalendar.from(ZonedDateTime.now()); + return DatatypeFactory.newInstance().newXMLGregorianCalendar(gregorianCalendar); + } catch (DatatypeConfigurationException e) { + throw new RuntimeException(e); + } + } +} From edb341e0cc3087d0a6bf986fc14e6eb28eb163de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Mon, 25 Sep 2023 18:14:58 +0200 Subject: [PATCH 068/183] Refactor(ch-app): Replaced rocket with axum --- clearing-house-app/Cargo.lock | 577 +++++------------- clearing-house-app/Cargo.toml | 8 +- clearing-house-app/src/config.rs | 2 + clearing-house-app/src/db/doc_store.rs | 2 +- clearing-house-app/src/db/key_store.rs | 2 +- clearing-house-app/src/db/process_store.rs | 2 +- clearing-house-app/src/errors.rs | 22 + clearing-house-app/src/main.rs | 118 +++- clearing-house-app/src/model/claims.rs | 54 +- clearing-house-app/src/model/mod.rs | 4 +- clearing-house-app/src/ports/doc_type_api.rs | 110 ++-- clearing-house-app/src/ports/logging_api.rs | 157 ++--- clearing-house-app/src/ports/mod.rs | 35 +- .../src/services/document_service.rs | 5 +- .../src/services/logging_service.rs | 15 +- clearing-house-app/src/util.rs | 32 - 16 files changed, 464 insertions(+), 681 deletions(-) create mode 100644 clearing-house-app/src/errors.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 7e29dde..d970703 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -104,28 +104,6 @@ dependencies = [ "nodrop", ] -[[package]] -name = "async-stream" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.37", -] - [[package]] name = "async-trait" version = "0.1.73" @@ -137,18 +115,62 @@ dependencies = [ "syn 2.0.37", ] -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -176,12 +198,6 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" -[[package]] -name = "binascii" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" - [[package]] name = "biscuit" version = "0.6.0" @@ -325,11 +341,14 @@ dependencies = [ "aes", "aes-gcm-siv", "anyhow", + "async-trait", + "axum", "base64 0.21.4", "biscuit", "blake2-rfc", "chrono", "config", + "futures", "generic-array", "hex", "hkdf", @@ -339,12 +358,12 @@ dependencies = [ "openssh-keys", "rand", "ring", - "rocket", "serde", "serde_json", "serial_test", "sha2", "tempfile", + "tokio", "tracing", "tracing-subscriber", "uuid", @@ -361,7 +380,7 @@ dependencies = [ "nom", "pathdiff", "serde", - "toml 0.5.11", + "toml", ] [[package]] @@ -376,17 +395,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "cookie" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -506,39 +514,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "devise" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" -dependencies = [ - "devise_codegen", - "devise_core", -] - -[[package]] -name = "devise_codegen" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" -dependencies = [ - "devise_core", - "quote", -] - -[[package]] -name = "devise_core" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" -dependencies = [ - "bitflags 2.4.0", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.37", -] - [[package]] name = "digest" version = "0.10.7" @@ -550,21 +525,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "either" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" - -[[package]] -name = "encoding_rs" -version = "0.8.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" -dependencies = [ - "cfg-if", -] - [[package]] name = "enum-as-inner" version = "0.4.0" @@ -610,20 +570,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" -[[package]] -name = "figment" -version = "0.10.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4547e226f4c9ab860571e070a9034192b3175580ecea38da34fcdb53a018c9a5" -dependencies = [ - "atomic", - "pear", - "serde", - "toml 0.7.8", - "uncased", - "version_check", -] - [[package]] name = "finl_unicode" version = "1.2.0" @@ -755,19 +701,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -795,12 +728,6 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" -[[package]] -name = "glob" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" - [[package]] name = "h2" version = "0.3.21" @@ -832,6 +759,31 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +[[package]] +name = "headers" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -995,7 +947,6 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", - "serde", ] [[package]] @@ -1008,12 +959,6 @@ dependencies = [ "hashbrown 0.14.0", ] -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - [[package]] name = "inout" version = "0.1.3" @@ -1041,17 +986,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] - [[package]] name = "itoa" version = "1.0.9" @@ -1107,21 +1041,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "serde", - "serde_json", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru-cache" version = "0.1.2" @@ -1152,6 +1071,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.5" @@ -1249,26 +1174,6 @@ dependencies = [ "webpki-roots", ] -[[package]] -name = "multer" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http", - "httparse", - "log", - "memchr", - "mime", - "spin 0.9.8", - "tokio", - "tokio-util", - "version_check", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -1458,34 +1363,31 @@ dependencies = [ ] [[package]] -name = "pear" -version = "0.2.7" +name = "percent-encoding" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a386cd715229d399604b50d1361683fe687066f42d56f54be995bc6868f71c" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi 1.0.0-rc.1", + "pin-project-internal", ] [[package]] -name = "pear_codegen" -version = "0.2.7" +name = "pin-project-internal" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f0f13dac8069c139e8300a6510e3f4143ecf5259c60b116a9b271b4ca0d54" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", - "proc-macro2-diagnostics", "quote", "syn 2.0.37", ] -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1531,19 +1433,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.37", - "version_check", - "yansi 1.0.0-rc.1", -] - [[package]] name = "quick-error" version = "1.2.3" @@ -1604,26 +1493,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "ref-cast" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acde58d073e9c79da00f2b5b84eed919c8326832648a5b109b3fce1bb1175280" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7473c2cfcf90008193dd0e3e16599455cb601a9fce322b5bb55de799664925" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.37", -] - [[package]] name = "regex" version = "1.9.5" @@ -1687,94 +1556,12 @@ dependencies = [ "cc", "libc", "once_cell", - "spin 0.5.2", + "spin", "untrusted", "web-sys", "winapi", ] -[[package]] -name = "rocket" -version = "0.5.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58734f7401ae5cfd129685b48f61182331745b357b96f2367f01aebaf1cc9cc9" -dependencies = [ - "async-stream", - "async-trait", - "atomic", - "binascii", - "bytes", - "either", - "figment", - "futures", - "indexmap 1.9.3", - "is-terminal", - "log", - "memchr", - "multer", - "num_cpus", - "parking_lot", - "pin-project-lite", - "rand", - "ref-cast", - "rocket_codegen", - "rocket_http", - "serde", - "serde_json", - "state", - "tempfile", - "time", - "tokio", - "tokio-stream", - "tokio-util", - "ubyte", - "version_check", - "yansi 0.5.1", -] - -[[package]] -name = "rocket_codegen" -version = "0.5.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7093353f14228c744982e409259fb54878ba9563d08214f2d880d59ff2fc508b" -dependencies = [ - "devise", - "glob", - "indexmap 1.9.3", - "proc-macro2", - "quote", - "rocket_http", - "syn 2.0.37", - "unicode-xid", -] - -[[package]] -name = "rocket_http" -version = "0.5.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936012c99162a03a67f37f9836d5f938f662e26f2717809761a9ac46432090f4" -dependencies = [ - "cookie", - "either", - "futures", - "http", - "hyper", - "indexmap 1.9.3", - "log", - "memchr", - "pear", - "percent-encoding", - "pin-project-lite", - "ref-cast", - "serde", - "smallvec", - "stable-pattern", - "state", - "time", - "tokio", - "uncased", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1855,12 +1642,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" @@ -1940,11 +1721,24 @@ dependencies = [ ] [[package]] -name = "serde_spanned" -version = "0.6.3" +name = "serde_path_to_error" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", "serde", ] @@ -2006,6 +1800,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.7" @@ -2076,30 +1881,6 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable-pattern" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" -dependencies = [ - "memchr", -] - -[[package]] -name = "state" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" -dependencies = [ - "loom", -] - [[package]] name = "stringprep" version = "0.1.4" @@ -2145,6 +1926,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "take_mut" version = "0.2.2" @@ -2295,17 +2082,6 @@ dependencies = [ "webpki", ] -[[package]] -name = "tokio-stream" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.9" @@ -2331,38 +2107,26 @@ dependencies = [ ] [[package]] -name = "toml" -version = "0.7.8" +name = "tower" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit", -] - -[[package]] -name = "toml_datetime" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" -dependencies = [ - "serde", + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "toml_edit" -version = "0.19.15" +name = "tower-layer" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.0.0", - "serde", - "serde_spanned", - "toml_datetime", - "winnow", -] +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] name = "tower-service" @@ -2377,6 +2141,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2500,25 +2265,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ubyte" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c81f0dae7d286ad0d9366d7679a77934cfc3cf3a8d67e82669794412b2368fe6" -dependencies = [ - "serde", -] - -[[package]] -name = "uncased" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" -dependencies = [ - "serde", - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2540,12 +2286,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "universal-hash" version = "0.5.1" @@ -2802,15 +2542,6 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" -[[package]] -name = "winnow" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" -dependencies = [ - "memchr", -] - [[package]] name = "winreg" version = "0.50.0" @@ -2830,18 +2561,6 @@ dependencies = [ "tap", ] -[[package]] -name = "yansi" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" - -[[package]] -name = "yansi" -version = "1.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" - [[package]] name = "zeroize" version = "1.6.0" diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index efa2edf..3652e44 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -13,8 +13,6 @@ edition = "2021" biscuit = "0.6.0" # Database mongodb = { version = ">= 2.6.1" , features = ["openssl-tls"]} -# HTTP-Server -rocket = { version = "0.5.0-rc.1", features = ["json"] } # Serialization serde = { version = ">1.0.184", features = ["derive"] } serde_json = "1" @@ -49,6 +47,12 @@ uuid = { version = "1.4.1", features = ["serde"] } num-bigint = "0.4.3" # Generating fingerprint of RSA keys openssh-keys = "0.6.2" +# Async runtime +tokio = { version = ">= 1.32.0", features = ["macros", "rt-multi-thread", "signal"] } +# HTTP server +axum = { version = "0.6.20", features = ["json", "http2", "headers"] } +async-trait = "0.1.73" +futures = "0.3.28" [dev-dependencies] # Controlling execution of unit test cases, which could interfere with each other diff --git a/clearing-house-app/src/config.rs b/clearing-house-app/src/config.rs index 5bca6a9..b232e37 100644 --- a/clearing-house-app/src/config.rs +++ b/clearing-house-app/src/config.rs @@ -7,6 +7,8 @@ pub(crate) struct CHConfig { pub(crate) clear_db: bool, #[serde(default)] pub(crate) log_level: Option, + #[serde(default)] + pub(crate) signing_key: Option, } /// Contains the log level for the application diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 3642dee..3a0e3d3 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -7,7 +7,7 @@ use crate::model::SortingOrder; use mongodb::bson::doc; use mongodb::options::{AggregateOptions, CreateCollectionOptions, IndexOptions, UpdateOptions, WriteConcern}; use mongodb::{bson, Client, IndexModel}; -use rocket::futures::StreamExt; +use futures::StreamExt; #[derive(Clone)] pub struct DataStore { diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index f84d8d7..da85f76 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -3,7 +3,7 @@ use crate::model::constants::{FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIE use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; use mongodb::bson::doc; -use rocket::futures::TryStreamExt; +use futures::TryStreamExt; use std::process::exit; use anyhow::anyhow; use crate::db::init_database_client; diff --git a/clearing-house-app/src/db/process_store.rs b/clearing-house-app/src/db/process_store.rs index ddd5cb6..5f46177 100644 --- a/clearing-house-app/src/db/process_store.rs +++ b/clearing-house-app/src/db/process_store.rs @@ -6,7 +6,7 @@ use crate::model::process::TransactionCounter; use mongodb::bson::doc; use mongodb::options::{CreateCollectionOptions, FindOneAndUpdateOptions, UpdateModifications, WriteConcern}; use mongodb::{Client, Database}; -use rocket::futures::TryStreamExt; +use futures::TryStreamExt; #[derive(Clone)] pub struct ProcessStore { diff --git a/clearing-house-app/src/errors.rs b/clearing-house-app/src/errors.rs new file mode 100644 index 0000000..19fc53b --- /dev/null +++ b/clearing-house-app/src/errors.rs @@ -0,0 +1,22 @@ +type Result = std::result::Result; + +#[derive(Debug)] +pub enum AppError { + Generic(anyhow::Error), +} + +impl std::error::Error for AppError {} + +impl std::fmt::Display for AppError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + AppError::Generic(e) => write!(f, "{}", e), + } + } +} + +impl From for AppError{ + fn from(err: anyhow::Error) -> Self { + Self::Generic(err.into()) + } +} \ No newline at end of file diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index 3d1b66f..76d5865 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -3,10 +3,13 @@ #[macro_use] extern crate tracing; -use crate::model::constants::ENV_LOGGING_SERVICE_ID; +use std::net::SocketAddr; +use std::sync::Arc; +use crate::model::constants::{ENV_LOGGING_SERVICE_ID, SIGNING_KEY}; use crate::db::doc_store::DataStore; use crate::db::key_store::KeyStore; use crate::db::process_store::ProcessStore; +use crate::util::ServiceConfig; mod config; mod crypto; @@ -15,9 +18,43 @@ mod model; mod ports; mod services; mod util; +mod errors; -#[rocket::main] -async fn main() -> Result<(), rocket::Error> { +#[derive(Clone)] +pub struct AppState { + pub keyring_service: Arc, + pub doc_service: Arc, + pub logging_service: Arc, + pub service_config: Arc, + pub signing_key_path: String, +} + +fn init_service_config(service_id: String) -> anyhow::Result { + match std::env::var(&service_id) { + Ok(id) => Ok(ServiceConfig { service_id: id }), + Err(_e) => { + anyhow::bail!( + "Service ID not configured. Please configure environment variable {}", + &service_id + ); + } + } +} + +fn init_signing_key(signing_key_path: Option<&str>) -> anyhow::Result { + let private_key_path = signing_key_path + .unwrap_or("keys/private_key.der"); + if std::path::Path::new(&private_key_path).exists() { + Ok(private_key_path.to_string()) + } else { + anyhow::bail!( + "Signing key not found! Aborting startup! Please configure signing_key!" + ); + } +} + +#[tokio::main] +async fn main() -> Result<(), anyhow::Error> { // Read configuration let conf = config::read_config(None); config::configure_logging(conf.log_level); @@ -35,24 +72,67 @@ async fn main() -> Result<(), rocket::Error> { .await .expect("Failure to initialize document store! Exiting..."); - let keyring_service = services::keyring_service::KeyringService::new(keyring_store); + let keyring_service = Arc::new(services::keyring_service::KeyringService::new(keyring_store)); let doc_service = - services::document_service::DocumentService::new(doc_store, keyring_service.clone()); + Arc::new(services::document_service::DocumentService::new(doc_store, keyring_service.clone())); let logging_service = - services::logging_service::LoggingService::new(process_store, doc_service.clone()); - - let _rocket = rocket::build() - .manage(keyring_service) - .manage(doc_service) - .manage(logging_service) - .attach(util::add_signing_key()) - .attach(util::add_service_config(ENV_LOGGING_SERVICE_ID.to_string())) - .attach(ports::logging_api::mount_api()) - .attach(ports::doc_type_api::mount_api()) - .ignite() - .await? - .launch() - .await?; + Arc::new(services::logging_service::LoggingService::new(process_store, doc_service.clone())); + + let service_config = Arc::new(init_service_config(ENV_LOGGING_SERVICE_ID.to_string())?); + let signing_key = init_signing_key(conf.signing_key.as_deref())?; + + let app_state = AppState { + signing_key_path: signing_key, + service_config, + keyring_service, + doc_service, + logging_service, + }; + + let app = axum::Router::new() + .route("/log/message/:pid", axum::routing::post(ports::logging_api::log)) + .route("/process/:pid", axum::routing::post(ports::logging_api::create_process)) + .route("/messages/query/:pid", axum::routing::post(ports::logging_api::query_pid)) + .route("/messages/query/:pid/:id", axum::routing::post(ports::logging_api::query_id)) + .route("/.well-known/jwks.json", axum::routing::get(ports::logging_api::get_public_sign_key)) + .nest("/doctype", ports::doc_type_api::router()) + .with_state(app_state); + + + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + tracing::debug!("listening on {}", addr); + axum::Server::bind(&addr) + .serve(app.into_make_service()) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); Ok(()) } + +/// Signal handler to catch a Ctrl+C and initiate a graceful shutdown +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + println!("signal received, starting graceful shutdown"); +} diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index 3688da7..a53ebd0 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -3,11 +3,13 @@ use crate::util::ServiceConfig; use chrono::{Duration, Utc}; use num_bigint::BigUint; use ring::signature::KeyPair; -use rocket::http::Status; -use rocket::request::{FromRequest, Outcome, Request}; use std::env; use std::fmt::{Display, Formatter}; +use std::sync::Arc; use anyhow::Context; +use axum::extract::FromRef; +use axum::response::IntoResponse; +use crate::AppState; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ChClaims { @@ -34,27 +36,36 @@ pub enum ChClaimsError { Invalid, } -#[rocket::async_trait] -impl<'r> FromRequest<'r> for ChClaims { - type Error = ChClaimsError; - - async fn from_request(request: &'r Request<'_>) -> Outcome { - match request.headers().get_one(SERVICE_HEADER) { - None => Outcome::Failure((Status::BadRequest, ChClaimsError::Missing)), - Some(token) => { - debug!("...received service header: {:?}", token); - let service_config = request.rocket().state::().unwrap(); - match decode_token::(token, service_config.service_id.as_str()) { - Ok(claims) => { - debug!("...retrieved claims and succeed"); - Outcome::Success(claims) - } - Err(e) => { - error!("...failed to retrieve and validate claims: {}", e); - Outcome::Failure((Status::BadRequest, ChClaimsError::Invalid)) - }, +pub struct ExtractChClaims(pub ChClaims); + +#[async_trait::async_trait] +impl axum::extract::FromRequestParts for ExtractChClaims + where + S: Send + Sync, + AppState: FromRef, +{ + type Rejection = axum::response::Response; + + async fn from_request_parts(parts: &mut axum::http::request::Parts, state: &S) -> Result { + let axum::extract::State(app_state) = axum::extract::State::::from_request_parts(parts, state) + .await + .map_err(|err| err.into_response())?; + if let Some(token) = parts.headers.get(SERVICE_HEADER) { + let token = token.to_str().unwrap(); + debug!("...received service header: {:?}", token); + + match decode_token::(token, app_state.service_config.service_id.as_str()) { + Ok(claims) => { + debug!("...retrieved claims and succeed"); + Ok(ExtractChClaims(claims)) + } + Err(e) => { + error!("...failed to retrieve and validate claims: {}", e); + Err((axum::http::StatusCode::BAD_REQUEST, "Invalid token").into_response()) } } + } else { + Err((axum::http::StatusCode::BAD_REQUEST, "Missing token").into_response()) } } } @@ -211,5 +222,4 @@ pub fn decode_token serde::Deserialize<'d decoded_jwt.validate(val_options) .with_context(|| "Failed validating JWT")?; Ok(decoded_jwt.payload()?.private.clone()) - } diff --git a/clearing-house-app/src/model/mod.rs b/clearing-house-app/src/model/mod.rs index 43e55b0..9b1d734 100644 --- a/clearing-house-app/src/model/mod.rs +++ b/clearing-house-app/src/model/mod.rs @@ -7,12 +7,10 @@ pub mod ids; pub(crate) mod process; pub(crate) mod util; -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, rocket::FromFormField)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum SortingOrder { - #[field(value = "asc")] #[serde(rename = "asc")] Ascending, - #[field(value = "desc")] #[serde(rename = "desc")] Descending, } diff --git a/clearing-house-app/src/ports/doc_type_api.rs b/clearing-house-app/src/ports/doc_type_api.rs index b02b440..a2dbc66 100644 --- a/clearing-house-app/src/ports/doc_type_api.rs +++ b/clearing-house-app/src/ports/doc_type_api.rs @@ -1,19 +1,16 @@ use crate::model::constants::{DEFAULT_PROCESS_ID, ROCKET_DOC_TYPE_API}; use crate::ports::ApiResponse; -use rocket::fairing::AdHoc; -use rocket::serde::json::{json, Json}; -use rocket::State; +use crate::AppState; use crate::model::doc_type::DocumentType; -use crate::services::keyring_service::KeyringService; -#[rocket::post("/", format = "json", data = "")] +//#[rocket::post("/", format = "json", data = "")] async fn create_doc_type( - key_api: &State, - doc_type: Json, -) -> ApiResponse { - match key_api.inner().create_doc_type(doc_type.into_inner()).await { - Ok(dt) => ApiResponse::SuccessCreate(json!(dt)), + axum::extract::State(state): axum::extract::State, + axum::extract::Json(doc_type): axum::extract::Json, +) -> ApiResponse { + match state.keyring_service.create_doc_type(doc_type).await { + Ok(dt) => ApiResponse::SuccessCreate(dt), Err(e) => { error!("Error while adding doctype: {:?}", e); ApiResponse::InternalError(e.to_string()) @@ -21,18 +18,14 @@ async fn create_doc_type( } } -#[rocket::post("/", format = "json", data = "")] +//#[rocket::post("/", format = "json", data = "")] async fn update_doc_type( - key_api: &State, - id: String, - doc_type: Json, -) -> ApiResponse { - match key_api - .inner() - .update_doc_type(id, doc_type.into_inner()) - .await - { - Ok(id) => ApiResponse::SuccessOk(json!(id)), + axum::extract::State(state): axum::extract::State, + axum::extract::Path(id): axum::extract::Path, + axum::extract::Json(doc_type): axum::extract::Json, +) -> ApiResponse { + match state.keyring_service.update_doc_type(id, doc_type).await { + Ok(id) => ApiResponse::SuccessOk(id), Err(e) => { error!("Error while adding doctype: {:?}", e); ApiResponse::InternalError(e.to_string()) @@ -40,15 +33,22 @@ async fn update_doc_type( } } -#[rocket::delete("/", format = "json")] -async fn delete_default_doc_type(key_api: &State, id: String) -> ApiResponse { - delete_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await +//#[rocket::delete("/", format = "json")] +async fn delete_default_doc_type( + state: axum::extract::State, + id: axum::extract::Path, +) -> ApiResponse { + delete_doc_type(state, id, axum::extract::Path(DEFAULT_PROCESS_ID.to_string())).await } -#[rocket::delete("//", format = "json")] -async fn delete_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse { - match key_api.inner().delete_doc_type(id, pid).await { - Ok(id) => ApiResponse::SuccessOk(json!(id)), +//#[rocket::delete("//", format = "json")] +async fn delete_doc_type( + axum::extract::State(state): axum::extract::State, + axum::extract::Path(id): axum::extract::Path, + axum::extract::Path(pid): axum::extract::Path, +) -> ApiResponse { + match state.keyring_service.delete_doc_type(id, pid).await { + Ok(id) => ApiResponse::SuccessOk(id), Err(e) => { error!("Error while deleting doctype: {:?}", e); ApiResponse::InternalError(e.to_string()) @@ -56,17 +56,20 @@ async fn delete_doc_type(key_api: &State, id: String, pid: Strin } } -#[rocket::get("/", format = "json")] -async fn get_default_doc_type(key_api: &State, id: String) -> ApiResponse { - get_doc_type(key_api, id, DEFAULT_PROCESS_ID.to_string()).await +//#[rocket::get("/", format = "json")] +async fn get_default_doc_type(state: axum::extract::State, + id: axum::extract::Path) -> ApiResponse>{ + get_doc_type(state, id, axum::extract::Path(DEFAULT_PROCESS_ID.to_string())).await } -#[rocket::get("//", format = "json")] -async fn get_doc_type(key_api: &State, id: String, pid: String) -> ApiResponse { - match key_api.inner().get_doc_type(id, pid).await { +//#[rocket::get("//", format = "json")] +async fn get_doc_type(axum::extract::State(state): axum::extract::State, + axum::extract::Path(id): axum::extract::Path, + axum::extract::Path(pid): axum::extract::Path) -> ApiResponse> { + match state.keyring_service.get_doc_type(id, pid).await { Ok(dt) => match dt { - Some(dt) => ApiResponse::SuccessOk(json!(dt)), - None => ApiResponse::SuccessOk(json!(null)), + Some(dt) => ApiResponse::SuccessOk(Some(dt)), + None => ApiResponse::SuccessOk(None), }, Err(e) => { error!("Error while retrieving doctype: {:?}", e); @@ -75,10 +78,10 @@ async fn get_doc_type(key_api: &State, id: String, pid: String) } } -#[rocket::get("/", format = "json")] -async fn get_doc_types(key_api: &State) -> ApiResponse { - match key_api.inner().get_doc_types().await { - Ok(dt) => ApiResponse::SuccessOk(json!(dt)), +//#[rocket::get("/", format = "json")] +async fn get_doc_types(axum::extract::State(state): axum::extract::State) -> ApiResponse> { + match state.keyring_service.get_doc_types().await { + Ok(dt) => ApiResponse::SuccessOk(dt), Err(e) => { error!("Error while retrieving doctypes: {:?}", e); ApiResponse::InternalError(e.to_string()) @@ -86,19 +89,16 @@ async fn get_doc_types(key_api: &State) -> ApiResponse { } } -pub fn mount_api() -> AdHoc { - AdHoc::on_ignite("Mounting Document Type API", |rocket| async { - rocket.mount( - ROCKET_DOC_TYPE_API, - rocket::routes![ - create_doc_type, - update_doc_type, - delete_default_doc_type, - delete_doc_type, - get_default_doc_type, - get_doc_type, - get_doc_types - ], - ) - }) +pub fn router() -> axum::Router { + axum::Router::new() + .route("/", + axum::routing::get(get_doc_types) + .post(create_doc_type)) + .route("/:id", + axum::routing::get(get_default_doc_type) + .post(update_doc_type) + .delete(delete_default_doc_type)) + .route("/:pid/:id", + axum::routing::get(get_doc_type) + .delete(delete_doc_type)) } diff --git a/clearing-house-app/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs index 6a1453e..7842b4e 100644 --- a/clearing-house-app/src/ports/logging_api.rs +++ b/clearing-house-app/src/ports/logging_api.rs @@ -1,33 +1,29 @@ -use crate::{ - model::claims::{get_jwks, ChClaims}, - model::SortingOrder, - ports::ApiResponse, -}; -use rocket::fairing::AdHoc; -use rocket::serde::json::{json, Json}; -use rocket::State; + +use biscuit::jwk::JWKSet; +use crate::{AppState, model::claims::get_jwks, model::SortingOrder, ports::ApiResponse}; +use crate::model::claims::ExtractChClaims; use crate::model::constants::{ ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_PK_API, ROCKET_PROCESS_API, ROCKET_QUERY_API, }; +use crate::model::ids::IdsQueryResult; +use crate::model::ids::message::IdsMessage; use crate::model::ids::request::ClearingHouseMessage; -use crate::services::logging_service::LoggingService; +use crate::model::process::Receipt; -#[rocket::post("/", format = "json", data = "")] -async fn log( - ch_claims: ChClaims, - logging_api: &State, - key_path: &State, - message: Json, - pid: String, -) -> ApiResponse { - match logging_api - .inner() - .log(ch_claims, key_path, message.into_inner(), pid) +//#[rocket::post("/", format = "json", data = "")] +pub async fn log( + ExtractChClaims(ch_claims): ExtractChClaims, + axum::extract::State(state): axum::extract::State, + axum::extract::Path(pid): axum::extract::Path, + axum::extract::Json(message): axum::extract::Json, +) -> ApiResponse { + match state.logging_service + .log(ch_claims, state.signing_key_path.as_str(), message, pid) .await { - Ok(id) => ApiResponse::SuccessCreate(json!(id)), + Ok(id) => ApiResponse::SuccessCreate(id), Err(e) => { error!("Error while logging: {:?}", e); ApiResponse::InternalError(String::from("Error while logging!")) @@ -35,19 +31,18 @@ async fn log( } } -#[rocket::post("/", format = "json", data = "")] -async fn create_process( - ch_claims: ChClaims, - logging_api: &State, - message: Json, - pid: String, -) -> ApiResponse { - match logging_api - .inner() - .create_process(ch_claims, message.into_inner(), pid) +//#[rocket::post("/", format = "json", data = "")] +pub async fn create_process( + ExtractChClaims(ch_claims): ExtractChClaims, + axum::extract::State(state): axum::extract::State, + axum::extract::Path(pid): axum::extract::Path, + axum::extract::Json(message): axum::extract::Json, +) -> ApiResponse { + match state.logging_service + .create_process(ch_claims, message, pid) .await { - Ok(id) => ApiResponse::SuccessCreate(json!(id)), + Ok(id) => ApiResponse::SuccessCreate(id), Err(e) => { error!("Error while creating process: {:?}", e); ApiResponse::InternalError(String::from("Error while creating process!")) @@ -55,34 +50,33 @@ async fn create_process( } } -#[rocket::post("/<_pid>", format = "json", rank = 50)] -async fn unauth(_pid: Option) -> ApiResponse { +//#[rocket::post("/<_pid>", format = "json", rank = 50)] +/*async fn unauth(_pid: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) -} +}*/ -#[rocket::post("/<_pid>/<_id>", format = "json", rank = 50)] -async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { +//#[rocket::post("/<_pid>/<_id>", format = "json", rank = 50)] +/*async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { ApiResponse::Unauthorized(String::from("Token not valid!")) -} +}*/ -#[rocket::post( +/*#[rocket::post( "/?&&&&", format = "json", data = "" -)] -async fn query_pid( - ch_claims: ChClaims, - logging_api: &State, - page: Option, - size: Option, - sort: Option, - date_to: Option, - date_from: Option, - pid: String, - message: Json, -) -> ApiResponse { - match logging_api - .inner() +)]*/ +pub async fn query_pid( + ExtractChClaims(ch_claims): ExtractChClaims, + axum::extract::State(state): axum::extract::State, + axum::extract::Query(page): axum::extract::Query>, + axum::extract::Query(size): axum::extract::Query>, + axum::extract::Query(sort): axum::extract::Query>, + axum::extract::Query(date_to): axum::extract::Query>, + axum::extract::Query(date_from): axum::extract::Query>, + axum::extract::Path(pid): axum::extract::Path, + axum::extract::Json(message): axum::extract::Json, +) -> ApiResponse { + match state.logging_service .query_pid( ch_claims, page, @@ -91,11 +85,11 @@ async fn query_pid( date_to, date_from, pid, - message.into_inner(), + message, ) .await { - Ok(result) => ApiResponse::SuccessOk(json!(result)), + Ok(result) => ApiResponse::SuccessOk(result), Err(e) => { error!("Error while querying: {:?}", e); ApiResponse::InternalError(String::from("Error while querying!")) @@ -103,20 +97,19 @@ async fn query_pid( } } -#[rocket::post("//", format = "json", data = "")] -async fn query_id( - ch_claims: ChClaims, - logging_api: &State, - pid: String, - id: String, - message: Json, -) -> ApiResponse { - match logging_api - .inner() - .query_id(ch_claims, pid, id, message.into_inner()) +//#[rocket::post("//", format = "json", data = "")] +pub async fn query_id( + ExtractChClaims(ch_claims): ExtractChClaims, + axum::extract::State(state): axum::extract::State, + axum::extract::Path(pid): axum::extract::Path, + axum::extract::Path(id): axum::extract::Path, + axum::extract::Json(message): axum::extract::Json, +) -> ApiResponse { + match state.logging_service + .query_id(ch_claims, pid, id, message) .await { - Ok(result) => ApiResponse::SuccessOk(json!(result)), + Ok(result) => ApiResponse::SuccessOk(result), Err(e) => { error!("Error while querying: {:?}", e); ApiResponse::InternalError(String::from("Error while querying!")) @@ -124,32 +117,10 @@ async fn query_id( } } -#[rocket::get("/.well-known/jwks.json", format = "json")] -async fn get_public_sign_key(key_path: &State) -> ApiResponse { - match get_jwks(key_path.as_str()) { - Some(jwks) => ApiResponse::SuccessOk(json!(jwks)), +//#[rocket::get("/.well-known/jwks.json", format = "json")] +pub async fn get_public_sign_key(axum::extract::State(state): axum::extract::State) -> ApiResponse> { + match get_jwks(state.signing_key_path.as_str()) { + Some(jwks) => ApiResponse::SuccessOk(jwks), None => ApiResponse::InternalError(String::from("Error reading signing key")), } } - -pub fn mount_api() -> AdHoc { - AdHoc::on_ignite("Mounting Clearing House API", |rocket| async { - rocket - .mount( - format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API).as_str(), - rocket::routes![log, unauth], - ) - .mount( - ROCKET_PROCESS_API.to_string().as_str(), - rocket::routes![create_process, unauth], - ) - .mount( - format!("{}{}", ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_QUERY_API).as_str(), - rocket::routes![query_id, query_pid, unauth, unauth_id], - ) - .mount( - ROCKET_PK_API.to_string().as_str(), - rocket::routes![get_public_sign_key], - ) - }) -} diff --git a/clearing-house-app/src/ports/mod.rs b/clearing-house-app/src/ports/mod.rs index 04881a1..7705a16 100644 --- a/clearing-house-app/src/ports/mod.rs +++ b/clearing-house-app/src/ports/mod.rs @@ -3,27 +3,36 @@ //! This module contains the ports of the logging service. Ports are used to communicate with other //! services. In this case, the logging service implements REST-API endpoints to provide access to //! the logging service. +use axum::response::Response; + pub(crate) mod doc_type_api; pub(crate) mod logging_api; -#[derive(rocket::Responder, Debug)] -pub enum ApiResponse { - #[response(status = 200)] +#[derive(Debug)] +pub enum ApiResponse { PreFlight(()), - #[response(status = 400, content_type = "text/plain")] BadRequest(String), - #[response(status = 201, content_type = "json")] - SuccessCreate(rocket::serde::json::Value), - #[response(status = 200, content_type = "json")] - SuccessOk(rocket::serde::json::Value), - #[response(status = 204, content_type = "text/plain")] + SuccessCreate(T), + SuccessOk(T), SuccessNoContent(String), - #[response(status = 401, content_type = "text/plain")] Unauthorized(String), - #[response(status = 403, content_type = "text/plain")] Forbidden(String), - #[response(status = 404, content_type = "text/plain")] NotFound(String), - #[response(status = 500, content_type = "text/plain")] InternalError(String), } + +impl axum::response::IntoResponse for ApiResponse { + fn into_response(self) -> Response { + match self { + ApiResponse::PreFlight(_) => (axum::http::StatusCode::OK, "").into_response(), + ApiResponse::BadRequest(s) => (axum::http::StatusCode::BAD_REQUEST, s).into_response(), + ApiResponse::SuccessCreate(v) => (axum::http::StatusCode::CREATED, axum::response::Json(v)).into_response(), + ApiResponse::SuccessOk(v) => (axum::http::StatusCode::OK, axum::response::Json(v)).into_response(), + ApiResponse::SuccessNoContent(s) => (axum::http::StatusCode::NO_CONTENT, s).into_response(), + ApiResponse::Unauthorized(s) => (axum::http::StatusCode::UNAUTHORIZED, s).into_response(), + ApiResponse::Forbidden(s) => (axum::http::StatusCode::FORBIDDEN, s).into_response(), + ApiResponse::NotFound(s) => (axum::http::StatusCode::NOT_FOUND, s).into_response(), + ApiResponse::InternalError(s) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, s).into_response(), + } + } +} \ No newline at end of file diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 1121e8d..9b4fb2c 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -12,15 +12,16 @@ use crate::services::keyring_service::KeyringService; use crate::services::{DocumentReceipt, QueryResult}; use anyhow::anyhow; use std::convert::TryFrom; +use std::sync::Arc; #[derive(Clone)] pub struct DocumentService { db: DataStore, - key_api: KeyringService, + key_api: Arc, } impl DocumentService { - pub fn new(db: DataStore, key_api: KeyringService) -> Self { + pub fn new(db: DataStore, key_api: Arc) -> Self { Self { db, key_api } } diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index c547795..7dfb299 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -4,9 +4,8 @@ use crate::model::{ {document::Document, process::Process, SortingOrder}, }; use anyhow::anyhow; -use rocket::form::validate::Contains; -use rocket::State; use std::convert::TryFrom; +use std::sync::Arc; use crate::db::process_store::ProcessStore; use crate::model::{ @@ -18,19 +17,19 @@ use crate::services::document_service::DocumentService; #[derive(Clone)] pub struct LoggingService { db: ProcessStore, - doc_api: DocumentService, - write_lock: std::sync::Arc>, + doc_api: Arc, + write_lock: Arc>, } impl LoggingService { - pub fn new(db: ProcessStore, doc_api: DocumentService) -> LoggingService { - LoggingService { db, doc_api, write_lock: std::sync::Arc::new(rocket::tokio::sync::Mutex::new(())) } + pub fn new(db: ProcessStore, doc_api: Arc) -> LoggingService { + LoggingService { db, doc_api, write_lock: Arc::new(tokio::sync::Mutex::new(())) } } pub async fn log( &self, ch_claims: ChClaims, - key_path: &State, + key_path: &str, msg: ClearingHouseMessage, pid: String, ) -> anyhow::Result { @@ -96,7 +95,7 @@ impl LoggingService { } debug!("logging message for pid {}", &pid); - self.log_message(user, key_path.inner().as_str(), m.clone()) + self.log_message(user, key_path, m.clone()) .await } } diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index f5a2ca9..2a7f7d2 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -7,38 +7,6 @@ pub struct ServiceConfig { pub service_id: String, } -pub fn add_service_config(service_id: String) -> rocket::fairing::AdHoc { - rocket::fairing::AdHoc::try_on_ignite("Adding Service Config", move |rocket| async move { - match std::env::var(&service_id) { - Ok(id) => Ok(rocket.manage(ServiceConfig { service_id: id })), - Err(_e) => { - error!( - "Service ID not configured. Please configure environment variable {}", - &service_id - ); - Err(rocket) - } - } - }) -} - -pub fn add_signing_key() -> rocket::fairing::AdHoc { - rocket::fairing::AdHoc::try_on_ignite("Adding Signing Key", |rocket| async { - let private_key_path = rocket - .figment() - .extract_inner(SIGNING_KEY) - .unwrap_or(String::from("keys/private_key.der")); - if Path::new(&private_key_path).exists() { - Ok(rocket.manage(private_key_path)) - } else { - tracing::error!( - "Signing key not found! Aborting startup! Please configure signing_key!" - ); - Err(rocket) - } - }) -} - /// Reads a file into a string pub fn read_file(file: &str) -> anyhow::Result { std::fs::read_to_string(file) From bc4f64b22b99feaa880f4c6b5fc4d1309fe4e738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 26 Sep 2023 12:37:47 +0200 Subject: [PATCH 069/183] Refactor(ch-app): Cleanup from replacement of the http server --- clearing-house-app/Cargo.lock | 37 ----- clearing-house-app/Cargo.toml | 4 +- clearing-house-app/src/crypto.rs | 9 +- clearing-house-app/src/db/doc_store.rs | 18 ++- clearing-house-app/src/db/key_store.rs | 19 +-- clearing-house-app/src/db/mod.rs | 9 +- clearing-house-app/src/db/process_store.rs | 17 ++- clearing-house-app/src/errors.rs | 4 +- clearing-house-app/src/main.rs | 127 ++++++------------ clearing-house-app/src/model/claims.rs | 41 +++--- clearing-house-app/src/model/constants.rs | 2 + clearing-house-app/src/model/crypto.rs | 2 +- clearing-house-app/src/model/document.rs | 11 +- clearing-house-app/src/model/ids/message.rs | 53 ++++---- clearing-house-app/src/model/ids/mod.rs | 1 - clearing-house-app/src/ports/doc_type_api.rs | 62 +++++---- clearing-house-app/src/ports/logging_api.rs | 89 ++++++------ clearing-house-app/src/ports/mod.rs | 24 +++- .../src/services/document_service.rs | 8 +- .../src/services/keyring_service.rs | 13 +- .../src/services/logging_service.rs | 12 +- clearing-house-app/src/util.rs | 54 +++++++- 22 files changed, 309 insertions(+), 307 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index d970703..40acce9 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -132,7 +132,6 @@ dependencies = [ "bitflags 1.3.2", "bytes", "futures-util", - "headers", "http", "http-body", "hyper", @@ -759,31 +758,6 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" -[[package]] -name = "headers" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" -dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.4.1" @@ -1800,17 +1774,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha1" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha2" version = "0.10.7" diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 3652e44..5b468f0 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -50,8 +50,10 @@ openssh-keys = "0.6.2" # Async runtime tokio = { version = ">= 1.32.0", features = ["macros", "rt-multi-thread", "signal"] } # HTTP server -axum = { version = "0.6.20", features = ["json", "http2", "headers"] } +axum = { version = "0.6.20", features = ["json", "http2"] } +# Helper to allow defining traits for async functions async-trait = "0.1.73" +# Helper for working with futures futures = "0.3.28" [dev-dependencies] diff --git a/clearing-house-app/src/crypto.rs b/clearing-house-app/src/crypto.rs index fed00c9..9d6f1e1 100644 --- a/clearing-house-app/src/crypto.rs +++ b/clearing-house-app/src/crypto.rs @@ -124,11 +124,10 @@ pub fn restore_keys(secret: &String, dt: DocumentType) -> anyhow::Result fn restore_kdf(secret: &String) -> anyhow::Result> { debug!("restoring kdf from secret"); - let prk = hex::decode(secret) - .map_err(|e| { - error!("Error while decoding master key: {}", e); - anyhow!("Error while encrypting key seed!") - })?; + let prk = hex::decode(secret).map_err(|e| { + error!("Error while decoding master key: {}", e); + anyhow!("Error while encrypting key seed!") + })?; match Hkdf::::from_prk(prk.as_slice()) { Ok(kdf) => Ok(kdf), diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 3a0e3d3..7d75540 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -1,13 +1,19 @@ -use anyhow::anyhow; use crate::db::doc_store::bucket::{restore_from_bucket, DocumentBucketSize, DocumentBucketUpdate}; -use crate::db::{DataStoreApi, init_database_client}; -use crate::model::constants::{DOCUMENT_DB, DOCUMENT_DB_CLIENT, MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_ID, MONGO_PID, MONGO_TC, MONGO_TO_TS, MONGO_TS}; +use crate::db::{init_database_client, DataStoreApi}; +use crate::model::constants::{ + DOCUMENT_DB, DOCUMENT_DB_CLIENT, MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, + MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_ID, MONGO_PID, MONGO_TC, + MONGO_TO_TS, MONGO_TS, +}; use crate::model::document::{Document, EncryptedDocument}; use crate::model::SortingOrder; +use anyhow::anyhow; +use futures::StreamExt; use mongodb::bson::doc; -use mongodb::options::{AggregateOptions, CreateCollectionOptions, IndexOptions, UpdateOptions, WriteConcern}; +use mongodb::options::{ + AggregateOptions, CreateCollectionOptions, IndexOptions, UpdateOptions, WriteConcern, +}; use mongodb::{bson, Client, IndexModel}; -use futures::StreamExt; #[derive(Clone)] pub struct DataStore { @@ -31,7 +37,7 @@ impl DataStore { db_url.as_str(), Some(DOCUMENT_DB_CLIENT.to_string()), ) - .await + .await { Ok(datastore) => { debug!("Check if database is empty..."); diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index da85f76..1743736 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -1,12 +1,15 @@ use super::DataStoreApi; -use crate::model::constants::{FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, MONGO_COLL_DOC_TYPES, MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID}; +use crate::db::init_database_client; +use crate::model::constants::{ + FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, MONGO_COLL_DOC_TYPES, + MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID, +}; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; -use mongodb::bson::doc; +use anyhow::anyhow; use futures::TryStreamExt; +use mongodb::bson::doc; use std::process::exit; -use anyhow::anyhow; -use crate::db::init_database_client; #[derive(Clone, Debug)] pub struct KeyStore { @@ -27,10 +30,7 @@ impl KeyStore { pub async fn init_keystore(db_url: String, clear_db: bool) -> anyhow::Result { debug!("Using database url: '{:#?}'", &db_url); - match init_database_client::( - db_url.as_str(), - Some(KEYRING_DB_CLIENT.to_string()), - ) + match init_database_client::(db_url.as_str(), Some(KEYRING_DB_CLIENT.to_string())) .await { Ok(keystore) => { @@ -59,7 +59,8 @@ impl KeyStore { debug!("Database empty. Need to initialize..."); debug!("Adding initial document type..."); match serde_json::from_str::( - &crate::util::read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or(String::new()), + &crate::util::read_file(FILE_DEFAULT_DOC_TYPE) + .unwrap_or(String::new()), ) { Ok(dt) => match keystore.add_document_type(dt).await { Ok(_) => { diff --git a/clearing-house-app/src/db/mod.rs b/clearing-house-app/src/db/mod.rs index ff1c505..055c68f 100644 --- a/clearing-house-app/src/db/mod.rs +++ b/clearing-house-app/src/db/mod.rs @@ -9,11 +9,12 @@ pub trait DataStoreApi { fn new(client: Client) -> Self; } -pub async fn init_database_client(db_url: &str, client_name: Option) -> anyhow::Result { +pub async fn init_database_client( + db_url: &str, + client_name: Option, +) -> anyhow::Result { let mut client_options = match ClientOptions::parse(&db_url.to_string()).await { - Ok(co) => { - co - } + Ok(co) => co, Err(_) => { anyhow::bail!("Can't parse database connection string"); } diff --git a/clearing-house-app/src/db/process_store.rs b/clearing-house-app/src/db/process_store.rs index 5f46177..fc48f48 100644 --- a/clearing-house-app/src/db/process_store.rs +++ b/clearing-house-app/src/db/process_store.rs @@ -1,12 +1,17 @@ -use anyhow::anyhow; -use crate::db::{DataStoreApi, init_database_client}; -use crate::model::constants::{MONGO_COLL_PROCESSES, MONGO_COLL_TRANSACTIONS, MONGO_ID, MONGO_TC, PROCESS_DB, PROCESS_DB_CLIENT}; +use crate::db::{init_database_client, DataStoreApi}; +use crate::model::constants::{ + MONGO_COLL_PROCESSES, MONGO_COLL_TRANSACTIONS, MONGO_ID, MONGO_TC, PROCESS_DB, + PROCESS_DB_CLIENT, +}; use crate::model::process::Process; use crate::model::process::TransactionCounter; +use anyhow::anyhow; +use futures::TryStreamExt; use mongodb::bson::doc; -use mongodb::options::{CreateCollectionOptions, FindOneAndUpdateOptions, UpdateModifications, WriteConcern}; +use mongodb::options::{ + CreateCollectionOptions, FindOneAndUpdateOptions, UpdateModifications, WriteConcern, +}; use mongodb::{Client, Database}; -use futures::TryStreamExt; #[derive(Clone)] pub struct ProcessStore { @@ -31,7 +36,7 @@ impl ProcessStore { db_url.as_str(), Some(PROCESS_DB_CLIENT.to_string()), ) - .await + .await { Ok(process_store) => { debug!("...check if database is empty..."); diff --git a/clearing-house-app/src/errors.rs b/clearing-house-app/src/errors.rs index 19fc53b..1db9216 100644 --- a/clearing-house-app/src/errors.rs +++ b/clearing-house-app/src/errors.rs @@ -15,8 +15,8 @@ impl std::fmt::Display for AppError { } } -impl From for AppError{ +impl From for AppError { fn from(err: anyhow::Error) -> Self { Self::Generic(err.into()) } -} \ No newline at end of file +} diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index 76d5865..b0dbc2d 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -3,136 +3,85 @@ #[macro_use] extern crate tracing; -use std::net::SocketAddr; -use std::sync::Arc; -use crate::model::constants::{ENV_LOGGING_SERVICE_ID, SIGNING_KEY}; use crate::db::doc_store::DataStore; use crate::db::key_store::KeyStore; use crate::db::process_store::ProcessStore; +use crate::model::constants::ENV_LOGGING_SERVICE_ID; use crate::util::ServiceConfig; +use std::net::SocketAddr; +use std::sync::Arc; mod config; mod crypto; mod db; +mod errors; mod model; mod ports; mod services; mod util; -mod errors; #[derive(Clone)] -pub struct AppState { +pub(crate) struct AppState { pub keyring_service: Arc, - pub doc_service: Arc, pub logging_service: Arc, - pub service_config: Arc, + pub service_config: Arc, pub signing_key_path: String, } -fn init_service_config(service_id: String) -> anyhow::Result { - match std::env::var(&service_id) { - Ok(id) => Ok(ServiceConfig { service_id: id }), - Err(_e) => { - anyhow::bail!( - "Service ID not configured. Please configure environment variable {}", - &service_id - ); - } - } -} - -fn init_signing_key(signing_key_path: Option<&str>) -> anyhow::Result { - let private_key_path = signing_key_path - .unwrap_or("keys/private_key.der"); - if std::path::Path::new(&private_key_path).exists() { - Ok(private_key_path.to_string()) - } else { - anyhow::bail!( - "Signing key not found! Aborting startup! Please configure signing_key!" - ); - } -} - #[tokio::main] async fn main() -> Result<(), anyhow::Error> { // Read configuration let conf = config::read_config(None); config::configure_logging(conf.log_level); - let process_store = - ProcessStore::init_process_store(conf.process_database_url, conf.clear_db) - .await - .expect("Failure to initialize process store! Exiting..."); - let keyring_store = - KeyStore::init_keystore(conf.keyring_database_url, conf.clear_db) - .await - .expect("Failure to initialize keyring store! Exiting..."); - let doc_store = - DataStore::init_datastore(conf.document_database_url, conf.clear_db) - .await - .expect("Failure to initialize document store! Exiting..."); - - let keyring_service = Arc::new(services::keyring_service::KeyringService::new(keyring_store)); - let doc_service = - Arc::new(services::document_service::DocumentService::new(doc_store, keyring_service.clone())); - let logging_service = - Arc::new(services::logging_service::LoggingService::new(process_store, doc_service.clone())); - - let service_config = Arc::new(init_service_config(ENV_LOGGING_SERVICE_ID.to_string())?); - let signing_key = init_signing_key(conf.signing_key.as_deref())?; + let process_store = ProcessStore::init_process_store(conf.process_database_url, conf.clear_db) + .await + .expect("Failure to initialize process store! Exiting..."); + let keyring_store = KeyStore::init_keystore(conf.keyring_database_url, conf.clear_db) + .await + .expect("Failure to initialize keyring store! Exiting..."); + let doc_store = DataStore::init_datastore(conf.document_database_url, conf.clear_db) + .await + .expect("Failure to initialize document store! Exiting..."); + + let keyring_service = Arc::new(services::keyring_service::KeyringService::new( + keyring_store, + )); + let doc_service = Arc::new(services::document_service::DocumentService::new( + doc_store, + keyring_service.clone(), + )); + let logging_service = Arc::new(services::logging_service::LoggingService::new( + process_store, + doc_service.clone(), + )); + + let service_config = Arc::new(util::init_service_config( + ENV_LOGGING_SERVICE_ID.to_string(), + )?); + let signing_key = util::init_signing_key(conf.signing_key.as_deref())?; let app_state = AppState { signing_key_path: signing_key, service_config, keyring_service, - doc_service, logging_service, }; + // Setup router let app = axum::Router::new() - .route("/log/message/:pid", axum::routing::post(ports::logging_api::log)) - .route("/process/:pid", axum::routing::post(ports::logging_api::create_process)) - .route("/messages/query/:pid", axum::routing::post(ports::logging_api::query_pid)) - .route("/messages/query/:pid/:id", axum::routing::post(ports::logging_api::query_id)) - .route("/.well-known/jwks.json", axum::routing::get(ports::logging_api::get_public_sign_key)) + .merge(ports::logging_api::router()) .nest("/doctype", ports::doc_type_api::router()) .with_state(app_state); - - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + // Bind port and start server + let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); tracing::debug!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) - .with_graceful_shutdown(shutdown_signal()) + .with_graceful_shutdown(util::shutdown_signal()) .await - .unwrap(); + .expect("Starting axum server failed!"); Ok(()) } - -/// Signal handler to catch a Ctrl+C and initiate a graceful shutdown -async fn shutdown_signal() { - let ctrl_c = async { - tokio::signal::ctrl_c() - .await - .expect("failed to install Ctrl+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) - .expect("failed to install signal handler") - .recv() - .await; - }; - - #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); - - tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, - } - - println!("signal received, starting graceful shutdown"); -} diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index a53ebd0..aa2adaf 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -1,15 +1,12 @@ use crate::model::constants::{ENV_SHARED_SECRET, SERVICE_HEADER}; -use crate::util::ServiceConfig; +use crate::AppState; +use anyhow::Context; +use axum::extract::FromRef; +use axum::response::IntoResponse; use chrono::{Duration, Utc}; use num_bigint::BigUint; use ring::signature::KeyPair; use std::env; -use std::fmt::{Display, Formatter}; -use std::sync::Arc; -use anyhow::Context; -use axum::extract::FromRef; -use axum::response::IntoResponse; -use crate::AppState; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ChClaims { @@ -24,8 +21,8 @@ impl ChClaims { } } -impl Display for ChClaims { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { +impl std::fmt::Display for ChClaims { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "<{}>", self.client_id) } } @@ -40,16 +37,20 @@ pub struct ExtractChClaims(pub ChClaims); #[async_trait::async_trait] impl axum::extract::FromRequestParts for ExtractChClaims - where - S: Send + Sync, - AppState: FromRef, +where + S: Send + Sync, + AppState: FromRef, { type Rejection = axum::response::Response; - async fn from_request_parts(parts: &mut axum::http::request::Parts, state: &S) -> Result { - let axum::extract::State(app_state) = axum::extract::State::::from_request_parts(parts, state) - .await - .map_err(|err| err.into_response())?; + async fn from_request_parts( + parts: &mut axum::http::request::Parts, + state: &S, + ) -> Result { + let axum::extract::State(app_state) = + axum::extract::State::::from_request_parts(parts, state) + .await + .map_err(|err| err.into_response())?; if let Some(token) = parts.headers.get(SERVICE_HEADER) { let token = token.to_str().unwrap(); debug!("...received service header: {:?}", token); @@ -138,7 +139,7 @@ pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> St create_token(issuer, audience, &private_claims) } -pub fn create_token serde::Deserialize<'de>>( +pub fn create_token serde::Deserialize<'de>>( issuer: &str, audience: &str, private_claims: &T, @@ -196,7 +197,8 @@ pub fn decode_token serde::Deserialize<'d return Err(e.into()); } }; - let jwt: biscuit::jws::Compact, biscuit::Empty> = biscuit::JWT::<_, biscuit::Empty>::new_encoded(token); + let jwt: biscuit::jws::Compact, biscuit::Empty> = + biscuit::JWT::<_, biscuit::Empty>::new_encoded(token); let decoded_jwt = match jwt.decode(&signing_secret, biscuit::jwa::SignatureAlgorithm::HS256) { Ok(x) => Ok(x), Err(e) => { @@ -219,7 +221,8 @@ pub fn decode_token serde::Deserialize<'d ..Default::default() }; - decoded_jwt.validate(val_options) + decoded_jwt + .validate(val_options) .with_context(|| "Failed validating JWT")?; Ok(decoded_jwt.payload()?.private.clone()) } diff --git a/clearing-house-app/src/model/constants.rs b/clearing-house-app/src/model/constants.rs index a6e027f..ff66bc4 100644 --- a/clearing-house-app/src/model/constants.rs +++ b/clearing-house-app/src/model/constants.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + pub const CONTENT_TYPE: &str = "Content-Type"; pub const APPLICATION_JSON: &str = "application/json"; pub const SIGNING_KEY: &str = "signing_key"; diff --git a/clearing-house-app/src/model/crypto.rs b/clearing-house-app/src/model/crypto.rs index f191556..5555c4f 100644 --- a/clearing-house-app/src/model/crypto.rs +++ b/clearing-house-app/src/model/crypto.rs @@ -1,8 +1,8 @@ use crate::crypto::generate_random_seed; +use crate::model::util::new_uuid; use hkdf::Hkdf; use sha2::Sha256; use std::collections::HashMap; -use crate::model::util::new_uuid; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct MasterKey { diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index 6897792..6ec9e10 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -3,11 +3,11 @@ use crate::model::crypto::{KeyEntry, KeyMap}; use crate::model::util::new_uuid; use aes_gcm_siv::aead::Aead; use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; +use base64::Engine; use blake2_rfc::blake2b::Blake2b; use chrono::Local; use generic_array::GenericArray; use std::collections::HashMap; -use base64::Engine; use uuid::Uuid; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] @@ -104,10 +104,8 @@ impl Document { let mut cts = vec![]; let keys = key_map.keys; - let key_ct= match key_map.keys_enc { - Some(ct) => { - hex::encode(ct) - } + let key_ct = match key_map.keys_enc { + Some(ct) => hex::encode(ct), None => { anyhow::bail!("Missing key ct"); } @@ -323,7 +321,6 @@ fn format_tc(tc: i64) -> String { format!("{:08}", tc) } - #[cfg(test)] mod test { /// Purpose of this test case: The `base64::encode` function has been deprecated in favor of @@ -344,4 +341,4 @@ mod test { let hash = doc.hash(); assert_eq!("X/BsEutzaPbi555duyusiD9z5aUCwE7oNIMteMtdYLEAqJ7FJ0Ln13J3t1Qw8MMJhLCb9rRE8bRbqHtV4mYqRA==", hash); } -} \ No newline at end of file +} diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index c4f6ea3..24e533f 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -44,9 +44,9 @@ pub struct IdsMessage { pub model_version: String, /// Correlated message, e.g., response to a previous message. Value: URI of the correlatedMessage #[serde( - rename = "ids:correlationMessage", - alias = "correlationMessage", - skip_serializing_if = "Option::is_none" + rename = "ids:correlationMessage", + alias = "correlationMessage", + skip_serializing_if = "Option::is_none" )] pub correlation_message: Option, /// Date of issuing the Message @@ -60,46 +60,46 @@ pub struct IdsMessage { pub sender_agent: String, /// Target Connector. Value: URI of target Connector. Can have multiple values at the same time. #[serde( - rename = "ids:recipientConnector", - alias = "recipientConnector", - skip_serializing_if = "Option::is_none" + rename = "ids:recipientConnector", + alias = "recipientConnector", + skip_serializing_if = "Option::is_none" )] pub recipient_connector: Option>, /// Agent, for which the message is intended. Value: URI of an instance of ids:Agent. Can have multiple values at the same time #[serde( - rename = "ids:recipientAgent", - alias = "recipientAgent", - skip_serializing_if = "Option::is_none" + rename = "ids:recipientAgent", + alias = "recipientAgent", + skip_serializing_if = "Option::is_none" )] pub recipient_agent: Option>, /// Contract which is (or will be) the legal basis of the data transfer. Value: Instance of class ids:Contract. #[serde( - rename = "ids:transferContract", - alias = "transferContract", - skip_serializing_if = "Option::is_none" + rename = "ids:transferContract", + alias = "transferContract", + skip_serializing_if = "Option::is_none" )] pub transfer_contract: Option, /// Value describing the version of the content. Value: Version number of the content. #[serde( - rename = "ids:contentVersion", - alias = "contentVersion", - skip_serializing_if = "Option::is_none" + rename = "ids:contentVersion", + alias = "contentVersion", + skip_serializing_if = "Option::is_none" )] pub content_version: Option, /// Token representing a claim, that the sender supports a certain security profile. Value: Instance of ids:DynamicAttributeToken. #[serde( - rename = "ids:securityToken", - alias = "securityToken", - skip_serializing + rename = "ids:securityToken", + alias = "securityToken", + skip_serializing )] pub security_token: Option, /// An authorization token. The token can be issued from the Connector of the Data Provider (A) to the Connector of the /// Data Consumer (B). Can be used to avoid full authentication via DAPS, if Connector B wants to access the data of /// Connector A. Value: Instance of ids:Token #[serde( - rename = "ids:authorizationToken", - alias = "authorizationToken", - skip_serializing_if = "Option::is_none" + rename = "ids:authorizationToken", + alias = "authorizationToken", + skip_serializing_if = "Option::is_none" )] pub authorization_token: Option, //IDS name @@ -117,7 +117,10 @@ impl Default for IdsMessage { IdsMessage { context: Some(std::collections::HashMap::from([ ("ids".to_string(), "https://w3id.org/idsa/core/".to_string()), - ("idsc".to_string(), "https://w3id.org/idsa/code/".to_string()) + ( + "idsc".to_string(), + "https://w3id.org/idsa/code/".to_string(), + ), ])), type_message: MessageType::Message, id: Some(autogen("MessageProcessedNotification")), @@ -379,5 +382,9 @@ impl From for Document { } fn autogen(message: &str) -> String { - format!("https://w3id.org/idsa/autogen/{}/{}", message, Document::create_uuid()) + format!( + "https://w3id.org/idsa/autogen/{}/{}", + message, + Document::create_uuid() + ) } diff --git a/clearing-house-app/src/model/ids/mod.rs b/clearing-house-app/src/model/ids/mod.rs index 020738e..d6fa87c 100644 --- a/clearing-house-app/src/model/ids/mod.rs +++ b/clearing-house-app/src/model/ids/mod.rs @@ -159,7 +159,6 @@ pub enum MessageType { #[serde(rename = "ids:RejectionMessage")] RejectionMessage, - /// ## Self-description /// Message requesting metadata. If no URI is supplied via the ids:requestedElement field, this messages is treated like a self-description request and the recipient should return its self-description via an ids:DescriptionResponseMessage. However, if a URI is supplied, the Connector should either return metadata about the requested element via an ids:DescriptionResponseMessage, or send an ids:RejectionMessage, e.g., because the element was not found. #[serde(rename = "ids:DescriptionRequestMessage")] diff --git a/clearing-house-app/src/ports/doc_type_api.rs b/clearing-house-app/src/ports/doc_type_api.rs index a2dbc66..ce342da 100644 --- a/clearing-house-app/src/ports/doc_type_api.rs +++ b/clearing-house-app/src/ports/doc_type_api.rs @@ -1,10 +1,9 @@ -use crate::model::constants::{DEFAULT_PROCESS_ID, ROCKET_DOC_TYPE_API}; +use crate::model::constants::DEFAULT_PROCESS_ID; use crate::ports::ApiResponse; use crate::AppState; use crate::model::doc_type::DocumentType; -//#[rocket::post("/", format = "json", data = "")] async fn create_doc_type( axum::extract::State(state): axum::extract::State, axum::extract::Json(doc_type): axum::extract::Json, @@ -18,7 +17,6 @@ async fn create_doc_type( } } -//#[rocket::post("/", format = "json", data = "")] async fn update_doc_type( axum::extract::State(state): axum::extract::State, axum::extract::Path(id): axum::extract::Path, @@ -33,15 +31,18 @@ async fn update_doc_type( } } -//#[rocket::delete("/", format = "json")] async fn delete_default_doc_type( state: axum::extract::State, id: axum::extract::Path, ) -> ApiResponse { - delete_doc_type(state, id, axum::extract::Path(DEFAULT_PROCESS_ID.to_string())).await + delete_doc_type( + state, + id, + axum::extract::Path(DEFAULT_PROCESS_ID.to_string()), + ) + .await } -//#[rocket::delete("//", format = "json")] async fn delete_doc_type( axum::extract::State(state): axum::extract::State, axum::extract::Path(id): axum::extract::Path, @@ -56,16 +57,24 @@ async fn delete_doc_type( } } -//#[rocket::get("/", format = "json")] -async fn get_default_doc_type(state: axum::extract::State, - id: axum::extract::Path) -> ApiResponse>{ - get_doc_type(state, id, axum::extract::Path(DEFAULT_PROCESS_ID.to_string())).await +async fn get_default_doc_type( + state: axum::extract::State, + id: axum::extract::Path, +) -> ApiResponse> { + get_doc_type( + state, + id, + axum::extract::Path(DEFAULT_PROCESS_ID.to_string()), + ) + .await } //#[rocket::get("//", format = "json")] -async fn get_doc_type(axum::extract::State(state): axum::extract::State, - axum::extract::Path(id): axum::extract::Path, - axum::extract::Path(pid): axum::extract::Path) -> ApiResponse> { +async fn get_doc_type( + axum::extract::State(state): axum::extract::State, + axum::extract::Path(id): axum::extract::Path, + axum::extract::Path(pid): axum::extract::Path, +) -> ApiResponse> { match state.keyring_service.get_doc_type(id, pid).await { Ok(dt) => match dt { Some(dt) => ApiResponse::SuccessOk(Some(dt)), @@ -79,7 +88,9 @@ async fn get_doc_type(axum::extract::State(state): axum::extract::State) -> ApiResponse> { +async fn get_doc_types( + axum::extract::State(state): axum::extract::State, +) -> ApiResponse> { match state.keyring_service.get_doc_types().await { Ok(dt) => ApiResponse::SuccessOk(dt), Err(e) => { @@ -89,16 +100,17 @@ async fn get_doc_types(axum::extract::State(state): axum::extract::State axum::Router { +pub(crate) fn router() -> axum::Router { axum::Router::new() - .route("/", - axum::routing::get(get_doc_types) - .post(create_doc_type)) - .route("/:id", - axum::routing::get(get_default_doc_type) - .post(update_doc_type) - .delete(delete_default_doc_type)) - .route("/:pid/:id", - axum::routing::get(get_doc_type) - .delete(delete_doc_type)) + .route("/", axum::routing::get(get_doc_types).post(create_doc_type)) + .route( + "/:id", + axum::routing::get(get_default_doc_type) + .post(update_doc_type) + .delete(delete_default_doc_type), + ) + .route( + "/:pid/:id", + axum::routing::get(get_doc_type).delete(delete_doc_type), + ) } diff --git a/clearing-house-app/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs index 7842b4e..93fb709 100644 --- a/clearing-house-app/src/ports/logging_api.rs +++ b/clearing-house-app/src/ports/logging_api.rs @@ -1,25 +1,20 @@ - -use biscuit::jwk::JWKSet; -use crate::{AppState, model::claims::get_jwks, model::SortingOrder, ports::ApiResponse}; use crate::model::claims::ExtractChClaims; +use crate::{model::claims::get_jwks, model::SortingOrder, ports::ApiResponse, AppState}; +use biscuit::jwk::JWKSet; -use crate::model::constants::{ - ROCKET_CLEARING_HOUSE_BASE_API, ROCKET_LOG_API, ROCKET_PK_API, ROCKET_PROCESS_API, - ROCKET_QUERY_API, -}; -use crate::model::ids::IdsQueryResult; use crate::model::ids::message::IdsMessage; use crate::model::ids::request::ClearingHouseMessage; +use crate::model::ids::IdsQueryResult; use crate::model::process::Receipt; -//#[rocket::post("/", format = "json", data = "")] -pub async fn log( +async fn log( ExtractChClaims(ch_claims): ExtractChClaims, axum::extract::State(state): axum::extract::State, axum::extract::Path(pid): axum::extract::Path, axum::extract::Json(message): axum::extract::Json, ) -> ApiResponse { - match state.logging_service + match state + .logging_service .log(ch_claims, state.signing_key_path.as_str(), message, pid) .await { @@ -31,14 +26,14 @@ pub async fn log( } } -//#[rocket::post("/", format = "json", data = "")] -pub async fn create_process( +async fn create_process( ExtractChClaims(ch_claims): ExtractChClaims, axum::extract::State(state): axum::extract::State, axum::extract::Path(pid): axum::extract::Path, axum::extract::Json(message): axum::extract::Json, ) -> ApiResponse { - match state.logging_service + match state + .logging_service .create_process(ch_claims, message, pid) .await { @@ -50,40 +45,31 @@ pub async fn create_process( } } -//#[rocket::post("/<_pid>", format = "json", rank = 50)] -/*async fn unauth(_pid: Option) -> ApiResponse { - ApiResponse::Unauthorized(String::from("Token not valid!")) -}*/ - -//#[rocket::post("/<_pid>/<_id>", format = "json", rank = 50)] -/*async fn unauth_id(_pid: Option, _id: Option) -> ApiResponse { - ApiResponse::Unauthorized(String::from("Token not valid!")) -}*/ +#[derive(serde::Deserialize)] +struct QueryParams { + pub page: Option, + pub size: Option, + pub sort: Option, + pub date_to: Option, + pub date_from: Option, +} -/*#[rocket::post( - "/?&&&&", - format = "json", - data = "" -)]*/ -pub async fn query_pid( +async fn query_pid( ExtractChClaims(ch_claims): ExtractChClaims, axum::extract::State(state): axum::extract::State, - axum::extract::Query(page): axum::extract::Query>, - axum::extract::Query(size): axum::extract::Query>, - axum::extract::Query(sort): axum::extract::Query>, - axum::extract::Query(date_to): axum::extract::Query>, - axum::extract::Query(date_from): axum::extract::Query>, + axum::extract::Query(params): axum::extract::Query, axum::extract::Path(pid): axum::extract::Path, axum::extract::Json(message): axum::extract::Json, ) -> ApiResponse { - match state.logging_service + match state + .logging_service .query_pid( ch_claims, - page, - size, - sort, - date_to, - date_from, + params.page, + params.size, + params.sort, + params.date_to, + params.date_from, pid, message, ) @@ -97,15 +83,15 @@ pub async fn query_pid( } } -//#[rocket::post("//", format = "json", data = "")] -pub async fn query_id( +async fn query_id( ExtractChClaims(ch_claims): ExtractChClaims, axum::extract::State(state): axum::extract::State, axum::extract::Path(pid): axum::extract::Path, axum::extract::Path(id): axum::extract::Path, axum::extract::Json(message): axum::extract::Json, ) -> ApiResponse { - match state.logging_service + match state + .logging_service .query_id(ch_claims, pid, id, message) .await { @@ -117,10 +103,23 @@ pub async fn query_id( } } -//#[rocket::get("/.well-known/jwks.json", format = "json")] -pub async fn get_public_sign_key(axum::extract::State(state): axum::extract::State) -> ApiResponse> { +async fn get_public_sign_key( + axum::extract::State(state): axum::extract::State, +) -> ApiResponse> { match get_jwks(state.signing_key_path.as_str()) { Some(jwks) => ApiResponse::SuccessOk(jwks), None => ApiResponse::InternalError(String::from("Error reading signing key")), } } + +pub(crate) fn router() -> axum::routing::Router { + axum::Router::new() + .route("/log/message/:pid", axum::routing::post(log)) + .route("/process/:pid", axum::routing::post(create_process)) + .route("/messages/query/:pid", axum::routing::post(query_pid)) + .route("/messages/query/:pid/:id", axum::routing::post(query_id)) + .route( + "/.well-known/jwks.json", + axum::routing::get(get_public_sign_key), + ) +} diff --git a/clearing-house-app/src/ports/mod.rs b/clearing-house-app/src/ports/mod.rs index 7705a16..dbd85be 100644 --- a/clearing-house-app/src/ports/mod.rs +++ b/clearing-house-app/src/ports/mod.rs @@ -9,7 +9,7 @@ pub(crate) mod doc_type_api; pub(crate) mod logging_api; #[derive(Debug)] -pub enum ApiResponse { +pub(crate) enum ApiResponse { PreFlight(()), BadRequest(String), SuccessCreate(T), @@ -26,13 +26,23 @@ impl axum::response::IntoResponse for ApiResponse { match self { ApiResponse::PreFlight(_) => (axum::http::StatusCode::OK, "").into_response(), ApiResponse::BadRequest(s) => (axum::http::StatusCode::BAD_REQUEST, s).into_response(), - ApiResponse::SuccessCreate(v) => (axum::http::StatusCode::CREATED, axum::response::Json(v)).into_response(), - ApiResponse::SuccessOk(v) => (axum::http::StatusCode::OK, axum::response::Json(v)).into_response(), - ApiResponse::SuccessNoContent(s) => (axum::http::StatusCode::NO_CONTENT, s).into_response(), - ApiResponse::Unauthorized(s) => (axum::http::StatusCode::UNAUTHORIZED, s).into_response(), + ApiResponse::SuccessCreate(v) => { + (axum::http::StatusCode::CREATED, axum::response::Json(v)).into_response() + } + ApiResponse::SuccessOk(v) => { + (axum::http::StatusCode::OK, axum::response::Json(v)).into_response() + } + ApiResponse::SuccessNoContent(s) => { + (axum::http::StatusCode::NO_CONTENT, s).into_response() + } + ApiResponse::Unauthorized(s) => { + (axum::http::StatusCode::UNAUTHORIZED, s).into_response() + } ApiResponse::Forbidden(s) => (axum::http::StatusCode::FORBIDDEN, s).into_response(), ApiResponse::NotFound(s) => (axum::http::StatusCode::NOT_FOUND, s).into_response(), - ApiResponse::InternalError(s) => (axum::http::StatusCode::INTERNAL_SERVER_ERROR, s).into_response(), + ApiResponse::InternalError(s) => { + (axum::http::StatusCode::INTERNAL_SERVER_ERROR, s).into_response() + } } } -} \ No newline at end of file +} diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 9b4fb2c..354916e 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -5,9 +5,7 @@ use crate::model::constants::{ }; use crate::model::crypto::{KeyCt, KeyCtList}; use crate::model::document::Document; -use crate::model::{ - parse_date, validate_and_sanitize_dates, SortingOrder, -}; +use crate::model::{parse_date, validate_and_sanitize_dates, SortingOrder}; use crate::services::keyring_service::KeyringService; use crate::services::{DocumentReceipt, QueryResult}; use anyhow::anyhow; @@ -176,7 +174,9 @@ impl DocumentService { let parsed_date_to = parse_date(date_to, true); // Validation of dates with various checks. If none given dates default to date_now (date_to) and (date_now - 2 weeks) (date_from) - let Ok((sanitized_date_from, sanitized_date_to)) = validate_and_sanitize_dates(parsed_date_from, parsed_date_to, None) else { + let Ok((sanitized_date_from, sanitized_date_to)) = + validate_and_sanitize_dates(parsed_date_from, parsed_date_to, None) + else { debug!("date validation failed!"); return Err(anyhow!("Invalid date parameter!")); // BadRequest }; diff --git a/clearing-house-app/src/services/keyring_service.rs b/clearing-house-app/src/services/keyring_service.rs index 65e2f3a..b107b21 100644 --- a/clearing-house-app/src/services/keyring_service.rs +++ b/clearing-house-app/src/services/keyring_service.rs @@ -143,16 +143,13 @@ impl KeyringService { match self.db.get_document_type(&dt_id).await { Ok(Some(dt)) => { // validate keys_ct input - let keys_ct = hex::decode(keys_ct) - .map_err(|e| { - error!("Error while decoding key ciphertext: {}", e); - anyhow!("Error while decrypting keys") // InternalError - })?; + let keys_ct = hex::decode(keys_ct).map_err(|e| { + error!("Error while decoding key ciphertext: {}", e); + anyhow!("Error while decrypting keys") // InternalError + })?; match restore_key_map(key, dt, keys_ct) { - Ok(key_map) => { - Ok(key_map) - } + Ok(key_map) => Ok(key_map), Err(e) => { error!("Error while generating key map: {}", e); Err(anyhow!("Error while restoring keys")) // InternalError diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 7dfb299..63af9ae 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -23,7 +23,11 @@ pub struct LoggingService { impl LoggingService { pub fn new(db: ProcessStore, doc_api: Arc) -> LoggingService { - LoggingService { db, doc_api, write_lock: Arc::new(tokio::sync::Mutex::new(())) } + LoggingService { + db, + doc_api, + write_lock: Arc::new(tokio::sync::Mutex::new(())), + } } pub async fn log( @@ -68,7 +72,8 @@ impl LoggingService { if self.db.store_process(new_process).await.is_err() { error!("Error while creating process '{}'", &pid); - return Err(anyhow!("Error while creating process")); // InternalError + return Err(anyhow!("Error while creating process")); + // InternalError } } Err(_) => { @@ -95,8 +100,7 @@ impl LoggingService { } debug!("logging message for pid {}", &pid); - self.log_message(user, key_path, m.clone()) - .await + self.log_message(user, key_path, m.clone()).await } } } diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index 2a7f7d2..0fe8524 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -1,14 +1,60 @@ -use crate::model::constants::SIGNING_KEY; -use std::path::Path; use anyhow::Context; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct ServiceConfig { +pub(crate) struct ServiceConfig { pub service_id: String, } /// Reads a file into a string -pub fn read_file(file: &str) -> anyhow::Result { +pub(crate) fn read_file(file: &str) -> anyhow::Result { std::fs::read_to_string(file) .with_context(|| format!("Failed to read contents of file '{}'", file)) } + +pub(super) fn init_service_config(service_id: String) -> anyhow::Result { + match std::env::var(&service_id) { + Ok(id) => Ok(ServiceConfig { service_id: id }), + Err(_e) => { + anyhow::bail!( + "Service ID not configured. Please configure environment variable {}", + &service_id + ); + } + } +} + +pub(super) fn init_signing_key(signing_key_path: Option<&str>) -> anyhow::Result { + let private_key_path = signing_key_path.unwrap_or("keys/private_key.der"); + if std::path::Path::new(&private_key_path).exists() { + Ok(private_key_path.to_string()) + } else { + anyhow::bail!("Signing key not found! Aborting startup! Please configure signing_key!"); + } +} + +/// Signal handler to catch a Ctrl+C and initiate a graceful shutdown +pub(super) async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + } + + info!("signal received, starting graceful shutdown"); +} From 6fb53063bdea69bc41c5b9eaa49203006a826cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 26 Sep 2023 13:34:16 +0200 Subject: [PATCH 070/183] Fix(ch-app): Fix URL from log endpoint --- clearing-house-app/Rocket.toml | 26 ------- clearing-house-app/config.toml | 5 +- clearing-house-app/src/config.rs | 2 +- clearing-house-app/src/db/doc_store.rs | 8 +- clearing-house-app/src/db/key_store.rs | 6 +- clearing-house-app/src/db/process_store.rs | 9 +-- clearing-house-app/src/main.rs | 86 +++++++++++---------- clearing-house-app/src/model/claims.rs | 6 +- clearing-house-app/src/ports/logging_api.rs | 2 +- 9 files changed, 63 insertions(+), 87 deletions(-) delete mode 100644 clearing-house-app/Rocket.toml diff --git a/clearing-house-app/Rocket.toml b/clearing-house-app/Rocket.toml deleted file mode 100644 index b068a57..0000000 --- a/clearing-house-app/Rocket.toml +++ /dev/null @@ -1,26 +0,0 @@ -[global] -limits = { json = 5242880 } -connector_name = "https://clearinghouse.aisec.fraunhofer.de/" -infomodel_version = "4.0.0" -server_agent = "https://clearinghouse.aisec.fraunhofer.de" -signing_key = "keys/private_key.der" - -[debug] -address = "0.0.0.0" -port = 8000 -log_level = "normal" -limits = { forms = 32768 } -database_url = "mongodb://localhost:27019" -keyring_api_url = "http://localhost:8002" -document_api_url = "http://localhost:8001" -clear_db = true - -[release] -address = "0.0.0.0" -port = 8000 -log_level = "normal" -limits = { forms = 32768 } -database_url = "mongodb://logging-service-mongo:27017" -keyring_api_url = "http://keyring-api:8002" -document_api_url = "http://document-api:8001" -clear_db = false diff --git a/clearing-house-app/config.toml b/clearing-house-app/config.toml index fe0d348..dc0ede0 100644 --- a/clearing-house-app/config.toml +++ b/clearing-house-app/config.toml @@ -1,5 +1,6 @@ -log_level = "DEBUG" # TRACE, DEBUG, INFO, WARN, ERROR +log_level = "INFO" # TRACE, DEBUG, INFO, WARN, ERROR document_database_url= "mongodb://localhost:27017" process_database_url= "mongodb://localhost:27017" keyring_database_url= "mongodb://localhost:27017" -clear_db = true \ No newline at end of file +clear_db = true +signing_key = "keys/private_key.der" # Optional \ No newline at end of file diff --git a/clearing-house-app/src/config.rs b/clearing-house-app/src/config.rs index b232e37..81ae095 100644 --- a/clearing-house-app/src/config.rs +++ b/clearing-house-app/src/config.rs @@ -72,7 +72,7 @@ pub(crate) fn read_config(config_file_override: Option<&std::path::Path>) -> CHC } /// Configure logging based on environment variable `RUST_LOG` -pub(crate) fn configure_logging(log_level: Option) { +pub(crate) fn configure_logging(log_level: &Option) { if std::env::var("RUST_LOG").is_err() { if let Some(level) = log_level { std::env::set_var("RUST_LOG", level.to_string()); diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 7d75540..f4b36f8 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -31,13 +31,9 @@ impl DataStoreApi for DataStore { } impl DataStore { - pub async fn init_datastore(db_url: String, clear_db: bool) -> anyhow::Result { + pub async fn init_datastore(db_url: &str, clear_db: bool) -> anyhow::Result { debug!("Using mongodb url: '{:#?}'", &db_url); - match init_database_client::( - db_url.as_str(), - Some(DOCUMENT_DB_CLIENT.to_string()), - ) - .await + match init_database_client::(db_url, Some(DOCUMENT_DB_CLIENT.to_string())).await { Ok(datastore) => { debug!("Check if database is empty..."); diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index 1743736..20f3880 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -27,12 +27,10 @@ impl DataStoreApi for KeyStore { } impl KeyStore { - pub async fn init_keystore(db_url: String, clear_db: bool) -> anyhow::Result { + pub async fn init_keystore(db_url: &str, clear_db: bool) -> anyhow::Result { debug!("Using database url: '{:#?}'", &db_url); - match init_database_client::(db_url.as_str(), Some(KEYRING_DB_CLIENT.to_string())) - .await - { + match init_database_client::(db_url, Some(KEYRING_DB_CLIENT.to_string())).await { Ok(keystore) => { debug!("Check if database is empty..."); match keystore diff --git a/clearing-house-app/src/db/process_store.rs b/clearing-house-app/src/db/process_store.rs index fc48f48..ff00788 100644 --- a/clearing-house-app/src/db/process_store.rs +++ b/clearing-house-app/src/db/process_store.rs @@ -29,14 +29,11 @@ impl DataStoreApi for ProcessStore { } impl ProcessStore { - pub async fn init_process_store(db_url: String, clear_db: bool) -> anyhow::Result { + pub async fn init_process_store(db_url: &str, clear_db: bool) -> anyhow::Result { debug!("...using database url: '{:#?}'", &db_url); - match init_database_client::( - db_url.as_str(), - Some(PROCESS_DB_CLIENT.to_string()), - ) - .await + match init_database_client::(db_url, Some(PROCESS_DB_CLIENT.to_string())) + .await { Ok(process_store) => { debug!("...check if database is empty..."); diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index b0dbc2d..f6f6970 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -20,6 +20,7 @@ mod ports; mod services; mod util; +/// Contains the application state #[derive(Clone)] pub(crate) struct AppState { pub keyring_service: Arc, @@ -28,45 +29,55 @@ pub(crate) struct AppState { pub signing_key_path: String, } +impl AppState { + /// Initialize the application state from config + async fn init(conf: &config::CHConfig) -> anyhow::Result { + let process_store = + ProcessStore::init_process_store(&conf.process_database_url, conf.clear_db) + .await + .expect("Failure to initialize process store! Exiting..."); + let keyring_store = KeyStore::init_keystore(&conf.keyring_database_url, conf.clear_db) + .await + .expect("Failure to initialize keyring store! Exiting..."); + let doc_store = DataStore::init_datastore(&conf.document_database_url, conf.clear_db) + .await + .expect("Failure to initialize document store! Exiting..."); + + let keyring_service = Arc::new(services::keyring_service::KeyringService::new( + keyring_store, + )); + let doc_service = Arc::new(services::document_service::DocumentService::new( + doc_store, + keyring_service.clone(), + )); + let logging_service = Arc::new(services::logging_service::LoggingService::new( + process_store, + doc_service.clone(), + )); + + let service_config = Arc::new(util::init_service_config( + ENV_LOGGING_SERVICE_ID.to_string(), + )?); + let signing_key = util::init_signing_key(conf.signing_key.as_deref())?; + + Ok(Self { + signing_key_path: signing_key, + service_config, + keyring_service, + logging_service, + }) + } +} + +/// Main function: Reading config, initializing application state, starting server #[tokio::main] async fn main() -> Result<(), anyhow::Error> { // Read configuration let conf = config::read_config(None); - config::configure_logging(conf.log_level); + config::configure_logging(&conf.log_level); - let process_store = ProcessStore::init_process_store(conf.process_database_url, conf.clear_db) - .await - .expect("Failure to initialize process store! Exiting..."); - let keyring_store = KeyStore::init_keystore(conf.keyring_database_url, conf.clear_db) - .await - .expect("Failure to initialize keyring store! Exiting..."); - let doc_store = DataStore::init_datastore(conf.document_database_url, conf.clear_db) - .await - .expect("Failure to initialize document store! Exiting..."); - - let keyring_service = Arc::new(services::keyring_service::KeyringService::new( - keyring_store, - )); - let doc_service = Arc::new(services::document_service::DocumentService::new( - doc_store, - keyring_service.clone(), - )); - let logging_service = Arc::new(services::logging_service::LoggingService::new( - process_store, - doc_service.clone(), - )); - - let service_config = Arc::new(util::init_service_config( - ENV_LOGGING_SERVICE_ID.to_string(), - )?); - let signing_key = util::init_signing_key(conf.signing_key.as_deref())?; - - let app_state = AppState { - signing_key_path: signing_key, - service_config, - keyring_service, - logging_service, - }; + // Initialize application state + let app_state = AppState::init(&conf).await?; // Setup router let app = axum::Router::new() @@ -77,11 +88,8 @@ async fn main() -> Result<(), anyhow::Error> { // Bind port and start server let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); tracing::debug!("listening on {}", addr); - axum::Server::bind(&addr) + Ok(axum::Server::bind(&addr) .serve(app.into_make_service()) .with_graceful_shutdown(util::shutdown_signal()) - .await - .expect("Starting axum server failed!"); - - Ok(()) + .await?) } diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index aa2adaf..ebb944b 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -139,7 +139,9 @@ pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> St create_token(issuer, audience, &private_claims) } -pub fn create_token serde::Deserialize<'de>>( +pub fn create_token< + T: std::fmt::Display + Clone + serde::Serialize + for<'de> serde::Deserialize<'de>, +>( issuer: &str, audience: &str, private_claims: &T, @@ -215,8 +217,8 @@ pub fn decode_token serde::Deserialize<'d }; let val_options = biscuit::ValidationOptions { claim_presence_options, - // issued_at: Validate(Duration::minutes(5)), // Issuer is not validated. Wouldn't make much of a difference if we did + // issued_at: Validate(Duration::minutes(5)), audience: Validate(audience.to_string()), ..Default::default() }; diff --git a/clearing-house-app/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs index 93fb709..c80bd7d 100644 --- a/clearing-house-app/src/ports/logging_api.rs +++ b/clearing-house-app/src/ports/logging_api.rs @@ -114,7 +114,7 @@ async fn get_public_sign_key( pub(crate) fn router() -> axum::routing::Router { axum::Router::new() - .route("/log/message/:pid", axum::routing::post(log)) + .route("/messages/log/:pid", axum::routing::post(log)) .route("/process/:pid", axum::routing::post(create_process)) .route("/messages/query/:pid", axum::routing::post(query_pid)) .route("/messages/query/:pid/:id", axum::routing::post(query_id)) From f1726e74574a596e1216d4cf468af1ccfd07443e Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Tue, 26 Sep 2023 14:57:12 -0300 Subject: [PATCH 071/183] feat: basic endpoint functions working --- .../edc/multipart/MultipartExtension.java | 11 ++- .../controller/MultipartController.java | 75 +++++++++++++++- .../edc/multipart/handler/Handler.java | 12 +++ .../edc/multipart/handler/LogHandler.java | 49 +++++++++++ .../multipart/message/MultipartRequest.java | 86 +++++++++++++++++++ .../multipart/message/MultipartResponse.java | 76 ++++++++++++++++ .../edc/multipart/types/TypeManagerUtil.java | 6 +- .../ids/{Message.java => LogMessage.java} | 55 +----------- .../multipart/types/ids/RejectionMessage.java | 2 +- .../edc/multipart/util/ResponseUtil.java | 69 ++++++++++++--- 10 files changed, 368 insertions(+), 73 deletions(-) create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/Handler.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogHandler.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java rename clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/{Message.java => LogMessage.java} (68%) diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java index 1ccde37..d9695d9 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java @@ -1,6 +1,8 @@ package de.truzzt.clearinghouse.edc.multipart; import de.truzzt.clearinghouse.edc.multipart.controller.MultipartController; +import de.truzzt.clearinghouse.edc.multipart.handler.Handler; +import de.truzzt.clearinghouse.edc.multipart.handler.LogHandler; import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.protocol.ids.jsonld.JsonLd; @@ -12,6 +14,8 @@ import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebService; +import java.util.LinkedList; + import static org.eclipse.edc.protocol.ids.util.ConnectorIdUtil.resolveConnectorId; @Extension(value = MultipartExtension.NAME) @@ -43,7 +47,12 @@ public void initialize(ServiceExtensionContext context) { var connectorId = resolveConnectorId(context); var typeManagerUtil = new TypeManagerUtil(JsonLd.getObjectMapper()); - var multipartController = new MultipartController(connectorId, typeManagerUtil); + var monitor = context.getMonitor(); + + var handlers = new LinkedList(); + handlers.add(new LogHandler(monitor,connectorId,typeManagerUtil)); + + var multipartController = new MultipartController(monitor, connectorId, typeManagerUtil, handlers); webService.registerResource(managementApiConfig.getContextAlias(), multipartController); } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java index 36c756e..0c25d6f 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java @@ -1,7 +1,10 @@ package de.truzzt.clearinghouse.edc.multipart.controller; +import de.truzzt.clearinghouse.edc.multipart.handler.Handler; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; -import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -9,14 +12,20 @@ import jakarta.ws.rs.core.MediaType; import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.monitor.Monitor; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.jetbrains.annotations.NotNull; import java.io.InputStream; +import java.util.List; import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.malformedMessage; +import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.messageTypeNotSupported; +import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.notAuthenticated; +import static java.lang.String.format; @Consumes({MediaType.MULTIPART_FORM_DATA}) @Produces({MediaType.MULTIPART_FORM_DATA}) @@ -26,13 +35,21 @@ public class MultipartController { private static final String HEADER = "header"; private static final String PAYLOAD = "payload"; + private final Monitor monitor; private final IdsId connectorId; + private final List multipartHandlers; + private final TypeManagerUtil typeManagerUtil; - public MultipartController(@NotNull IdsId connectorId, TypeManagerUtil typeManagerUtil) { + public MultipartController(@NotNull Monitor monitor, + @NotNull IdsId connectorId, + @NotNull TypeManagerUtil typeManagerUtil, + @NotNull List multipartHandlers) { + this.monitor = monitor; this.connectorId = connectorId; this.typeManagerUtil = typeManagerUtil; + this.multipartHandlers = multipartHandlers; } @POST @@ -44,10 +61,60 @@ public FormDataMultiPart request(@FormDataParam(HEADER) InputStream headerInputS return createFormDataMultiPart(malformedMessage(null, connectorId)); } - return null; + LogMessage header; + try { + header = typeManagerUtil.parseMessage(headerInputStream); + } catch (Exception e) { + monitor.warning(format("InfrastructureController: Header parsing failed: %s", e.getMessage())); + return createFormDataMultiPart(malformedMessage(null, connectorId)); + } + + if (header == null) { + return createFormDataMultiPart(malformedMessage(null, connectorId)); + } + + // Check if any required header field missing + if (header.getId() == null || header.getIssuerConnector() == null || header.getSenderAgent() == null) { + return createFormDataMultiPart(malformedMessage(header, connectorId)); + } + + // Check if DAT present + var dynamicAttributeToken = header.getSecurityToken(); + if (dynamicAttributeToken == null || dynamicAttributeToken.getTokenValue() == null) { + monitor.warning("InfrastructureController: Token is missing in header"); + return createFormDataMultiPart(notAuthenticated(header, connectorId)); + } + + // Build the multipart request + var emptyClaimToken = ClaimToken.Builder.newInstance().build(); + var multipartRequest = MultipartRequest.Builder.newInstance() + .header(header) + .payload(payload) + .claimToken(emptyClaimToken) + .build(); + + var multipartResponse = multipartHandlers.stream() + .filter(h -> h.canHandle(multipartRequest)) + .findFirst() + .map(it -> it.handleRequest(multipartRequest)) + .orElseGet(() -> MultipartResponse.Builder.newInstance() + .header(messageTypeNotSupported(header, connectorId)) + .build()); + + return createFormDataMultiPart(multipartResponse.getHeader(), multipartResponse.getPayload()); + } + + private FormDataMultiPart createFormDataMultiPart(LogMessage header, Object payload) { + var multiPart = createFormDataMultiPart(header); + + if (payload != null) { + multiPart.bodyPart(new FormDataBodyPart(PAYLOAD, typeManagerUtil.toJson(payload), MediaType.APPLICATION_JSON_TYPE)); + } + + return multiPart; } - private FormDataMultiPart createFormDataMultiPart(Message header) { + private FormDataMultiPart createFormDataMultiPart(LogMessage header) { var multiPart = new FormDataMultiPart(); if (header != null) { multiPart.bodyPart(new FormDataBodyPart(HEADER, typeManagerUtil.toJson(header), MediaType.APPLICATION_JSON_TYPE)); diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/Handler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/Handler.java new file mode 100644 index 0000000..d04af3b --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/Handler.java @@ -0,0 +1,12 @@ +package de.truzzt.clearinghouse.edc.multipart.handler; + +import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; +import org.jetbrains.annotations.NotNull; + +public interface Handler { + + boolean canHandle(@NotNull MultipartRequest multipartRequest); + + @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest); +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogHandler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogHandler.java new file mode 100644 index 0000000..1811379 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogHandler.java @@ -0,0 +1,49 @@ +package de.truzzt.clearinghouse.edc.multipart.handler; + +import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; +import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.multipart.types.jwt.JwtPayload; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.NotNull; + +import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.badParameters; +import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.createMultipartResponse; +import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.messageProcessedNotification; + +public class LogHandler implements Handler { + + private final Monitor monitor; + private final IdsId connectorId; + private final TypeManagerUtil typeManagerUtil; + + public LogHandler(Monitor monitor, IdsId connectorId, TypeManagerUtil typeManagerUtil) { + this.monitor = monitor; + this.connectorId = connectorId; + this.typeManagerUtil = typeManagerUtil; + } + + @Override + public boolean canHandle(@NotNull MultipartRequest multipartRequest) { + return multipartRequest.getHeader().getType().equals("ids:LogMessage"); + } + + @Override + public @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest) { + + var header = multipartRequest.getHeader(); + + JwtPayload jwt; + try { + jwt = typeManagerUtil.parseToken(header.getSecurityToken()); + } catch (EdcException e) { + monitor.severe("LogMessage: Security Token is invalid", e); + return createMultipartResponse(badParameters(header, connectorId)); + } + + + return createMultipartResponse(messageProcessedNotification(header, connectorId)); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java new file mode 100644 index 0000000..ad7c2d9 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.message; + + +import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +public class MultipartRequest { + + private final LogMessage header; + private final String payload; + private final ClaimToken claimToken; + + private MultipartRequest(@NotNull LogMessage header, @Nullable String payload, ClaimToken claimToken) { + this.header = header; + this.payload = payload; + this.claimToken = claimToken; + } + + @NotNull + public LogMessage getHeader() { + return header; + } + + @Nullable + public String getPayload() { + return payload; + } + + @Nullable + public ClaimToken getClaimToken() { + return claimToken; + } + + public static class Builder { + + private LogMessage header; + private String payload; + private ClaimToken claimToken; + + private Builder() { + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder header(@NotNull LogMessage header) { + this.header = header; + return this; + } + + public Builder payload(@Nullable String payload) { + this.payload = payload; + return this; + } + + public Builder claimToken(@NotNull ClaimToken claimToken) { + this.claimToken = claimToken; + return this; + } + + public MultipartRequest build() { + Objects.requireNonNull(header, "Multipart request header is null."); + Objects.requireNonNull(claimToken, "Multipart request claim token is null."); + return new MultipartRequest(header, payload, claimToken); + } + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java new file mode 100644 index 0000000..97d7e33 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.message; + +import de.truzzt.clearinghouse.edc.multipart.types.ids.DynamicAttributeToken; +import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.Function; + +public class MultipartResponse { + + private final LogMessage header; + private final Object payload; + + private MultipartResponse(@NotNull LogMessage header, @Nullable Object payload) { + this.header = header; + this.payload = payload; + } + + @NotNull + public LogMessage getHeader() { + return header; + } + + @Nullable + public Object getPayload() { + return payload; + } + + public void setSecurityToken(Function getToken) { + getHeader().setSecurityToken(getToken.apply(getHeader())); + } + + public static class Builder { + + private LogMessage header; + private Object payload; + + private Builder() { + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder header(@Nullable LogMessage header) { + this.header = header; + return this; + } + + public Builder payload(@Nullable Object payload) { + this.payload = payload; + return this; + } + + public MultipartResponse build() { + Objects.requireNonNull(header, "Multipart response header is null."); + return new MultipartResponse(header, payload); + } + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java index 07fe3a6..c997a26 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java @@ -17,7 +17,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import de.truzzt.clearinghouse.edc.multipart.types.ids.DynamicAttributeToken; -import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; import de.truzzt.clearinghouse.edc.multipart.types.jwt.JwtPayload; import org.eclipse.edc.spi.EdcException; @@ -33,9 +33,9 @@ public TypeManagerUtil(ObjectMapper mapper) { this.mapper = mapper; } - public Message parseMessage(InputStream streamToken) { + public LogMessage parseMessage(InputStream streamToken) { try { - return mapper.readValue(streamToken, Message.class); + return mapper.readValue(streamToken, LogMessage.class); } catch (IOException e) { throw new EdcException("Error parsing Header to Message", e); } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/LogMessage.java similarity index 68% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java rename to clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/LogMessage.java index 80d0352..e8f259e 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/LogMessage.java @@ -22,10 +22,9 @@ import javax.xml.datatype.XMLGregorianCalendar; import java.net.URI; -import java.util.List; import java.util.Map; -public class Message { +public class LogMessage { @JsonProperty("@context") @@ -55,18 +54,6 @@ public class Message { @JsonAlias({"ids:modelVersion", "modelVersion"}) String modelVersion; - @JsonProperty("ids:correlationMessage") - @JsonAlias({"ids:correlationMessage", "correlationMessage"}) - URI correlationMessage; - - @JsonProperty("ids:recipientConnector") - @JsonAlias({"ids:recipientConnector", "recipientConnector"}) - List recipientConnector; - - @JsonProperty("ids:recipientAgent") - @JsonAlias({"ids:recipientAgent", "recipientAgent"}) - List recipientAgent; - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSzzz") @NotNull @JsonProperty("ids:issued") @@ -79,18 +66,14 @@ public class Message { @JsonAlias({"ids:senderAgent", "senderAgent"}) private URI senderAgent; - @JsonProperty("ids:contentVersion") - @JsonAlias({"ids:contentVersion", "contentVersion"}) - String contentVersion; - // all classes have a generic property array @JsonIgnore protected Map properties; - public Message() { + public LogMessage() { } - public Message(URI id) { + public LogMessage(URI id) { this.id = id; } @@ -126,30 +109,6 @@ public void setModelVersion(String modelVersion) { this.modelVersion = modelVersion; } - public URI getCorrelationMessage() { - return correlationMessage; - } - - public void setCorrelationMessage(URI correlationMessage) { - this.correlationMessage = correlationMessage; - } - - public List getRecipientConnector() { - return recipientConnector; - } - - public void setRecipientConnector(List recipientConnector) { - this.recipientConnector = recipientConnector; - } - - public List getRecipientAgent() { - return recipientAgent; - } - - public void setRecipientAgent(List recipientAgent) { - this.recipientAgent = recipientAgent; - } - public XMLGregorianCalendar getIssued() { return issued; } @@ -174,14 +133,6 @@ public void setSenderAgent(URI senderAgent) { this.senderAgent = senderAgent; } - public String getContentVersion() { - return contentVersion; - } - - public void setContentVersion(String contentVersion) { - this.contentVersion = contentVersion; - } - public Context getContext() { return context; } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java index 6f0d1d0..666b130 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java @@ -18,7 +18,7 @@ import java.net.URI; -public class RejectionMessage extends Message { +public class RejectionMessage extends LogMessage { @JsonAlias({"https://w3id.org/idsa/core/rejectionReason", "ids:rejectionReason", "rejectionReason"}) RejectionReason rejectionReason; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java index b1d7252..03c6844 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java @@ -1,6 +1,7 @@ package de.truzzt.clearinghouse.edc.multipart.util; -import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; +import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; import de.truzzt.clearinghouse.edc.multipart.types.ids.RejectionMessage; import de.truzzt.clearinghouse.edc.multipart.types.ids.RejectionReason; import org.eclipse.edc.protocol.ids.spi.domain.IdsConstants; @@ -14,15 +15,48 @@ import javax.xml.datatype.XMLGregorianCalendar; import java.net.URI; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Collections; import java.util.GregorianCalendar; import java.util.UUID; +import static org.eclipse.edc.protocol.ids.util.CalendarUtil.gregorianNow; + public class ResponseUtil { + private static final String PROCESSED_NOTIFICATION_TYPE = "ids:MessageProcessedNotificationMessage"; + + public static MultipartResponse createMultipartResponse(@NotNull LogMessage header) { + return MultipartResponse.Builder.newInstance() + .header(header) + .build(); + } + + public static LogMessage messageProcessedNotification(@NotNull LogMessage correlationMessage, + @NotNull IdsId connectorId) { + var messageId = getMessageId(); + + LogMessage message = new LogMessage(messageId); + message.setContext(correlationMessage.getContext()); + message.setType(PROCESSED_NOTIFICATION_TYPE); + message.setSecurityToken(correlationMessage.getSecurityToken()); + message.setIssuerConnector(connectorId.toUri()); + message.setModelVersion(IdsConstants.INFORMATION_MODEL_VERSION); + message.setIssued(gregorianNow()); + message.setSenderAgent(connectorId.toUri()); + + return message; + } + + @NotNull + public static RejectionMessage notAuthenticated(@NotNull LogMessage correlationMessage, + @NotNull IdsId connectorId) { + RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); + rejectionMessage.setRejectionReason(RejectionReason.NOT_AUTHENTICATED); + + return rejectionMessage; + } + @NotNull - public static RejectionMessage malformedMessage(@Nullable Message correlationMessage, + public static RejectionMessage malformedMessage(@Nullable LogMessage correlationMessage, @NotNull IdsId connectorId) { RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); rejectionMessage.setRejectionReason(RejectionReason.MALFORMED_MESSAGE); @@ -31,23 +65,34 @@ public static RejectionMessage malformedMessage(@Nullable Message correlationMes } @NotNull - private static RejectionMessage createRejectionMessage(@Nullable Message correlationMessage, + public static RejectionMessage messageTypeNotSupported(@NotNull LogMessage correlationMessage, + @NotNull IdsId connectorId) { + RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); + rejectionMessage.setRejectionReason(RejectionReason.MESSAGE_TYPE_NOT_SUPPORTED); + + return rejectionMessage; + } + + @NotNull + public static RejectionMessage badParameters(@NotNull LogMessage correlationMessage, + @NotNull IdsId connectorId) { + RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); + rejectionMessage.setRejectionReason(RejectionReason.BAD_PARAMETERS); + + return rejectionMessage; + } + + @NotNull + private static RejectionMessage createRejectionMessage(@Nullable LogMessage correlationMessage, @NotNull IdsId connectorId) { var messageId = getMessageId(); var rejectionMessage = new RejectionMessage(messageId); - rejectionMessage.setContentVersion(IdsConstants.INFORMATION_MODEL_VERSION); rejectionMessage.setModelVersion(IdsConstants.INFORMATION_MODEL_VERSION); rejectionMessage.setIssued(gregorianNow()); rejectionMessage.setIssuerConnector(connectorId.toUri()); rejectionMessage.setSenderAgent(connectorId.toUri()); - if (correlationMessage != null) { - rejectionMessage.setCorrelationMessage(correlationMessage.getId()); - rejectionMessage.setRecipientAgent(new ArrayList<>(Collections.singletonList(correlationMessage.getSenderAgent()))); - rejectionMessage.setRecipientConnector(new ArrayList<>(Collections.singletonList(correlationMessage.getIssuerConnector()))); - } - return rejectionMessage; } From 0cf4adaa5494a8ae3bc679ee0387b90bc3079e38 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 27 Sep 2023 14:53:17 +0200 Subject: [PATCH 072/183] feat(docs): add mdbook for documentation --- .github/workflows/docs.yml | 27 +++++++++++++++++++++++++++ book.toml | 6 ++++++ doc/SUMMARY.md | 4 ++++ 3 files changed, 37 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 book.toml create mode 100644 doc/SUMMARY.md diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..ffdcb1a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,27 @@ +# This is a basic workflow to help you get started with Actions + +name: Deploy to Github-Pages + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + branches: [ main ] + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: mdBook Action + uses: peaceiris/actions-mdbook@v1 + + - run: mdbook build + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./book \ No newline at end of file diff --git a/book.toml b/book.toml new file mode 100644 index 0000000..ae53865 --- /dev/null +++ b/book.toml @@ -0,0 +1,6 @@ +[book] +authors = ["mschoenenberg", "dhommen"] +language = "en" +multilingual = false +src = "docs" +title = "Documentation" \ No newline at end of file diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md new file mode 100644 index 0000000..897434d --- /dev/null +++ b/doc/SUMMARY.md @@ -0,0 +1,4 @@ +# Summary + +- [Installation]() +- [Usage]() From 6c73f028497576041f8e4549c5840eaf94b418b2 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Wed, 27 Sep 2023 17:55:31 -0300 Subject: [PATCH 073/183] feat (ch-edc): logging message multipart endpoint --- .../extensions/multipart/build.gradle.kts | 2 + ... => ClearingHouseMultipartController.java} | 86 +++++--- ...a => ClearingHouseMultipartExtension.java} | 31 +-- .../edc/multipart/handler/LogHandler.java | 49 ----- .../multipart/handler/LogMessageHandler.java | 84 ++++++++ .../handler/QueryMessageHandler.java | 34 ++++ .../handler/RequestMessageHandler.java | 34 ++++ .../message/ClearingHouseAppRequest.java | 82 ++++++++ .../multipart/message/MultipartRequest.java | 46 ++--- .../multipart/message/MultipartResponse.java | 16 +- .../sender/ClearingHouseAppSender.java | 83 ++++++++ .../LoggingMessageSenderDelegate.java | 60 ++++++ .../sender/delegate/SenderDelegate.java | 26 +++ .../edc/multipart/types/TypeManagerUtil.java | 21 +- .../types/clearinghouse/Context.java | 41 ++++ .../clearinghouse/LoggingMessageRequest.java | 35 ++++ .../clearinghouse/LoggingMessageResponse.java | 30 +++ .../types/clearinghouse/RequestHeader.java | 183 ++++++++++++++++++ .../types/clearinghouse/SecurityToken.java | 110 +++++++++++ .../TokenFormat.java} | 21 +- .../edc/multipart/types/ids/Context.java | 16 +- .../ids/{LogMessage.java => Message.java} | 41 ++-- .../multipart/types/ids/RejectionMessage.java | 12 +- .../multipart/types/ids/RejectionReason.java | 6 +- ...AttributeToken.java => SecurityToken.java} | 38 ++-- .../edc/multipart/types/ids/TokenFormat.java | 18 -- .../edc/multipart/util/JWTUtil.java | 12 ++ .../edc/multipart/util/ResponseUtil.java | 21 +- ...rg.eclipse.edc.spi.system.ServiceExtension | 2 +- 29 files changed, 985 insertions(+), 255 deletions(-) rename clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/{controller/MultipartController.java => ClearingHouseMultipartController.java} (50%) rename clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/{MultipartExtension.java => ClearingHouseMultipartExtension.java} (57%) delete mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogHandler.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogMessageHandler.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/QueryMessageHandler.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/RequestMessageHandler.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/ClearingHouseAppRequest.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/ClearingHouseAppSender.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/LoggingMessageSenderDelegate.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/SenderDelegate.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/Context.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageRequest.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageResponse.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/RequestHeader.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/SecurityToken.java rename clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/{jwt/JwtPayload.java => clearinghouse/TokenFormat.java} (53%) rename clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/{LogMessage.java => Message.java} (75%) rename clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/{DynamicAttributeToken.java => SecurityToken.java} (55%) create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/JWTUtil.java diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index 570361c..e846b09 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -24,4 +24,6 @@ dependencies { implementation(edc.api.management.config) implementation(libs.jakarta.rsApi) implementation(libs.jersey.multipart) + + implementation("com.auth0:java-jwt:4.2.2") } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartController.java similarity index 50% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java rename to clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartController.java index 0c25d6f..0ebb4ae 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/controller/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartController.java @@ -1,18 +1,20 @@ -package de.truzzt.clearinghouse.edc.multipart.controller; +package de.truzzt.clearinghouse.edc.multipart; import de.truzzt.clearinghouse.edc.multipart.handler.Handler; import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; -import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; +import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; + import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; import org.eclipse.edc.protocol.ids.spi.types.IdsId; -import org.eclipse.edc.spi.iam.ClaimToken; import org.eclipse.edc.spi.monitor.Monitor; import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; @@ -30,10 +32,12 @@ @Consumes({MediaType.MULTIPART_FORM_DATA}) @Produces({MediaType.MULTIPART_FORM_DATA}) @Path("/") -public class MultipartController { +public class ClearingHouseMultipartController { private static final String HEADER = "header"; private static final String PAYLOAD = "payload"; + private static final String PID = "pid"; + private static final String LOG_ID = "InfrastructureController"; private final Monitor monitor; private final IdsId connectorId; @@ -42,10 +46,10 @@ public class MultipartController { private final TypeManagerUtil typeManagerUtil; - public MultipartController(@NotNull Monitor monitor, - @NotNull IdsId connectorId, - @NotNull TypeManagerUtil typeManagerUtil, - @NotNull List multipartHandlers) { + public ClearingHouseMultipartController(@NotNull Monitor monitor, + @NotNull IdsId connectorId, + @NotNull TypeManagerUtil typeManagerUtil, + @NotNull List multipartHandlers) { this.monitor = monitor; this.connectorId = connectorId; this.typeManagerUtil = typeManagerUtil; @@ -53,46 +57,67 @@ public MultipartController(@NotNull Monitor monitor, } @POST - @Path("log") - public FormDataMultiPart request(@FormDataParam(HEADER) InputStream headerInputStream, - @FormDataParam(PAYLOAD) String payload) { + @Path("messages/log/{pid}") + public Response request(@PathParam(PID) String pid, + @FormDataParam(HEADER) InputStream headerInputStream, + @FormDataParam(PAYLOAD) String payload) { + // Check if header is missing if (headerInputStream == null) { - return createFormDataMultiPart(malformedMessage(null, connectorId)); + monitor.warning(LOG_ID + ": Header is missing"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) + .build(); } - LogMessage header; + // Convert header to message + Message header; try { - header = typeManagerUtil.parseMessage(headerInputStream); + header = typeManagerUtil.parse(headerInputStream, Message.class); } catch (Exception e) { - monitor.warning(format("InfrastructureController: Header parsing failed: %s", e.getMessage())); - return createFormDataMultiPart(malformedMessage(null, connectorId)); - } - - if (header == null) { - return createFormDataMultiPart(malformedMessage(null, connectorId)); + monitor.warning(format(LOG_ID + ": Header parsing failed: %s", e.getMessage())); + return Response.status(Response.Status.BAD_REQUEST) + .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) + .build(); } // Check if any required header field missing - if (header.getId() == null || header.getIssuerConnector() == null || header.getSenderAgent() == null) { - return createFormDataMultiPart(malformedMessage(header, connectorId)); + if (header.getId() == null + || header.getType() == null + || header.getModelVersion() == null + || header.getIssued() == null + || header.getIssuerConnector() == null + || header.getSenderAgent() == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(createFormDataMultiPart(malformedMessage(header, connectorId))) + .build(); } // Check if DAT present var dynamicAttributeToken = header.getSecurityToken(); if (dynamicAttributeToken == null || dynamicAttributeToken.getTokenValue() == null) { - monitor.warning("InfrastructureController: Token is missing in header"); - return createFormDataMultiPart(notAuthenticated(header, connectorId)); + monitor.warning(LOG_ID + ": Token is missing in header"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(createFormDataMultiPart(notAuthenticated(header, connectorId))) + .build(); + } + + // Check if payload is missing + if (payload == null) { + monitor.warning(LOG_ID + ": Payload is missing"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) + .build(); } // Build the multipart request - var emptyClaimToken = ClaimToken.Builder.newInstance().build(); var multipartRequest = MultipartRequest.Builder.newInstance() + .pid(pid) .header(header) .payload(payload) - .claimToken(emptyClaimToken) .build(); + // Send to handler processing var multipartResponse = multipartHandlers.stream() .filter(h -> h.canHandle(multipartRequest)) .findFirst() @@ -101,10 +126,13 @@ public FormDataMultiPart request(@FormDataParam(HEADER) InputStream headerInputS .header(messageTypeNotSupported(header, connectorId)) .build()); - return createFormDataMultiPart(multipartResponse.getHeader(), multipartResponse.getPayload()); + // Build response + return Response.status(Response.Status.CREATED) + .entity(createFormDataMultiPart(multipartResponse.getHeader(), multipartResponse.getPayload())) + .build(); } - private FormDataMultiPart createFormDataMultiPart(LogMessage header, Object payload) { + private FormDataMultiPart createFormDataMultiPart(Message header, Object payload) { var multiPart = createFormDataMultiPart(header); if (payload != null) { @@ -114,7 +142,7 @@ private FormDataMultiPart createFormDataMultiPart(LogMessage header, Object payl return multiPart; } - private FormDataMultiPart createFormDataMultiPart(LogMessage header) { + private FormDataMultiPart createFormDataMultiPart(Message header) { var multiPart = new FormDataMultiPart(); if (header != null) { multiPart.bodyPart(new FormDataBodyPart(HEADER, typeManagerUtil.toJson(header), MediaType.APPLICATION_JSON_TYPE)); diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartExtension.java similarity index 57% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java rename to clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartExtension.java index d9695d9..1db6cb0 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartExtension.java @@ -1,15 +1,17 @@ package de.truzzt.clearinghouse.edc.multipart; -import de.truzzt.clearinghouse.edc.multipart.controller.MultipartController; import de.truzzt.clearinghouse.edc.multipart.handler.Handler; -import de.truzzt.clearinghouse.edc.multipart.handler.LogHandler; +import de.truzzt.clearinghouse.edc.multipart.handler.LogMessageHandler; +import de.truzzt.clearinghouse.edc.multipart.handler.QueryMessageHandler; +import de.truzzt.clearinghouse.edc.multipart.handler.RequestMessageHandler; +import de.truzzt.clearinghouse.edc.multipart.sender.ClearingHouseAppSender; import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.protocol.ids.jsonld.JsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Requires; -import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.http.EdcHttpClient; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.eclipse.edc.web.spi.WebService; @@ -18,16 +20,13 @@ import static org.eclipse.edc.protocol.ids.util.ConnectorIdUtil.resolveConnectorId; -@Extension(value = MultipartExtension.NAME) +@Extension(value = ClearingHouseMultipartExtension.NAME) @Requires(value = { WebService.class, - ManagementApiConfiguration.class + ManagementApiConfiguration.class, + EdcHttpClient.class }) -public class MultipartExtension implements ServiceExtension { - - @Setting - public static final String EDC_IDS_ID = "edc.ids.id"; - public static final String DEFAULT_EDC_IDS_ID = "urn:connector:edc"; +public class ClearingHouseMultipartExtension implements ServiceExtension { public static final String NAME = "Clearing House Multipart Extension"; @@ -37,6 +36,9 @@ public class MultipartExtension implements ServiceExtension { @Inject private ManagementApiConfiguration managementApiConfig; + @Inject + private EdcHttpClient httpClient; + @Override public String name() { return NAME; @@ -44,15 +46,18 @@ public String name() { @Override public void initialize(ServiceExtensionContext context) { + var monitor = context.getMonitor(); var connectorId = resolveConnectorId(context); var typeManagerUtil = new TypeManagerUtil(JsonLd.getObjectMapper()); - var monitor = context.getMonitor(); + var clearingHouseAppSender = new ClearingHouseAppSender(monitor, httpClient, typeManagerUtil); var handlers = new LinkedList(); - handlers.add(new LogHandler(monitor,connectorId,typeManagerUtil)); + handlers.add(new RequestMessageHandler(monitor, connectorId, clearingHouseAppSender)); + handlers.add(new LogMessageHandler(monitor, connectorId, typeManagerUtil, clearingHouseAppSender)); + handlers.add(new QueryMessageHandler(monitor, connectorId, clearingHouseAppSender)); - var multipartController = new MultipartController(monitor, connectorId, typeManagerUtil, handlers); + var multipartController = new ClearingHouseMultipartController(monitor, connectorId, typeManagerUtil, handlers); webService.registerResource(managementApiConfig.getContextAlias(), multipartController); } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogHandler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogHandler.java deleted file mode 100644 index 1811379..0000000 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.truzzt.clearinghouse.edc.multipart.handler; - -import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; -import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; -import de.truzzt.clearinghouse.edc.multipart.types.jwt.JwtPayload; -import org.eclipse.edc.protocol.ids.spi.types.IdsId; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; -import org.jetbrains.annotations.NotNull; - -import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.badParameters; -import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.createMultipartResponse; -import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.messageProcessedNotification; - -public class LogHandler implements Handler { - - private final Monitor monitor; - private final IdsId connectorId; - private final TypeManagerUtil typeManagerUtil; - - public LogHandler(Monitor monitor, IdsId connectorId, TypeManagerUtil typeManagerUtil) { - this.monitor = monitor; - this.connectorId = connectorId; - this.typeManagerUtil = typeManagerUtil; - } - - @Override - public boolean canHandle(@NotNull MultipartRequest multipartRequest) { - return multipartRequest.getHeader().getType().equals("ids:LogMessage"); - } - - @Override - public @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest) { - - var header = multipartRequest.getHeader(); - - JwtPayload jwt; - try { - jwt = typeManagerUtil.parseToken(header.getSecurityToken()); - } catch (EdcException e) { - monitor.severe("LogMessage: Security Token is invalid", e); - return createMultipartResponse(badParameters(header, connectorId)); - } - - - return createMultipartResponse(messageProcessedNotification(header, connectorId)); - } -} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogMessageHandler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogMessageHandler.java new file mode 100644 index 0000000..a1bc43e --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogMessageHandler.java @@ -0,0 +1,84 @@ +package de.truzzt.clearinghouse.edc.multipart.handler; + +import com.auth0.jwt.JWT; + +import com.auth0.jwt.algorithms.Algorithm; +import de.truzzt.clearinghouse.edc.multipart.message.ClearingHouseAppRequest; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; +import de.truzzt.clearinghouse.edc.multipart.sender.ClearingHouseAppSender; +import de.truzzt.clearinghouse.edc.multipart.sender.delegate.LoggingMessageSenderDelegate; +import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; + +import de.truzzt.clearinghouse.edc.multipart.util.JWTUtil; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.NotNull; + +import java.time.LocalDateTime; + +import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.createMultipartResponse; +import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.messageProcessedNotification; + +public class LogMessageHandler implements Handler { + + private final Monitor monitor; + private final IdsId connectorId; + private final ClearingHouseAppSender clearingHouseAppSender; + private final TypeManagerUtil typeManagerUtil; + private final LoggingMessageSenderDelegate senderDelegate; + + public LogMessageHandler(Monitor monitor, + IdsId connectorId, + TypeManagerUtil typeManagerUtil, + ClearingHouseAppSender clearingHouseAppSender) { + this.monitor = monitor; + this.connectorId = connectorId; + this.typeManagerUtil = typeManagerUtil; + this.clearingHouseAppSender = clearingHouseAppSender; + + this.senderDelegate = new LoggingMessageSenderDelegate(typeManagerUtil); + } + + @Override + public boolean canHandle(@NotNull MultipartRequest multipartRequest) { + return multipartRequest.getHeader().getType().equals("ids:LogMessage"); + } + + @Override + public @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest) { + var header = multipartRequest.getHeader(); + + var baseUrl = "http://localhost:8000"; // TODO Move to a configuration + var url = senderDelegate.buildRequestUrl(baseUrl, multipartRequest); + + var tokenValue = multipartRequest.getHeader().getSecurityToken().getTokenValue(); + + // TODO Move to a shared class + // TODO Validate if tokenFormat is JWT + + var decodedDat = JWT.decode(tokenValue); + var claimedClientId = decodedDat.getSubject(); + + // TODO Validate if token subject is null + + var issuedAt = LocalDateTime.now(); + var expiresAt = issuedAt.plusSeconds(60); // Config + + var jwtToken = JWT.create() + .withAudience("1") // TODO Move to a configuration + .withIssuer("1") // TODO Move to a configuration + .withClaim("client_id", claimedClientId) + .withIssuedAt(JWTUtil.convertLocalDateTime(issuedAt)) + .withExpiresAt(JWTUtil.convertLocalDateTime(expiresAt)) + .sign(Algorithm.HMAC256("123")); // TODO Move to a configuration + + var body = senderDelegate.buildRequestBody(multipartRequest); + + var request = ClearingHouseAppRequest.Builder.newInstance().url(url).token(jwtToken).body(body).build(); + + var response = clearingHouseAppSender.send(request, senderDelegate); + + return createMultipartResponse(messageProcessedNotification(header, connectorId), response); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/QueryMessageHandler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/QueryMessageHandler.java new file mode 100644 index 0000000..d65093c --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/QueryMessageHandler.java @@ -0,0 +1,34 @@ +package de.truzzt.clearinghouse.edc.multipart.handler; + +import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; +import de.truzzt.clearinghouse.edc.multipart.sender.ClearingHouseAppSender; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.NotNull; + +public class QueryMessageHandler implements Handler { + + private final Monitor monitor; + private final IdsId connectorId; + private final ClearingHouseAppSender clearingHouseAppSender; + + public QueryMessageHandler(Monitor monitor, + IdsId connectorId, + ClearingHouseAppSender clearingHouseAppSender) { + this.monitor = monitor; + this.connectorId = connectorId; + this.clearingHouseAppSender = clearingHouseAppSender; + } + + @Override + public boolean canHandle(@NotNull MultipartRequest multipartRequest) { + return multipartRequest.getHeader().getType().equals("ids:QueryMessage"); + } + + @Override + public @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest) { + throw new EdcException("Handler not implemented !"); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/RequestMessageHandler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/RequestMessageHandler.java new file mode 100644 index 0000000..701d169 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/RequestMessageHandler.java @@ -0,0 +1,34 @@ +package de.truzzt.clearinghouse.edc.multipart.handler; + +import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; +import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; +import de.truzzt.clearinghouse.edc.multipart.sender.ClearingHouseAppSender; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.NotNull; + +public class RequestMessageHandler implements Handler { + + private final Monitor monitor; + private final IdsId connectorId; + private final ClearingHouseAppSender clearingHouseAppSender; + + public RequestMessageHandler(Monitor monitor, + IdsId connectorId, + ClearingHouseAppSender clearingHouseAppSender) { + this.monitor = monitor; + this.connectorId = connectorId; + this.clearingHouseAppSender = clearingHouseAppSender; + } + + @Override + public boolean canHandle(@NotNull MultipartRequest multipartRequest) { + return multipartRequest.getHeader().getType().equals("ids:RequestMessage"); + } + + @Override + public @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest) { + throw new EdcException("Handler not implemented !"); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/ClearingHouseAppRequest.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/ClearingHouseAppRequest.java new file mode 100644 index 0000000..2d2538a --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/ClearingHouseAppRequest.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.message; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class ClearingHouseAppRequest { + + private final String url; + private final String token; + private final B body; + + private ClearingHouseAppRequest(@NotNull String url, @NotNull String token, @NotNull B body) { + this.url = url; + this.token = token; + this.body = body; + } + + public String getUrl() { + return url; + } + + public String getToken() { + return token; + } + + public B getBody() { + return body; + } + + public static class Builder { + + private String url; + private String token; + private R body; + + private Builder() { + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder url(@NotNull String url) { + this.url = url; + return this; + } + + public Builder token(@NotNull String token) { + this.token = token; + return this; + } + + public Builder body(@NotNull R body) { + this.body = body; + return this; + } + + public ClearingHouseAppRequest build() { + Objects.requireNonNull(url, "ClearingHouseApp request url is null."); + Objects.requireNonNull(token, "ClearingHouseApp request token is null."); + Objects.requireNonNull(body, "ClearingHouseApp request body is null."); + + return new ClearingHouseAppRequest(url, token, body); + } + } + +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java index ad7c2d9..9d8bd8e 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java @@ -14,9 +14,7 @@ package de.truzzt.clearinghouse.edc.multipart.message; - -import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; -import org.eclipse.edc.spi.iam.ClaimToken; +import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,36 +22,34 @@ public class MultipartRequest { - private final LogMessage header; + private final String pid; + private final Message header; private final String payload; - private final ClaimToken claimToken; - private MultipartRequest(@NotNull LogMessage header, @Nullable String payload, ClaimToken claimToken) { + + private MultipartRequest(String pid, @NotNull Message header, @Nullable String payload) { + this.pid = pid; this.header = header; this.payload = payload; - this.claimToken = claimToken; } - @NotNull - public LogMessage getHeader() { + public String getPid() { + return pid; + } + + public Message getHeader() { return header; } - @Nullable public String getPayload() { return payload; } - @Nullable - public ClaimToken getClaimToken() { - return claimToken; - } - public static class Builder { - private LogMessage header; + private String pid; + private Message header; private String payload; - private ClaimToken claimToken; private Builder() { } @@ -62,25 +58,25 @@ public static Builder newInstance() { return new Builder(); } - public Builder header(@NotNull LogMessage header) { - this.header = header; + public Builder pid(@NotNull String pid) { + this.pid = pid; return this; } - public Builder payload(@Nullable String payload) { - this.payload = payload; + public Builder header(@NotNull Message header) { + this.header = header; return this; } - public Builder claimToken(@NotNull ClaimToken claimToken) { - this.claimToken = claimToken; + public Builder payload(@NotNull String payload) { + this.payload = payload; return this; } public MultipartRequest build() { Objects.requireNonNull(header, "Multipart request header is null."); - Objects.requireNonNull(claimToken, "Multipart request claim token is null."); - return new MultipartRequest(header, payload, claimToken); + + return new MultipartRequest(pid, header, payload); } } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java index 97d7e33..e709f73 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java @@ -14,8 +14,8 @@ package de.truzzt.clearinghouse.edc.multipart.message; -import de.truzzt.clearinghouse.edc.multipart.types.ids.DynamicAttributeToken; -import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; +import de.truzzt.clearinghouse.edc.multipart.types.ids.SecurityToken; +import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -24,16 +24,16 @@ public class MultipartResponse { - private final LogMessage header; + private final Message header; private final Object payload; - private MultipartResponse(@NotNull LogMessage header, @Nullable Object payload) { + private MultipartResponse(@NotNull Message header, @Nullable Object payload) { this.header = header; this.payload = payload; } @NotNull - public LogMessage getHeader() { + public Message getHeader() { return header; } @@ -42,13 +42,13 @@ public Object getPayload() { return payload; } - public void setSecurityToken(Function getToken) { + public void setSecurityToken(Function getToken) { getHeader().setSecurityToken(getToken.apply(getHeader())); } public static class Builder { - private LogMessage header; + private Message header; private Object payload; private Builder() { @@ -58,7 +58,7 @@ public static Builder newInstance() { return new Builder(); } - public Builder header(@Nullable LogMessage header) { + public Builder header(@Nullable Message header) { this.header = header; return this; } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/ClearingHouseAppSender.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/ClearingHouseAppSender.java new file mode 100644 index 0000000..e44c667 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/ClearingHouseAppSender.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.truzzt.clearinghouse.edc.multipart.sender; + +import de.truzzt.clearinghouse.edc.multipart.message.ClearingHouseAppRequest; +import de.truzzt.clearinghouse.edc.multipart.sender.delegate.SenderDelegate; +import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; +import jakarta.ws.rs.core.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; + +import static java.lang.String.format; + +public class ClearingHouseAppSender { + + private final Monitor monitor; + private final EdcHttpClient httpClient; + private final TypeManagerUtil typeManagerUtil; + + public ClearingHouseAppSender(Monitor monitor, + EdcHttpClient httpClient, + TypeManagerUtil typeManagerUtil) { + this.monitor = monitor; + this.httpClient = httpClient; + this.typeManagerUtil = typeManagerUtil; + } + + public P send(ClearingHouseAppRequest request, SenderDelegate senderDelegate) { + + var json = typeManagerUtil.toJson(request.getBody()); + + var requestBody = RequestBody.create(json, + okhttp3.MediaType.get(MediaType.APPLICATION_JSON)); + + var httpRequest = new Request.Builder() + .url(request.getUrl()) + .addHeader("Ch-Service", request.getToken()) + .addHeader("Content-Type", MediaType.APPLICATION_JSON) + .post(requestBody) + .build(); + + Response response; + try { + response = httpClient.execute(httpRequest); + monitor.debug("Response received from Clearing House App. Status: " + response.code()); + + } catch (java.io.IOException e) { + throw new EdcException("Error sending request to Clearing House App", e); + } + + if (response.isSuccessful()) { + try (var body = response.body()) { + if (body == null) { + throw new EdcException("Received an empty response body from Clearing House App"); + } else { + monitor.debug("Response received from Clearing House App. Body: " + body); + + var responseBody = senderDelegate.parseResponseBody(body); + return responseBody; + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + throw new EdcException(format("Received an error from Clearing House App. Status: %s, message: %s", response.code(), response.message())); + } + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/LoggingMessageSenderDelegate.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/LoggingMessageSenderDelegate.java new file mode 100644 index 0000000..2d86a37 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/LoggingMessageSenderDelegate.java @@ -0,0 +1,60 @@ +package de.truzzt.clearinghouse.edc.multipart.sender.delegate; + +import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; +import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.Context; +import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.SecurityToken; +import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.LoggingMessageRequest; +import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.LoggingMessageResponse; +import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.RequestHeader; +import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.TokenFormat; +import okhttp3.ResponseBody; + +public class LoggingMessageSenderDelegate implements SenderDelegate { + + private final TypeManagerUtil typeManagerUtil; + + public LoggingMessageSenderDelegate(TypeManagerUtil typeManagerUtil) { + this.typeManagerUtil = typeManagerUtil; + } + + @Override + public String buildRequestUrl(String baseUrl, MultipartRequest multipartRequest) { + return baseUrl + "/messages/log/" + multipartRequest.getPid(); + } + + @Override + public LoggingMessageRequest buildRequestBody(MultipartRequest multipartRequest) { + var header = multipartRequest.getHeader(); + + var multipartContext = header.getContext(); + var context = new Context(multipartContext.getIds(), multipartContext.getIdsc()); + + var multipartSecurityToken = header.getSecurityToken(); + var multipartTokenFormat = multipartSecurityToken.getTokenFormat(); + var securityToken = SecurityToken.Builder.newInstance(). + type(multipartSecurityToken.getType()). + id(multipartSecurityToken.getId()). + tokenFormat(new TokenFormat(multipartTokenFormat.getId())). + tokenValue(multipartSecurityToken.getTokenValue()). + build(); + + var requestHeader = RequestHeader.Builder.newInstance() + .context(context) + .id(header.getId()) + .type(header.getType()) + .securityToken(securityToken) + .issuerConnector(header.getIssuerConnector()) + .modelVersion(header.getModelVersion()) + .issued(header.getIssued()) + .senderAgent(header.getSenderAgent()) + .build(); + + return new LoggingMessageRequest(requestHeader, multipartRequest.getPayload()); + } + + @Override + public LoggingMessageResponse parseResponseBody(ResponseBody responseBody) { + return typeManagerUtil.parse(responseBody.byteStream(), LoggingMessageResponse.class); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/SenderDelegate.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/SenderDelegate.java new file mode 100644 index 0000000..51f86fd --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/SenderDelegate.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 sovity GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * sovity GmbH - initial API and implementation + * + */ +package de.truzzt.clearinghouse.edc.multipart.sender.delegate; + +import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; +import okhttp3.ResponseBody; + +public interface SenderDelegate { + + String buildRequestUrl(String baseUrl, MultipartRequest request); + + R buildRequestBody(MultipartRequest request); + + P parseResponseBody(ResponseBody responseBody); +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java index c997a26..936ace7 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java @@ -16,14 +16,10 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import de.truzzt.clearinghouse.edc.multipart.types.ids.DynamicAttributeToken; -import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; -import de.truzzt.clearinghouse.edc.multipart.types.jwt.JwtPayload; import org.eclipse.edc.spi.EdcException; import java.io.IOException; import java.io.InputStream; -import java.util.Base64; public class TypeManagerUtil { @@ -33,22 +29,11 @@ public TypeManagerUtil(ObjectMapper mapper) { this.mapper = mapper; } - public LogMessage parseMessage(InputStream streamToken) { + public T parse(InputStream inputStream, Class type) { try { - return mapper.readValue(streamToken, LogMessage.class); + return mapper.readValue(inputStream, type); } catch (IOException e) { - throw new EdcException("Error parsing Header to Message", e); - } - } - - public JwtPayload parseToken(DynamicAttributeToken token) { - try { - Base64.Decoder decoder = Base64.getUrlDecoder(); - String[] chunks = token.getTokenValue().split("\\."); - return mapper.readValue(decoder.decode(chunks[1]), JwtPayload.class); - - } catch (IOException e) { - throw new EdcException("Error parsing Token", e); + throw new EdcException("Error parsing to type " + type, e); } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/Context.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/Context.java new file mode 100644 index 0000000..f800dd2 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/Context.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +public class Context { + @JsonProperty("ids") + @NotNull + private String ids; + + @JsonProperty("idsc") + @NotNull + private String idsc; + + public Context(@NotNull String ids, @NotNull String idsc) { + this.ids = ids; + this.idsc = idsc; + } + + public String getIds() { + return ids; + } + + public String getIdsc() { + return idsc; + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageRequest.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageRequest.java new file mode 100644 index 0000000..62e32c9 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +public class LoggingMessageRequest { + + @JsonProperty("header") + @NotNull + private RequestHeader header; + + @JsonProperty("payload") + @NotNull + private String payload; + + public LoggingMessageRequest(@NotNull RequestHeader header, @NotNull String payload) { + this.header = header; + this.payload = payload; + } +} + diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageResponse.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageResponse.java new file mode 100644 index 0000000..110f56c --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageResponse.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +public class LoggingMessageResponse { + + @JsonProperty("data") + @NotNull + private String data; + + public String getData() { + return data; + } + +} \ No newline at end of file diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/RequestHeader.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/RequestHeader.java new file mode 100644 index 0000000..04a3869 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/RequestHeader.java @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +import javax.xml.datatype.XMLGregorianCalendar; +import java.net.URI; +import java.util.Objects; + +public class RequestHeader { + + @JsonProperty("@context") + @NotNull + private final Context context; + + @JsonProperty("@id") + @NotNull + private final String id; + + @JsonProperty("@type") + @NotNull + private final String type; + + @JsonProperty("securityToken") + @NotNull + private final SecurityToken securityToken; + + @JsonProperty("issuerConnector") + @NotNull + private final String issuerConnector; + + @JsonProperty("modelVersion") + @NotNull + private final String modelVersion; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSzzz") + @NotNull + @JsonProperty("issued") + private final XMLGregorianCalendar issued; + + @JsonProperty("senderAgent") + @NotNull + private final String senderAgent; + + private RequestHeader(@NotNull Context context, + @NotNull String id, + @NotNull String type, + @NotNull SecurityToken securityToken, + @NotNull String issuerConnector, + @NotNull String modelVersion, + @NotNull XMLGregorianCalendar issued, + @NotNull String senderAgent) { + this.context = context; + this.id = id; + this.type = type; + this.securityToken = securityToken; + this.issuerConnector = issuerConnector; + this.modelVersion = modelVersion; + this.issued = issued; + this.senderAgent = senderAgent; + } + + public Context getContext() { + return context; + } + + public String getId() { + return id; + } + + public String getType() { + return type; + } + + public SecurityToken getSecurityToken() { + return securityToken; + } + + public String getIssuerConnector() { + return issuerConnector; + } + + public String getModelVersion() { + return modelVersion; + } + + public XMLGregorianCalendar getIssued() { + return issued; + } + + public String getSenderAgent() { + return senderAgent; + } + + public static class Builder { + + private Context context; + private String id; + private String type; + private SecurityToken securityToken; + private String issuerConnector; + private String modelVersion; + private XMLGregorianCalendar issued; + private String senderAgent; + + private Builder() { + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder context(@NotNull Context context) { + this.context = context; + return this; + } + + public Builder id(@NotNull URI id) { + this.id = id.toString(); + return this; + } + + public Builder type(@NotNull String type) { + this.type = type; + return this; + } + + public Builder securityToken(@NotNull SecurityToken securityToken) { + this.securityToken = securityToken; + return this; + } + + public Builder issuerConnector(@NotNull URI issuerConnector) { + this.issuerConnector = issuerConnector.toString(); + return this; + } + + public Builder modelVersion(@NotNull String modelVersion) { + this.modelVersion = modelVersion; + return this; + } + + public Builder issued(@NotNull XMLGregorianCalendar issued) { + this.issued = issued; + return this; + } + + public Builder senderAgent(@NotNull URI senderAgent) { + this.senderAgent = senderAgent.toString(); + return this; + } + + public RequestHeader build() { + Objects.requireNonNull(context, "Logging message request header context null."); + Objects.requireNonNull(id, "Logging message request header id is null."); + Objects.requireNonNull(type, "Logging message request header type is null."); + Objects.requireNonNull(securityToken, "Logging message request header security token is null."); + + Objects.requireNonNull(issuerConnector, "Logging message request header issuer connector is null."); + Objects.requireNonNull(modelVersion, "Logging message request header model version is null."); + Objects.requireNonNull(issued, "Logging message request header issued is null."); + Objects.requireNonNull(senderAgent, "Logging message request header sender agent is null."); + + return new RequestHeader(context, id, type, securityToken, issuerConnector, modelVersion, issued, senderAgent); + } + } +} + diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/SecurityToken.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/SecurityToken.java new file mode 100644 index 0000000..41cfd1b --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/SecurityToken.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; +import java.util.Objects; + +public class SecurityToken { + + @JsonProperty("@type") + @NotNull + private String type; + + @JsonProperty("@id") + @NotNull + private String id; + + @JsonProperty("tokenFormat") + @NotNull + private TokenFormat tokenFormat; + + @JsonProperty("tokenValue") + @NotNull + private String tokenValue; + + private SecurityToken(@NotNull String type, + @NotNull String id, + @NotNull TokenFormat tokenFormat, + @NotNull String tokenValue) { + this.type = type; + this.id = id; + this.tokenFormat = tokenFormat; + this.tokenValue = tokenValue; + } + + public String getType() { + return type; + } + + public String getId() { + return id; + } + + public TokenFormat getTokenFormat() { + return tokenFormat; + } + + public String getTokenValue() { + return tokenValue; + } + + public static class Builder { + + private String type; + private String id; + private TokenFormat tokenFormat; + private String tokenValue; + + private Builder() { + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder type(@NotNull String type) { + this.type = type; + return this; + } + + public Builder id(@NotNull URI id) { + this.id = id.toString(); + return this; + } + + public Builder tokenFormat(@NotNull TokenFormat tokenFormat) { + this.tokenFormat = tokenFormat; + return this; + } + + public Builder tokenValue(@NotNull String tokenValue) { + this.tokenValue = tokenValue; + return this; + } + + public SecurityToken build() { + Objects.requireNonNull(type, "Security token type is null."); + Objects.requireNonNull(id, "Security token id is null."); + Objects.requireNonNull(tokenFormat, "Security token tokenFormat is null."); + Objects.requireNonNull(tokenValue, "Security token tokenValue is null."); + + return new SecurityToken(type, id, tokenFormat, tokenValue); + } + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/jwt/JwtPayload.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/TokenFormat.java similarity index 53% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/jwt/JwtPayload.java rename to clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/TokenFormat.java index 0de5032..5d6b54f 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/jwt/JwtPayload.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/TokenFormat.java @@ -9,26 +9,27 @@ * * Contributors: * Microsoft Corporation - Initial implementation - * truzzt GmbH - PostgreSQL implementation * */ -package de.truzzt.clearinghouse.edc.multipart.types.jwt; +package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; -import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; -public class JwtPayload { +import java.net.URI; +public class TokenFormat { + + @JsonProperty("@id") @NotNull - @JsonAlias({"https://w3id.org/idsa/core/sub", "ids:sub", "sub"}) - private String sub; + private final String id; - public String getSub() { - return sub; + public TokenFormat(@NotNull URI id) { + this.id = id.toString(); } - public void setSub(String sub) { - this.sub = sub; + public String getId() { + return id; } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java index 71bda98..11ac37d 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java @@ -14,23 +14,23 @@ package de.truzzt.clearinghouse.edc.multipart.types.ids; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + public class Context { + @JsonProperty("ids") + @NotNull private String ids; + + @JsonProperty("idsc") + @NotNull private String idsc; public String getIds() { return ids; } - public void setIds(String ids) { - this.ids = ids; - } - public String getIdsc() { return idsc; } - - public void setIdsc(String idsc) { - this.idsc = idsc; - } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/LogMessage.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java similarity index 75% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/LogMessage.java rename to clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java index e8f259e..c1a8d64 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/LogMessage.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java @@ -14,18 +14,14 @@ package de.truzzt.clearinghouse.edc.multipart.types.ids; -import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; import javax.xml.datatype.XMLGregorianCalendar; import java.net.URI; -import java.util.Map; - -public class LogMessage { +public class Message { @JsonProperty("@context") @NotNull @@ -39,41 +35,31 @@ public class LogMessage { @NotNull private String type; - @NotNull @JsonProperty("ids:securityToken") - @JsonAlias({"ids:securityToken", "securityToken"}) - private DynamicAttributeToken securityToken; - @NotNull + private SecurityToken securityToken; + @JsonProperty("ids:issuerConnector") - @JsonAlias({"ids:issuerConnector", "issuerConnector"}) + @NotNull private URI issuerConnector; - @NotNull @JsonProperty("ids:modelVersion") - @JsonAlias({"ids:modelVersion", "modelVersion"}) + @NotNull String modelVersion; + @JsonProperty("ids:issued") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSzzz") @NotNull - @JsonProperty("ids:issued") - @JsonAlias({"ids:issued", "issued"}) XMLGregorianCalendar issued; - - @NotNull @JsonProperty("ids:senderAgent") - @JsonAlias({"ids:senderAgent", "senderAgent"}) + @NotNull private URI senderAgent; - // all classes have a generic property array - @JsonIgnore - protected Map properties; - - public LogMessage() { + public Message() { } - public LogMessage(URI id) { + public Message(URI id) { this.id = id; } @@ -81,10 +67,6 @@ public URI getId() { return id; } - public void setId(URI id) { - this.id = id; - } - public String getType() { return type; } @@ -117,11 +99,11 @@ public void setIssued(XMLGregorianCalendar issued) { this.issued = issued; } - public DynamicAttributeToken getSecurityToken() { + public SecurityToken getSecurityToken() { return securityToken; } - public void setSecurityToken(DynamicAttributeToken securityToken) { + public void setSecurityToken(SecurityToken securityToken) { this.securityToken = securityToken; } @@ -141,4 +123,3 @@ public void setContext(Context context) { this.context = context; } } - diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java index 666b130..30e0d7b 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java @@ -14,19 +14,21 @@ package de.truzzt.clearinghouse.edc.multipart.types.ids; -import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; import java.net.URI; -public class RejectionMessage extends LogMessage { +public class RejectionMessage extends Message { - @JsonAlias({"https://w3id.org/idsa/core/rejectionReason", "ids:rejectionReason", "rejectionReason"}) + @JsonProperty("ids:rejectionReason") + @NotNull RejectionReason rejectionReason; public RejectionMessage() { } - public RejectionMessage(URI id) { + public RejectionMessage(@NotNull URI id) { super(id); } @@ -34,7 +36,7 @@ public RejectionReason getRejectionReason() { return rejectionReason; } - public void setRejectionReason(RejectionReason rejectionReason) { + public void setRejectionReason(@NotNull RejectionReason rejectionReason) { this.rejectionReason = rejectionReason; } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java index 4960f7d..ac8a94a 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java @@ -14,7 +14,6 @@ package de.truzzt.clearinghouse.edc.multipart.types.ids; -import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeName; @@ -27,20 +26,19 @@ public class RejectionReason { @JsonProperty("@id") - @JsonAlias({"@id", "id"}) @NotNull private URI id; public RejectionReason() { } - public RejectionReason(URI id) { + public RejectionReason(@NotNull URI id) { this.id = id; } public static final RejectionReason BAD_PARAMETERS = new RejectionReason(URI.create("https://w3id.org/idsa/code/BAD_PARAMETERS")); - public static final RejectionReason INTERNAL_RECIPIENT_ERROR = + public static final RejectionReason INTERNAL_RECIPIENT_ERROR = new RejectionReason(URI.create("https://w3id.org/idsa/code/INTERNAL_RECIPIENT_ERROR")); public static final RejectionReason MALFORMED_MESSAGE = diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/DynamicAttributeToken.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/SecurityToken.java similarity index 55% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/DynamicAttributeToken.java rename to clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/SecurityToken.java index b8770bc..f7516bb 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/DynamicAttributeToken.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/SecurityToken.java @@ -14,61 +14,47 @@ package de.truzzt.clearinghouse.edc.multipart.types.ids; -import com.fasterxml.jackson.annotation.JsonAlias; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeName; import de.truzzt.clearinghouse.edc.multipart.types.ids.util.VocabUtil; import org.jetbrains.annotations.NotNull; import java.net.URI; -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "@type") -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonTypeName("ids:DynamicAttributeToken") -public class DynamicAttributeToken { +public class SecurityToken { + + @JsonProperty("@type") + @NotNull + private String type; @JsonProperty("@id") - @JsonAlias({"@id", "id"}) @NotNull private URI id; + @JsonProperty("ids:tokenFormat") @NotNull - @JsonAlias({"ids:tokenFormat", "tokenFormat"}) private TokenFormat tokenFormat; + @JsonProperty("ids:tokenValue") @NotNull - @JsonAlias({"ids:tokenValue", "tokenValue"}) private String tokenValue; - private DynamicAttributeToken() { + private SecurityToken() { id = VocabUtil.createRandomUrl("dynamicAttributeToken"); } - @JsonProperty("@id") - public URI getId() { - return id; + public String getType() { + return type; } - public void setId(URI id) { - this.id = id; + public URI getId() { + return id; } public TokenFormat getTokenFormat() { return tokenFormat; } - public void setTokenFormat(TokenFormat tokenFormat) { - this.tokenFormat = tokenFormat; - } - public String getTokenValue() { return tokenValue; } - - public void setTokenValue(String tokenValue) { - this.tokenValue = tokenValue; - } } - diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java index ac377aa..0b9db00 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java @@ -14,36 +14,18 @@ package de.truzzt.clearinghouse.edc.multipart.types.ids; -import com.fasterxml.jackson.annotation.JsonAlias; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonTypeName; import org.jetbrains.annotations.NotNull; import java.net.URI; -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonTypeName("ids:tokenFormat") public class TokenFormat { - @JsonProperty("@type") - @NotNull - private String type; - @JsonProperty("@id") - @JsonAlias({"@id", "id"}) @NotNull private URI id; public URI getId() { return id; } - - public void setId(URI id) { - this.id = id; - } - - public String getType() { - return "ids:tokenFormat"; - } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/JWTUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/JWTUtil.java new file mode 100644 index 0000000..070aa49 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/JWTUtil.java @@ -0,0 +1,12 @@ +package de.truzzt.clearinghouse.edc.multipart.util; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +public class JWTUtil { + + public static Date convertLocalDateTime(LocalDateTime localDateTime) { + return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java index 03c6844..9cfdecc 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java @@ -1,7 +1,7 @@ package de.truzzt.clearinghouse.edc.multipart.util; import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; -import de.truzzt.clearinghouse.edc.multipart.types.ids.LogMessage; +import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; import de.truzzt.clearinghouse.edc.multipart.types.ids.RejectionMessage; import de.truzzt.clearinghouse.edc.multipart.types.ids.RejectionReason; import org.eclipse.edc.protocol.ids.spi.domain.IdsConstants; @@ -18,23 +18,22 @@ import java.util.GregorianCalendar; import java.util.UUID; -import static org.eclipse.edc.protocol.ids.util.CalendarUtil.gregorianNow; - public class ResponseUtil { private static final String PROCESSED_NOTIFICATION_TYPE = "ids:MessageProcessedNotificationMessage"; - public static MultipartResponse createMultipartResponse(@NotNull LogMessage header) { + public static MultipartResponse createMultipartResponse(@NotNull Message header, @NotNull Object payload) { return MultipartResponse.Builder.newInstance() .header(header) + .payload(payload) .build(); } - public static LogMessage messageProcessedNotification(@NotNull LogMessage correlationMessage, + public static Message messageProcessedNotification(@NotNull Message correlationMessage, @NotNull IdsId connectorId) { var messageId = getMessageId(); - LogMessage message = new LogMessage(messageId); + Message message = new Message(messageId); message.setContext(correlationMessage.getContext()); message.setType(PROCESSED_NOTIFICATION_TYPE); message.setSecurityToken(correlationMessage.getSecurityToken()); @@ -47,7 +46,7 @@ public static LogMessage messageProcessedNotification(@NotNull LogMessage correl } @NotNull - public static RejectionMessage notAuthenticated(@NotNull LogMessage correlationMessage, + public static RejectionMessage notAuthenticated(@NotNull Message correlationMessage, @NotNull IdsId connectorId) { RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); rejectionMessage.setRejectionReason(RejectionReason.NOT_AUTHENTICATED); @@ -56,7 +55,7 @@ public static RejectionMessage notAuthenticated(@NotNull LogMessage correlationM } @NotNull - public static RejectionMessage malformedMessage(@Nullable LogMessage correlationMessage, + public static RejectionMessage malformedMessage(@Nullable Message correlationMessage, @NotNull IdsId connectorId) { RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); rejectionMessage.setRejectionReason(RejectionReason.MALFORMED_MESSAGE); @@ -65,7 +64,7 @@ public static RejectionMessage malformedMessage(@Nullable LogMessage correlation } @NotNull - public static RejectionMessage messageTypeNotSupported(@NotNull LogMessage correlationMessage, + public static RejectionMessage messageTypeNotSupported(@NotNull Message correlationMessage, @NotNull IdsId connectorId) { RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); rejectionMessage.setRejectionReason(RejectionReason.MESSAGE_TYPE_NOT_SUPPORTED); @@ -74,7 +73,7 @@ public static RejectionMessage messageTypeNotSupported(@NotNull LogMessage corre } @NotNull - public static RejectionMessage badParameters(@NotNull LogMessage correlationMessage, + public static RejectionMessage badParameters(@NotNull Message correlationMessage, @NotNull IdsId connectorId) { RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); rejectionMessage.setRejectionReason(RejectionReason.BAD_PARAMETERS); @@ -83,7 +82,7 @@ public static RejectionMessage badParameters(@NotNull LogMessage correlationMess } @NotNull - private static RejectionMessage createRejectionMessage(@Nullable LogMessage correlationMessage, + private static RejectionMessage createRejectionMessage(@Nullable Message correlationMessage, @NotNull IdsId connectorId) { var messageId = getMessageId(); diff --git a/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index ae7a3a9..479afa7 100644 --- a/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -12,4 +12,4 @@ # # -de.truzzt.clearinghouse.edc.multipart.MultipartExtension \ No newline at end of file +de.truzzt.clearinghouse.edc.multipart.ClearingHouseMultipartExtension \ No newline at end of file From 2b4ab0208e7634f7b60a243e09d7bcb9aea7a2c3 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 28 Sep 2023 11:39:42 -0300 Subject: [PATCH 074/183] feat (ch-edc): refactoring modules extension and core --- clearing-house-edc/{spi => }/build.gradle.kts | 14 ++-- clearing-house-edc/core/build.gradle.kts | 8 ++ .../clearinghouse/edc/app/AppSender.java} | 34 ++++---- .../edc/app/delegate/AppSenderDelegate.java} | 9 +- .../app/delegate/LoggingMessageDelegate.java} | 38 ++++----- .../de/truzzt/clearinghouse/edc/core/.gitkeep | 0 .../edc/dto/AppSenderRequest.java} | 16 ++-- .../edc/dto/HandlerRequest.java} | 13 ++- .../edc/dto/HandlerResponse.java} | 14 ++-- .../edc/dto}/LoggingMessageRequest.java | 7 +- .../edc/dto}/LoggingMessageResponse.java | 2 +- .../clearinghouse/edc/handler/Handler.java | 53 ++++++++++++ .../edc/handler/LogMessageHandler.java | 55 ++++++++++++ .../edc}/types/TypeManagerUtil.java | 2 +- .../edc}/types/clearinghouse/Context.java | 2 +- .../edc/types/clearinghouse/Header.java} | 24 +++--- .../types/clearinghouse/SecurityToken.java | 2 +- .../edc}/types/clearinghouse/TokenFormat.java | 2 +- .../clearinghouse/edc}/types/ids/Context.java | 2 +- .../clearinghouse/edc}/types/ids/Message.java | 2 +- .../edc}/types/ids/RejectionMessage.java | 2 +- .../edc}/types/ids/RejectionReason.java | 2 +- .../edc}/types/ids/SecurityToken.java | 4 +- .../edc}/types/ids/TokenFormat.java | 4 +- .../edc}/types/ids/util/VocabUtil.java | 2 +- .../clearinghouse/edc}/util/ResponseUtil.java | 14 ++-- .../extensions/multipart/build.gradle.kts | 4 +- ...ntroller.java => MultipartController.java} | 38 ++++----- ...Extension.java => MultipartExtension.java} | 21 ++--- .../edc/multipart/handler/Handler.java | 12 --- .../multipart/handler/LogMessageHandler.java | 84 ------------------- .../handler/QueryMessageHandler.java | 34 -------- .../handler/RequestMessageHandler.java | 34 -------- .../edc/multipart/util/JWTUtil.java | 12 --- ...rg.eclipse.edc.spi.system.ServiceExtension | 2 +- clearing-house-edc/gradle.properties | 3 +- .../launchers/connector/Dockerfile | 16 ++++ .../de/truzzt/clearinghouse/edc/spi/.gitkeep | 0 38 files changed, 266 insertions(+), 321 deletions(-) rename clearing-house-edc/{spi => }/build.gradle.kts (57%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/ClearingHouseAppSender.java => core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java} (64%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/SenderDelegate.java => core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java} (60%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/LoggingMessageSenderDelegate.java => core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java} (50%) delete mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/core/.gitkeep rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/ClearingHouseAppRequest.java => core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java} (71%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java => core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerRequest.java} (81%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java => core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java} (79%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse => core/src/main/java/de/truzzt/clearinghouse/edc/dto}/LoggingMessageRequest.java (76%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse => core/src/main/java/de/truzzt/clearinghouse/edc/dto}/LoggingMessageResponse.java (90%) create mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java create mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/TypeManagerUtil.java (95%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/clearinghouse/Context.java (92%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/RequestHeader.java => core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Header.java} (86%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/clearinghouse/SecurityToken.java (97%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/clearinghouse/TokenFormat.java (91%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/ids/Context.java (92%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/ids/Message.java (97%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/ids/RejectionMessage.java (94%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/ids/RejectionReason.java (96%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/ids/SecurityToken.java (90%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/ids/TokenFormat.java (85%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/types/ids/util/VocabUtil.java (95%) rename clearing-house-edc/{extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart => core/src/main/java/de/truzzt/clearinghouse/edc}/util/ResponseUtil.java (89%) rename clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/{ClearingHouseMultipartController.java => MultipartController.java} (77%) rename clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/{ClearingHouseMultipartExtension.java => MultipartExtension.java} (62%) delete mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/Handler.java delete mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogMessageHandler.java delete mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/QueryMessageHandler.java delete mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/RequestMessageHandler.java delete mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/JWTUtil.java create mode 100644 clearing-house-edc/launchers/connector/Dockerfile delete mode 100644 clearing-house-edc/spi/src/main/java/de/truzzt/clearinghouse/edc/spi/.gitkeep diff --git a/clearing-house-edc/spi/build.gradle.kts b/clearing-house-edc/build.gradle.kts similarity index 57% rename from clearing-house-edc/spi/build.gradle.kts rename to clearing-house-edc/build.gradle.kts index 1d90441..b9a452e 100644 --- a/clearing-house-edc/spi/build.gradle.kts +++ b/clearing-house-edc/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2022 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,7 +8,7 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - Initial implementation + * Microsoft Corporation - initial implementation * */ @@ -16,6 +16,10 @@ plugins { `java-library` } -dependencies { - api(edc.spi.core) -} +val javaVersion: String by project + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(javaVersion) + } +} \ No newline at end of file diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 1d90441..38506d5 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -16,6 +16,14 @@ plugins { `java-library` } +val auth0JWTVersion: String by project + dependencies { api(edc.spi.core) + + implementation(edc.ids) + implementation(edc.ids.jsonld.serdes) + implementation(edc.api.management.config) + + implementation("com.auth0:java-jwt:${auth0JWTVersion}") } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/ClearingHouseAppSender.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java similarity index 64% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/ClearingHouseAppSender.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java index e44c667..0c917c7 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/ClearingHouseAppSender.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java @@ -11,12 +11,12 @@ * sovity GmbH - initial API and implementation * */ -package de.truzzt.clearinghouse.edc.multipart.sender; +package de.truzzt.clearinghouse.edc.app; -import de.truzzt.clearinghouse.edc.multipart.message.ClearingHouseAppRequest; -import de.truzzt.clearinghouse.edc.multipart.sender.delegate.SenderDelegate; -import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; -import jakarta.ws.rs.core.MediaType; +import de.truzzt.clearinghouse.edc.app.delegate.AppSenderDelegate; +import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import okhttp3.MediaType; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; @@ -26,31 +26,30 @@ import static java.lang.String.format; -public class ClearingHouseAppSender { +public class AppSender { + private static final String JSON_CONTENT_TYPE = "application/json"; private final Monitor monitor; private final EdcHttpClient httpClient; private final TypeManagerUtil typeManagerUtil; - public ClearingHouseAppSender(Monitor monitor, - EdcHttpClient httpClient, - TypeManagerUtil typeManagerUtil) { + public AppSender(Monitor monitor, + EdcHttpClient httpClient, + TypeManagerUtil typeManagerUtil) { this.monitor = monitor; this.httpClient = httpClient; this.typeManagerUtil = typeManagerUtil; } - public P send(ClearingHouseAppRequest request, SenderDelegate senderDelegate) { + public P send(AppSenderRequest request, AppSenderDelegate appSenderDelegate) { var json = typeManagerUtil.toJson(request.getBody()); - - var requestBody = RequestBody.create(json, - okhttp3.MediaType.get(MediaType.APPLICATION_JSON)); + var requestBody = RequestBody.create(json, MediaType.get(JSON_CONTENT_TYPE)); var httpRequest = new Request.Builder() .url(request.getUrl()) .addHeader("Ch-Service", request.getToken()) - .addHeader("Content-Type", MediaType.APPLICATION_JSON) + .addHeader("Content-Type", JSON_CONTENT_TYPE) .post(requestBody) .build(); @@ -68,13 +67,10 @@ public P send(ClearingHouseAppRequest request, SenderDelegate sende if (body == null) { throw new EdcException("Received an empty response body from Clearing House App"); } else { - monitor.debug("Response received from Clearing House App. Body: " + body); - - var responseBody = senderDelegate.parseResponseBody(body); - return responseBody; + return appSenderDelegate.parseResponseBody(body); } } catch (Exception e) { - throw new RuntimeException(e); + throw new EdcException("Error reading Clearing House App response body", e); } } else { throw new EdcException(format("Received an error from Clearing House App. Status: %s, message: %s", response.code(), response.message())); diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/SenderDelegate.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java similarity index 60% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/SenderDelegate.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java index 51f86fd..d9c7a70 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/SenderDelegate.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java @@ -11,16 +11,11 @@ * sovity GmbH - initial API and implementation * */ -package de.truzzt.clearinghouse.edc.multipart.sender.delegate; +package de.truzzt.clearinghouse.edc.app.delegate; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; import okhttp3.ResponseBody; -public interface SenderDelegate { - - String buildRequestUrl(String baseUrl, MultipartRequest request); - - R buildRequestBody(MultipartRequest request); +public interface AppSenderDelegate { P parseResponseBody(ResponseBody responseBody); } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/LoggingMessageSenderDelegate.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java similarity index 50% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/LoggingMessageSenderDelegate.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java index 2d86a37..ecbb4c9 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/sender/delegate/LoggingMessageSenderDelegate.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java @@ -1,31 +1,29 @@ -package de.truzzt.clearinghouse.edc.multipart.sender.delegate; - -import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; -import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; -import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.Context; -import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.SecurityToken; -import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.LoggingMessageRequest; -import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.LoggingMessageResponse; -import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.RequestHeader; -import de.truzzt.clearinghouse.edc.multipart.types.clearinghouse.TokenFormat; +package de.truzzt.clearinghouse.edc.app.delegate; + +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageRequest; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.types.clearinghouse.Context; +import de.truzzt.clearinghouse.edc.types.clearinghouse.Header; +import de.truzzt.clearinghouse.edc.types.clearinghouse.SecurityToken; +import de.truzzt.clearinghouse.edc.types.clearinghouse.TokenFormat; import okhttp3.ResponseBody; -public class LoggingMessageSenderDelegate implements SenderDelegate { +public class LoggingMessageDelegate implements AppSenderDelegate { private final TypeManagerUtil typeManagerUtil; - public LoggingMessageSenderDelegate(TypeManagerUtil typeManagerUtil) { + public LoggingMessageDelegate(TypeManagerUtil typeManagerUtil) { this.typeManagerUtil = typeManagerUtil; } - @Override - public String buildRequestUrl(String baseUrl, MultipartRequest multipartRequest) { - return baseUrl + "/messages/log/" + multipartRequest.getPid(); + public String buildRequestUrl(String baseUrl, HandlerRequest handlerRequest) { + return baseUrl + "/messages/log/" + handlerRequest.getPid(); } - @Override - public LoggingMessageRequest buildRequestBody(MultipartRequest multipartRequest) { - var header = multipartRequest.getHeader(); + public LoggingMessageRequest buildRequestBody(HandlerRequest handlerRequest) { + var header = handlerRequest.getHeader(); var multipartContext = header.getContext(); var context = new Context(multipartContext.getIds(), multipartContext.getIdsc()); @@ -39,7 +37,7 @@ public LoggingMessageRequest buildRequestBody(MultipartRequest multipartRequest) tokenValue(multipartSecurityToken.getTokenValue()). build(); - var requestHeader = RequestHeader.Builder.newInstance() + var requestHeader = Header.Builder.newInstance() .context(context) .id(header.getId()) .type(header.getType()) @@ -50,7 +48,7 @@ public LoggingMessageRequest buildRequestBody(MultipartRequest multipartRequest) .senderAgent(header.getSenderAgent()) .build(); - return new LoggingMessageRequest(requestHeader, multipartRequest.getPayload()); + return new LoggingMessageRequest(requestHeader, handlerRequest.getPayload()); } @Override diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/core/.gitkeep b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/core/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/ClearingHouseAppRequest.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java similarity index 71% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/ClearingHouseAppRequest.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java index 2d2538a..a1af118 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/ClearingHouseAppRequest.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java @@ -12,19 +12,19 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.message; +package de.truzzt.clearinghouse.edc.dto; import org.jetbrains.annotations.NotNull; import java.util.Objects; -public class ClearingHouseAppRequest { +public class AppSenderRequest { private final String url; private final String token; private final B body; - private ClearingHouseAppRequest(@NotNull String url, @NotNull String token, @NotNull B body) { + private AppSenderRequest(@NotNull String url, @NotNull String token, @NotNull B body) { this.url = url; this.token = token; this.body = body; @@ -70,12 +70,12 @@ public Builder body(@NotNull R body) { return this; } - public ClearingHouseAppRequest build() { - Objects.requireNonNull(url, "ClearingHouseApp request url is null."); - Objects.requireNonNull(token, "ClearingHouseApp request token is null."); - Objects.requireNonNull(body, "ClearingHouseApp request body is null."); + public AppSenderRequest build() { + Objects.requireNonNull(url, "ClearingHouse request url is null."); + Objects.requireNonNull(token, "ClearingHouse request token is null."); + Objects.requireNonNull(body, "ClearingHouse request body is null."); - return new ClearingHouseAppRequest(url, token, body); + return new AppSenderRequest(url, token, body); } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerRequest.java similarity index 81% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerRequest.java index 9d8bd8e..554601c 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartRequest.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerRequest.java @@ -12,22 +12,21 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.message; +package de.truzzt.clearinghouse.edc.dto; -import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import de.truzzt.clearinghouse.edc.types.ids.Message; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; -public class MultipartRequest { +public class HandlerRequest { private final String pid; private final Message header; private final String payload; - - private MultipartRequest(String pid, @NotNull Message header, @Nullable String payload) { + private HandlerRequest(String pid, @NotNull Message header, @Nullable String payload) { this.pid = pid; this.header = header; this.payload = payload; @@ -73,10 +72,10 @@ public Builder payload(@NotNull String payload) { return this; } - public MultipartRequest build() { + public HandlerRequest build() { Objects.requireNonNull(header, "Multipart request header is null."); - return new MultipartRequest(pid, header, payload); + return new HandlerRequest(pid, header, payload); } } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java similarity index 79% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java index e709f73..0f6a30e 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/message/MultipartResponse.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java @@ -12,22 +12,22 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.message; +package de.truzzt.clearinghouse.edc.dto; -import de.truzzt.clearinghouse.edc.multipart.types.ids.SecurityToken; -import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import de.truzzt.clearinghouse.edc.types.ids.Message; +import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; import java.util.function.Function; -public class MultipartResponse { +public class HandlerResponse { private final Message header; private final Object payload; - private MultipartResponse(@NotNull Message header, @Nullable Object payload) { + private HandlerResponse(@NotNull Message header, @Nullable Object payload) { this.header = header; this.payload = payload; } @@ -68,9 +68,9 @@ public Builder payload(@Nullable Object payload) { return this; } - public MultipartResponse build() { + public HandlerResponse build() { Objects.requireNonNull(header, "Multipart response header is null."); - return new MultipartResponse(header, payload); + return new HandlerResponse(header, payload); } } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageRequest.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageRequest.java similarity index 76% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageRequest.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageRequest.java index 62e32c9..172c84c 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageRequest.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageRequest.java @@ -12,22 +12,23 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; +package de.truzzt.clearinghouse.edc.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import de.truzzt.clearinghouse.edc.types.clearinghouse.Header; import org.jetbrains.annotations.NotNull; public class LoggingMessageRequest { @JsonProperty("header") @NotNull - private RequestHeader header; + private Header header; @JsonProperty("payload") @NotNull private String payload; - public LoggingMessageRequest(@NotNull RequestHeader header, @NotNull String payload) { + public LoggingMessageRequest(@NotNull Header header, @NotNull String payload) { this.header = header; this.payload = payload; } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageResponse.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageResponse.java similarity index 90% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageResponse.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageResponse.java index 110f56c..0db5f92 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/LoggingMessageResponse.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageResponse.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; +package de.truzzt.clearinghouse.edc.dto; import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java new file mode 100644 index 0000000..14841c3 --- /dev/null +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java @@ -0,0 +1,53 @@ +package de.truzzt.clearinghouse.edc.handler; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; +import de.truzzt.clearinghouse.edc.types.ids.TokenFormat; +import org.eclipse.edc.spi.EdcException; +import org.jetbrains.annotations.NotNull; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +public interface Handler { + + boolean canHandle(@NotNull HandlerRequest handlerRequest); + + @NotNull HandlerResponse handleRequest(@NotNull HandlerRequest handlerRequest); + + default Date convertLocalDateTime(LocalDateTime localDateTime) { + return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); + } + + default @NotNull String buildJWTToken(@NotNull SecurityToken securityToken) { + + var tokenFormat = securityToken.getTokenFormat().getId().toString(); + if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { + throw new EdcException("Invalid security token format: " + securityToken.getTokenFormat().getId()); + } + + var tokenValue = securityToken.getTokenValue(); + var decodedToken = JWT.decode(tokenValue); + + var subject = decodedToken.getSubject(); + if (subject == null) { + throw new EdcException("JWT Token subject is missing"); + } + + var issuedAt = LocalDateTime.now(); + var expiresAt = issuedAt.plusSeconds(60); // TODO Move to a configuration + + var jwtToken = JWT.create() + .withAudience("1") // TODO Move to a configuration + .withIssuer("1") // TODO Move to a configuration + .withClaim("client_id", subject) + .withIssuedAt(convertLocalDateTime(issuedAt)) + .withExpiresAt(convertLocalDateTime(expiresAt)); + + return jwtToken.sign(Algorithm.HMAC256("123")); // TODO Move to a configuration + } +} diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java new file mode 100644 index 0000000..ba3ffd9 --- /dev/null +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java @@ -0,0 +1,55 @@ +package de.truzzt.clearinghouse.edc.handler; + +import de.truzzt.clearinghouse.edc.app.AppSender; +import de.truzzt.clearinghouse.edc.app.delegate.LoggingMessageDelegate; +import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.monitor.Monitor; +import org.jetbrains.annotations.NotNull; + +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.createMultipartResponse; +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.messageProcessedNotification; + +public class LogMessageHandler implements Handler { + + private final Monitor monitor; + private final IdsId connectorId; + private final TypeManagerUtil typeManagerUtil; + private final AppSender appSender; + private final LoggingMessageDelegate senderDelegate; + + public LogMessageHandler(Monitor monitor, + IdsId connectorId, + TypeManagerUtil typeManagerUtil, + AppSender appSender) { + this.monitor = monitor; + this.connectorId = connectorId; + this.typeManagerUtil = typeManagerUtil; + this.appSender = appSender; + + this.senderDelegate = new LoggingMessageDelegate(typeManagerUtil); + } + + @Override + public boolean canHandle(@NotNull HandlerRequest handlerRequest) { + return handlerRequest.getHeader().getType().equals("ids:LogMessage"); + } + + @Override + public @NotNull HandlerResponse handleRequest(@NotNull HandlerRequest handlerRequest) { + var baseUrl = "http://localhost:8000"; // TODO Move to a configuration + var header = handlerRequest.getHeader(); + + var url = senderDelegate.buildRequestUrl(baseUrl, handlerRequest); + var token = buildJWTToken(handlerRequest.getHeader().getSecurityToken()); + var body = senderDelegate.buildRequestBody(handlerRequest); + + var request = AppSenderRequest.Builder.newInstance().url(url).token(token).body(body).build(); + + var response = appSender.send(request, senderDelegate); + return createMultipartResponse(messageProcessedNotification(header, connectorId), response); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java similarity index 95% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java index 936ace7..b4d744b 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/TypeManagerUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types; +package de.truzzt.clearinghouse.edc.types; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/Context.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java similarity index 92% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/Context.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java index f800dd2..f19228a 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/Context.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; +package de.truzzt.clearinghouse.edc.types.clearinghouse; import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/RequestHeader.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Header.java similarity index 86% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/RequestHeader.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Header.java index 04a3869..dfc3dd6 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/RequestHeader.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Header.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; +package de.truzzt.clearinghouse.edc.types.clearinghouse; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; @@ -22,7 +22,7 @@ import java.net.URI; import java.util.Objects; -public class RequestHeader { +public class Header { @JsonProperty("@context") @NotNull @@ -57,14 +57,14 @@ public class RequestHeader { @NotNull private final String senderAgent; - private RequestHeader(@NotNull Context context, - @NotNull String id, - @NotNull String type, - @NotNull SecurityToken securityToken, - @NotNull String issuerConnector, - @NotNull String modelVersion, - @NotNull XMLGregorianCalendar issued, - @NotNull String senderAgent) { + private Header(@NotNull Context context, + @NotNull String id, + @NotNull String type, + @NotNull SecurityToken securityToken, + @NotNull String issuerConnector, + @NotNull String modelVersion, + @NotNull XMLGregorianCalendar issued, + @NotNull String senderAgent) { this.context = context; this.id = id; this.type = type; @@ -165,7 +165,7 @@ public Builder senderAgent(@NotNull URI senderAgent) { return this; } - public RequestHeader build() { + public Header build() { Objects.requireNonNull(context, "Logging message request header context null."); Objects.requireNonNull(id, "Logging message request header id is null."); Objects.requireNonNull(type, "Logging message request header type is null."); @@ -176,7 +176,7 @@ public RequestHeader build() { Objects.requireNonNull(issued, "Logging message request header issued is null."); Objects.requireNonNull(senderAgent, "Logging message request header sender agent is null."); - return new RequestHeader(context, id, type, securityToken, issuerConnector, modelVersion, issued, senderAgent); + return new Header(context, id, type, securityToken, issuerConnector, modelVersion, issued, senderAgent); } } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/SecurityToken.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java similarity index 97% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/SecurityToken.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java index 41cfd1b..bf808d9 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/SecurityToken.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; +package de.truzzt.clearinghouse.edc.types.clearinghouse; import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/TokenFormat.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/TokenFormat.java similarity index 91% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/TokenFormat.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/TokenFormat.java index 5d6b54f..216c4ad 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/clearinghouse/TokenFormat.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/TokenFormat.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.clearinghouse; +package de.truzzt.clearinghouse.edc.types.clearinghouse; import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Context.java similarity index 92% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Context.java index 11ac37d..14a2223 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Context.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Context.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.ids; +package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java similarity index 97% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java index c1a8d64..5658e4b 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/Message.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.ids; +package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java similarity index 94% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java index 30e0d7b..827be98 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionMessage.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.ids; +package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java similarity index 96% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java index ac8a94a..3178c23 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/RejectionReason.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.ids; +package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/SecurityToken.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java similarity index 90% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/SecurityToken.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java index f7516bb..2236966 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/SecurityToken.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java @@ -12,10 +12,10 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.ids; +package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; -import de.truzzt.clearinghouse.edc.multipart.types.ids.util.VocabUtil; +import de.truzzt.clearinghouse.edc.types.ids.util.VocabUtil; import org.jetbrains.annotations.NotNull; import java.net.URI; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java similarity index 85% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java index 0b9db00..6d05134 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/TokenFormat.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.ids; +package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; import org.jetbrains.annotations.NotNull; @@ -21,6 +21,8 @@ public class TokenFormat { + public static final String JWT_TOKEN_FORMAT = "idsc:JWT"; + @JsonProperty("@id") @NotNull private URI id; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/util/VocabUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java similarity index 95% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/util/VocabUtil.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java index 802df1d..5f16361 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/types/ids/util/VocabUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java @@ -12,7 +12,7 @@ * */ -package de.truzzt.clearinghouse.edc.multipart.types.ids.util; +package de.truzzt.clearinghouse.edc.types.ids.util; import java.net.MalformedURLException; import java.net.URI; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java similarity index 89% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java index 9cfdecc..9e26e17 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/ResponseUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java @@ -1,9 +1,9 @@ -package de.truzzt.clearinghouse.edc.multipart.util; +package de.truzzt.clearinghouse.edc.util; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; -import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; -import de.truzzt.clearinghouse.edc.multipart.types.ids.RejectionMessage; -import de.truzzt.clearinghouse.edc.multipart.types.ids.RejectionReason; +import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.types.ids.Message; +import de.truzzt.clearinghouse.edc.types.ids.RejectionMessage; +import de.truzzt.clearinghouse.edc.types.ids.RejectionReason; import org.eclipse.edc.protocol.ids.spi.domain.IdsConstants; import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.protocol.ids.spi.types.IdsType; @@ -22,8 +22,8 @@ public class ResponseUtil { private static final String PROCESSED_NOTIFICATION_TYPE = "ids:MessageProcessedNotificationMessage"; - public static MultipartResponse createMultipartResponse(@NotNull Message header, @NotNull Object payload) { - return MultipartResponse.Builder.newInstance() + public static HandlerResponse createMultipartResponse(@NotNull Message header, @NotNull Object payload) { + return HandlerResponse.Builder.newInstance() .header(header) .payload(payload) .build(); diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index e846b09..00a7a0e 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -19,11 +19,11 @@ plugins { dependencies { api(edc.spi.core) + implementation(project(":core")) + implementation(edc.ids) implementation(edc.ids.jsonld.serdes) implementation(edc.api.management.config) implementation(libs.jakarta.rsApi) implementation(libs.jersey.multipart) - - implementation("com.auth0:java-jwt:4.2.2") } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java similarity index 77% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartController.java rename to clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index 0ebb4ae..99111d1 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -1,10 +1,10 @@ package de.truzzt.clearinghouse.edc.multipart; -import de.truzzt.clearinghouse.edc.multipart.handler.Handler; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; -import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; -import de.truzzt.clearinghouse.edc.multipart.types.ids.Message; +import de.truzzt.clearinghouse.edc.handler.Handler; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.types.ids.Message; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; @@ -24,15 +24,15 @@ import java.io.InputStream; import java.util.List; -import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.malformedMessage; -import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.messageTypeNotSupported; -import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.notAuthenticated; +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.malformedMessage; +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.messageTypeNotSupported; +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.notAuthenticated; import static java.lang.String.format; @Consumes({MediaType.MULTIPART_FORM_DATA}) @Produces({MediaType.MULTIPART_FORM_DATA}) @Path("/") -public class ClearingHouseMultipartController { +public class MultipartController { private static final String HEADER = "header"; private static final String PAYLOAD = "payload"; @@ -46,10 +46,10 @@ public class ClearingHouseMultipartController { private final TypeManagerUtil typeManagerUtil; - public ClearingHouseMultipartController(@NotNull Monitor monitor, - @NotNull IdsId connectorId, - @NotNull TypeManagerUtil typeManagerUtil, - @NotNull List multipartHandlers) { + public MultipartController(@NotNull Monitor monitor, + @NotNull IdsId connectorId, + @NotNull TypeManagerUtil typeManagerUtil, + @NotNull List multipartHandlers) { this.monitor = monitor; this.connectorId = connectorId; this.typeManagerUtil = typeManagerUtil; @@ -64,7 +64,7 @@ public Response request(@PathParam(PID) String pid, // Check if header is missing if (headerInputStream == null) { - monitor.warning(LOG_ID + ": Header is missing"); + monitor.severe(LOG_ID + ": Header is missing"); return Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) .build(); @@ -75,7 +75,7 @@ public Response request(@PathParam(PID) String pid, try { header = typeManagerUtil.parse(headerInputStream, Message.class); } catch (Exception e) { - monitor.warning(format(LOG_ID + ": Header parsing failed: %s", e.getMessage())); + monitor.severe(format(LOG_ID + ": Header parsing failed: %s", e.getMessage())); return Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) .build(); @@ -96,7 +96,7 @@ public Response request(@PathParam(PID) String pid, // Check if DAT present var dynamicAttributeToken = header.getSecurityToken(); if (dynamicAttributeToken == null || dynamicAttributeToken.getTokenValue() == null) { - monitor.warning(LOG_ID + ": Token is missing in header"); + monitor.severe(LOG_ID + ": Token is missing in header"); return Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(notAuthenticated(header, connectorId))) .build(); @@ -104,14 +104,14 @@ public Response request(@PathParam(PID) String pid, // Check if payload is missing if (payload == null) { - monitor.warning(LOG_ID + ": Payload is missing"); + monitor.severe(LOG_ID + ": Payload is missing"); return Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) .build(); } // Build the multipart request - var multipartRequest = MultipartRequest.Builder.newInstance() + var multipartRequest = HandlerRequest.Builder.newInstance() .pid(pid) .header(header) .payload(payload) @@ -122,7 +122,7 @@ public Response request(@PathParam(PID) String pid, .filter(h -> h.canHandle(multipartRequest)) .findFirst() .map(it -> it.handleRequest(multipartRequest)) - .orElseGet(() -> MultipartResponse.Builder.newInstance() + .orElseGet(() -> HandlerResponse.Builder.newInstance() .header(messageTypeNotSupported(header, connectorId)) .build()); diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java similarity index 62% rename from clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartExtension.java rename to clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java index 1db6cb0..f6675ea 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/ClearingHouseMultipartExtension.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java @@ -1,11 +1,9 @@ package de.truzzt.clearinghouse.edc.multipart; -import de.truzzt.clearinghouse.edc.multipart.handler.Handler; -import de.truzzt.clearinghouse.edc.multipart.handler.LogMessageHandler; -import de.truzzt.clearinghouse.edc.multipart.handler.QueryMessageHandler; -import de.truzzt.clearinghouse.edc.multipart.handler.RequestMessageHandler; -import de.truzzt.clearinghouse.edc.multipart.sender.ClearingHouseAppSender; -import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.handler.Handler; +import de.truzzt.clearinghouse.edc.handler.LogMessageHandler; +import de.truzzt.clearinghouse.edc.app.AppSender; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.protocol.ids.jsonld.JsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Extension; @@ -20,13 +18,13 @@ import static org.eclipse.edc.protocol.ids.util.ConnectorIdUtil.resolveConnectorId; -@Extension(value = ClearingHouseMultipartExtension.NAME) +@Extension(value = MultipartExtension.NAME) @Requires(value = { WebService.class, ManagementApiConfiguration.class, EdcHttpClient.class }) -public class ClearingHouseMultipartExtension implements ServiceExtension { +public class MultipartExtension implements ServiceExtension { public static final String NAME = "Clearing House Multipart Extension"; @@ -50,15 +48,12 @@ public void initialize(ServiceExtensionContext context) { var connectorId = resolveConnectorId(context); var typeManagerUtil = new TypeManagerUtil(JsonLd.getObjectMapper()); - var clearingHouseAppSender = new ClearingHouseAppSender(monitor, httpClient, typeManagerUtil); + var clearingHouseAppSender = new AppSender(monitor, httpClient, typeManagerUtil); var handlers = new LinkedList(); - handlers.add(new RequestMessageHandler(monitor, connectorId, clearingHouseAppSender)); handlers.add(new LogMessageHandler(monitor, connectorId, typeManagerUtil, clearingHouseAppSender)); - handlers.add(new QueryMessageHandler(monitor, connectorId, clearingHouseAppSender)); - var multipartController = new ClearingHouseMultipartController(monitor, connectorId, typeManagerUtil, handlers); + var multipartController = new MultipartController(monitor, connectorId, typeManagerUtil, handlers); webService.registerResource(managementApiConfig.getContextAlias(), multipartController); } - } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/Handler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/Handler.java deleted file mode 100644 index d04af3b..0000000 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/Handler.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.truzzt.clearinghouse.edc.multipart.handler; - -import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; -import org.jetbrains.annotations.NotNull; - -public interface Handler { - - boolean canHandle(@NotNull MultipartRequest multipartRequest); - - @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest); -} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogMessageHandler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogMessageHandler.java deleted file mode 100644 index a1bc43e..0000000 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/LogMessageHandler.java +++ /dev/null @@ -1,84 +0,0 @@ -package de.truzzt.clearinghouse.edc.multipart.handler; - -import com.auth0.jwt.JWT; - -import com.auth0.jwt.algorithms.Algorithm; -import de.truzzt.clearinghouse.edc.multipart.message.ClearingHouseAppRequest; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; -import de.truzzt.clearinghouse.edc.multipart.sender.ClearingHouseAppSender; -import de.truzzt.clearinghouse.edc.multipart.sender.delegate.LoggingMessageSenderDelegate; -import de.truzzt.clearinghouse.edc.multipart.types.TypeManagerUtil; - -import de.truzzt.clearinghouse.edc.multipart.util.JWTUtil; -import org.eclipse.edc.protocol.ids.spi.types.IdsId; -import org.eclipse.edc.spi.monitor.Monitor; -import org.jetbrains.annotations.NotNull; - -import java.time.LocalDateTime; - -import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.createMultipartResponse; -import static de.truzzt.clearinghouse.edc.multipart.util.ResponseUtil.messageProcessedNotification; - -public class LogMessageHandler implements Handler { - - private final Monitor monitor; - private final IdsId connectorId; - private final ClearingHouseAppSender clearingHouseAppSender; - private final TypeManagerUtil typeManagerUtil; - private final LoggingMessageSenderDelegate senderDelegate; - - public LogMessageHandler(Monitor monitor, - IdsId connectorId, - TypeManagerUtil typeManagerUtil, - ClearingHouseAppSender clearingHouseAppSender) { - this.monitor = monitor; - this.connectorId = connectorId; - this.typeManagerUtil = typeManagerUtil; - this.clearingHouseAppSender = clearingHouseAppSender; - - this.senderDelegate = new LoggingMessageSenderDelegate(typeManagerUtil); - } - - @Override - public boolean canHandle(@NotNull MultipartRequest multipartRequest) { - return multipartRequest.getHeader().getType().equals("ids:LogMessage"); - } - - @Override - public @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest) { - var header = multipartRequest.getHeader(); - - var baseUrl = "http://localhost:8000"; // TODO Move to a configuration - var url = senderDelegate.buildRequestUrl(baseUrl, multipartRequest); - - var tokenValue = multipartRequest.getHeader().getSecurityToken().getTokenValue(); - - // TODO Move to a shared class - // TODO Validate if tokenFormat is JWT - - var decodedDat = JWT.decode(tokenValue); - var claimedClientId = decodedDat.getSubject(); - - // TODO Validate if token subject is null - - var issuedAt = LocalDateTime.now(); - var expiresAt = issuedAt.plusSeconds(60); // Config - - var jwtToken = JWT.create() - .withAudience("1") // TODO Move to a configuration - .withIssuer("1") // TODO Move to a configuration - .withClaim("client_id", claimedClientId) - .withIssuedAt(JWTUtil.convertLocalDateTime(issuedAt)) - .withExpiresAt(JWTUtil.convertLocalDateTime(expiresAt)) - .sign(Algorithm.HMAC256("123")); // TODO Move to a configuration - - var body = senderDelegate.buildRequestBody(multipartRequest); - - var request = ClearingHouseAppRequest.Builder.newInstance().url(url).token(jwtToken).body(body).build(); - - var response = clearingHouseAppSender.send(request, senderDelegate); - - return createMultipartResponse(messageProcessedNotification(header, connectorId), response); - } -} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/QueryMessageHandler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/QueryMessageHandler.java deleted file mode 100644 index d65093c..0000000 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/QueryMessageHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.truzzt.clearinghouse.edc.multipart.handler; - -import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; -import de.truzzt.clearinghouse.edc.multipart.sender.ClearingHouseAppSender; -import org.eclipse.edc.protocol.ids.spi.types.IdsId; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; -import org.jetbrains.annotations.NotNull; - -public class QueryMessageHandler implements Handler { - - private final Monitor monitor; - private final IdsId connectorId; - private final ClearingHouseAppSender clearingHouseAppSender; - - public QueryMessageHandler(Monitor monitor, - IdsId connectorId, - ClearingHouseAppSender clearingHouseAppSender) { - this.monitor = monitor; - this.connectorId = connectorId; - this.clearingHouseAppSender = clearingHouseAppSender; - } - - @Override - public boolean canHandle(@NotNull MultipartRequest multipartRequest) { - return multipartRequest.getHeader().getType().equals("ids:QueryMessage"); - } - - @Override - public @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest) { - throw new EdcException("Handler not implemented !"); - } -} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/RequestMessageHandler.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/RequestMessageHandler.java deleted file mode 100644 index 701d169..0000000 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/handler/RequestMessageHandler.java +++ /dev/null @@ -1,34 +0,0 @@ -package de.truzzt.clearinghouse.edc.multipart.handler; - -import de.truzzt.clearinghouse.edc.multipart.message.MultipartRequest; -import de.truzzt.clearinghouse.edc.multipart.message.MultipartResponse; -import de.truzzt.clearinghouse.edc.multipart.sender.ClearingHouseAppSender; -import org.eclipse.edc.protocol.ids.spi.types.IdsId; -import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; -import org.jetbrains.annotations.NotNull; - -public class RequestMessageHandler implements Handler { - - private final Monitor monitor; - private final IdsId connectorId; - private final ClearingHouseAppSender clearingHouseAppSender; - - public RequestMessageHandler(Monitor monitor, - IdsId connectorId, - ClearingHouseAppSender clearingHouseAppSender) { - this.monitor = monitor; - this.connectorId = connectorId; - this.clearingHouseAppSender = clearingHouseAppSender; - } - - @Override - public boolean canHandle(@NotNull MultipartRequest multipartRequest) { - return multipartRequest.getHeader().getType().equals("ids:RequestMessage"); - } - - @Override - public @NotNull MultipartResponse handleRequest(@NotNull MultipartRequest multipartRequest) { - throw new EdcException("Handler not implemented !"); - } -} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/JWTUtil.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/JWTUtil.java deleted file mode 100644 index 070aa49..0000000 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/util/JWTUtil.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.truzzt.clearinghouse.edc.multipart.util; - -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Date; - -public class JWTUtil { - - public static Date convertLocalDateTime(LocalDateTime localDateTime) { - return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); - } -} diff --git a/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 479afa7..ae7a3a9 100644 --- a/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -12,4 +12,4 @@ # # -de.truzzt.clearinghouse.edc.multipart.ClearingHouseMultipartExtension \ No newline at end of file +de.truzzt.clearinghouse.edc.multipart.MultipartExtension \ No newline at end of file diff --git a/clearing-house-edc/gradle.properties b/clearing-house-edc/gradle.properties index 66d3a2d..46d914a 100644 --- a/clearing-house-edc/gradle.properties +++ b/clearing-house-edc/gradle.properties @@ -1,3 +1,2 @@ javaVersion=17 -edcGroup=org.eclipse.edc -edcVersion=0.3.0 +auth0JWTVersion=4.2.2 \ No newline at end of file diff --git a/clearing-house-edc/launchers/connector/Dockerfile b/clearing-house-edc/launchers/connector/Dockerfile new file mode 100644 index 0000000..7ff2573 --- /dev/null +++ b/clearing-house-edc/launchers/connector/Dockerfile @@ -0,0 +1,16 @@ +FROM openjdk:17-slim-buster + +RUN apt update \ + && apt install -y curl \ + && rm -rf /var/cache/apt/archives /var/lib/apt/lists + +WORKDIR /app + +COPY ./build/libs/connector.jar /app + +ENV WEB_HTTP_PORT="9191" +ENV WEB_HTTP_PATH="/api" + +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8181/api/check/health + +ENTRYPOINT [ "sh", "-c", "exec java $ENV_JVM_ARGS -jar connector.jar"] diff --git a/clearing-house-edc/spi/src/main/java/de/truzzt/clearinghouse/edc/spi/.gitkeep b/clearing-house-edc/spi/src/main/java/de/truzzt/clearinghouse/edc/spi/.gitkeep deleted file mode 100644 index e69de29..0000000 From b8b6e076f141ccbb663fbe5e4ff3d2bbf3b3b616 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 28 Sep 2023 11:52:23 -0300 Subject: [PATCH 075/183] chore (ch-edc): check style issues --- .../clearinghouse/edc/app/AppSender.java | 33 +++++++++---------- .../edc/app/delegate/AppSenderDelegate.java | 4 +-- .../app/delegate/LoggingMessageDelegate.java | 2 +- .../edc/dto/HandlerResponse.java | 6 ---- .../edc/types/clearinghouse/Context.java | 4 +-- .../types/clearinghouse/SecurityToken.java | 8 ++--- .../clearinghouse/edc/types/ids/Message.java | 11 +++++++ .../edc/types/ids/RejectionMessage.java | 3 -- .../edc/types/ids/RejectionReason.java | 6 ++-- .../clearinghouse/edc/util/ResponseUtil.java | 1 + 10 files changed, 38 insertions(+), 40 deletions(-) diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java index 0c917c7..978b227 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java @@ -41,7 +41,7 @@ public AppSender(Monitor monitor, this.typeManagerUtil = typeManagerUtil; } - public P send(AppSenderRequest request, AppSenderDelegate appSenderDelegate) { + public P send(AppSenderRequest request, AppSenderDelegate

appSenderDelegate) { var json = typeManagerUtil.toJson(request.getBody()); var requestBody = RequestBody.create(json, MediaType.get(JSON_CONTENT_TYPE)); @@ -53,27 +53,24 @@ public P send(AppSenderRequest request, AppSenderDelegate appSender .post(requestBody) .build(); - Response response; - try { - response = httpClient.execute(httpRequest); + try (Response response = httpClient.execute(httpRequest)) { monitor.debug("Response received from Clearing House App. Status: " + response.code()); - } catch (java.io.IOException e) { - throw new EdcException("Error sending request to Clearing House App", e); - } - - if (response.isSuccessful()) { - try (var body = response.body()) { - if (body == null) { - throw new EdcException("Received an empty response body from Clearing House App"); - } else { - return appSenderDelegate.parseResponseBody(body); + if (response.isSuccessful()) { + try (var body = response.body()) { + if (body == null) { + throw new EdcException("Received an empty response body from Clearing House App"); + } else { + return appSenderDelegate.parseResponseBody(body); + } + } catch (Exception e) { + throw new EdcException("Error reading Clearing House App response body", e); } - } catch (Exception e) { - throw new EdcException("Error reading Clearing House App response body", e); + } else { + throw new EdcException(format("Received an error from Clearing House App. Status: %s, message: %s", response.code(), response.message())); } - } else { - throw new EdcException(format("Received an error from Clearing House App. Status: %s, message: %s", response.code(), response.message())); + } catch (java.io.IOException e) { + throw new EdcException("Error sending request to Clearing House App", e); } } } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java index d9c7a70..bd807ea 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java @@ -15,7 +15,7 @@ import okhttp3.ResponseBody; -public interface AppSenderDelegate { +public interface AppSenderDelegate { - P parseResponseBody(ResponseBody responseBody); + B parseResponseBody(ResponseBody responseBody); } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java index ecbb4c9..7734ce6 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java @@ -10,7 +10,7 @@ import de.truzzt.clearinghouse.edc.types.clearinghouse.TokenFormat; import okhttp3.ResponseBody; -public class LoggingMessageDelegate implements AppSenderDelegate { +public class LoggingMessageDelegate implements AppSenderDelegate { private final TypeManagerUtil typeManagerUtil; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java index 0f6a30e..fbb048b 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java @@ -15,12 +15,10 @@ package de.truzzt.clearinghouse.edc.dto; import de.truzzt.clearinghouse.edc.types.ids.Message; -import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Objects; -import java.util.function.Function; public class HandlerResponse { @@ -42,10 +40,6 @@ public Object getPayload() { return payload; } - public void setSecurityToken(Function getToken) { - getHeader().setSecurityToken(getToken.apply(getHeader())); - } - public static class Builder { private Message header; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java index f19228a..f0b800a 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java @@ -20,11 +20,11 @@ public class Context { @JsonProperty("ids") @NotNull - private String ids; + private final String ids; @JsonProperty("idsc") @NotNull - private String idsc; + private final String idsc; public Context(@NotNull String ids, @NotNull String idsc) { this.ids = ids; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java index bf808d9..8ca532b 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java @@ -24,19 +24,19 @@ public class SecurityToken { @JsonProperty("@type") @NotNull - private String type; + private final String type; @JsonProperty("@id") @NotNull - private String id; + private final String id; @JsonProperty("tokenFormat") @NotNull - private TokenFormat tokenFormat; + private final TokenFormat tokenFormat; @JsonProperty("tokenValue") @NotNull - private String tokenValue; + private final String tokenValue; private SecurityToken(@NotNull String type, @NotNull String id, diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java index 5658e4b..3f8e2bd 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java @@ -56,6 +56,9 @@ public class Message { @NotNull private URI senderAgent; + @JsonProperty("ids:correlationMessage") + private Message correlationMessage; + public Message() { } @@ -122,4 +125,12 @@ public Context getContext() { public void setContext(Context context) { this.context = context; } + + public Message getCorrelationMessage() { + return correlationMessage; + } + + public void setCorrelationMessage(Message correlationMessage) { + this.correlationMessage = correlationMessage; + } } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java index 827be98..ce4652a 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java @@ -25,9 +25,6 @@ public class RejectionMessage extends Message { @NotNull RejectionReason rejectionReason; - public RejectionMessage() { - } - public RejectionMessage(@NotNull URI id) { super(id); } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java index 3178c23..4d0528e 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java @@ -36,10 +36,8 @@ public RejectionReason(@NotNull URI id) { this.id = id; } - public static final RejectionReason BAD_PARAMETERS = new RejectionReason(URI.create("https://w3id.org/idsa/code/BAD_PARAMETERS")); - - public static final RejectionReason INTERNAL_RECIPIENT_ERROR = - new RejectionReason(URI.create("https://w3id.org/idsa/code/INTERNAL_RECIPIENT_ERROR")); + public static final RejectionReason BAD_PARAMETERS = + new RejectionReason(URI.create("https://w3id.org/idsa/code/BAD_PARAMETERS")); public static final RejectionReason MALFORMED_MESSAGE = new RejectionReason(URI.create("https://w3id.org/idsa/code/MALFORMED_MESSAGE")); diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java index 9e26e17..67e0a00 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java @@ -91,6 +91,7 @@ private static RejectionMessage createRejectionMessage(@Nullable Message correla rejectionMessage.setIssued(gregorianNow()); rejectionMessage.setIssuerConnector(connectorId.toUri()); rejectionMessage.setSenderAgent(connectorId.toUri()); + rejectionMessage.setCorrelationMessage(correlationMessage); return rejectionMessage; } From 2333e420907556892f06c9e9ebaf6821fda82fd8 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 28 Sep 2023 12:17:42 -0300 Subject: [PATCH 076/183] chore (ch-edc): connector dockerfile --- .../build.gradle.kts | 7 ++++ .../{connector => connector-prod}/Dockerfile | 8 ++--- .../launchers/connector-prod/build.gradle.kts | 35 +++++++++++++++++++ clearing-house-edc/settings.gradle.kts | 4 +-- 4 files changed, 48 insertions(+), 6 deletions(-) rename clearing-house-edc/launchers/{connector => connector-local}/build.gradle.kts (75%) rename clearing-house-edc/launchers/{connector => connector-prod}/Dockerfile (54%) create mode 100644 clearing-house-edc/launchers/connector-prod/build.gradle.kts diff --git a/clearing-house-edc/launchers/connector/build.gradle.kts b/clearing-house-edc/launchers/connector-local/build.gradle.kts similarity index 75% rename from clearing-house-edc/launchers/connector/build.gradle.kts rename to clearing-house-edc/launchers/connector-local/build.gradle.kts index c97d394..896f526 100644 --- a/clearing-house-edc/launchers/connector/build.gradle.kts +++ b/clearing-house-edc/launchers/connector-local/build.gradle.kts @@ -16,6 +16,7 @@ plugins { `java-library` id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" } dependencies { @@ -28,3 +29,9 @@ dependencies { application { mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") } + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("clearing-house-edc.jar") +} + diff --git a/clearing-house-edc/launchers/connector/Dockerfile b/clearing-house-edc/launchers/connector-prod/Dockerfile similarity index 54% rename from clearing-house-edc/launchers/connector/Dockerfile rename to clearing-house-edc/launchers/connector-prod/Dockerfile index 7ff2573..02287d8 100644 --- a/clearing-house-edc/launchers/connector/Dockerfile +++ b/clearing-house-edc/launchers/connector-prod/Dockerfile @@ -6,11 +6,11 @@ RUN apt update \ WORKDIR /app -COPY ./build/libs/connector.jar /app +COPY ./build/libs/clearing-house-edc.jar /app -ENV WEB_HTTP_PORT="9191" +ENV WEB_HTTP_PORT="8181" ENV WEB_HTTP_PATH="/api" -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8181/api/check/health +HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8111/api/check/health -ENTRYPOINT [ "sh", "-c", "exec java $ENV_JVM_ARGS -jar connector.jar"] +ENTRYPOINT [ "sh", "-c", "exec java $ENV_JVM_ARGS -jar clearing-house-edc.jar"] diff --git a/clearing-house-edc/launchers/connector-prod/build.gradle.kts b/clearing-house-edc/launchers/connector-prod/build.gradle.kts new file mode 100644 index 0000000..09550ed --- /dev/null +++ b/clearing-house-edc/launchers/connector-prod/build.gradle.kts @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2021 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * + */ + +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "7.1.2" +} + +dependencies { + runtimeOnly(project(":extensions:multipart")) + + runtimeOnly(edc.bundles.connector) + runtimeOnly(edc.oauth2.core) +} + +application { + mainClass.set("org.eclipse.edc.boot.system.runtime.BaseRuntime") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("clearing-house-edc.jar") +} diff --git a/clearing-house-edc/settings.gradle.kts b/clearing-house-edc/settings.gradle.kts index 6c97c1e..0c25593 100644 --- a/clearing-house-edc/settings.gradle.kts +++ b/clearing-house-edc/settings.gradle.kts @@ -68,7 +68,7 @@ dependencyResolutionManagement { } } -include(":spi") include(":core") include(":extensions:multipart") -include(":launchers:connector") +include(":launchers:connector-prod") +include(":launchers:connector-local") From f8e187e59c32483c8250252683804f0b86643de7 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Thu, 28 Sep 2023 17:45:44 -0300 Subject: [PATCH 077/183] feat: externalization of environments variables --- .../clearinghouse/edc/handler/Handler.java | 26 +++++++++++++++---- .../edc/handler/LogMessageHandler.java | 13 +++++++--- .../edc/multipart/MultipartExtension.java | 2 +- .../connector-local/build.gradle.kts | 2 ++ .../connector-local/config.properties | 5 ++++ 5 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 clearing-house-edc/launchers/connector-local/config.properties diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java index 14841c3..64cbde1 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java @@ -6,7 +6,9 @@ import de.truzzt.clearinghouse.edc.dto.HandlerResponse; import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; import de.truzzt.clearinghouse.edc.types.ids.TokenFormat; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.jetbrains.annotations.NotNull; import java.time.LocalDateTime; @@ -15,6 +17,19 @@ public interface Handler { + @Setting + String JWT_AUDIENCE = "edc.truzzt.jwt.audience"; + + @Setting + String JWT_ISSUER = "edc.truzzt.jwt.issuer"; + + @Setting + String JWT_SIGN_SECRET = "edc.truzzt.jwt.sign.secret"; + + @Setting + String JWT_EXPIRES_AT = "edc.truzzt.jwt.expires.at"; + + boolean canHandle(@NotNull HandlerRequest handlerRequest); @NotNull HandlerResponse handleRequest(@NotNull HandlerRequest handlerRequest); @@ -23,7 +38,7 @@ default Date convertLocalDateTime(LocalDateTime localDateTime) { return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); } - default @NotNull String buildJWTToken(@NotNull SecurityToken securityToken) { + default @NotNull String buildJWTToken(@NotNull SecurityToken securityToken, ServiceExtensionContext context) { var tokenFormat = securityToken.getTokenFormat().getId().toString(); if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { @@ -39,15 +54,16 @@ default Date convertLocalDateTime(LocalDateTime localDateTime) { } var issuedAt = LocalDateTime.now(); - var expiresAt = issuedAt.plusSeconds(60); // TODO Move to a configuration + var expiresAt = issuedAt.plusSeconds( + Long.valueOf(context.getSetting(JWT_EXPIRES_AT,JWT_EXPIRES_AT))); var jwtToken = JWT.create() - .withAudience("1") // TODO Move to a configuration - .withIssuer("1") // TODO Move to a configuration + .withAudience(context.getSetting(JWT_AUDIENCE, JWT_AUDIENCE)) + .withIssuer(context.getSetting(JWT_ISSUER, JWT_ISSUER)) .withClaim("client_id", subject) .withIssuedAt(convertLocalDateTime(issuedAt)) .withExpiresAt(convertLocalDateTime(expiresAt)); - return jwtToken.sign(Algorithm.HMAC256("123")); // TODO Move to a configuration + return jwtToken.sign(Algorithm.HMAC256(context.getSetting(JWT_SIGN_SECRET,JWT_SIGN_SECRET))); } } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java index ba3ffd9..5bd60c7 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java @@ -8,6 +8,7 @@ import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.jetbrains.annotations.NotNull; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.createMultipartResponse; @@ -15,20 +16,26 @@ public class LogMessageHandler implements Handler { + private static final String APP_BASE_URL = "edc.truzzt.app.base.url"; + private final Monitor monitor; private final IdsId connectorId; private final TypeManagerUtil typeManagerUtil; private final AppSender appSender; private final LoggingMessageDelegate senderDelegate; + private final ServiceExtensionContext context; + public LogMessageHandler(Monitor monitor, IdsId connectorId, TypeManagerUtil typeManagerUtil, - AppSender appSender) { + AppSender appSender, + ServiceExtensionContext context) { this.monitor = monitor; this.connectorId = connectorId; this.typeManagerUtil = typeManagerUtil; this.appSender = appSender; + this.context = context; this.senderDelegate = new LoggingMessageDelegate(typeManagerUtil); } @@ -40,11 +47,11 @@ public boolean canHandle(@NotNull HandlerRequest handlerRequest) { @Override public @NotNull HandlerResponse handleRequest(@NotNull HandlerRequest handlerRequest) { - var baseUrl = "http://localhost:8000"; // TODO Move to a configuration + var baseUrl = context.getSetting(APP_BASE_URL,APP_BASE_URL); var header = handlerRequest.getHeader(); var url = senderDelegate.buildRequestUrl(baseUrl, handlerRequest); - var token = buildJWTToken(handlerRequest.getHeader().getSecurityToken()); + var token = buildJWTToken(handlerRequest.getHeader().getSecurityToken(), context); var body = senderDelegate.buildRequestBody(handlerRequest); var request = AppSenderRequest.Builder.newInstance().url(url).token(token).body(body).build(); diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java index f6675ea..877bc3b 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java @@ -51,7 +51,7 @@ public void initialize(ServiceExtensionContext context) { var clearingHouseAppSender = new AppSender(monitor, httpClient, typeManagerUtil); var handlers = new LinkedList(); - handlers.add(new LogMessageHandler(monitor, connectorId, typeManagerUtil, clearingHouseAppSender)); + handlers.add(new LogMessageHandler(monitor, connectorId, typeManagerUtil, clearingHouseAppSender, context)); var multipartController = new MultipartController(monitor, connectorId, typeManagerUtil, handlers); webService.registerResource(managementApiConfig.getContextAlias(), multipartController); diff --git a/clearing-house-edc/launchers/connector-local/build.gradle.kts b/clearing-house-edc/launchers/connector-local/build.gradle.kts index 896f526..06f2391 100644 --- a/clearing-house-edc/launchers/connector-local/build.gradle.kts +++ b/clearing-house-edc/launchers/connector-local/build.gradle.kts @@ -22,8 +22,10 @@ plugins { dependencies { runtimeOnly(project(":extensions:multipart")) + runtimeOnly(edc.config.filesystem) runtimeOnly(edc.bundles.connector) runtimeOnly(edc.iam.mock) + } application { diff --git a/clearing-house-edc/launchers/connector-local/config.properties b/clearing-house-edc/launchers/connector-local/config.properties new file mode 100644 index 0000000..c88ff4d --- /dev/null +++ b/clearing-house-edc/launchers/connector-local/config.properties @@ -0,0 +1,5 @@ +edc.truzzt.jwt.audience=1 +edc.truzzt.jwt.issuer=1 +edc.truzzt.jwt.sign.secret=123 +edc.truzzt.jwt.expires.at=60 +edc.truzzt.app.base.url=http://localhost:8000 \ No newline at end of file From 71e10008e8dedabe37f32b579d983337f60ca716 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Mon, 2 Oct 2023 18:06:05 -0300 Subject: [PATCH 078/183] feat (ch-edc): logging message multipart endpoint --- .../clearinghouse/edc/handler/Handler.java | 37 ++---- .../edc/handler/LogMessageHandler.java | 7 +- .../clearinghouse/edc/types/ids/Message.java | 13 ++ .../edc/types/ids/RejectionReason.java | 21 ++++ .../edc/types/ids/SecurityToken.java | 28 +++-- .../clearinghouse/edc/util/ResponseUtil.java | 4 +- .../edc/util/SettingsConstants.java | 20 +++ .../edc/multipart/MultipartController.java | 116 +++++++++++++++--- .../edc/multipart/MultipartExtension.java | 15 ++- .../connector-local/build.gradle.kts | 6 +- clearing-house-edc/settings.gradle.kts | 4 +- 11 files changed, 212 insertions(+), 59 deletions(-) create mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/SettingsConstants.java diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java index 64cbde1..c70a71d 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java @@ -5,8 +5,6 @@ import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.HandlerResponse; import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; -import de.truzzt.clearinghouse.edc.types.ids.TokenFormat; -import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.jetbrains.annotations.NotNull; @@ -15,20 +13,16 @@ import java.time.ZoneId; import java.util.Date; -public interface Handler { - - @Setting - String JWT_AUDIENCE = "edc.truzzt.jwt.audience"; - - @Setting - String JWT_ISSUER = "edc.truzzt.jwt.issuer"; - - @Setting - String JWT_SIGN_SECRET = "edc.truzzt.jwt.sign.secret"; - - @Setting - String JWT_EXPIRES_AT = "edc.truzzt.jwt.expires.at"; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_AUDIENCE_DEFAULT_VALUE; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_AUDIENCE_SETTING; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_EXPIRES_AT_DEFAULT_VALUE; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_EXPIRES_AT_SETTING; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_ISSUER_DEFAULT_VALUE; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_ISSUER_SETTING; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_SIGN_SECRET_DEFAULT_VALUE; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_SIGN_SECRET_SETTING; +public interface Handler { boolean canHandle(@NotNull HandlerRequest handlerRequest); @@ -40,11 +34,6 @@ default Date convertLocalDateTime(LocalDateTime localDateTime) { default @NotNull String buildJWTToken(@NotNull SecurityToken securityToken, ServiceExtensionContext context) { - var tokenFormat = securityToken.getTokenFormat().getId().toString(); - if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { - throw new EdcException("Invalid security token format: " + securityToken.getTokenFormat().getId()); - } - var tokenValue = securityToken.getTokenValue(); var decodedToken = JWT.decode(tokenValue); @@ -55,15 +44,15 @@ default Date convertLocalDateTime(LocalDateTime localDateTime) { var issuedAt = LocalDateTime.now(); var expiresAt = issuedAt.plusSeconds( - Long.valueOf(context.getSetting(JWT_EXPIRES_AT,JWT_EXPIRES_AT))); + Long.parseLong(context.getSetting(JWT_EXPIRES_AT_SETTING ,JWT_EXPIRES_AT_DEFAULT_VALUE))); var jwtToken = JWT.create() - .withAudience(context.getSetting(JWT_AUDIENCE, JWT_AUDIENCE)) - .withIssuer(context.getSetting(JWT_ISSUER, JWT_ISSUER)) + .withAudience(context.getSetting(JWT_AUDIENCE_SETTING, JWT_AUDIENCE_DEFAULT_VALUE)) + .withIssuer(context.getSetting(JWT_ISSUER_SETTING, JWT_ISSUER_DEFAULT_VALUE)) .withClaim("client_id", subject) .withIssuedAt(convertLocalDateTime(issuedAt)) .withExpiresAt(convertLocalDateTime(expiresAt)); - return jwtToken.sign(Algorithm.HMAC256(context.getSetting(JWT_SIGN_SECRET,JWT_SIGN_SECRET))); + return jwtToken.sign(Algorithm.HMAC256(context.getSetting(JWT_SIGN_SECRET_SETTING ,JWT_SIGN_SECRET_DEFAULT_VALUE))); } } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java index 5bd60c7..ecab1df 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java @@ -14,9 +14,10 @@ import static de.truzzt.clearinghouse.edc.util.ResponseUtil.createMultipartResponse; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.messageProcessedNotification; -public class LogMessageHandler implements Handler { +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.APP_BASE_URL_SETTING; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.APP_BASE_URL_DEFAULT_VALUE; - private static final String APP_BASE_URL = "edc.truzzt.app.base.url"; +public class LogMessageHandler implements Handler { private final Monitor monitor; private final IdsId connectorId; @@ -47,7 +48,7 @@ public boolean canHandle(@NotNull HandlerRequest handlerRequest) { @Override public @NotNull HandlerResponse handleRequest(@NotNull HandlerRequest handlerRequest) { - var baseUrl = context.getSetting(APP_BASE_URL,APP_BASE_URL); + var baseUrl = context.getSetting(APP_BASE_URL_SETTING, APP_BASE_URL_DEFAULT_VALUE); var header = handlerRequest.getHeader(); var url = senderDelegate.buildRequestUrl(baseUrl, handlerRequest); diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java index 3f8e2bd..323c3dd 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java @@ -20,6 +20,7 @@ import javax.xml.datatype.XMLGregorianCalendar; import java.net.URI; +import java.util.List; public class Message { @@ -43,6 +44,10 @@ public class Message { @NotNull private URI issuerConnector; + @JsonProperty("ids:recipientConnector") + @NotNull + private List recipientConnector; + @JsonProperty("ids:modelVersion") @NotNull String modelVersion; @@ -86,6 +91,14 @@ public void setIssuerConnector(URI issuerConnector) { this.issuerConnector = issuerConnector; } + public List getRecipientConnector() { + return recipientConnector; + } + + public void setRecipientConnector(List recipientConnector) { + this.recipientConnector = recipientConnector; + } + public String getModelVersion() { return modelVersion; } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java index 4d0528e..9fdde06 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java @@ -48,6 +48,27 @@ public RejectionReason(@NotNull URI id) { public static final RejectionReason NOT_AUTHENTICATED = new RejectionReason(URI.create("https://w3id.org/idsa/code/NOT_AUTHENTICATED")); + public static final RejectionReason INTERNAL_RECIPIENT_ERROR = + new RejectionReason(URI.create("https://w3id.org/idsa/code/INTERNAL_RECIPIENT_ERROR")); + + public static final RejectionReason METHOD_NOT_SUPPORTED = + new RejectionReason(URI.create("https://w3id.org/idsa/code/METHOD_NOT_SUPPORTED")); + + public static final RejectionReason NOT_AUTHORIZED = + new RejectionReason(URI.create("https://w3id.org/idsa/code/NOT_AUTHORIZED")); + + public static final RejectionReason NOT_FOUND = + new RejectionReason(URI.create("https://w3id.org/idsa/code/NOT_FOUND")); + + public static final RejectionReason TEMPORARILY_NOT_AVAILABLE = + new RejectionReason(URI.create("https://w3id.org/idsa/code/TEMPORARILY_NOT_AVAILABLE")); + + public static final RejectionReason TOO_MANY_RESULTS = + new RejectionReason(URI.create("https://w3id.org/idsa/code/TOO_MANY_RESULTS")); + + public static final RejectionReason VERSION_NOT_SUPPORTED = + new RejectionReason(URI.create("https://w3id.org/idsa/code/VERSION_NOT_SUPPORTED")); + public URI getId() { return id; } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java index 2236966..795e708 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java @@ -22,14 +22,14 @@ public class SecurityToken { - @JsonProperty("@type") - @NotNull - private String type; - @JsonProperty("@id") @NotNull private URI id; + @JsonProperty("@type") + @NotNull + private String type; + @JsonProperty("ids:tokenFormat") @NotNull private TokenFormat tokenFormat; @@ -38,23 +38,35 @@ public class SecurityToken { @NotNull private String tokenValue; - private SecurityToken() { + public SecurityToken() { id = VocabUtil.createRandomUrl("dynamicAttributeToken"); } + public URI getId() { + return id; + } + public void setId(URI id) { + this.id = id; + } + public String getType() { return type; } - - public URI getId() { - return id; + public void setType(String type) { + this.type = type; } public TokenFormat getTokenFormat() { return tokenFormat; } + public void setTokenFormat(TokenFormat tokenFormat) { + this.tokenFormat = tokenFormat; + } public String getTokenValue() { return tokenValue; } + public void setTokenValue(String tokenValue) { + this.tokenValue = tokenValue; + } } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java index 67e0a00..11699de 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java @@ -73,10 +73,10 @@ public static RejectionMessage messageTypeNotSupported(@NotNull Message correlat } @NotNull - public static RejectionMessage badParameters(@NotNull Message correlationMessage, + public static RejectionMessage internalRecipientError(@NotNull Message correlationMessage, @NotNull IdsId connectorId) { RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); - rejectionMessage.setRejectionReason(RejectionReason.BAD_PARAMETERS); + rejectionMessage.setRejectionReason(RejectionReason.INTERNAL_RECIPIENT_ERROR); return rejectionMessage; } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/SettingsConstants.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/SettingsConstants.java new file mode 100644 index 0000000..ddef2f4 --- /dev/null +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/SettingsConstants.java @@ -0,0 +1,20 @@ +package de.truzzt.clearinghouse.edc.util; + +public class SettingsConstants { + + public static final String JWT_AUDIENCE_SETTING = "truzzt.clearinghouse.jwt.audience"; + public static final String JWT_AUDIENCE_DEFAULT_VALUE = "1"; + + public static final String JWT_ISSUER_SETTING = "truzzt.clearinghouse.jwt.issuer"; + public static final String JWT_ISSUER_DEFAULT_VALUE = "1"; + + public static final String JWT_SIGN_SECRET_SETTING = "truzzt.clearinghouse.jwt.sign.secret"; + public static final String JWT_SIGN_SECRET_DEFAULT_VALUE = "123"; + + public static final String JWT_EXPIRES_AT_SETTING = "truzzt.clearinghouse.jwt.expires.at"; + public static final String JWT_EXPIRES_AT_DEFAULT_VALUE = "30"; + + public static final String APP_BASE_URL_SETTING = "truzzt.clearinghouse.app.base.url"; + public static final String APP_BASE_URL_DEFAULT_VALUE = "http://localhost:8000"; + +} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index 99111d1..cc15449 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -1,11 +1,14 @@ package de.truzzt.clearinghouse.edc.multipart; +import de.fraunhofer.iais.eis.DynamicAttributeTokenBuilder; import de.truzzt.clearinghouse.edc.handler.Handler; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.HandlerResponse; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.Message; +import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; +import de.truzzt.clearinghouse.edc.types.ids.TokenFormat; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -14,6 +17,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; +import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.spi.monitor.Monitor; import org.glassfish.jersey.media.multipart.FormDataBodyPart; @@ -24,6 +28,7 @@ import java.io.InputStream; import java.util.List; +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.internalRecipientError; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.malformedMessage; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.messageTypeNotSupported; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.notAuthenticated; @@ -41,18 +46,22 @@ public class MultipartController { private final Monitor monitor; private final IdsId connectorId; - - private final List multipartHandlers; - private final TypeManagerUtil typeManagerUtil; + private final DynamicAttributeTokenService tokenService; + private final String idsWebhookAddress; + private final List multipartHandlers; public MultipartController(@NotNull Monitor monitor, @NotNull IdsId connectorId, @NotNull TypeManagerUtil typeManagerUtil, + @NotNull DynamicAttributeTokenService tokenService, + @NotNull String idsWebhookAddress, @NotNull List multipartHandlers) { this.monitor = monitor; this.connectorId = connectorId; this.typeManagerUtil = typeManagerUtil; + this.tokenService = tokenService; + this.idsWebhookAddress = idsWebhookAddress; this.multipartHandlers = multipartHandlers; } @@ -93,15 +102,24 @@ public Response request(@PathParam(PID) String pid, .build(); } - // Check if DAT present - var dynamicAttributeToken = header.getSecurityToken(); - if (dynamicAttributeToken == null || dynamicAttributeToken.getTokenValue() == null) { + // Check if security token is present + var securityToken = header.getSecurityToken(); + if (securityToken == null || securityToken.getTokenValue() == null) { monitor.severe(LOG_ID + ": Token is missing in header"); return Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(notAuthenticated(header, connectorId))) .build(); } + // Check the security token type + var tokenFormat = securityToken.getTokenFormat().getId().toString(); + if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { + monitor.severe(LOG_ID + ": Invalid security token type: " + tokenFormat); + return Response.status(Response.Status.BAD_REQUEST) + .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) + .build(); + } + // Check if payload is missing if (payload == null) { monitor.severe(LOG_ID + ": Payload is missing"); @@ -110,6 +128,13 @@ public Response request(@PathParam(PID) String pid, .build(); } + // Validate DAT + if (!validateToken(header)) { + return Response.status(Response.Status.FORBIDDEN) + .entity(createFormDataMultiPart(notAuthenticated(header, connectorId))) + .build(); + } + // Build the multipart request var multipartRequest = HandlerRequest.Builder.newInstance() .pid(pid) @@ -118,20 +143,79 @@ public Response request(@PathParam(PID) String pid, .build(); // Send to handler processing - var multipartResponse = multipartHandlers.stream() - .filter(h -> h.canHandle(multipartRequest)) - .findFirst() - .map(it -> it.handleRequest(multipartRequest)) - .orElseGet(() -> HandlerResponse.Builder.newInstance() - .header(messageTypeNotSupported(header, connectorId)) - .build()); - - // Build response + HandlerResponse handlerResponse; + try { + handlerResponse = multipartHandlers.stream() + .filter(h -> h.canHandle(multipartRequest)) + .findFirst() + .map(it -> it.handleRequest(multipartRequest)) + .orElseGet(() -> HandlerResponse.Builder.newInstance() + .header(messageTypeNotSupported(header, connectorId)) + .build()); + } catch (Exception e) { + monitor.severe(LOG_ID + ": Error in message handler processing", e); + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(createFormDataMultiPart(internalRecipientError(header, connectorId))) + .build(); + } + + // Get the response token + if (!getResponseToken(header, handlerResponse)) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(createFormDataMultiPart(internalRecipientError(header, connectorId))) + .build(); + } + + // Build the response return Response.status(Response.Status.CREATED) - .entity(createFormDataMultiPart(multipartResponse.getHeader(), multipartResponse.getPayload())) + .entity(createFormDataMultiPart(handlerResponse.getHeader(), handlerResponse.getPayload())) .build(); } + private boolean validateToken(Message header) { + + var dynamicAttributeToken = new DynamicAttributeTokenBuilder(). + _tokenValue_(header.getSecurityToken().getTokenValue()). + _tokenFormat_(de.fraunhofer.iais.eis.TokenFormat.JWT) + .build(); + + var verificationResult = tokenService + .verifyDynamicAttributeToken(dynamicAttributeToken, header.getIssuerConnector(), idsWebhookAddress); + + if (verificationResult.failed()) { + monitor.warning(format("MultipartController: Token validation failed %s", verificationResult.getFailure().getMessages())); + return false; + } else { + return true; + } + } + + private boolean getResponseToken(Message header, HandlerResponse handlerResponse) { + + if ((header.getRecipientConnector() == null) || (header.getRecipientConnector().isEmpty())) { + monitor.severe(LOG_ID + ": Recipient connector is missing"); + return false; + } + + var recipient = header.getRecipientConnector().get(0); + var tokenResult = tokenService.obtainDynamicAttributeToken(recipient.toString()); + + if (tokenResult.succeeded()) { + var responseToken = tokenResult.getContent(); + SecurityToken securityToken = new SecurityToken(); + securityToken.setType(header.getSecurityToken().getType()); + securityToken.setTokenFormat(header.getSecurityToken().getTokenFormat()); + securityToken.setTokenValue(responseToken.getTokenValue()); + + handlerResponse.getHeader().setSecurityToken(securityToken); + return true; + + } else { + monitor.severe(LOG_ID + ": Failed to get response token: " + tokenResult.getFailureDetail()); + return false; + } + } + private FormDataMultiPart createFormDataMultiPart(Message header, Object payload) { var multiPart = createFormDataMultiPart(header); diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java index 877bc3b..fa3e3cb 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java @@ -5,7 +5,9 @@ import de.truzzt.clearinghouse.edc.app.AppSender; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; +import org.eclipse.edc.protocol.ids.api.configuration.IdsApiConfiguration; import org.eclipse.edc.protocol.ids.jsonld.JsonLd; +import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Requires; @@ -37,6 +39,12 @@ public class MultipartExtension implements ServiceExtension { @Inject private EdcHttpClient httpClient; + @Inject + private DynamicAttributeTokenService tokenService; + + @Inject + private IdsApiConfiguration idsApiConfiguration; + @Override public String name() { return NAME; @@ -53,7 +61,12 @@ public void initialize(ServiceExtensionContext context) { var handlers = new LinkedList(); handlers.add(new LogMessageHandler(monitor, connectorId, typeManagerUtil, clearingHouseAppSender, context)); - var multipartController = new MultipartController(monitor, connectorId, typeManagerUtil, handlers); + var multipartController = new MultipartController(monitor, + connectorId, + typeManagerUtil, + tokenService, + idsApiConfiguration.getIdsWebhookAddress(), + handlers); webService.registerResource(managementApiConfig.getContextAlias(), multipartController); } } diff --git a/clearing-house-edc/launchers/connector-local/build.gradle.kts b/clearing-house-edc/launchers/connector-local/build.gradle.kts index 06f2391..ffe907f 100644 --- a/clearing-house-edc/launchers/connector-local/build.gradle.kts +++ b/clearing-house-edc/launchers/connector-local/build.gradle.kts @@ -22,10 +22,10 @@ plugins { dependencies { runtimeOnly(project(":extensions:multipart")) - runtimeOnly(edc.config.filesystem) runtimeOnly(edc.bundles.connector) - runtimeOnly(edc.iam.mock) - + runtimeOnly(edc.config.filesystem) + runtimeOnly(edc.vault.filesystem) + runtimeOnly(edc.oauth2.core) } application { diff --git a/clearing-house-edc/settings.gradle.kts b/clearing-house-edc/settings.gradle.kts index 0c25593..b8d9f44 100644 --- a/clearing-house-edc/settings.gradle.kts +++ b/clearing-house-edc/settings.gradle.kts @@ -57,8 +57,8 @@ dependencyResolutionManagement { library("spi-ids", "org.eclipse.edc", "ids-spi").versionRef("edc") library("ids", "org.eclipse.edc", "ids").versionRef("edc") library("ids-jsonld-serdes", "org.eclipse.edc", "ids-jsonld-serdes").versionRef("edc") - library("iam-mock", "org.eclipse.edc", "iam-mock").versionRef("edc") library("oauth2-core", "org.eclipse.edc", "oauth2-core").versionRef("edc") + library("vault-filesystem", "org.eclipse.edc", "vault-filesystem").versionRef("edc") bundle( "connector", @@ -70,5 +70,5 @@ dependencyResolutionManagement { include(":core") include(":extensions:multipart") -include(":launchers:connector-prod") include(":launchers:connector-local") +include(":launchers:connector-prod") From d0b079adbcb4a66dcd9675987918ff834d56561a Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 3 Oct 2023 11:30:53 -0300 Subject: [PATCH 079/183] feat (ch-edc): logging message multipart endpoint --- .../.github/workflows/docker-publish.yml | 75 +++++++++++++++++++ .../edc/multipart/MultipartController.java | 7 +- 2 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 clearing-house-edc/.github/workflows/docker-publish.yml diff --git a/clearing-house-edc/.github/workflows/docker-publish.yml b/clearing-house-edc/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..80013b0 --- /dev/null +++ b/clearing-house-edc/.github/workflows/docker-publish.yml @@ -0,0 +1,75 @@ +name: Docker + +on: + push: + tags: [ 'v*.*.*' ] + pull_request: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build project deliveries + run: ./gradlew clean build + + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@v2.6.0 + with: + cosign-release: 'v1.13.1' + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + secrets: | + "gpc=gpr.user=${{ github.actor }} + gpr.key=${{ secrets.GITHUB_TOKEN }}" + + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + COSIGN_EXPERIMENTAL: "true" + run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index cc15449..673f00b 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -192,7 +192,10 @@ private boolean validateToken(Message header) { private boolean getResponseToken(Message header, HandlerResponse handlerResponse) { - if ((header.getRecipientConnector() == null) || (header.getRecipientConnector().isEmpty())) { + handlerResponse.getHeader().setSecurityToken(header.getSecurityToken()); + return true; + + /*if ((header.getRecipientConnector() == null) || (header.getRecipientConnector().isEmpty())) { monitor.severe(LOG_ID + ": Recipient connector is missing"); return false; } @@ -213,7 +216,7 @@ private boolean getResponseToken(Message header, HandlerResponse handlerResponse } else { monitor.severe(LOG_ID + ": Failed to get response token: " + tokenResult.getFailureDetail()); return false; - } + }*/ } private FormDataMultiPart createFormDataMultiPart(Message header, Object payload) { From 259698aaa1bf9b57c3a328bf869f233a048e36fb Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 3 Oct 2023 11:36:48 -0300 Subject: [PATCH 080/183] feat (ch-edc): logging message multipart endpoint --- .../workflows/docker-publish.yml => .github/workflows/publish.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename clearing-house-edc/.github/workflows/docker-publish.yml => .github/workflows/publish.yml (100%) diff --git a/clearing-house-edc/.github/workflows/docker-publish.yml b/.github/workflows/publish.yml similarity index 100% rename from clearing-house-edc/.github/workflows/docker-publish.yml rename to .github/workflows/publish.yml From 3dbd26d28e1b2c064e8477bda93d81452a73ff43 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 3 Oct 2023 11:40:56 -0300 Subject: [PATCH 081/183] feat (ch-edc): logging message multipart endpoint --- .github/workflows/publish.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 80013b0..9c8848b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,8 +2,7 @@ name: Docker on: push: - tags: [ 'v*.*.*' ] - pull_request: + tags: [ 'v*' ] env: REGISTRY: ghcr.io From a249275ef2f4b99153b93f186a598194d9adbed4 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 3 Oct 2023 11:46:20 -0300 Subject: [PATCH 082/183] feat (ch-edc): logging message multipart endpoint --- .github/workflows/publish.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9c8848b..c72650d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,8 +1,8 @@ -name: Docker - +name: Publish on: - push: - tags: [ 'v*' ] + push: + branches: + - feat/edc-extension env: REGISTRY: ghcr.io From 24cbfc4ea3e6f6a3e64d7bc8013d65eadb27c66a Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 3 Oct 2023 11:53:29 -0300 Subject: [PATCH 083/183] chore (ci): creating docker image publish workflow --- .github/workflows/publish.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c72650d..d8eabc8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,6 +11,9 @@ env: jobs: build: runs-on: ubuntu-latest + defaults: + run: + working-directory: ./clearing-house-edc permissions: contents: read packages: write From c11734af739cc5d87eb39236cfa58d2d4d7e7db5 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 3 Oct 2023 12:00:24 -0300 Subject: [PATCH 084/183] chore (ci): creating docker image publish workflow --- .github/workflows/publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d8eabc8..97fb2ab 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest defaults: run: - working-directory: ./clearing-house-edc + working-directory: ./clearing-house-edc permissions: contents: read packages: write @@ -29,7 +29,7 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Build project deliveries + - name: Build project extensions run: ./gradlew clean build - name: Install cosign @@ -59,7 +59,7 @@ jobs: id: build-and-push uses: docker/build-push-action@v4 with: - context: . + context: ./launchers/connector-prod push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From ebedee7448e65d6997bcbbdb88c739975fb333f9 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 3 Oct 2023 12:04:14 -0300 Subject: [PATCH 085/183] chore (ci): creating docker image publish workflow --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 97fb2ab..5573c89 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -59,7 +59,7 @@ jobs: id: build-and-push uses: docker/build-push-action@v4 with: - context: ./launchers/connector-prod + context: ./clearing-house-edc/launchers/connector-prod push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From f64aa14c802e91a34b85437d07d79eba756ea504 Mon Sep 17 00:00:00 2001 From: dhommen Date: Tue, 3 Oct 2023 23:39:45 +0200 Subject: [PATCH 086/183] feat(ch-app): add Dockerfile and GH action --- .github/workflows/publish.yml | 22 +++++++++++++++++++++- clearing-house-app/Dockerfile | 23 +++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 clearing-house-app/Dockerfile diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5573c89..d707ae1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,13 +3,33 @@ on: push: branches: - feat/edc-extension + - feat/ch-app-docker-image env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: - build: + publish_ch_app: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Change directory + run: cd clearing-house-app + + - name: Login to GitHub Container Registry + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build Docker image + run: docker build -t ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:latest . + + - name: Push Docker image + run: docker push ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:latest + + publish_ch_edc: runs-on: ubuntu-latest defaults: run: diff --git a/clearing-house-app/Dockerfile b/clearing-house-app/Dockerfile new file mode 100644 index 0000000..592aff7 --- /dev/null +++ b/clearing-house-app/Dockerfile @@ -0,0 +1,23 @@ +# Use an official Rust runtime as a parent image +FROM rust:latest + +# Set the working directory inside the container +WORKDIR /usr/src/chapp + +# Copy the Cargo.toml and Cargo.lock files to leverage Docker cache +COPY Cargo.toml Cargo.lock config.toml ./ + +# Copy the source code into the container +COPY src ./src +COPY init_db ./init_db +COPY keys ./keys +COPY certs ./certs + +# Build the Rust application with dependencies (this helps to cache dependencies) +RUN cargo build --release + +# Expose any necessary ports (if your Rust app listens on a port) +# EXPOSE 8000 + +# Run the Rust application when the container starts +CMD ["cargo", "run", "--release"] \ No newline at end of file From aad7649d60e61f3f5bc1d54e4f949360a774469b Mon Sep 17 00:00:00 2001 From: dhommen Date: Tue, 3 Oct 2023 23:48:17 +0200 Subject: [PATCH 087/183] moved cd into docker build step --- .github/workflows/publish.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d707ae1..81382ea 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -16,15 +16,12 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v3 - - - name: Change directory - run: cd clearing-house-app - name: Login to GitHub Container Registry run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - name: Build Docker image - run: docker build -t ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:latest . + run: cd clearing-house-app && docker build -t ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:latest . - name: Push Docker image run: docker push ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:latest From e845269a2149f9b02b5dac71c4f40649052a8d12 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 4 Oct 2023 09:44:03 +0200 Subject: [PATCH 088/183] fix(ch-edc): add missing vault filesystem --- clearing-house-app/Dockerfile | 2 +- clearing-house-edc/launchers/connector-prod/build.gradle.kts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/clearing-house-app/Dockerfile b/clearing-house-app/Dockerfile index 592aff7..25444a3 100644 --- a/clearing-house-app/Dockerfile +++ b/clearing-house-app/Dockerfile @@ -20,4 +20,4 @@ RUN cargo build --release # EXPOSE 8000 # Run the Rust application when the container starts -CMD ["cargo", "run", "--release"] \ No newline at end of file +CMD ["cargo", "run", "--release"] diff --git a/clearing-house-edc/launchers/connector-prod/build.gradle.kts b/clearing-house-edc/launchers/connector-prod/build.gradle.kts index 09550ed..1ec96fb 100644 --- a/clearing-house-edc/launchers/connector-prod/build.gradle.kts +++ b/clearing-house-edc/launchers/connector-prod/build.gradle.kts @@ -23,6 +23,9 @@ dependencies { runtimeOnly(edc.bundles.connector) runtimeOnly(edc.oauth2.core) + + // Vault + runtimeOnly(edc.vault.filesystem) } application { From c64d346a3d2b700bbe14cb04262a1c896f17a3d5 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 4 Oct 2023 13:11:32 +0200 Subject: [PATCH 089/183] run publish job only on completed releases --- .github/workflows/publish.yml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 81382ea..7c6e52a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,9 +1,10 @@ name: Publish + on: - push: - branches: - - feat/edc-extension - - feat/ch-app-docker-image + workflow_run: + workflows: [release] + types: + - completed env: REGISTRY: ghcr.io @@ -20,11 +21,19 @@ jobs: - name: Login to GitHub Container Registry run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin + - name: Get latest release + id: get_latest_release + run: echo "LATEST_RELEASE=${{ github.event.release.tag_name }}" >> $GITHUB_ENV + - name: Build Docker image - run: cd clearing-house-app && docker build -t ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:latest . + env: + DOCKER_IMAGE_TAG: ${{ env.LATEST_RELEASE }} + run: cd clearing-house-app && docker build -t ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:$DOCKER_IMAGE_TAG . - name: Push Docker image - run: docker push ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:latest + env: + DOCKER_IMAGE_TAG: ${{ env.LATEST_RELEASE }} + run: docker push ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:$DOCKER_IMAGE_TAG publish_ch_edc: runs-on: ubuntu-latest From 4e89ba6755095d30d23df8caec3463561112cafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 4 Oct 2023 13:28:13 +0200 Subject: [PATCH 090/183] feat(doc): Add internal description to docs --- book.toml | 24 ++- doc/SUMMARY.md | 4 - docs/Clearinghouse.md | 30 ++++ docs/SUMMARY.md | 12 ++ docs/content/admin-guide/installation.md | 1 + docs/content/admin-guide/maintenance.md | 1 + docs/content/internals/architecture.md | 23 +++ docs/content/internals/communication.md | 162 ++++++++++++++++++ docs/content/internals/functionality.md | 91 ++++++++++ {doc => docs}/images/LogMessage.drawio | 0 {doc => docs}/images/LogMessage.jpg | Bin {doc => docs}/images/LogMessage.png | Bin {doc => docs}/images/QueryMessage.drawio | 0 {doc => docs}/images/QueryMessage.png | Bin .../images/ch_container_dependencies.png | Bin .../images/ch_container_dependencies.puml | 0 16 files changed, 342 insertions(+), 6 deletions(-) delete mode 100644 doc/SUMMARY.md create mode 100644 docs/Clearinghouse.md create mode 100644 docs/SUMMARY.md create mode 100644 docs/content/admin-guide/installation.md create mode 100644 docs/content/admin-guide/maintenance.md create mode 100644 docs/content/internals/architecture.md create mode 100644 docs/content/internals/communication.md create mode 100644 docs/content/internals/functionality.md rename {doc => docs}/images/LogMessage.drawio (100%) rename {doc => docs}/images/LogMessage.jpg (100%) rename {doc => docs}/images/LogMessage.png (100%) rename {doc => docs}/images/QueryMessage.drawio (100%) rename {doc => docs}/images/QueryMessage.png (100%) rename {doc => docs}/images/ch_container_dependencies.png (100%) rename {doc => docs}/images/ch_container_dependencies.puml (100%) diff --git a/book.toml b/book.toml index ae53865..848f634 100644 --- a/book.toml +++ b/book.toml @@ -1,6 +1,26 @@ [book] -authors = ["mschoenenberg", "dhommen"] +authors = ["schoenenberg", "dhommen"] language = "en" multilingual = false src = "docs" -title = "Documentation" \ No newline at end of file +title = "Documentation" + +[preprocessor.d2] + +# path to d2 binary. +# optional. default is "d2" (ie. on the path). +path = "d2" + +# layout engine for diagrams. See https://github.com/terrastruct/d2#plugins. +# optional. default is "dagre". +layout = "dagre" + +# whether to use inline svg when rendering. +# if 'false', separate files will be generated in src/ and referenced. +# optional. default is 'true' +inline = true + +# output directory relative to `src/` for generated diagrams. +# This is ignored if 'inline' is 'true'. +# optional. default is "d2". +#output-dir = "d2" \ No newline at end of file diff --git a/doc/SUMMARY.md b/doc/SUMMARY.md deleted file mode 100644 index 897434d..0000000 --- a/doc/SUMMARY.md +++ /dev/null @@ -1,4 +0,0 @@ -# Summary - -- [Installation]() -- [Usage]() diff --git a/docs/Clearinghouse.md b/docs/Clearinghouse.md new file mode 100644 index 0000000..163c6cc --- /dev/null +++ b/docs/Clearinghouse.md @@ -0,0 +1,30 @@ +# Clearinghouse: How it works + +The Clearingouse consist of two services. The Clearinghouse-edc and the Clearinghouse-App. The Clearinghouse-edc is used to terminate IDS connections and map the requests to the API of the +Clearinghouse-App. The Clearinghouse-App is the brain of the the Clearinghouse and uses complex algorithms to encrypt and store log messages in the clearinghouse and provide mechanisms to query for log messages. + +## Clearinghouse-edc + +## Clearinghouse-App + +First of all the clearinghouse-app consisted before out of three separate microservices - logging, keyring, document - which were merged into one service. The reason for this is that the services were too tightly coupled and there was no benefit in having them separated. The new service is called just "clearinghouse-app". + +### Functionality + +The Clearinghouse-App provides the following functionality: logging and querying of log messages. Adding and changing of DocumentTypes. + +#### Log Message + +The logging service (as an entity inside the remaining clearinghouse-app) is responsible for orchestrating the flow between document service and keyring service: + +When logging a message, the message consists of two parts, originating from the IDS communication structure. There is a `header` and a `payload`. First part is to merge those two parts into a single struct (a Document). + +The logging service creates a process id (if not exists) and checks the authorization. + +After all prerequisites are checked and completed, it starts to get the transaction counter and assign i + +### API + +The API is described here in the [OpenAPI specification](). + +#### \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000..6fa552e --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,12 @@ +# Summary + +# Admin Guide +- [Installation](content/admin-guide/installation.md) +- [Maintenance](content/admin-guide/maintenance.md) + +# Internals +- [Architecture](content/internals/architecture.md) + - [Communication](content/internals/communication.md) + - [Functionality](content/internals/functionality.md) + + diff --git a/docs/content/admin-guide/installation.md b/docs/content/admin-guide/installation.md new file mode 100644 index 0000000..25267fe --- /dev/null +++ b/docs/content/admin-guide/installation.md @@ -0,0 +1 @@ +# Installation diff --git a/docs/content/admin-guide/maintenance.md b/docs/content/admin-guide/maintenance.md new file mode 100644 index 0000000..dfec7eb --- /dev/null +++ b/docs/content/admin-guide/maintenance.md @@ -0,0 +1 @@ +# Maintenance diff --git a/docs/content/internals/architecture.md b/docs/content/internals/architecture.md new file mode 100644 index 0000000..e73014d --- /dev/null +++ b/docs/content/internals/architecture.md @@ -0,0 +1,23 @@ +# Architecture + +The Clearingouse consist of two services: The Clearinghouse-EDC and the Clearinghouse-App. The Clearinghouse-EDC is used to terminate IDS connections and map the requests to the API of the Clearinghouse-App. The Clearinghouse-App is the brain of the the Clearinghouse and uses complex algorithms to encrypt and store log messages in the MongoDB and provide mechanisms to query for log messages. + +```d2 +direction: right +ch: Clearinghouse { + cha: Clearinghouse-App + che: Clearinghouse-EDC + m: MongoDB { + shape: cylinder + } + + che -> cha: REST + cha -> m +} +c: Connector +c -> ch: IDS Multipart +``` + +> **Short history lesson** +> +> The clearinghouse-app consisted before out of three separate microservices - logging, keyring, document - which were merged into one service. The reason for this is that the services were too tightly coupled and there was no benefit in having them separated. The new service is called just "clearinghouse-app". \ No newline at end of file diff --git a/docs/content/internals/communication.md b/docs/content/internals/communication.md new file mode 100644 index 0000000..eee461d --- /dev/null +++ b/docs/content/internals/communication.md @@ -0,0 +1,162 @@ +# Communication + +The APIs are documented in the following dscriptions: +- Connector to Clearinghouse: [IDS-G](https://github.com/International-Data-Spaces-Association/IDS-G/tree/main/Communication/protocols/multipart) +- Clearinghouse to Clearinghouse-App: [OpenAPI](https://github.com/truzzt/ids-basecamp-clearinghouse-postman/blob/main/index.yaml) + +The following section contains examples of the communication between the components. + +## Connector to Clearinghouse-EDC + +The Clearinghouse-EDC received IDS-Multipart messages of the type `ids:LogMessage` in the *header* and an arbitrary *payload*. The following shows an example of a multipart message: + +``` +POST /messages/log/1 HTTP/1.1 +Host: ch-ids.aisec.fraunhofer.de +Content-Type: multipart/form-data; boundary=X-TEST-REQUEST-BOUNDARY +Accept: */* + +--X-TEST-REQUEST-BOUNDARY +Content-Disposition: form-data; name="header" +Content-Type: application/json +{ + "@context" : { + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type" : "ids:LogMessage", + "@id" : "https://w3id.org/idsa/autogen/logMessage/c6c15a90-7799-4aa1-ac21-9323b87a7xv9", + "ids:securityToken" : { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/6378asd9-480d-80df-c5cb02e4e260", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + }, + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoi....." + }, + "ids:senderAgent" : "http://example.org", + "ids:modelVersion" : "4.1.0", + "ids:issued" : { + "@value" : "2020-12-14T08:57:57.057+01:00", + "@type" : "http://www.w3.org/2001/XMLSchema#dateTimeStamp" + }, + "ids:issuerConnector" : { + "@id" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" + } +} +--X-TEST-REQUEST-BOUNDARY +Content-Disposition: form-data; name="payload" +Content-Type: application/json +{ + "@context" : "https://w3id.org/idsa/contexts/context.jsonld", + "@type" : "ids:ConnectorUpdateMessage", + "id" : "http://industrialdataspace.org/connectorAvailableMessage/34d761cf-5ca4-4a77-a7f4-b14d8f75636a", + "issued" : "2019-12-02T08:25:08.245Z", + "modelVersion" : "4.1.0", + "issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b", + "securityToken" : { + "@type" : "ids:DynamicAttributeToken", + "tokenFormat" : "https://w3id.org/idsa/code/tokenformat/JWT", + "tokenValue" : "eyJhbGciOiJSUzI1NiIsInR5cCI..." +} +--X-TEST-REQUEST-BOUNDARY-- +``` + +## Clearinghouse-EDC to Clearinghouse-App + +The Clearinghouse-EDC extracts the *header* and *payload* and forwards it to the Clearinghouse-App via REST. The message looks like this: + +```json +{ + "header": { + "@context" : { + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type" : "ids:LogMessage", + "@id" : "https://w3id.org/idsa/autogen/logMessage/c6c15a90-7799-4aa1-ac21-9323b87a7xv9", + "ids:securityToken" : { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/6378asd9-480d-80df-c5cb02e4e260", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + }, + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoi....." + }, + "ids:senderAgent" : "http://example.org", + "ids:modelVersion" : "4.1.0", + "ids:issued" : { + "@value" : "2020-12-14T08:57:57.057+01:00", + "@type" : "http://www.w3.org/2001/XMLSchema#dateTimeStamp" + }, + "ids:issuerConnector" : { + "@id" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" + } + }, + "payload": { + "@context" : "https://w3id.org/idsa/contexts/context.jsonld", + "@type" : "ids:ConnectorUpdateMessage", + "id" : "http://industrialdataspace.org/connectorAvailableMessage/34d761cf-5ca4-4a77-a7f4-b14d8f75636a", + "issued" : "2019-12-02T08:25:08.245Z", + "modelVersion" : "4.1.0", + "issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b", + "securityToken" : { + "@type" : "ids:DynamicAttributeToken", + "tokenFormat" : "https://w3id.org/idsa/code/tokenformat/JWT", + "tokenValue" : "eyJhbGciOiJSUzI1NiIsInR5cCI..." + } + } +} +``` + +## Clearinghouse-App to Clearinghouse-EDC + +```json +{ + "data": "eyJhbGciOiJQUzUxMiIsImtpZCI6IlFyYS8vMjlGcnhiajVoaDVBemVmK0czNlNlaU9tOXE3czgrdzh1R0xEMjgifQ.eyJ0cmFuc2FjdGlvbl9pZCI6IjAwMDAwMDAwIiwidGltZXN0YW1wIjoxNjk2NDExMTM2LCJwcm9jZXNzX2lkIjoiMSIsImRvY3VtZW50X2lkIjoiNmNkNDQwNjQtZWFjNi00NmQzLWFhZTUtODcxYjgwYjU4OWMxIiwicGF5bG9hZCI6Int9IiwiY2hhaW5faGFzaCI6IjAiLCJjbGllbnRfaWQiOiJGNjoyNTo1ODpDNTo2MTo2ODo3QToyMTpGMTo0MDo5Rjo0RTpGQjo5NTpEQjo5OTo4ODpDOTpBNzoxQTpDNTpGODpCRjo0Qzo1NToxODo1NjozNTozNTo0MzpDNTpEQzo5NDpCNTpFQjo0NTozMDpGNTpBRjpDRSIsImNsZWFyaW5nX2hvdXNlX3ZlcnNpb24iOiIwLjEwLjAifQ.eo1KoF9gAZLF7CuhuQ-Sd9WSjw6dvDsrmM8w-A-FdTl4cOaPqp75k9O0tKxY8_ZNBsWmOzBzAfGng6YdvpDHIw9xFZTA7N_UMjTrrPuc8ehrVO2rwltTKb8N2bK4bQ4_Uq22Kd8mSFI6IyOZ7KeTkZ_iN30PXlYFAdt2GQHoT7xNERyQbHNEkJmOgGnaraMv0xEbl2zJktQqkTH9Kk4ZF2T_GbxKInhVxUhOsJ707ZeQ2Nxk4H6yO2RXwG5yKXFkwBDOMLg1f0Dnrgz_H1f-fQ7gPOrAL_4G4L7M9o7EVkMJlMpJR1xNBCeYbT_IvfL1CB5gi1NF-VNzt-8Zg5Yj-vNNR9j38yZTe6vH2dMkGl20B99KrEKTjkyVkCUIKnlb3oEKldse0E4ouw9v6WnIWq33-KnGV0ajwZrs13bQLZyLWvdNCBmYA5NujzbqOGkDROXloAB6MXBm5KiGTU8FxrqS6s_J7OW1CLTlAlTFF_U2Tr1xSvcusnpOGrU22IrCuqVuGCNNGCrPYjKJmMc05wIG0cmdxTdRnoe8R-vOVg2Zd07jdrBLX5l5tZtF60LC8DZKw4k2JaCu37W_dXdWHLSXEnpR9MGgnqC8MbOAMIIzSXpWKFdXcS-86SkgTvDA16geN_Bj7Ac6xcuUnEhM3_9tVnpjNMgPcStyO0KiP3c" +} +``` + +## Clearinghouse-EDC to Connector + +``` +--Boundary_1_377557244_1696411137008 +Content-Type: application/json +Content-Disposition: form-data; name="header" + +{ + "@context": { + "ids": "https://w3id.org/idsa/core/", + "idsc": "https://w3id.org/idsa/code/" + }, + "@id": "urn:message:92a2da5a-b5de-4709-bda9-c16a0ae293f6", + "@type": "ids:MessageProcessedNotificationMessage", + "ids:securityToken": { + "@id": "https://w3id.org/idsa/autogen/dynamicAttributeToken/6378asd9-480d-80df-c5cb02e4e260", + "@type": "ids:DynamicAttributeToken", + "ids:tokenFormat": { + "@id": "idsc:JWT" + }, + "ids:tokenValue": "eyJ0eXAiOiJhdCtqd3QiLCJraWQiOiJkNzRlYzU1MGY0MzkxYTAwZGIwODA5Mzg5MjdjOGU4YWQ0NjE3NmM4NGQ3MzhkZGMwODM1ODMzYzM5YWJkMzRhIiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZSI6Imlkc2M6SURTX0NPTk5FQ1RPUl9BVFRSSUJVVEVTX0FMTCIsImF1ZCI6WyJpZHNjOklEU19DT05ORUNUT1JTX0FMTCJdLCJpc3MiOiJkYXBzLmRlbW8udHJ1enp0cG9ydC5jb20iLCJzdWIiOiJGNjoyNTo1ODpDNTo2MTo2ODo3QToyMTpGMTo0MDo5Rjo0RTpGQjo5NTpEQjo5OTo4ODpDOTpBNzoxQTpDNTpGODpCRjo0Qzo1NToxODo1NjozNTozNTo0MzpDNTpEQzo5NDpCNTpFQjo0NTozMDpGNTpBRjpDRSIsIm5iZiI6MTY5NjQxMTAxNiwiaWF0IjoxNjk2NDExMDE2LCJqdGkiOiI0MjY2OTY0NC01MzgzLTQ2NDYtYmMxMC0zMzJlMzRkMjdmNGMiLCJleHAiOjE2OTY0MTQ2MTYsImNsaWVudF9pZCI6IkY2OjI1OjU4OkM1OjYxOjY4OjdBOjIxOkYxOjQwOjlGOjRFOkZCOjk1OkRCOjk5Ojg4OkM5OkE3OjFBOkM1OkY4OkJGOjRDOjU1OjE4OjU2OjM1OjM1OjQzOkM1OkRDOjk0OkI1OkVCOjQ1OjMwOkY1OkFGOkNFIn0.sa2zCMCwap7KjqV6RkzQ4jeR-nMPXo546oqxSzyZSPamhfkPc35LfldZTkuX_gxy6P1Ra2ltrannQTH7467FC8H00giF3mamZ_LuyUHMRUZzab0UvNJaGqt1mJZaMiOnupixP1cUhsXszfmCRKXWvatbwvlc0nhw5gdO2lH_njWBrXUy5Bt2MIIFp892ijf_rP5KC7yfa0cW9lwTFuWZYMMRBeOfY_g1Mx_YVkQXy9mFI0x3zC6rms8jq8OWRompNfkQ7mZsiFPAafls2f0iP8M2HKWA8JeOG5rkAIw0ESWSVT7iB-oV50LlX7L7zAYVLGdDyM3s_khDNxrbvlW_bQ" + }, + "ids:issuerConnector": { + "@id": "urn:connector:example-connector" + }, + "ids:modelVersion": "4.1.3", + "ids:issued": { + "@value": "2023-10-04T09:18:56.998Z", + "@type": "http://www.w3.org/2001/XMLSchema#dateTimeStamp" + }, + "ids:senderAgent": { + "@id": "urn:connector:example-connector" + } +} +--Boundary_1_377557244_1696411137008 +Content-Type: application/json +Content-Disposition: form-data; name="payload" + +{ + "data": "eyJhbGciOiJQUzUxMiIsImtpZCI6IlFyYS8vMjlGcnhiajVoaDVBemVmK0czNlNlaU9tOXE3czgrdzh1R0xEMjgifQ.eyJ0cmFuc2FjdGlvbl9pZCI6IjAwMDAwMDAwIiwidGltZXN0YW1wIjoxNjk2NDExMTM2LCJwcm9jZXNzX2lkIjoiMSIsImRvY3VtZW50X2lkIjoiNmNkNDQwNjQtZWFjNi00NmQzLWFhZTUtODcxYjgwYjU4OWMxIiwicGF5bG9hZCI6Int9IiwiY2hhaW5faGFzaCI6IjAiLCJjbGllbnRfaWQiOiJGNjoyNTo1ODpDNTo2MTo2ODo3QToyMTpGMTo0MDo5Rjo0RTpGQjo5NTpEQjo5OTo4ODpDOTpBNzoxQTpDNTpGODpCRjo0Qzo1NToxODo1NjozNTozNTo0MzpDNTpEQzo5NDpCNTpFQjo0NTozMDpGNTpBRjpDRSIsImNsZWFyaW5nX2hvdXNlX3ZlcnNpb24iOiIwLjEwLjAifQ.eo1KoF9gAZLF7CuhuQ-Sd9WSjw6dvDsrmM8w-A-FdTl4cOaPqp75k9O0tKxY8_ZNBsWmOzBzAfGng6YdvpDHIw9xFZTA7N_UMjTrrPuc8ehrVO2rwltTKb8N2bK4bQ4_Uq22Kd8mSFI6IyOZ7KeTkZ_iN30PXlYFAdt2GQHoT7xNERyQbHNEkJmOgGnaraMv0xEbl2zJktQqkTH9Kk4ZF2T_GbxKInhVxUhOsJ707ZeQ2Nxk4H6yO2RXwG5yKXFkwBDOMLg1f0Dnrgz_H1f-fQ7gPOrAL_4G4L7M9o7EVkMJlMpJR1xNBCeYbT_IvfL1CB5gi1NF-VNzt-8Zg5Yj-vNNR9j38yZTe6vH2dMkGl20B99KrEKTjkyVkCUIKnlb3oEKldse0E4ouw9v6WnIWq33-KnGV0ajwZrs13bQLZyLWvdNCBmYA5NujzbqOGkDROXloAB6MXBm5KiGTU8FxrqS6s_J7OW1CLTlAlTFF_U2Tr1xSvcusnpOGrU22IrCuqVuGCNNGCrPYjKJmMc05wIG0cmdxTdRnoe8R-vOVg2Zd07jdrBLX5l5tZtF60LC8DZKw4k2JaCu37W_dXdWHLSXEnpR9MGgnqC8MbOAMIIzSXpWKFdXcS-86SkgTvDA16geN_Bj7Ac6xcuUnEhM3_9tVnpjNMgPcStyO0KiP3c" +} +--Boundary_1_377557244_1696411137008-- +``` \ No newline at end of file diff --git a/docs/content/internals/functionality.md b/docs/content/internals/functionality.md new file mode 100644 index 0000000..d60396b --- /dev/null +++ b/docs/content/internals/functionality.md @@ -0,0 +1,91 @@ +# Functionality + + +## Logging a message + +The logging service (as an entity inside the remaining clearinghouse-app) is responsible for orchestrating the flow between document service and keyring service: + +When logging a message, the message consists of two parts, originating from the IDS communication structure. There is a `header` and a `payload`. + +The logging service creates a process id (if not exists) and checks the authorization. + +After all prerequisites are checked and completed, the logging-service merges `header` and `payload` into a Document starts to get the transaction counter and assigns it to the Document. + +Now the document service comes into play: First checking if the document exists already, then requesting the keyring service to generate a key map for the document. The key map is then used to encrypt the document (back in the document service) and then the document is stored in the database. + +Finally the transaction counter is incremented and a reciept is signed and send back to the Clearinghouse-EDC. + +### Encryption + +There is a randomly generated Master Key stored in the database. + +Each document has a number of fields. For each document a random secret is generated. This secret is used to derive multiple secrets with the HKDF Algorithm from the original secret. These derived secrets are used to encrypt the fields of the document with AES-256-GCM-SIV. + +The original secret is encrypted also with AES-256-GCM-SIV with a derived key from the Master Key and stored in the database alongside the Document. + +### Detailed internal diagram + +```d2 +log: fn log { + gp: fn db.get_process + ia: fn db.is_authorized + sp: fn db.store_process + de: process exists? { + shape: diamond + } + + gp -> de + de -> sp: No + de -> ia: Yes + sp -> ia +} + +lm: fn log_message { + + gt: fn db.get_transaction_counter + df: Document::from(message) + ced: fn doc_api.create_encrypted_document { + ed: fn db.exists_document + gk: fn key_api.generate_keys { + gm: fn db.get_master_key + gdt: fn db.get_document_type + gkm: fn generate_key_map + + gm -> gdt + gdt -> gkm + + } + de: fn doc.encrypt + pt: fn db.get_document_with_previous_transaction_counter + ad: fn db.add_document + + ed -> gk + gk -> de + de -> pt + pt -> ad + } + itc: fn db.increment_transaction_counter + + df -> gt + gt -> ced + ced -> itc +} + +log -> lm + +lm.ced.gk.gkm -> gkm + +gkm: fn generate_key_map { + ik: fn initialize_kdf + dk: fn derive_key_map + rk: fn restore_kdf + ke: fn kdf.expand + es: fn encrypt_secret + + ik -> dk + dk -> rk + rk -> ke + ke -> es + +} +``` \ No newline at end of file diff --git a/doc/images/LogMessage.drawio b/docs/images/LogMessage.drawio similarity index 100% rename from doc/images/LogMessage.drawio rename to docs/images/LogMessage.drawio diff --git a/doc/images/LogMessage.jpg b/docs/images/LogMessage.jpg similarity index 100% rename from doc/images/LogMessage.jpg rename to docs/images/LogMessage.jpg diff --git a/doc/images/LogMessage.png b/docs/images/LogMessage.png similarity index 100% rename from doc/images/LogMessage.png rename to docs/images/LogMessage.png diff --git a/doc/images/QueryMessage.drawio b/docs/images/QueryMessage.drawio similarity index 100% rename from doc/images/QueryMessage.drawio rename to docs/images/QueryMessage.drawio diff --git a/doc/images/QueryMessage.png b/docs/images/QueryMessage.png similarity index 100% rename from doc/images/QueryMessage.png rename to docs/images/QueryMessage.png diff --git a/doc/images/ch_container_dependencies.png b/docs/images/ch_container_dependencies.png similarity index 100% rename from doc/images/ch_container_dependencies.png rename to docs/images/ch_container_dependencies.png diff --git a/doc/images/ch_container_dependencies.puml b/docs/images/ch_container_dependencies.puml similarity index 100% rename from doc/images/ch_container_dependencies.puml rename to docs/images/ch_container_dependencies.puml From 4cb4563a76ebb0cbe254888960360e7882904d38 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 4 Oct 2023 13:45:24 +0200 Subject: [PATCH 091/183] doc: add communication proposal --- .gitignore | 1 + docs/Proposal.md | 46 +++++++++++++++++++++++++++++++ docs/SUMMARY.md | 1 + docs/images/CreateLogMessage.png | Bin 0 -> 27647 bytes docs/images/CreatePid.png | Bin 0 -> 25875 bytes 5 files changed, 48 insertions(+) create mode 100644 docs/Proposal.md create mode 100644 docs/images/CreateLogMessage.png create mode 100644 docs/images/CreatePid.png diff --git a/.gitignore b/.gitignore index b324702..734100a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ data node_modules/ **/*.iml .vscode/ +book/ diff --git a/docs/Proposal.md b/docs/Proposal.md new file mode 100644 index 0000000..4dd34dd --- /dev/null +++ b/docs/Proposal.md @@ -0,0 +1,46 @@ +# Communication Proposal + +Für den aktuellen Betrieb des MDS würden wir auf die Clearinghouse Specification des IDS RAM 4.0 setzen. +Dabie kann das bestehende Clearinghouse angepasst und verbessert werden durch die folgenden Punkte: + +Austausch des Trusted Connectors mittels EDC +Zusammenführung der MS zur CH-APP +Austausch des Webservers Rocket durch Axum +Wartung und Optimierungen +Stabilität durch Mutex +Update der Dependencies +Dadurch ist das Clearinghouse IDS RAM 4.0 complient und rückwärts kompatibel mit EDC MS8 + +### Offene Entscheidungen: + +Blockchain +Masterkey +Future +Im DSP wird es kein Clearinghouse wie es in der IDS RAM 4.0 spezifiziert mehr geben. +Das Clearinghouse wird vom DSP ledeglich als Teilnehmer gesehen. +Dabei werden die Logs der Connectoren dezentral nur im jeweiligen Connector liegen. +Das Clearinghouse im bereich Logging könnte somit einen Vertrag mit allen Connectoren schließen um diese Logs anzufragen. + +### Clearinghouse und DAPS +In Hinblick auf die anstehende Migration zu did:web bietet das Clearnghouse einen sinnvollen Ersatz für den DAPS. +Das Clearinghouse könnte Verifiable Credentials ausstellen, sobald die Teilnehmer den Vertrag mit diesem eingegangen sind und die Grundvorraussetzungen um am Dataspace zu partizipieren erfüllt sind. Jeder Teilnehmer darf nur mit Mitgliedern des Dataspaces interagieren, die dieses Verifiable Credential vorweisen können. +Dadurch wird sichergestellt das alle Teilnehmer am Datenraum das Clearinghouse akzeptieren. + +## Aktuelle Implementierung +Der Endpunkt ```POST /messages/log/:PID``` wird mit einer zufällig generierten PID aufgerufen. Das hat einige Nachteile: +- Es wird für jede Transaktion ein neuer Prozess angelegt. +- Transaktionen können nicht gruppiert (einem Vertrag zugeordnet) werden. +- Transaktionen von anderen Connectoren können nicht zur gleichen Transaktion gefiltert werden. + +## Optimierter Ansatz +Bevor eine Transaktion stattfindet, wird ein Vertrag geschlossen. In diesem Schritt könnte der Prozess im Clearinghouse bereits angelegt werden. Hierbei ist es auch möglich, mehrere Connector IDs anzugeben, um festzulegen, wer Lese- und Schreibrechte besitzt. +- Die erstellte PID muss mit allen Connectoren geteilt werden. +- Die Connectoren können auf die gleiche PID loggen, um die Transaktionen nach Verträgen zu gruppieren. +- Der MDS kann seinen eigenen Connector als Standard festlegen, um Zugriff auf alle Transaktionen zu erhalten. + +## Ablauf +### CreateLogMessage +![](./images/CreateLogMessage.png) + +### CreatePID +![](./images/CreatePid.png) \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 6fa552e..436543d 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -8,5 +8,6 @@ - [Architecture](content/internals/architecture.md) - [Communication](content/internals/communication.md) - [Functionality](content/internals/functionality.md) +- [Proposal](Proposal.md) diff --git a/docs/images/CreateLogMessage.png b/docs/images/CreateLogMessage.png new file mode 100644 index 0000000000000000000000000000000000000000..4829d7ee5085e575592a57a085f0003c5d0ea53c GIT binary patch literal 27647 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 literal 0 HcmV?d00001 diff --git a/docs/images/CreatePid.png b/docs/images/CreatePid.png new file mode 100644 index 0000000000000000000000000000000000000000..6405cb849da972a28be6e844689d48925c1473a9 GIT binary patch literal 25875 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 literal 0 HcmV?d00001 From 36bfaa3f569ee86be8f8cc072cb951aeaca8e295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 4 Oct 2023 14:21:42 +0200 Subject: [PATCH 092/183] feat(docs): Enable GitHub Pages generation --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index ffdcb1a..cc8f9a8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -6,7 +6,7 @@ name: Deploy to Github-Pages on: # Triggers the workflow on push or pull request events but only for the main branch push: - branches: [ main ] + branches: [ development ] # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: @@ -24,4 +24,4 @@ jobs: uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./book \ No newline at end of file + publish_dir: ./book From 24e87efc96516a22dc1edc4d89662cebd537d2bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 4 Oct 2023 14:32:24 +0200 Subject: [PATCH 093/183] feat(docs): add d2 diagramming integration to workflow --- .github/workflows/docs.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cc8f9a8..d211040 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,10 +17,17 @@ jobs: - name: mdBook Action uses: peaceiris/actions-mdbook@v1 + + - name: Install D2 (Diagram generator) + run: curl -fsSL https://d2lang.com/install.sh | sh -s -- + + - name: Install mdbook-d2 integration + run: cargo install mdbook-d2 --locked - - run: mdbook build + - name: Build book + run: mdbook build - - name: Deploy + - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} From 26135597ccc4a8f9f040f496732fb7e275504ce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 4 Oct 2023 14:40:18 +0200 Subject: [PATCH 094/183] fix(ch-app): Fix security issue through updating dependencies --- clearing-house-app/Cargo.lock | 42 ++++++++++++++++------------------- clearing-house-app/Cargo.toml | 2 +- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 40acce9..e586388 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -1100,9 +1100,9 @@ dependencies = [ [[package]] name = "mongodb" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16928502631c0db72214720aa479c722397fe5aed6bf1c740a3830b3fe4bfcfe" +checksum = "e22d517e7e678e1c9a2983ec704b43f3b22f38b1b7a247ea3ddb36d21578bf4e" dependencies = [ "async-trait", "base64 0.13.1", @@ -1585,14 +1585,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.9" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" dependencies = [ "log", "ring", + "rustls-webpki", "sct", - "webpki", ] [[package]] @@ -1604,6 +1604,16 @@ dependencies = [ "base64 0.21.4", ] +[[package]] +name = "rustls-webpki" +version = "0.101.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -2036,13 +2046,12 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ "rustls", "tokio", - "webpki", ] [[package]] @@ -2383,24 +2392,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "webpki-roots" -version = "0.22.6" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] +checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "widestring" diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 5b468f0..18e2b28 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" # JWT biscuit = "0.6.0" # Database -mongodb = { version = ">= 2.6.1" , features = ["openssl-tls"]} +mongodb = { version = ">= 2.7.0" , features = ["openssl-tls"]} # Serialization serde = { version = ">1.0.184", features = ["derive"] } serde_json = "1" From 077716d3571fbf4dd9afa5d4750c00ab9c6e5c32 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Wed, 4 Oct 2023 13:15:53 -0300 Subject: [PATCH 095/183] chore (ch-edc): reviewing license headers --- clearing-house-edc/build.gradle.kts | 5 +++-- clearing-house-edc/core/build.gradle.kts | 3 ++- .../truzzt/clearinghouse/edc/app/AppSender.java | 4 ++-- .../edc/app/delegate/AppSenderDelegate.java | 4 ++-- .../edc/app/delegate/LoggingMessageDelegate.java | 13 +++++++++++++ .../clearinghouse/edc/dto/AppSenderRequest.java | 5 ++--- .../clearinghouse/edc/dto/HandlerRequest.java | 4 ++-- .../clearinghouse/edc/dto/HandlerResponse.java | 4 ++-- .../edc/dto/LoggingMessageRequest.java | 5 ++--- .../edc/dto/LoggingMessageResponse.java | 5 ++--- .../truzzt/clearinghouse/edc/handler/Handler.java | 14 ++++++++++++++ .../edc/handler/LogMessageHandler.java | 13 +++++++++++++ .../clearinghouse/edc/types/TypeManagerUtil.java | 6 +++--- .../edc/types/clearinghouse/Context.java | 4 ++-- .../edc/types/clearinghouse/Header.java | 4 ++-- .../edc/types/clearinghouse/SecurityToken.java | 4 ++-- .../edc/types/clearinghouse/TokenFormat.java | 4 ++-- .../clearinghouse/edc/types/ids/Context.java | 4 ++-- .../clearinghouse/edc/types/ids/Message.java | 4 ++-- .../edc/types/ids/RejectionMessage.java | 4 ++-- .../edc/types/ids/RejectionReason.java | 4 ++-- .../edc/types/ids/SecurityToken.java | 4 ++-- .../clearinghouse/edc/types/ids/TokenFormat.java | 4 ++-- .../edc/types/ids/util/VocabUtil.java | 4 ++-- .../clearinghouse/edc/util/ResponseUtil.java | 14 ++++++++++++++ .../clearinghouse/edc/util/SettingsConstants.java | 13 +++++++++++++ .../extensions/multipart/build.gradle.kts | 3 ++- .../edc/multipart/MultipartController.java | 15 ++++++++++++++- .../edc/multipart/MultipartExtension.java | 14 ++++++++++++++ .../org.eclipse.edc.spi.system.ServiceExtension | 4 ++-- .../launchers/connector-local/build.gradle.kts | 4 ++-- .../launchers/connector-local/config.properties | 5 ----- .../launchers/connector-prod/build.gradle.kts | 4 ++++ clearing-house-edc/settings.gradle.kts | 5 +++-- 34 files changed, 152 insertions(+), 58 deletions(-) delete mode 100644 clearing-house-edc/launchers/connector-local/config.properties diff --git a/clearing-house-edc/build.gradle.kts b/clearing-house-edc/build.gradle.kts index b9a452e..083e527 100644 --- a/clearing-house-edc/build.gradle.kts +++ b/clearing-house-edc/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - initial implementation + * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 38506d5..e2b40fe 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,6 +9,7 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java index 978b227..f0584de 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/AppSender.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 sovity GmbH + * Copyright (c) 2023 truzzt GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,7 +8,7 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - initial API and implementation + * truzzt GmbH - Initial implementation * */ package de.truzzt.clearinghouse.edc.app; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java index bd807ea..95ecb31 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/AppSenderDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 sovity GmbH + * Copyright (c) 2023 truzzt GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,7 +8,7 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * sovity GmbH - initial API and implementation + * truzzt GmbH - Initial implementation * */ package de.truzzt.clearinghouse.edc.app.delegate; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java index 7734ce6..bbc9892 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegate.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2023 truzzt GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - Initial implementation + * + */ package de.truzzt.clearinghouse.edc.app.delegate; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java index a1af118..35ad69b 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 truzzt GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,10 +8,9 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - Initial implementation + * truzzt GmbH - Initial implementation * */ - package de.truzzt.clearinghouse.edc.dto; import org.jetbrains.annotations.NotNull; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerRequest.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerRequest.java index 554601c..090ed37 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerRequest.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.dto; import de.truzzt.clearinghouse.edc.types.ids.Message; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java index fbb048b..e79ae88 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/HandlerResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.dto; import de.truzzt.clearinghouse.edc.types.ids.Message; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageRequest.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageRequest.java index 172c84c..32c96a6 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageRequest.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageRequest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 truzzt GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,10 +8,9 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - Initial implementation + * truzzt GmbH - Initial implementation * */ - package de.truzzt.clearinghouse.edc.dto; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageResponse.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageResponse.java index 0db5f92..ac90345 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageResponse.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/LoggingMessageResponse.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 truzzt GmbH * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,10 +8,9 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - Initial implementation + * truzzt GmbH - Initial implementation * */ - package de.truzzt.clearinghouse.edc.dto; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java index c70a71d..88cc093 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation + * + */ package de.truzzt.clearinghouse.edc.handler; import com.auth0.jwt.JWT; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java index ecab1df..2d48528 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2023 truzzt GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - Initial implementation + * + */ package de.truzzt.clearinghouse.edc.handler; import de.truzzt.clearinghouse.edc.app.AppSender; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java index b4d744b..254dfef 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,10 +8,10 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * truzzt GmbH - Initial implementation + * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types; import com.fasterxml.jackson.core.JsonProcessingException; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java index f0b800a..a835cde 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Context.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.clearinghouse; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Header.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Header.java index dfc3dd6..f5e55c7 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Header.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/Header.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.clearinghouse; import com.fasterxml.jackson.annotation.JsonFormat; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java index 8ca532b..9f34154 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/SecurityToken.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.clearinghouse; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/TokenFormat.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/TokenFormat.java index 216c4ad..13fd596 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/TokenFormat.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/clearinghouse/TokenFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.clearinghouse; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Context.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Context.java index 14a2223..d0bd908 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Context.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Context.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java index 323c3dd..8cac60c 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonFormat; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java index ce4652a..61b2247 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java index 9fdde06..506998b 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionReason.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java index 795e708..b8f1b46 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java index 6d05134..56d86cd 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java index 5f16361..15a7680 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,9 +9,9 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - package de.truzzt.clearinghouse.edc.types.ids.util; import java.net.MalformedURLException; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java index 11699de..fd8cc46 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation + * + */ package de.truzzt.clearinghouse.edc.util; import de.truzzt.clearinghouse.edc.dto.HandlerResponse; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/SettingsConstants.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/SettingsConstants.java index ddef2f4..79b75ec 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/SettingsConstants.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/SettingsConstants.java @@ -1,3 +1,16 @@ +/* + * Copyright (c) 2023 truzzt GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - initial implementation + * + */ package de.truzzt.clearinghouse.edc.util; public class SettingsConstants { diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index 00a7a0e..5de17ae 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,6 +9,7 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index 673f00b..065493c 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation + * + */ package de.truzzt.clearinghouse.edc.multipart; import de.fraunhofer.iais.eis.DynamicAttributeTokenBuilder; @@ -7,7 +21,6 @@ import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.Message; -import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; import de.truzzt.clearinghouse.edc.types.ids.TokenFormat; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java index fa3e3cb..59d3cc6 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java @@ -1,3 +1,17 @@ +/* + * Copyright (c) 2023 Microsoft Corporation + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation + * + */ package de.truzzt.clearinghouse.edc.multipart; import de.truzzt.clearinghouse.edc.handler.Handler; diff --git a/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index ae7a3a9..21d508b 100644 --- a/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/clearing-house-edc/extensions/multipart/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -1,5 +1,5 @@ # -# Copyright (c) 2022 Microsoft Corporation +# Copyright (c) 2023 Microsoft Corporation # # This program and the accompanying materials are made available under the # terms of the Apache License, Version 2.0 which is available at @@ -9,7 +9,7 @@ # # Contributors: # Microsoft Corporation - initial implementation -# +# truzzt GmbH - EDC extension implementation # de.truzzt.clearinghouse.edc.multipart.MultipartExtension \ No newline at end of file diff --git a/clearing-house-edc/launchers/connector-local/build.gradle.kts b/clearing-house-edc/launchers/connector-local/build.gradle.kts index ffe907f..e909111 100644 --- a/clearing-house-edc/launchers/connector-local/build.gradle.kts +++ b/clearing-house-edc/launchers/connector-local/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -9,10 +9,10 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ - plugins { `java-library` id("application") diff --git a/clearing-house-edc/launchers/connector-local/config.properties b/clearing-house-edc/launchers/connector-local/config.properties deleted file mode 100644 index c88ff4d..0000000 --- a/clearing-house-edc/launchers/connector-local/config.properties +++ /dev/null @@ -1,5 +0,0 @@ -edc.truzzt.jwt.audience=1 -edc.truzzt.jwt.issuer=1 -edc.truzzt.jwt.sign.secret=123 -edc.truzzt.jwt.expires.at=60 -edc.truzzt.app.base.url=http://localhost:8000 \ No newline at end of file diff --git a/clearing-house-edc/launchers/connector-prod/build.gradle.kts b/clearing-house-edc/launchers/connector-prod/build.gradle.kts index 09550ed..ecb088f 100644 --- a/clearing-house-edc/launchers/connector-prod/build.gradle.kts +++ b/clearing-house-edc/launchers/connector-prod/build.gradle.kts @@ -1,6 +1,9 @@ /* * Copyright (c) 2021 Microsoft Corporation * +/* + * Copyright (c) 2023 Microsoft Corporation + * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at * https://www.apache.org/licenses/LICENSE-2.0 @@ -9,6 +12,7 @@ * * Contributors: * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ diff --git a/clearing-house-edc/settings.gradle.kts b/clearing-house-edc/settings.gradle.kts index b8d9f44..a21f7ec 100644 --- a/clearing-house-edc/settings.gradle.kts +++ b/clearing-house-edc/settings.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Microsoft Corporation + * Copyright (c) 2023 Microsoft Corporation * * This program and the accompanying materials are made available under the * terms of the Apache License, Version 2.0 which is available at @@ -8,7 +8,8 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - initial implementation + * Microsoft Corporation - Initial implementation + * truzzt GmbH - EDC extension implementation * */ From a8c928064d5e8fe7afff1b99c2b31dc90413a37a Mon Sep 17 00:00:00 2001 From: dhommen Date: Thu, 5 Oct 2023 16:48:57 +0200 Subject: [PATCH 096/183] fic(ci): add version tags to docker images --- .github/workflows/publish.yml | 103 --------------------- .github/workflows/release-publish.yml | 124 ++++++++++++++++++++++++++ .github/workflows/release.yml | 26 ------ 3 files changed, 124 insertions(+), 129 deletions(-) delete mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release-publish.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 7c6e52a..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Publish - -on: - workflow_run: - workflows: [release] - types: - - completed - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - publish_ch_app: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Login to GitHub Container Registry - run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - - name: Get latest release - id: get_latest_release - run: echo "LATEST_RELEASE=${{ github.event.release.tag_name }}" >> $GITHUB_ENV - - - name: Build Docker image - env: - DOCKER_IMAGE_TAG: ${{ env.LATEST_RELEASE }} - run: cd clearing-house-app && docker build -t ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:$DOCKER_IMAGE_TAG . - - - name: Push Docker image - env: - DOCKER_IMAGE_TAG: ${{ env.LATEST_RELEASE }} - run: docker push ghcr.io/truzzt/ids-basecamp-clearinghouse/chapp:$DOCKER_IMAGE_TAG - - publish_ch_edc: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./clearing-house-edc - permissions: - contents: read - packages: write - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Set up JDK 17 - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'temurin' - - - name: Build project extensions - run: ./gradlew clean build - - - name: Install cosign - if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@v2.6.0 - with: - cosign-release: 'v1.13.1' - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v2 - - - name: Log into registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v4 - with: - context: ./clearing-house-edc/launchers/connector-prod - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 - secrets: | - "gpc=gpr.user=${{ github.actor }} - gpr.key=${{ secrets.GITHUB_TOKEN }}" - - - name: Sign the published Docker image - if: ${{ github.event_name != 'pull_request' }} - env: - COSIGN_EXPERIMENTAL: "true" - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000..255384d --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,124 @@ +name: Release +on: + push: + branches: + - master + - alpha + - beta +jobs: + release: + runs-on: ubuntu-latest + outputs: + new_tag_version: ${{ steps.tag_version.outputs.new_tag_version }} + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Node.js + uses: actions/setup-node@v1 + with: + node-version: 18 + - name: Install dependencies + run: npm ci + - name: Dry run to get the next release version + id: tag_version + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + export NEXT_TAG_VERSION=$(npx semantic-release --dry-run | grep 'The next release version is' | sed -E 's/.* ([[:alnum:].\-]+)$/\1/') + echo "new_tag_version=${NEXT_TAG_VERSION}" >> $GITHUB_OUTPUT + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release + + publish-ch-app: + needs: release + if: ${{ needs.release.outputs.new_tag_version != '' }} + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Login to GitHub Container Registry + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build Docker image + env: + DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} + run: cd clearing-house-app && docker build -t ghcr.io/truzzt/ids-basecamp-clearing/ch-app:$DOCKER_IMAGE_TAG . + + - name: Push Docker image + env: + DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} + run: docker push ghcr.io/truzzt/ids-basecamp-clearing/ch-app:$DOCKER_IMAGE_TAG + + publish-ch-edc: + runs-on: ubuntu-latest + needs: release + if: ${{ needs.release.outputs.new_tag_version != '' }} + defaults: + run: + working-directory: ./clearing-house-edc + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'temurin' + + - name: Build project extensions + run: ./gradlew clean build + + - name: Install cosign + if: github.event_name != 'pull_request' + uses: sigstore/cosign-installer@v2.6.0 + with: + cosign-release: 'v1.13.1' + + - name: Setup Docker buildx + uses: docker/setup-buildx-action@v2 + + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v4 + with: + context: ./clearing-house-edc/launchers/connector-prod + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ needs.release.outputs.new_tag_version }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + secrets: | + "gpc=gpr.user=${{ github.actor }} + gpr.key=${{ secrets.GITHUB_TOKEN }}" + + - name: Sign the published Docker image + if: ${{ github.event_name != 'pull_request' }} + env: + COSIGN_EXPERIMENTAL: "true" + run: echo "${{ needs.release.outputs.new_tag_version }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 950d799..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Release -on: - push: - branches: - - master - - alpha - - beta -jobs: - release: - name: Release - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v1 - with: - node-version: 18 - - name: Install dependencies - run: npm ci - - name: Release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release From 8542389fb851b24532d1ebaf25a275a83ff94634 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 10 Oct 2023 16:18:40 -0300 Subject: [PATCH 097/183] feat (ch-edc): unit tests base structure --- clearing-house-edc/build.gradle.kts | 7 ++- clearing-house-edc/core/build.gradle.kts | 1 + .../clearinghouse/edc/util/ResponseUtil.java | 30 ++++++++- .../extensions/multipart/build.gradle.kts | 15 +++++ .../edc/multipart/MultipartController.java | 41 ++++-------- .../multipart/MultipartControllerTest.java | 62 +++++++++++++++++++ 6 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java diff --git a/clearing-house-edc/build.gradle.kts b/clearing-house-edc/build.gradle.kts index 083e527..c76c9fe 100644 --- a/clearing-house-edc/build.gradle.kts +++ b/clearing-house-edc/build.gradle.kts @@ -15,6 +15,7 @@ plugins { `java-library` + `jacoco-report-aggregation` } val javaVersion: String by project @@ -23,4 +24,8 @@ java { toolchain { languageVersion = JavaLanguageVersion.of(javaVersion) } -} \ No newline at end of file +} + +tasks.check { + dependsOn(tasks.named("testCodeCoverageReport")) +} diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index e2b40fe..7256820 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(edc.ids) implementation(edc.ids.jsonld.serdes) implementation(edc.api.management.config) + implementation(libs.jersey.multipart) implementation("com.auth0:java-jwt:${auth0JWTVersion}") } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java index fd8cc46..de09a5b 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java @@ -15,15 +15,20 @@ package de.truzzt.clearinghouse.edc.util; import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.Message; import de.truzzt.clearinghouse.edc.types.ids.RejectionMessage; import de.truzzt.clearinghouse.edc.types.ids.RejectionReason; +import jakarta.ws.rs.core.MediaType; import org.eclipse.edc.protocol.ids.spi.domain.IdsConstants; import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.protocol.ids.spi.types.IdsType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.glassfish.jersey.media.multipart.FormDataBodyPart; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; + import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; @@ -36,6 +41,29 @@ public class ResponseUtil { private static final String PROCESSED_NOTIFICATION_TYPE = "ids:MessageProcessedNotificationMessage"; + public static FormDataMultiPart createFormDataMultiPart(TypeManagerUtil typeManagerUtil, + String headerName, + Message headerValue, + String payloadName, + Object payloadValue) { + var multiPart = createFormDataMultiPart(typeManagerUtil, headerName, headerValue); + + if (payloadValue != null) { + multiPart.bodyPart(new FormDataBodyPart(payloadName, typeManagerUtil.toJson(payloadValue), MediaType.APPLICATION_JSON_TYPE)); + } + + return multiPart; + } + + public static FormDataMultiPart createFormDataMultiPart(TypeManagerUtil typeManagerUtil, String headerName, Message headerValue) { + var multiPart = new FormDataMultiPart(); + + if (headerValue != null) { + multiPart.bodyPart(new FormDataBodyPart(headerName, typeManagerUtil.toJson(headerValue), MediaType.APPLICATION_JSON_TYPE)); + } + return multiPart; + } + public static HandlerResponse createMultipartResponse(@NotNull Message header, @NotNull Object payload) { return HandlerResponse.Builder.newInstance() .header(header) @@ -114,7 +142,7 @@ private static URI getMessageId() { return IdsId.Builder.newInstance().value(UUID.randomUUID().toString()).type(IdsType.MESSAGE).build().toUri(); } - public static XMLGregorianCalendar gregorianNow() { + private static XMLGregorianCalendar gregorianNow() { try { GregorianCalendar gregorianCalendar = GregorianCalendar.from(ZonedDateTime.now()); return DatatypeFactory.newInstance().newXMLGregorianCalendar(gregorianCalendar); diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index 5de17ae..3e5010b 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -15,6 +15,7 @@ plugins { `java-library` + `jacoco-report-aggregation` } dependencies { @@ -27,4 +28,18 @@ dependencies { implementation(edc.api.management.config) implementation(libs.jakarta.rsApi) implementation(libs.jersey.multipart) + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.mockito.inline) + testImplementation(libs.mockito.inline) + + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks.test { + useJUnitPlatform() +} + +tasks.check { + dependsOn(tasks.named("testCodeCoverageReport")) } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index 065493c..67e829b 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -33,14 +33,13 @@ import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.spi.monitor.Monitor; -import org.glassfish.jersey.media.multipart.FormDataBodyPart; import org.glassfish.jersey.media.multipart.FormDataParam; -import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.jetbrains.annotations.NotNull; import java.io.InputStream; import java.util.List; +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.createFormDataMultiPart; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.internalRecipientError; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.malformedMessage; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.messageTypeNotSupported; @@ -88,7 +87,7 @@ public Response request(@PathParam(PID) String pid, if (headerInputStream == null) { monitor.severe(LOG_ID + ": Header is missing"); return Response.status(Response.Status.BAD_REQUEST) - .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) .build(); } @@ -99,7 +98,7 @@ public Response request(@PathParam(PID) String pid, } catch (Exception e) { monitor.severe(format(LOG_ID + ": Header parsing failed: %s", e.getMessage())); return Response.status(Response.Status.BAD_REQUEST) - .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) .build(); } @@ -111,7 +110,7 @@ public Response request(@PathParam(PID) String pid, || header.getIssuerConnector() == null || header.getSenderAgent() == null) { return Response.status(Response.Status.BAD_REQUEST) - .entity(createFormDataMultiPart(malformedMessage(header, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(header, connectorId))) .build(); } @@ -120,7 +119,7 @@ public Response request(@PathParam(PID) String pid, if (securityToken == null || securityToken.getTokenValue() == null) { monitor.severe(LOG_ID + ": Token is missing in header"); return Response.status(Response.Status.BAD_REQUEST) - .entity(createFormDataMultiPart(notAuthenticated(header, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, notAuthenticated(header, connectorId))) .build(); } @@ -129,7 +128,7 @@ public Response request(@PathParam(PID) String pid, if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { monitor.severe(LOG_ID + ": Invalid security token type: " + tokenFormat); return Response.status(Response.Status.BAD_REQUEST) - .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) .build(); } @@ -137,14 +136,14 @@ public Response request(@PathParam(PID) String pid, if (payload == null) { monitor.severe(LOG_ID + ": Payload is missing"); return Response.status(Response.Status.BAD_REQUEST) - .entity(createFormDataMultiPart(malformedMessage(null, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) .build(); } // Validate DAT if (!validateToken(header)) { return Response.status(Response.Status.FORBIDDEN) - .entity(createFormDataMultiPart(notAuthenticated(header, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, notAuthenticated(header, connectorId))) .build(); } @@ -168,20 +167,20 @@ public Response request(@PathParam(PID) String pid, } catch (Exception e) { monitor.severe(LOG_ID + ": Error in message handler processing", e); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(createFormDataMultiPart(internalRecipientError(header, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, internalRecipientError(header, connectorId))) .build(); } // Get the response token if (!getResponseToken(header, handlerResponse)) { return Response.status(Response.Status.INTERNAL_SERVER_ERROR) - .entity(createFormDataMultiPart(internalRecipientError(header, connectorId))) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, internalRecipientError(header, connectorId))) .build(); } // Build the response return Response.status(Response.Status.CREATED) - .entity(createFormDataMultiPart(handlerResponse.getHeader(), handlerResponse.getPayload())) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, handlerResponse.getHeader(), PAYLOAD, handlerResponse.getPayload())) .build(); } @@ -232,22 +231,4 @@ private boolean getResponseToken(Message header, HandlerResponse handlerResponse }*/ } - private FormDataMultiPart createFormDataMultiPart(Message header, Object payload) { - var multiPart = createFormDataMultiPart(header); - - if (payload != null) { - multiPart.bodyPart(new FormDataBodyPart(PAYLOAD, typeManagerUtil.toJson(payload), MediaType.APPLICATION_JSON_TYPE)); - } - - return multiPart; - } - - private FormDataMultiPart createFormDataMultiPart(Message header) { - var multiPart = new FormDataMultiPart(); - if (header != null) { - multiPart.bodyPart(new FormDataBodyPart(HEADER, typeManagerUtil.toJson(header), MediaType.APPLICATION_JSON_TYPE)); - } - return multiPart; - } - } diff --git a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java new file mode 100644 index 0000000..8d19233 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java @@ -0,0 +1,62 @@ +package de.truzzt.clearinghouse.edc.multipart; + +import de.truzzt.clearinghouse.edc.handler.Handler; +import de.truzzt.clearinghouse.edc.handler.LogMessageHandler; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.types.ids.Message; +import de.truzzt.clearinghouse.edc.types.ids.RejectionMessage; +import jakarta.ws.rs.core.Response; +import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.monitor.Monitor; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.util.List; +import java.util.UUID; + +import static org.mockito.Mockito.mock; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class MultipartControllerTest { + + private static final String IDS_WEBHOOK_ADDRESSS = "http://localhost/callback"; + private static final String TEST_PAYLOAD = "Hello World"; + + private MultipartController controller; + private Monitor monitor; + private IdsId connectorId; + private TypeManagerUtil typeManagerUtil; + private DynamicAttributeTokenService tokenService; + private LogMessageHandler logMessageHandler = mock(LogMessageHandler.class); + + @BeforeEach + public void setUp() { + monitor = mock(Monitor.class); + connectorId = mock(IdsId.class); + typeManagerUtil = mock(TypeManagerUtil.class); + tokenService = mock(DynamicAttributeTokenService.class); + logMessageHandler = mock(LogMessageHandler.class); + + List multipartHandlers = List.of(logMessageHandler); + controller = new MultipartController(monitor, connectorId, typeManagerUtil, tokenService, IDS_WEBHOOK_ADDRESSS, multipartHandlers); + } + + @Test + public void missingHeaderError() { + var pid = UUID.randomUUID().toString(); + + var response = controller.request(pid, null, TEST_PAYLOAD); + assertNotNull(response); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + assertInstanceOf(FormDataMultiPart.class, response.getEntity()); + FormDataMultiPart multiPartResponse = (FormDataMultiPart) response.getEntity(); + // TODO Find a way to get the FormDataMultiPart header value + } + +} From fcd8ceaf5865ab3e3600a60b71c0ce9ebd3044f0 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 11 Oct 2023 11:43:53 +0200 Subject: [PATCH 098/183] chore(docs): update docs with API reference --- docs/SUMMARY.md | 3 +++ docs/content/references/API.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 docs/content/references/API.md diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 436543d..3b06385 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -4,6 +4,9 @@ - [Installation](content/admin-guide/installation.md) - [Maintenance](content/admin-guide/maintenance.md) +# References +- [API](content/references/API.md) + # Internals - [Architecture](content/internals/architecture.md) - [Communication](content/internals/communication.md) diff --git a/docs/content/references/API.md b/docs/content/references/API.md new file mode 100644 index 0000000..549af36 --- /dev/null +++ b/docs/content/references/API.md @@ -0,0 +1,3 @@ +# API Docs + +Swagger and Postman Collection can be found [here](https://github.com/truzzt/ids-basecamp-clearinghouse-postman) \ No newline at end of file From cd59461fb2dfa5b8c95c80fbaa3bafd511e036c0 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 11 Oct 2023 11:56:25 +0200 Subject: [PATCH 099/183] feat(release): add more release types --- .releaserc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.releaserc b/.releaserc index 87d910a..35403cd 100644 --- a/.releaserc +++ b/.releaserc @@ -1,7 +1,17 @@ { "branches": ["+([0-9])?(.{+([0-9]),x}).x", "master", {"name": "beta", "prerelease": true}, {"name": "alpha", "prerelease": true}], "plugins": [ - "@semantic-release/commit-analyzer", + [ + "@semantic-release/commit-analyzer", + { + "preset": "angular", + "releaseRules": [ + { "type": "docs", "release": "patch" }, + { "type": "refactor", "release": "patch" }, + { "scope": "no-release", "release": false } + ] + } + ], "@semantic-release/release-notes-generator", "@semantic-release/changelog", "@semantic-release/git", From f0cb1e149160b945e6e03d2426e6b40165c6fb55 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 11 Oct 2023 12:47:59 +0200 Subject: [PATCH 100/183] fix(ci): simplified ch-edc docker build --- .github/workflows/release-publish.yml | 58 +++++---------------------- 1 file changed, 9 insertions(+), 49 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 255384d..662bbbd 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -70,55 +70,15 @@ jobs: - name: Checkout repository uses: actions/checkout@v3 - - name: Set up JDK 17 - uses: actions/setup-java@v2 - with: - java-version: '17' - distribution: 'temurin' - - - name: Build project extensions - run: ./gradlew clean build - - - name: Install cosign - if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@v2.6.0 - with: - cosign-release: 'v1.13.1' - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v2 - - - name: Log into registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + - name: Login to GitHub Container Registry + run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@v4 - with: - context: ./clearing-house-edc/launchers/connector-prod - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ needs.release.outputs.new_tag_version }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - platforms: linux/amd64,linux/arm64 - secrets: | - "gpc=gpr.user=${{ github.actor }} - gpr.key=${{ secrets.GITHUB_TOKEN }}" + - name: Build Docker image + env: + DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} + run: cd clearing-house-edc && docker build -t ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$DOCKER_IMAGE_TAG . - - name: Sign the published Docker image - if: ${{ github.event_name != 'pull_request' }} + - name: Push Docker image env: - COSIGN_EXPERIMENTAL: "true" - run: echo "${{ needs.release.outputs.new_tag_version }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} + DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} + run: docker push ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$DOCKER_IMAGE_TAG \ No newline at end of file From 8e8026e39059debc5df27f24b58829c081c58da0 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 11 Oct 2023 12:48:23 +0200 Subject: [PATCH 101/183] fix(ch-edc): add multistage dockerfile --- .../launchers/connector-prod/Dockerfile | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/clearing-house-edc/launchers/connector-prod/Dockerfile b/clearing-house-edc/launchers/connector-prod/Dockerfile index 02287d8..901dca4 100644 --- a/clearing-house-edc/launchers/connector-prod/Dockerfile +++ b/clearing-house-edc/launchers/connector-prod/Dockerfile @@ -1,16 +1,19 @@ -FROM openjdk:17-slim-buster +FROM gradle:7-jdk17 AS build + +COPY --chown=gradle:gradle . /home/gradle/project/ +WORKDIR /home/gradle/project/ +RUN ./gradlew clean build +RUN ls -la + -RUN apt update \ - && apt install -y curl \ - && rm -rf /var/cache/apt/archives /var/lib/apt/lists +FROM openjdk:17-slim-buster WORKDIR /app -COPY ./build/libs/clearing-house-edc.jar /app + +COPY --from=build /home/gradle/project/build/libs/clearing-house-edc.jar /app ENV WEB_HTTP_PORT="8181" ENV WEB_HTTP_PATH="/api" -HEALTHCHECK --interval=5s --timeout=5s --retries=10 CMD curl --fail http://localhost:8111/api/check/health - ENTRYPOINT [ "sh", "-c", "exec java $ENV_JVM_ARGS -jar clearing-house-edc.jar"] From 078a8c2ed4ce9a35b387fe55e49e6ae3678cf8b6 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 11 Oct 2023 17:02:32 +0200 Subject: [PATCH 102/183] set correct file locations --- .github/workflows/release-publish.yml | 2 +- clearing-house-edc/launchers/connector-prod/Dockerfile | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 662bbbd..bba3ad3 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -76,7 +76,7 @@ jobs: - name: Build Docker image env: DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} - run: cd clearing-house-edc && docker build -t ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$DOCKER_IMAGE_TAG . + run: cd clearing-house-edc && docker build -t ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$DOCKER_IMAGE_TAG -f launchers/connector-prod/Dockerfile . - name: Push Docker image env: diff --git a/clearing-house-edc/launchers/connector-prod/Dockerfile b/clearing-house-edc/launchers/connector-prod/Dockerfile index 901dca4..66284fe 100644 --- a/clearing-house-edc/launchers/connector-prod/Dockerfile +++ b/clearing-house-edc/launchers/connector-prod/Dockerfile @@ -3,15 +3,12 @@ FROM gradle:7-jdk17 AS build COPY --chown=gradle:gradle . /home/gradle/project/ WORKDIR /home/gradle/project/ RUN ./gradlew clean build -RUN ls -la - FROM openjdk:17-slim-buster WORKDIR /app - -COPY --from=build /home/gradle/project/build/libs/clearing-house-edc.jar /app +COPY --from=build /home/gradle/project/launchers/connector-prod/build/libs/clearing-house-edc.jar /app ENV WEB_HTTP_PORT="8181" ENV WEB_HTTP_PATH="/api" From f1612e027f9815ad9525c7f78aab876baf1f64a1 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Thu, 12 Oct 2023 17:45:35 -0300 Subject: [PATCH 103/183] feat: Create TestUtils with mock and start to create application tests --- .../edc/dto/AppSenderRequest.java | 2 +- .../clearinghouse/edc/app/AppSenderTest.java | 100 +++++++++++++++ .../clearinghouse/edc/app/TestUtils.java | 121 ++++++++++++++++++ .../delegate/LoggingMessageDelegateTest.java | 63 +++++++++ .../clearinghouse/edc/app/logMessage.json | 20 +++ 5 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/TestUtils.java create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/logMessage.json diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java index 35ad69b..dfd7397 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/AppSenderRequest.java @@ -23,7 +23,7 @@ public class AppSenderRequest { private final String token; private final B body; - private AppSenderRequest(@NotNull String url, @NotNull String token, @NotNull B body) { + public AppSenderRequest(@NotNull String url, @NotNull String token, @NotNull B body) { this.url = url; this.token = token; this.body = body; diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java new file mode 100644 index 0000000..3c2c5d6 --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java @@ -0,0 +1,100 @@ +package de.truzzt.clearinghouse.edc.app; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.app.delegate.LoggingMessageDelegate; +import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +public class AppSenderTest { + + + + private AppSender sender; + @Mock + private Monitor monitor; + @Mock + private TypeManagerUtil typeManagerUtil; + @Mock + private LoggingMessageDelegate senderDelegate; + @Mock + private ObjectMapper objectMapper; + @Mock + private AppSenderRequest appSenderRequest; + @Mock + private EdcHttpClient httpClient; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + senderDelegate = spy(new LoggingMessageDelegate(typeManagerUtil)); + sender = new AppSender(monitor, httpClient ,typeManagerUtil); + } + + @Test + public void sendSuccessful() throws IOException { + + doReturn(TestUtils.getValidHandlerRequest().toString().getBytes()).when(typeManagerUtil).toJson(any(Object.class)); + doReturn(TestUtils.getValidResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(httpClient).execute(any(Request.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + + var response = sender.send(TestUtils.getValidAppSenderRequest(), senderDelegate); + + assertNotNull(response); + } + + @Test + public void sendWithHttpResquestError() throws IOException { + + doReturn(TestUtils.getValidHandlerRequest().toString().getBytes()).when(typeManagerUtil).toJson(any(Object.class)); + + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> sender.send(TestUtils.getInvalidUrlAppSenderRequest(), senderDelegate)); + + assertEquals("Expected URL scheme 'http' or 'https'", exception.getMessage().substring(0,37)); + } + + @Test + public void sendWithUnsuccessfulResponseBodyError() throws IOException { + + doReturn(TestUtils.getValidHandlerRequest().toString().getBytes()).when(typeManagerUtil).toJson(any(Object.class)); + doReturn(TestUtils.getUnsuccessfulResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(httpClient).execute(any(Request.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + + EdcException exception = assertThrows(EdcException.class, () -> sender.send(TestUtils.getValidAppSenderRequest(), senderDelegate)); + + assertEquals("Received an error from Clearing House App. Status: 401, message: Unauthorized", exception.getMessage()); + } + + @Test + public void sendWithNullResponseBodyError() throws IOException { + + doReturn(TestUtils.getValidHandlerRequest().toString().getBytes()).when(typeManagerUtil).toJson(any(Object.class)); + doReturn(TestUtils.getResponseWithoutBody(TestUtils.getValidAppSenderRequest().getUrl())).when(httpClient).execute(any(Request.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + + EdcException exception = assertThrows(EdcException.class, () -> sender.send(TestUtils.getValidAppSenderRequest(), senderDelegate)); + + assertEquals("Error reading Clearing House App response body", exception.getMessage()); + } + + + + +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/TestUtils.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/TestUtils.java new file mode 100644 index 0000000..b91d87c --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/TestUtils.java @@ -0,0 +1,121 @@ +package de.truzzt.clearinghouse.edc.app; + +import com.auth0.jwt.JWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; +import de.truzzt.clearinghouse.edc.types.ids.Message; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class TestUtils { + + public static final String TEST_PAYLOAD = "Hello World"; + public static final String TEST_BASE_URL = "http://localhost:8000"; + + public static Message getValidHeader() { + try { + ObjectMapper mapper = new ObjectMapper(); + + File file = new File("src/test/java/de/truzzt/clearinghouse/edc/app/logMessage.json"); + file.createNewFile(); + + Message message = mapper.readValue(file, Message.class); + + return message; + } catch (IOException ioe){ + ioe.printStackTrace(); + return null; + } + } + + public static Response getValidResponse(String url) { + + Request mockRequest = new Request.Builder() + .url(url) + .build(); + ResponseBody body = getValidResponseBody(); + + Headers headers = new Headers.Builder().add("Test","Test").build(); + + return new Response(mockRequest, Protocol.HTTP_2, "", 200, null, + headers, body, null, null, + null, 1000L, 1000L, null); + } + + public static Response getResponseWithoutBody(String url) { + + Request mockRequest = new Request.Builder() + .url(url) + .build(); + + Headers headers = new Headers.Builder().add("Test","Test").build(); + + return new Response(mockRequest, Protocol.HTTP_2, "", 200, null, + headers, null, null, null, + null, 1000L, 1000L, null); + } + + public static Response getUnsuccessfulResponse(String url) { + + Request mockRequest = new Request.Builder() + .url(url) + .build(); + ResponseBody body = getValidResponseBody(); + + Headers headers = new Headers.Builder().add("Test","Test").build(); + + return new Response(mockRequest, Protocol.HTTP_2, "Unauthorized", 401, null, + headers, body, null, null, + null, 1000L, 1000L, null); + } + + public static LoggingMessageResponse getValidLoggingMessageResponse(String url) { + try { + ObjectMapper mapper = new ObjectMapper(); + + return mapper.readValue(getValidResponse(url).body().byteStream(), LoggingMessageResponse.class); + + } catch (IOException ioe) { + ioe.printStackTrace(); + return null; + } + } + public static ResponseBody getValidResponseBody(){ + return ResponseBody.create( + MediaType.get("application/json; charset=utf-8"), + "{}" + ); + } + + + public static HandlerRequest getValidHandlerRequest(){ + return HandlerRequest.Builder.newInstance() + .pid(UUID.randomUUID().toString()) + .header(getValidHeader() ) + .payload(TEST_PAYLOAD).build(); + } + + public static AppSenderRequest getValidAppSenderRequest(){ + return new AppSenderRequest(TEST_BASE_URL+"/messages/log/" + UUID.randomUUID(), + JWT.create().toString(), + getValidHandlerRequest() + ); + } + + public static AppSenderRequest getInvalidUrlAppSenderRequest(){ + return new AppSenderRequest("" + UUID.randomUUID(), + JWT.create().toString(), + getValidHandlerRequest() + ); + } +} diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java new file mode 100644 index 0000000..9c78d2d --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java @@ -0,0 +1,63 @@ +package de.truzzt.clearinghouse.edc.app.delegate; + +import de.truzzt.clearinghouse.edc.app.TestUtils; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageRequest; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import okhttp3.ResponseBody; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +class LoggingMessageDelegateTest { + + @Mock + private TypeManagerUtil typeManagerUtil; + @Mock + private LoggingMessageDelegate senderDelegate; + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + senderDelegate = spy(new LoggingMessageDelegate(typeManagerUtil)); + } + + @Test + public void successfulBuildRequestUrl() { + + HandlerRequest request = TestUtils.getValidHandlerRequest(); + + String response = senderDelegate.buildRequestUrl(TestUtils.TEST_BASE_URL, request); + + assertNotNull(response); + assertEquals(response, "http://localhost:8000/messages/log/" +request.getPid()); + } + + @Test + public void successfulBuildRequestBody() { + + HandlerRequest request = TestUtils.getValidHandlerRequest(); + + LoggingMessageRequest response = senderDelegate.buildRequestBody(request); + + assertNotNull(response); + } + + @Test + public void successfulParseResponseBody() { + + ResponseBody body = TestUtils.getValidResponseBody(); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + LoggingMessageResponse response = senderDelegate.parseResponseBody(body); + + assertNotNull(response); + } + +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/logMessage.json b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/logMessage.json new file mode 100644 index 0000000..85057bb --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/logMessage.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:LogMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file From 5127591162bec3ee6e92227ffbb80f36ffa08f62 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Mon, 16 Oct 2023 09:11:15 -0300 Subject: [PATCH 104/183] feat: AppSender, LoggingMessageDelegate, LogMessageHandler tests implemented --- .../truzzt/clearinghouse/edc/TestUtils.java | 232 ++++++++++++++++++ .../clearinghouse/edc/app/AppSenderTest.java | 1 + .../clearinghouse/edc/app/TestUtils.java | 121 --------- .../delegate/LoggingMessageDelegateTest.java | 2 +- .../edc/handler/LogMessageHandlerTest.java | 123 ++++++++++ .../edc/{app => }/logMessage.json | 0 6 files changed, 357 insertions(+), 122 deletions(-) create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/TestUtils.java delete mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/TestUtils.java create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java rename clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/{app => }/logMessage.json (100%) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/TestUtils.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/TestUtils.java new file mode 100644 index 0000000..98f757b --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/TestUtils.java @@ -0,0 +1,232 @@ +package de.truzzt.clearinghouse.edc; + +import com.auth0.jwt.JWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.app.AppSender; +import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageRequest; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; +import de.truzzt.clearinghouse.edc.handler.LogMessageHandler; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.types.clearinghouse.Context; +import de.truzzt.clearinghouse.edc.types.clearinghouse.Header; +import de.truzzt.clearinghouse.edc.types.clearinghouse.SecurityToken; +import de.truzzt.clearinghouse.edc.types.clearinghouse.TokenFormat; +import de.truzzt.clearinghouse.edc.types.ids.Message; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtensionContext; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public class TestUtils { + + public static final String TEST_PAYLOAD = "Hello World"; + public static final String TEST_BASE_URL = "http://localhost:8000"; + + public static final String LOG_MESSAGE_JSON_PATH = "src/test/java/de/truzzt/clearinghouse/edc/logMessage.json"; + + public static Message getValidHeader() { + try { + ObjectMapper mapper = new ObjectMapper(); + + File file = new File(LOG_MESSAGE_JSON_PATH); + file.createNewFile(); + + Message message = mapper.readValue(file, Message.class); + + return message; + } catch (IOException ioe){ + ioe.printStackTrace(); + return null; + } + } + + public static Message getinvalidTokenHeader() { + try { + ObjectMapper mapper = new ObjectMapper(); + + File file = new File(LOG_MESSAGE_JSON_PATH); + file.createNewFile(); + + Message message = mapper.readValue(file, Message.class); + message.getSecurityToken().setTokenValue("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzY29wZXMiOlsiaWRzYz" + + "pJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjo" + + "iaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSw" + + "ianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWx" + + "lIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLml" + + "kcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHB" + + "zOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc" + + "0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCJ9.hekZoP" + + "DjEWaXreQl3l0PUIjBOPQhAl0w2mH4_PdNWuA"); + return message; + } catch (IOException ioe){ + ioe.printStackTrace(); + return null; + } + } + + public static Message getNotLogMessageValidHeader() { + try { + ObjectMapper mapper = new ObjectMapper(); + + File file = new File(LOG_MESSAGE_JSON_PATH); + file.createNewFile(); + + Message message = mapper.readValue(file, Message.class); + message.setType("ids:otherMessage"); + return message; + } catch (IOException ioe){ + ioe.printStackTrace(); + return null; + } + } + + public static Response getValidResponse(String url) { + + Request mockRequest = new Request.Builder() + .url(url) + .build(); + ResponseBody body = getValidResponseBody(); + + Headers headers = new Headers.Builder().add("Test","Test").build(); + + return new Response(mockRequest, Protocol.HTTP_2, "", 200, null, + headers, body, null, null, + null, 1000L, 1000L, null); + } + + public static Response getResponseWithoutBody(String url) { + + Request mockRequest = new Request.Builder() + .url(url) + .build(); + + Headers headers = new Headers.Builder().add("Test","Test").build(); + + return new Response(mockRequest, Protocol.HTTP_2, "", 200, null, + headers, null, null, null, + null, 1000L, 1000L, null); + } + + public static Response getUnsuccessfulResponse(String url) { + + Request mockRequest = new Request.Builder() + .url(url) + .build(); + ResponseBody body = getValidResponseBody(); + + Headers headers = new Headers.Builder().add("Test","Test").build(); + + return new Response(mockRequest, Protocol.HTTP_2, "Unauthorized", 401, null, + headers, body, null, null, + null, 1000L, 1000L, null); + } + + public static LoggingMessageResponse getValidLoggingMessageResponse(String url) { + try { + ObjectMapper mapper = new ObjectMapper(); + + return mapper.readValue(getValidResponse(url).body().byteStream(), LoggingMessageResponse.class); + + } catch (IOException ioe) { + ioe.printStackTrace(); + return null; + } + } + + public static LoggingMessageRequest getValidLoggingMessageRequest(HandlerRequest handlerRequest) { + + var header = handlerRequest.getHeader(); + + var multipartContext = header.getContext(); + var context = new Context(multipartContext.getIds(), multipartContext.getIdsc()); + + var multipartSecurityToken = header.getSecurityToken(); + var multipartTokenFormat = multipartSecurityToken.getTokenFormat(); + var securityToken = SecurityToken.Builder.newInstance(). + type(multipartSecurityToken.getType()). + id(multipartSecurityToken.getId()). + tokenFormat(new TokenFormat(multipartTokenFormat.getId())). + tokenValue(multipartSecurityToken.getTokenValue()). + build(); + + var requestHeader = Header.Builder.newInstance() + .context(context) + .id(header.getId()) + .type(header.getType()) + .securityToken(securityToken) + .issuerConnector(header.getIssuerConnector()) + .modelVersion(header.getModelVersion()) + .issued(header.getIssued()) + .senderAgent(header.getSenderAgent()) + .build(); + + return new LoggingMessageRequest(requestHeader, handlerRequest.getPayload()); + + } + public static ResponseBody getValidResponseBody(){ + return ResponseBody.create( + MediaType.get("application/json; charset=utf-8"), + "{}" + ); + } + + + public static HandlerRequest getValidHandlerRequest(){ + return HandlerRequest.Builder.newInstance() + .pid(UUID.randomUUID().toString()) + .header(getValidHeader() ) + .payload(TEST_PAYLOAD).build(); + } + + public static HandlerRequest getInvalidTokenHandlerRequest(){ + return HandlerRequest.Builder.newInstance() + .pid(UUID.randomUUID().toString()) + .header(getinvalidTokenHeader()) + .payload(TEST_PAYLOAD).build(); + } + + public static HandlerRequest getInvalidHandlerRequest(){ + return HandlerRequest.Builder.newInstance() + .pid(UUID.randomUUID().toString()) + .header(getNotLogMessageValidHeader() ) + .payload(TEST_PAYLOAD).build(); + } + + public static AppSenderRequest getValidAppSenderRequest(){ + return new AppSenderRequest(TEST_BASE_URL+"/messages/log/" + UUID.randomUUID(), + JWT.create().toString(), + getValidHandlerRequest() + ); + } + + public static AppSenderRequest getInvalidUrlAppSenderRequest(){ + return new AppSenderRequest("" + UUID.randomUUID(), + JWT.create().toString(), + getValidHandlerRequest() + ); + } + + public static String getBuildJwtToken(Monitor monitor, + IdsId connectorId, + TypeManagerUtil typeManagerUtil, + AppSender appSender, + ServiceExtensionContext context, + HandlerRequest handlerRequest){ + + LogMessageHandler handler = new LogMessageHandler(monitor, connectorId, typeManagerUtil, appSender,context); + return handler.buildJWTToken(handlerRequest.getHeader().getSecurityToken(), context); + } + + +} diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java index 3c2c5d6..3a17f61 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java @@ -1,6 +1,7 @@ package de.truzzt.clearinghouse.edc.app; import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.TestUtils; import de.truzzt.clearinghouse.edc.app.delegate.LoggingMessageDelegate; import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/TestUtils.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/TestUtils.java deleted file mode 100644 index b91d87c..0000000 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/TestUtils.java +++ /dev/null @@ -1,121 +0,0 @@ -package de.truzzt.clearinghouse.edc.app; - -import com.auth0.jwt.JWT; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; -import de.truzzt.clearinghouse.edc.dto.HandlerRequest; -import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; -import de.truzzt.clearinghouse.edc.types.ids.Message; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Protocol; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; - -import java.io.File; -import java.io.IOException; -import java.util.UUID; - -public class TestUtils { - - public static final String TEST_PAYLOAD = "Hello World"; - public static final String TEST_BASE_URL = "http://localhost:8000"; - - public static Message getValidHeader() { - try { - ObjectMapper mapper = new ObjectMapper(); - - File file = new File("src/test/java/de/truzzt/clearinghouse/edc/app/logMessage.json"); - file.createNewFile(); - - Message message = mapper.readValue(file, Message.class); - - return message; - } catch (IOException ioe){ - ioe.printStackTrace(); - return null; - } - } - - public static Response getValidResponse(String url) { - - Request mockRequest = new Request.Builder() - .url(url) - .build(); - ResponseBody body = getValidResponseBody(); - - Headers headers = new Headers.Builder().add("Test","Test").build(); - - return new Response(mockRequest, Protocol.HTTP_2, "", 200, null, - headers, body, null, null, - null, 1000L, 1000L, null); - } - - public static Response getResponseWithoutBody(String url) { - - Request mockRequest = new Request.Builder() - .url(url) - .build(); - - Headers headers = new Headers.Builder().add("Test","Test").build(); - - return new Response(mockRequest, Protocol.HTTP_2, "", 200, null, - headers, null, null, null, - null, 1000L, 1000L, null); - } - - public static Response getUnsuccessfulResponse(String url) { - - Request mockRequest = new Request.Builder() - .url(url) - .build(); - ResponseBody body = getValidResponseBody(); - - Headers headers = new Headers.Builder().add("Test","Test").build(); - - return new Response(mockRequest, Protocol.HTTP_2, "Unauthorized", 401, null, - headers, body, null, null, - null, 1000L, 1000L, null); - } - - public static LoggingMessageResponse getValidLoggingMessageResponse(String url) { - try { - ObjectMapper mapper = new ObjectMapper(); - - return mapper.readValue(getValidResponse(url).body().byteStream(), LoggingMessageResponse.class); - - } catch (IOException ioe) { - ioe.printStackTrace(); - return null; - } - } - public static ResponseBody getValidResponseBody(){ - return ResponseBody.create( - MediaType.get("application/json; charset=utf-8"), - "{}" - ); - } - - - public static HandlerRequest getValidHandlerRequest(){ - return HandlerRequest.Builder.newInstance() - .pid(UUID.randomUUID().toString()) - .header(getValidHeader() ) - .payload(TEST_PAYLOAD).build(); - } - - public static AppSenderRequest getValidAppSenderRequest(){ - return new AppSenderRequest(TEST_BASE_URL+"/messages/log/" + UUID.randomUUID(), - JWT.create().toString(), - getValidHandlerRequest() - ); - } - - public static AppSenderRequest getInvalidUrlAppSenderRequest(){ - return new AppSenderRequest("" + UUID.randomUUID(), - JWT.create().toString(), - getValidHandlerRequest() - ); - } -} diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java index 9c78d2d..78f716c 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java @@ -1,6 +1,6 @@ package de.truzzt.clearinghouse.edc.app.delegate; -import de.truzzt.clearinghouse.edc.app.TestUtils; +import de.truzzt.clearinghouse.edc.TestUtils; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.LoggingMessageRequest; import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java new file mode 100644 index 0000000..655152a --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -0,0 +1,123 @@ +package de.truzzt.clearinghouse.edc.handler; + +import com.auth0.jwt.JWT; +import de.truzzt.clearinghouse.edc.TestUtils; +import de.truzzt.clearinghouse.edc.app.AppSender; +import de.truzzt.clearinghouse.edc.app.delegate.AppSenderDelegate; +import de.truzzt.clearinghouse.edc.app.delegate.LoggingMessageDelegate; +import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; +import okhttp3.Request; +import okhttp3.ResponseBody; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.IOException; + +import static de.truzzt.clearinghouse.edc.TestUtils.getBuildJwtToken; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.APP_BASE_URL_DEFAULT_VALUE; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_EXPIRES_AT_DEFAULT_VALUE; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +class LogMessageHandlerTest { + @Mock + private Monitor monitor; + @Mock + private IdsId connectorId; + @Mock + private TypeManagerUtil typeManagerUtil; + @Mock + private AppSender appSender; + @Mock + private ServiceExtensionContext context; + @Mock + private LogMessageHandler logMessageHandler; + @Mock + private LoggingMessageDelegate senderDelegate; + @Mock + private EdcHttpClient httpClient; + + private AppSender sender; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + senderDelegate = spy(new LoggingMessageDelegate(typeManagerUtil)); + logMessageHandler = spy(new LogMessageHandler(monitor, connectorId, typeManagerUtil, appSender, context)); + sender = new AppSender(monitor, httpClient ,typeManagerUtil); + } + + @Test + public void successfulCanHandle(){ + + HandlerRequest request = TestUtils.getValidHandlerRequest(); + + Boolean response = logMessageHandler.canHandle(request); + + assertNotNull(response); + assertEquals(response, true); + } + + @Test + public void invalidMessageTypeCanHandle(){ + + HandlerRequest request = TestUtils.getInvalidHandlerRequest(); + + Boolean response = logMessageHandler.canHandle(request); + + assertNotNull(response); + assertEquals(response, false); + } + + @Test + public void successfulHandleRequest(){ + HandlerRequest request = TestUtils.getValidHandlerRequest(); + doReturn(JWT.create().toString()).when(logMessageHandler).buildJWTToken(any(SecurityToken.class), any(ServiceExtensionContext.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + doReturn(APP_BASE_URL_DEFAULT_VALUE+"/messages/log/" + request.getPid()).when(senderDelegate).buildRequestUrl(any(String.class), any(HandlerRequest.class)); + doReturn(TestUtils.getValidLoggingMessageRequest(request)).when(senderDelegate).buildRequestBody(any(HandlerRequest.class)); + + HandlerResponse response = logMessageHandler.handleRequest(request); + + assertNotNull(response); + assertEquals(response.getHeader().getType(), "ids:MessageProcessedNotificationMessage"); + } + + @Test + public void missingSubjectBuildJwtToken() { + EdcException exception = assertThrows(EdcException.class, () -> logMessageHandler.buildJWTToken( + TestUtils.getInvalidTokenHandlerRequest() + .getHeader() + .getSecurityToken(), context)); + + assertEquals("JWT Token subject is missing",exception.getMessage()); + } + + @Test + public void successfulBuildJwtToken() { + var response = logMessageHandler.buildJWTToken( + TestUtils.getValidHandlerRequest() + .getHeader() + .getSecurityToken(), context); + + assertNotNull(response); + } + +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/logMessage.json b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/logMessage.json similarity index 100% rename from clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/logMessage.json rename to clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/logMessage.json From 4d382b5877dda24b6143b08a47549d3c29a61d71 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Mon, 16 Oct 2023 09:44:35 -0300 Subject: [PATCH 105/183] feat: readme added --- clearing-house-edc/README.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 clearing-house-edc/README.md diff --git a/clearing-house-edc/README.md b/clearing-house-edc/README.md new file mode 100644 index 0000000..99cbca3 --- /dev/null +++ b/clearing-house-edc/README.md @@ -0,0 +1,31 @@ +## CLEARING HOUSE +This repository contains the Clearing House Extension that works with the Eclipse Dataspace Connector +allowing logging operations. + +## Install +### Configurations +It is required to configure those parameters: + +| Parameter name | Description | Default value | +|----------------------------------------|-------------------------|------------------------| +| `truzzt.clearinghouse.jwt.audience` | 1 | 1 | +| `truzzt.clearinghouse.jwt.issuer` | 1 | 1 | +| `truzzt.clearinghouse.jwt.sign.secret` | 123 | 123 | +| `truzzt.clearinghouse.jwt.expires.at` | 30 | 30 | +| `truzzt.clearinghouse.app.base.url` | http://localhost:8000 | http://localhost:8000 | + +### Build +To build the project run the command below: + + ./gradlew build + + +### Running +Local execution: + + java -Dedc.fs.config=launchers/connector-local/resources/config.properties -Dedc.keystore=launchers/connector-local/resources/keystore.jks -Dedc.keystore.password=password -Dedc.vault=launchers/connector-local/resources/vault.properties -jar launchers/connector-local/build/libs/clearing-house-edc.jar + + +## Operate + + From 50e0dbc88bbdd9823f8970424df31786ed2ea289 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Sun, 15 Oct 2023 17:56:59 -0300 Subject: [PATCH 106/183] feat (ch-edc): unit tests coverage tool --- clearing-house-edc/build.gradle.kts | 5 ----- clearing-house-edc/core/build.gradle.kts | 11 +++++++++++ .../extensions/multipart/build.gradle.kts | 4 ---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/clearing-house-edc/build.gradle.kts b/clearing-house-edc/build.gradle.kts index c76c9fe..940fa25 100644 --- a/clearing-house-edc/build.gradle.kts +++ b/clearing-house-edc/build.gradle.kts @@ -15,7 +15,6 @@ plugins { `java-library` - `jacoco-report-aggregation` } val javaVersion: String by project @@ -25,7 +24,3 @@ java { languageVersion = JavaLanguageVersion.of(javaVersion) } } - -tasks.check { - dependsOn(tasks.named("testCodeCoverageReport")) -} diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 7256820..3e6db76 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -15,6 +15,7 @@ plugins { `java-library` + `jacoco-report-aggregation` } val auth0JWTVersion: String by project @@ -28,4 +29,14 @@ dependencies { implementation(libs.jersey.multipart) implementation("com.auth0:java-jwt:${auth0JWTVersion}") + + testImplementation(libs.junit.jupiter.api) + testImplementation(libs.mockito.inline) + testImplementation(libs.mockito.inline) + + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks.test { + useJUnitPlatform() } diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index 3e5010b..86d5238 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -39,7 +39,3 @@ dependencies { tasks.test { useJUnitPlatform() } - -tasks.check { - dependsOn(tasks.named("testCodeCoverageReport")) -} From 47493b9719fd8c85fdb18164db42bfd8dd80bb69 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Mon, 16 Oct 2023 17:23:40 -0300 Subject: [PATCH 107/183] feat (ch-edc): unit tests refactoring --- .../edc/{ => tests}/TestUtils.java | 49 ++++++++++++++----- .../messages}/logMessage.json | 0 2 files changed, 38 insertions(+), 11 deletions(-) rename clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/{ => tests}/TestUtils.java (84%) rename clearing-house-edc/core/src/test/{java/de/truzzt/clearinghouse/edc => resources/messages}/logMessage.json (100%) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/TestUtils.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java similarity index 84% rename from clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/TestUtils.java rename to clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java index 98f757b..438dc01 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/TestUtils.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java @@ -21,34 +21,61 @@ import okhttp3.Response; import okhttp3.ResponseBody; import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.glassfish.jersey.internal.routing.RequestSpecificConsumesProducesAcceptor; import java.io.File; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.UUID; public class TestUtils { - public static final String TEST_PAYLOAD = "Hello World"; public static final String TEST_BASE_URL = "http://localhost:8000"; + private static final String TEST_PAYLOAD = "Hello World"; + private static final String VALID_LOG_MESSAGE_HEADER_PATH = "./messages/LogMessage.json"; - public static final String LOG_MESSAGE_JSON_PATH = "src/test/java/de/truzzt/clearinghouse/edc/logMessage.json"; + private static T readJsonFile(ObjectMapper mapper, Class type, String path) { - public static Message getValidHeader() { + ClassLoader classLoader = TestUtils.class.getClassLoader(); + var jsonResource = classLoader.getResource(path); + + if (jsonResource == null) { + throw new EdcException("Header json file not found: " + path); + } + + URI jsonUrl; try { - ObjectMapper mapper = new ObjectMapper(); + jsonUrl = jsonResource.toURI(); + } catch (URISyntaxException e) { + throw new EdcException("Error finding json file on classpath", e); + } - File file = new File(LOG_MESSAGE_JSON_PATH); - file.createNewFile(); + Path filePath = Path.of(jsonUrl); + if (!Files.exists(filePath)) { + throw new EdcException("Header json file not found: " + path); + } - Message message = mapper.readValue(file, Message.class); + T object = null; + try { + var jsonContents = Files.readAllBytes(filePath); + object = mapper.readValue(jsonContents, type); - return message; - } catch (IOException ioe){ - ioe.printStackTrace(); - return null; + } catch (IOException e){ + throw new EdcException("Error parsing json file", e); } + + return object; + } + + + public static Message getValidHeader(ObjectMapper mapper) { + return readJsonFile(mapper, Message.class, VALID_LOG_MESSAGE_HEADER_PATH); } public static Message getinvalidTokenHeader() { diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/logMessage.json b/clearing-house-edc/core/src/test/resources/messages/logMessage.json similarity index 100% rename from clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/logMessage.json rename to clearing-house-edc/core/src/test/resources/messages/logMessage.json From 74c44db7ebe5d433de246da15cec6943580852ae Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Mon, 16 Oct 2023 17:23:57 -0300 Subject: [PATCH 108/183] feat (ch-edc): unit tests refactoring --- .../clearinghouse/edc/app/AppSenderTest.java | 51 +++++++------ .../delegate/LoggingMessageDelegateTest.java | 15 ++-- .../edc/handler/LogMessageHandlerTest.java | 41 +++++------ .../clearinghouse/edc/tests/TestUtils.java | 72 ++++++------------- .../test/resources/messages/logMessage.json | 20 ------ 5 files changed, 79 insertions(+), 120 deletions(-) delete mode 100644 clearing-house-edc/core/src/test/resources/messages/logMessage.json diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java index 3a17f61..b45b2ab 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java @@ -1,7 +1,7 @@ package de.truzzt.clearinghouse.edc.app; import com.fasterxml.jackson.databind.ObjectMapper; -import de.truzzt.clearinghouse.edc.TestUtils; +import de.truzzt.clearinghouse.edc.tests.TestUtils; import de.truzzt.clearinghouse.edc.app.delegate.LoggingMessageDelegate; import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; @@ -42,6 +42,8 @@ public class AppSenderTest { @Mock private EdcHttpClient httpClient; + private ObjectMapper mapper = new ObjectMapper(); + @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); @@ -52,21 +54,26 @@ public void setUp() { @Test public void sendSuccessful() throws IOException { - doReturn(TestUtils.getValidHandlerRequest().toString().getBytes()).when(typeManagerUtil).toJson(any(Object.class)); - doReturn(TestUtils.getValidResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(httpClient).execute(any(Request.class)); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + doReturn(TestUtils.getValidHandlerRequest(mapper).toString().getBytes()) + .when(typeManagerUtil).toJson(any(Object.class)); + doReturn(TestUtils.getValidResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + .when(httpClient).execute(any(Request.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); - var response = sender.send(TestUtils.getValidAppSenderRequest(), senderDelegate); + var response = sender.send(TestUtils.getValidAppSenderRequest(mapper), senderDelegate); assertNotNull(response); } @Test - public void sendWithHttpResquestError() throws IOException { + public void sendWithHttpRequestError() throws IOException { - doReturn(TestUtils.getValidHandlerRequest().toString().getBytes()).when(typeManagerUtil).toJson(any(Object.class)); + doReturn(TestUtils.getValidHandlerRequest(mapper).toString().getBytes()) + .when(typeManagerUtil).toJson(any(Object.class)); - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> sender.send(TestUtils.getInvalidUrlAppSenderRequest(), senderDelegate)); + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> + sender.send(TestUtils.getInvalidUrlAppSenderRequest(mapper), senderDelegate)); assertEquals("Expected URL scheme 'http' or 'https'", exception.getMessage().substring(0,37)); } @@ -74,11 +81,15 @@ public void sendWithHttpResquestError() throws IOException { @Test public void sendWithUnsuccessfulResponseBodyError() throws IOException { - doReturn(TestUtils.getValidHandlerRequest().toString().getBytes()).when(typeManagerUtil).toJson(any(Object.class)); - doReturn(TestUtils.getUnsuccessfulResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(httpClient).execute(any(Request.class)); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + doReturn(TestUtils.getValidHandlerRequest(mapper).toString().getBytes()) + .when(typeManagerUtil).toJson(any(Object.class)); + doReturn(TestUtils.getUnsuccessfulResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + .when(httpClient).execute(any(Request.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); - EdcException exception = assertThrows(EdcException.class, () -> sender.send(TestUtils.getValidAppSenderRequest(), senderDelegate)); + EdcException exception = assertThrows(EdcException.class, () -> + sender.send(TestUtils.getValidAppSenderRequest(mapper), senderDelegate)); assertEquals("Received an error from Clearing House App. Status: 401, message: Unauthorized", exception.getMessage()); } @@ -86,16 +97,16 @@ public void sendWithUnsuccessfulResponseBodyError() throws IOException { @Test public void sendWithNullResponseBodyError() throws IOException { - doReturn(TestUtils.getValidHandlerRequest().toString().getBytes()).when(typeManagerUtil).toJson(any(Object.class)); - doReturn(TestUtils.getResponseWithoutBody(TestUtils.getValidAppSenderRequest().getUrl())).when(httpClient).execute(any(Request.class)); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + doReturn(TestUtils.getValidHandlerRequest(mapper).toString().getBytes()) + .when(typeManagerUtil).toJson(any(Object.class)); + doReturn(TestUtils.getResponseWithoutBody(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + .when(httpClient).execute(any(Request.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); - EdcException exception = assertThrows(EdcException.class, () -> sender.send(TestUtils.getValidAppSenderRequest(), senderDelegate)); + EdcException exception = assertThrows(EdcException.class, () -> + sender.send(TestUtils.getValidAppSenderRequest(mapper), senderDelegate)); assertEquals("Error reading Clearing House App response body", exception.getMessage()); } - - - - } \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java index 78f716c..a6dd414 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java @@ -1,6 +1,7 @@ package de.truzzt.clearinghouse.edc.app.delegate; -import de.truzzt.clearinghouse.edc.TestUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.tests.TestUtils; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.LoggingMessageRequest; import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; @@ -23,6 +24,9 @@ class LoggingMessageDelegateTest { private TypeManagerUtil typeManagerUtil; @Mock private LoggingMessageDelegate senderDelegate; + + private ObjectMapper mapper = new ObjectMapper(); + @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); @@ -32,7 +36,7 @@ public void setUp() { @Test public void successfulBuildRequestUrl() { - HandlerRequest request = TestUtils.getValidHandlerRequest(); + HandlerRequest request = TestUtils.getValidHandlerRequest(mapper); String response = senderDelegate.buildRequestUrl(TestUtils.TEST_BASE_URL, request); @@ -43,7 +47,7 @@ public void successfulBuildRequestUrl() { @Test public void successfulBuildRequestBody() { - HandlerRequest request = TestUtils.getValidHandlerRequest(); + HandlerRequest request = TestUtils.getValidHandlerRequest(mapper); LoggingMessageRequest response = senderDelegate.buildRequestBody(request); @@ -54,10 +58,11 @@ public void successfulBuildRequestBody() { public void successfulParseResponseBody() { ResponseBody body = TestUtils.getValidResponseBody(); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + LoggingMessageResponse response = senderDelegate.parseResponseBody(body); assertNotNull(response); } - } \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index 655152a..e6da943 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -1,16 +1,14 @@ package de.truzzt.clearinghouse.edc.handler; import com.auth0.jwt.JWT; -import de.truzzt.clearinghouse.edc.TestUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.tests.TestUtils; import de.truzzt.clearinghouse.edc.app.AppSender; -import de.truzzt.clearinghouse.edc.app.delegate.AppSenderDelegate; import de.truzzt.clearinghouse.edc.app.delegate.LoggingMessageDelegate; -import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.HandlerResponse; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; -import okhttp3.Request; import okhttp3.ResponseBody; import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.spi.EdcException; @@ -21,20 +19,12 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import java.io.IOException; - -import static de.truzzt.clearinghouse.edc.TestUtils.getBuildJwtToken; import static de.truzzt.clearinghouse.edc.util.SettingsConstants.APP_BASE_URL_DEFAULT_VALUE; -import static de.truzzt.clearinghouse.edc.util.SettingsConstants.JWT_EXPIRES_AT_DEFAULT_VALUE; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; class LogMessageHandlerTest { @Mock @@ -54,6 +44,8 @@ class LogMessageHandlerTest { @Mock private EdcHttpClient httpClient; + private ObjectMapper mapper = new ObjectMapper(); + private AppSender sender; @BeforeEach @@ -67,7 +59,7 @@ public void setUp() { @Test public void successfulCanHandle(){ - HandlerRequest request = TestUtils.getValidHandlerRequest(); + HandlerRequest request = TestUtils.getValidHandlerRequest(mapper); Boolean response = logMessageHandler.canHandle(request); @@ -78,7 +70,7 @@ public void successfulCanHandle(){ @Test public void invalidMessageTypeCanHandle(){ - HandlerRequest request = TestUtils.getInvalidHandlerRequest(); + HandlerRequest request = TestUtils.getInvalidHandlerRequest(mapper); Boolean response = logMessageHandler.canHandle(request); @@ -88,11 +80,16 @@ public void invalidMessageTypeCanHandle(){ @Test public void successfulHandleRequest(){ - HandlerRequest request = TestUtils.getValidHandlerRequest(); - doReturn(JWT.create().toString()).when(logMessageHandler).buildJWTToken(any(SecurityToken.class), any(ServiceExtensionContext.class)); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest().getUrl())).when(senderDelegate).parseResponseBody(any(ResponseBody.class)); - doReturn(APP_BASE_URL_DEFAULT_VALUE+"/messages/log/" + request.getPid()).when(senderDelegate).buildRequestUrl(any(String.class), any(HandlerRequest.class)); - doReturn(TestUtils.getValidLoggingMessageRequest(request)).when(senderDelegate).buildRequestBody(any(HandlerRequest.class)); + HandlerRequest request = TestUtils.getValidHandlerRequest(mapper); + doReturn(JWT.create().toString()) + .when(logMessageHandler).buildJWTToken(any(SecurityToken.class), any(ServiceExtensionContext.class)); + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); + doReturn(APP_BASE_URL_DEFAULT_VALUE+"/messages/log/" + request.getPid()) + .when(senderDelegate) + .buildRequestUrl(any(String.class), any(HandlerRequest.class)); + doReturn(TestUtils.getValidLoggingMessageRequest(request)) + .when(senderDelegate).buildRequestBody(any(HandlerRequest.class)); HandlerResponse response = logMessageHandler.handleRequest(request); @@ -103,21 +100,19 @@ public void successfulHandleRequest(){ @Test public void missingSubjectBuildJwtToken() { EdcException exception = assertThrows(EdcException.class, () -> logMessageHandler.buildJWTToken( - TestUtils.getInvalidTokenHandlerRequest() + TestUtils.getInvalidTokenHandlerRequest(mapper) .getHeader() .getSecurityToken(), context)); assertEquals("JWT Token subject is missing",exception.getMessage()); } - @Test public void successfulBuildJwtToken() { var response = logMessageHandler.buildJWTToken( - TestUtils.getValidHandlerRequest() + TestUtils.getValidHandlerRequest(mapper) .getHeader() .getSecurityToken(), context); assertNotNull(response); } - } \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java index 438dc01..ef44f85 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java @@ -1,4 +1,4 @@ -package de.truzzt.clearinghouse.edc; +package de.truzzt.clearinghouse.edc.tests; import com.auth0.jwt.JWT; import com.fasterxml.jackson.databind.ObjectMapper; @@ -24,9 +24,7 @@ import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.glassfish.jersey.internal.routing.RequestSpecificConsumesProducesAcceptor; -import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -38,7 +36,7 @@ public class TestUtils { public static final String TEST_BASE_URL = "http://localhost:8000"; private static final String TEST_PAYLOAD = "Hello World"; - private static final String VALID_LOG_MESSAGE_HEADER_PATH = "./messages/LogMessage.json"; + private static final String VALID_HEADER_JSON = "messages/valid-header.json"; private static T readJsonFile(ObjectMapper mapper, Class type, String path) { @@ -75,17 +73,13 @@ private static T readJsonFile(ObjectMapper mapper, Class type, String pat public static Message getValidHeader(ObjectMapper mapper) { - return readJsonFile(mapper, Message.class, VALID_LOG_MESSAGE_HEADER_PATH); + return readJsonFile(mapper, Message.class, VALID_HEADER_JSON); } - public static Message getinvalidTokenHeader() { - try { - ObjectMapper mapper = new ObjectMapper(); + public static Message getInvalidTokenHeader(ObjectMapper mapper) { - File file = new File(LOG_MESSAGE_JSON_PATH); - file.createNewFile(); + Message message = readJsonFile(mapper, Message.class, VALID_HEADER_JSON); - Message message = mapper.readValue(file, Message.class); message.getSecurityToken().setTokenValue("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzY29wZXMiOlsiaWRzYz" + "pJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjo" + "iaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSw" + @@ -96,26 +90,14 @@ public static Message getinvalidTokenHeader() { "0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCJ9.hekZoP" + "DjEWaXreQl3l0PUIjBOPQhAl0w2mH4_PdNWuA"); return message; - } catch (IOException ioe){ - ioe.printStackTrace(); - return null; - } } - public static Message getNotLogMessageValidHeader() { - try { - ObjectMapper mapper = new ObjectMapper(); + public static Message getNotLogMessageValidHeader(ObjectMapper mapper) { - File file = new File(LOG_MESSAGE_JSON_PATH); - file.createNewFile(); + Message message = readJsonFile(mapper, Message.class, VALID_HEADER_JSON); - Message message = mapper.readValue(file, Message.class); - message.setType("ids:otherMessage"); - return message; - } catch (IOException ioe){ - ioe.printStackTrace(); - return null; - } + message.setType("ids:otherMessage"); + return message; } public static Response getValidResponse(String url) { @@ -199,8 +181,8 @@ public static LoggingMessageRequest getValidLoggingMessageRequest(HandlerRequest .build(); return new LoggingMessageRequest(requestHeader, handlerRequest.getPayload()); - } + public static ResponseBody getValidResponseBody(){ return ResponseBody.create( MediaType.get("application/json; charset=utf-8"), @@ -208,52 +190,38 @@ public static ResponseBody getValidResponseBody(){ ); } - - public static HandlerRequest getValidHandlerRequest(){ + public static HandlerRequest getValidHandlerRequest(ObjectMapper mapper){ return HandlerRequest.Builder.newInstance() .pid(UUID.randomUUID().toString()) - .header(getValidHeader() ) + .header(getValidHeader(mapper)) .payload(TEST_PAYLOAD).build(); } - public static HandlerRequest getInvalidTokenHandlerRequest(){ + public static HandlerRequest getInvalidTokenHandlerRequest(ObjectMapper mapper){ return HandlerRequest.Builder.newInstance() .pid(UUID.randomUUID().toString()) - .header(getinvalidTokenHeader()) + .header(getInvalidTokenHeader(mapper)) .payload(TEST_PAYLOAD).build(); } - public static HandlerRequest getInvalidHandlerRequest(){ + public static HandlerRequest getInvalidHandlerRequest(ObjectMapper mapper){ return HandlerRequest.Builder.newInstance() .pid(UUID.randomUUID().toString()) - .header(getNotLogMessageValidHeader() ) + .header(getNotLogMessageValidHeader(mapper) ) .payload(TEST_PAYLOAD).build(); } - public static AppSenderRequest getValidAppSenderRequest(){ + public static AppSenderRequest getValidAppSenderRequest(ObjectMapper mapper){ return new AppSenderRequest(TEST_BASE_URL+"/messages/log/" + UUID.randomUUID(), JWT.create().toString(), - getValidHandlerRequest() + getValidHandlerRequest(mapper) ); } - public static AppSenderRequest getInvalidUrlAppSenderRequest(){ + public static AppSenderRequest getInvalidUrlAppSenderRequest(ObjectMapper mapper){ return new AppSenderRequest("" + UUID.randomUUID(), JWT.create().toString(), - getValidHandlerRequest() + getValidHandlerRequest(mapper) ); } - - public static String getBuildJwtToken(Monitor monitor, - IdsId connectorId, - TypeManagerUtil typeManagerUtil, - AppSender appSender, - ServiceExtensionContext context, - HandlerRequest handlerRequest){ - - LogMessageHandler handler = new LogMessageHandler(monitor, connectorId, typeManagerUtil, appSender,context); - return handler.buildJWTToken(handlerRequest.getHeader().getSecurityToken(), context); - } - - } diff --git a/clearing-house-edc/core/src/test/resources/messages/logMessage.json b/clearing-house-edc/core/src/test/resources/messages/logMessage.json deleted file mode 100644 index 85057bb..0000000 --- a/clearing-house-edc/core/src/test/resources/messages/logMessage.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context":{ - "ids" : "https://w3id.org/idsa/core/", - "idsc" : "https://w3id.org/idsa/code/" - }, - "@type":"ids:LogMessage", - "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", - "ids:securityToken": { - "@type" : "ids:DynamicAttributeToken", - "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", - "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", - "ids:tokenFormat" : { - "@id" : "idsc:JWT" - } - }, - "ids:senderAgent":"http://example.org", - "ids:modelVersion":"4.1.0", - "ids:issued" : "2021-06-23T17:27:23.566+02:00", - "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" -} \ No newline at end of file From 47f4e086f4e00385cb8959c8d3f05048f3cfdf83 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Mon, 16 Oct 2023 16:16:41 -0300 Subject: [PATCH 109/183] feat (ch-edc): unit tests refactoring --- .../test/resources/messages/LogMessage.json | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 clearing-house-edc/core/src/test/resources/messages/LogMessage.json diff --git a/clearing-house-edc/core/src/test/resources/messages/LogMessage.json b/clearing-house-edc/core/src/test/resources/messages/LogMessage.json new file mode 100644 index 0000000..85057bb --- /dev/null +++ b/clearing-house-edc/core/src/test/resources/messages/LogMessage.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:LogMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file From 7a03fb9a1e6e7e53f9b4acdc3de77dc9c79ebcf4 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Mon, 16 Oct 2023 17:31:44 -0300 Subject: [PATCH 110/183] feat (ch-edc): TypeManagerUtilTest included --- .../edc/types/TypeManagerUtilTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java new file mode 100644 index 0000000..c2660e5 --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java @@ -0,0 +1,64 @@ +package de.truzzt.clearinghouse.edc.types; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.TestUtils; +import de.truzzt.clearinghouse.edc.types.ids.Message; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Spy; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.*; + +class TypeManagerUtilTest { + + @Spy + private ObjectMapper objectMapper; + @Mock + private TypeManagerUtil typeManagerUtil; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + typeManagerUtil = new TypeManagerUtil(objectMapper); + } + + @Test + void successfulParse() throws IOException { + + File file = new File(TestUtils.LOG_MESSAGE_JSON_PATH); + file.createNewFile(); + + InputStream is = new FileInputStream(file); + Message msg = typeManagerUtil.parse(is, Message.class); + assertNotNull(msg); + assertEquals("ids:LogMessage", msg.getType()); + } + + @Test + void successfulToJson() throws IOException { + File file = new File(TestUtils.LOG_MESSAGE_JSON_PATH); + file.createNewFile(); + + Message msgBefore = objectMapper.readValue(file, Message.class); + + byte[] json = typeManagerUtil.toJson(msgBefore); + assertNotNull(json); + + InputStream is = new ByteArrayInputStream(json); + Message msgAfter = typeManagerUtil.parse(is, Message.class); + + assertEquals(msgBefore.getType(), msgAfter.getType()); + + } +} \ No newline at end of file From eb9f827d1312684f315d3555f4df71efe2d75407 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Mon, 16 Oct 2023 17:46:12 -0300 Subject: [PATCH 111/183] feat (ch-edc): unit tests refactoring --- clearing-house-edc/README.md | 15 ++++++++++++++- .../edc/handler/LogMessageHandlerTest.java | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/clearing-house-edc/README.md b/clearing-house-edc/README.md index 99cbca3..1e3803f 100644 --- a/clearing-house-edc/README.md +++ b/clearing-house-edc/README.md @@ -25,7 +25,20 @@ Local execution: java -Dedc.fs.config=launchers/connector-local/resources/config.properties -Dedc.keystore=launchers/connector-local/resources/keystore.jks -Dedc.keystore.password=password -Dedc.vault=launchers/connector-local/resources/vault.properties -jar launchers/connector-local/build/libs/clearing-house-edc.jar +## Tests -## Operate +### Running Tests +To run the unit-tests execute the following command: + ./gradlew test + +### Test Coverage +To generate the tests coverage execute the following command: + + ./gradlew jacocoTestReport + +The coverage reports will be available in the following folders: + +- [core/build/reports/jacoco/test/html/index.html](./core/build/reports/jacoco/test/html/index.html) +- [extensions/multipart/build/reports/jacoco/test/html/index.html](./extensions/multipart/build/reports/jacoco/test/html/index.html) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index e6da943..c8ed87f 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -23,6 +23,7 @@ import static de.truzzt.clearinghouse.edc.util.SettingsConstants.APP_BASE_URL_DEFAULT_VALUE; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.spy; @@ -107,7 +108,10 @@ public void missingSubjectBuildJwtToken() { assertEquals("JWT Token subject is missing",exception.getMessage()); } + @Test public void successfulBuildJwtToken() { + doReturn("1").when(context).getSetting(anyString(), anyString()); + var response = logMessageHandler.buildJWTToken( TestUtils.getValidHandlerRequest(mapper) .getHeader() From 6199fee8d0ef63e978d7481d0698cb9041402401 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Mon, 16 Oct 2023 19:00:40 -0300 Subject: [PATCH 112/183] feat (ch-edc): TypeManagerUtilTest successfulParse included --- .../edc/handler/LogMessageHandlerTest.java | 2 - .../clearinghouse/edc/tests/TestUtils.java | 37 ++++++++++++++++++ .../edc/types/TypeManagerUtilTest.java | 38 ++++++++++++------- .../messages/invalid-log-message-header.json | 19 ++++++++++ .../{LogMessage.json => valid-header.json} | 0 5 files changed, 81 insertions(+), 15 deletions(-) create mode 100644 clearing-house-edc/core/src/test/resources/messages/invalid-log-message-header.json rename clearing-house-edc/core/src/test/resources/messages/{LogMessage.json => valid-header.json} (100%) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index c8ed87f..1e3fe4d 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -107,11 +107,9 @@ public void missingSubjectBuildJwtToken() { assertEquals("JWT Token subject is missing",exception.getMessage()); } - @Test public void successfulBuildJwtToken() { doReturn("1").when(context).getSetting(anyString(), anyString()); - var response = logMessageHandler.buildJWTToken( TestUtils.getValidHandlerRequest(mapper) .getHeader() diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java index ef44f85..76ad8fb 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java @@ -25,6 +25,7 @@ import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -37,6 +38,7 @@ public class TestUtils { public static final String TEST_BASE_URL = "http://localhost:8000"; private static final String TEST_PAYLOAD = "Hello World"; private static final String VALID_HEADER_JSON = "messages/valid-header.json"; + private static final String INVALID_LOG_MESSAGE_HEADER_JSON = "messages/invalid-log-message-header.json"; private static T readJsonFile(ObjectMapper mapper, Class type, String path) { @@ -71,6 +73,29 @@ private static T readJsonFile(ObjectMapper mapper, Class type, String pat return object; } + private static File returnJonFile(String path) { + + ClassLoader classLoader = TestUtils.class.getClassLoader(); + var jsonResource = classLoader.getResource(path); + + if (jsonResource == null) { + throw new EdcException("Header json file not found: " + path); + } + + URI jsonUrl; + try { + jsonUrl = jsonResource.toURI(); + } catch (URISyntaxException e) { + throw new EdcException("Error finding json file on classpath", e); + } + + Path filePath = Path.of(jsonUrl); + if (!Files.exists(filePath)) { + throw new EdcException("Header json file not found: " + path); + } + + return filePath.toFile(); + } public static Message getValidHeader(ObjectMapper mapper) { return readJsonFile(mapper, Message.class, VALID_HEADER_JSON); @@ -224,4 +249,16 @@ public static AppSenderRequest getInvalidUrlAppSenderRequest(ObjectMapper mapper getValidHandlerRequest(mapper) ); } + + public static File getValidHeaderFile() { + + return returnJonFile(VALID_HEADER_JSON); + } + + public static File getInvalidHeaderFile() { + + return returnJonFile(INVALID_LOG_MESSAGE_HEADER_JSON); + } + + } diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java index c2660e5..d0794de 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java @@ -1,8 +1,10 @@ package de.truzzt.clearinghouse.edc.types; import com.fasterxml.jackson.databind.ObjectMapper; -import de.truzzt.clearinghouse.edc.TestUtils; +import de.fraunhofer.iais.eis.LogMessage; +import de.truzzt.clearinghouse.edc.tests.TestUtils; import de.truzzt.clearinghouse.edc.types.ids.Message; +import org.eclipse.edc.spi.EdcException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -11,17 +13,17 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; class TypeManagerUtilTest { + private static final String VALID_HEADER_JSON = "messages/valid-header.json"; + @Spy private ObjectMapper objectMapper; @Mock @@ -36,21 +38,31 @@ void setUp() { @Test void successfulParse() throws IOException { - File file = new File(TestUtils.LOG_MESSAGE_JSON_PATH); - file.createNewFile(); - - InputStream is = new FileInputStream(file); + InputStream is = new FileInputStream(TestUtils.getValidHeaderFile()); Message msg = typeManagerUtil.parse(is, Message.class); assertNotNull(msg); assertEquals("ids:LogMessage", msg.getType()); } @Test + void typeErrorParse() { + + EdcException exception = + assertThrows(EdcException.class, + () -> typeManagerUtil.parse( + new FileInputStream(TestUtils.getInvalidHeaderFile()), + Message.class) + ); + assertEquals( + "Error parsing to type class de.truzzt.clearinghouse.edc.types.ids.Message", + exception.getMessage() + ); + + } + void successfulToJson() throws IOException { - File file = new File(TestUtils.LOG_MESSAGE_JSON_PATH); - file.createNewFile(); - Message msgBefore = objectMapper.readValue(file, Message.class); + Message msgBefore = objectMapper.readValue(TestUtils.getValidHeaderFile(), Message.class); byte[] json = typeManagerUtil.toJson(msgBefore); assertNotNull(json); diff --git a/clearing-house-edc/core/src/test/resources/messages/invalid-log-message-header.json b/clearing-house-edc/core/src/test/resources/messages/invalid-log-message-header.json new file mode 100644 index 0000000..58522c0 --- /dev/null +++ b/clearing-house-edc/core/src/test/resources/messages/invalid-log-message-header.json @@ -0,0 +1,19 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "senderAgent":"http://example.org", + "modelVersion":"4.1.0", + "issued" : "2021-06-23T17:27:23.566+02:00", + "issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/resources/messages/LogMessage.json b/clearing-house-edc/core/src/test/resources/messages/valid-header.json similarity index 100% rename from clearing-house-edc/core/src/test/resources/messages/LogMessage.json rename to clearing-house-edc/core/src/test/resources/messages/valid-header.json From 6a94116157db1705ef064260eb32480276be3f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 27 Sep 2023 11:33:40 +0200 Subject: [PATCH 113/183] Feat(ch-app): Refactor error handling and add performance tracing --- clearing-house-app/Cargo.lock | 1 + clearing-house-app/Cargo.toml | 1 + clearing-house-app/config.toml | 3 +- clearing-house-app/src/config.rs | 17 ++-- clearing-house-app/src/db/doc_store.rs | 2 +- clearing-house-app/src/db/process_store.rs | 14 ++- clearing-house-app/src/errors.rs | 5 +- clearing-house-app/src/main.rs | 2 +- clearing-house-app/src/ports/doc_type_api.rs | 3 +- .../src/services/document_service.rs | 5 +- .../src/services/keyring_service.rs | 6 +- .../src/services/logging_service.rs | 88 ++++++++++++++----- 12 files changed, 105 insertions(+), 42 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index e586388..285beed 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -362,6 +362,7 @@ dependencies = [ "serial_test", "sha2", "tempfile", + "thiserror", "tokio", "tracing", "tracing-subscriber", diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 18e2b28..51b9ab6 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -55,6 +55,7 @@ axum = { version = "0.6.20", features = ["json", "http2"] } async-trait = "0.1.73" # Helper for working with futures futures = "0.3.28" +thiserror = "1.0.48" [dev-dependencies] # Controlling execution of unit test cases, which could interfere with each other diff --git a/clearing-house-app/config.toml b/clearing-house-app/config.toml index dc0ede0..457bd54 100644 --- a/clearing-house-app/config.toml +++ b/clearing-house-app/config.toml @@ -3,4 +3,5 @@ document_database_url= "mongodb://localhost:27017" process_database_url= "mongodb://localhost:27017" keyring_database_url= "mongodb://localhost:27017" clear_db = true -signing_key = "keys/private_key.der" # Optional \ No newline at end of file +signing_key = "keys/private_key.der" # Optional +performance_tracing = true \ No newline at end of file diff --git a/clearing-house-app/src/config.rs b/clearing-house-app/src/config.rs index 81ae095..d4e45ec 100644 --- a/clearing-house-app/src/config.rs +++ b/clearing-house-app/src/config.rs @@ -9,6 +9,7 @@ pub(crate) struct CHConfig { pub(crate) log_level: Option, #[serde(default)] pub(crate) signing_key: Option, + performance_tracing: Option, } /// Contains the log level for the application @@ -72,17 +73,23 @@ pub(crate) fn read_config(config_file_override: Option<&std::path::Path>) -> CHC } /// Configure logging based on environment variable `RUST_LOG` -pub(crate) fn configure_logging(log_level: &Option) { +pub(crate) fn configure_logging(config: &CHConfig) { if std::env::var("RUST_LOG").is_err() { - if let Some(level) = log_level { + if let Some(level) = &config.log_level { std::env::set_var("RUST_LOG", level.to_string()); } } // setup logging - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .init(); + let mut subscriber_builder = tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()); + + // Add performance tracing + if let Some(true) = config.performance_tracing { + subscriber_builder = subscriber_builder.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE); + } + + subscriber_builder.init(); } #[cfg(test)] diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index f4b36f8..67eac28 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -15,7 +15,7 @@ use mongodb::options::{ }; use mongodb::{bson, Client, IndexModel}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DataStore { pub(crate) client: mongodb::Client, database: mongodb::Database, diff --git a/clearing-house-app/src/db/process_store.rs b/clearing-house-app/src/db/process_store.rs index ff00788..1d8aff6 100644 --- a/clearing-house-app/src/db/process_store.rs +++ b/clearing-house-app/src/db/process_store.rs @@ -13,7 +13,7 @@ use mongodb::options::{ }; use mongodb::{Client, Database}; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ProcessStore { pub(crate) client: Client, database: Database, @@ -91,6 +91,7 @@ impl ProcessStore { } } + #[tracing::instrument(skip_all)] pub async fn get_transaction_counter(&self) -> anyhow::Result> { debug!("Getting transaction counter..."); let coll = self @@ -102,6 +103,7 @@ impl ProcessStore { } } + #[tracing::instrument(skip_all)] pub async fn increment_transaction_counter(&self) -> anyhow::Result> { debug!("Getting transaction counter..."); let coll = self @@ -142,6 +144,7 @@ impl ProcessStore { } /// checks if the id exits + #[tracing::instrument(skip_all)] pub async fn exists_process(&self, pid: &String) -> anyhow::Result { debug!("Check if process with pid '{}' exists...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); @@ -158,6 +161,7 @@ impl ProcessStore { } } + #[tracing::instrument(skip_all)] pub async fn get_process(&self, pid: &String) -> anyhow::Result> { debug!("Trying to get process with id {}...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); @@ -170,6 +174,7 @@ impl ProcessStore { } } + #[tracing::instrument(skip_all)] pub async fn is_authorized(&self, user: &String, pid: &String) -> anyhow::Result { debug!( "checking if user '{}' is authorized to access '{}'", @@ -191,14 +196,15 @@ impl ProcessStore { } } - // store process in db - pub async fn store_process(&self, process: Process) -> anyhow::Result { + /// store process in db + #[tracing::instrument(skip_all)] + pub async fn store_process(&self, process: Process) -> anyhow::Result<()> { debug!("Storing process with pid {:#?}...", &process.id); let coll = self.database.collection::(MONGO_COLL_PROCESSES); match coll.insert_one(process, None).await { Ok(_r) => { debug!("...added new process: {}", &_r.inserted_id); - Ok(true) + Ok(()) } Err(e) => { error!("...failed to store process: {:#?}", &e); diff --git a/clearing-house-app/src/errors.rs b/clearing-house-app/src/errors.rs index 1db9216..9a4b019 100644 --- a/clearing-house-app/src/errors.rs +++ b/clearing-house-app/src/errors.rs @@ -1,4 +1,4 @@ -type Result = std::result::Result; +type AppResult = Result; #[derive(Debug)] pub enum AppError { @@ -11,12 +11,13 @@ impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { AppError::Generic(e) => write!(f, "{}", e), + _ => unreachable!(), } } } impl From for AppError { fn from(err: anyhow::Error) -> Self { - Self::Generic(err.into()) + Self::Generic(err) } } diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index f6f6970..f5aeab4 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -74,7 +74,7 @@ impl AppState { async fn main() -> Result<(), anyhow::Error> { // Read configuration let conf = config::read_config(None); - config::configure_logging(&conf.log_level); + config::configure_logging(&conf); // Initialize application state let app_state = AppState::init(&conf).await?; diff --git a/clearing-house-app/src/ports/doc_type_api.rs b/clearing-house-app/src/ports/doc_type_api.rs index ce342da..ac404f9 100644 --- a/clearing-house-app/src/ports/doc_type_api.rs +++ b/clearing-house-app/src/ports/doc_type_api.rs @@ -1,6 +1,5 @@ use crate::model::constants::DEFAULT_PROCESS_ID; use crate::ports::ApiResponse; -use crate::AppState; use crate::model::doc_type::DocumentType; @@ -100,7 +99,7 @@ async fn get_doc_types( } } -pub(crate) fn router() -> axum::Router { +pub(crate) fn router() -> axum::Router { axum::Router::new() .route("/", axum::routing::get(get_doc_types).post(create_doc_type)) .route( diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 354916e..dd2a4b7 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -12,7 +12,7 @@ use anyhow::anyhow; use std::convert::TryFrom; use std::sync::Arc; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct DocumentService { db: DataStore, key_api: Arc, @@ -23,6 +23,7 @@ impl DocumentService { Self { db, key_api } } + #[tracing::instrument(skip_all)] pub(crate) async fn create_enc_document( &self, ch_claims: ChClaims, @@ -120,6 +121,7 @@ impl DocumentService { } } + #[tracing::instrument(skip_all)] pub(crate) async fn get_enc_documents_for_pid( &self, ch_claims: ChClaims, @@ -285,6 +287,7 @@ impl DocumentService { } } + #[tracing::instrument(skip_all)] pub(crate) async fn get_enc_document( &self, ch_claims: ChClaims, diff --git a/clearing-house-app/src/services/keyring_service.rs b/clearing-house-app/src/services/keyring_service.rs index b107b21..3cc7a13 100644 --- a/clearing-house-app/src/services/keyring_service.rs +++ b/clearing-house-app/src/services/keyring_service.rs @@ -6,7 +6,7 @@ use crate::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; use crate::model::doc_type::DocumentType; use anyhow::anyhow; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct KeyringService { db: KeyStore, } @@ -16,6 +16,7 @@ impl KeyringService { KeyringService { db } } + #[tracing::instrument(skip_all)] pub async fn generate_keys( &self, ch_claims: ChClaims, @@ -58,6 +59,7 @@ impl KeyringService { } } + #[tracing::instrument(skip_all)] pub(crate) async fn decrypt_keys( &self, ch_claims: ChClaims, @@ -126,6 +128,7 @@ impl KeyringService { } } + #[tracing::instrument(skip_all)] pub async fn decrypt_key_map( &self, ch_claims: ChClaims, @@ -173,6 +176,7 @@ impl KeyringService { } } + #[tracing::instrument(skip_all)] pub(crate) async fn decrypt_multiple_keys( &self, ch_claims: ChClaims, diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 63af9ae..b8a283a 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -14,13 +14,54 @@ use crate::model::{ }; use crate::services::document_service::DocumentService; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct LoggingService { db: ProcessStore, doc_api: Arc, write_lock: Arc>, } +#[derive(Debug, thiserror::Error)] +pub enum LoggingServiceError { + // BadRequest + #[error("Received empty payload, which cannot be logged!")] + EmptyPayloadReceived, + // BadRequest + #[error("Logging to default PID is not allowed!")] + AttemptedLogToDefaultPid, + // InternalError + #[error("Error during database operation: {description}: {source}")] + DatabaseError { + source: anyhow::Error, + description: String, + }, + #[error("User not authorized!")] + UserNotAuthorized, + // Forbidden + #[error("Authorization failed!")] + AuthorizationFailed { + source: anyhow::Error, + description: String, + }, + // BadRequest + #[error("Document already exists!")] + DocumentAlreadyExists, +} + +impl axum::response::IntoResponse for LoggingServiceError { + fn into_response(self) -> axum::response::Response { + use axum::http::StatusCode; + match self { + LoggingServiceError::EmptyPayloadReceived => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + LoggingServiceError::AttemptedLogToDefaultPid => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + LoggingServiceError::DatabaseError { source, description } => (StatusCode::INTERNAL_SERVER_ERROR, format!("{}: {}", description, source)).into_response(), + LoggingServiceError::UserNotAuthorized => (StatusCode::FORBIDDEN, self.to_string()).into_response(), + LoggingServiceError::AuthorizationFailed { source, description } => (StatusCode::INTERNAL_SERVER_ERROR, format!("{}: {}", description, source)).into_response(), + LoggingServiceError::DocumentAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + } + } +} + impl LoggingService { pub fn new(db: ProcessStore, doc_api: Arc) -> LoggingService { LoggingService { @@ -36,7 +77,7 @@ impl LoggingService { key_path: &str, msg: ClearingHouseMessage, pid: String, - ) -> anyhow::Result { + ) -> Result { trace!("...user '{:?}'", &ch_claims.client_id); let user = &ch_claims.client_id; // Add non-InfoModel information to IdsMessage @@ -50,14 +91,14 @@ impl LoggingService { || (m.payload.is_some() && m.payload.as_ref().unwrap().trim().is_empty()) { error!("Trying to log an empty payload!"); - return Err(anyhow!("No payload received for logging!")); // BadRequest + return Err(LoggingServiceError::EmptyPayloadReceived); // BadRequest } // filter out calls for default process id and call application logic match DEFAULT_PROCESS_ID.eq(pid.as_str()) { true => { warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); - Err(anyhow!("Document already exists")) // BadRequest + Err(LoggingServiceError::AttemptedLogToDefaultPid) // BadRequest } false => { // convenience: if process does not exist, we create it but only if no error occurred before @@ -70,15 +111,14 @@ impl LoggingService { // create a new process let new_process = Process::new(pid.clone(), vec![user.clone()]); - if self.db.store_process(new_process).await.is_err() { + if let Err(e) = self.db.store_process(new_process).await { error!("Error while creating process '{}'", &pid); - return Err(anyhow!("Error while creating process")); - // InternalError + return Err(LoggingServiceError::DatabaseError { source: e, description: "Creating process failed".to_string() }); // InternalError } } - Err(_) => { + Err(e) => { error!("Error while getting process '{}'", &pid); - return Err(anyhow!("Error while getting process")); // InternalError + return Err(LoggingServiceError::DatabaseError { source: e, description: "Getting process failed".to_string() }); // InternalError } } @@ -87,15 +127,18 @@ impl LoggingService { Ok(true) => info!("User authorized."), Ok(false) => { warn!("User is not authorized to write to pid '{}'", &pid); - warn!("This is the forbidden branch"); - return Err(anyhow!("User not authorized!")); // Forbidden + return Err(LoggingServiceError::UserNotAuthorized); // Forbidden } - Err(_) => { + Err(e) => { error!( "Error while checking authorization of user '{}' for '{}'", &user, &pid ); - return Err(anyhow!("Error during authorization")); + return Err(LoggingServiceError::AuthorizationFailed { + source: e, + description: format!("Error while checking authorization of user '{}' for '{}'", + &user, &pid), + }); } } @@ -175,12 +218,13 @@ impl LoggingService { } } + #[tracing::instrument(skip_all)] async fn log_message( &self, user: &str, key_path: &str, message: IdsMessage, - ) -> anyhow::Result { + ) -> Result { debug!("transforming message to document..."); let payload = message.payload.as_ref().unwrap().clone(); // transform message to document @@ -214,27 +258,23 @@ impl LoggingService { debug!("...done. Signing receipt..."); Ok(transaction.sign(key_path)) } - _ => { + Ok(None) => unreachable!("increment_transaction_counter never returns None!"), + Err(e) => { error!("Error while incrementing transaction id!"); - Err(anyhow!("Internal error while preparing transaction data")) - // InternalError + Err(LoggingServiceError::DatabaseError { source: e, description: "Error while incrementing transaction id!".to_string() }) // InternalError } } } Err(e) => { error!("Error while creating document: {:?}", e); - Err(anyhow!("Document already exists")) // BadRequest + Err(LoggingServiceError::DocumentAlreadyExists) // BadRequest } } } - Ok(None) => { - println!("None!"); - Err(anyhow!("Internal error while preparing transaction data")) // InternalError - } + Ok(None) => unreachable!("get_transaction_counter never returns None!"), Err(e) => { error!("Error while getting transaction id!"); - println!("{}", e); - Err(anyhow!("Internal error while preparing transaction data")) // InternalError + Err(LoggingServiceError::DatabaseError { source: e, description: "Error while getting transaction id".to_string()}) // InternalError } } } From b3f8edec027aa8168f64fd552ec7bed0e7f4ac30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 4 Oct 2023 08:36:15 +0200 Subject: [PATCH 114/183] feat(ch-app): Added tests, refactored unwrap --- clearing-house-app/src/config.rs | 3 +- clearing-house-app/src/crypto.rs | 71 +++++++++++++-- clearing-house-app/src/db/doc_store.rs | 9 ++ clearing-house-app/src/db/key_store.rs | 2 + clearing-house-app/src/main.rs | 1 + clearing-house-app/src/model/crypto.rs | 13 +-- clearing-house-app/src/ports/logging_api.rs | 4 +- .../src/services/document_service.rs | 10 +-- .../src/services/logging_service.rs | 89 ++++++++++++------- 9 files changed, 145 insertions(+), 57 deletions(-) diff --git a/clearing-house-app/src/config.rs b/clearing-house-app/src/config.rs index d4e45ec..0074ead 100644 --- a/clearing-house-app/src/config.rs +++ b/clearing-house-app/src/config.rs @@ -86,7 +86,8 @@ pub(crate) fn configure_logging(config: &CHConfig) { // Add performance tracing if let Some(true) = config.performance_tracing { - subscriber_builder = subscriber_builder.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE); + subscriber_builder = + subscriber_builder.with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE); } subscriber_builder.init(); diff --git a/clearing-house-app/src/crypto.rs b/clearing-house-app/src/crypto.rs index 9d6f1e1..01db133 100644 --- a/clearing-house-app/src/crypto.rs +++ b/clearing-house-app/src/crypto.rs @@ -70,16 +70,14 @@ pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result Ok(KeyMap::new(true, key_map, Some(ct))), + Ok(ct) => Ok(KeyMap::new(key_map, Some(ct))), Err(e) => { error!("Error while encrypting key seed: {:?}", e); Err(anyhow!("Error while encrypting key seed!")) @@ -105,7 +103,9 @@ pub fn restore_key_map( match decrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], &keys_ct) { Ok(key_seed) => { // generate new random key map - restore_keys(&key_seed, dt) + let mut km = restore_keys(&key_seed, dt)?; + km.keys_enc = Some(keys_ct); + Ok(km) } Err(e) => { error!("Error while decrypting key ciphertext: {}", e); @@ -119,7 +119,7 @@ pub fn restore_keys(secret: &String, dt: DocumentType) -> anyhow::Result let kdf = restore_kdf(secret)?; let key_map = derive_key_map(kdf, dt, false); - Ok(KeyMap::new(false, key_map, None)) + Ok(KeyMap::new(key_map, None)) } fn restore_kdf(secret: &String) -> anyhow::Result> { @@ -191,6 +191,9 @@ pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result()); } } + + #[test] + fn encrypt_decrypt_secret() { + let seed = super::generate_random_seed(); + let key = &seed[..super::EXP_KEY_SIZE]; + let nonce = &seed[..super::EXP_NONCE_SIZE]; + let secret = "This is a secret".to_string(); + + let ct = super::encrypt_secret(key, nonce, secret.clone()).expect("Encryption failed"); + let pt = super::decrypt_secret(key, nonce, &ct).expect("Decryption failed"); + + assert_eq!(secret, pt); + } + + #[test] + fn restore_kdf() { + let salt = "abcdefghijklmnopqrstuvwx"; + let (secret, kdf) = super::initialize_kdf(); + let restored_kdf = super::restore_kdf(&secret).expect("kdf restoration failed"); + + let mut okm = [0u8; super::EXP_BUFF_SIZE]; + let mut restored_okm = [0u8; super::EXP_BUFF_SIZE]; + kdf + .expand(salt.as_bytes(), &mut okm) + .expect("kdf expansion failed"); + restored_kdf + .expand(salt.as_bytes(), &mut restored_okm) + .expect("restored_kdf expansion failed"); + assert_eq!(restored_okm, okm); + } + + #[test] + fn restore_key_map() { + let msk: MasterKey = MasterKey::new_random(); + let dt: DocumentType = DocumentType::new( + "test".to_string(), + "hello_world".to_string(), + vec![ + DocumentTypePart::new("0".to_string()), + DocumentTypePart::new("1".to_string()), + DocumentTypePart::new("2".to_string()), + ], + ); + + let key_map = super::generate_key_map(msk.clone(), dt.clone()).expect("key_map generation failed"); + let restored_key_map = + super::restore_key_map(msk, dt, key_map.clone().keys_enc.unwrap()).expect("key_map restoration failed"); + + assert_eq!(key_map, restored_key_map); + } } diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 67eac28..39f5ab6 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -140,6 +140,7 @@ impl DataStore { } } + #[tracing::instrument(skip_all)] pub async fn add_document(&self, doc: EncryptedDocument) -> anyhow::Result { debug!("add_document to bucket"); let coll = self @@ -178,6 +179,7 @@ impl DataStore { /// checks if the document exists /// document ids are globally unique + #[tracing::instrument(skip_all)] pub async fn exists_document(&self, id: &String) -> anyhow::Result { debug!("Check if document with id '{}' exists...", id); let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone()}; @@ -198,6 +200,7 @@ impl DataStore { } /// gets the model from the db + #[tracing::instrument(skip_all)] pub async fn get_document( &self, id: &str, @@ -230,6 +233,7 @@ impl DataStore { } /// gets documents for a single process from the db + #[tracing::instrument(skip_all)] pub async fn get_document_with_previous_tc( &self, tc: i64, @@ -268,6 +272,7 @@ impl DataStore { } /// gets a page of documents of a specific document type for a single process from the db defined by parameters page, size and sort + #[tracing::instrument(skip_all)] pub async fn get_documents_for_pid( &self, dt_id: &String, @@ -356,6 +361,7 @@ impl DataStore { } /// offset is necessary for duration queries. There, start_entries of bucket depend on timestamps which usually creates an offset in the bucket + #[tracing::instrument(skip_all)] async fn get_start_bucket_size( &self, dt_id: &String, @@ -418,10 +424,12 @@ impl DataStore { Ok(bucket_size) } + #[tracing::instrument(skip_all)] fn get_offset(bucket_size: &DocumentBucketSize) -> u64 { (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES } + #[tracing::instrument(skip_all)] fn get_start_bucket( page: u64, size: u64, @@ -433,6 +441,7 @@ impl DataStore { (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1 } + #[tracing::instrument(skip_all)] fn get_start_entry( page: u64, size: u64, diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index 20f3880..d8721c8 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -104,6 +104,7 @@ impl KeyStore { } /// Only one master key may exist in the database. + #[tracing::instrument(skip_all)] pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result { tracing::debug!("Storing new master key..."); let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); @@ -137,6 +138,7 @@ impl KeyStore { } /// Only one master key may exist in the database. + #[tracing::instrument(skip_all)] pub async fn get_msk(&self) -> anyhow::Result { let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); let result = coll diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index f5aeab4..fa8ae7a 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -1,4 +1,5 @@ #![forbid(unsafe_code)] +#![warn(clippy::unwrap_used)] #[macro_use] extern crate tracing; diff --git a/clearing-house-app/src/model/crypto.rs b/clearing-house-app/src/model/crypto.rs index 5555c4f..6c9407c 100644 --- a/clearing-house-app/src/model/crypto.rs +++ b/clearing-house-app/src/model/crypto.rs @@ -29,7 +29,7 @@ impl MasterKey { } } -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq)] pub struct KeyEntry { pub id: String, pub key: Vec, @@ -42,20 +42,15 @@ impl KeyEntry { } } -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq)] pub struct KeyMap { - pub enc: bool, pub keys: HashMap, pub keys_enc: Option>, } impl KeyMap { - pub fn new(enc: bool, keys: HashMap, keys_enc: Option>) -> KeyMap { - KeyMap { - enc, - keys, - keys_enc, - } + pub fn new(keys: HashMap, keys_enc: Option>) -> KeyMap { + KeyMap { keys, keys_enc } } } diff --git a/clearing-house-app/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs index c80bd7d..58a0ba0 100644 --- a/clearing-house-app/src/ports/logging_api.rs +++ b/clearing-house-app/src/ports/logging_api.rs @@ -47,8 +47,8 @@ async fn create_process( #[derive(serde::Deserialize)] struct QueryParams { - pub page: Option, - pub size: Option, + pub page: Option, + pub size: Option, pub sort: Option, pub date_to: Option, pub date_from: Option, diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index dd2a4b7..b42b614 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -126,8 +126,8 @@ impl DocumentService { &self, ch_claims: ChClaims, doc_type: Option, - page: Option, // TODO: Why i32? This should be and unsigned int - size: Option, // TODO: Why i32? This should be and unsigned int + page: Option, + size: Option, sort: Option, date_from: Option, date_to: Option, @@ -146,7 +146,7 @@ impl DocumentService { let sanitized_page = match page { Some(p) => { if p > 0 { - u64::try_from(p).unwrap() + p } else { warn!("...invalid page requested. Falling back to 1."); 1 @@ -158,8 +158,8 @@ impl DocumentService { // Valid sizes are between 1 and MAX_NUM_RESPONSE_ENTRIES (1000) let sanitized_size = match size { Some(s) => { - if s > 0 && s <= i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap() { - u64::try_from(s).unwrap() + if s > 0 && s <= MAX_NUM_RESPONSE_ENTRIES { + s } else { warn!("...invalid size requested. Falling back to default."); DEFAULT_NUM_RESPONSE_ENTRIES diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index b8a283a..06f568f 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -52,12 +52,34 @@ impl axum::response::IntoResponse for LoggingServiceError { fn into_response(self) -> axum::response::Response { use axum::http::StatusCode; match self { - LoggingServiceError::EmptyPayloadReceived => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), - LoggingServiceError::AttemptedLogToDefaultPid => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), - LoggingServiceError::DatabaseError { source, description } => (StatusCode::INTERNAL_SERVER_ERROR, format!("{}: {}", description, source)).into_response(), - LoggingServiceError::UserNotAuthorized => (StatusCode::FORBIDDEN, self.to_string()).into_response(), - LoggingServiceError::AuthorizationFailed { source, description } => (StatusCode::INTERNAL_SERVER_ERROR, format!("{}: {}", description, source)).into_response(), - LoggingServiceError::DocumentAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + LoggingServiceError::EmptyPayloadReceived => { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } + LoggingServiceError::AttemptedLogToDefaultPid => { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } + LoggingServiceError::DatabaseError { + source, + description, + } => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("{}: {}", description, source), + ) + .into_response(), + LoggingServiceError::UserNotAuthorized => { + (StatusCode::FORBIDDEN, self.to_string()).into_response() + } + LoggingServiceError::AuthorizationFailed { + source, + description, + } => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("{}: {}", description, source), + ) + .into_response(), + LoggingServiceError::DocumentAlreadyExists => { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } } } } @@ -113,12 +135,18 @@ impl LoggingService { if let Err(e) = self.db.store_process(new_process).await { error!("Error while creating process '{}'", &pid); - return Err(LoggingServiceError::DatabaseError { source: e, description: "Creating process failed".to_string() }); // InternalError + return Err(LoggingServiceError::DatabaseError { + source: e, + description: "Creating process failed".to_string(), + }); // InternalError } } Err(e) => { error!("Error while getting process '{}'", &pid); - return Err(LoggingServiceError::DatabaseError { source: e, description: "Getting process failed".to_string() }); // InternalError + return Err(LoggingServiceError::DatabaseError { + source: e, + description: "Getting process failed".to_string(), + }); // InternalError } } @@ -136,8 +164,10 @@ impl LoggingService { ); return Err(LoggingServiceError::AuthorizationFailed { source: e, - description: format!("Error while checking authorization of user '{}' for '{}'", - &user, &pid), + description: format!( + "Error while checking authorization of user '{}' for '{}'", + &user, &pid + ), }); } } @@ -258,10 +288,16 @@ impl LoggingService { debug!("...done. Signing receipt..."); Ok(transaction.sign(key_path)) } - Ok(None) => unreachable!("increment_transaction_counter never returns None!"), + Ok(None) => { + unreachable!("increment_transaction_counter never returns None!") + } Err(e) => { error!("Error while incrementing transaction id!"); - Err(LoggingServiceError::DatabaseError { source: e, description: "Error while incrementing transaction id!".to_string() }) // InternalError + Err(LoggingServiceError::DatabaseError { + source: e, + description: "Error while incrementing transaction id!" + .to_string(), + }) // InternalError } } } @@ -274,7 +310,10 @@ impl LoggingService { Ok(None) => unreachable!("get_transaction_counter never returns None!"), Err(e) => { error!("Error while getting transaction id!"); - Err(LoggingServiceError::DatabaseError { source: e, description: "Error while getting transaction id".to_string()}) // InternalError + Err(LoggingServiceError::DatabaseError { + source: e, + description: "Error while getting transaction id".to_string(), + }) // InternalError } } } @@ -282,8 +321,8 @@ impl LoggingService { pub(crate) async fn query_pid( &self, ch_claims: ChClaims, - page: Option, - size: Option, + page: Option, + size: Option, sort: Option, date_to: Option, date_from: Option, @@ -326,22 +365,10 @@ impl LoggingService { } } - // sanity check for pagination - let sanitized_page = match page { - Some(p) => { - if p >= 0 { - p - } else { - warn!("...invalid page requested. Falling back to 0."); - 1 - } - } - None => 1, - }; - + let sanitized_page = page.unwrap_or(1); let sanitized_size = match size { Some(s) => { - let converted_max = i32::try_from(MAX_NUM_RESPONSE_ENTRIES).unwrap(); + let converted_max = MAX_NUM_RESPONSE_ENTRIES; if s > converted_max { warn!("...invalid size requested. Falling back to default."); converted_max @@ -349,10 +376,10 @@ impl LoggingService { s } else { warn!("...invalid size requested. Falling back to default."); - i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap() + DEFAULT_NUM_RESPONSE_ENTRIES } } - None => i32::try_from(DEFAULT_NUM_RESPONSE_ENTRIES).unwrap(), + None => DEFAULT_NUM_RESPONSE_ENTRIES, }; let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); From 39235311ded6a614f0ad097e5621ae47d9e1abc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 5 Oct 2023 15:57:45 +0200 Subject: [PATCH 115/183] Feat(ch-app): Finished refactoring error handling of logging service and refactored logging_service.rs --- clearing-house-app/src/errors.rs | 1 - clearing-house-app/src/main.rs | 8 +- clearing-house-app/src/model/process.rs | 4 + .../src/services/logging_service.rs | 408 ++++++++---------- 4 files changed, 190 insertions(+), 231 deletions(-) diff --git a/clearing-house-app/src/errors.rs b/clearing-house-app/src/errors.rs index 9a4b019..5baf5be 100644 --- a/clearing-house-app/src/errors.rs +++ b/clearing-house-app/src/errors.rs @@ -11,7 +11,6 @@ impl std::fmt::Display for AppError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { AppError::Generic(e) => write!(f, "{}", e), - _ => unreachable!(), } } } diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index fa8ae7a..003f175 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -33,17 +33,21 @@ pub(crate) struct AppState { impl AppState { /// Initialize the application state from config async fn init(conf: &config::CHConfig) -> anyhow::Result { + trace!("Initializing Process store"); let process_store = ProcessStore::init_process_store(&conf.process_database_url, conf.clear_db) .await .expect("Failure to initialize process store! Exiting..."); + trace!("Initializing Keyring store"); let keyring_store = KeyStore::init_keystore(&conf.keyring_database_url, conf.clear_db) .await .expect("Failure to initialize keyring store! Exiting..."); + trace!("Initializing Document store"); let doc_store = DataStore::init_datastore(&conf.document_database_url, conf.clear_db) .await .expect("Failure to initialize document store! Exiting..."); + trace!("Initializing services"); let keyring_service = Arc::new(services::keyring_service::KeyringService::new( keyring_store, )); @@ -77,6 +81,8 @@ async fn main() -> Result<(), anyhow::Error> { let conf = config::read_config(None); config::configure_logging(&conf); + info!("Config read successfully! Initializing application ..."); + // Initialize application state let app_state = AppState::init(&conf).await?; @@ -88,7 +94,7 @@ async fn main() -> Result<(), anyhow::Error> { // Bind port and start server let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); - tracing::debug!("listening on {}", addr); + tracing::info!("Starting server: Listening on {}", addr); Ok(axum::Server::bind(&addr) .serve(app.into_make_service()) .with_graceful_shutdown(util::shutdown_signal()) diff --git a/clearing-house-app/src/model/process.rs b/clearing-house-app/src/model/process.rs index 1c48b7d..c623f90 100644 --- a/clearing-house-app/src/model/process.rs +++ b/clearing-house-app/src/model/process.rs @@ -8,6 +8,10 @@ impl Process { pub fn new(id: String, owners: Vec) -> Self { Self { id, owners } } + + pub fn is_authorized(&self, owner: &str) -> bool { + self.owners.contains(&owner.to_string()) + } } #[derive(serde::Serialize, serde::Deserialize)] diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 06f568f..f30d843 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -3,8 +3,6 @@ use crate::model::{ constants::{DEFAULT_NUM_RESPONSE_ENTRIES, DEFAULT_PROCESS_ID, MAX_NUM_RESPONSE_ENTRIES}, {document::Document, process::Process, SortingOrder}, }; -use anyhow::anyhow; -use std::convert::TryFrom; use std::sync::Arc; use crate::db::process_store::ProcessStore; @@ -27,8 +25,8 @@ pub enum LoggingServiceError { #[error("Received empty payload, which cannot be logged!")] EmptyPayloadReceived, // BadRequest - #[error("Logging to default PID is not allowed!")] - AttemptedLogToDefaultPid, + #[error("Accessing default PID is not allowed!")] + AttemptedAccessToDefaultPid, // InternalError #[error("Error during database operation: {description}: {source}")] DatabaseError { @@ -46,6 +44,12 @@ pub enum LoggingServiceError { // BadRequest #[error("Document already exists!")] DocumentAlreadyExists, + #[error("Invalid request received!")] + InvalidRequest, + #[error("Process already exists!")] + ProcessAlreadyExists, + #[error("Process '{0}' does not exist!")] + ProcessDoesNotExist(String), } impl axum::response::IntoResponse for LoggingServiceError { @@ -55,7 +59,7 @@ impl axum::response::IntoResponse for LoggingServiceError { LoggingServiceError::EmptyPayloadReceived => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } - LoggingServiceError::AttemptedLogToDefaultPid => { + LoggingServiceError::AttemptedAccessToDefaultPid => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } LoggingServiceError::DatabaseError { @@ -80,6 +84,15 @@ impl axum::response::IntoResponse for LoggingServiceError { LoggingServiceError::DocumentAlreadyExists => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } + LoggingServiceError::InvalidRequest => { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } + LoggingServiceError::ProcessAlreadyExists => { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } + LoggingServiceError::ProcessDoesNotExist(_) => { + (StatusCode::NOT_FOUND, self.to_string()).into_response() + } } } } @@ -108,157 +121,39 @@ impl LoggingService { m.payload_type = msg.payload_type; m.pid = Some(pid.clone()); - // validate that there is a payload - if m.payload.is_none() - || (m.payload.is_some() && m.payload.as_ref().unwrap().trim().is_empty()) - { - error!("Trying to log an empty payload!"); - return Err(LoggingServiceError::EmptyPayloadReceived); // BadRequest - } - - // filter out calls for default process id and call application logic - match DEFAULT_PROCESS_ID.eq(pid.as_str()) { - true => { - warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); - Err(LoggingServiceError::AttemptedLogToDefaultPid) // BadRequest - } - false => { - // convenience: if process does not exist, we create it but only if no error occurred before - match self.db.get_process(&pid).await { - Ok(Some(_p)) => { - debug!("Requested pid '{}' exists. Nothing to create.", &pid); - } - Ok(None) => { - info!("Requested pid '{}' does not exist. Creating...", &pid); - // create a new process - let new_process = Process::new(pid.clone(), vec![user.clone()]); - - if let Err(e) = self.db.store_process(new_process).await { - error!("Error while creating process '{}'", &pid); - return Err(LoggingServiceError::DatabaseError { - source: e, - description: "Creating process failed".to_string(), - }); // InternalError - } - } - Err(e) => { - error!("Error while getting process '{}'", &pid); - return Err(LoggingServiceError::DatabaseError { - source: e, - description: "Getting process failed".to_string(), - }); // InternalError - } - } - - // now check if user is authorized to write to pid - match self.db.is_authorized(user, &pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - return Err(LoggingServiceError::UserNotAuthorized); // Forbidden - } - Err(e) => { - error!( - "Error while checking authorization of user '{}' for '{}'", - &user, &pid - ); - return Err(LoggingServiceError::AuthorizationFailed { - source: e, - description: format!( - "Error while checking authorization of user '{}' for '{}'", - &user, &pid - ), - }); - } - } - - debug!("logging message for pid {}", &pid); - self.log_message(user, key_path, m.clone()).await - } - } - } - - pub(crate) async fn create_process( - &self, - ch_claims: ChClaims, - msg: ClearingHouseMessage, - pid: String, - ) -> anyhow::Result { - let mut m = msg.header; - m.payload = msg.payload; - m.payload_type = msg.payload_type; - - trace!("...user '{:?}'", &ch_claims.client_id); - let user = &ch_claims.client_id; - - // validate payload - let mut owners = vec![user.clone()]; - let payload = m.payload.clone().unwrap_or(String::new()); - if !payload.is_empty() { - trace!("OwnerList: '{:#?}'", &payload); - match serde_json::from_str::(&payload) { - Ok(owner_list) => { - for o in owner_list.owners { - if !owners.contains(&o) { - owners.push(o); - } - } - } - Err(e) => { - error!("Error while creating process '{}': {}", &pid, e); - return Err(anyhow!("Invalid owner list!")); // BadRequest - } - }; - }; + // Check for default process id + Self::check_for_default_pid(&pid)?; - // check if the pid already exists - match self.db.get_process(&pid).await { - Ok(Some(p)) => { - warn!("Requested pid '{}' already exists.", &p.id); - if !p.owners.contains(user) { - Err(anyhow!("User not authorized!")) // Forbidden - } else { - Err(anyhow!("Process already exists!")) // BadRequest - } - } + // validate that there is a payload + let payload = match m.payload.clone() { + Some(p) if !p.trim().is_empty() => Ok(p), _ => { - // filter out calls for default process id - match DEFAULT_PROCESS_ID.eq(pid.as_str()) { - true => { - warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); - Err(anyhow!("Document already exists")) // BadRequest - } - false => { - info!("Requested pid '{}' will have {} owners", &pid, owners.len()); + error!("Trying to log an empty payload!"); + Err(LoggingServiceError::EmptyPayloadReceived) // BadRequest + } + }?; - // create process - info!("Requested pid '{}' does not exist. Creating...", &pid); - let new_process = Process::new(pid.clone(), owners); + // Check if process exists and if the user is authorized to access the process + if let Err(LoggingServiceError::ProcessDoesNotExist(_)) = self.get_process_and_check_authorized(&pid, user).await { + // convenience: if process does not exist, we create it but only if no error occurred before + info!("Requested pid '{}' does not exist. Creating...", &pid); + // create a new process + let new_process = Process::new(pid.clone(), vec![user.clone()]); - match self.db.store_process(new_process).await { - Ok(_) => Ok(pid.clone()), - Err(e) => { - error!("Error while creating process '{}': {}", &pid, e); - Err(anyhow!("Error while creating process")) // InternalError - } - } - } - } + if let Err(e) = self.db.store_process(new_process).await { + error!("Error while creating process '{}'", &pid); + return Err(LoggingServiceError::DatabaseError { + source: e, + description: "Creating process failed".to_string(), + }); // InternalError } } - } - #[tracing::instrument(skip_all)] - async fn log_message( - &self, - user: &str, - key_path: &str, - message: IdsMessage, - ) -> Result { - debug!("transforming message to document..."); - let payload = message.payload.as_ref().unwrap().clone(); // transform message to document - let mut doc = Document::from(message); + debug!("transforming message to document..."); + let mut doc = Document::from(m); + + // lock write access let _x = self.write_lock.lock().await; match self.db.get_transaction_counter().await { Ok(Some(tid)) => { @@ -318,6 +213,80 @@ impl LoggingService { } } + pub(crate) async fn create_process( + &self, + ch_claims: ChClaims, + msg: ClearingHouseMessage, + pid: String, + ) -> Result { + let mut m = msg.header; + m.payload = msg.payload; + m.payload_type = msg.payload_type; + + trace!("...user '{:?}'", &ch_claims.client_id); + let user = &ch_claims.client_id; + + // Check for default process id + Self::check_for_default_pid(&pid)?; + + // validate payload + let mut owners = vec![user.clone()]; + match m.payload { + Some(ref payload) if !payload.is_empty() => { + trace!("OwnerList: '{:#?}'", &payload); + match serde_json::from_str::(&payload) { + Ok(owner_list) => { + for o in owner_list.owners { + if !owners.contains(&o) { + owners.push(o); + } + } + } + Err(e) => { + error!("Could not parse OwnerList '{payload}' for pid '{pid}': {e}"); + return Err(LoggingServiceError::InvalidRequest); // BadRequest + } + }; + } + _ => {} + }; + + // check if the pid already exists + match self.db.get_process(&pid).await { + Ok(Some(p)) => { + warn!("Requested pid '{}' already exists.", &p.id); + if !p.owners.contains(user) { + Err(LoggingServiceError::UserNotAuthorized) // Forbidden + } else { + Err(LoggingServiceError::ProcessAlreadyExists) // BadRequest + } + } + Ok(None) => { + info!("Requested pid '{}' will have {} owners", &pid, owners.len()); + + // create process + info!("Requested pid '{}' does not exist. Creating...", &pid); + let new_process = Process::new(pid.clone(), owners); + + match self.db.store_process(new_process).await { + Ok(_) => Ok(pid.clone()), + Err(e) => { + error!("Error while creating process '{}': {}", &pid, e); + Err(LoggingServiceError::DatabaseError { + source: e, + description: "Creating process failed".to_string(), + }) // InternalError + } + } + } + Err(e) => + Err(LoggingServiceError::DatabaseError { + source: e, + description: "Error while getting process".to_string(), + }) + } + } + pub(crate) async fn query_pid( &self, ch_claims: ChClaims, @@ -328,57 +297,18 @@ impl LoggingService { date_from: Option, pid: String, _message: ClearingHouseMessage, - ) -> anyhow::Result { + ) -> Result { debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); trace!("...user '{:?}'", &ch_claims.client_id); let user = &ch_claims.client_id; - // check if process exists - match self.db.exists_process(&pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => return Err(anyhow!("Process does not exist!")), // NotFound - Err(_e) => { - error!( - "Error while checking process '{}' for user '{}'", - &pid, &user - ); - return Err(anyhow!("Cannot authorize user!")); // InternalError - } - }; - - // now check if user is authorized to read infos in pid - match self.db.is_authorized(user, &pid).await { - Ok(true) => { - info!("User authorized."); - } - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - return Err(anyhow!("User not authorized!")); // Forbidden - } - Err(_) => { - error!( - "Error while checking authorization of user '{}' for '{}'", - &user, &pid - ); - return Err(anyhow!("Cannot authorize user!")); // InternalError - } - } + // Check if process exists and if the user is authorized to access the process + self.get_process_and_check_authorized(&pid, user).await?; let sanitized_page = page.unwrap_or(1); let sanitized_size = match size { - Some(s) => { - let converted_max = MAX_NUM_RESPONSE_ENTRIES; - if s > converted_max { - warn!("...invalid size requested. Falling back to default."); - converted_max - } else if s > 0 { - s - } else { - warn!("...invalid size requested. Falling back to default."); - DEFAULT_NUM_RESPONSE_ENTRIES - } - } + Some(s) => s.min(MAX_NUM_RESPONSE_ENTRIES), None => DEFAULT_NUM_RESPONSE_ENTRIES, }; @@ -410,8 +340,10 @@ impl LoggingService { } Err(e) => { error!("Error while retrieving message: {:?}", e); - Err(anyhow!("Error while retrieving messages for pid {}!", &pid)) - // InternalError + Err(LoggingServiceError::DatabaseError { + source: e, + description: format!("Error while retrieving messages for pid '{pid}'"), + }) } } } @@ -422,40 +354,12 @@ impl LoggingService { pid: String, id: String, _message: ClearingHouseMessage, - ) -> anyhow::Result { + ) -> Result { trace!("...user '{:?}'", &ch_claims.client_id); let user = &ch_claims.client_id; - // check if process exists - match self.db.exists_process(&pid).await { - Ok(true) => info!("User authorized."), - Ok(false) => return Err(anyhow!("Process does not exist!")), // NotFound - Err(_e) => { - error!( - "Error while checking process '{}' for user '{}'", - &pid, &user - ); - return Err(anyhow!("Cannot authorize user!")); // InternalError - } - }; - - // now check if user is authorized to read infos in pid - match self.db.is_authorized(user, &pid).await { - Ok(true) => { - info!("User authorized."); - } - Ok(false) => { - warn!("User is not authorized to write to pid '{}'", &pid); - return Err(anyhow!("User not authorized!")); // Forbidden - } - Err(_) => { - error!( - "Error while checking authorization of user '{}' for '{}'", - &user, &pid - ); - return Err(anyhow!("Cannot authorize user!")); // InternalError - } - } + // Check if process exists and if the user is authorized to access the process + self.get_process_and_check_authorized(&pid, user).await?; match self .doc_api @@ -467,14 +371,60 @@ impl LoggingService { let queried_message = IdsMessage::from(doc); Ok(queried_message) } - /*Result::Ok(None) => { - debug!("Queried a non-existing document: {}", &id); - ApiResponse::NotFound(format!("No message found with id {}!", &id)) - }*/ Err(e) => { error!("Error while retrieving message: {:?}", e); - Err(anyhow!("Error while retrieving message with id {}!", &id)) // InternalError + Err(LoggingServiceError::DatabaseError { + source: e, + description: format!("Error while retrieving messages for pid '{pid}'"), + }) + } + } + } + + /// Checks if the given pid is the default pid + fn check_for_default_pid(pid: &str) -> Result<(), LoggingServiceError> { + // Check for default process id + if DEFAULT_PROCESS_ID.eq(pid) { + warn!("Log to default pid '{}' not allowed", DEFAULT_PROCESS_ID); + Err(LoggingServiceError::AttemptedAccessToDefaultPid) + } else { + Ok(()) + } + } + + /// Checks if a process exists and the user is authorized to access the process + async fn get_process_and_check_authorized(&self, pid: &String, user: &str) -> Result { + match self.db.get_process(pid).await { + Ok(Some(p)) if !p.is_authorized(user) => { + warn!("User is not authorized to read from pid '{}'", &pid); + Err(LoggingServiceError::UserNotAuthorized) + } + Ok(Some(p)) => { + info!("User authorized."); + Ok(p) + } + Ok(None) => { + Err(LoggingServiceError::ProcessDoesNotExist(pid.clone())) + } + Err(e) => { + error!("Error while getting process '{}': {}", &pid, e); + Err(LoggingServiceError::DatabaseError { + source: e, + description: "Getting process failed".to_string(), + }) } } } } + + +#[cfg(test)] +mod test { + use super::LoggingService; + use crate::model::constants::DEFAULT_PROCESS_ID; + #[test] + fn check_for_default_pid() { + assert!(LoggingService::check_for_default_pid(DEFAULT_PROCESS_ID).is_err()); + assert!(LoggingService::check_for_default_pid("not_default").is_ok()); + } +} \ No newline at end of file From 8965f5e8a1ccbfdf8c36040f3736a3dd7fee7929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Mon, 16 Oct 2023 22:14:58 +0200 Subject: [PATCH 116/183] feat(ch-app): Finished refactoring document-service error-handling --- clearing-house-app/src/model/document.rs | 31 ++-- clearing-house-app/src/model/ids/message.rs | 80 +++++---- .../src/services/document_service.rs | 169 +++++++++++------- .../src/services/logging_service.rs | 55 +++--- 4 files changed, 191 insertions(+), 144 deletions(-) diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index 6ec9e10..da6219c 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -13,11 +13,11 @@ use uuid::Uuid; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct DocumentPart { pub name: String, - pub content: Option, + pub content: String, } impl DocumentPart { - pub fn new(name: String, content: Option) -> DocumentPart { + pub fn new(name: String, content: String) -> DocumentPart { DocumentPart { name, content } } @@ -46,18 +46,11 @@ impl DocumentPart { let nonce = GenericArray::from_slice(nonce); let cipher = Aes256GcmSiv::new(key); - match &self.content { - Some(pt) => { - let pt = format_pt_for_storage(&self.name, pt); - match cipher.encrypt(nonce, pt.as_bytes()) { - Ok(ct) => Ok(ct), - Err(e) => anyhow::bail!("Error while encrypting {}", e), - } - } - None => { - error!("Tried to encrypt empty document part."); - anyhow::bail!("Nothing to encrypt"); - } + + let pt = format_pt_for_storage(&self.name, &self.content); + match cipher.encrypt(nonce, pt.as_bytes()) { + Ok(ct) => Ok(ct), + Err(e) => anyhow::bail!("Error while encrypting {}", e), } } } @@ -71,7 +64,7 @@ impl DocumentPart { Ok(pt) => { let pt = String::from_utf8(pt)?; let (name, content) = restore_pt_no_dt(&pt)?; - Ok(DocumentPart::new(name, Some(content))) + Ok(DocumentPart::new(name, content)) } Err(e) => { anyhow::bail!("Error while decrypting: {}", e) @@ -112,10 +105,6 @@ impl Document { }; for part in self.parts.iter() { - if part.content.is_none() { - // no content, so we skip this one - continue; - } // check if there's a key for this part if !keys.contains_key(&part.name) { error!("Missing key for part '{}'", &part.name); @@ -150,8 +139,8 @@ impl Document { format_tc(self.tc) } - pub fn get_parts_map(&self) -> HashMap> { - let mut p_map = HashMap::new(); + pub fn get_parts_map(&self) -> HashMap { + let mut p_map = HashMap::with_capacity(self.parts.len()); for part in self.parts.iter() { p_map.insert(part.name.clone(), part.content.clone()); } diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index 24e533f..a089797 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -224,26 +224,26 @@ impl From for IdsMessage { // message_id let p_map = doc.get_parts_map(); if let Some(v) = p_map.get(MESSAGE_ID) { - m.id = Some(v.as_ref().unwrap().clone()); + m.id = Some(v.clone()); } // model_version if let Some(v) = p_map.get(MODEL_VERSION) { - m.model_version = v.as_ref().unwrap().clone(); + m.model_version = v.clone(); } // correlation_message if let Some(v) = p_map.get(CORRELATION_MESSAGE) { - m.correlation_message = Some(v.as_ref().unwrap().clone()); + m.correlation_message = Some(v.clone()); } // transfer_contract if let Some(v) = p_map.get(TRANSFER_CONTRACT) { - m.transfer_contract = Some(v.as_ref().unwrap().clone()); + m.transfer_contract = Some(v.clone()); } // issued if let Some(v) = p_map.get(ISSUED) { - match serde_json::from_str(v.as_ref().unwrap()) { + match serde_json::from_str(v) { Ok(date_time) => { m.issued = date_time; } @@ -258,27 +258,27 @@ impl From for IdsMessage { // issuer_connector if let Some(v) = p_map.get(ISSUER_CONNECTOR) { - m.issuer_connector = InfoModelId::SimpleId(v.as_ref().unwrap().clone()); + m.issuer_connector = InfoModelId::SimpleId(v.clone()); } // content_version if let Some(v) = p_map.get(CONTENT_VERSION) { - m.content_version = Some(v.as_ref().unwrap().clone()); + m.content_version = Some(v.clone()); } // sender_agent if let Some(v) = p_map.get(SENDER_AGENT) { - m.sender_agent = v.clone().unwrap(); + m.sender_agent = v.clone(); } // payload if let Some(v) = p_map.get(PAYLOAD) { - m.payload = Some(v.as_ref().unwrap().clone()); + m.payload = Some(v.clone()); } // payload_type if let Some(v) = p_map.get(PAYLOAD_TYPE) { - m.payload_type = Some(v.as_ref().unwrap().clone()); + m.payload_type = Some(v.clone()); } //TODO: security_token @@ -307,8 +307,10 @@ impl From for IdsMessage { /// - authorization_token /// - payload /// - payload_type -impl From for Document { - fn from(m: IdsMessage) -> Self { +impl TryFrom for Document { + type Error = serde_json::Error; + + fn try_from(m: IdsMessage) -> Result { let mut doc_parts = vec![]; // message_id @@ -317,49 +319,55 @@ impl From for Document { None => autogen("Message"), }; - doc_parts.push(DocumentPart::new(MESSAGE_ID.to_string(), Some(id))); + doc_parts.push(DocumentPart::new(MESSAGE_ID.to_string(), id)); // model_version doc_parts.push(DocumentPart::new( MODEL_VERSION.to_string(), - Some(m.model_version), + m.model_version, )); // correlation_message - doc_parts.push(DocumentPart::new( - CORRELATION_MESSAGE.to_string(), - m.correlation_message, - )); + if let Some(s) = m.correlation_message { + doc_parts.push(DocumentPart::new( + CORRELATION_MESSAGE.to_string(), + s, + )); + } // issued doc_parts.push(DocumentPart::new( ISSUED.to_string(), - serde_json::to_string(&m.issued).ok(), + serde_json::to_string(&m.issued)?, )); // issuer_connector doc_parts.push(DocumentPart::new( ISSUER_CONNECTOR.to_string(), - Some(m.issuer_connector.to_string()), + m.issuer_connector.to_string(), )); // sender_agent doc_parts.push(DocumentPart::new( SENDER_AGENT.to_string(), - Some(m.sender_agent.to_string()), + m.sender_agent.to_string(), )); // transfer_contract - doc_parts.push(DocumentPart::new( - TRANSFER_CONTRACT.to_string(), - m.transfer_contract, - )); + if let Some(s) = m.transfer_contract { + doc_parts.push(DocumentPart::new( + TRANSFER_CONTRACT.to_string(), + s, + )); + } // content_version - doc_parts.push(DocumentPart::new( - CONTENT_VERSION.to_string(), - m.content_version, - )); + if let Some(s) = m.content_version { + doc_parts.push(DocumentPart::new( + CONTENT_VERSION.to_string(), + s, + )); + } // security_token //TODO @@ -368,19 +376,21 @@ impl From for Document { //TODO // payload - doc_parts.push(DocumentPart::new(PAYLOAD.to_string(), m.payload.clone())); + if let Some(s) = m.payload { + doc_parts.push(DocumentPart::new(PAYLOAD.to_string(), s)); + } // payload_type - doc_parts.push(DocumentPart::new( - PAYLOAD_TYPE.to_string(), - m.payload_type.clone(), - )); + if let Some(s) = m.payload_type { + doc_parts.push(DocumentPart::new(PAYLOAD_TYPE.to_string(), s)); + } // pid - Document::new(m.pid.unwrap(), DEFAULT_DOC_TYPE.to_string(), -1, doc_parts) + Ok(Document::new(m.pid.unwrap(), DEFAULT_DOC_TYPE.to_string(), -1, doc_parts)) } } +#[inline] fn autogen(message: &str) -> String { format!( "https://w3id.org/idsa/autogen/{}/{}", diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index b42b614..5dc2b3f 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -12,6 +12,52 @@ use anyhow::anyhow; use std::convert::TryFrom; use std::sync::Arc; +#[derive(thiserror::Error, Debug)] +pub enum DocumentServiceError { + #[error("Document already exists!")] + DocumentAlreadyExists, + #[error("Document contains no payload!")] + MissingPayload, + #[error("Error during database operation: {description}: {source}")] + DatabaseError { + source: anyhow::Error, + description: String, + }, + #[error("Error while creating the chain hash!")] + ChainHashError, + #[error("Error while retrieving keys from keyring!")] + KeyringServiceError(#[from] anyhow::Error), + #[error("Invalid dates in query!")] + InvalidDates, + #[error("Document not found!")] + NotFound, + #[error("Key Ciphertext corrupted!")] + CorruptedCiphertext(#[from] hex::FromHexError), +} + +impl axum::response::IntoResponse for DocumentServiceError { + fn into_response(self) -> axum::response::Response { + use axum::http::StatusCode; + match self { + Self::DocumentAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::MissingPayload => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::DatabaseError { + source, + description, + } => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("{}: {}", description, source), + ) + .into_response(), + Self::ChainHashError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), + Self::KeyringServiceError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + Self::InvalidDates => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), + Self::CorruptedCiphertext(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + } + } +} + #[derive(Clone, Debug)] pub struct DocumentService { db: DataStore, @@ -28,30 +74,30 @@ impl DocumentService { &self, ch_claims: ChClaims, doc: Document, - ) -> anyhow::Result { + ) -> Result { trace!("...user '{:?}'", &ch_claims.client_id); // data validation let payload: Vec = doc .parts .iter() .filter(|p| *PAYLOAD_PART == p.name) - .map(|p| p.content.as_ref().unwrap().clone()) + .map(|p| p.content.clone()) .collect(); - if payload.len() > 1 { - return Err(anyhow!("Document contains two or more payloads!")); // BadRequest - } else if payload.is_empty() { - return Err(anyhow!("Document contains no payload!")); // BadRequest + + // If the document contains more than 1 payload we panic. This should never happen! + assert!(payload.len() <= 1, "Document contains two or more payloads!"); + if payload.is_empty() { + return Err(DocumentServiceError::MissingPayload); } // check if doc id already exists match self.db.exists_document(&doc.id).await { Ok(true) => { warn!("Document exists already!"); - Err(anyhow!("Document exists already!")) // BadRequest + Err(DocumentServiceError::DocumentAlreadyExists) } _ => { - debug!("Document does not exists!"); - debug!("getting keys"); + trace!("getting keys"); // TODO: This needs some attention, because keyring api called `create_service_token` on `ch_claims` let keys = match self @@ -93,13 +139,12 @@ impl DocumentService { info!("No entries found for pid {}. Beginning new chain!", doc.pid); } else { // If this happens, db didn't find a tc entry that should exist. - return Err(anyhow!("Error while creating the chain hash!")); - // InternalError + return Err(DocumentServiceError::ChainHashError); } } Err(e) => { error!("Error while creating the chain hash: {:?}", e); - return Err(anyhow!("Error while creating the chain hash!")); + return Err(DocumentServiceError::ChainHashError); } } @@ -108,13 +153,13 @@ impl DocumentService { let receipt = DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id, &enc_doc.hash); - debug!("storing document ...."); + trace!("storing document ...."); // store document match self.db.add_document(enc_doc).await { Ok(_b) => Ok(receipt), Err(e) => { error!("Error while adding: {:?}", e); - Err(anyhow!("Error while storing document!")) + Err(DocumentServiceError::DatabaseError { source: e, description: "Error while adding document".to_string() }) } } } @@ -132,7 +177,7 @@ impl DocumentService { date_from: Option, date_to: Option, pid: String, - ) -> anyhow::Result { + ) -> Result { debug!("Trying to retrieve documents for pid '{}'...", &pid); trace!("...user '{:?}'", &ch_claims.client_id); debug!( @@ -140,33 +185,8 @@ impl DocumentService { page, size, sort ); - // Parameter validation for pagination: - // Valid pages start from 1 - // Max page number as of yet unknown - let sanitized_page = match page { - Some(p) => { - if p > 0 { - p - } else { - warn!("...invalid page requested. Falling back to 1."); - 1 - } - } - None => 1, - }; - - // Valid sizes are between 1 and MAX_NUM_RESPONSE_ENTRIES (1000) - let sanitized_size = match size { - Some(s) => { - if s > 0 && s <= MAX_NUM_RESPONSE_ENTRIES { - s - } else { - warn!("...invalid size requested. Falling back to default."); - DEFAULT_NUM_RESPONSE_ENTRIES - } - } - None => DEFAULT_NUM_RESPONSE_ENTRIES, - }; + let sanitized_page = Self::sanitize_page(page); + let sanitized_size = Self::sanitize_size(size); // Sorting order is already validated and defaults to descending let sanitized_sort = sort.unwrap_or(SortingOrder::Descending); @@ -178,14 +198,13 @@ impl DocumentService { // Validation of dates with various checks. If none given dates default to date_now (date_to) and (date_now - 2 weeks) (date_from) let Ok((sanitized_date_from, sanitized_date_to)) = validate_and_sanitize_dates(parsed_date_from, parsed_date_to, None) - else { - debug!("date validation failed!"); - return Err(anyhow!("Invalid date parameter!")); // BadRequest - }; + else { + debug!("date validation failed!"); + return Err(DocumentServiceError::InvalidDates); + }; //new behavior: if pages are "invalid" return {}. Do not adjust page //either call db with type filter or without to get cts - let start = chrono::Local::now(); debug!( "... using pagination with page: {}, size:{} and sort:{:#?}", sanitized_page, sanitized_size, &sanitized_sort @@ -211,7 +230,7 @@ impl DocumentService { Ok(cts) => cts, Err(e) => { error!("Error while retrieving document: {:?}", e); - return Err(anyhow!("Error while retrieving document for {}", &pid)); + return Err(DocumentServiceError::DatabaseError { source: e, description: "Error while retrieving document".to_string() }); } }; @@ -257,8 +276,7 @@ impl DocumentService { Ok(key_map) => key_map, Err(e) => { error!("Error while retrieving keys from keyring: {:?}", e); - return Err(anyhow!("Error while retrieving keys from keyring")); - // InternalError + return Err(DocumentServiceError::KeyringServiceError(e)); } }; debug!("... keys received. Starting decryption..."); @@ -279,9 +297,7 @@ impl DocumentService { }) .collect(); debug!("...done."); - let end = chrono::Local::now(); - let diff = end - start; - info!("Total time taken to run in ms: {}", diff.num_milliseconds()); + result.documents = pts_bulk; Ok(result) } @@ -294,7 +310,7 @@ impl DocumentService { pid: String, id: String, hash: Option, - ) -> anyhow::Result { + ) -> Result { trace!("...user '{:?}'", &ch_claims.client_id); trace!( "trying to retrieve document with id '{}' for pid '{}'", @@ -327,32 +343,63 @@ impl DocumentService { Ok(d) => Ok(d), Err(e) => { warn!("Got empty document from decryption! {:?}", e); - Err(anyhow!("Document {} not found!", &id)) - // NotFound + Err(DocumentServiceError::NotFound) } } } Err(e) => { error!("Error while retrieving keys from keyring: {:?}", e); - Err(anyhow!("Error while retrieving keys")) - // InternalError + Err(DocumentServiceError::KeyringServiceError(e)) } } } Err(e) => { error!("Error while decoding ciphertext: {:?}", e); - Err(anyhow!("Key Ciphertext corrupted")) // InternalError + Err(DocumentServiceError::CorruptedCiphertext(e)) // InternalError } } } Ok(None) => { debug!("Nothing found in db!"); - Err(anyhow!("Document {} not found!", &id)) // NotFound + Err(DocumentServiceError::NotFound) // NotFound } Err(e) => { error!("Error while retrieving document: {:?}", e); - Err(anyhow!("Error while retrieving document {}", &id)) // InternalError + Err(DocumentServiceError::DatabaseError {source: e, description: "Error while retrieving document".to_string()}) } } } + + #[inline] + fn sanitize_page(page: Option) -> u64 { + // Parameter validation for pagination: + // Valid pages start from 1 + match page { + Some(p) => { + if p > 0 { + p + } else { + warn!("...invalid page requested. Falling back to 1."); + 1 + } + } + None => 1, + } + } + + #[inline] + fn sanitize_size(size: Option) -> u64 { + // Valid sizes are between 1 and MAX_NUM_RESPONSE_ENTRIES (1000) + match size { + Some(s) => { + if s > 0 && s <= MAX_NUM_RESPONSE_ENTRIES { + s + } else { + warn!("...invalid size requested. Falling back to default."); + DEFAULT_NUM_RESPONSE_ENTRIES + } + } + None => DEFAULT_NUM_RESPONSE_ENTRIES, + } + } } diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index f30d843..b1a9078 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -12,22 +12,12 @@ use crate::model::{ }; use crate::services::document_service::DocumentService; -#[derive(Clone, Debug)] -pub struct LoggingService { - db: ProcessStore, - doc_api: Arc, - write_lock: Arc>, -} - #[derive(Debug, thiserror::Error)] pub enum LoggingServiceError { - // BadRequest #[error("Received empty payload, which cannot be logged!")] EmptyPayloadReceived, - // BadRequest #[error("Accessing default PID is not allowed!")] AttemptedAccessToDefaultPid, - // InternalError #[error("Error during database operation: {description}: {source}")] DatabaseError { source: anyhow::Error, @@ -35,13 +25,11 @@ pub enum LoggingServiceError { }, #[error("User not authorized!")] UserNotAuthorized, - // Forbidden #[error("Authorization failed!")] AuthorizationFailed { source: anyhow::Error, description: String, }, - // BadRequest #[error("Document already exists!")] DocumentAlreadyExists, #[error("Invalid request received!")] @@ -50,19 +38,23 @@ pub enum LoggingServiceError { ProcessAlreadyExists, #[error("Process '{0}' does not exist!")] ProcessDoesNotExist(String), + #[error("Parsing error in {0}")] + ParsingError(#[from] serde_json::Error), + #[error("DocumentService error in {0}")] + DocumentServiceError(#[from] crate::services::document_service::DocumentServiceError), } impl axum::response::IntoResponse for LoggingServiceError { fn into_response(self) -> axum::response::Response { use axum::http::StatusCode; match self { - LoggingServiceError::EmptyPayloadReceived => { + Self::EmptyPayloadReceived => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } - LoggingServiceError::AttemptedAccessToDefaultPid => { + Self::AttemptedAccessToDefaultPid => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } - LoggingServiceError::DatabaseError { + Self::DatabaseError { source, description, } => ( @@ -70,10 +62,10 @@ impl axum::response::IntoResponse for LoggingServiceError { format!("{}: {}", description, source), ) .into_response(), - LoggingServiceError::UserNotAuthorized => { + Self::UserNotAuthorized => { (StatusCode::FORBIDDEN, self.to_string()).into_response() } - LoggingServiceError::AuthorizationFailed { + Self::AuthorizationFailed { source, description, } => ( @@ -81,22 +73,33 @@ impl axum::response::IntoResponse for LoggingServiceError { format!("{}: {}", description, source), ) .into_response(), - LoggingServiceError::DocumentAlreadyExists => { + Self::DocumentAlreadyExists => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } - LoggingServiceError::InvalidRequest => { + Self::InvalidRequest => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } - LoggingServiceError::ProcessAlreadyExists => { + Self::ProcessAlreadyExists => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } - LoggingServiceError::ProcessDoesNotExist(_) => { + Self::ProcessDoesNotExist(_) => { (StatusCode::NOT_FOUND, self.to_string()).into_response() } + Self::ParsingError(_) => { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } + Self::DocumentServiceError(e) => e.into_response(), } } } +#[derive(Clone, Debug)] +pub struct LoggingService { + db: ProcessStore, + doc_api: Arc, + write_lock: Arc>, +} + impl LoggingService { pub fn new(db: ProcessStore, doc_api: Arc) -> LoggingService { LoggingService { @@ -151,7 +154,7 @@ impl LoggingService { // transform message to document debug!("transforming message to document..."); - let mut doc = Document::from(m); + let mut doc = Document::try_from(m).map_err(LoggingServiceError::ParsingError)?; // lock write access let _x = self.write_lock.lock().await; @@ -198,7 +201,7 @@ impl LoggingService { } Err(e) => { error!("Error while creating document: {:?}", e); - Err(LoggingServiceError::DocumentAlreadyExists) // BadRequest + Err(LoggingServiceError::DocumentServiceError(e)) } } } @@ -340,10 +343,7 @@ impl LoggingService { } Err(e) => { error!("Error while retrieving message: {:?}", e); - Err(LoggingServiceError::DatabaseError { - source: e, - description: format!("Error while retrieving messages for pid '{pid}'"), - }) + Err(LoggingServiceError::DocumentServiceError(e)) } } } @@ -422,6 +422,7 @@ impl LoggingService { mod test { use super::LoggingService; use crate::model::constants::DEFAULT_PROCESS_ID; + #[test] fn check_for_default_pid() { assert!(LoggingService::check_for_default_pid(DEFAULT_PROCESS_ID).is_err()); From 387498c15ff2bd8c2890625dd92d8d3be1250b42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Mon, 16 Oct 2023 23:04:12 +0200 Subject: [PATCH 117/183] feat(ch-app): Finished error-handling in keyring service and introduces 'doc_type' feature --- clearing-house-app/Cargo.toml | 4 + clearing-house-app/src/main.rs | 4 +- clearing-house-app/src/ports/mod.rs | 17 +++++ .../src/services/document_service.rs | 13 ++-- .../src/services/keyring_service.rs | 75 ++++++++++++++----- .../src/services/logging_service.rs | 6 +- 6 files changed, 87 insertions(+), 32 deletions(-) diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 51b9ab6..4fe145e 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -62,3 +62,7 @@ thiserror = "1.0.48" serial_test = "2.0.0" # Tempfile creation for testing tempfile = "3.8.0" + +[features] +# Enables the doc_type API +doc_type = [] diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index 003f175..1f3a4b7 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -87,9 +87,7 @@ async fn main() -> Result<(), anyhow::Error> { let app_state = AppState::init(&conf).await?; // Setup router - let app = axum::Router::new() - .merge(ports::logging_api::router()) - .nest("/doctype", ports::doc_type_api::router()) + let app = ports::router() .with_state(app_state); // Bind port and start server diff --git a/clearing-house-app/src/ports/mod.rs b/clearing-house-app/src/ports/mod.rs index dbd85be..3322d40 100644 --- a/clearing-house-app/src/ports/mod.rs +++ b/clearing-house-app/src/ports/mod.rs @@ -4,10 +4,27 @@ //! services. In this case, the logging service implements REST-API endpoints to provide access to //! the logging service. use axum::response::Response; +use crate::AppState; +#[cfg(doc_type)] pub(crate) mod doc_type_api; pub(crate) mod logging_api; +/// Router for the logging service and the doc_type service +#[cfg(doc_type)] +pub(crate) fn router() -> axum::routing::Router { + axum::Router::new() + .merge(ports::logging_api::router()) + .nest("/doctype", ports::doc_type_api::router()); +} + +/// Router for the logging service +#[cfg(not(doc_type))] +pub(crate) fn router() -> axum::routing::Router { + axum::Router::new() + .merge(logging_api::router()) +} + #[derive(Debug)] pub(crate) enum ApiResponse { PreFlight(()), diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 5dc2b3f..3c1d930 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -8,10 +8,10 @@ use crate::model::document::Document; use crate::model::{parse_date, validate_and_sanitize_dates, SortingOrder}; use crate::services::keyring_service::KeyringService; use crate::services::{DocumentReceipt, QueryResult}; -use anyhow::anyhow; use std::convert::TryFrom; use std::sync::Arc; +/// Error type for DocumentService #[derive(thiserror::Error, Debug)] pub enum DocumentServiceError { #[error("Document already exists!")] @@ -26,13 +26,15 @@ pub enum DocumentServiceError { #[error("Error while creating the chain hash!")] ChainHashError, #[error("Error while retrieving keys from keyring!")] - KeyringServiceError(#[from] anyhow::Error), + KeyringServiceError(#[from] crate::services::keyring_service::KeyringServiceError), #[error("Invalid dates in query!")] InvalidDates, #[error("Document not found!")] NotFound, #[error("Key Ciphertext corrupted!")] CorruptedCiphertext(#[from] hex::FromHexError), + #[error("Error while encrypting!")] + EncryptionError, } impl axum::response::IntoResponse for DocumentServiceError { @@ -50,10 +52,11 @@ impl axum::response::IntoResponse for DocumentServiceError { ) .into_response(), Self::ChainHashError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), - Self::KeyringServiceError(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + Self::KeyringServiceError(e) => e.into_response(), Self::InvalidDates => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), Self::CorruptedCiphertext(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + Self::EncryptionError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), } } } @@ -111,7 +114,7 @@ impl DocumentService { } Err(e) => { error!("Error while retrieving keys: {:?}", e); - Err(anyhow!("Error while retrieving keys!")) // InternalError + Err(DocumentServiceError::KeyringServiceError(e)) } }?; @@ -123,7 +126,7 @@ impl DocumentService { } Err(e) => { error!("Error while encrypting: {:?}", e); - Err(anyhow!("Error while encrypting!")) // InternalError + Err(DocumentServiceError::EncryptionError) } }?; diff --git a/clearing-house-app/src/services/keyring_service.rs b/clearing-house-app/src/services/keyring_service.rs index 3cc7a13..2cbd6a2 100644 --- a/clearing-house-app/src/services/keyring_service.rs +++ b/clearing-house-app/src/services/keyring_service.rs @@ -3,8 +3,37 @@ use crate::crypto::restore_key_map; use crate::db::key_store::KeyStore; use crate::model::claims::ChClaims; use crate::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; -use crate::model::doc_type::DocumentType; -use anyhow::anyhow; + +#[derive(Debug, thiserror::Error)] +pub enum KeyringServiceError { + #[error("Keymap generation error")] + KeymapGenerationFailed, + #[error("Keymap restoration error")] + KeymapRestorationFailed, + #[error("Document type not found")] + DocumentTypeNotFound, + #[error("Error during database operation: {description}: {source}")] + DatabaseError { + source: anyhow::Error, + description: String, + }, + #[error("Error while decrypting keys")] + DecryptionError, +} + +impl axum::response::IntoResponse for KeyringServiceError { + fn into_response(self) -> axum::response::Response { + use axum::http::StatusCode; + match self { + Self::KeymapGenerationFailed => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), + Self::KeymapRestorationFailed => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), + Self::DocumentTypeNotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), + Self::DatabaseError { source, description } => + (StatusCode::INTERNAL_SERVER_ERROR, format!("{}: {}", description, source)).into_response(), + Self::DecryptionError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), + } + } +} #[derive(Clone, Debug)] pub struct KeyringService { @@ -22,7 +51,7 @@ impl KeyringService { ch_claims: ChClaims, _pid: String, dt_id: String, - ) -> anyhow::Result { + ) -> Result { trace!("generate_keys"); trace!("...user '{:?}'", &ch_claims.client_id); match self.db.get_msk().await { @@ -38,23 +67,23 @@ impl KeyringService { } Err(e) => { error!("Error while generating key map: {}", e); - Err(anyhow!("Error while generating keys")) // InternalError + Err(KeyringServiceError::KeymapGenerationFailed) } } } Ok(None) => { warn!("document type {} not found", &dt_id); - Err(anyhow!("Document type not found!")) // BadRequest + Err(KeyringServiceError::DocumentTypeNotFound) } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(anyhow!("Error while retrieving document type")) // InternalError + Err(KeyringServiceError::DatabaseError {source: e, description: "Error while retrieving document type".to_string()}) } } } Err(e) => { error!("Error while retrieving master key: {}", e); - Err(anyhow!("Error while generating keys")) // InternalError + Err(KeyringServiceError::DatabaseError {source: e, description: "Error while retrieving master key".to_string()}) } } } @@ -65,7 +94,7 @@ impl KeyringService { ch_claims: ChClaims, _pid: Option, key_cts: &KeyCtList, - ) -> anyhow::Result> { + ) -> Result, KeyringServiceError> { trace!("decrypt_keys"); trace!("...user '{:?}'", &ch_claims.client_id); debug!("number of cts to decrypt: {}", &key_cts.cts.len()); @@ -106,24 +135,24 @@ impl KeyringService { // Currently, we don't tolerate errors while decrypting keys if error_count > 0 { - Err(anyhow!("Error while decrypting keys")) // InternalError + Err(KeyringServiceError::DecryptionError) } else { Ok(key_maps) } } Ok(None) => { warn!("document type {} not found", &key_cts.dt); - Err(anyhow!("Document type not found!")) // BadRequest + Err(KeyringServiceError::DocumentTypeNotFound) } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(anyhow!("Document type not found!")) // NotFound + Err(KeyringServiceError::DatabaseError {source: e, description: "Error while retrieving document type".to_string()}) } } } Err(e) => { error!("Error while retrieving master key: {}", e); - Err(anyhow!("Error while decrypting keys")) // InternalError + Err(KeyringServiceError::DecryptionError) } } } @@ -135,7 +164,7 @@ impl KeyringService { keys_ct: String, _pid: Option, dt_id: String, - ) -> anyhow::Result { + ) -> Result { trace!("decrypt_key_map"); trace!("...user '{:?}'", &ch_claims.client_id); trace!("ct: {}", &keys_ct); @@ -148,30 +177,30 @@ impl KeyringService { // validate keys_ct input let keys_ct = hex::decode(keys_ct).map_err(|e| { error!("Error while decoding key ciphertext: {}", e); - anyhow!("Error while decrypting keys") // InternalError + KeyringServiceError::DecryptionError })?; match restore_key_map(key, dt, keys_ct) { Ok(key_map) => Ok(key_map), Err(e) => { error!("Error while generating key map: {}", e); - Err(anyhow!("Error while restoring keys")) // InternalError + Err(KeyringServiceError::KeymapRestorationFailed) } } } Ok(None) => { warn!("document type {} not found", &dt_id); - Err(anyhow!("Document type not found!")) // BadRequest + Err(KeyringServiceError::DocumentTypeNotFound) } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(anyhow!("Document type not found!")) // NotFound + Err(KeyringServiceError::DatabaseError {source: e, description: "Error while retrieving document type".to_string()}) } } } Err(e) => { error!("Error while retrieving master key: {}", e); - Err(anyhow!("Error while decrypting keys")) // InternalError + Err(KeyringServiceError::DecryptionError) } } } @@ -182,10 +211,11 @@ impl KeyringService { ch_claims: ChClaims, pid: Option, cts: &KeyCtList, - ) -> anyhow::Result> { + ) -> Result, KeyringServiceError> { self.decrypt_keys(ch_claims, pid, cts).await } + #[cfg(doc_type)] pub(crate) async fn create_doc_type( &self, doc_type: DocumentType, @@ -213,6 +243,7 @@ impl KeyringService { } } + #[cfg(doc_type)] pub(crate) async fn update_doc_type( &self, id: String, @@ -240,6 +271,7 @@ impl KeyringService { } } + #[cfg(doc_type)] pub(crate) async fn delete_doc_type(&self, id: String, pid: String) -> anyhow::Result { match self.db.delete_document_type(&id, &pid).await { Ok(true) => Ok(String::from("Document type deleted!")), // NoContent @@ -254,6 +286,7 @@ impl KeyringService { } } + #[cfg(doc_type)] pub(crate) async fn get_doc_type( &self, id: String, @@ -273,12 +306,14 @@ impl KeyringService { } } + #[cfg(doc_type)] + pub(crate) async fn get_doc_types(&self) -> anyhow::Result> { match self.db.get_all_document_types().await { //TODO: would like to send "{}" instead of "null" when dt is not found Ok(dt) => Ok(dt), Err(e) => { - error!("Error while retrieving default doctypes: {:?}", e); + error!("Error while retrieving default doc_types: {:?}", e); Err(anyhow!("Error while retrieving all document types")) // InternalError } } diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index b1a9078..d1ddf96 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -12,6 +12,7 @@ use crate::model::{ }; use crate::services::document_service::DocumentService; +/// Error type for LoggingService #[derive(Debug, thiserror::Error)] pub enum LoggingServiceError { #[error("Received empty payload, which cannot be logged!")] @@ -373,10 +374,7 @@ impl LoggingService { } Err(e) => { error!("Error while retrieving message: {:?}", e); - Err(LoggingServiceError::DatabaseError { - source: e, - description: format!("Error while retrieving messages for pid '{pid}'"), - }) + Err(LoggingServiceError::DocumentServiceError(e)) } } } From fc710b7afc2f8ff28729ee88315fd74777476c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 17 Oct 2023 00:11:47 +0200 Subject: [PATCH 118/183] feat(ch-app): Removed ApiResponse, fixed warnings and hid more doc_type related functions --- clearing-house-app/Cargo.toml | 1 + clearing-house-app/src/db/key_store.rs | 8 ++- clearing-house-app/src/errors.rs | 22 ------- clearing-house-app/src/main.rs | 4 +- clearing-house-app/src/model/claims.rs | 59 +----------------- clearing-house-app/src/model/doc_type.rs | 2 + clearing-house-app/src/model/document.rs | 14 ----- clearing-house-app/src/model/ids/message.rs | 46 -------------- clearing-house-app/src/model/ids/mod.rs | 17 ------ clearing-house-app/src/model/ids/request.rs | 15 +---- clearing-house-app/src/model/process.rs | 6 -- clearing-house-app/src/ports/doc_type_api.rs | 42 +++++++------ clearing-house-app/src/ports/logging_api.rs | 40 ++++++------ clearing-house-app/src/ports/mod.rs | 41 +------------ .../src/services/keyring_service.rs | 61 ++++++++++--------- .../src/services/logging_service.rs | 21 +------ 16 files changed, 95 insertions(+), 304 deletions(-) delete mode 100644 clearing-house-app/src/errors.rs diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 4fe145e..cfaf038 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -64,5 +64,6 @@ serial_test = "2.0.0" tempfile = "3.8.0" [features] +default = [] # Enables the doc_type API doc_type = [] diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index d8721c8..ad85931 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -2,8 +2,10 @@ use super::DataStoreApi; use crate::db::init_database_client; use crate::model::constants::{ FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, MONGO_COLL_DOC_TYPES, - MONGO_COLL_MASTER_KEY, MONGO_ID, MONGO_PID, + MONGO_COLL_MASTER_KEY, MONGO_ID }; +#[cfg(doc_type)] +use crate::model::constants::MONGO_PID; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; use anyhow::anyhow; @@ -179,6 +181,7 @@ impl KeyStore { } //TODO: Do we need to check that no documents of this type exist before we remove it from the db? + #[cfg(doc_type)] pub async fn delete_document_type(&self, id: &String, pid: &String) -> anyhow::Result { let coll = self .database @@ -194,6 +197,7 @@ impl KeyStore { } /// checks if the model exits + #[cfg(doc_type)] pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> anyhow::Result { let coll = self .database @@ -214,6 +218,7 @@ impl KeyStore { } } + #[cfg(doc_type)] pub async fn get_all_document_types(&self) -> anyhow::Result> { let coll = self .database @@ -241,6 +246,7 @@ impl KeyStore { } } + #[cfg(doc_type)] pub async fn update_document_type( &self, doc_type: DocumentType, diff --git a/clearing-house-app/src/errors.rs b/clearing-house-app/src/errors.rs deleted file mode 100644 index 5baf5be..0000000 --- a/clearing-house-app/src/errors.rs +++ /dev/null @@ -1,22 +0,0 @@ -type AppResult = Result; - -#[derive(Debug)] -pub enum AppError { - Generic(anyhow::Error), -} - -impl std::error::Error for AppError {} - -impl std::fmt::Display for AppError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - AppError::Generic(e) => write!(f, "{}", e), - } - } -} - -impl From for AppError { - fn from(err: anyhow::Error) -> Self { - Self::Generic(err) - } -} diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index 1f3a4b7..75f5284 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -15,7 +15,6 @@ use std::sync::Arc; mod config; mod crypto; mod db; -mod errors; mod model; mod ports; mod services; @@ -24,6 +23,7 @@ mod util; /// Contains the application state #[derive(Clone)] pub(crate) struct AppState { + #[cfg_attr(not(doc_type), allow(dead_code))] pub keyring_service: Arc, pub logging_service: Arc, pub service_config: Arc, @@ -92,7 +92,7 @@ async fn main() -> Result<(), anyhow::Error> { // Bind port and start server let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); - tracing::info!("Starting server: Listening on {}", addr); + info!("Starting server: Listening on {}", addr); Ok(axum::Server::bind(&addr) .serve(app.into_make_service()) .with_graceful_shutdown(util::shutdown_signal()) diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index ebb944b..f5b3a07 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -3,7 +3,6 @@ use crate::AppState; use anyhow::Context; use axum::extract::FromRef; use axum::response::IntoResponse; -use chrono::{Duration, Utc}; use num_bigint::BigUint; use ring::signature::KeyPair; use std::env; @@ -27,12 +26,6 @@ impl std::fmt::Display for ChClaims { } } -#[derive(Debug)] -pub enum ChClaimsError { - Missing, - Invalid, -} - pub struct ExtractChClaims(pub ChClaims); #[async_trait::async_trait] @@ -72,7 +65,8 @@ where } pub fn get_jwks(key_path: &str) -> Option> { - let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); + let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path) + .unwrap_or_else(|_| panic!("Failed to load keyfile from path {key_path}")); if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { let pk_modulus = BigUint::from_bytes_be( @@ -134,55 +128,6 @@ pub fn get_fingerprint(key_path: &str) -> Option { } } -pub fn create_service_token(issuer: &str, audience: &str, client_id: &str) -> String { - let private_claims = ChClaims::new(client_id); - create_token(issuer, audience, &private_claims) -} - -pub fn create_token< - T: std::fmt::Display + Clone + serde::Serialize + for<'de> serde::Deserialize<'de>, ->( - issuer: &str, - audience: &str, - private_claims: &T, -) -> String { - let signing_secret = match env::var(ENV_SHARED_SECRET) { - Ok(secret) => biscuit::jws::Secret::Bytes(secret.to_string().into_bytes()), - Err(_) => { - panic!( - "Shared Secret not configured. Please configure environment variable {}", - ENV_SHARED_SECRET - ); - } - }; - let expiration_date = Utc::now() + Duration::minutes(5); - - let claims = biscuit::ClaimsSet:: { - registered: biscuit::RegisteredClaims { - issuer: Some(issuer.to_string()), - issued_at: Some(biscuit::Timestamp::from(Utc::now())), - audience: Some(biscuit::SingleOrMultiple::Single(audience.to_string())), - expiry: Some(biscuit::Timestamp::from(expiration_date)), - ..Default::default() - }, - private: private_claims.clone(), - }; - - // Construct the JWT - let jwt = biscuit::jws::Compact::new_decoded( - From::from(biscuit::jws::RegisteredHeader { - algorithm: biscuit::jwa::SignatureAlgorithm::HS256, - ..Default::default() - }), - claims, - ); - - jwt.into_encoded(&signing_secret) - .unwrap() - .unwrap_encoded() - .to_string() -} - pub fn decode_token serde::Deserialize<'de>>( token: &str, audience: &str, diff --git a/clearing-house-app/src/model/doc_type.rs b/clearing-house-app/src/model/doc_type.rs index 2cff304..af9235b 100644 --- a/clearing-house-app/src/model/doc_type.rs +++ b/clearing-house-app/src/model/doc_type.rs @@ -6,6 +6,7 @@ pub struct DocumentType { } impl DocumentType { + #[cfg(test)] pub fn new(id: String, pid: String, parts: Vec) -> DocumentType { DocumentType { id, pid, parts } } @@ -17,6 +18,7 @@ pub struct DocumentTypePart { } impl DocumentTypePart { + #[cfg(test)] pub fn new(name: String) -> DocumentTypePart { DocumentTypePart { name } } diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index da6219c..5104db7 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -277,20 +277,6 @@ impl EncryptedDocument { } } -/// companion to format_pt_for_storage -pub fn restore_pt(pt: &str) -> anyhow::Result<(String, String, String)> { - trace!("Trying to restore plain text"); - let vec: Vec<&str> = pt.split(SPLIT_CT).collect(); - if vec.len() != 3 { - anyhow::bail!("Could not restore plaintext"); - } - Ok(( - String::from(vec[0]), - String::from(vec[1]), - String::from(vec[2]), - )) -} - /// companion to format_pt_for_storage_no_dt pub fn restore_pt_no_dt(pt: &str) -> anyhow::Result<(String, String)> { trace!("Trying to restore plain text"); diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index a089797..b5c6970 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -16,10 +16,6 @@ const SENDER_AGENT: &str = "sender_agent"; const PAYLOAD: &str = "payload"; const PAYLOAD_TYPE: &str = "payload_type"; -pub const RESULT_MESSAGE: &str = "ResultMessage"; -pub const REJECTION_MESSAGE: &str = "RejectionMessage"; -pub const MESSAGE_PROC_NOTIFICATION_MESSAGE: &str = "MessageProcessedNotificationMessage"; - /// Metadata describing payload exchanged by interacting Connectors. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct IdsMessage { @@ -143,48 +139,6 @@ impl Default for IdsMessage { } impl IdsMessage { - pub fn processed(msg: IdsMessage) -> IdsMessage { - let mut message = IdsMessage::clone(msg); - message.id = Some(autogen(MESSAGE_PROC_NOTIFICATION_MESSAGE)); - message.type_message = MessageType::MessageProcessedNotificationMessage; - message - } - - pub fn return_result(msg: IdsMessage) -> IdsMessage { - let mut message = IdsMessage::clone(msg); - message.id = Some(autogen(RESULT_MESSAGE)); - message.type_message = MessageType::ResultMessage; - message - } - - pub fn error(msg: IdsMessage) -> IdsMessage { - let mut message = IdsMessage::clone(msg); - message.id = Some(autogen(REJECTION_MESSAGE)); - message.type_message = MessageType::RejectionMessage; - message - } - - fn clone(msg: IdsMessage) -> IdsMessage { - IdsMessage { - context: msg.context.clone(), - type_message: msg.type_message.clone(), - id: msg.id.clone(), - pid: msg.pid.clone(), - model_version: msg.model_version.clone(), - correlation_message: msg.correlation_message.clone(), - issued: msg.issued.clone(), - issuer_connector: msg.issuer_connector.clone(), - sender_agent: msg.sender_agent.clone(), - recipient_connector: msg.recipient_connector.clone(), - recipient_agent: msg.recipient_agent.clone(), - transfer_contract: msg.transfer_contract.clone(), - security_token: msg.security_token.clone(), - authorization_token: msg.authorization_token.clone(), - payload: msg.payload.clone(), - content_version: msg.content_version.clone(), - payload_type: msg.payload.clone(), - } - } pub fn restore() -> IdsMessage { IdsMessage { diff --git a/clearing-house-app/src/model/ids/mod.rs b/clearing-house-app/src/model/ids/mod.rs index d6fa87c..ad494b3 100644 --- a/clearing-house-app/src/model/ids/mod.rs +++ b/clearing-house-app/src/model/ids/mod.rs @@ -42,9 +42,6 @@ impl InfoModelId { pub fn new(id: String) -> InfoModelId { InfoModelId::SimpleId(id) } - pub fn complex(id: InfoModelComplexId) -> InfoModelId { - InfoModelId::ComplexId(id) - } } impl std::fmt::Display for InfoModelId { @@ -73,9 +70,6 @@ impl InfoModelDateTime { pub fn new() -> InfoModelDateTime { InfoModelDateTime::Time(chrono::Local::now()) } - pub fn complex() -> InfoModelDateTime { - InfoModelDateTime::ComplexTime(InfoModelTimeStamp::default()) - } } impl std::fmt::Display for InfoModelDateTime { @@ -346,17 +340,6 @@ pub struct SecurityToken { pub token_value: String, } -impl SecurityToken { - pub fn new() -> SecurityToken { - SecurityToken { - type_message: MessageType::DAPSToken, - id: Some(String::new()), - token_format: None, - token_value: String::new(), - } - } -} - #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct IdsQueryResult { pub date_from: String, diff --git a/clearing-house-app/src/model/ids/request.rs b/clearing-house-app/src/model/ids/request.rs index fac3d7f..88fa8cf 100644 --- a/clearing-house-app/src/model/ids/request.rs +++ b/clearing-house-app/src/model/ids/request.rs @@ -1,5 +1,6 @@ use crate::model::ids::message::IdsMessage; +/// IDS Multipart message represented as a JSON struct #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ClearingHouseMessage { pub header: IdsMessage, @@ -7,17 +8,3 @@ pub struct ClearingHouseMessage { #[serde(rename = "payloadType")] pub payload_type: Option, } - -impl ClearingHouseMessage { - pub fn new( - header: IdsMessage, - payload: Option, - payload_type: Option, - ) -> ClearingHouseMessage { - ClearingHouseMessage { - header, - payload, - payload_type, - } - } -} diff --git a/clearing-house-app/src/model/process.rs b/clearing-house-app/src/model/process.rs index c623f90..d2f095c 100644 --- a/clearing-house-app/src/model/process.rs +++ b/clearing-house-app/src/model/process.rs @@ -24,12 +24,6 @@ pub struct OwnerList { pub owners: Vec, } -impl OwnerList { - pub fn new(owners: Vec) -> Self { - Self { owners } - } -} - #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Receipt { pub data: biscuit::jws::Compact, diff --git a/clearing-house-app/src/ports/doc_type_api.rs b/clearing-house-app/src/ports/doc_type_api.rs index ac404f9..eb4df3e 100644 --- a/clearing-house-app/src/ports/doc_type_api.rs +++ b/clearing-house-app/src/ports/doc_type_api.rs @@ -1,17 +1,21 @@ +use axum::http::StatusCode; use crate::model::constants::DEFAULT_PROCESS_ID; use crate::ports::ApiResponse; use crate::model::doc_type::DocumentType; +use crate::services::keyring_service::KeyringServiceError; + +type DocApiResult = super::ApiResult; async fn create_doc_type( axum::extract::State(state): axum::extract::State, axum::extract::Json(doc_type): axum::extract::Json, -) -> ApiResponse { +) -> DocApiResult { match state.keyring_service.create_doc_type(doc_type).await { - Ok(dt) => ApiResponse::SuccessCreate(dt), + Ok(dt) => Ok((StatusCode::CREATED, Json(dt))), Err(e) => { error!("Error while adding doctype: {:?}", e); - ApiResponse::InternalError(e.to_string()) + Err(e) } } } @@ -20,12 +24,12 @@ async fn update_doc_type( axum::extract::State(state): axum::extract::State, axum::extract::Path(id): axum::extract::Path, axum::extract::Json(doc_type): axum::extract::Json, -) -> ApiResponse { +) -> DocApiResult { match state.keyring_service.update_doc_type(id, doc_type).await { - Ok(id) => ApiResponse::SuccessOk(id), + Ok(id) => Ok((StatusCode::OK, Json(id))), Err(e) => { error!("Error while adding doctype: {:?}", e); - ApiResponse::InternalError(e.to_string()) + Err(e) } } } @@ -33,7 +37,7 @@ async fn update_doc_type( async fn delete_default_doc_type( state: axum::extract::State, id: axum::extract::Path, -) -> ApiResponse { +) -> DocApiResult { delete_doc_type( state, id, @@ -46,12 +50,12 @@ async fn delete_doc_type( axum::extract::State(state): axum::extract::State, axum::extract::Path(id): axum::extract::Path, axum::extract::Path(pid): axum::extract::Path, -) -> ApiResponse { +) -> DocApiResult { match state.keyring_service.delete_doc_type(id, pid).await { - Ok(id) => ApiResponse::SuccessOk(id), + Ok(id) => Ok((StatusCode::OK, Json(id))), Err(e) => { error!("Error while deleting doctype: {:?}", e); - ApiResponse::InternalError(e.to_string()) + Err(e) } } } @@ -59,7 +63,7 @@ async fn delete_doc_type( async fn get_default_doc_type( state: axum::extract::State, id: axum::extract::Path, -) -> ApiResponse> { +) -> DocApiResult> { get_doc_type( state, id, @@ -73,15 +77,15 @@ async fn get_doc_type( axum::extract::State(state): axum::extract::State, axum::extract::Path(id): axum::extract::Path, axum::extract::Path(pid): axum::extract::Path, -) -> ApiResponse> { +) -> DocApiResult> { match state.keyring_service.get_doc_type(id, pid).await { Ok(dt) => match dt { - Some(dt) => ApiResponse::SuccessOk(Some(dt)), - None => ApiResponse::SuccessOk(None), + Some(dt) => Ok((StatusCode::OK, Json(Some(dt)))), + None => Ok((StatusCode::OK, Json(None))) }, Err(e) => { error!("Error while retrieving doctype: {:?}", e); - ApiResponse::InternalError(e.to_string()) + Err(e) } } } @@ -89,12 +93,12 @@ async fn get_doc_type( //#[rocket::get("/", format = "json")] async fn get_doc_types( axum::extract::State(state): axum::extract::State, -) -> ApiResponse> { +) -> DocApiResult> { match state.keyring_service.get_doc_types().await { - Ok(dt) => ApiResponse::SuccessOk(dt), + Ok(dt) => Ok((StatusCode::OK, Json(dt))), Err(e) => { - error!("Error while retrieving doctypes: {:?}", e); - ApiResponse::InternalError(e.to_string()) + error!("Error while retrieving doc_types: {:?}", e); + Err(e) } } } diff --git a/clearing-house-app/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs index 58a0ba0..4060d31 100644 --- a/clearing-house-app/src/ports/logging_api.rs +++ b/clearing-house-app/src/ports/logging_api.rs @@ -1,27 +1,32 @@ +use axum::http::StatusCode; +use axum::Json; use crate::model::claims::ExtractChClaims; -use crate::{model::claims::get_jwks, model::SortingOrder, ports::ApiResponse, AppState}; +use crate::{model::claims::get_jwks, model::SortingOrder, AppState}; use biscuit::jwk::JWKSet; use crate::model::ids::message::IdsMessage; use crate::model::ids::request::ClearingHouseMessage; use crate::model::ids::IdsQueryResult; use crate::model::process::Receipt; +use crate::services::logging_service::LoggingServiceError; + +type LoggingApiResult = super::ApiResult; async fn log( ExtractChClaims(ch_claims): ExtractChClaims, axum::extract::State(state): axum::extract::State, axum::extract::Path(pid): axum::extract::Path, axum::extract::Json(message): axum::extract::Json, -) -> ApiResponse { +) -> LoggingApiResult { match state .logging_service .log(ch_claims, state.signing_key_path.as_str(), message, pid) .await { - Ok(id) => ApiResponse::SuccessCreate(id), + Ok(id) => Ok((StatusCode::CREATED, Json(id))), Err(e) => { error!("Error while logging: {:?}", e); - ApiResponse::InternalError(String::from("Error while logging!")) + Err(e) } } } @@ -31,16 +36,16 @@ async fn create_process( axum::extract::State(state): axum::extract::State, axum::extract::Path(pid): axum::extract::Path, axum::extract::Json(message): axum::extract::Json, -) -> ApiResponse { +) -> LoggingApiResult { match state .logging_service .create_process(ch_claims, message, pid) .await { - Ok(id) => ApiResponse::SuccessCreate(id), + Ok(id) => Ok((StatusCode::CREATED, Json(id))), Err(e) => { error!("Error while creating process: {:?}", e); - ApiResponse::InternalError(String::from("Error while creating process!")) + Err(e) } } } @@ -59,8 +64,8 @@ async fn query_pid( axum::extract::State(state): axum::extract::State, axum::extract::Query(params): axum::extract::Query, axum::extract::Path(pid): axum::extract::Path, - axum::extract::Json(message): axum::extract::Json, -) -> ApiResponse { + axum::extract::Json(_): axum::extract::Json, +) -> LoggingApiResult { match state .logging_service .query_pid( @@ -71,14 +76,13 @@ async fn query_pid( params.date_to, params.date_from, pid, - message, ) .await { - Ok(result) => ApiResponse::SuccessOk(result), + Ok(result) => Ok((StatusCode::OK, Json(result))), Err(e) => { error!("Error while querying: {:?}", e); - ApiResponse::InternalError(String::from("Error while querying!")) + Err(e) } } } @@ -89,26 +93,26 @@ async fn query_id( axum::extract::Path(pid): axum::extract::Path, axum::extract::Path(id): axum::extract::Path, axum::extract::Json(message): axum::extract::Json, -) -> ApiResponse { +) -> LoggingApiResult { match state .logging_service .query_id(ch_claims, pid, id, message) .await { - Ok(result) => ApiResponse::SuccessOk(result), + Ok(result) => Ok((StatusCode::OK, Json(result))), Err(e) => { error!("Error while querying: {:?}", e); - ApiResponse::InternalError(String::from("Error while querying!")) + Err(e) } } } async fn get_public_sign_key( axum::extract::State(state): axum::extract::State, -) -> ApiResponse> { +) -> super::ApiResult, &'static str> { match get_jwks(state.signing_key_path.as_str()) { - Some(jwks) => ApiResponse::SuccessOk(jwks), - None => ApiResponse::InternalError(String::from("Error reading signing key")), + Some(jwks) => Ok((StatusCode::OK, Json(jwks))), + None => Err("Error reading signing key"), } } diff --git a/clearing-house-app/src/ports/mod.rs b/clearing-house-app/src/ports/mod.rs index 3322d40..3636fbb 100644 --- a/clearing-house-app/src/ports/mod.rs +++ b/clearing-house-app/src/ports/mod.rs @@ -3,7 +3,6 @@ //! This module contains the ports of the logging service. Ports are used to communicate with other //! services. In this case, the logging service implements REST-API endpoints to provide access to //! the logging service. -use axum::response::Response; use crate::AppState; #[cfg(doc_type)] @@ -25,41 +24,5 @@ pub(crate) fn router() -> axum::routing::Router { .merge(logging_api::router()) } -#[derive(Debug)] -pub(crate) enum ApiResponse { - PreFlight(()), - BadRequest(String), - SuccessCreate(T), - SuccessOk(T), - SuccessNoContent(String), - Unauthorized(String), - Forbidden(String), - NotFound(String), - InternalError(String), -} - -impl axum::response::IntoResponse for ApiResponse { - fn into_response(self) -> Response { - match self { - ApiResponse::PreFlight(_) => (axum::http::StatusCode::OK, "").into_response(), - ApiResponse::BadRequest(s) => (axum::http::StatusCode::BAD_REQUEST, s).into_response(), - ApiResponse::SuccessCreate(v) => { - (axum::http::StatusCode::CREATED, axum::response::Json(v)).into_response() - } - ApiResponse::SuccessOk(v) => { - (axum::http::StatusCode::OK, axum::response::Json(v)).into_response() - } - ApiResponse::SuccessNoContent(s) => { - (axum::http::StatusCode::NO_CONTENT, s).into_response() - } - ApiResponse::Unauthorized(s) => { - (axum::http::StatusCode::UNAUTHORIZED, s).into_response() - } - ApiResponse::Forbidden(s) => (axum::http::StatusCode::FORBIDDEN, s).into_response(), - ApiResponse::NotFound(s) => (axum::http::StatusCode::NOT_FOUND, s).into_response(), - ApiResponse::InternalError(s) => { - (axum::http::StatusCode::INTERNAL_SERVER_ERROR, s).into_response() - } - } - } -} +/// Result type alias for the API +pub(crate) type ApiResult = Result<(axum::http::StatusCode, axum::response::Json), E>; \ No newline at end of file diff --git a/clearing-house-app/src/services/keyring_service.rs b/clearing-house-app/src/services/keyring_service.rs index 2cbd6a2..df08149 100644 --- a/clearing-house-app/src/services/keyring_service.rs +++ b/clearing-house-app/src/services/keyring_service.rs @@ -19,6 +19,9 @@ pub enum KeyringServiceError { }, #[error("Error while decrypting keys")] DecryptionError, + #[cfg_attr(not(doc_type), allow(dead_code))] + #[error("Document type already exists")] + DocumentTypeAlreadyExists, } impl axum::response::IntoResponse for KeyringServiceError { @@ -31,6 +34,7 @@ impl axum::response::IntoResponse for KeyringServiceError { Self::DatabaseError { source, description } => (StatusCode::INTERNAL_SERVER_ERROR, format!("{}: {}", description, source)).into_response(), Self::DecryptionError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), + Self::DocumentTypeAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), } } } @@ -77,13 +81,13 @@ impl KeyringService { } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError {source: e, description: "Error while retrieving document type".to_string()}) + Err(KeyringServiceError::DatabaseError { source: e, description: "Error while retrieving document type".to_string() }) } } } Err(e) => { error!("Error while retrieving master key: {}", e); - Err(KeyringServiceError::DatabaseError {source: e, description: "Error while retrieving master key".to_string()}) + Err(KeyringServiceError::DatabaseError { source: e, description: "Error while retrieving master key".to_string() }) } } } @@ -146,7 +150,7 @@ impl KeyringService { } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError {source: e, description: "Error while retrieving document type".to_string()}) + Err(KeyringServiceError::DatabaseError { source: e, description: "Error while retrieving document type".to_string() }) } } } @@ -194,7 +198,7 @@ impl KeyringService { } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError {source: e, description: "Error while retrieving document type".to_string()}) + Err(KeyringServiceError::DatabaseError { source: e, description: "Error while retrieving document type".to_string() }) } } } @@ -218,27 +222,27 @@ impl KeyringService { #[cfg(doc_type)] pub(crate) async fn create_doc_type( &self, - doc_type: DocumentType, - ) -> anyhow::Result { + doc_type: crate::model::doc_type::DocumentType, + ) -> Result { debug!("adding doctype: {:?}", &doc_type); match self .db .exists_document_type(&doc_type.pid, &doc_type.id) .await { - Ok(true) => Err(anyhow!("doctype already exists!")), // BadRequest + Ok(true) => Err(KeyringServiceError::DocumentTypeAlreadyExists), // BadRequest Ok(false) => { match self.db.add_document_type(doc_type.clone()).await { Ok(()) => Ok(doc_type), Err(e) => { error!("Error while adding doctype: {:?}", e); - Err(anyhow!("Error while adding document type!")) // InternalError + Err(KeyringServiceError::DatabaseError { source: e, description: "Error while adding doctype".to_string() }) } } } Err(e) => { error!("Error while adding document type: {:?}", e); - Err(anyhow!("Error while checking database!")) // InternalError + Err(KeyringServiceError::DatabaseError { source: e, description: "Error while checking doctype".to_string() }) } } } @@ -247,41 +251,42 @@ impl KeyringService { pub(crate) async fn update_doc_type( &self, id: String, - doc_type: DocumentType, - ) -> anyhow::Result { + doc_type: crate::model::doc_type::DocumentType, + ) -> Result { match self .db .exists_document_type(&doc_type.pid, &doc_type.id) .await { - Ok(true) => Err(anyhow!("Doctype already exists!")), // BadRequest + Ok(true) => Err(KeyringServiceError::DocumentTypeAlreadyExists), Ok(false) => { match self.db.update_document_type(doc_type, &id).await { Ok(id) => Ok(id), Err(e) => { error!("Error while adding doctype: {:?}", e); - Err(anyhow!("Error while storing document type!")) // InternalError + Err(KeyringServiceError::DatabaseError { source: e, description: "Error while storing document type!".to_string() }) } } } Err(e) => { error!("Error while adding document type: {:?}", e); - Err(anyhow!("Error while checking database!")) // InternalError + Err(KeyringServiceError::DatabaseError { source: e, description: "Error while checking doctype".to_string() }) } } } #[cfg(doc_type)] - pub(crate) async fn delete_doc_type(&self, id: String, pid: String) -> anyhow::Result { + pub(crate) async fn delete_doc_type(&self, id: String, pid: String) -> Result { match self.db.delete_document_type(&id, &pid).await { Ok(true) => Ok(String::from("Document type deleted!")), // NoContent - Ok(false) => Err(anyhow!("Document type does not exist!")), // NotFound + Ok(false) => Err(KeyringServiceError::DocumentTypeNotFound), Err(e) => { error!("Error while deleting doctype: {:?}", e); - Err(anyhow!( - "Error while deleting document type with id {}!", - id - )) // InternalError + Err(KeyringServiceError::DatabaseError { + source: e, + description: format!("Error while deleting document type with id {}!", + id), + }) } } } @@ -291,30 +296,28 @@ impl KeyringService { &self, id: String, pid: String, - ) -> anyhow::Result> { + ) -> Result, KeyringServiceError> { match self.db.get_document_type(&id).await { //TODO: would like to send "{}" instead of "null" when dt is not found Ok(dt) => Ok(dt), Err(e) => { error!("Error while retrieving doctype: {:?}", e); - Err(anyhow!( - "Error while retrieving document type with id {} and pid {}!", - id, - pid - )) // InternalError + Err(KeyringServiceError::DatabaseError { source: e, description: format!("Error while retrieving document type with id {} and pid {}!", id, pid) }) } } } #[cfg(doc_type)] - - pub(crate) async fn get_doc_types(&self) -> anyhow::Result> { + pub(crate) async fn get_doc_types(&self) -> Result, KeyringServiceError> { match self.db.get_all_document_types().await { //TODO: would like to send "{}" instead of "null" when dt is not found Ok(dt) => Ok(dt), Err(e) => { error!("Error while retrieving default doc_types: {:?}", e); - Err(anyhow!("Error while retrieving all document types")) // InternalError + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while retrieving all document types".to_string(), + }) } } } diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index d1ddf96..26010c3 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -26,13 +26,6 @@ pub enum LoggingServiceError { }, #[error("User not authorized!")] UserNotAuthorized, - #[error("Authorization failed!")] - AuthorizationFailed { - source: anyhow::Error, - description: String, - }, - #[error("Document already exists!")] - DocumentAlreadyExists, #[error("Invalid request received!")] InvalidRequest, #[error("Process already exists!")] @@ -66,17 +59,6 @@ impl axum::response::IntoResponse for LoggingServiceError { Self::UserNotAuthorized => { (StatusCode::FORBIDDEN, self.to_string()).into_response() } - Self::AuthorizationFailed { - source, - description, - } => ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("{}: {}", description, source), - ) - .into_response(), - Self::DocumentAlreadyExists => { - (StatusCode::BAD_REQUEST, self.to_string()).into_response() - } Self::InvalidRequest => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } @@ -238,7 +220,7 @@ impl LoggingService { match m.payload { Some(ref payload) if !payload.is_empty() => { trace!("OwnerList: '{:#?}'", &payload); - match serde_json::from_str::(&payload) { + match serde_json::from_str::(payload) { Ok(owner_list) => { for o in owner_list.owners { if !owners.contains(&o) { @@ -300,7 +282,6 @@ impl LoggingService { date_to: Option, date_from: Option, pid: String, - _message: ClearingHouseMessage, ) -> Result { debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); From 092d474c2219cc672dcfa84bee0f0f826e78b2e1 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Tue, 17 Oct 2023 15:05:30 -0300 Subject: [PATCH 119/183] feat (ch-edc): TypeManagerUtilTest errorConvertingToJson and successfulToJson included --- .../clearinghouse/edc/types/ids/Message.java | 2 +- .../edc/types/TypeManagerUtilTest.java | 35 +++++++++++++------ 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java index 8cac60c..c363fbc 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/Message.java @@ -53,7 +53,7 @@ public class Message { String modelVersion; @JsonProperty("ids:issued") - @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSzzz") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS") @NotNull XMLGregorianCalendar issued; diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java index d0794de..b886b25 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java @@ -1,17 +1,15 @@ package de.truzzt.clearinghouse.edc.types; import com.fasterxml.jackson.databind.ObjectMapper; -import de.fraunhofer.iais.eis.LogMessage; import de.truzzt.clearinghouse.edc.tests.TestUtils; import de.truzzt.clearinghouse.edc.types.ids.Message; import org.eclipse.edc.spi.EdcException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; -import org.mockito.Spy; +import org.mockito.MockitoAnnotations; import java.io.ByteArrayInputStream; -import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; @@ -19,26 +17,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; class TypeManagerUtilTest { - private static final String VALID_HEADER_JSON = "messages/valid-header.json"; - - @Spy + @Mock private ObjectMapper objectMapper; @Mock private TypeManagerUtil typeManagerUtil; @BeforeEach void setUp() { - objectMapper = new ObjectMapper(); + MockitoAnnotations.openMocks(this); typeManagerUtil = new TypeManagerUtil(objectMapper); } @Test - void successfulParse() throws IOException { - + void successfulParse() throws IOException, InstantiationException, IllegalAccessException { + typeManagerUtil = new TypeManagerUtil(new ObjectMapper()); InputStream is = new FileInputStream(TestUtils.getValidHeaderFile()); + Message msg = typeManagerUtil.parse(is, Message.class); assertNotNull(msg); assertEquals("ids:LogMessage", msg.getType()); @@ -46,7 +45,7 @@ void successfulParse() throws IOException { @Test void typeErrorParse() { - + typeManagerUtil = new TypeManagerUtil(new ObjectMapper()); EdcException exception = assertThrows(EdcException.class, () -> typeManagerUtil.parse( @@ -60,8 +59,10 @@ void typeErrorParse() { } + @Test void successfulToJson() throws IOException { - + objectMapper = new ObjectMapper(); + typeManagerUtil = new TypeManagerUtil(objectMapper); Message msgBefore = objectMapper.readValue(TestUtils.getValidHeaderFile(), Message.class); byte[] json = typeManagerUtil.toJson(msgBefore); @@ -73,4 +74,16 @@ void successfulToJson() throws IOException { assertEquals(msgBefore.getType(), msgAfter.getType()); } + + @Test + void errorConvertingToJson() throws IOException { + doThrow(new EdcException("Error converting to JSON")).when(objectMapper).writeValueAsBytes(anyString()); + + EdcException exception = + assertThrows(EdcException.class, + () -> typeManagerUtil.toJson("") + ); + + assertEquals("Error converting to JSON",exception.getMessage() ); + } } \ No newline at end of file From 391c42f9f3d5ed5b0db888a748b7330db24e5c57 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Wed, 18 Oct 2023 19:22:32 -0300 Subject: [PATCH 120/183] feat (ch-edc): ResponseUtilTest and VocabUtilTest included --- .../edc/types/ids/util/VocabUtilTest.java | 46 +++++++++ .../edc/util/ResponseUtilTest.java | 97 +++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtilTest.java create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/util/ResponseUtilTest.java diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtilTest.java new file mode 100644 index 0000000..5d8da42 --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtilTest.java @@ -0,0 +1,46 @@ +package de.truzzt.clearinghouse.edc.types.ids.util; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VocabUtilTest { + @Mock + private VocabUtil vocabUtil; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void successfulCreateRandomUrl() { + vocabUtil.randomUrlBase = "http://www.test"; + var response = vocabUtil.createRandomUrl("test-successful"); + assertNotNull(response); + assertEquals("http://www.test/test-successful/", response.toString().substring(0,32)); + } + + @Test + void errorInvalidUrlCreateRandomUrl() { + vocabUtil.randomUrlBase = "htt://....."; + RuntimeException exception = assertThrows(RuntimeException.class, () -> + vocabUtil.createRandomUrl("test-successful")); + + assertNotNull(exception); + assertEquals("java.net.MalformedURLException: unknown protocol: htt", exception.getMessage()); + } + + @Test + void successfulNullRandomUrlCreateRandomUrl() { + vocabUtil.randomUrlBase = null; + var response = vocabUtil.createRandomUrl("test-successful"); + assertNotNull(response); + assertEquals("https://w3id.org/idsa/autogen/test-successful/", response.toString().substring(0,46)); + } +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/util/ResponseUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/util/ResponseUtilTest.java new file mode 100644 index 0000000..fce718e --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/util/ResponseUtilTest.java @@ -0,0 +1,97 @@ +package de.truzzt.clearinghouse.edc.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.tests.TestUtils; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class ResponseUtilTest { + + @Mock + private ResponseUtil responseUtil; + @Mock + private TypeManagerUtil typeManagerUtil; + @Mock + private IdsId connectorId; + + private ObjectMapper objectMapper; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + typeManagerUtil = new TypeManagerUtil(new ObjectMapper()); + objectMapper = new ObjectMapper(); + } + + @Test + public void createFormDataMultiPart() { + + var response = responseUtil.createFormDataMultiPart(typeManagerUtil, + "Header Name", + TestUtils.getValidHeader(new ObjectMapper()), + "Payload", + "Payload Value" + ); + + assertNotNull(response); + } + + @Test + public void testCreateFormDataMultiPart() { + var response = responseUtil.createFormDataMultiPart(typeManagerUtil, + "Header Name", + TestUtils.getValidHeader(objectMapper) + ); + + assertNotNull(response); + } + + @Test + public void createMultipartResponse() { + var response = responseUtil.createMultipartResponse(TestUtils.getValidHeader(objectMapper), + "Payload Value"); + + assertNotNull(response); + } + + @Test + public void messageProcessedNotification() { + var response = responseUtil.messageProcessedNotification(TestUtils.getValidHeader(objectMapper), connectorId); + + assertNotNull(response); + } + + @Test + public void notAuthenticated() { + var response = responseUtil.notAuthenticated(TestUtils.getValidHeader(objectMapper), connectorId); + + assertNotNull(response); + } + + @Test + public void malformedMessage() { + var response = responseUtil.malformedMessage(TestUtils.getValidHeader(objectMapper), connectorId); + + assertNotNull(response); + } + + @Test + public void messageTypeNotSupported() { + var response = responseUtil.messageTypeNotSupported(TestUtils.getValidHeader(objectMapper), connectorId); + + assertNotNull(response); + } + + @Test + public void internalRecipientError() { + var response = responseUtil.internalRecipientError(TestUtils.getValidHeader(objectMapper), connectorId); + + assertNotNull(response); + } +} \ No newline at end of file From 463abc06ff25f2707039bbe39d9c1d6645e646a6 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 19 Oct 2023 12:28:45 -0300 Subject: [PATCH 121/183] feat (ch-edc): extension/multipart unit tests --- clearing-house-edc/build.gradle.kts | 4 +- clearing-house-edc/core/build.gradle.kts | 14 +- .../edc/types/TypeManagerUtil.java | 4 +- .../edc/types/ids/RejectionMessage.java | 2 + .../clearinghouse/edc/util/ResponseUtil.java | 10 + .../clearinghouse/edc/app/AppSenderTest.java | 8 +- .../edc/handler/LogMessageHandlerTest.java | 2 +- .../edc/types/TypeManagerUtilTest.java | 13 +- .../clearinghouse/edc/tests/TestUtils.java | 126 +++++----- .../resources/headers/invalid-header.json} | 0 .../resources/headers/invalid-token.json | 20 ++ .../resources/headers/invalid-type.json | 20 ++ .../resources/headers/missing-fields.json | 20 ++ .../resources/headers/missing-token.json | 12 + .../resources/headers}/valid-header.json | 0 .../resources/headers/valid-response.json | 20 ++ .../extensions/multipart/build.gradle.kts | 12 +- .../edc/multipart/MultipartController.java | 37 ++- .../multipart/MultipartControllerTest.java | 228 ++++++++++++++++-- 19 files changed, 438 insertions(+), 114 deletions(-) rename clearing-house-edc/core/src/{test => testFixtures}/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java (63%) rename clearing-house-edc/core/src/{test/resources/messages/invalid-log-message-header.json => testFixtures/resources/headers/invalid-header.json} (100%) create mode 100644 clearing-house-edc/core/src/testFixtures/resources/headers/invalid-token.json create mode 100644 clearing-house-edc/core/src/testFixtures/resources/headers/invalid-type.json create mode 100644 clearing-house-edc/core/src/testFixtures/resources/headers/missing-fields.json create mode 100644 clearing-house-edc/core/src/testFixtures/resources/headers/missing-token.json rename clearing-house-edc/core/src/{test/resources/messages => testFixtures/resources/headers}/valid-header.json (100%) create mode 100644 clearing-house-edc/core/src/testFixtures/resources/headers/valid-response.json diff --git a/clearing-house-edc/build.gradle.kts b/clearing-house-edc/build.gradle.kts index 940fa25..8759e5f 100644 --- a/clearing-house-edc/build.gradle.kts +++ b/clearing-house-edc/build.gradle.kts @@ -8,13 +8,13 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - Initial implementation - * truzzt GmbH - EDC extension implementation + * truzzt GmbH - Initial implementation * */ plugins { `java-library` + `jacoco-report-aggregation` } val javaVersion: String by project diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 3e6db76..4993e15 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -8,14 +8,14 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - Initial implementation - * truzzt GmbH - EDC extension implementation + * truzzt GmbH - Initial implementation * */ plugins { `java-library` - `jacoco-report-aggregation` + `java-test-fixtures` + jacoco } val auth0JWTVersion: String by project @@ -27,16 +27,22 @@ dependencies { implementation(edc.ids.jsonld.serdes) implementation(edc.api.management.config) implementation(libs.jersey.multipart) - implementation("com.auth0:java-jwt:${auth0JWTVersion}") testImplementation(libs.junit.jupiter.api) testImplementation(libs.mockito.inline) testImplementation(libs.mockito.inline) + testFixturesImplementation(edc.ids) + testFixturesImplementation("com.auth0:java-jwt:${auth0JWTVersion}") + testRuntimeOnly(libs.junit.jupiter.engine) } tasks.test { useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} +tasks.jacocoTestReport { + dependsOn(tasks.test) } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java index 254dfef..370002f 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtil.java @@ -37,9 +37,9 @@ public T parse(InputStream inputStream, Class type) { } } - public byte[] toJson(Object object) { + public String toJson(Object object) { try { - return mapper.writeValueAsBytes(object); + return mapper.writeValueAsString(object); } catch (JsonProcessingException e) { throw new EdcException("Error converting to JSON", e); } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java index 61b2247..f6a77ca 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/RejectionMessage.java @@ -25,6 +25,8 @@ public class RejectionMessage extends Message { @NotNull RejectionReason rejectionReason; + public RejectionMessage() { + } public RejectionMessage(@NotNull URI id) { super(id); } diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java index de09a5b..0403b3b 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/util/ResponseUtil.java @@ -138,6 +138,16 @@ private static RejectionMessage createRejectionMessage(@Nullable Message correla return rejectionMessage; } + @NotNull + public static RejectionMessage createRejectionMessage(@NotNull RejectionReason reason, + @Nullable Message correlationMessage, + @NotNull IdsId connectorId) { + RejectionMessage rejectionMessage = createRejectionMessage(correlationMessage, connectorId); + rejectionMessage.setRejectionReason(reason); + + return rejectionMessage; + } + private static URI getMessageId() { return IdsId.Builder.newInstance().value(UUID.randomUUID().toString()).type(IdsType.MESSAGE).build().toUri(); } diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java index b45b2ab..f311750 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java @@ -54,7 +54,7 @@ public void setUp() { @Test public void sendSuccessful() throws IOException { - doReturn(TestUtils.getValidHandlerRequest(mapper).toString().getBytes()) + doReturn(TestUtils.getValidHandlerRequest(mapper).toString()) .when(typeManagerUtil).toJson(any(Object.class)); doReturn(TestUtils.getValidResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) .when(httpClient).execute(any(Request.class)); @@ -69,7 +69,7 @@ public void sendSuccessful() throws IOException { @Test public void sendWithHttpRequestError() throws IOException { - doReturn(TestUtils.getValidHandlerRequest(mapper).toString().getBytes()) + doReturn(TestUtils.getValidHandlerRequest(mapper).toString()) .when(typeManagerUtil).toJson(any(Object.class)); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> @@ -81,7 +81,7 @@ public void sendWithHttpRequestError() throws IOException { @Test public void sendWithUnsuccessfulResponseBodyError() throws IOException { - doReturn(TestUtils.getValidHandlerRequest(mapper).toString().getBytes()) + doReturn(TestUtils.getValidHandlerRequest(mapper).toString()) .when(typeManagerUtil).toJson(any(Object.class)); doReturn(TestUtils.getUnsuccessfulResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) .when(httpClient).execute(any(Request.class)); @@ -97,7 +97,7 @@ public void sendWithUnsuccessfulResponseBodyError() throws IOException { @Test public void sendWithNullResponseBodyError() throws IOException { - doReturn(TestUtils.getValidHandlerRequest(mapper).toString().getBytes()) + doReturn(TestUtils.getValidHandlerRequest(mapper).toString()) .when(typeManagerUtil).toJson(any(Object.class)); doReturn(TestUtils.getResponseWithoutBody(TestUtils.getValidAppSenderRequest(mapper).getUrl())) .when(httpClient).execute(any(Request.class)); diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index 1e3fe4d..f531a35 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -86,7 +86,7 @@ public void successfulHandleRequest(){ .when(logMessageHandler).buildJWTToken(any(SecurityToken.class), any(ServiceExtensionContext.class)); doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); - doReturn(APP_BASE_URL_DEFAULT_VALUE+"/messages/log/" + request.getPid()) + doReturn(APP_BASE_URL_DEFAULT_VALUE+ "/headers/log/" + request.getPid()) .when(senderDelegate) .buildRequestUrl(any(String.class), any(HandlerRequest.class)); doReturn(TestUtils.getValidLoggingMessageRequest(request)) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java index b886b25..182e686 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java @@ -52,11 +52,7 @@ void typeErrorParse() { new FileInputStream(TestUtils.getInvalidHeaderFile()), Message.class) ); - assertEquals( - "Error parsing to type class de.truzzt.clearinghouse.edc.types.ids.Message", - exception.getMessage() - ); - + assertEquals("Error parsing to type class de.truzzt.clearinghouse.edc.types.ids.Message", exception.getMessage()); } @Test @@ -65,14 +61,13 @@ void successfulToJson() throws IOException { typeManagerUtil = new TypeManagerUtil(objectMapper); Message msgBefore = objectMapper.readValue(TestUtils.getValidHeaderFile(), Message.class); - byte[] json = typeManagerUtil.toJson(msgBefore); + var json = typeManagerUtil.toJson(msgBefore); assertNotNull(json); - InputStream is = new ByteArrayInputStream(json); + InputStream is = new ByteArrayInputStream(json.getBytes()); Message msgAfter = typeManagerUtil.parse(is, Message.class); assertEquals(msgBefore.getType(), msgAfter.getType()); - } @Test @@ -81,7 +76,7 @@ void errorConvertingToJson() throws IOException { EdcException exception = assertThrows(EdcException.class, - () -> typeManagerUtil.toJson("") + () -> typeManagerUtil.toJson("fadsfsdafd") ); assertEquals("Error converting to JSON",exception.getMessage() ); diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java b/clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java similarity index 63% rename from clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java rename to clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java index 76ad8fb..735837b 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java +++ b/clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java @@ -2,13 +2,10 @@ import com.auth0.jwt.JWT; import com.fasterxml.jackson.databind.ObjectMapper; -import de.truzzt.clearinghouse.edc.app.AppSender; import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.LoggingMessageRequest; import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; -import de.truzzt.clearinghouse.edc.handler.LogMessageHandler; -import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.clearinghouse.Context; import de.truzzt.clearinghouse.edc.types.clearinghouse.Header; import de.truzzt.clearinghouse.edc.types.clearinghouse.SecurityToken; @@ -20,10 +17,7 @@ import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.monitor.Monitor; -import org.eclipse.edc.spi.system.ServiceExtensionContext; import java.io.File; import java.io.IOException; @@ -36,11 +30,17 @@ public class TestUtils { public static final String TEST_BASE_URL = "http://localhost:8000"; + private static final String TEST_PAYLOAD = "Hello World"; - private static final String VALID_HEADER_JSON = "messages/valid-header.json"; - private static final String INVALID_LOG_MESSAGE_HEADER_JSON = "messages/invalid-log-message-header.json"; + public static final String VALID_HEADER_JSON = "headers/valid-header.json"; + public static final String INVALID_HEADER_JSON = "headers/invalid-header.json"; + public static final String INVALID_TYPE_HEADER_JSON = "headers/invalid-type.json"; + public static final String INVALID_TOKEN_HEADER_JSON = "headers/invalid-token.json"; + public static final String MISSING_FIELDS_HEADER_JSON = "headers/missing-fields.json"; + public static final String MISSING_TOKEN_HEADER_JSON = "headers/missing-token.json"; + public static final String VALID_RESPONSE_JSON = "headers/valid-response.json"; - private static T readJsonFile(ObjectMapper mapper, Class type, String path) { + private static T parseFile(ObjectMapper mapper, Class type, String path) { ClassLoader classLoader = TestUtils.class.getClassLoader(); var jsonResource = classLoader.getResource(path); @@ -73,7 +73,7 @@ private static T readJsonFile(ObjectMapper mapper, Class type, String pat return object; } - private static File returnJonFile(String path) { + private static Path getFile(String path) { ClassLoader classLoader = TestUtils.class.getClassLoader(); var jsonResource = classLoader.getResource(path); @@ -94,35 +94,33 @@ private static File returnJonFile(String path) { throw new EdcException("Header json file not found: " + path); } - return filePath.toFile(); + return filePath; + } + + public static String readFile(String path) { + var file = getFile(path); + + try { + return Files.readString(file); + } catch (IOException e) { + throw new EdcException("Error reading file contents", e); + } } public static Message getValidHeader(ObjectMapper mapper) { - return readJsonFile(mapper, Message.class, VALID_HEADER_JSON); + return parseFile(mapper, Message.class, VALID_HEADER_JSON); } public static Message getInvalidTokenHeader(ObjectMapper mapper) { - - Message message = readJsonFile(mapper, Message.class, VALID_HEADER_JSON); - - message.getSecurityToken().setTokenValue("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzY29wZXMiOlsiaWRzYz" + - "pJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjo" + - "iaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSw" + - "ianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWx" + - "lIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLml" + - "kcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHB" + - "zOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc" + - "0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCJ9.hekZoP" + - "DjEWaXreQl3l0PUIjBOPQhAl0w2mH4_PdNWuA"); - return message; + return parseFile(mapper, Message.class, INVALID_TOKEN_HEADER_JSON); } public static Message getNotLogMessageValidHeader(ObjectMapper mapper) { + return parseFile(mapper, Message.class, INVALID_TYPE_HEADER_JSON); + } - Message message = readJsonFile(mapper, Message.class, VALID_HEADER_JSON); - - message.setType("ids:otherMessage"); - return message; + public static Message getValidResponseHeader(ObjectMapper mapper) { + return parseFile(mapper, Message.class, VALID_RESPONSE_JSON); } public static Response getValidResponse(String url) { @@ -172,40 +170,39 @@ public static LoggingMessageResponse getValidLoggingMessageResponse(String url) return mapper.readValue(getValidResponse(url).body().byteStream(), LoggingMessageResponse.class); - } catch (IOException ioe) { - ioe.printStackTrace(); - return null; + } catch (IOException e) { + throw new EdcException("Error parsing response", e); } } public static LoggingMessageRequest getValidLoggingMessageRequest(HandlerRequest handlerRequest) { - var header = handlerRequest.getHeader(); - - var multipartContext = header.getContext(); - var context = new Context(multipartContext.getIds(), multipartContext.getIdsc()); - - var multipartSecurityToken = header.getSecurityToken(); - var multipartTokenFormat = multipartSecurityToken.getTokenFormat(); - var securityToken = SecurityToken.Builder.newInstance(). - type(multipartSecurityToken.getType()). - id(multipartSecurityToken.getId()). - tokenFormat(new TokenFormat(multipartTokenFormat.getId())). - tokenValue(multipartSecurityToken.getTokenValue()). - build(); - - var requestHeader = Header.Builder.newInstance() - .context(context) - .id(header.getId()) - .type(header.getType()) - .securityToken(securityToken) - .issuerConnector(header.getIssuerConnector()) - .modelVersion(header.getModelVersion()) - .issued(header.getIssued()) - .senderAgent(header.getSenderAgent()) - .build(); - - return new LoggingMessageRequest(requestHeader, handlerRequest.getPayload()); + var header = handlerRequest.getHeader(); + + var multipartContext = header.getContext(); + var context = new Context(multipartContext.getIds(), multipartContext.getIdsc()); + + var multipartSecurityToken = header.getSecurityToken(); + var multipartTokenFormat = multipartSecurityToken.getTokenFormat(); + var securityToken = SecurityToken.Builder.newInstance(). + type(multipartSecurityToken.getType()). + id(multipartSecurityToken.getId()). + tokenFormat(new TokenFormat(multipartTokenFormat.getId())). + tokenValue(multipartSecurityToken.getTokenValue()). + build(); + + var requestHeader = Header.Builder.newInstance() + .context(context) + .id(header.getId()) + .type(header.getType()) + .securityToken(securityToken) + .issuerConnector(header.getIssuerConnector()) + .modelVersion(header.getModelVersion()) + .issued(header.getIssued()) + .senderAgent(header.getSenderAgent()) + .build(); + + return new LoggingMessageRequest(requestHeader, handlerRequest.getPayload()); } public static ResponseBody getValidResponseBody(){ @@ -237,28 +234,23 @@ public static HandlerRequest getInvalidHandlerRequest(ObjectMapper mapper){ } public static AppSenderRequest getValidAppSenderRequest(ObjectMapper mapper){ - return new AppSenderRequest(TEST_BASE_URL+"/messages/log/" + UUID.randomUUID(), + return new AppSenderRequest(TEST_BASE_URL+ "/headers/log/" + UUID.randomUUID(), JWT.create().toString(), getValidHandlerRequest(mapper) ); } public static AppSenderRequest getInvalidUrlAppSenderRequest(ObjectMapper mapper){ - return new AppSenderRequest("" + UUID.randomUUID(), + return new AppSenderRequest(UUID.randomUUID().toString(), JWT.create().toString(), getValidHandlerRequest(mapper) ); } public static File getValidHeaderFile() { - - return returnJonFile(VALID_HEADER_JSON); + return getFile(VALID_HEADER_JSON).toFile(); } - public static File getInvalidHeaderFile() { - - return returnJonFile(INVALID_LOG_MESSAGE_HEADER_JSON); + return getFile(INVALID_HEADER_JSON).toFile(); } - - } diff --git a/clearing-house-edc/core/src/test/resources/messages/invalid-log-message-header.json b/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-header.json similarity index 100% rename from clearing-house-edc/core/src/test/resources/messages/invalid-log-message-header.json rename to clearing-house-edc/core/src/testFixtures/resources/headers/invalid-header.json diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-token.json b/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-token.json new file mode 100644 index 0000000..ea2bbf7 --- /dev/null +++ b/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-token.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:LogMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCJ9.hekZoPDjEWaXreQl3l0PUIjBOPQhAl0w2mH4_PdNWuA", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-type.json b/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-type.json new file mode 100644 index 0000000..e96fc8d --- /dev/null +++ b/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-type.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:otherMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/missing-fields.json b/clearing-house-edc/core/src/testFixtures/resources/headers/missing-fields.json new file mode 100644 index 0000000..d1ef397 --- /dev/null +++ b/clearing-house-edc/core/src/testFixtures/resources/headers/missing-fields.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:LogMessage", + "@id":"", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/missing-token.json b/clearing-house-edc/core/src/testFixtures/resources/headers/missing-token.json new file mode 100644 index 0000000..8c609c9 --- /dev/null +++ b/clearing-house-edc/core/src/testFixtures/resources/headers/missing-token.json @@ -0,0 +1,12 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:LogMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/resources/messages/valid-header.json b/clearing-house-edc/core/src/testFixtures/resources/headers/valid-header.json similarity index 100% rename from clearing-house-edc/core/src/test/resources/messages/valid-header.json rename to clearing-house-edc/core/src/testFixtures/resources/headers/valid-header.json diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/valid-response.json b/clearing-house-edc/core/src/testFixtures/resources/headers/valid-response.json new file mode 100644 index 0000000..85057bb --- /dev/null +++ b/clearing-house-edc/core/src/testFixtures/resources/headers/valid-response.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:LogMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index 86d5238..d525360 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -8,14 +8,14 @@ * SPDX-License-Identifier: Apache-2.0 * * Contributors: - * Microsoft Corporation - Initial implementation - * truzzt GmbH - EDC extension implementation + * truzzt GmbH - Initial implementation * */ plugins { `java-library` - `jacoco-report-aggregation` + `java-test-fixtures` + jacoco } dependencies { @@ -33,9 +33,15 @@ dependencies { testImplementation(libs.mockito.inline) testImplementation(libs.mockito.inline) + testImplementation(testFixtures(project(":core"))) + testRuntimeOnly(libs.junit.jupiter.engine) } tasks.test { useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) +} +tasks.jacocoTestReport { + dependsOn(tasks.test) } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index 67e829b..0fdf8a9 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -21,6 +21,7 @@ import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.Message; +import de.truzzt.clearinghouse.edc.types.ids.RejectionMessage; import de.truzzt.clearinghouse.edc.types.ids.TokenFormat; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; @@ -33,6 +34,7 @@ import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.util.string.StringUtils; import org.glassfish.jersey.media.multipart.FormDataParam; import org.jetbrains.annotations.NotNull; @@ -40,6 +42,7 @@ import java.util.List; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.createFormDataMultiPart; +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.createRejectionMessage; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.internalRecipientError; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.malformedMessage; import static de.truzzt.clearinghouse.edc.util.ResponseUtil.messageTypeNotSupported; @@ -83,6 +86,14 @@ public Response request(@PathParam(PID) String pid, @FormDataParam(HEADER) InputStream headerInputStream, @FormDataParam(PAYLOAD) String payload) { + // Check if pid is missing + if (pid == null) { + monitor.severe(LOG_ID + ": PID is missing"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) + .build(); + } + // Check if header is missing if (headerInputStream == null) { monitor.severe(LOG_ID + ": Header is missing"); @@ -104,11 +115,15 @@ public Response request(@PathParam(PID) String pid, // Check if any required header field missing if (header.getId() == null - || header.getType() == null - || header.getModelVersion() == null + || (header.getId() != null && StringUtils.isNullOrBlank(header.getId().toString())) + || StringUtils.isNullOrBlank(header.getType()) + || StringUtils.isNullOrBlank(header.getModelVersion()) || header.getIssued() == null || header.getIssuerConnector() == null - || header.getSenderAgent() == null) { + || (header.getIssuerConnector() != null && StringUtils.isNullOrBlank(header.getIssuerConnector().toString())) + || header.getSenderAgent() == null + || (header.getSenderAgent() != null && StringUtils.isNullOrBlank(header.getSenderAgent().toString())) + ) { return Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(header, connectorId))) .build(); @@ -179,9 +194,19 @@ public Response request(@PathParam(PID) String pid, } // Build the response - return Response.status(Response.Status.CREATED) - .entity(createFormDataMultiPart(typeManagerUtil, HEADER, handlerResponse.getHeader(), PAYLOAD, handlerResponse.getPayload())) - .build(); + if (handlerResponse.getHeader() instanceof RejectionMessage) { + var rejectionMessage = (RejectionMessage) handlerResponse.getHeader(); + + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, + createRejectionMessage(rejectionMessage.getRejectionReason(), header, connectorId)) + ).build(); + } + else { + return Response.status(Response.Status.CREATED) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, handlerResponse.getHeader(), PAYLOAD, handlerResponse.getPayload())) + .build(); + } } private boolean validateToken(Message header) { diff --git a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java index 8d19233..eaeccad 100644 --- a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java +++ b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java @@ -1,62 +1,258 @@ package de.truzzt.clearinghouse.edc.multipart; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.fraunhofer.iais.eis.DynamicAttributeToken; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerResponse; import de.truzzt.clearinghouse.edc.handler.Handler; import de.truzzt.clearinghouse.edc.handler.LogMessageHandler; +import de.truzzt.clearinghouse.edc.tests.TestUtils; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.Message; import de.truzzt.clearinghouse.edc.types.ids.RejectionMessage; +import de.truzzt.clearinghouse.edc.types.ids.RejectionReason; import jakarta.ws.rs.core.Response; import org.eclipse.edc.protocol.ids.spi.service.DynamicAttributeTokenService; import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.protocol.ids.spi.types.IdsType; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; import org.glassfish.jersey.media.multipart.FormDataMultiPart; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; -import java.io.ByteArrayInputStream; +import java.io.*; +import java.net.URI; import java.util.List; import java.util.UUID; -import static org.mockito.Mockito.mock; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; public class MultipartControllerTest { - private static final String IDS_WEBHOOK_ADDRESSS = "http://localhost/callback"; - private static final String TEST_PAYLOAD = "Hello World"; + private static final String IDS_WEBHOOK_ADDRESS = "http://localhost/callback"; + private static final String REQUEST_PAYLOAD = "Hello World"; + private static final String RESPONSE_PAYLOAD = ""; private MultipartController controller; - private Monitor monitor; + private IdsId connectorId; private TypeManagerUtil typeManagerUtil; + + @Mock + private Monitor monitor; + @Mock private DynamicAttributeTokenService tokenService; - private LogMessageHandler logMessageHandler = mock(LogMessageHandler.class); + @Mock + private LogMessageHandler logMessageHandler; + + private ObjectMapper mapper = new ObjectMapper(); @BeforeEach public void setUp() { - monitor = mock(Monitor.class); - connectorId = mock(IdsId.class); - typeManagerUtil = mock(TypeManagerUtil.class); - tokenService = mock(DynamicAttributeTokenService.class); - logMessageHandler = mock(LogMessageHandler.class); + MockitoAnnotations.openMocks(this); + + connectorId = IdsId.Builder.newInstance().type(IdsType.CONNECTOR).value("http://test.connector").build(); + typeManagerUtil = new TypeManagerUtil(new ObjectMapper()); List multipartHandlers = List.of(logMessageHandler); - controller = new MultipartController(monitor, connectorId, typeManagerUtil, tokenService, IDS_WEBHOOK_ADDRESSS, multipartHandlers); + controller = new MultipartController(monitor, connectorId, typeManagerUtil, tokenService, IDS_WEBHOOK_ADDRESS, multipartHandlers); + } + + private T extractHeader(Response response, Class type) { + + assertInstanceOf(FormDataMultiPart.class, response.getEntity()); + FormDataMultiPart multiPartResponse = (FormDataMultiPart) response.getEntity(); + + var header = multiPartResponse.getField("header"); + assertNotNull(header); + + assertInstanceOf(String.class, header.getEntity()); + var entity = (String) header.getEntity(); + return typeManagerUtil.parse(new ByteArrayInputStream(entity.getBytes()), type); + } + + private String extractPayload(Response response) { + + assertInstanceOf(FormDataMultiPart.class, response.getEntity()); + FormDataMultiPart multiPartResponse = (FormDataMultiPart) response.getEntity(); + + var payload = multiPartResponse.getField("payload"); + assertNotNull(payload); + + assertInstanceOf(String.class, payload.getEntity()); + return (String) payload.getEntity(); + } + + private InputStream getInputStream(String path) { + var json = TestUtils.readFile(path); + return new ByteArrayInputStream(json.getBytes()); + } + + @Test + public void success() { + var responseHeader = TestUtils.getValidResponseHeader(mapper); + + doReturn(Result.success()) + .when(tokenService).verifyDynamicAttributeToken(any(DynamicAttributeToken.class), any(URI.class), any(String.class)); + doReturn(true) + .when(logMessageHandler).canHandle(any(HandlerRequest.class)); + doReturn(HandlerResponse.Builder.newInstance().header(responseHeader).payload(REQUEST_PAYLOAD).build()) + .when(logMessageHandler).handleRequest(any(HandlerRequest.class)); + + var pid = UUID.randomUUID().toString(); + var header = getInputStream(TestUtils.VALID_HEADER_JSON); + + var response = controller.request(pid, header, REQUEST_PAYLOAD); + + assertNotNull(response); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, Message.class); + assertEquals("ids:LogMessage", message.getType()); + + var payload = extractPayload(response); + assertNotNull(payload); + } + + @Test + public void missingPIDError() { + var header = getInputStream(TestUtils.VALID_HEADER_JSON); + + var response = controller.request(null, header, REQUEST_PAYLOAD); + + assertNotNull(response); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, RejectionMessage.class); + + assertNotNull(message.getRejectionReason()); + assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); } @Test public void missingHeaderError() { var pid = UUID.randomUUID().toString(); - var response = controller.request(pid, null, TEST_PAYLOAD); + var response = controller.request(pid, null, REQUEST_PAYLOAD); + assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); - assertInstanceOf(FormDataMultiPart.class, response.getEntity()); - FormDataMultiPart multiPartResponse = (FormDataMultiPart) response.getEntity(); - // TODO Find a way to get the FormDataMultiPart header value + var message = extractHeader(response, RejectionMessage.class); + + assertNotNull(message.getRejectionReason()); + assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); + } + + @Test + public void invalidHeaderError() { + var pid = UUID.randomUUID().toString(); + var header = getInputStream(TestUtils.INVALID_HEADER_JSON); + + var response = controller.request(pid, header, REQUEST_PAYLOAD); + + assertNotNull(response); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, RejectionMessage.class); + + assertNotNull(message.getRejectionReason()); + assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); + } + + @Test + public void missingHeaderFieldsError() { + var pid = UUID.randomUUID().toString(); + var header = getInputStream(TestUtils.MISSING_FIELDS_HEADER_JSON); + + var response = controller.request(pid, header, REQUEST_PAYLOAD); + + assertNotNull(response); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, RejectionMessage.class); + + assertNotNull(message.getRejectionReason()); + assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); + } + + @Test + public void invalidSecurityTokenError() { + doReturn(Result.failure("Invalid token")) + .when(tokenService).verifyDynamicAttributeToken(any(DynamicAttributeToken.class), any(URI.class), any(String.class)); + + var pid = UUID.randomUUID().toString(); + var header = getInputStream(TestUtils.INVALID_TOKEN_HEADER_JSON); + + var response = controller.request(pid, header, REQUEST_PAYLOAD); + + assertNotNull(response); + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, RejectionMessage.class); + + assertNotNull(message.getRejectionReason()); + assertEquals(RejectionReason.NOT_AUTHENTICATED.getId(), message.getRejectionReason().getId()); + } + + @Test + public void missingSecurityTokenError() { + var pid = UUID.randomUUID().toString(); + var header = getInputStream(TestUtils.MISSING_TOKEN_HEADER_JSON); + + var response = controller.request(pid, header, REQUEST_PAYLOAD); + + assertNotNull(response); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, RejectionMessage.class); + + assertNotNull(message.getRejectionReason()); + assertEquals(RejectionReason.NOT_AUTHENTICATED.getId(), message.getRejectionReason().getId()); + } + + @Test + public void missingPayloadError() { + var pid = UUID.randomUUID().toString(); + var header = getInputStream(TestUtils.VALID_HEADER_JSON); + + var response = controller.request(pid, header, null); + + assertNotNull(response); + assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, RejectionMessage.class); + + assertNotNull(message.getRejectionReason()); + assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); + } + + @Test + public void invalidMessageTypeError() { + doReturn(Result.success()) + .when(tokenService).verifyDynamicAttributeToken(any(DynamicAttributeToken.class), any(URI.class), any(String.class)); + doReturn(false) + .when(logMessageHandler).canHandle(any(HandlerRequest.class)); + + var pid = UUID.randomUUID().toString(); + var header = getInputStream(TestUtils.INVALID_TYPE_HEADER_JSON); + + var response = controller.request(pid, header, REQUEST_PAYLOAD); + + assertNotNull(response); + assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, RejectionMessage.class); + + assertNotNull(message.getRejectionReason()); + assertEquals(RejectionReason.MESSAGE_TYPE_NOT_SUPPORTED.getId(), message.getRejectionReason().getId()); } } From 660d8efc646ca934efd1895823d808ecce90cc10 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Thu, 19 Oct 2023 16:13:35 -0300 Subject: [PATCH 122/183] feat (ch-edc): TypeManagerUtilTest fixed --- .../de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java index 182e686..2fc4de2 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java @@ -72,7 +72,7 @@ void successfulToJson() throws IOException { @Test void errorConvertingToJson() throws IOException { - doThrow(new EdcException("Error converting to JSON")).when(objectMapper).writeValueAsBytes(anyString()); + doThrow(new EdcException("Error converting to JSON")).when(objectMapper).writeValueAsString(anyString()); EdcException exception = assertThrows(EdcException.class, From 2138f33f1ae7010d66ea25795f408e74bd6ff62c Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 19 Oct 2023 19:54:03 -0300 Subject: [PATCH 123/183] feat (ch-edc): unit tests review --- clearing-house-edc/build.gradle.kts | 3 +- clearing-house-edc/core/build.gradle.kts | 1 - .../clearinghouse/edc/app/AppSenderTest.java | 15 +-- .../delegate/LoggingMessageDelegateTest.java | 4 +- .../edc/handler/LogMessageHandlerTest.java | 9 +- .../clearinghouse/edc/tests/TestUtils.java | 103 ++---------------- .../edc/types/TypeManagerUtilTest.java | 22 ++-- .../edc/util/ResponseUtilTest.java | 23 ++-- .../resources/headers/invalid-header.json | 0 .../resources/headers/invalid-token.json | 0 .../resources/headers/invalid-type.json | 0 .../resources/headers/valid-header.json | 0 .../edc/tests/BaseTestUtils.java | 80 ++++++++++++++ .../extensions/multipart/build.gradle.kts | 1 - .../multipart/MultipartControllerTest.java | 57 +++++----- .../edc/multipart/tests/TestUtils.java | 35 ++++++ .../resources/headers/invalid-header.json | 19 ++++ .../test/resources/headers/invalid-token.json | 20 ++++ .../test/resources/headers/invalid-type.json | 20 ++++ .../resources/headers/missing-fields.json | 0 .../resources/headers/missing-token.json | 0 .../test/resources/headers/valid-header.json} | 0 .../resources/headers/valid-response.json | 20 ++++ .../resources/payloads/valid-response.json | 3 + 24 files changed, 264 insertions(+), 171 deletions(-) rename clearing-house-edc/core/src/{testFixtures => test}/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java (65%) rename clearing-house-edc/core/src/{testFixtures => test}/resources/headers/invalid-header.json (100%) rename clearing-house-edc/core/src/{testFixtures => test}/resources/headers/invalid-token.json (100%) rename clearing-house-edc/core/src/{testFixtures => test}/resources/headers/invalid-type.json (100%) rename clearing-house-edc/core/src/{testFixtures => test}/resources/headers/valid-header.json (100%) create mode 100644 clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/BaseTestUtils.java create mode 100644 clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/tests/TestUtils.java create mode 100644 clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-header.json create mode 100644 clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-token.json create mode 100644 clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-type.json rename clearing-house-edc/{core/src/testFixtures => extensions/multipart/src/test}/resources/headers/missing-fields.json (100%) rename clearing-house-edc/{core/src/testFixtures => extensions/multipart/src/test}/resources/headers/missing-token.json (100%) rename clearing-house-edc/{core/src/testFixtures/resources/headers/valid-response.json => extensions/multipart/src/test/resources/headers/valid-header.json} (100%) create mode 100644 clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-response.json create mode 100644 clearing-house-edc/extensions/multipart/src/test/resources/payloads/valid-response.json diff --git a/clearing-house-edc/build.gradle.kts b/clearing-house-edc/build.gradle.kts index 8759e5f..589224c 100644 --- a/clearing-house-edc/build.gradle.kts +++ b/clearing-house-edc/build.gradle.kts @@ -13,8 +13,7 @@ */ plugins { - `java-library` - `jacoco-report-aggregation` + `java-base` } val javaVersion: String by project diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 4993e15..57ffa61 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -41,7 +41,6 @@ dependencies { tasks.test { useJUnitPlatform() - finalizedBy(tasks.jacocoTestReport) } tasks.jacocoTestReport { dependsOn(tasks.test) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java index f311750..d25e793 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/AppSenderTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import de.truzzt.clearinghouse.edc.tests.TestUtils; import de.truzzt.clearinghouse.edc.app.delegate.LoggingMessageDelegate; -import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import okhttp3.Request; import okhttp3.ResponseBody; @@ -26,8 +25,6 @@ public class AppSenderTest { - - private AppSender sender; @Mock private Monitor monitor; @@ -36,13 +33,9 @@ public class AppSenderTest { @Mock private LoggingMessageDelegate senderDelegate; @Mock - private ObjectMapper objectMapper; - @Mock - private AppSenderRequest appSenderRequest; - @Mock private EdcHttpClient httpClient; - private ObjectMapper mapper = new ObjectMapper(); + private final ObjectMapper mapper = new ObjectMapper(); @BeforeEach public void setUp() { @@ -58,7 +51,7 @@ public void sendSuccessful() throws IOException { .when(typeManagerUtil).toJson(any(Object.class)); doReturn(TestUtils.getValidResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) .when(httpClient).execute(any(Request.class)); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl(), mapper)) .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); var response = sender.send(TestUtils.getValidAppSenderRequest(mapper), senderDelegate); @@ -85,7 +78,7 @@ public void sendWithUnsuccessfulResponseBodyError() throws IOException { .when(typeManagerUtil).toJson(any(Object.class)); doReturn(TestUtils.getUnsuccessfulResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) .when(httpClient).execute(any(Request.class)); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl(), mapper)) .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); EdcException exception = assertThrows(EdcException.class, () -> @@ -101,7 +94,7 @@ public void sendWithNullResponseBodyError() throws IOException { .when(typeManagerUtil).toJson(any(Object.class)); doReturn(TestUtils.getResponseWithoutBody(TestUtils.getValidAppSenderRequest(mapper).getUrl())) .when(httpClient).execute(any(Request.class)); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl(), mapper)) .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); EdcException exception = assertThrows(EdcException.class, () -> diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java index a6dd414..9e92711 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/app/delegate/LoggingMessageDelegateTest.java @@ -25,7 +25,7 @@ class LoggingMessageDelegateTest { @Mock private LoggingMessageDelegate senderDelegate; - private ObjectMapper mapper = new ObjectMapper(); + private final ObjectMapper mapper = new ObjectMapper(); @BeforeEach public void setUp() { @@ -58,7 +58,7 @@ public void successfulBuildRequestBody() { public void successfulParseResponseBody() { ResponseBody body = TestUtils.getValidResponseBody(); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl(), mapper)) .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); LoggingMessageResponse response = senderDelegate.parseResponseBody(body); diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index f531a35..f72e158 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -42,19 +42,14 @@ class LogMessageHandlerTest { private LogMessageHandler logMessageHandler; @Mock private LoggingMessageDelegate senderDelegate; - @Mock - private EdcHttpClient httpClient; - - private ObjectMapper mapper = new ObjectMapper(); - private AppSender sender; + private final ObjectMapper mapper = new ObjectMapper(); @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); senderDelegate = spy(new LoggingMessageDelegate(typeManagerUtil)); logMessageHandler = spy(new LogMessageHandler(monitor, connectorId, typeManagerUtil, appSender, context)); - sender = new AppSender(monitor, httpClient ,typeManagerUtil); } @Test @@ -84,7 +79,7 @@ public void successfulHandleRequest(){ HandlerRequest request = TestUtils.getValidHandlerRequest(mapper); doReturn(JWT.create().toString()) .when(logMessageHandler).buildJWTToken(any(SecurityToken.class), any(ServiceExtensionContext.class)); - doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl())) + doReturn(TestUtils.getValidLoggingMessageResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl(), mapper)) .when(senderDelegate).parseResponseBody(any(ResponseBody.class)); doReturn(APP_BASE_URL_DEFAULT_VALUE+ "/headers/log/" + request.getPid()) .when(senderDelegate) diff --git a/clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java similarity index 65% rename from clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java rename to clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java index 735837b..f1a4458 100644 --- a/clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java @@ -11,101 +11,22 @@ import de.truzzt.clearinghouse.edc.types.clearinghouse.SecurityToken; import de.truzzt.clearinghouse.edc.types.clearinghouse.TokenFormat; import de.truzzt.clearinghouse.edc.types.ids.Message; -import okhttp3.Headers; -import okhttp3.MediaType; -import okhttp3.Protocol; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; +import okhttp3.*; import org.eclipse.edc.spi.EdcException; import java.io.File; import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.UUID; -public class TestUtils { +public class TestUtils extends BaseTestUtils { public static final String TEST_BASE_URL = "http://localhost:8000"; private static final String TEST_PAYLOAD = "Hello World"; - public static final String VALID_HEADER_JSON = "headers/valid-header.json"; - public static final String INVALID_HEADER_JSON = "headers/invalid-header.json"; - public static final String INVALID_TYPE_HEADER_JSON = "headers/invalid-type.json"; - public static final String INVALID_TOKEN_HEADER_JSON = "headers/invalid-token.json"; - public static final String MISSING_FIELDS_HEADER_JSON = "headers/missing-fields.json"; - public static final String MISSING_TOKEN_HEADER_JSON = "headers/missing-token.json"; - public static final String VALID_RESPONSE_JSON = "headers/valid-response.json"; - - private static T parseFile(ObjectMapper mapper, Class type, String path) { - - ClassLoader classLoader = TestUtils.class.getClassLoader(); - var jsonResource = classLoader.getResource(path); - - if (jsonResource == null) { - throw new EdcException("Header json file not found: " + path); - } - - URI jsonUrl; - try { - jsonUrl = jsonResource.toURI(); - } catch (URISyntaxException e) { - throw new EdcException("Error finding json file on classpath", e); - } - - Path filePath = Path.of(jsonUrl); - if (!Files.exists(filePath)) { - throw new EdcException("Header json file not found: " + path); - } - - T object = null; - try { - var jsonContents = Files.readAllBytes(filePath); - object = mapper.readValue(jsonContents, type); - - } catch (IOException e){ - throw new EdcException("Error parsing json file", e); - } - - return object; - } - - private static Path getFile(String path) { - - ClassLoader classLoader = TestUtils.class.getClassLoader(); - var jsonResource = classLoader.getResource(path); - - if (jsonResource == null) { - throw new EdcException("Header json file not found: " + path); - } - - URI jsonUrl; - try { - jsonUrl = jsonResource.toURI(); - } catch (URISyntaxException e) { - throw new EdcException("Error finding json file on classpath", e); - } - - Path filePath = Path.of(jsonUrl); - if (!Files.exists(filePath)) { - throw new EdcException("Header json file not found: " + path); - } - - return filePath; - } - - public static String readFile(String path) { - var file = getFile(path); - - try { - return Files.readString(file); - } catch (IOException e) { - throw new EdcException("Error reading file contents", e); - } - } + private static final String VALID_HEADER_JSON = "headers/valid-header.json"; + private static final String INVALID_HEADER_JSON = "headers/invalid-header.json"; + private static final String INVALID_TYPE_HEADER_JSON = "headers/invalid-type.json"; + private static final String INVALID_TOKEN_HEADER_JSON = "headers/invalid-token.json"; public static Message getValidHeader(ObjectMapper mapper) { return parseFile(mapper, Message.class, VALID_HEADER_JSON); @@ -115,14 +36,10 @@ public static Message getInvalidTokenHeader(ObjectMapper mapper) { return parseFile(mapper, Message.class, INVALID_TOKEN_HEADER_JSON); } - public static Message getNotLogMessageValidHeader(ObjectMapper mapper) { + public static Message getInvalidTypeHeader(ObjectMapper mapper) { return parseFile(mapper, Message.class, INVALID_TYPE_HEADER_JSON); } - public static Message getValidResponseHeader(ObjectMapper mapper) { - return parseFile(mapper, Message.class, VALID_RESPONSE_JSON); - } - public static Response getValidResponse(String url) { Request mockRequest = new Request.Builder() @@ -164,10 +81,8 @@ public static Response getUnsuccessfulResponse(String url) { null, 1000L, 1000L, null); } - public static LoggingMessageResponse getValidLoggingMessageResponse(String url) { + public static LoggingMessageResponse getValidLoggingMessageResponse(String url, ObjectMapper mapper) { try { - ObjectMapper mapper = new ObjectMapper(); - return mapper.readValue(getValidResponse(url).body().byteStream(), LoggingMessageResponse.class); } catch (IOException e) { @@ -229,7 +144,7 @@ public static HandlerRequest getInvalidTokenHandlerRequest(ObjectMapper mapper){ public static HandlerRequest getInvalidHandlerRequest(ObjectMapper mapper){ return HandlerRequest.Builder.newInstance() .pid(UUID.randomUUID().toString()) - .header(getNotLogMessageValidHeader(mapper) ) + .header(getInvalidTypeHeader(mapper) ) .payload(TEST_PAYLOAD).build(); } diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java index 2fc4de2..7fa48ac 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/TypeManagerUtilTest.java @@ -19,23 +19,23 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; class TypeManagerUtilTest { - @Mock - private ObjectMapper objectMapper; @Mock private TypeManagerUtil typeManagerUtil; + private final ObjectMapper mapper = new ObjectMapper(); + @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - typeManagerUtil = new TypeManagerUtil(objectMapper); + typeManagerUtil = new TypeManagerUtil(mapper); } @Test - void successfulParse() throws IOException, InstantiationException, IllegalAccessException { - typeManagerUtil = new TypeManagerUtil(new ObjectMapper()); + void successfulParse() throws IOException { InputStream is = new FileInputStream(TestUtils.getValidHeaderFile()); Message msg = typeManagerUtil.parse(is, Message.class); @@ -45,7 +45,6 @@ void successfulParse() throws IOException, InstantiationException, IllegalAccess @Test void typeErrorParse() { - typeManagerUtil = new TypeManagerUtil(new ObjectMapper()); EdcException exception = assertThrows(EdcException.class, () -> typeManagerUtil.parse( @@ -57,9 +56,7 @@ void typeErrorParse() { @Test void successfulToJson() throws IOException { - objectMapper = new ObjectMapper(); - typeManagerUtil = new TypeManagerUtil(objectMapper); - Message msgBefore = objectMapper.readValue(TestUtils.getValidHeaderFile(), Message.class); + Message msgBefore = mapper.readValue(TestUtils.getValidHeaderFile(), Message.class); var json = typeManagerUtil.toJson(msgBefore); assertNotNull(json); @@ -72,7 +69,12 @@ void successfulToJson() throws IOException { @Test void errorConvertingToJson() throws IOException { - doThrow(new EdcException("Error converting to JSON")).when(objectMapper).writeValueAsString(anyString()); + + var mockedMapper = mock(ObjectMapper.class); + doThrow(new EdcException("Error converting to JSON")) + .when(mockedMapper).writeValueAsString(anyString()); + + typeManagerUtil = new TypeManagerUtil(mockedMapper); EdcException exception = assertThrows(EdcException.class, diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/util/ResponseUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/util/ResponseUtilTest.java index fce718e..8adabf0 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/util/ResponseUtilTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/util/ResponseUtilTest.java @@ -13,26 +13,23 @@ class ResponseUtilTest { - @Mock - private ResponseUtil responseUtil; @Mock private TypeManagerUtil typeManagerUtil; @Mock private IdsId connectorId; - private ObjectMapper objectMapper; + private final ObjectMapper mapper = new ObjectMapper(); @BeforeEach public void setUp() { MockitoAnnotations.openMocks(this); typeManagerUtil = new TypeManagerUtil(new ObjectMapper()); - objectMapper = new ObjectMapper(); } @Test public void createFormDataMultiPart() { - var response = responseUtil.createFormDataMultiPart(typeManagerUtil, + var response = ResponseUtil.createFormDataMultiPart(typeManagerUtil, "Header Name", TestUtils.getValidHeader(new ObjectMapper()), "Payload", @@ -44,9 +41,9 @@ public void createFormDataMultiPart() { @Test public void testCreateFormDataMultiPart() { - var response = responseUtil.createFormDataMultiPart(typeManagerUtil, + var response = ResponseUtil.createFormDataMultiPart(typeManagerUtil, "Header Name", - TestUtils.getValidHeader(objectMapper) + TestUtils.getValidHeader(mapper) ); assertNotNull(response); @@ -54,7 +51,7 @@ public void testCreateFormDataMultiPart() { @Test public void createMultipartResponse() { - var response = responseUtil.createMultipartResponse(TestUtils.getValidHeader(objectMapper), + var response = ResponseUtil.createMultipartResponse(TestUtils.getValidHeader(mapper), "Payload Value"); assertNotNull(response); @@ -62,35 +59,35 @@ public void createMultipartResponse() { @Test public void messageProcessedNotification() { - var response = responseUtil.messageProcessedNotification(TestUtils.getValidHeader(objectMapper), connectorId); + var response = ResponseUtil.messageProcessedNotification(TestUtils.getValidHeader(mapper), connectorId); assertNotNull(response); } @Test public void notAuthenticated() { - var response = responseUtil.notAuthenticated(TestUtils.getValidHeader(objectMapper), connectorId); + var response = ResponseUtil.notAuthenticated(TestUtils.getValidHeader(mapper), connectorId); assertNotNull(response); } @Test public void malformedMessage() { - var response = responseUtil.malformedMessage(TestUtils.getValidHeader(objectMapper), connectorId); + var response = ResponseUtil.malformedMessage(TestUtils.getValidHeader(mapper), connectorId); assertNotNull(response); } @Test public void messageTypeNotSupported() { - var response = responseUtil.messageTypeNotSupported(TestUtils.getValidHeader(objectMapper), connectorId); + var response = ResponseUtil.messageTypeNotSupported(TestUtils.getValidHeader(mapper), connectorId); assertNotNull(response); } @Test public void internalRecipientError() { - var response = responseUtil.internalRecipientError(TestUtils.getValidHeader(objectMapper), connectorId); + var response = ResponseUtil.internalRecipientError(TestUtils.getValidHeader(mapper), connectorId); assertNotNull(response); } diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-header.json b/clearing-house-edc/core/src/test/resources/headers/invalid-header.json similarity index 100% rename from clearing-house-edc/core/src/testFixtures/resources/headers/invalid-header.json rename to clearing-house-edc/core/src/test/resources/headers/invalid-header.json diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-token.json b/clearing-house-edc/core/src/test/resources/headers/invalid-token.json similarity index 100% rename from clearing-house-edc/core/src/testFixtures/resources/headers/invalid-token.json rename to clearing-house-edc/core/src/test/resources/headers/invalid-token.json diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/invalid-type.json b/clearing-house-edc/core/src/test/resources/headers/invalid-type.json similarity index 100% rename from clearing-house-edc/core/src/testFixtures/resources/headers/invalid-type.json rename to clearing-house-edc/core/src/test/resources/headers/invalid-type.json diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/valid-header.json b/clearing-house-edc/core/src/test/resources/headers/valid-header.json similarity index 100% rename from clearing-house-edc/core/src/testFixtures/resources/headers/valid-header.json rename to clearing-house-edc/core/src/test/resources/headers/valid-header.json diff --git a/clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/BaseTestUtils.java b/clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/BaseTestUtils.java new file mode 100644 index 0000000..d4ac055 --- /dev/null +++ b/clearing-house-edc/core/src/testFixtures/java/de/truzzt/clearinghouse/edc/tests/BaseTestUtils.java @@ -0,0 +1,80 @@ +package de.truzzt.clearinghouse.edc.tests; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.spi.EdcException; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; + +public class BaseTestUtils { + + protected static T parseFile(ObjectMapper mapper, Class type, String path) { + + ClassLoader classLoader = BaseTestUtils.class.getClassLoader(); + var jsonResource = classLoader.getResource(path); + + if (jsonResource == null) { + throw new EdcException("Header json file not found: " + path); + } + + URI jsonUrl; + try { + jsonUrl = jsonResource.toURI(); + } catch (URISyntaxException e) { + throw new EdcException("Error finding json file on classpath", e); + } + + Path filePath = Path.of(jsonUrl); + if (!Files.exists(filePath)) { + throw new EdcException("Header json file not found: " + path); + } + + T object; + try { + var jsonContents = Files.readAllBytes(filePath); + object = mapper.readValue(jsonContents, type); + + } catch (IOException e){ + throw new EdcException("Error parsing json file", e); + } + + return object; + } + + protected static Path getFile(String path) { + + ClassLoader classLoader = BaseTestUtils.class.getClassLoader(); + var jsonResource = classLoader.getResource(path); + + if (jsonResource == null) { + throw new EdcException("Header json file not found: " + path); + } + + URI jsonUrl; + try { + jsonUrl = jsonResource.toURI(); + } catch (URISyntaxException e) { + throw new EdcException("Error finding json file on classpath", e); + } + + Path filePath = Path.of(jsonUrl); + if (!Files.exists(filePath)) { + throw new EdcException("Header json file not found: " + path); + } + + return filePath; + } + + protected static String readFile(String path) { + var file = getFile(path); + + try { + return Files.readString(file); + } catch (IOException e) { + throw new EdcException("Error reading file contents", e); + } + } +} diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index d525360..a80c6a9 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -40,7 +40,6 @@ dependencies { tasks.test { useJUnitPlatform() - finalizedBy(tasks.jacocoTestReport) } tasks.jacocoTestReport { dependsOn(tasks.test) diff --git a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java index eaeccad..3b7861e 100644 --- a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java +++ b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java @@ -4,9 +4,10 @@ import de.fraunhofer.iais.eis.DynamicAttributeToken; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; import de.truzzt.clearinghouse.edc.handler.Handler; import de.truzzt.clearinghouse.edc.handler.LogMessageHandler; -import de.truzzt.clearinghouse.edc.tests.TestUtils; +import de.truzzt.clearinghouse.edc.multipart.tests.TestUtils; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.Message; import de.truzzt.clearinghouse.edc.types.ids.RejectionMessage; @@ -37,8 +38,7 @@ public class MultipartControllerTest { private static final String IDS_WEBHOOK_ADDRESS = "http://localhost/callback"; - private static final String REQUEST_PAYLOAD = "Hello World"; - private static final String RESPONSE_PAYLOAD = ""; + private static final String PAYLOAD = "Hello World"; private MultipartController controller; @@ -52,7 +52,7 @@ public class MultipartControllerTest { @Mock private LogMessageHandler logMessageHandler; - private ObjectMapper mapper = new ObjectMapper(); + private final ObjectMapper mapper = new ObjectMapper(); @BeforeEach public void setUp() { @@ -78,7 +78,7 @@ private T extractHeader(Response response, Class type) { return typeManagerUtil.parse(new ByteArrayInputStream(entity.getBytes()), type); } - private String extractPayload(Response response) { + private T extractPayload(Response response, Class type) { assertInstanceOf(FormDataMultiPart.class, response.getEntity()); FormDataMultiPart multiPartResponse = (FormDataMultiPart) response.getEntity(); @@ -87,29 +87,26 @@ private String extractPayload(Response response) { assertNotNull(payload); assertInstanceOf(String.class, payload.getEntity()); - return (String) payload.getEntity(); - } - - private InputStream getInputStream(String path) { - var json = TestUtils.readFile(path); - return new ByteArrayInputStream(json.getBytes()); + var entity = (String) payload.getEntity(); + return typeManagerUtil.parse(new ByteArrayInputStream(entity.getBytes()), type); } @Test public void success() { var responseHeader = TestUtils.getValidResponseHeader(mapper); + var responsePayload = TestUtils.getValidResponsePayload(mapper); doReturn(Result.success()) .when(tokenService).verifyDynamicAttributeToken(any(DynamicAttributeToken.class), any(URI.class), any(String.class)); doReturn(true) .when(logMessageHandler).canHandle(any(HandlerRequest.class)); - doReturn(HandlerResponse.Builder.newInstance().header(responseHeader).payload(REQUEST_PAYLOAD).build()) + doReturn(HandlerResponse.Builder.newInstance().header(responseHeader).payload(responsePayload).build()) .when(logMessageHandler).handleRequest(any(HandlerRequest.class)); var pid = UUID.randomUUID().toString(); - var header = getInputStream(TestUtils.VALID_HEADER_JSON); + var header = TestUtils.getHeaderInputStream(TestUtils.VALID_HEADER_JSON); - var response = controller.request(pid, header, REQUEST_PAYLOAD); + var response = controller.request(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); @@ -117,15 +114,15 @@ public void success() { var message = extractHeader(response, Message.class); assertEquals("ids:LogMessage", message.getType()); - var payload = extractPayload(response); - assertNotNull(payload); + var payload = extractPayload(response, LoggingMessageResponse.class); + assertNotNull(payload.getData()); } @Test public void missingPIDError() { - var header = getInputStream(TestUtils.VALID_HEADER_JSON); + var header = TestUtils.getHeaderInputStream(TestUtils.VALID_HEADER_JSON); - var response = controller.request(null, header, REQUEST_PAYLOAD); + var response = controller.request(null, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -140,7 +137,7 @@ public void missingPIDError() { public void missingHeaderError() { var pid = UUID.randomUUID().toString(); - var response = controller.request(pid, null, REQUEST_PAYLOAD); + var response = controller.request(pid, null, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -154,9 +151,9 @@ public void missingHeaderError() { @Test public void invalidHeaderError() { var pid = UUID.randomUUID().toString(); - var header = getInputStream(TestUtils.INVALID_HEADER_JSON); + var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_HEADER_JSON); - var response = controller.request(pid, header, REQUEST_PAYLOAD); + var response = controller.request(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -170,9 +167,9 @@ public void invalidHeaderError() { @Test public void missingHeaderFieldsError() { var pid = UUID.randomUUID().toString(); - var header = getInputStream(TestUtils.MISSING_FIELDS_HEADER_JSON); + var header = TestUtils.getHeaderInputStream(TestUtils.MISSING_FIELDS_HEADER_JSON); - var response = controller.request(pid, header, REQUEST_PAYLOAD); + var response = controller.request(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -189,9 +186,9 @@ public void invalidSecurityTokenError() { .when(tokenService).verifyDynamicAttributeToken(any(DynamicAttributeToken.class), any(URI.class), any(String.class)); var pid = UUID.randomUUID().toString(); - var header = getInputStream(TestUtils.INVALID_TOKEN_HEADER_JSON); + var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_TOKEN_HEADER_JSON); - var response = controller.request(pid, header, REQUEST_PAYLOAD); + var response = controller.request(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); @@ -205,9 +202,9 @@ public void invalidSecurityTokenError() { @Test public void missingSecurityTokenError() { var pid = UUID.randomUUID().toString(); - var header = getInputStream(TestUtils.MISSING_TOKEN_HEADER_JSON); + var header = TestUtils.getHeaderInputStream(TestUtils.MISSING_TOKEN_HEADER_JSON); - var response = controller.request(pid, header, REQUEST_PAYLOAD); + var response = controller.request(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -221,7 +218,7 @@ public void missingSecurityTokenError() { @Test public void missingPayloadError() { var pid = UUID.randomUUID().toString(); - var header = getInputStream(TestUtils.VALID_HEADER_JSON); + var header = TestUtils.getHeaderInputStream(TestUtils.VALID_HEADER_JSON); var response = controller.request(pid, header, null); @@ -242,9 +239,9 @@ public void invalidMessageTypeError() { .when(logMessageHandler).canHandle(any(HandlerRequest.class)); var pid = UUID.randomUUID().toString(); - var header = getInputStream(TestUtils.INVALID_TYPE_HEADER_JSON); + var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_TYPE_HEADER_JSON); - var response = controller.request(pid, header, REQUEST_PAYLOAD); + var response = controller.request(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); diff --git a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/tests/TestUtils.java b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/tests/TestUtils.java new file mode 100644 index 0000000..4cda33e --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/tests/TestUtils.java @@ -0,0 +1,35 @@ +package de.truzzt.clearinghouse.edc.multipart.tests; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; +import de.truzzt.clearinghouse.edc.tests.BaseTestUtils; +import de.truzzt.clearinghouse.edc.types.ids.Message; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +public class TestUtils extends BaseTestUtils { + + public static final String VALID_HEADER_JSON = "headers/valid-header.json"; + public static final String INVALID_HEADER_JSON = "headers/invalid-header.json"; + public static final String INVALID_TYPE_HEADER_JSON = "headers/invalid-type.json"; + public static final String INVALID_TOKEN_HEADER_JSON = "headers/invalid-token.json"; + public static final String MISSING_FIELDS_HEADER_JSON = "headers/missing-fields.json"; + public static final String MISSING_TOKEN_HEADER_JSON = "headers/missing-token.json"; + public static final String VALID_RESPONSE_HEADER_JSON = "headers/valid-response.json"; + + public static final String VALID_RESPONSE_PAYLOAD_JSON = "payloads/valid-response.json"; + + public static InputStream getHeaderInputStream(String path) { + var json = TestUtils.readFile(path); + return new ByteArrayInputStream(json.getBytes()); + } + + public static Message getValidResponseHeader(ObjectMapper mapper) { + return parseFile(mapper, Message.class, VALID_RESPONSE_HEADER_JSON); + } + + public static LoggingMessageResponse getValidResponsePayload(ObjectMapper mapper) { + return parseFile(mapper, LoggingMessageResponse.class, VALID_RESPONSE_PAYLOAD_JSON); + } +} diff --git a/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-header.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-header.json new file mode 100644 index 0000000..58522c0 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-header.json @@ -0,0 +1,19 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "senderAgent":"http://example.org", + "modelVersion":"4.1.0", + "issued" : "2021-06-23T17:27:23.566+02:00", + "issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-token.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-token.json new file mode 100644 index 0000000..ea2bbf7 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-token.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:LogMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCJ9.hekZoPDjEWaXreQl3l0PUIjBOPQhAl0w2mH4_PdNWuA", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-type.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-type.json new file mode 100644 index 0000000..e96fc8d --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/test/resources/headers/invalid-type.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:otherMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/missing-fields.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/missing-fields.json similarity index 100% rename from clearing-house-edc/core/src/testFixtures/resources/headers/missing-fields.json rename to clearing-house-edc/extensions/multipart/src/test/resources/headers/missing-fields.json diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/missing-token.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/missing-token.json similarity index 100% rename from clearing-house-edc/core/src/testFixtures/resources/headers/missing-token.json rename to clearing-house-edc/extensions/multipart/src/test/resources/headers/missing-token.json diff --git a/clearing-house-edc/core/src/testFixtures/resources/headers/valid-response.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-header.json similarity index 100% rename from clearing-house-edc/core/src/testFixtures/resources/headers/valid-response.json rename to clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-header.json diff --git a/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-response.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-response.json new file mode 100644 index 0000000..85057bb --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-response.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:LogMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file diff --git a/clearing-house-edc/extensions/multipart/src/test/resources/payloads/valid-response.json b/clearing-house-edc/extensions/multipart/src/test/resources/payloads/valid-response.json new file mode 100644 index 0000000..ecf63af --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/test/resources/payloads/valid-response.json @@ -0,0 +1,3 @@ +{ + "data": "eyJhbGciOiJQUzUxMiIsImtpZCI6IlFyYS8vMjlGcnhiajVoaDVBemVmK0czNlNlaU9tOXE3czgrdzh1R0xEMjgifQ.eyJ0cmFuc2FjdGlvbl9pZCI6IjAwMDAwMDAwIiwidGltZXN0YW1wIjoxNjk3NzUxNzUzLCJwcm9jZXNzX2lkIjoiODcyIiwiZG9jdW1lbnRfaWQiOiIyYmMxMGVmNC03NjFjLTQ5NGYtYmQ1YS0xMWVmYTJmMzNmM2EiLCJwYXlsb2FkIjoiSGVsbG8gV29ybGQiLCJjaGFpbl9oYXNoIjoiMCIsImNsaWVudF9pZCI6IjY5OkY1OjlEOkIwOkREOkE2OjlEOjMwOjVGOjU4OkFBOjJEOjIwOjREOkIyOjM5OkYwOjU0OkZDOjNCOmtleWlkOjRGOjY2OjdEOkJEOjA4OkVFOkM2OjRBOkQxOjk2OkQ4OjdDOjZDOkEyOjMyOjhBOkVDOkE2OkFEOjQ5IiwiY2xlYXJpbmdfaG91c2VfdmVyc2lvbiI6IjAuMTAuMCJ9.NhMDSBTRiJJP04NEjBlB1Rt4LlvwDHrOEvNm0qbYRWqe8Vfdza1SSy-OLDCwMnC14hxHmwD5GpWOCbC5iswmuEeWspSMCGcGnGKZr_ra23jr4HV60YKnCAbBhOi5dmiPb6R64DSSJBH9Dw1Cni9zFNLBgUGr8pGEbm_AdijomUfl88fXUiyBWdrP0S-VVtlcygYROZtTusqBz95E_WKSyFU57hf4vOjkFRjfHHkuu92MUrJJwVXwf55YuVa-uLC8Exr2pScqeo2JI-1Y2JBCInOtBtskXmFfocav8ReIZhvL255O1-vHi5ZFsbQppEtstcz2txjP34EHoPCu8NO9s7G-BqJ8hKw5QTMKIV8-N1yrtGb2sK4qXUQCJpCKfJoMPG_BLQo9vHifWJ6gO1z4NZvOvqOXyIWGd89C1wsCWV8cSJcbye-BgAo4SUAdN5KQXTqiyWRc4wrNXC7S7Ajy639xW6k7epXEuya5qIdkP2qh-ZrL0WndA20jExLFzgYmvXVR15WcFsiprgxutFevQ1a-EWOZDsnnTSPhTt5KPFwziKepTzKq73X3cs-IRxAc_4qkEi0-zEy_YIfoWNMxWdkh4EiBj_wpgiN7msskGCGPzq2wslz64n2-AKsQiXqFEPMNv2ihRNhJHxL0PAJKMWYXStauafOffUazfZag0p8" +} \ No newline at end of file From ef8e31f1f867c6707554ce17558a788eb309a805 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 19 Oct 2023 20:06:13 -0300 Subject: [PATCH 124/183] feat (ch-edc): unit tests and coverage workflow --- .github/workflows/test.yml | 33 +++++++++++++++++-- clearing-house-edc/core/build.gradle.kts | 3 ++ .../extensions/multipart/build.gradle.kts | 3 ++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9b73abf..4dcc152 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,10 +7,10 @@ on: - beta - alpha - development - + - feat/unit-tests jobs: - unit-tests: + app-unit-tests: runs-on: ubuntu-latest steps: @@ -27,3 +27,32 @@ jobs: cd clearing-house-app cargo build --verbose cargo test --verbose + + edc-unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Setup JDK 17 + uses: actions/setup-java@v2 + with: + java-version: '17' + distribution: 'temurin' + + - name: Rn Unit Tests + run: | + cd clearing-house-edc + ./gradlew test jacocoTestReport + + - name: Add Coverage Report Comment + id: jacoco + uses: madrapps/jacoco-report@v1.6.1 + with: + paths: | + ${{ github.workspace }}/clearing-house-edc/core/build/reports/jacoco/test/jacocoTestReport.xml, + ${{ github.workspace }}clearing-house-edc/extensions/multipart/build/reports/jacoco/test/jacocoTestReport.xml + token: ${{ secrets.GITHUB_TOKEN }} + min-coverage-overall: 70 + min-coverage-changed-files: 80 diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 57ffa61..c02efb7 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -43,5 +43,8 @@ tasks.test { useJUnitPlatform() } tasks.jacocoTestReport { + reports { + xml.required = true + } dependsOn(tasks.test) } diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index a80c6a9..5bce05d 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -43,4 +43,7 @@ tasks.test { } tasks.jacocoTestReport { dependsOn(tasks.test) + reports { + xml.required = true + } } From 08fda1bbd4d2a67850c10adb041f70ff8037d1ee Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 19 Oct 2023 20:07:40 -0300 Subject: [PATCH 125/183] feat (ch-edc): unit tests and coverage workflow --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4dcc152..1767b3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,6 +7,8 @@ on: - beta - alpha - development + push: + branches: - feat/unit-tests jobs: From 65c2c7ebf0938ef65cf043f573bdbdbde2e6807e Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 19 Oct 2023 20:17:22 -0300 Subject: [PATCH 126/183] feat (ch-edc): unit tests and coverage workflow --- .github/workflows/test.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1767b3c..6b4833e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,6 +32,8 @@ jobs: edc-unit-tests: runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - name: Checkout code @@ -43,12 +45,12 @@ jobs: java-version: '17' distribution: 'temurin' - - name: Rn Unit Tests + - name: Run Unit Tests run: | cd clearing-house-edc ./gradlew test jacocoTestReport - - name: Add Coverage Report Comment + - name: Add Coverage Report id: jacoco uses: madrapps/jacoco-report@v1.6.1 with: From 790adf9a0c50b9a1601d8bdce763a65698dc1f67 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Thu, 19 Oct 2023 20:20:20 -0300 Subject: [PATCH 127/183] feat (ch-edc): unit tests and coverage workflow --- .github/workflows/test.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6b4833e..1f16c91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,9 +7,6 @@ on: - beta - alpha - development - push: - branches: - - feat/unit-tests jobs: app-unit-tests: From 657405fd63b9c28f12b08fb52f9784cbe5bd2957 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Mon, 23 Oct 2023 14:54:19 -0300 Subject: [PATCH 128/183] chore (ch-edc): workaround to fraunhofer infomodel expired ssl certificate --- clearing-house-edc/core/build.gradle.kts | 7 +++++++ .../extensions/multipart/build.gradle.kts | 7 +++++++ .../connector-local/build.gradle.kts | 7 +++++++ .../launchers/connector-prod/build.gradle.kts | 12 +++++++----- .../libs/fraunhofer/infomodel-java-4.1.3.jar | Bin 0 -> 1324708 bytes .../libs/fraunhofer/infomodel-util-4.0.4.jar | Bin 0 -> 13437 bytes clearing-house-edc/settings.gradle.kts | 4 ++-- 7 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 clearing-house-edc/libs/fraunhofer/infomodel-java-4.1.3.jar create mode 100644 clearing-house-edc/libs/fraunhofer/infomodel-util-4.0.4.jar diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index c02efb7..8fe966c 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -20,6 +20,10 @@ plugins { val auth0JWTVersion: String by project +configurations.all { + exclude(group = "de.fraunhofer.iais.eis.ids.infomodel", module = "java") +} + dependencies { api(edc.spi.core) @@ -29,6 +33,9 @@ dependencies { implementation(libs.jersey.multipart) implementation("com.auth0:java-jwt:${auth0JWTVersion}") + implementation(":infomodel-java-4.1.3") + implementation(":infomodel-util-4.0.4") + testImplementation(libs.junit.jupiter.api) testImplementation(libs.mockito.inline) testImplementation(libs.mockito.inline) diff --git a/clearing-house-edc/extensions/multipart/build.gradle.kts b/clearing-house-edc/extensions/multipart/build.gradle.kts index 5bce05d..68ed15c 100644 --- a/clearing-house-edc/extensions/multipart/build.gradle.kts +++ b/clearing-house-edc/extensions/multipart/build.gradle.kts @@ -18,6 +18,10 @@ plugins { jacoco } +configurations.all { + exclude(group = "de.fraunhofer.iais.eis.ids.infomodel", module = "java") +} + dependencies { api(edc.spi.core) @@ -29,6 +33,9 @@ dependencies { implementation(libs.jakarta.rsApi) implementation(libs.jersey.multipart) + implementation(":infomodel-java-4.1.3") + implementation(":infomodel-util-4.0.4") + testImplementation(libs.junit.jupiter.api) testImplementation(libs.mockito.inline) testImplementation(libs.mockito.inline) diff --git a/clearing-house-edc/launchers/connector-local/build.gradle.kts b/clearing-house-edc/launchers/connector-local/build.gradle.kts index e909111..f1ebdcb 100644 --- a/clearing-house-edc/launchers/connector-local/build.gradle.kts +++ b/clearing-house-edc/launchers/connector-local/build.gradle.kts @@ -19,6 +19,10 @@ plugins { id("com.github.johnrengelman.shadow") version "7.1.2" } +configurations.all { + exclude(group = "de.fraunhofer.iais.eis.ids.infomodel", module = "java") +} + dependencies { runtimeOnly(project(":extensions:multipart")) @@ -26,6 +30,9 @@ dependencies { runtimeOnly(edc.config.filesystem) runtimeOnly(edc.vault.filesystem) runtimeOnly(edc.oauth2.core) + + runtimeOnly(":infomodel-java-4.1.3") + runtimeOnly(":infomodel-util-4.0.4") } application { diff --git a/clearing-house-edc/launchers/connector-prod/build.gradle.kts b/clearing-house-edc/launchers/connector-prod/build.gradle.kts index 2e15278..52265b9 100644 --- a/clearing-house-edc/launchers/connector-prod/build.gradle.kts +++ b/clearing-house-edc/launchers/connector-prod/build.gradle.kts @@ -1,6 +1,3 @@ -/* - * Copyright (c) 2021 Microsoft Corporation - * /* * Copyright (c) 2023 Microsoft Corporation * @@ -22,14 +19,19 @@ plugins { id("com.github.johnrengelman.shadow") version "7.1.2" } +configurations.all { + exclude(group = "de.fraunhofer.iais.eis.ids.infomodel", module = "java") +} + dependencies { runtimeOnly(project(":extensions:multipart")) runtimeOnly(edc.bundles.connector) runtimeOnly(edc.oauth2.core) - - // Vault runtimeOnly(edc.vault.filesystem) + + runtimeOnly(":infomodel-java-4.1.3") + runtimeOnly(":infomodel-util-4.0.4") } application { diff --git a/clearing-house-edc/libs/fraunhofer/infomodel-java-4.1.3.jar b/clearing-house-edc/libs/fraunhofer/infomodel-java-4.1.3.jar new file mode 100644 index 0000000000000000000000000000000000000000..fad447d3c951acbfe21d5b2b25b72195a83f3486 GIT binary patch literal 1324708 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%0Du+YRa zWGWwSGx1l@84@|vCt9%q7Q}O1)50n@FbZNGRRt|f069wnbWV&Q{RIU5y>sb!{(li& z1if!W_otQ5xLs^iLH4y^V@{D#mK_q#x1)YAxBE_8V-U(`M?rPucP(H1d*W_I7I+U`>i+E8j_DpQqeaw=Oy)(DMPI zGK}NN0EWAmm@u|8grK_^*rUS*oXJ^v5zGH2$$j9)BbK7I*W{Xzl z>v>NED3dh3>vqrAU&hULHbBNhE^x;6?k8Llf$sp0r}C$_bRPn3CmSBCNEg~LJ$&@U z4uac#t!CD({i9o(S0N0nYa>j|UZMX;SLu)+L+XqF2Up&1#QmE@{9B?XH#tz|(=Iu! z@tzr&&XYb+w%lzJ#r}J-=c5LoTMNwZgy_|U`z71|X2QiAkC*ph-|a(m{H4x6XLL#i z`C~HThz$=cD>(=C@ae?J6@m2V9M$NB*qd0TCpl+ll-0Xp*9BNeSX9@1gvP1`CG^}_ z_u}|T*)J_aL2e=Rlv{ZI6#BvL<0ve9kV9xjW zCm79vw*mpm%CBQ>amwwgAcb~cizd&?Ho8w{JpqaMw!Z#mQa*Iy(QY`zkhIyz(li*y zp~-4RHp%|BwbYi&D3b#kI14X#)!cq%79)&3zK}SkG4*vT7(r&nB)rii_7zIb-|o4* zc}4jFb>7?vbWDWkv#%y{qbk3#!IkFvt18IKi|DS_A-+>WY4@N*1a5PH>p}AnOqT8h zqpYL39b)w;kJtc=#hf2sOB!81NEJFM=DYJ5{{ryg8hX?ms>c&xF-n1Y;wrv<5|WtK z9u3Ka*~&zdX1Q>Z-=En(nPfiY7n(^`GL!$>Vj_>0_zYz!$BkLpUy44cQn;F?$4F`k zWem9i&7l`_%X0+VmZ9J6jkyR*9rAHJ+;u_*T#$-RgZ}ERA`)o!IXsRT0pM3!ym17{ zpI33Mk+u6kTb3rEDoiX#B$9RL8(8NhS;938*aD5lliChq_}I=w%29|W!~5lSAtva4 z+B~x?7>Ww5Yl#RdNow;~qxP_89z{;~yUr4AixdGRTHS`jhzwSU?hME*2^_fPQ!F(6 zMX;NxA*?dD;)e?DQpV8yYLx(QT3ddaZT|yrH9sSgc9$;e_+Tn?3cZJ$CZkzp*r)?y z)+k`N-w{Dp=LvPM;yoDEc&8nKpgze7t zg%Tk;fkvgYMu7tIqx)j)%j<^oY|g7C(1PoVXIe%E&iCR4u35joE6KUlbNwngq}U@$ zQZ@_$1?}{#Wk=9O1xBgxRFg$$r1JKQ(dV%m$SKm6tsh1$EbM-Td5|rwgkLEKi zkzYsl@lq46 zatcs9M4?Cm-zeC}@D_}-Et~BlxxitXYNgoBPX%LPh1+ zvuBHRIX~8=N~EGi5D&Udq|8@D)|M*ZlV!P%1A)Sx}TCG*_F@wsM!Xt4f`oq|Nm#(+_UEr`9hfFfMBq_?%X@jXEHxhZ%S+h<)Ps(6HVZZM$)Ga76eyWRay-Ba0r2_DEo<6r2l}S{mUk}eXpE8svqpgtRNJyt?wFQx|^KQ^C#dU8A zbGb@{8!s^$6#7n%K}Hr?C|HRO1BHbQ_9;llWYAt+y~hn8)ElbWtrk%*y^n5s#<8Ng z=t=YFoLlyT@plQd1M62lcYoCSlErlk_m|5Je#qpNq^N6=ZswPQk@am~ zJ}FNL34d6kdaM@u_pFtxm4#l^>Y?XFbhi&3RegVWUOrRi^-_m`Bkk!&i~QN`H_xsS z)2CiP)Q1Rfm5x6~+^#vufyl>Lo~AZIv$x)DhrCJ-L}bu1-qPY3!#+Vgsir)!HUQrt zxu}kJG0?3R)n^=AJ#}OHbhkj{?%X~;RUFU>bB;W85_>oM?}#ZlBukuxmKhc*y8-R&8j*EID!*YX{yx4Eew91PyEQ;hx8s=}!0^8={G}}eSQ7Tjat$ad3FZ-xg#`JztW|ddNdd?1 zFHt!l!H^oUFgcHC#AVk>c^H_w{ESsEp$|Xz7A%)WX)6%SC!?MlTcDfn12aWeZlA{a zCl}##gbzn4QS+Gco^B(Lew2~0CiZ)8TZ0t29$FyMo%~Q}yPdiPm;W(=3?<}HA9Mj! zSe9_s4!G&z;z9Zg(irkm00fm0Oj=)A#UVllu_aR186lY71k^!mwfQUQBpqJudkdN= zH*p7hvW>E>9l#h`5$*a-m1}aeWL;n7Y7nX8x=jmF_lNc-A%HTX5y=gPcECv>wA`PX zXRq9^bW5Gn_LfZ|Droq zn!**{;#th@_1A9W4StLMtGd&MYgKrEJ#gtb>GCGie_`Q`7Qx3zWz92q5$u(Ek$O#p zarlThTO1{X*N5M&5%oG59%Lq<6tvrXUDhjh}hLG|)-0KH%IY zZyvvVU8YJnss$K%HG)-sfx5ImgGxZIq!Ind9Cmv$MI_2(p=T%$msy*9(+hyoe!LX- zQst__v0E*HSvD4(RMy?-FCuqUV_qM5#6xxSG387)dg-FJK9#@3ZwSySBM4sebbP4DXMOsT_{YRWqJF6&tLVLox!rhHk_t45~cFuvp(JC zEAgyK^BJkL#6=kwX;Y}uit^0pFt#XZeIB^tI8`+!mq4;5T)6dWoR-?`%IWi91bIhM zzfvCVw|ZE(diaa|Fk= z#mAEN7(iN^iNjSQ(9au0(er^l+%{sjO{Z*`U>>>a={J4s__V@Y(%{3dGC`jBt|1&o ztGmrhi_9G)P9Efl3NRk9XqMX+3c+&ny1n;6l{nub8{xj}e$G!?m>p`3+4^&!c;~%b z4WWwg#97|VAh|QKyf+SM4Bx{#Z`fSEM8*@~21w4~Cs*kHBu0`tSdyZmskUpH&(Wa7 zP0w{1-H3$JP2XYx!*+C7kFMVlhe&++5ZS}EaMr$ffGompGF(|#jLr~0-Bdy;gg>M{ zF{c@fgPM4);_8UH(}P}1gc8M`;KiIRIP|gv=5wL*xg6K+A&_CQGwQkLujUEl9C_s7 z;6Oe9<>s-&)1Cd7E9Jyqq> zNF~H(q}`ZX53=4utGn+rytPTIPd|JT|4AAl!ZeeaP};H>?#~ zv4BWvA6{NM9=m)x98acQ^?d!kgZDjnp@{5iMeNtmbKDhYebQx-3|7ZZJ?M60QCTBe zqk7oIsCKvj&uCfe95~iuVO=X+-PAXz6Kfa?)Dx&zGxTH$)P$r6+yL%S>>D3fQ*$;H zLeZK~D%y9<%S2_1uPtgjRbA4rZY0B2P%GCLf0JU(p{J;IlSEGP5YA=f$V58Dpk0&C zrOgY!mv4$qp6*PVR*><@PprEan~?jsc5!ZFS3+Q0_TO+&N@^(*$Ziq()lx+~K_$M@ z$-&7ze$AOkJ=@W(gO}J|Vab|;*m^ocu4+BaRZ@Rq6zm^|i)}5@$LEmRo2)2ZZaGaH zvzJJrtXYv7_h8@WqT{5rEm<1AQG6gu7~E7Danp4#9(e$3Fye@V*}S5{?H;_lob?7>(YlJrJfuxhPe`e3{ZjsYVvu~b{U5B#x+okh8_=7QEZJ(u z(!yK(&W62!6-#fA3Td-v*3$xR__`?0pR4KhM#KlD42<$0+GA=)YH5y@3@Oi2X2$@m z=w^aom&<;8VfRIG$Q!83(B-F%LrqrN5}OGa)NgIHbQZ2p&^otyaexaumr6MYI(uHL zGyJ)LOKK9@Y6!o&wM9#_pUGXOAL?Da9~S^T+)|tjvlWCiFdPU~2}GjRLKj^C{Cdv| z1b0^e1h?)rHN+cuq_<@AEmGr|9;qLM#M62g+S5!IumCKgF%8weg%Lnidow17!uDE7 zg;8SVge`%*RY-BfZSwfHc%q$P3+$8U=W4+_SFTKiH3rMk*16(cS(iN!q$T|JIa4=j zhqL6D`0uK&29K5JS=K}&g{SLC@CD14U|)Xiiz01ZQ$kc&s5OBF zI>Kiai}@7Qv@jPT_ad9|Nh*y5WovOnPYWTB+K^lVJ#ajV4}Lo`Rvd|jIVCL)r-dj# z!?=S(|As|h#LT_^)asGdftZ4DHv^pwp%EOl9sqJ@G`egnapq`IErO2O%H(`5^~SXzD-@)D>0C=Xb#u zsP-RM(!2KH7Uo_FL>vAA&a;~@@n4(iJBLNg!F7U?z!EG^Dx2^+cQSc%=t!t3$tV$DQJ4S7!Y+@S&% zD%am%>VG#t-g=1t?wCEufIi4Do#xD*?c^FLeTwn= zlyusUb}l>|CO!NO_Di4D`Y;Xr6q@<=RIT#hzLETGFFnyGlAaBOXXYl&<#>neP?5{% zn$BiUm5M^cP>VQ%p>dm;_@9sO+3S=>&ucObqP=7htMt?eAA91*I|EZ#`Q{B4lnDr0?EQel%zJ^$l)xO-bQN|N_xR^rh zs9;cEaqpxz2GMwlyKR1>xw|5?|LvtR^yDL<#IK{ZjYQF6vPSaP(#=9bx~a36qp(J= zG{;M;rEOx7*LefQwv5wK5sZ$#!0xShj=YzOo3)Yo*UEw3KoocJwZGBamxbI zc?q%HQq8~j6KePVJS%eAm(&IBgZNa>uw~Aq2xyFZS=Ys-Us{Uq?=Lw)uUWH3Q1jKK9XIiEH$yp?!BYBC$cYYq=Agl<$JGyD-PAM0=DtG)&#p7ZnFMU%b? zS<%?OsWQD4<ASe#nEt`E<)e_hBNczVZ*;N0<9+W zKs6aY#ZdnAvyE{^xERy?@Khc|s~LH<%sfGinZQta`Gzymcg<;gk#sZ}eaA2wv`@HL z(QX52!Dg1{T_F8zIw$vxS+Q-wT3$3b0(3EsEO}59Jw z-L$t(=p{;Tjrk7{P@?{!G;dccv8WF41xnDKd>dx0lH`UIsHl;?{zqPVizUN!_5BiZ z{~f*mAC>X{!vT1%$q*_ZI8jt#t9MX3jx zn{O))N(A}f`uy_$A5@lJ!0@jpJL*$m17H*dhLCok=85FqEE z;H>iPOir6%Y_sw<-kg9(rA`t>> zs{fT&m};#KqrAJl4i&~`)#DXC;$3bs2|zpWF4<6DRHpbzKz&S`nxm4@ZF#~NHt5bK zRG0~5GD6bw-9HuqN@K#SH%{C<&UGBpnM6KoV2%uOW_qa8>Pwd9I=*zdLL`l%Y5=Z; zU{2R#P}Hx93JE>bqHK?duX;>FdjV%T;IEh!8&B%#NK3cWwarzBvFcoU~juJQD| z>F1K_>e)K@epn%#JJ=yTceegLtQtndfAz+X+89~>?TrV^kBNHfIxSBUcXd2=Cqj*G zETsI?8=qQPFGtdp{iip+YlMB3@lS6YeQv)>T`?x4iD>mBuC|V1s;tn%Tt=1kEj|vP z{omerB(F)~1I&4xA3v5&IT&qm5qJP5A;`&vS6nK|^YJf)0>z?&tom$78nuY$J#iK! zcVdmlyh3ZfjN<$aOQ2#5#WiBW`HZvV$I$WMCZLCtHZkiKF=1jO)y}l)8q(qr!t_

;k4(C7SyB)baXv?m9uXd0hG?>-nOPZAlk(8HSTt{7e9AXi{4LK{MNu75^1I zcoY8CYT{xr_|^MV+TBT{xQBe4qT(zdZ;Q6Br-*w+u)mDoW9~X-b!VPA&XuSMrRI08 zGlYVj1wm!RmdL*QiEX8djP-)Mkf@VKzD&gRtZvT!(z4MqU za^O)_*B<|+9bn}B1Nx#a9w6gClu;SF@v+U@%@wUZ@pLkyUghuY>mOGiA*L>z;ls!hF8XL~U9rmpOE2P7-0p5N0 zN(jHAWK_v4B1V})Z(Eae5m%1Jt#$;tnjpz|VXDI*OY^U4ZWC_73VG91`N>8ewtqs4 zwAfYk==7|&?bB0vFJMF5S3mvWW{!U5AzB}W&&3{fX!zhCZsZJZQiAa-q1FgG+@w|w zQ&$f;f%dY527f8BN@0SY1d}EstUtK;TQLf6y|%j83O&hCI8qTLZNKRAzO+0ugNZ=5 z+2nS2s8Y8~*=Gh5EV!iWNrPZ*gx5wWe+>}vG%ld1^)|*kIhUX%X1Xn6s>VNqUp6^l z*gbYptGmUktf5+#$Q118DeplzG9?>B6W#oFmH*i=v=90-N4GN!AfJ-m>QOkeNi^I% zY`tv2v0E`f+w-jg3S0r8(ZdA8b*Gj|d!l2dXWQ$8Zi;9+? zWJq}0wylZWHUUfWGYoS@YC>E@l7u6ByN4w{yBNWWL5B zA{1@2;+DlhNd*wiO1$8|P}-4Gqw>P}`IGVMokZ&3?4(vQHg5;<2A&eHP>xXP+gIM> zO6-eitLq{kXSl`efvq$S{`Ts$W=O^M3z_1@-k?-S4*pl~V4-4HYl1DH@ESPT_Fzr9 zE9T&ZE2b}8G<5z7Je2t zmp1rH;icf6A(?OTiAX9#J-Kk!zAM&zB#4PDIs52M#l!zf?HL7%6-5$T*(04m{>1pR zu(xHQ^bJKrmjS}M6Wn8JY1JNlVsRaR zk=_Tz(cfQUa_hyxTvV+jzJ}$g!5f!T61y8U$_fGWQV+@2P>1`c0nWhRvt!jXcL8TJ z(GyLNp^8~snhKyDClz)Kpr7@oYy4A5jj(YS%Uaih$5b)?2rbN%#6}EQWeW?M_9XJx zuAln>7)&A4XRuM|gRirPHX>8gHAnpfD#gG!j_x)#Ww@l9tq4=1ezB@Bt1X7^&~bsa&l5jB(IH>8Sr;+L7CgddBn zhMRz>*w&-AF&CnP(TjxqTn(AwU!u^fXugw|@Y-Pv!$a(XE6{v!Ujzk?^wW8G@g@q; z=J}=9%#qQgROLjRuwb%R_VaAUub9m+bZE15l!Ll})2V8GNIyIJ6Gw2-q;`O1;XjB@ zSxA!t%BHkoW9vhD2|HQinnRqhy#W=yY@i0s0-m+Fc*Lx!)F$${M&k2$McUT#$YY`@ zNlBn9wE~_3A@7?CqdtMv1B@`hLkDN@uRl_3?KnoFA-pliN`K;}+Oi&_Qxn)}bS>nr z)q1^o9`7IoUV^RY(0a72<;KbbfqlmXzcBpZCRB^GZMnp>*SVpfJbla*R=Oyoop&Z?=FrPcA)U5_H7e7y{>NpWFtvW=7jiOp zj}=-#{xaT^r_FI=OCby>8Q7l^feOvbOu<(z4Q7T!w1+-8kjnC4PG+T@vld8FfJTiflI( zyyLavrG-EEuIyFSC^l;en#L8>=y03VDWrtvp`6Fx>3v^Qn3G?Vj&X~L*JP7eqA#@K zQ2Uv|eD-sqh9IwX;ZD@-Qy|?V0o`fr8I#6}PdI}UL4m5&3-W||?UCzLF0rP1hXNPf zLVev1Y|KB<$wkw-gyQE|BUsiV8WL2@@2wH$(8D>7rhHWe@KqcJPn1L3zoQckKwBbi z1A!X#5BdP`WE(zz{ z<=l~Az7{DgoLES~Q*u1__ti-4#lr>7dhO5(o_0`wrPo&hVpD$*hg#wT3d7MnXz zc=bHDK#ReKYDHhDEKZ(&g$(mLQF2P9TA791^Ewf7^z&8QbGWCEyY(dVIu!L%H_0c) z&!b-8UB=Jl&#%|kf0|XJ?^mD*!DEv3b zYnk~fi*cWr@Kl*mLKq>RnsC|XB0O7B+2%lv`*UN?jwzNeBs#j~`d(%IZ?WDz8NHtz zx9{ZyybWrmimSFY?7})V5p?pCN^R$aV5-#V5Vz)j7+SQN)jWjUY$i4!Kem*2>fUn* zR9W|7!@Z#i;X_WlVfbxUx2koKPYcG`B%;uS$hoNl+uH@s=uHQu-@3hu@;42CcF z>?G7}{}V~=Q!XsFW;@&mEt9YKYEJIh`Blv$)n`=Xo+(?1?}+mi+%{LQx58(l{g`cC zs^)ItUTZE)^EW6}+yyUa!3}?}h z^JyblUWexy+b;za`sE_YkFkp6Zo})(Ycc%kxBl-dNW-p~{>%r!z)H~duyGhxKj}D& z$qwB4Tl!ge+@T$}ihOH>twtb}v%|^Ie-Z$_=m2#_y{|+#Ez&Y33QnyacpiHb9I~mJ z{`D$N#C3iYTCNP740z$p010EE{o!krf&n+?+XF1_$Qy?h!>^m?>jE;HiD<;?H*ODn zTdom7zkW%Ez@M@Ys<92VueV+G&oeZTcol5i(zr*|Ny$AEB$Z}5V&MqsNzLH+l)wvY zmXAz#-V*$)>-c-hXCL(Tte4*yQ0)Nt_SJYo>YueBLOs$1Zu7_DFLe^?J`}x94RBiZg?-ntC z5~Gg#UL8e&8!&#oe9(7)_@r%dC>;{Lj7*zcinfu7SgLq~p8jbYbh>zP%g$||EsB3n zqAwq$tY=Pmpkx-4CRHQ+gGVT59RJR3ZaZhP#P#{gdCue+)D*q4-Bfuxkh9ritE&I! z^2oq3&_I~m&Y|q&Q)yZ$Y^6PM>_`9p5_Er^9sp4Y=>tdXIQ7i*TK^NmH`?S{aSZvI z*1aV-bx+QN${nrdo$BA^b|-Sp0FiGV>+5@k^j~sTC2X9G9bELSRQ}bT^N-W_-)O9& zR7)+Kf6-VtwEngHxqhnFmNaI_MsLx|gRTPI;Lbb`pGYgX&R=#{*FU1WsF-fIHm$z0q!-hPuILuq<|MctYm;PkhxLX45s^9 zY$XAg&oOd{Q{U`34@is-C@hwYpshAr!`gwDB-7Hoat!y!jiBZPsj`x(^V*8V#W5)~O0-xO z)mdwliQ;ybyqDUB5}Htb(nInxYUJe{6SBv5-SIB~EoMqtag~Y`OI2Zz=Ry}+QNk1j z)`DWyM}D!@X2ceKqm@4iwgO8D7S_V|dZB=3onBY4xxnV{a9k6932%)iHV4Q^zQ*!g zxO8&ss{$J_U5{YWsJrYrUzD17;?!^GRhaM=OU(hz8Oh*3fW`Dk-q|0kJl6Fgc`D)y zZSo6WN=~1Ab;$ch3QTt%WrY}U35>Q=LMyY5lG=0{6CwzUN@muJw5T4A=L3uyOD>wI zb}LAnEJ8h#Dm<2@sw*TRx^(Rf;!1{VKPFQmH&9;$L4P;Znr)mPQ*4yWt{jAf`YcD2 z*)`u3F5FvLSY_;1wrYPN}thj~zPc*b{jA`Rt7p zhXBKP4NQf~i;03BU z3Yi+q__6nD}Ml-`KCuyk)xqAaa-<3no^ z7;cU>v>F)va|T|~O5t93wIXz9+}_v#zywBZwzBkXl~Dg2tQnzg{JsUsi6s|w=HRz0 zD74PLMiU+6g62H$GHIqmbCaA4bRGoaqwodPGqg09I4C6j!@0?y%&N3PH$ zuPu=sUU*Ln9;XRqrlg3!ajs=SSE`POs}sZVRgU!5Qj?#mDm&G&xNHZ|oastu%K1i0 z-I$#~L)eHe%-2%sv-oq4@`=1) zNq+iURKkwNDJ| zFbO9VXNE|x!j>ZSTom`L9%HKY&(A#y_4^UaeZBlWUJbPQ-8-syYKAo z#uko&aXo0A9UujgeZE5))=aof;g)>&>pI0+m>Ukcg{AXY(V1N_RRO zgJe~u+E=u+d#d1z@Whv7ns0B0FR&T!W67;>rQqQy^4?eIwHxg?C-24ZwLtKrg!{Fd z_+tz9UX16pAkFoj?pk~)c9HMr>=)?CmqIRI1djF>(N-7c10|2f{-z#-IyhpT7|%Te zkFU!`SSRZ}-fLsRk%?VOu076oQ+clU4bT*B{%ULeSg?7;ko?%`vh5D5FYU*F2n_m9 zI>tV}RrToKmowde5g7b$7xVv1P}$7!qw*v|UJ_3;b4!xTo6RLO^Z=dGW&}Zz6iiDp zJIyWh0~$~=vBsEUj`iw-!3lcA2&;u6f^vyc)ZH%E&zO$1b1dK1p6K*|&-ZQ^9hUEF z5AqK6GxYU;8qYJ#aP%d56mZ3PVMYq)0P><*kw+aQQfsoJ$cVgb2grB{FD3RX9P~xAh||&+nFlXC{CDZ(I>%=u zTL(@W0|AVbDPN2bFY;eVOJW|EkQ?EXPG z=P(?2DAFD5%_5tZNkh84AgV(qA&?eAYfKBM^xop-*< z{3+Xb`<;4ybbH@VNr~EQ?wVDNgWRkzfEVe!PP*TTQj3mX*banPCpr!D*Tw*{SyN?Q z3=fAZlJ7qlqEI;D%!2RNyWKZK#Pt6MN&gE$qz?N{5IOUTQJEKR%=F!&yeH&nc6Ou$ zMc3gPYGG>q92q==>?IsEV@X)ly@<$)uNx=krw10)1xsMx&+?PlZE(RJTdr0zLz$gr zhK83h6IT=zBA}o!7hGdzAo&7dRhfv@KaMu?ZL~hEJ5GLFwH{@9Uh+OI0%}lZ(B6}O zve?!2NYkACqj+?m~l6gP<55U`+cZlgvC9$Q@{!a!vg*i9U*Z zqJu7c<$yH_uaI^W?8Zwc@1`GjXlEaWe`H~fPSK%WoC@`%ctV3hJ|@-jOZ<+#(#|?A z#V)J&K}EPjanV+>r8Oc=4f^dAxl3_}<crqwDaTGYuEG;Idkove&Bq^GJzRwt@Het6E7d=& z4>kl>3a+CG+p^n@?Gk%Eh2{rrQ~yrPb_hML5_7qi^kORsbi|f*80NU}N0K-*{jn7D zq?$A45=}7i65qmO+wFw*He>`avM^;!VoS%zs9TWF)?gK9^Rg#}ozEG(MJqGr8>bO{ z2MqIMVK2@dk~g5?O(PR?D*Xp~BLcf;$+34v>(8A&EXJo_Do zCW;JAKRGqn;poJUqM0wmA2)p~df$z+PRYpv^A5NgqA zQL|R*G1ZKfaMPe^IDUkupSU^XE{Loww#&r=&~s<;8J33QvP~5Cya!jq^+$x8wOfAA zr9o$$KmyPYRv!^I#G?xh1H97 z0+Gn0*fPtE78d4nhGe0thghK=ik;!leg}lehVGm@EW}~-9ET1XeN{xq_(2}q8bbDc zwW1Zb3tc?K%yl7J3e!CDp4&ZvH=8Y;`==ckfVe-Zb>kUDlu0a<$<7Gm3*T|O-v&q-WPz#Gyl-cEUi zeUmCGd*hc@jXucs5ge3Z;H!gkvRDvnIsjA66c?^1u*b6k14@GR;CzgBhmnRcNfSw; z7Un7(V(7W)y6H+QDbOUti`v*qIS-V5nh0B5Z#_ zk(^2Jt_T{PdsN5z5?PjHe+f)B3B{(!TGsAMW9&u?I>@pf*TS3ktsG90mp>$GA9Z)f z#dBy>YM@LG(IQik_F?6{%b?=1DU9Trz^%@NOk0YPoHj?9L91I9=QP>~8hyJoZu0oc zT?zKzoV;6TSfUni>>J$0a`z&Iq@`}fO{JyoM#M@fY17>8eV=0bQB()bCkf7eX^B>| zQ=!(|1iW4=Uk-DhMO!MJ5X#_|4r9)TAaRH*iYT1)a;({_l&m4A>x+Y!0U`#AQV@=5 zVlLNbHXFequonAK;!2OAcl8WSO%3(*CMMIXw3ZOyt5~AYX3XxV^Msb$7|hS$cRE~Y zV>IKvXCAjh8~}4&k?@FliesfyVuef;D`N76^-_PgU)GO(tmKET-H z+8Qx-TE*8eSeFX)N|pZhZ8)06m}Mh)sM#Qm#%pbVfc#=Bq}4zT<>#y(tdc8VJL0 zH59*FeM)CNPY7{!yPPSJ4N=QBOPdM-F!v7V1`sS$;5Qgayl$!uj3@!&6*`+>0Pd&c*+$CPAE&<)OWO$W$WkHQO>MH z9>7eMR`!w5be*V>OPXzzUVeHj6p_tblM^VO5Z{xR=LpONty_C>flPRVhwofsZbcQD z-2F}CwF)&lC{`X#(o+iIa(1L&FjYpq%SwK)h6%n>e6i^gJ8-@0w2Q4&FWv9^-6Ds% zNeuVYr@&@J*r=mkq##-5#GhWs{1iz6Q7Dm^H_N+Pa}7$#H+-e?Lg+(q3ZUR=RpuPI z5(989j7l9Kh8bJlRDGE&1^Sd~jHVl8=}im)Q*q#&*aBRvZ`g@)N`{LhyOd^Y7Uuz6L@zw1J8lcj7e+0Ky*e%v2R_4X#Ig&Z5~XZ_5v+x z?Vi1Yuc15 zYRz?SZ0!Z482?*KO@d-gLO*yiw`dN3X=H*LgI1U=M$-5@I@*o>NoKJ|6x*6raZEik z4$k+CSeC|R;#q!Tt{`tAg8_!x6B=^SrmF^aWL?z<2@pu6}wIFpIi;uBD1-| zpSO}Dg&a@~7GRn*8JK(8-blB~(-3P>J`Hog0j z2=QHaD^gyrnEulfU_t*yf#-^QS!-CRxu!OHmI=4BO!m8iEX`UwSXo{roujWt)AZT&o`sC) zs;5I-m@4q7G>;32Bzopj&`VXgom8A?=F-4RRkR(=D?NXM^qE<#oho=WpeAnY8qI^F ztLV90+*b|vPI$?%`Z&U5_l-n$%caEfFAWZ-g4!1t8XA6um=Smkp;Bc4K$V9Ulzre- z)_>WryX1J3*hxLkprIFggVK*7LcO46d(f zRlQgZJ76|FSogo*gy?&$onW$GP$wUld1Os{+=(1}VeMZ_epBC!q5$f7fo*OixvV0S zh#t6k@~2PX6EWNLN+ws+@YG-=ji zryP;gb?mCVy%adkw7jr?RyX6x^T{aIDi1!Y=YNO8S$V^Dp9yTZO$zL|%wfWF*zftB zLM*4aC!CDZP}&_*y``RRx}uFmKo`QGgr0l#3ZlIO{=7y3{GQF%gQn|eE4ZY^0mOR+ z^zNzWhlI$FAPv3!^Z_i&4b%#TCT@=4N5>BI1^g7+3?T&M37qn>_?yq~=@m%h1Y8mP z=bR&u`%HN&;By8;JP~8qO7AD%E#&MzI3pte;Stbn0_gq_RQCvfdM7{n3&;z_?jodk zJ;tyDP}vCZzVVsPz&(@40Hk;%hIl7koNsZ?8?gBehz9ge84kY=3m!zk=RmV0?H!0l za=qlI+Vp4i=Z3w=yie?gt!E*XPWg5qXDx(M zTj_vTh|4njZ-H|wl5^je^ld-G>W)l3ESXSa(ASep{k`>tlrs=-N*6m*iweyP`5EyxiaU^>DTl<(i?K_OTqk6k! zCyX|=axktwerk4LzO$A)R4oUw|4!>b!VP7hg8~6PK>pXA&v(x#zLl-% zzi~RrNpG^>6=#Fp8Zg1eG|3qls2ywQ_MN%;KVjS~XU&K-3m6#YTm~4%Z0a|Vy{TkS zJ4kr_5RKnWG$gf3dkMHw>rH*OI2=t5pK=JgfuxCTiGs^WXw&KG7eYa6Wms{sS_-XG zogIq^)EKOHv8&GxLtFq7yC%rZ32A7U2@9tV$B>f=a7AY|`)U={J;8zlbz2aBE<5kG zIQ^>%%U2M(gR?e+?#C(Qg6}|d09Fb=>-N=f{rv-OGlw1)5>U7kNmEn)2I!^OC_qR~ z85$CKl~{m0;*(;WjaIP(t00=dTpgr0@Q^j+@J&ovmR&0`BpP9`ojSE_PjErJqdKE^%u zFZ*ha0lplc@$86=NtCRe zWev(=+t1@YHS<8|P&lsCKpG$U&o1DvdE?NYLQ%sRgi&06L}-Zb zAj6`g*CqAyz&_pd^a^qRu|q!sD}GeL$=-X6M#B7hnuHp6vI|2XlhT9*E$gT)^-?>X zWwz)y%{>eXUW6S9H9PX&_r}*=8F03%=$gQwTd>&CBBA0ICkAAxOm!i%ztvnvcJvh; z;X>at`Ee)4>jI1?TzFHeV6o({(w$U9LS#dKMzD`Gvx9$2{o!;By3TlT@7!wf4y}-M zaDh+OA%ri+WoCq$p%o#9F%QO9wF8CN$93;w`a8JQu+96>LATUeMS{vpvZ97 z|79A%BD2m%YH7;cJ~l#KDic3(%Iqmnr>II^oZ))G^M=qB_7F|fO=;avzn_KgWHvX= ze|A^deYUM%8U@{ARZiOPII1W|#aX|n)F0XNeFxCdy?_XUTM&R1!ePV=7#xpu zvp9}kwJp26zi!cbfFIT3`-%f;AXy-t{fm$kmKk_DQJ_7XMjV&wQGhA)v(fm#UjoFv_s4grqow#;CmM}KtL!c~mcYbJpO}Jr z$aoKHIUzyS6m~oBQF>PzH9Eu_AN5LGj&9Efot4=aUm7OQAa)SSTW}0PpD^9Oc!XhN zXA?t*S|+Ap`fLnvzL)dd{Gf=EX!@yJt=i8{uoHP7y_@Mv^htc{6KX^HgN9#AuS9)C_ z>^%i4Nwo*R*967{GW16h8Ba5gqx75W+3rc64C7Cg_Qt-eO_6EMqlxsBp<=wt@foc; zWExfSaE}3!^CWRRRneqjGthmDBGhFnz`lf4KO-gw47>}hskJ?;RZno_7%SnEQ^d^K z<8RcznH@tjZKTXFb>o+O^}3RXbhD$5(Z}w$pYx-`g>_VVWzzZ|t`THP3@C6D!Ng>~ zH-U&cC=Plgs)Gc5esaNYgbeACdOttYZ*Me&LtaDnN6m@w&KZ*+@P({%8>8O*#gQeP zrXpt=A#@LS6s{(u(p^w9#0OJ~buKgo_b+XdWEls8TFRM_3g@@*XJU?UAqUtsS7e1- zv)6t`1@vK;w+Q!*`kR)T67Tp=U%lKIQR<9MkKWDWo-=XNSX2jwGAHgnnYw9)V2YErYaP4KB5S& zB)h*ST3mF?D@;ASV4RuBTg{zNR#_r9PxoJd;j{=1w_o+Nwd7=JbN9#uutK@uakb1? z@8WYVuWNk%NAOk-b?k4Do0z69`yN#AK7+n14AzIS{`%bp zjmTs=xpL)o#9GhimNZ!8o*OT{Ls+?VAwh6@))HNhQdIN29+e`<;b zkRyEJG#V1I$X}-2Fj_U4zb5E6RyCaW`^bfCkkEw~thMxVq2)>5hcn=0QH>U-H?^qM zGjXLX_UBQRl&EE0uy5pXuk9c=crhZpsFJc&$;h;ElE4a?GbYbbFzr|`J$wDMBI%pz z8Vc?GFp56hr3>pO?({^OmWK@4rR`VcT-G0Mc!hgOS!A^G=+%^PqMMsh(fly-${L)( zY4Z>;1Hlq?dT@9M`=K^@Npea0O&mH9?+heuIp$x)a3IXtD<^cOC?_0xG`#mz9K`pr zgmf~;zy%wLucN$1bzi|3;V_(+cmN?h9@iCLvf<3&h~GGqwRJK#F*p3Dk@Wuzw8(#ewu@HR)M#93E*(Yp zdm$*qVAep-3p*tbPuqo+tG@q}O7Gz>!FDWTwuo>d$jLA7Wq_k>*Im}x`IO$4FVHfu zo+ykQZY8JN>F;R$JSyYbia@21i^c~NwOd_sqA%emqv2%v5UJNwf$3S*ha{@X+0QO! zQQWa5xT3(}W{aOVpYm7>H%1#zjd=cWLhk5#M#)24;#6U~2j{hrv&ZC42lvYWosDET zDw%^ZRp#U2S{icD5J9E(UM}Rkh=)1`Lpju-A~Of7Sd zYYRO%a3TBky#flnLm4$hH#tnIK046Yr{K30vz$oiu&ek@UU)NM9vT0+_;>_XOvPwg zq3WGC#`T9FhJ<5Xl7b56wi{f{hY!~V34u~jEX~zIs|n=Y^%XqPthAXQYA)m*VYh1} ztB+xZWLRRPyy0e!lbL5ic^z(4pE$R;&tiI+&S_7|o04*?(5#`@8GrybAT0;SOug#zsYeD%^H16iaKgfv|k zeqLEtkwKb7Ypg4n8#+MCC}%Ay-$~EeJ3FVlw*Nj= znQ1ziDSUst#6tboccOsl_b~MzpDdP+bE2prTPm5$3Q7I@5S{kc*Q9=aA%bT=#Cs>^ zgS6=9d`5;J4N{OCL{qSy)h?u=JIC)K@5P*7t1H=KxE(eSS9S2R?lPawvlq6$-fuAc zRJa-92M(C4)9CHok?UJ59JWV$*jAg$LoJn_(U%@hL-4zpZg?a;)aNGI=G5i(RI!`E ze`OWl2i&glsBK#n5niZu&E4oW>q=~?##pQ+Obz(rz#PLU2Pvyk6@nR8 zO(JXNvbCJuvbUh6aZFa-u&;=zx%V`Z2Cq@k`t5F(KM~`AGizFH_%e!flx672SgXfP zmFgrY!~$j#vkE@j?hm8gN@d3raRN5lNL7xP1Vd~xHV6uy5`%uDf0Y~8n0BS97&9#d z$x2BX*R_|j_$>~tPR~Yh^2$H8|N9lmVqq_LSD++jj4mby^Ic(U)P7mf$=}GGJ?AFl z2qt?JT4Gr0{^YF_Vt>KR3Wkn}Yy&uRlX)r`_sIVl?j=?C`9820qttuzWLFFPFGYi0~6>`tIrsDx15ju+?D2nCd3x zF={1Ef)00h@P+&1Tth1GWO2}p73_V!mgrU}ydk`HpLI;y^&;%FH`XYI+Q2KjWMR8G z^gi!Fb+O_ff*JmMxTeQ>L#P_*E&;ZxY!Ep+j+5z6BK=fMqubmvn{GF1g{r&;L;c|g(i@m~ zc2*ryDjf5wYx<{7k4CdR$X;a5zYQJd)y1Z6&yQs~Ngg#n@KMwsq zRZhE8a{1VD`7q{kq-Ob&3%}4Gwq&PsBJc0o#9nP376GQdfeYYrXA}tD{qF5GLMFs! z@p2I<$>$Jx7(}IbzkuJ22$(`k!%GK+#@%p*rfe=S>?txYeE(zKfE5r-y8oV4qrYcW zqW>~){Oer)k0CW#>3`bfks$?>!ls}M`osKURU`2-`b$;nKr|B!_R5(~_=hB(lwIW{ zKS!%*-+}l>Z(X92U_p&$Vm;5+9c^=%?(UYJ2!LF!jgUAh#oT`6q3;m(dC847n9rhw zoF1ySleuq2NE2jqHE2U0v=lC|C#+sER(g#$sC{c2i4T<7;^C{JF?6vmFc(XbO?64% zlkWq_E#}%gQfQmXsXW2P*kt4wsT-41-d1XRh&; zyZ$3q)LZ59%DARE@crWS%D?Ei-ly8#xLAcYF(#$njMJ|D!}z!gTG_#GTv1(R6_JZ= zOQ*CCje^ABT2*87=4UlmO^7lRg`XT)+cYgJhkOjL;8&706D7|^ss%Stc6;aWj5d0X z!z$>JB}DJ6k*rO}>zHuFXwKVBF?m&+SfmqwKQm85>UTz{P!6Xf!?g6*dY}^sakvej z-C@3f_o?fWxMCIZ0|YFtj}+`*d7g+J0pd{PGcx=jd4HSOSF%Vz zGgh2k(etWQt*xsTDS2cpE9NPAYQd9(git-rEE`=duhur7bRt);CZt*0Gi1U(Z^ynds|*C$Rq_7((NwjFl5V z9oj#&58o|BlzV2t$WgiG;Ip`K2iGh8HC$43N=M~Zj18ifysvk+a|YA3GsJd7GxA~$ zz72MY5@IXxQH01Dr()y(I@(d8ZaV*=zfbpS_H|PhW zB<5<+(OjOdx=xX?o&a%VHI^R3oNkY5m_b?6l4jC3Pl>0W1NpYeGW@t)m`%xE<@tel zHJ#5j2hCJz9NwFH=V?e8Kd-ciFG;mgG{Ds6XWFQ|$jrbG)FWFT^yfpY_EWJdYfS*%5(mt1F@)li04Kl>-nzDO3~Z&CN*h z<2z9wpN4j^K!|Xmk?2Kx;i4UHF?kH;s!Gy-WH~ywI$p^Wqrr)5<0&pFpdhAjR;opk z(o1SqihojR+F-0?zmTVe-f%8^qt!A`3u%nVuCzTyzHa(u(AIB}<{@U1D<)^X=s}|6 zOoFtqtD`v@w{=;(bjs#dzj|}NGAqW^08dgxC-vr35nZRKvL%n1UZiTERH^oRa9E)l z8q72&{&$OgVOtS>arY31j z>)^UULPnK{6qLEBgvp+&N=ZSc%N}}^#m*aPlSrQ^Uhbp;GZoCZp3+Ts`(3ajF{bt) zQQFLL%D7Dg8!HKrG}As5FQ7$JMnKi7e~$iw%Vo8sND-Zq2?%Tvv1!eIXg31YWZYWU z5P%9l^Xwn{ROwys^anV?svE z##PjxI!RlygmGBhkiCnVe*uz;n%$kxocxEf%+=Z^xk4JlN_wO!VP1o+YXr9opJxzu zBFYk!hoOp=E%R&=nZ2g#=hKi!rQ3K|*83Jq%=Uqky!T8Tf!MN5v7jkYV#H}G_Uvr( z3U<3RBmIe-Q zk&1S6o>L=y_xR2eg25#Qd<9m_U)+Gdl=5~g4&Azqyk}ps=h=i*_t7#70VB3gvts;+~wC1iV(uOJ!;{M$m9tv(_!74V(E4!Xrf#&NX~i+U_JIOADb=yFL! zI=BcPO?Craq@m=pqv=@2muyjbGq+e}`WDPZmML8lOork9G!&!dG!A_X1uG}3ZZ69d zd${uyrhs?CaIFbK_EI0B?N#-mtbwf z2b1wq9Y3uXLvp6y&4xv6@tP$fZwEp2gk@3dMTtyF(LUKd#cB|mkHh(POPCyfmXW>V zgz@~27RD|;_oMofgK~9nmW~jKi!CX#W9B4FD~~33g|>2`YKKP^i6KbH#_BhCGCGq} zJ5)m~Gv8d>F0hSLui}1?h-gS`VqazQ9(qaqc(N*0V)Of?+%*Cw@W4Lj{+Uvh4U8{bPel(XKp?eU*13F^nKkApoAw`tEKE( zY4W0^)s{`;p(oirjiy*pGp%v^*rH7fepgEyqKt6L2{my<)1=7?B6zKP!9!mrF27tN zAP9kVlF83ra7>CdmaK9`%}x@NbHR~5^`tt=<8veUKG9Pm2;u3-LgNbB>%>89pvdOjl2DS`1;1+UZQWy*vXA;+qP}nwr$(C zZ96x%ZRaL8HtsLU(lw>CB@8-Dx=7=3;5F!0F65*hKZ5G7yamE)6atdi}d@nnGj76FS^|EQe5v zuVWT)4n2b>YG(;$p$-||=!nQRuCz@z5Ya^0OpwubD?w8Q2+6!0^Yb|;1q_avXu@S1 zNgWH}GvJK?YA@uCt9a`n?Y0y_ka0`*E;v!zDqY~+kf`5Ry0*vtLMT2pM_!Tj8=^7{ z2B+H|wk|z4p&B}|q`DulyP5OY_Fz&d71I4j zJWualC@UXnp%UNVJxg#LIeNsc9gWMF&-L#Z%|ok0+M9YKPP8f)p~1C)d0(#Q3Ri`8 zVcOJGPIO`NA}%eHFYTUyB^=SMK=(#^V13A#cshG2tLW!H?zp!j){kfH#p}Q?D z4gd-bGTLO=;s64{cZBBBb^zsThS44Z{rR9;q~Noh-I!xTG8?dG2O1n|$=%QTI}mi!$jdZx}b2^tyDkpn;m5${VMYfA`0l z@)CJyNrnkWwm~oA9C_Q=yRP&Xmfpy)IpL0TDV!vvmwfAgrX1D)M70~;0s;SeNjL*A zF*Ya|1c+}x|AHBC)*rx@8Djq{4$*?mmx~BON&e-*v!(lTip(%1>#JD4~ zeRKTC4|nX3I}EycqB$e}$ljZmTt8oLiM?LVzqBu`KVA-h*B5CGKCLAnf2a#i#!z?$ zo|-G{HHd&^Pp6$!fvSE2Nl`n|!GwTIg!7)hM-Lmmp(vp;;c&|@x))(5#fG59PCkVx zcDZ}v-a3&t-R0cc8&ks*EvyDxgrETRJa?qHDqNY6{5ry^_!=M7&4FR923zvBE5>c- zLbL*IQly?XaldJHQ~t*cMi!Q>ITSLd+3v4mP@yTi;>ikAuxqxSSXiHTfnLPP-%nky z%J7ozn(|q?IJ6?d=NW>bDJGaBuCRLIn!+LajUj#bEtuX|tI2nwo`uCGmerR#mbzG>vZV#&XNzm&!{kCh8~mHju?uVyUFEzWLk2Mhv$W)9=PaZ151R0po3dJ#*mRC-zSDXSG!NpR8dwZ6uQTe;Gw)JoAarFHXW_E|0K; z3o-K>1<>9`MiaMD>g7cr=OnZe*1H_OR+_C4A<1Oou-q4z;-SWig zKJCo)jENH@v)doG$NkA1}38SZ5b!8e?>+Q)VpPav5ZFcVospV^D%hb1gn9ZnDQ$$!*ZakQ9QzRcWh30dkaVc50L$WKso z3Dk5;%EBnsxg#TR^7jY+!-yd@Uyq5g)-Qx12DXi`+(C}whoMRk}+J_=QS zBPI;F9SQuu&C@^d7#dAPSYzFR}9U+uV?aR z?nR#nT8Rsp)YJooMOqTfCDYU;Tdw(ri7OSZkEO2#&%7rhKi{_45|M~{k+d-Cha2J< zj4O?dj0!#oDs%@HLIJgx?MV~atFgPxv&_bD@%8zV;HC1}{ny99Pwlz7n!xMg{1eDA zXbCsO%h80+mls0K(S&a%OkrffunNYWcz@QEU+N5*`bD!*q(`WitW4ef+qTiS!T%@A%p_Lk!O^~Vlcg@1j)#~2u8hqX!t-E@3}gRSz{r%rZZ^hwcLHU#&p3o@CFPm9^Q6HX-f;w!I1MXG-% zt$4%T7qx9m+G~#Ph4#?tDjr_Dm9@gh)C-9-Xb{cf{*tk#BFqkU%XWAuApEOjkJ#ky zQ#Q|&cB%Fw#a5^&Kt`002QKqcWzT=bu7|pe7-c#=FJ6ipm#wmrm~iPXi=w(Rq4_Y% z#*lc{=c&;J<0E-UNNI1VGvQq_5$~m3;HG1V^%S(GBbTJ&#$~&K9yFzXn)wG$rrHE6 zFj^H9m4;O=#`@T0$HuNnCj@)}l^p^M}>6I8zG|Cfs`evgiOoou!+Zc#(cH zP27n80$EM9sm(B2Z9D97)VCzNB9hmR;)PN-40OI|EswWI;yox1g z>d!RkfRd&Cs;6JnpDEJc#Y+Q+0@Q#Ee1pPqjEnmK)N`_jPXa#}%IIpOoSv71_H8t!sav-o5EN z@*Kw6TFJ04zz|i=`e*AOzhxnW^DElVnGg*_SG2CBQIVkKlX+g~+6?Hm9L3zevVE8A zDLxog8!KIsl&ur4-F_^SG*O1V>BXF?6SNbVhT`)nN&3GY2iBFpXYM*wuBxRGM{`R5 zZRqq$8(SAiIAx4Do44YM1Pt|Q9;Gpico4;|oVZQ)fGN{MyM5C-{6&yZAwRH0+e**B zT+B%FQ)%35mR<+)O*1lBHLBt7CN^X`#*hkSyII5|U?%&;y5tumQpK~>?4c~RyU3CW zvOJ7Ch`hJMAi!G0qB1CzlrAX&iBZ%7xGEF`<&McA<2REos65WQ*{k|p3#DT*G^%%b z?VR7(I_ovmJxb0b`IY_$!J{jLo39(%8)a!192B;(6WaLdf?}Exn%4;a$z<~S+B&75 zyCPAJXW%rAV;eg+g=KVg7+OB%?KJ>BV-a1T&Hx-eThkTy3$ z$dVI*svS4nDrC`_{gc|DaI2gqXd#w$A-Ek4?GVMP9jHU-=$nG43~%r1Mm5Yg4w8fJ zIG-ZRBVp{XU^&r7DJW*GKsk3DPeawAYlK5oO^9i~YLpQ=bSqO))hz?}+L!O~a*n?Hr-oMDbU6C9KgE~>oRCnYCnh&*u z${7c(>Z00zm<&$EtcxTg2-zQ@SXBohqP~q+yrrai(FOEji|a4|Jz8JYBLVOax5jTC zGJu}LS^jT)q^b_)Z}Ip5kaq{eHxCknqy6~@hvYY#iGaYveTMtn@i(zA9MRIaw08~{ zK+8lx=HdRY9I)@%J9KmYc1cf?i2$DA0G3|01HGBKTXpDL=@B1}5RWE~fsRH`6w=;b zgnRf94lxlKrsJ2aR=xESy*QY?rL6jszrCfLFU^QK^g9kbcc)@CmUI|0-YSLo$fZ0z z2)}u-zA`*LvF9eHJX6SeNg_Q`2bT%S?v`r%D`Y z`VmO@6&OCr4JRHrGy)8W?ghdD1?F!!$N`}39{>ilq~^5uf#l*QU>$8cQV@_aB%)XDfW3Ll}buk0?nE^)(ajigEN6lF9QX6c6>I0JNHM zFXa3nKnF9R;o+$>dpE&C(i7nb4YRkV@3}cZ|AQGZr5YdC&jkCjka<6c|D~B1Utl4tGO6IaoY>czgS5`&&r; z97KAzv3mQG`+Y#)Eg3Qr^oK8EEKE-sOK$ZU2(C@ue_n||Ab`O1?Kb7@7WQs`{64`2 zKp^&&zyx^p0yLPdPur07QY3w=zZB8%!{b}(f&uqN5Cp@D7mdMMh(OPD z62h&yj-Qd-DcyfJW&8&QWb5>~!uoUgn)u<5{0|(r|GOmpZ;o5)|E4F6e~S!WgVWV% z)NXApp;uq4FL(3@YNEi|r3iog@RM-Q513`#Euwt?i#GBjCBvLA#LAWg1O3Z|D`g)r zpF4f?N?}is8RxXeVL~!}1N-(WyTp{sGWoZ& zniY3U4?N6e@-B3(I;g@&2D9oKJ|6jOn(_BGig**#7jzjV@v!7!cxq)k^AZE(R${yD z4MaLIgDSi=vpXlSdP)5Wm`mtaV8h9{0au!KdzIHvYty;09p)~N*<+J4-$6+Hl}yl8 zrEA7FWvLjaP|CK9c75JjdZdppNBMFSlbC?s5cu zqd;1X)cd|Xrwa5;(DbkX1qGGu!-n%l^CfR0)%_-jN{sJI^n57UYri0r@0lHl3<+ju z9P9jr0hEqM_~pLxbW6(AjQmdvR=K7nrJDzZs{Xd`Fz%n{?@RVu{0C1_&)^DAbR#?* zS^jSzCDFMX-taZdwj$G}i1ATc_IqR<9$10@NIjmOsGK-lAfO>FARxN`KVjxJwC5DJB>9T(=)kQE^~lC zGc#E%E*b`akEjqJ$J~vx;NyENiZ5E6fc&`mZovZ=dS85liJ-DKWA^@$_i1_GQE+ZE5L@nKO(^8=pQ|1#QaGy&iCA;K<*nbN5I6+_jp-?Cm4anv49>U()a0z zf-WPtfZNf)F3_7mxZgySK_~D%jrlh>&VUn4p+A^2F{hJb@}Lwsg;p*n*W?*#1PTU4 zS1x7liBs||GI(*~2ZosZp?UEJRe0wCsc*QDuWS@0k#edtGphz%2~yNymt!?&w2e6D z=wjCbtvEB&6qWcN;TTF2V>k*EF0AoMPx<&mQxFQ2ykcIAwMZ#hRZcN8de=y5a&i~u zuoUZTX{NLkD_Q*MvWr~YKd3CuMG`Y7e~-Di2C2+XhqR2@QP^^44o&M-W{!RPVLwJe zDk{y4i74E1X^%{qC@7qpLsQCFX^Qi0>E)fHQ;2SM@;pL1xX)u$8#MI|LKuC$ z3>c4Y@9UmwzFq58Xj9^>l3i{{y*y2WhCUaM?L{1LgRi1snuinB$z?U?vmJ75H-ayg7` z27JdUxP1q#nsF(7lV&UIqY@GB^KN}IF z0S?VS@Nb@XrQ8}ScxkR<(qID$aiQMW0KERfzcI#=wbLyNWp;V}^bQ7u%{%KjFIA?n zc3u@sk5R!}2N5pg?A|iAf6+jS-9tb7xU~vv7*|io6*uiJ{XQ)sJee$CB9-PesVd?; zlnOlP;9b0{ORN}0Tb(Ftx#~%I^k=Lti|Ce5EqZKpotSy_X>Bi`Us?<-;oOpsk3ZGy z*YBG}=dev!NU10#tDV}OU)mr6)*bC*tVNu8Ce#?MVRYgiEFEpk``SvP&eoL-JBK>B zS6A@woAYH?hE_IUlQCgKN?NncB2V#k+OR1#55{?y`Xr}Wy7rM)pJ>_tmeHu9N?YWf zU!EW=?e%7G$eN46%jB+KUOraHj2s*~n)p$yolz(?a&hvQv$JN{ z%+jM+E$h-0!Mm5MpvxfD1D9f3DLRxekr%{hTSF&ZMQs!FK8TX0l00rEcmd7LD2LJ>+MS7iwOb zf_7A*62{*XEZaLsKRG|X>wd?Ujy$7ouSqaTVlIWd*h!Z4Rd>L%{OtW32mORS3$q}$>I zZ9=43`9UqEP!E1S*ZCcxPa_O=U{zOsS~ALUnUH_T9K)&vw+zax)F%!(UB-lTFdsyg zgpu61_g@1Pw`hpa&|IRQS>2oKv%H44zweDSKZaCbY?zZR(2U5Th+6>Pc4m<4<_}`uDmO8z8vK?9@vdhVVn9 zSAMDYo0p*$BFCa0zX@^DIqeflrf~crw5A&$A-rLA|9~Guf&Vk1)&BYpn#W6}RyFfm zI4=im_k@#otz@B2nmT3*C6B0~m-y0YTU96Zj}7${+u2jgW(rxj9Qkqhw_N@!k-E_y*0E1 zp`4Bpo}H^!2*WN?>y-S;2Euy;=|-ESpu7cOk+adB8E=!yMi1f1bwvFODQs$${_%64 z-_BM=MpSUL{v1jxV9Xr;6-=Id_la3va;EmYE985)XKcnVxNQ1!>_`9D`Dit<;yTAR zw}(z*^{S;zd#?{nka_>+T1ralo#D_>ysR+}_HU0!#Q3ffb1k-ev=D+$unZ)V1~0t& z%KeSec<46~F0Ywp8NgEta8cIplG)pS2^_KoI0KkQ zJvF4Jlct6q&W#pwT_M?Ft>he)1!DV0Licx`V)^fzTZwm>1Rz?(Ma1)fM9etx;#*Gb zTqH7S(sdn@#sw|O?wP$mD@!L*_-=|%p5^vhE(Zz9CTyq?& z96sF)&*J=a*~kY!d)O&ryuu$6V}Mz7K`20bH~Qei@cgO}6o~PZq1|_j-OI;!QNn?R z6C7a>gB7hRe=Rh0bh`KfY6ydr>hnBphm2+));5)etl8d!QtI0QMBUQw( zoKHl8>ikvfv6`ehl3gs1WD7p)Sm@?LTZ}J*NmnH17Oe=zIv1Qcj~A7kqv(bT<2oZ2 zITBf1w6R8axVppgFLN}aL_NGc?u+Y}^r%#!n?SaK74ryE`9W{wYc)g#pBBK#t|Enz zNW&3Xo|uQVT6Hl<30E8D4h`oA4LcyYf z0jibbYSUDf2+y@pkZyYExYnSKOB~M_`XQ%_=ofEBxky}RzuG?Vt zHM&RUPBF6-A!&e+dL6GXo;N5#&fyp4%qKQ^?4C@!V-|ABaye1rW+Tc71 zsTW=vxa}gVY0|>8_BMP->n;?>=$3tZ>tbH+l&*%T7w*Wb+J^LON7l+7z0toQ7!O1o zy}xDJH_C_h)_V}Ik~smDLkX2$Jyj__WVSr2oe3K^5d<=AyoE#Z*8U3+!y?qYva>z1 zdA!6%KB{5kk=OU6Jmk&S>PCSZFStQ=4KV*Auc1-4{h_xNR5=B0A??IpRM7HE&ngbA ztR7jY0kPQu_AGZ6YM$}g1p-6A4HOT>jD03YUYbL%(a`*bY#XvS^ivx9>W3Jjt~DeK z|Fyvk5VdpI77kPmIaIyV<(%zT-T7F5g;b?&UtHXygZ zs=nbg^*OXCebo;MRKA0zr2itb{jM0276#sX0EGVHi-ajfQo*gV7!sQl>X7`ZgokTn9iK>lX z^=o{IJvgbJObq%n2^g?PqeGrel=`mYF{AfKiJncQ`m}Kv5o0mKN27;mK8KXAk-(hZ z9I?KlEq{!pHH4wdG?Jmo2Nf#=lNhnehfp)2TME(9lbz(l(Jh1BInlfevDk;Oou~0w zY4lER0VnZ8Cv@yfV|)cKt75J04aH{eY`$XJXuFAbY`(+k68@4Hwx*Tk==RgKp19&S z(Ls|-)E$|jEbvD8-MZCLUI8Z-CVo=~n#dMsw%A6aK(Wo#y_2Pk+lr9@e%p!hMY!LV zHFxUJ*dwwt$#A1qX#FC@wRL8$$;q}=$+$Jib|KW0LKu0`wI_VmvxU7;si}OBS)DPk z{|w=hZSZ0i8VSZ0wK4ZKyMDzBVQ+v@%L+bZqU2PQYcM{Z_$ks)7$|H(q9Isr-p}T! z^|Y!aEXoGrN_zyRAY|Zj>xL(x(1$=;9$tlBxMr^mi?r(EuPvx2&bU&J>TY=gpvxwI z^TC-H2Ya!mGiL_B`_35yuVySoKDQ5PZqKf$i`sb|OuWNL=bgI`JxQCKk1m>EMOdOJ z2bYaO;7j8wmSn+@2Y_^deNlbL{Qbq-Uj^*$7?Kgf?Pq$u%U@%e33Fa zO*yEp^ad#h&N|$v)?4BAqlt~px{=D9n;i$;ij=YM1y_ zSk%WzcGR`X7{FgJ5$s$gl2(E9%gpX@J1-h>ff;@;x?-m4Gm%jUzZPhV=^T=W1lxvUeMCOzdbUzq9;`wog#Fjwy7E7m)TC8;2@)_l9v!cTbD|%|PBEyRz3cp+l z%1a`OX1y}Z%ODEBT4~42BRacYiQ^R?NxfP*%c~+vw@}&Q)gSp|sw^;%tt5usmi;Hb zG7NhZy_Td}zU1tY7nx1Br2O6%S*=j%*i$}AyHu$c86Blfvm_Ubhj6%wrQOFq@ILh` zM)#SbHH5!Ri6VUva@J_6l zCrP&x4*IeDUxscQiQ_iT{13$5-r|241S2M_3MD;9vKHmem>rx(7a;ue6bGVydggo$ z1^ifZpN`00$tgIV!2GZ`(sPi7MhvV7f+!3PTH@EWme*9eJf;M+B9^txbLER_W1Ftl z%O@5@1IhB`r;`3FA`wdjUZY!$9OD}rjQ^xV_~yg=$j1D!TRNC|Omg^uYB1H}g4)QV z-PqRWn5_SZ+9>?lN?s9n3bGA^-|lg-Kq+k!W<$9ajz z^r2|^2loP4+310LF^ak}Q+lDA(O2Z+vJP(>`DS-m7XA9595DQ18rl1##Z{XeWJ&v! zRRQ7yy3}?jRd1lE^C2v3^L^yJ^~BlXW7-iK!;6+b1^}O%*`#kxKyuc%|MwiiXoIk8h2HMB1z>Tv?$6oh^Ob4Hk5#%^_6UX$13eO<%3 z*VhfsvB9a|U85FQ)V%SE+!LGN#uTS>8R+!KKpq66b4Swe{PWVv2_FUG-HU0hdQAehDT4{ z!^^{Dtv$T?zM^@xv6Vzndci_tYB z5V~}8gUh4lyehi!c1Pa2B>2J&o~YZSz*ndW#R|QFdPi)!G+&|qPMGUlVD%oR#=naU z6HLIuaZQ@mnc#=x$yM>{FrA)`AjZu)7oEsnc-#-T;w79?j=_27?-BG5JTVwteFHJL z5Dv+(Kp0kgpZOFeD@1_Pm!XdWVY@`pT@>$T%U~Q8eQjHhusHyQ{)A)-! zn{tPRn2^ntc1@}67PKoAFNo??<;sQ=#mcG<99hMF#wd!(-2aVvC0)OT&*hoKJsvS- zI$?@)9hN2!CyD?AG8qIXq)TM4MG!)K5Y_uw+10t>#<_%ify>bAE8~^CPcayaOGo5~ zbX9~GDoDun;4e^HbZ73UFIMk3bB&>Q0f_7-Zor7^FXlW9+p(d``1=KuzSZ3ueO;(v;Rzj=-F}hTP1iJ*cS~>xtbW77AZXUh;&vwq_t)DRRB`%0z%sYj2nnho+YT82=WSiBN@cAt@So#821WH+Y-9902P-6f?FlpU)5mlS%|;o zq?I`b4NR4Pl()S+`$y1bJB0Iy+{7>r!k9H!_m84SY=bpTK<|mKMevSBm2=XxgJwF~ zHpxm)l)Gl7>)3Tf1JSpEwsK8dX7Ai*%=b&$r%Ty&48yb;+BQ#}bLbkkYNH7AjY02W zhRrpkLjtvF5ED=*)E)>6*I-UqL6)pRPpnCxI>ouY#MB8bHstYh0Nn2>jqatMucFpea^R~^8f3BY#|OOyEa;GTPh@oy3jKVjZg z4D3h*Nxue6+i#fXW}%{+L=n11;lAubeG|~6;w+KzU)Q?f3zBO#1+qR@n%~hG>_ZGS+*LRP^+0h*Qk;MvxLE&~6 zdXZvpUmK!(z07J|_Nfht^@d_cfVaSVfcRrB*!H%p_D0GN4oHFb#UA)|g#*4vjD5cb zK0JDTB!3TzDZfr?2fw5?8B}l_aFHNmaAX8{`2G)EGbM4wPwz+5z39gpfd2oFYpOZg zIvAP$w-Uqu0XFkAp$&00yuJ$5mn3VFTD9@qk2cg#M3(+)PCg(NG|}?32b1D7_)d%= zmFy<9zF2#-uv}`A11BFOf`NoOW|crvbw+G+BTj4J@d%3KgtL>Kl9wtL%B7^Y|F{eQ zeST$auHIbL7R%xGdEExwe((PbTN*eNe9oR$L|V4J#pY~*NeQP$6*C_?t0Ymn#~>?d z%U_17l25(q&_j3R>;!u%4#r)frxjPrsy?m{r()lD?VhhbG}Z@ai}{6FBEph3Ey%%w zsV!KH!td-*Mv3wIp)}~v^pV9HTV+|gE)zSokY?|!kfuu+cV$SnKCJOewR%ZK%In`S zpy(x^pZTdfEf3FH+pjscr02Np>+H7bBXacFZ_&0( z6(>M$d%c$JTpb+R?OfF&_KbksJiEB?a5z*Uf*L3Kh@csFYz+5xIJacv0C5caj`;Zo zekOAgR#M_3tFk$&a@D_;RNw4(bOe+X;=GWTcza6k608lGAhmH|$6Z9@$BVHEeFYiX zP6^aoc4u=kq1_Xx1&_CCt~8WlygQ70DiEVnq*aI3q)CrZW*$GzEZRXbX z6J=3Tc+?wux?-g=UMC?38@2{WCojlYG0NmH+cp}UrC5{#0L+IJFK;_jc!keX4dDi= zcwL#iLSYhhCuQ<_xWU9mZ7J2Sg7s)<0~OHX^naxpO(h@ay&$(UBojkmm3^v5tcOx*-pP?lk)RQ-=4wM2z65nD2UcEo$Ll=DlY zH)|2Z@I52y5|7b_m`S{lIoD-+s{XaFn!E6I?KxZ|yjCw3%Fh$7R+dAgL^bl;?fP2q zi)``Zsg(~6>f>byq_3=|8m->>vlFGYH^u4|6R{ZqH?V6lQ=(~=WGjh=RI=Mw;_nh# zy3M4>yqqLUbA*)``N0mU$Xxmhjrw)|a{uk9iYF}}R~2Ohei%x}jjl@k4{ZZ30wYDU zRw4YbzTa1S10!K1HPI~nQ^g5El?kq`M$&$eVM4T$K)M zkqwO*r};VRlP<5F^Nu@|2SN*L0m-0gV30a0BzSGJXqHFWeU^nutiq$3do55bd^KJn zhq*94jjk(M$3^OZ<(aXX6d5K}y&$MFLu!qzyUt8SnwO`z9K4O&$=;ctAeY=NYNMsg zjNjWRfik8d>#U(zP+H6^C7Rvp0~NQH;H!KuOO<#*y)UI5VG!E*gLFHQhTR<%K%^G; zI(oFx2d=?GAh2zjeTcgBTay_Vx~VbV%gLT4Lqw2`)}N7w*~VSEF9b%*NKH#A0M&dR z-@z=UJzbc2iFZ0`+c=6U?7B!aNA<`NOx+}~r?Jgn^j6eBbdlA4H(!{Pp2Jko-pbI^ z;d?>elC}PB^yrC0?sy=6a4=r6(yP;Jy&hgg1&6hc1raKj*9+NV&3t}mkNK7a2f~8P z!LoiYQn>5APv50{z11{A6OVNU*JQWDnPS_973{ zdiw@wSCjw;3g{s7zoXnZI32J#LG$L?CEL)c8B^FUwk!%6GfldTvnDla!)MGKv$BVJ z0Dn${@SP=>a<>i)$`D1v9KBjC&!t@bw^m&h~-4pGD=1z^B zjD2EfTb*)-$bU*+sEWXoPpO^>wDL{fIe)Tx%)I4!31eO#;`kZxQ!l{p3rX>lFSdE# z_&FGuPK~n6&m)*WHG371Z~qJ`n0agQ5-->Eo>}zXL!N^u=q8RrUc>~a&qHz#ncfTw zArlCHP^yGB5{Ri+6*qxGX)ii+-lh;fcC{PR-|i(uEN$>VDGs|VBCooV zVXOXuJKl`dU4THTF0S!Ov!rBiV?a9medCD#FXkyOpY@`{iYh+<*niz@0A6S-^AvY1 zySdSg4{B6vnq@B<86QF9FZ8Y_M`1o)Ja))ch0_PoNw#5Ey(jWD9YW@xcSaYHHfTK_ z#5Ib?mDh-!Bc=O8-iC}%z~x^P-)|uV!r+?;9fFfD=7Y=y<}{Z4SO|C;y7*%0a1m<1^i<^etY2QOt_B$U+eZ1l|fSiH+?ctlv4fhvu|Kzprz+t08^e8BsauO9Hgf;5gZ#TCyvan~*?$(9hi=EhsHhFPh`ttnmJpNh=T zN7=Yq%t51A?Vd1OVqb8E^R+6H{%Li#3%S2$zhPm~NAcK9T%K)yL4H_}3CWsZ+}Y!++}9 zg8bUDcq?N5+A^RkGChlXz<`n`DD_El$`2sF12`M)m$0q`qqdc zSTkhkEHy4`{cFcVusH%U|X^}2FU82vy^hzOx zK{TyE9zBkY*T}oSV`&zX2$>_Nq&pP(41uQU+uT~34 zq5YK+1hgby6-jc7(B>9EJZUa+!3G8*K%(vgv=ib~gs-V#)^BmjCAS|Avk5ov|nPy4+dgl1D% z%ui_wsmcu0fEjKOYB+z~IGOsemKZU&KyXw#d{QRBj2gzGWqq>8|6y{KtDN8UrLc6V zGDTBy#1xg@lg?^f*cE1FXMuj0@8As8md{Ixag@{Fl zQrOO8nZ}*z#%h|zl>tFDXEdfys?rx5$FIxAKNU#6{a_9Mm^%l8v!*Y43}7()k^h?b z^rg24+a%7tSaZ8ZsS*#&ef8SYK=xzVT3NrNHKMhk7!*A>j>Ci6)y`vWc?Pn*!+Ck_ zFp1>UFP~v;4Xwz>Y;lQn=$F>9tW2v8h&@+SZf9zHMSUp7cIW^%HtoU$%84ZE?XD$h z_u>`ZiuHraNNZRdL&_xmp*m%kk$z=S9aipUSd{0#qDOC@BChWi{gmy^i{e}J=c7V$ z;QBqh%EIahWX|qM@8=`oBq2IaT<{6ceCg8J)}X}li3jrMu{En-e)|I4bDbkGs6Ptq zfiCpVZ6Vk@eeuF8(7TxJ8|coa%oE&a;#ammSbYGOYX^V|tnfsQ6YPrOQ8(Vkv`l52Pd@Fe|$*8jO#MEdiL z-~c-*;YTuPB7P4xLDaO0=w^37{DZLE4-Lr@elZ29&Xvn1FFXZmCH__DKysORv9l9K zwS1n{;|tk$%Wd5^qji|2x?(B>tSC9 zT?pq5)Sp^BlVP{}A^|a|XYaXIbf8nr5fH|8TQKj?%h?9@m%zUh5N|b?#Eo(J^i0Du z9w;IBW(n>DO;mFy6ai}H`IVRvR{#a}Sz{u2(F6+33F0-n+5f*Nd&l_5`gTpbT$t`1t#FrG-#H)8b0JZwrqB@}oIt&;vEMOB&XJ3SrbXeN;!5*H?57^u~LFuZa!7 zjt7-$-FwyS4(4iRZ8tt@pqHpyAo$K7QS`XLgLw&*uwBAD!!BOn*{vX`m%Iqc1`T#o5`OPQI4S z6K>p~%wZ5hI6Ip*NhwaJr2n&UM0o=$s3HFE^h2s@kS<-XfsoK1St~?2aZqaz;C+ zAm!PJDw4b%v>Nw}z3Hr*%Z5wr&aj9~GLL&>{Em;6$g2f!GM^DKSZ^c;hw^XYe2+3Z z!JmMGPmexoT+8d!XjqLhXyc9;{``x-D6ZTlFbrE4N2e7!YYO_@_TYaPS}AeKHQ(u#xgYO0 z?Q=?6lX|heZjx+@V7|bxI^@Q~%f?ykIqa{wJMO92hek?j+e6DabgGO9G2fQ8ykY_Puj#9Hlv)=H06fXQ6JxGUDBx<{uW=l-5&mi{=SH=K zl-3B&;CCIhlL+wE@VM7_5fK6pl^r}93vSqP-poLz_`riV9YkJ2r1<=@-q1N}G46vz zufBIWDgJKAY^qFN2+3h7)YKkYa|n2FQNs)NvcY86t)#27E1p)ud$mC!lQ=$R7X~W4 zzVJPTkUk(_iMBc7=$G@Vzc5ZOQnd!1@Tq_}fBmMx73GjFLPzV#%L*c{@3wPEiN#6h zS$$C2yot&p3woY1k9x|D z{C#yeInWdg^x`AV?fUK&ce>1Vn`?etFWFUCitC$1xN??r(k>2BDlN|RAoGnJ3ZIY^ zKI!H0akX~hyh-93v>E08TUG3sik`4P~F6X`A%a@jN4#+&__5zUp$Om zxg)&wFw;y;j725^8^~Goz%@}n-a-WkM&(qXI$B#k0tx`oxH1X{TN`B!qsh3FbwfH% zMEZm}pJpKC9c!sEm720+byBK|8^KsL4|XPqT^E0hn~GjzP3q)G7j%y|SW8wx&?0IH zBm4*+2CbyK1)tGlOzeYQbMX2+jMpX~UeI=?bYZqfxU5<1NRN{-lIK4UWzhXZT(3|~ zMGeq@c8ZTFf@)oZ4gK0GybMQnN56RjTpC3G^oY>DJK+U{^=J>%6oPXX&N7X)~tL2j2GR+rwYI#BOxWZm_8hz7QbY!9O3px)>)B)ie| zQ^&tB2dhPSSe0`#^>dNbW(;UWdUVyi$~@rJEhS8Ri5?+%M0jjP31VAkez&%c&T)A+ z;MAkzA-^fWG#5(n897?6(TCPr}!4JP; zPeH|(BpJS&2(=EwxQZ#Nf0>x6vf+S42!xa%;2$ulY0zA)kH_6N-8WueFWomjqrO)!v3vr;Rzu`ni)6n^pd^2n z3Zr0w7Qa797EDR$61o#ODP5zY7BmPe-?e5?@?I~w&sQE9=wPyh zzcJSH(WwUc6n01q%cH;Ak}Y5MG6;^sJfRcR3#0$@Dj~W zXthcdY>5fJdU&XXpQa31ZkL8;vTRlznc}itw72JEVz;gJO^ep9lGq;FuuAQd;aSHE z*QVMc1ZGz2b|^f4>qYRa54OIqni<61yreVS%l7JQ zhqPy!n#SYZ*X8ZwmAFJ#&I<-rfx%IXq4+Gbe0{mR-k~_n*cva(7g_AisEH0<+BRo4 zr3gWbJP&*a=~o}(t2qtHU`K=e3s2cU=t*;~wh;N=s^3M87+DEv1ho(mL$OyxHP#r+ zW|fQWXQ!^@{HvC+9``{g1g)VYsLy}|h1T-)Ge3W(gd#9n;8kZeD86- zB8@(i#0;KV4B1jX(n?#zCE@kK`#hw6ac2GDS!srI^| ztV6aUMQE7GShDfW@Ms@L4!rjF(va4x&mGzbms;jN#j|`==YLy1Hiombvg_E#BW~bg zjS46#t;v-Sg=zRW6B_s3+w?5%OW(9`gtE)!9uI^2P_n7S^w+@1sbrIG-K*pbo})Mq z650Dlo%y0c2^r>Q%0C&e&Z&%5&8pHqw=Qj54qWX2!7%pd^RFEKB&#G51+8`y%xnZ) zW*9|9$X?0ARsluAfZ*je81vIsW;l_y9wzjeof#-gkf2ah(}6h9CtgZDu76`w?v3_u zpx^cl^!f0&<<61n&6FOr9|vL!NpegxIwOE7QIn;q-in^OZkONg9v8V1B{9}3oB+!} z+<|Efs;02k`H{ z-W{+v@;V(qA9LWZF$-~Z2sycw*iFd%ICpmQba84aG;wIAgM<+NtC*UAIq%Z}UB8}A zxZ3C1DLf~NZkL5ThuMwoB-04!)_QdkVnL{~r<++DYZBRoF%|_tZ3$4!MvcU2WqULz z|6F#mP>E3nuTnWS)u`{G{*szW6;wX^M*5j2DU z>FY2Fn&~Jx=N5l_BmEA=!cGOB?`;bo1)rG14$awO6^@3Nc?~<#Tqm39*@VvT2WUTC z#KnM=5lvCmRCGCcD{z^Gi!o+%cMq>;$lx7+oVYDAb{EU%;U`x}8BQ1}vVcqH$oRzl zT0*yMN@(@wqs z*227;MUr{3?>O914+7-pDucz<&$l)-hS@br0L5gDLpy(Z%)CJQbxBZydm;@A1DSb# zf*b@9g&^aW>^Bq_<|mDvD`e~rN70Cigp`n;5JVOituSL<4{riDr5NlQ_-6dRiZgBW zkr+OzC9`>m4i2T#H>G0nane}euSmcSYBSC=-pHwnr3}N?TxiM%Ay2xkBZlB#z)1MY zdu)Q9CiTB6gc}r*#Ec!3aT14<_7t+NLKkQz|7H zGUA%jlC-tQX1300nLr^eKEUD0&J~et6GgU0c$wU>I^a8oR#F5Rzh${|;XN&x7&7t@ z%8u>hRy~$*93wFD&|aqwbN$K5MT8%Fta9f^f}eQAfzLTP1}otv0Wg2am!J=KlHA$i zy$hG54h=GPPg(hnWA}#6+zoMPbfxmfLU7C~^*NoWvM0FX6RmeO)~!e%GBM6kjWPv% zpi*=4p$V+8;7@6F8ZRfWOdpdy7ZhM`1mQ)%&J-4OJ&ttRX|m@zrV;7XT8s3BWqR$VPDS>GFo1)hf*i3oy9A_X+_uws7epCJkO-R(4IM1nH z&^U|q!3s`sI)kOuth1n#PeY03wQl{~qfiUbOJp!Uxu(*`lpJvG!^HdZ+$PJqpCYdx z-;n?LS71{Y-tTlgOhZKCpniyzlzp6Ixk_ryJoC7O_<Des{iA5>ZU7dO@x9yR5xi_g(j6ba5~m-_=#AfzOOh6wBw0(93$ zvxAiVdtzArGVld{AtA9C$3Ss#K%8LD(dV&cAVu^$hjOrSPU0jSJZ`2L!L1R!Nmq`z zfrEap*k!aj4%~%yEb+z0UM4jy>=-eJvybx95CYFZ#aj( z+1nB_>i$@WL!ouMN>gJYi=HvUR(qsW78fQM5 zyDfE6t-MnM&sdL$mxaFs!9af*2AxEz_43Y{EcU*8Xp;K1jhPy;Cio@zi2I-NeBPJz zPliePH#uc^OZ&!Q0!Ul<`-5|*`GQO{EVHH%(U}8z`SG1OA`Xpv0KsdF)$jq&25N-G zvV`Lr!C6JIecg3&_(m-V|8V{fD%5bM&31m5KstFI$JogK&iR3L2BRwH+EwRJ|Ka@e z6OmkWbNj$a3Q}})ittxlPoZ|LxeU<`$LBXhkak5$*ZINjKVjW}(z-+OIMY83oiEo<6;5I{9VgV1r!A{Zf<7S=dqN z7S2s7E1%|lD_!`!0vreD<$c?zY5GFCj*K@Fn@&ksT4Vr>dV|cgd=gLSBi`yJyb{pXT=`aAgS)D)VgW9oELE0#L1!h2O(fMF)QdxaY0&Hz6a=)a{vMX5 zC@hQO>M~g6fM;KM{2w2LQ{3Xzw>rUVaAjt+N%Igc)`O84eVG^`w4wUZ2fr%6LBAqu zb^+h8MCiDfpD9%Y?V2L-%kx~W(Mi&M)stVmZmyy(C2EXB(?VktP^26{ZynriKvgl) zgc`N_8E96~CbM=)S#{5ddu7=+9oXwftVf4czDwY|M4L7~oi$>O6N5eT!8wLc5Gi|f zQ}E{m4Be*D6k3fS)opUg&28;87q?LC8vQ-!ar%M2${ML9_T3K&Fa08f+?c+fdePO@lJoJk|b8Gs*Cs%Ik> z{7Xlx4YTsHTW4(Dv=e6KW;pQXYbI`ghbnyIva447foqZ9s2`l`6?k#<*^|QtEVUkFqw*;)vWV!9itp@2Yu!QSNJ~nJ zA6$!|?-2SK`07DO0PFGzv(4schIfQF_jOu{jN0jl#ro8uZ00(L3|n?7E#M#4FZ0d% zwbC_x1KvPkTQ=b8^0!?5qrX}I+BfU>X42kyd4;p4)F}_Qp%KX`OuA}Gx-KotD0bG% z!}VUxrZH}o(e?>=OLvDx_PYG=R3x(98XaT-FxSC{6$h&w ze#KjuZ|mPe^drrd^v=-58`|*MhImd+P~<0^_JiAAcn-wjjnN>w{U~2v>8aSG+hgsn zH{xP0LD)Nr%7D}()Q5D^!sLKhulV00cJC<{{L0nyf$7C379feQ~uBK=M#;7=+!(Y{7ZNayNgPDqR^(v5iQ5FFN_ z{pFU%Mc%{_cbRkQxMwXT*Y0L2o0{QYXA;N@Cfe3?LvRqng!lZ${c`(XkyOebDsNi2 z>#jlXESts9hj0+a3Q=uzM*;853AQ1xE<;J$W7)D}_uS2Iux}7-VcD`_Oa7;mAa|Hd z<*2_D*}{Cuhrk|wzsg-Be8`4H0y#+Fh0U%b^qys8Cvy?f&1PnQFq!WRJT(5koB&nk zMV6)Iul!L)37>a@5L?A>cQuei`BS zwt~FJaPR8<#y1n^3n_J7ru#`M8`j#fOvhL3=LFKQ4rz?;znKFuCUrau8R!c>ISICK z0NOB$IySp?RXGZI_`@G(3gpmaqsMnoahlMcGa#x1_>bF*d;JI`2`GUj9zpM+yTVfB z+$%l@e~HBy=0eI5Fhly+!_%*?l_f^>Tl%2ysx2a()c7TvvCfoc*P_b)QJg?L3371l z%~ECs8#L;H%Z~}^m-d(5Q;)jo+B3Fi)(=-O#pUG)pA>0;4F@)NmV#VTF&hQb1(Ct? z=j>)4LO>RWpH)w<=A-WL`wiUO%^CbqFS`Dn+OLDbsZ@O#y=}+n*Sgm-_V9O`WS4aN z9=v%P&D+^-xOqy>PyByJJBUy`Xkg#Kzw;aTv;4oKo&UuC$=YK1h1^3F@d#;c%o&0k z&dKtDDJf{b;lBY(z1=$GOw>_5@tc~pK|jQ;Lf8cY|4*8wvdHwmbLmdkn_SN;cDcPj zR!5G3q1<=#_VkDQEBdPBP_8p>m3P3jzg3ECn%m)Rk{Q&CRU!QK*BU=NeHGRlqM`!wHl*#?WTvXl~JRz>zw| z7JQgbl;;x5Xl1U^r7U_Z^~TduS;%wnpjkZrcJ) z_Iyv=iw=qTDj_x0GIzW zUc7#2@Oma{CM%t%@%dkETC1ZoRF3Y3`L=uI!6AJKLMX1z?u567PL(HdIHFXLGf#0u zXH+ZV`%k=&O^z|+zd3(7i;t8saU!g;%LcAKm19Tg9Kdl~LIlL9Trx`MvBuJc>&W#K zs56K-`7Zfgp#F2B)SQDor?3xCaZX)BC4Xk2|{sT>MJNC$DjHbLMt`=Em#ru9IRD;YAbgqNlo1X5@XpsR7D zUUde8g087xwkwH3PE9?#Q}J0u0D@2GrB4*3I;i0G&e z^||+}Hzz+Thnpg#BHEODOrTQ#j?V@&rV+^mGb?>?N0*}C!Tgy)nV>n-IS{+i#GHYX zk2?W%r<+xB@5wcFFz}bPFJ^{D(WL-H{`RVWO!=8qETVl>%7LTYwk{XhCc3u{MWjo2 z!A7yEE-*zM?4yFHS9)uyapGyQEDoTV*0k6ro948zy?yZ9CYWHO+9V2+fwFrBSkgTz zG1)1&SHiSN0H~&;8*UXXY!eJJnXiDZcdr&U5O*^@N*@P+wn@1*aPw@s#3;N_dr0}& z97sw%jB9lo65Xh(7>-1|(j+(F%tM>xwdI0cZ>()hJ4{dHxlwWF zUWB8~-K2LAV#yUXsWQT_rSn_F4G9)2F)4XGj0w;<^F-jV!rjHCn1XGG+{G9f%E*-l z=Tz;*BVo^^d!8E9b!_jk4Hc&SZxxX@;t`yMy{gWB+7~5gGUqS~;)W}a%F}?4w5O-J z!NQyF(}h`Vc}A6Qu3ySiVL>;GM572t4$k+qrC><{A8M{VuD9UvEM_i-?0o#{HmEM zTK#3D*+q{cOImo>V+vv7^#a6`zzBru7Tz8L{z#IDb;Yn81sgfiQ9J;aDXLhP?)%$9 zpvC59F?vXzk!ZDa^*qaw^x8r5j*J%UL3Lx4vZdd_k`m*Wvy|7ya?y6GxM;I`BpTp= zM-%C7GW(6zfyIg4MB0mp(Rj^4g7vWTk(d(V%>r{Buzz$QVdJ6T`)TgBsOhLO6B#9uUYT}0Esr{efjY?sYk`B2e5O1FC{?q+-|IJXrK1%gad~;}*%vC!|j8V!sJBD zh_^8EZ@9vFjC&Hd4(tMTK;rl*Us5)(CB4#qcbtWsdg2Z?9NKzLdCM>S&Bwh$X>se8 z7IxjImkuQ28ZYOHNfWVXIJN@ZgW@g|pEj&P*7Zx;0TC75BrBdb^!i0|XjZCVPW6$( z67%|+6mH1DVlv8$wU{5OS%jyWh(T{QLYgqk8P_J&K4DS96?)!dsi(X@{^050TT8%X z#5qOqY(PV#p&i8yq@kT81j$QCr`b4|xxtGVnWmIaf*pxQYSA$>y))kX+SoI)8OAIP zrwBXpBbQSCdTSOChDK5nQ17rGQ`MdtPf0VyKzfu(oNrrQ1nLgn)8W$MY8gNY%E;nf zxX&_pue7tPy*VpiS82A{$Os5v38|;Sfy3wVMA2d!vFWBudzcyKm}IP7qT3XMbJAq1 z-6Kk3x3d*b;vlJlDwwiYl4PPn`w3RXZJ|fwYQ0uS**|4X&;M}-J%MH`k*WbS$Z=mP z#q>+oMNldg4;eJDGArHEeHi^b*tbq4#z(0#0HvfCV~`1Ov99N z-9%40M)IIzvsOS2Ov%vLG6Tz5uOX1%MOjMSw>gutbxByM{D{+Nc=B9=-)%{xI7CV< zym1$J=E7+-Jzvv5BD|FgY9wUSp+riap++h6oWcUAmch)uMYJDv>e{++o5d4ZdN;MO zYBvK90N&x%G(21X7zlkSb+7urCfJEM&J*md7XA(ZwyXi8%pE*Z?c zrDI8l%<;R=I49jmBqvL8vFIuWKZ+b~)JJ%TqxY=X`C<Nye)8>LEHKEG!a!;6E(J_lD-I`z(gno#7{aAHbK=m zQPsqtq5?-|I6qc5+$~)kW}bQOfI_t;phLAqJMFVrY_EI-;#grh)y@-h=P%i55i1$X zn8zy_TfQjNF#(CE{K}e~NF(yN3bi2**Po~?dlv_V&H+=|4$Y7pJBO~~i3AJ(ppeWf z7@J>qCRa@0#L z$?X=^&frVMyKN6VUfka4kv9y?3-qAZ?>!**y*HSm6u$~)X~<+a3FOpcAO?|KW(h`R zPJRefU-_fZ*KR7lshh#ALGhP|54!qzQZkE7c=)-m*uY&pgRli3e`1&p4bT z==U&5`g>w1KlNhhN2X-meKAyDX>j_=M<_qxy;w%tHp`mOAAZusdUp;b!D_Q@1D_7b zx(B1_rgCvW!8gcM-7~q$f0UdR^mUZ~0GI=nQ-jLw^JD$ADc>;c9XpH;JDCtPmh!V<>t16^Kho~jN@SZs=#+(dk|CcQrx!H&d2#1+L{ zT9qPSNWfjm^R4nn;YN9mYM(Eq_+p^%Y~dz1*d7#M6`e$XWN?cp{B^%op3!&fDn;e4 z;goMkx1;9HzCSo7cwh^DG@4%__w)%8DzCpkqEbsw#p?Yhki(X7?#8n(3fn{v?Gu~J zH<>$mf4@Q8E8wU5PDC^wdA3}Yyw|3u$xW|RI_(*{j|d1CyRc@^wIgcRh}1_;?uXPE z4ygQ&bG3)l|8nUey;L-CFWAO#+0{J_U+yPz$7Pd!I|=vph=^qK7}lzM5=#67nuN6K z@kmzBVMaWL&5I;8gy`c%ewUHCS=l}pp;FR(J-U4OXgrxQlZv3kK8g-FT+Z=JPK5_N z~wS^Rr4^PC%Yijm;Ig z4hwdWVWswlipc`_rX9;ni9wsAE+GQwsEWPIuP9@m_eGdvy`7;?v%#+lKNi;HJWjE9 zJ-CkThV>Bdlo`eT+$#ngeiv#HPcO><%(r!-W9-Mtfj?{4UBRKUdJJX-+hIWX02g71 zcHD5{G@biVqLA+;3y%7_d2Qj_Jdson%F0f*Vy(P@$DBg z+-_voSrVfCJa%iDf|BFW~ri{^xWj z|ASA{%VDmsw%a<4i9lmg#5|dVz#*zqgel=16{7i)b6N(1N_AKTE2m{ab$~STbe`No zskm?6S)IddVB^2G(36{@c&*4{X9IXa^sKHQzVGK;kAFMTJ!-E26AEX(eBJW)8}ga9 z(*k!K`B~($#l18Qh1By3uZ&X+U|=1wqDuXjAapDsN~)<$Y2?J*VMAhl-8VtA?}$&f z(Pqc4)+gef#Ra`yBQ3w;nzlLejIJV2 zsU&|Wmxf8QHnAr#SuEP@?n@)XZ_)i**RL?re($vY#f-^$!%re925m=za1En2Roh}Vje|U{^y&dlmnIpOLhQq-5T;Y zp7x%oq>#o;ydQEiis^LV8A9KG;|z!UfqUi&I6E(LmphSn?+^Qj z)2|fwqx46OT%cC>V$>Ee$pFt_JQedLPYFBpyn!Z*ZNV@?C5Ho2P1xGb9+{{Y7?s5d zx8KX$BGbg*T}Bx5C3M)uJu+p4nOIoGHOS|VnH}E0ZQ126#LR=_v8^o!DO?KWv2*h& z1#P>qQz7AQkljF ze3de~WmY$ATv?#K#4>z3>uwNSUpJ;p0ihYfbBOGbrxsL&y~6!3v?O_U0H`-O7J%Nt zT<);JC+)(~PCmk4y!%IZ1l8I6uvfyEYmdWS{CSf%h9bSZn+3VkzmG)Sl6=KGbF-(h zk8+P>-9wFM=-j!w$5?NqD+j*ugL*)09Z{(C^SL17m8Lh0fBLAq?If%03$GAF;@+Y4 zn{WzZCTi!ngH`25do2Jp@(hY^fqe2gYVeG5MrsGOqnjLbQLZu~GW-J%VF60v!@56AWwo;I#vJhuFlw#)1L5 z1XthJ2>STBeF0?Ljx7%6FFg%>2-bLP`X(kv0BcLY#1rs7ge@L~J%;lmJris!lJ&d4 z?tKKr!Wl$k7xM5VObebM+Lk~HsE7(WE_KQc6+nM#0#6`gOVFId|C#Q4239l@mt_`_}dhCWNxJ_KuQvP|_yPv9@xo14}+9C=AA235of zo>Ps=cL>i~PeEE2p>9IzMZKWs+--TOEqOMe^J5x$J#2_4mL%^&dB31-KMz5bEqSd* z!4F#V-iMHij(pc2=g;5Q;6psQY$_GXe|36;oQHg2o`e%O0kTa`VnZ#VzHYQoXA*O3 ztoLLWsqb4d6tzN6gk*f)!ktHoPy3z0=;^oTZDbj!-C4c-ep%_CeIWASr?dwe+tLCo zQ;!1Hv8tHteh(a=u^&@3fSiE=9~%WsZ)Y9@|9K>jH>OUO6N2A3>GHWWDzD~(rQB&nMU+9{b2D<=pB+=uWarJt7$Ri^F;#A1x4PT>Y9Pt-kb$26M)&=YF)hi6Vp*mzC-GFuG ztQ4CgY#F8gWWnh%Tu#po&IWi`a&qVS3Cyl%&rDSm=_)9xHY=!+lm$|-d6`aA3URdG zCYLSPsDo=O)MPwNV4W42q+5vhYi^=j#Pf>wPmj{rHU`Tuq2qKksJiE)o->S_oBe?I zh$#$27O-%GUf^o)GPd7;EY<8VOu*dU`ITDF+Y2s!R`w+D+AXKgtApJ+>fm6f%;wUu z#a>>A|MA|KUHLZ4QHP~?eOj067X`NOt#@0xUbG^tc5+rYN8c%lD6~4aUi~UUP<~!c zbE=d{iVmThdeWR`(4@`Lro~@_Ei7L#(HNn2EDW5KGW#K+*y>x3C8M^gN*xjkMaNiD zd+BggtdQJH5yLq+*E6xiq&NvxF4B3W9Bs4Ld_G55JL!$J-QUSQnvR^mhjh&B9VN%i zEhsSPws5&$AHu^OzE>VX^vr@IE(iJjfe>NH4GZ@uG%Vk)80<>?K_$;Dw0{4?iM3MdP2r2->Iv=O)-`NOpp;!EHC*wC4}8Z%Ki%Get{*?j zRLl%SA<|P{OS&yQjI5C1>L7irZY5aJYj(VEzZ{~I9qc0o&r=@x3=fy@29-K@H7{w5 z;gLVJXndksvvcJ{nX`K=JjDIa#L*5)%MR=+eKl9e)ITOUV5R zq6GdMM=b@9UePk-$3p0F7s;-6)~+Bv%ic;=EGE2RTcgrs?W)+l@56sepyWvCC42sp zK>4R=&3ouZ1|XDfd*t`S>ksnO-wIW;fR*eqSG-Lc*0zrU2?pRclWhh1N+N zTQGyAg7K+=nn_toq;O6+NgNJE@vC1WKw_No7iiIba>_J5GvyJB4Z9AiIsoD;D?-p~=2J!+T9J#WF#8~3;3IFJ&6VB*NPy|>1y zx(a}F_PxkvZEcYf|M!3HZFk&1I;?$!6kz8fo_T8kcI3JvowJjqL(>stHD-yb zCN*24lR~`_UuSD{C*m?Z!py8F%ux2$QDR_u*@}&4sADM0wWZZ2<^wbnpjB@o3*VDp zW)o@vPWSSubpR6GX!8Y|!Yg1>w>p5eyVbmX34Hw#$j8p78jZGv#6|5rT|8BUofzZl z73$eVU#|>V0lk2Z+lHv?q6BZs3l><8>5T=JAilsE_SY(mJ$!gRr+%7f9wQV#_Ldze z?~h%=XhL={Zb5Ee>^+7b$gsc3r)juF_S5VAk7Snflvu`SOaT~7(wLfgCmFhPQbK&%gLf9D0j~`L^KYoz^|Bdedb}pgj zrjMrl!_>ShC* za0~zrW{vtmNg@l7r|`Pze%bbF?z`zsJWBL-)%l$3eA#B(zVW&6d0X_m_T}@RTuh>Qx$r+P(vhZNHJxTXygy_#JHTvtX)+grWzhU#&nrxYqJj z6XoL81$M`rZ0u%Yeg|;-VM_5XzUqh5FUS0@Jo6_VhA(vwUEv+67fGad`4(H_U3KuL zU^NiH_^unmx40Mk+BEe^GMH=f8VTc<%=B5ZcLSxOs+gaD6XvieZY)jips!~aI{@|< z3UgF46M}XFOCp_%F}=!&U6s{{n&eHi@oXr~fXnn#oimx;FiLiPO?rTWffTR*Y1^CC z>^S>iQPpQr9924a&XuFe$>E+$F~vZsZF<<1MgUiJfjAZ^G?A?;%7`4iVTPYDndso$ zaq4gE$%KPiX%r$;@!X+K%dUF4aIOg#N;PiN2vdmC81msV)ctZ#Q98yXcJP$2>(mrE z&0qsEnIBzfR>EbYJ1X%~V{}Canp@}goiwXmf*!Hg`V>3Ve3lu*B+05MeuOWv*vRCh zTf{FVQB!V))5Gd1_gK4=j(FnZvIF1>d{#h6@_M}Vu}o$L+f@4wjtJ4RG3iz11nJYg z$<}RJe2d~JL=^M%GPfG+fn?Hadx1&hp2J01`W@QSz`54S+sPpET2ngH2**mWoh#;@ zLWmRcS`>%8cx;-7raZ=^>-mH#i&k~ik+qR>iyMye&zlJ{nfgwa!G;}ul=0z)LecZQ z_*P?`O&Bg@G?_Zzp4C;Wp z<%JV4`v;gtzTd6WYu<4I*hfH}riS%7k=;G^+KMGx&|`B&9$6^}s9pZCo79ro*1AZM z`HJEkEfU$ZM#%7R{pMu~X!cRI2us-wcO-P$W%tlH?&|_OjG% zKv#aezNT5cVKUjVh9$NQEwXbC;G-q&YWM@^F2qPF~_U-hOoV6ahEPrX~56>0st z;ceSo?V34>hg4ZlET5+snC6bPGa@B|6*ILo>QQ`QbyqGIHWRhRyq1F6iI{|4bcw`I z>_nkUf^Zox&G3POOSCO$P0)9-Ya}6JkzK2|QzTI3xS#u?F8fONh zbtenga)wj-)$(&z`gHLW9Inqm6LVi4=d)pK%&0+Z$X}Oth_54qoG#f;D%=hG<{09N z2oN%tMCIf&q$m;N|@qh)}vW0B@!)d7Z(+E zXq6^imI_qof=w9Ki&PW#fDPK@F;wpgHYVdd^NWr6;7;9}U{((NOXwDsESp8utW>D2 zHb7kAICb;rOnY0>97_&VEVE-Mn`MfiQ(uAf z;Pfo4D}Ev_M_~=7@GGTAA>Q_i$aEwRZ!9bZYq-OoLRI zjc$gB?j0_;A0k|HyN67#_fs+n4aHsm( zbhe;&mY|dB7dhIR7E4koix+n*pOHG&cu|RAM?~fCnAQVa&g(u%P2*=8YA0qc3$nM( z(Cpwf3?|WZh)alzg+^+bdY`hhNmFv@%jBnR>5-|<)TCbO+&d%s^9RXyJTjk&CN}p# zAQNj|0T1~vz5=(yfd*Ws^3Et_%ediuF{RW7D!OxCC@Zw%!Qs^?b`-o2&EH23qNpv6 ziN3IYh-Jn0Y6ceA6l;?nsCavpW%+Z&I+{Dc%spSE z>n^BG-MsB$?MX)Pq{;n<(*z>Dj;qIr!mca1&74SqO--EPif9) z_;a(DN3R8ioDP^XX7tCi=`u*ZjXto>@)+3lz*p)rdbWb(HGL+J+pe0QdtG0)X@?6u z@)(0{UXhUMi$wm&ML(-KU9qHOlap53GP`;A&I{_&@?*G)e<&;1qAW@ZFZo2Lqf2mk4nGwn!^URKniIKBs#%njs0 z7DMrevfRKIqRe=0RH0gS{17LFI!h>0aL1!6&ra=mbaoFon6k;iG5=fb$uhTwihNw- z)K&%sQ1^ke5@dJY@725EJ|Y{by1<8zj#ryp)f!jQk~kyip&eS1GWKz9$ct7mh1S$( zT0Vjw^BtwVlW`2SQB5Bj3RrE}<+Bm)5tmyI`Tqvj;Dc0=6Qf1#Ka0))l}Qv-aDi>d`Lh^3BUC=lU3iw_skJ zij3=Xc33$pBd6 zgAb9I9{$epVPe+;-W0+F*+D`Ez8&D9Bx2_fp_Z@n5TVSz9U7xZei@;JR2XQCjp50}^&_*u8;pgFyqdb761cXZmEq12#3YdSWUOa%s1 zx;r)L{o59{mK^(sRa!e22FMZMvT4?Cp}<*g9my`In+~Ye*LBCU)fm8ZW&X-CSlZ@` z=}l62;O_XZ7-6(FY;P$x+-QzWN4k@ShmR)|liwM#k#!Z7FIPPM{gr3%;7UnH-!C%^ zy60!#D$9OSAI#fZMNj9JHF6YcyucDo4L3eKvnLt+Ib&FDenztt!LETfuzu^*ejBI_ zk4mSo&J$e;H($e3DoJ*FTpZ9^N3b(fx(BEG7qn(bYYnX{+ThNWH~5BNJ+W((E1ma- zigYCT@kVN^;(m1@%8e3>aG=Qx6ZQhr0BqpJC@6%s3ioTE;;;OLjCc=6-Wvcax%ov} z|H%(*>j=`0xRpJh42>^nX3dQ8{CZm^{h=lVEw=Ko0*mvnzt?RgNPx2n@E?AHxD`;4 zvkK6A#0hU_JOU*Vf>zKT(Whj7UXB~9N6qW*nErFhwF?MkJ}q8u{@lXLlM^6kYHWhq zI*-z^nL_plSq~61JDA@UYJwm4f$}>jFeS__sc4%AvBFY{7PU?^tGv~eB9j806Gs*T#$HSEp!b3 zi$&@7tlFA0d1;ds><;rKhZ=iE%gf+$hcTe4PL&u8ZzwvdC!I8#cy70q^ZQIGjwkOWSwrx8T+jcs( zor!JRo;VZZ^z81b^X^u?Th;xcyPp2~^i%i$x_{SFIY;?-9icSO5$ zCexen8>MkOI4{aa)lcJd&}z(8bGkyy9m;KaOpT}A8A|y$HY+}Qs6|C3x9_?#spm#> zIubC90Y-Qf3!EQIriPojV|tF_?c?ZN3ndKMiDxZ#-X0?s2p=y-ORgcdukZrobKMcg z56#(MUS(GvrKL6ndw1wg@_ze z1UvX=5G!uenx-QU*56BaY#zU}^qSf~9rBmxx1F)D_Cb(y-Zhk*?yk9(3h18-&S#Z>7Jc_x!8O?^5r&n zU1!F&VyobV4S6X=`516;B0kmxncVrnU(P}`prxrr8fgxn4E;2j6&*mJdXv}#txR9I z5>KH;Gc-8hbrD`RHmcEbj@oaaxOi%2xOdT6U%91xWbGkvHz%k)Kp>F3iH$}56)T?X zO21c!C=&_gQ8pk!fhZ{n3_TG_HPbB-((?E$y6_F75zyTY(Td?KxcY?7xcUS#x)w77 z_AKalr>m`}DXb0NOsud~WuapkCi;zaHR!)pYT3J9Y>YP^!}yo%q2(JIR5F4H66a)R z!0>PxTNMO4pMj*wOY$&SyCbyE(Oe{%lT|AXY+IBjpWZDuzAZ~P*E6`;FW(f|q)N-7 zghBH8q%E9{ZAvs6>D#;5%BjbmC-(J+VwUWgR1(n3YV*vSvf;nhQVukGx4ZR}GcSUt zZ!hAo;jh(rIK9WBV{*;U4hgZ+Q!LO#+HQ;Uwe;kq`YEWlFih2;w&qyf{+A*+SK(B{~ANlhH}K+9-8gQaJj4m zd(Xp)(#0KsC}4YodeH^JJpjrWf&y7f)-RL5S-1;uVA~}aaD=R7?dO%-#(})nz^*Mg zq#*5*x|@g1MY)sTCEt$(8zogZ-Kb?fMCnKuKTf#ww{j ziV}RHIgX4~QsRnfi4@Y~FvJBUM89VCz{Lf;$hvCJzBqxuFr?p&Ip_*n!P@hc*>;EB z=As1KhCK4SjJfHdx`u+h7QwzQzI%U?^bkyZN($n{$^CNqozQjPHIZ}if&l&(xWL|O zcFMZ#Z+Ys$PvVlP&dZLKWGTWJqDaOB_SlW(VG7>NtLVmV;Kpvu)u_jt=19?||AF=2 zCs6&SSW8~Oe5?!5Sq8p9OiUTgoD5y$tR%Y5rKHWlF+2p1qBzlq*j7%0sluzRxM!r-fjggtoB=bK#auDRHB!SA!Jp@Dw zIzwKg5-vKRQ_Ob)frb; z{;;Yq-=-h0MuS?4Hw3G|Q&$>x;VUO#btj4-QZf!o#o$TdMrx5@sIqrCcxXqd9C4)j z7F(|_cTQ}qDIkEXS>|k-lB5V_I980y%!#e!s+*x?D`bZzz*)#vbLJ%-(%!uGIFJVV z_7i?nW4EQCk3h;5c0|$WLs~2*B!&DR(7*ejYz1=y{v46Re=C_nqUIUVw2(0ogX4eN-PjsHZo+l1g~w;XrRX6 zE?RdK3r-iXtRUP%w)i`lq);OTHi+W)Q4C$w793@A|e+{4NYY; zcN>qoR{7hEt4m|h$XF}5^A*n0u+__3dI@3UJ5(V|tO|v41@rIFrS)U@E(!@BcMsH? z1?6@`6|=$Wf6%gpIGetgqnqqTwxo^$`Mpu_r;9_ypnN%w+(4q*FDz=clZVt3*z5K{zQDRV6lfXF={in-NC{4;)YzEQX{vR$w7o{%{ zS_ZfZcp0Wq%b27Cp=ARtanprJ0b6biJ{u53s60iWvmBh&@;CsqKg_lfPbJ`p{BEc! z_b`IC+Kr&j%P4u#{D&!0j45HB)G$?zN$IG;u6bEhgx$uEnv)C2G6YAVYMQEY2m0fO z&Is63q5EHmB+m2EMPA;Ts2E03&y}V?=oq4J5xX1Ofu{*!&Vpc~#ijr;ps%LaucN2l0!SMt>9 zR{DijPQhk>US4#dL5Q>CwnPa7m^l-#zSbTILaFZGi6k3cE)#9H(kV7OXm9gKf*I@l;S8Iu&ml-N*uRarjM>nc%+S4wxo=Ku6>l_EyulO zVi9yfI84U2>_4O7*X%!{AqHLZ7bMGCF@4D(8@;#uyZj=09j~Uvem;EBzI!(-%0GoU z<&5<|6=q`o$PR;brr&AEtz?X5k}^H9QD>~_MlA#gnTpjJ9%gOA2*X+{m62-hmcQ2yMe3}p~nTc>GL1Qe2PdtNhjHBP4g0;&q z2TMD%bkxCoy2}wDyGlCKbj%~)CR=dk^T^G@N_Xm&ZF~UF&_hT1u`f*ct^xEJyiL6_ zR^Ed+{fKgoA{rY!!UY&lJ3I};T8{*Y$#4D06Ckk!>@}#&F?`t(?d{qOspg0uPeF_fMk3pA$rQLJs zm)JivjdlH$xrK+nd;9(r1Fd8q>z{5t&i3YYgFmU6``0Y_CIDM44)^#CbS8K|oqN=m z)SOT(mnQW>s(4oKI><8@iH%Vui|xS|+5@uU{X_2-^wuFzeNyi;<1b3j7xDMo6yk*Ug2QOMH0rlcB52q~D%wwbDNSQSi?c-9Mdu-@T<6bJS?+7}M z_Dc=%oSpkqTK(aTLH@_>x|0erE%=-}xOIMH%=-K~>gM{5Qh#z1o;qFU=P~&)88U=k0D?>PJj;W*&95JW4IoH z`&I}DEQ-n6XUIcTDIY~lB2=Q3GmO_1*)Q=ggvz`|Dh4;~aIj!Axo9xa43*~)&R`~@ z96YIgk3Xy0Xn@_)_<0gY>a|h<0;#8 zq9J{p2p$&6AswmUdxlf2Ic+&0!pvhZ9iwlE7-DT^*Uua#jR-`T_j#ef>Zq7w?xtO} z7S4!Ee&Th=OS(iF;r=c1UHsRyl(FY-Kk!<Nt-BXqJ+Q6od@@Y22`mgd z{Wzu(5JD1#1xGXpJ&|7T6>2VJ?U#`=;)-e?G&5sY2}pFr?PFwpd#>!8j+zK)^G)@U z4_wd$o3IHC$}`~j!q3#KaAuLzorQiy_FX-qPE2}n!!ny_5&&D5)D21I4JodnK7{av z@B0<<4&6BPj41l!L%mEa`*_;Z3fUzW7ZO1)U%YLjGW7lA%*sqBWSTwb`@;TS?6jNS zre|&{`d&ig10TcvY)?|Hf8{}wWwf)pKa|u>f>TFSO%NGdAIkbJto5lKZom>!|1^#* zug9_?nJ=*6j)*qYc2m7iQ9#%qWK)hQTaayFfhcf0yq5iGB%>W>_7lQ6<&fqJfnEAR zDnjf1aZWHL!Yq%25!j-4!7)T?Gpa5BmRCGPRkkK5GvO=O=yC~T)g?4l#3vv__f26w zwjgOSPIVitX|?5zij2LEw}lTk>z6HpTi6jq2`s!Zg@-cDzmX0yCH4WXalI& z64=G5czRoxGsO@bcK$@106NSblMA}w)o9W3{YS43hEa|=G66*x>LaIF^nEzRwh!uN zw0nfcEe;sU3AKFAVg9z;dzN04kD~|a=CSpekx_?4e?g=q9h$K7rgy1%rPLjo(DQ^G zRQgLB1v*@jE|$~N%pWpT*X@V5r32Fgb8|K7YXY1ziA-Ow;=8OP#2S7BGPODf$; z(LQF6ZyqrHNpk2y71=Fhg|2OMU|$8v86XfuoahFre8tl^7gwQBAg<9DVd{zoxgr1a z8U-z&AG89*89p%^u~^)D@!f79%i=EoDhaK_^1-hyeNI2@XQ90IOt>4!oXIQL7}MPG0c=_U4}9Xy^neq- z%iV!16Ou~X{quXa--FueZNuC4a$18~kGBq&^axkM83;Mko7=*hD070udHgYxuk4%a zE9D`%!n^NO9SOOBtTViu8hfRno{;@B?q8?&WNiLW38$VypM)GfpziO!UXgx3{1v{w zO=AgvFei>GoTfdX0u!+qM-L4*b2F^WG=bR8h^i4#n`eY0+841CV(doy2ag1t%D|>Y z(m4bKgxa6nM(+2OD#=ULwf4Wq;ljOgyWm&%CIf2G%b-0tDs^gFrKv!Rx-Fm*r{V`f zHP){LMG2fuqgRT1U@dz&OgXhGS&p?}lX5vobo+;nMgO=6p%;RPVZM`p_LPY%=@;}a z6xec(>JRC52wk}dVFE@klt4!niv9y)mUmPGLd;Z$uq)%o7Y}iELLJ#cRv^VKAo1pt z^QynmiQMLNf5Q|z0wLy^D`fEh^cg_^!)Msj3RdMI;lQZs`-E(=?;oQl>1+U1ou-h3 zn{@?gssPD9iFd7Xu><-HIRAVGjBfi*tHuqOrCuRhzY*Q6EqPlva32NGo0$!u*HFn} zN;T-JzIgn!*-XRR+>R391qb3qqL9VN)%4=~8W_dlovqkP4}_S-=w0W`;*_&CK>WNx zZx~6nSc{2B2v6Q_t*8dIbv~Y{9 z-XKf;RQ-_y+^t!E&wfNe#{6u#3xROFVvCLiPbJ+R8E7)BZ04tb&K$n`FTkD8d1Ku7 zZY$Kz*^l~UePRA3-ZNDc(-)b0KVndiLW)A$Co4-))1no}eh61Bg!JFMk3!u0`X9^l zb*~AGhnPT+LHvIW5{Wq3Tbnxlzwr$~%hN!|6eZ7-Td(2G$u?H6N$xrup*t>lo@0?C zJ#{{L8QpcugmbJVYpcz^wJE3&Ix?~>-v|^#tV(h&D82b396qKaVSkHOUuU?M@qp9g#~!EG>Wn>Y zX)`|cf!_;a8F1M$%w2|zd?+7XR0&=NCDkYn-5U_@;qj};6hG;@?4e83$`Q$`gAGm3 z>ZCaB>6U`_`f1i>`w}4e9Dr{B^F;}#N7k%Y)yf_%IxX#XgP$#Gr{E5Q&}!ek!Uqr+ zzC-qVyYlYMXV9lw_DcxIU-1ZOjAup4u^XKzb)QVhM2ls?mBHg#M zOHzb)z^hFVRf{NQ7&TCg3v(%h$LpIB89^|xsVefCD8l6r4E&!Z#`j-CF70G^3g28(Pn>XIyWcX}hPyu8pK-w9m+t`}Eo*gH` zVE@5qk{2=h7+AFh@hXZ^V&&X8CdM^ChqjDH%y5l>Bn}>7f=|4n5xVjKAxaQ@E5yQs zj|V@*+aFlBjnY4pPErZs-M=bLJl~F4wGOwT`=rQ*iDcLlc}7-X5j4hXB~6bHwpxLY zt_@a{-bUX`1<8sUj_CS8xxaU+Xf8hk`Qa@B4g;>(E*LKh?=A^tEZeyG*dV|TdEe}y zsGmbYGoeQH66nf?3Az5gj~kniiG!SWufKI70i;qkMRjwPy$xXzM3^lyXJ}tHP>dKA z&*+@~p{s+wIMn4_7GJGo>9L08ONfO~2q|fWSh{slR_-23^i_zD54a(eXz9tn-IY2+ zw4Mho9$c_nVnR$9PAP)+vv(VL8G#DX#3JTnaUv7Z#v1>1{9^#r_anZ*C#J>pp09ts z$;gCeykSYp&_&OJDL$H`eVTNl`Nv+*ZuF=1!D!hYB^3T2y=VZ+W!18nDwRV`-8?BbkwcKz zvblTWfJp99vLCz_fxsa1a@b4T3(-RzyQ@*`i2JXVKVR`k;%%VfaF04xh1kYDO^j$w zO0x}pr49d*AHQi!8zIyHiiY7r20R}-YjN42TZ>&uD&~$xT+whg zu^TNMgT~&Xw^s>2+Z$yS;f6uQ_qr}aXEX0PiJuDE+jSJNoKz-P=8bhK%l!;~>j5HCIV0Sq*P#@4DkX;p6MgJm4PRtNHO@Z$c7L8BI+zKU37Zg= zF0y73TIPWAw?v#={kYCjwyw$!TkCXcObBAW!aZ@>4qKR>nVv@l{bC3F)qt999RqLZ zywRcQ09T{5+@FjzO$5BqF*{-DEXKKe13bx*asvS|woDKw7Q%Gneq`t1n{%#*#DGXO z$p&`_UXQVZMkTLfwQWNO_8fd31KZ!I6Hkl_xD~HI){UYdi^qQN@^h-)SCb`Mf4eSq zAmnpI^TTY_GN~JVsCFH5L>r{H`s+zK_XiSBi^kGVNAkYJC^`2@UX$xfrw}4}hQC^L!ZyCZ-MHg2w_#)W z!R3~gL>W%jve2=iAG-K3xoiVwflGelB3*IZY1z!aZ8fXQwa5#I`m{-A7BCj(gW3J= zFxg0XW=HlHkvaOt%0c!W#5@vbk*BAT9{1YI(Q_`Nu=8GD?N$|6a4@M?(Ezc*&dai%6U}Eh8Wo4d^+uYyK&P0Y}2lc&x(X) z_U8E9J&f^!%L+47(hL^@Hnt*07}N*@xOFgm&_&Nd~Q ziLLU~GhymJQ%P=yMULnR9{JDePSo^sPXZc)ingqBSPrBMvtm0Vqnq#in6F(HXKvbB z1aj3E>EfhKA#Vj0*c>R=tl5(_NuDC%AE^BTA<`y(WQ1Xf?~#(^SdOc4f5mTQ6_hes zhvvIUJe%r!E(ar8I|7t8#Jt1u2}$c>M^M@rdt?XS<+X z2}f1}DtZHUxr$0MdIirFnS%fuWOBH~u{1LBJGCt7w|AbBZJ5H;_DvFo#Cj|)CMKd?UDo_s?VcKNC`|>Lmt)+um zR1a)@3WoW+Ano`j$ndwqnt8mU50M?c448blPzj_p413d85~2VMgmqmeq}nJ6j#_^r4&0x`p8I0afSJ03OnvVT|!4jqEbUi zL6P#Ox4QAHen#WD zD&oR6V4;)w@_aJ&;x=WDVA^lM}N8p#`%8_j6 zw&8l1>nn5#NrH>`3v&+Qy5e^Fz!N1@59h^$qpaw+xL}S_UzUNdRKCyF^M`W$&sOYr zdpt>-yyY`DJYw?B8i>55GiMI4;?dX<|An!}TqmtzgR^ z6S_YM7S7rbmb%TkKG1l!!YoIR2CLh5kw`LP7?EJjj7S(FeN~l&dt5jXHa&6H{s(x))T2D1r zIjJNn3iY;_&lz^>jdfyBO$aYc&$K{gUvRN1YAh$vJ}>^;6zF9^c3C*JsDSm`*Cy-? z$}rm75W%smSbhhj*~~7jN2(P5#V4u<;fgd8F?v)(YGRA+?1$N$Qqg@P{(d5QJ@jf+ zZgi=Ur-?L-8}q2C6OP+Fi$qd2JmZw1Myk>1U1oCOOd(DYjoN-IZ+;)Y=#KMhq!mTa zeBi1|`E}7XRIKcHwQLPx+8ENAB$JKdopviTbGSZ=*8^{)x6Rs(SLftUEPtOgM0bGbFA6iLIhH?kz zqX*$k_}*pc-eq|ImR|z0TMh>N4hBIjkZdvz3-8#+B(lm9_oZoI~E{x#CAo9q-vZQ8AM#!n76U ztAg1&U89d*j^pNnf)^cvGGB{*lQX>$=SmpD%HSILpe(h<30s;3gmP0`yvtLFLt9$N zu7AZbfUC`pd{ece$(#!x>n$i%hILn!{xMZj6MoIa;ejM)`!~lHy7E~b$J&-vRhUmS zp}w;`o~Mts@@7)t`PkCs*K`n`UC$V zsbDevonJKE(BGw=H_M;;DDf8EzC+LSSC?pHlXS$KK zF6#Sq9}_pg86|fQp^y6R#Zc=veT1EqXSLl<*d1vf^#kn?d#awL=jDCvV0%cu+spyX z3%*R_oE7u;0oDf_X#%2Q8|Yu=%}sux(ve85{VY+40}I*U;(RA=mO|yrb_onEd+yQG z{6D_pa|)YREk5)(qScz`yu==3mQjoT#@i4vi?~F4PjBwNfN5 zyb|EFCZd;BNeP-#@v3FSBrK|#vs|5?HYNY^L)v8A@TJg+(5Pt;P$}3al>dogEn0`t_~9!f_xQ}Y65ng z_&;Lt`lzbwSvC8fY3f;WQB^6<5?!a$ z?|dBQ9@x&E%6zHC9`xMuCo*5wAz?Eik_w?Cm>HeyLq)XyUdCQtNwT$8wnRm6v2!iV z9mw}YX+x>8b8ecYEO~V#a;u%H;cf;UWw#B=hiDb1mI7EXmv<`HcP`iRXSxaN(~$>F zGQ>UdJk~m?8K(3LR1?rsH&-a&^rtJN+;6MyVvmo#q!&~U*|I<{!)-BW5EUT1FQ8# z#^LNsRP2LgYeZH*gqv?z{DB;6A+If2FJSIHYoaZ8!V}lqU3*gHvro+) z+}em=&A|lI1+3j}r3b41MA8cxUx+>U(#Z?Gz_;-vzNEZe3BOQt61%J-*?rG(mE78@ zUC8loc1o{p;y6bf;wNqcd{AS--71Du{=?q~Brd-#UNKqu3tuwp_%RYc(+6pS(>-bp zrs1%Q5xqlHi(J~P6j%LN8B|l2&?||a-yy+R7=4WmzsgZfP|dfB0xHnZ|% znBRmI!fFGdZIMRf=L7)8C^P`*$0k$!yRFrV-Z@T48iwsjlc^e}k32$uoC8mp^>os^ zV_EJs1H1ZE%}x(s%`;xAST3OQL9v8VI5JfCKjkN?U@KL4MXDP_2%JJ9A#!}D6)US0 z$pU!x!J3!9HDe~|<604@m7jcrGS;*T!Ocsr|D7Z6#`6qV4(^56ed-Lz3kpz0^s6Xh zqpL<{bWS;DW8$9B%uG?U>;Vs`aR6o8qMSM#Wn7rg4pcRpZzU*hpM?Fpg_dkaR>%uw z`Mpv49yh?h7w&nj%|$HJ8K2~Pt4Nn!5b_C+sE%ET2RO0cB;yayL8oAZPr0hJ>giqW1axjX=wPDbhu{+AyHJVXGzY^FnK%Otpi2FvQNmsc$3 zMyz{uXw^(PD=^Ztf2A(!^I1vacS%Q~cBlXxmkV&IdVW+}+Ts;?GF)azZt^lj+s$+_ zk7xO4{bnD=D~etrZt)#Q_D3MyEG>hcusMKJo^UB8MC`6+&@i9IM%ch?V?V)}&C>jB z&1XK|mez^~hfda$(zs#RHpsL1J*ei-=2Rzg$2hCt6OA(bmVs#^i(H1R@-wY6=dE{j-X00fy?6bz-4hD3Htw+cLWww3A-6u+87$y{NGNh|7*hTd4K{73_f*Yc zx1!i5;A$+Mt^hu*$x2ue5B4Y`mP(FbRq2!{24o59(JgwSdHo=g0c_`lAO5NjxRHdHZt>h9w z%KkQ%`)JltqipGd-r>~Hh8M}jozKO;i`(SGA^oB^#Hf`=+&+&b+NX%^e&{12=&`T_ zzKu3Kelzj=z<1A&WBp{GDBPd8(-%Wg)H}1`yO;#es__r@4*0du>xsH67-L{MTwX%m z%H?3W*^27g;xtGRilvFoC8L63aPoY*P=3-PoQ@S;PwZ6>)z*8^F*&*j{aWmSw$Fm? zTcCTk__tQ<@y0Ob!~JiL3eQ;Y#-)t96AtkfHw1r@7K_>49fYc-^)QvvsF$jqd!@>J zXy^m9BzpnC&KC@d2@*O1tU#R@ADLfTlshCZ*g5hqc))(kj*>c-Z{V#70RA^9k>#AZ z7r~|T_cEvZBCRYq3{@7v$hIhN7rTW^fodq!xVpfRZwfcb@mYDlE2xJ#v9zg?*%a!+ zR)@Tr1I(!pUZ&^m|9hsE5)FUUK|~QrK8YM>IVqZ?i$34R;AA4ey7`WdQqd+!6_!jnlK1uQ=OlL zzoXC9?RP%pRj0l$cGpDio-pMS6l?Ax9)$Bu=#L6!i;EI{;8K@q6x#!^bj!A-$=HVo zX=?Vf3v2yuhFyWQ#95)+R1_bqH2J^`(XI^lY_6-N*`veIqr1?d0AjQ208PfJjp8-p zsU_j<(JZkSc3Ah(U9AvKC`$$`sPddzWsfNM7U>0=slN?3(w27dhG}eDBv*U3^BaWw zc%ESoq$Jxkyc-yW)m_@u-sn9vVw?`O@OEk*_TgU7mn-AEC*?yQU`0Mq#wP}~N*!o{McysO}(Hj0bJIO!D<))%3(7q|R(PiSpAX$Z$mmSYFHP2<@GO@GN5~nnH3YseFz?{5RkB{D8f3T z1k%3E+TCLh-BLKT!6~Gw?ALjLFHNq@^Ndj! zcM6k4!)2vSUjl9u0w>BLg2oW=5NeZRP&6m5u}sx*6MPi zm6EG%;0A@Nae&iGR$Qhwo(L}o$j0U1Yh{?@pUVJKx1yN2IrdN|m*A=FSw z4Nzic4lqL)ihdu}9V;La+eG|GW!_@qm6V|PLPt0=ql)cqXqnEEkD5G;4HRzMJWS@b z0o{;Y_jM^Ko|slvO`5!tZ8n5;%zU8wk?O{K4e@w!U_?o@8B&PV z=``jtE^Ve%*vbaLGj?2~cE>LlFI%WFr{$Ltw z=FhC0TSyBgUpel6f-@sJlQzjdm%wks8Fm(DV8xW~OdgS$8KHSTwzhivV4YUd;!A0l z_42x{^qk~a_)ng^K@-kKWBor9bmJen(^4w8+gQFTXO%~ian>@;mc1CYg!4PBDCv8b zCz5xp3d=X$a%5o|-Ee35mwvYI&8Z%L?+m!Fcy}}_$ILg{x262yNk#l z%86Q!!uzKC*_m22AA(GcO$RH9$MR}1u~Cy8<=cX%gIOd$cX_(_M#^g54?QV@~2nv9wInf`yFKQ)9n)fw=*w!vOZOJ7lEF2l=2Un=S4t zb??|N9@?La23hS!O#&uyeW&X_Qk-`HG_eSZTbDRq&KU6wmUR8kD zhv4hj^GU{Vq+LtX1*q!n_U7yzR>pgFS5Bjv)L+?I^Jf<#n?5i+A zyGao%?+WRIWRPzEg7sJZE%{ZSBPN6 z%}9Ex4$G5hfMCr$;|uh{yo|Yl;ny$e+FN7Ud&WeRkn5y>^sLg$f{U6#j_M05TVE3? zqcMp=sX#mWCw;?5ll5m1dh4&BK5_z-eCaC3%xW9&(b0;@KaI z-^%&v;2Clg(}A&HA@ll9y*1ce9e(QlDl9BQl@J)~_zxM;R~Q^@)b7AJW>)1{oqel& zWi+D|q&!h|6t>JnVi29o=gF2&Wulug`&_*j; zNuPU#iiBtTKmC|jEWaR3`QZUfZbZG}HZvRWcO7xVNibpt*KFG!3%+t#8z^*wF`k5m z3rkJ(ah#|NHGDft3%b6y6nf&aoX59LnO7A@Yusu|j`9SWf<!b6+Gh zCjCsQT=AQ=VVzYg(Bp!($P~^j?6Dh;yvL#@mgRE9DI+8u99!so;IFb*{L(RXv*S`{ajnRo2~v~`7T;Bb zyl;%O#6UY>9`u6kO1kIQWFY_3o!trSMcvOZiGe-B03UNN&=<-C6kv;dEbBawI(w)U zpM2wYE^C{^T8H;xB@RW1G{iM+yLQ>kN0BXZddeYWKgEdvrpBmXJjk`Qa7TfpWC=RvP-D{nvt3HyPV z0SbJ(hPa0aUO%aEz&-yH-O+c?llBxLTT~gNjFwMaJYhk_ClUQgEW*05=@TE)TjA#^ zd3lDNW{I|*em30~kcVKR>}2ycW39++R-X4KGYZ|dUeE}L5O+&Z3R7jQ?nuIiN3II|(0cx$>3;B;h@bfMUIC^lO9 zLIzy1uIBSbTGDj*qSd0`DLcRyd4+rNSl#DXeAG$t*huiIBrs3&-z-@Bd*BRvlL!N{ z_`?wdlR375hgOUW>U9e_D&iybq7i~?X32Vtz8G{0xHHFJNo-%mx6>9(gd?d~YF)egIFrhU%JMq=1@)eP1RKrhS!UrM4 zOSB+D@@rc#N)wjnV7NfF`TJ}RXp4g?<{xnUrg#Pfi)wjMfGo2(sS*yKLQFR(v1Q}~ z=KccySqd?Xq*v6fCdQfSjb+3`>h1#WSv9&9UXRFk;5pRMbLxI2>4&c4B${qqdm&w7 z>~#{C??z4yc&p8+nC}3Zii$@SXaWXTsBAs1nbt=e{4Tn62c!|%r~E>SIq_I0`&W~+LMLI6gQnzc*wAIKsOVAPYhzibkhA<5TJ#V*Ed;)+SB zIzh=J85VqGp304XIs%zU{013P03*mhd5`?KPyc(E4E-=n^)QahE`;46&V%6_8Po4- z7uZ}++!L=*!qUcFz6622A+8VQfADMs5S|4A!m|V|;?fCTfrLLTrX-~7&GY*kEc=xH z!Lt|KT5z?U6*B+eS@u!)pk;pXOSC4xIPAFs4h5wh%EdZS1O?!%&oxC}EJkRxo>EtCW9mWL!Yr+}fX1}`VOy=`tRL)$x` ztqT;VQQ4zbvf(`=TMF?JZ~l*N|Mq2FCHJcL{GW|qKznvz$Br*oU=cki_P3^pf->X> z1#FK!DX4E$O}@P{?*}{LhbI2<4yo=Z0c`y>se(U|;a~j7aH8GQTuH=FlAKZ^;aA94 z#E8LZ)no@lSv@0y`ce-7Pllpz`S7&0+rSqIp z5D&l;LIj4LD}EtFqCaSKizu7Y5u{e}?E2a>r}Z)U&uRS ze_{qbVmJ^JLquXm9wEP=n=IJkB{-7^#`ruEJKumNufQsZ_)L5uy@qX7S;5jY;8&i3 zY|mgX=C2_{@f?itsvz@;;G=ZQ_wi@?K9z?ojPXm{m52V2mpi9B1QZ)v0pl#DF$fj} zAk8Nr4>CH@ZhfY;q{*GLjp{E*4|;z?w5~$DT;}oP~UCVHZ~v z@8m>2*uB1v;8g#LTu#FL<;?$BCaO3OSqCrMa1!$M1zzK*0KDdUT&@i7(>#(&eiqR_ zQSB0kBTQOrZf7edaXcXfGCv~mqp?yjAG zZC3`Cyg)vJXdZ(kwRg*wsicMDoYzkmLPyDJ?doeJ{*q=uYQTrJecEoqpez=MBHgI) z!SGt;$K0F*PFwF-*(FwmNjii*)xj&VVAmg(c$@en)fEEP2W&+zcS!Os8Wh7$l(CN#e3;4_lhgnO8yHFB$D5Qs%R4rpXa zp#n*}MR*#E{bG5FXP3*!Q-k_YaDrY5qU#W#O3@)vV@yq>r- z(B0G9k$qLvq$r9{4u}u+w~jsD=ZN@Q(&dhyR3jo1w6-dZxB-_j9Z)bkr!9UZs7Vuf zePryCI!$+rjCArC7;4I0%9)UFeRO>T8ZbbO7lW6Trqu=C>ZLn|(m`T#n z-(&yk}<`v%tU5&s(vdJ{nr4gR1(g`dJ1@Bi}&618%6ak4UYbukk&aq@ItCFobqqpAS;iQ|Tqh>PO7_*qzTcU)JyLZ7bLh-7^re}O( zi)Bp+6hc8xVFVDV>;xXeB+eQ+6YPT|;e`Zpx#Xmzu(S{qWv}9i8~Lb=CzN<;IljiVGqG*``|)70hsUY{DRKy zypr)w!K~4rU7BgXp`Mh_=ujyqXgtIMFdRl_Vu7{$<@^A(B>QR&-_`=HOUMs6L}G=5LRZ8OCI7T_jXBB z+EE8Pc<~k7>A$m95kiSb*e&TK0tY0k>Wd4)y;a2R$?+lgz~9o{V`G2bVMkYiYG+q0 zbK4F_3=M2CZ2{Ssk+Ns2#>(Ola0Rbjd@L^_)J+yJ`Dy=-?KuoejwB8I zip!{ic(zd*08?FKI3Fi%VogTDU0gW{LvIH(D+-kXumx2JYE-b+nc=@2PT^lJhLJrQxYABp6(z*w@p%4PbMf zRag$sA2bM$x?6?ALB4Bw&Np5*CzqvM((5EK?&44saiLD)9=TvMEjI1VC-Z3>;Xr<< zfZnbCu_D?x5yRO#!uCklb#A`gLqm2z2>SX{Eh@V*HJ52zax=<&^y~lw!wD%Z0Ovu< zj?~zqtMf?CMemHM((PFovm#wL9YQRbk z)#gk(Mw*ctcR7ZBRoJe4?w8I)%;Iz((dy1%(n7c+1c}+xTxbG-#&SsxiDzJWSSon| zP94~GmJ8Xx@+VXUCB$-HtRpn)E{Kbo1jhM8nc-qU$VGhc!CH!kj!(Cmm2=fHu<^lZaa>{rHy}hi`kG`$g%|tv_M5nR4sTnahky(5&%%>voYnNcLX>NLv(#;^c zDyN5LT|C23TUj)~L|a=nL^zkpjCMGhKOu5x3TyF>UeLgtY2w0y+hUt!4li^1LCci7 zfh|?Oys(EMV>Zi%Qj;ZRPCo1seM){q|OxViXU}K?i!)|G=ioPKT zXTTWtcR4qAxvvksQ{8r_hd)uX?t+FSaddAW+|k8%eojYNEKgUqmHZq2dgS0pvFu6q zqBfHHgat7U2QoYruy}f%ao+U^AuB`^wO*HS<@szWPWerTlplC$W6t7k?%a@njR)S~ zi3erDU10W*f*!gxnEElf*!?O}P`C>2wpBb>C9q?B^=L0;B#FQ|{n3K`8)Gmjyog-D z{^}=`EurnCbQfPtvsE$wKsE)}o@XeOpf`*&JSbrV2bvCf>MTiOV~nbnh0ZN5%Y zJDQvhc-Y@}JdVW3FWj%fTRS@6cQk)cQ>s-7+aL7@uB);1Xc6sOQeItcSU&Q=EB!MC z?W8yrR}~p3`X=|_r}hT)4-o^YE2@I(Far$@2|@!}RMvkLCmGNB zOv`B@l^8;wDTkTuH~RR{&}94880grdoD>1FB@x~>i+#xQoBN+TEWLzFqG8L zaic7(^E^%qshuahR-i1IHi|Mf!)8MN0B_;!6PU`y23sMUY(solIj9Fx194u zx#10jZQGl*4L;HspOX8t4WPHCP=@1+FzS z^-e#GfZ$uqy&7rea)QydYI1PJ;5Dl$-81K2aXh>QjcntQHy8CsgzCwgnLsJ`eb-WP zMa0@lurYu9rqbPq(!VESJJu?JALE&nbHeu$iO%$Um{_entI45qc_VL$zA;FL>Bswa zI@3Ni3k6zJUGHZukbmG`&3yJP5`w9}q;ANmk|Ssg={dPq3kfG1*Q$GpUc^tB zdI%-+I*XLki%6MCs+XQ(j^`{hk7lKb-|Majhve$sgj1vgS(nbA*>F+2^Wc7y!7~7gDd!Z?gyKsS1-! za3~x0c_=8KET|6CYZ(exc_^Tk-jH=byoCZicR~P^5tBl0fYmewV6vNmWNobI;A^}a z>nH}h<&zrU(1TE$YKR?yI5iHNkjmJ3QZ<+6KkK~72&3nW1 z0E}yP8T#VVbEKp%RWCRNCt{BKhn3xgt%}9KNy>$n9H9XWL$l_MJD3qR-wLs_L=80K zl&GUEI&B^KsSQp~bRlZ|KAWw*6O%vhh10>x*4EtGqBa$w>26AFsnUGHWKEcj-nDj>}cg8636){GcmJL!21l({1LcmKEdTMtl{~py`J35Y(|x+;EYa#g>0>xt zGVlPKBKS*TUEoEz1J(t)TZr&p7#%{Hdb@{{*_Z`dj>hA_urt(0sraJTtw|35C&Y8!I=K~Mv9W%4Kn>Ss##|tj} z!Jb^)4;i@y;N5gJa>AQlne>zej@llX=RXOn(;L5er%e|qp~h_Lx${E`&~CV?&<(a( zImR%NMFfJ2!U4v>EV(ARU8^##N9=dnv9d~i+!tM}iIdZZ-Yr3zGgx!ZH{#=y-4*Q* zWj8x0XM&DOCKBatocomVY zLPu?%kmKwEGGAtXF`3Tz`Ig+GMd-?2^(MR#r&B1s1hp*| zw4@{PAo(n5D6o+qAmRhx(@yDUIVM|mlf%JQ+vXauM4@k&8cB58kQ)#`QN$SausvMz9F^Vp^wy* ze=gg!Hw^ZGSH!b6I|uhQY3nB2l8!O{U8&41-4$Evs>hIOwpPtjLZx1d+-o1poZSgO z59eQ94*Z{%z<9)9HW-iaX}h%Z;@rR_hK&^Ibh;&~${HEn0~C`ypCs>LSBb60dYr_~ zfn^mXGmxY;i9O1n)iIBe$sr`ZN3TCL@!`P2wYUyMz%H>zJHu|IPF7=2E!?PcO0BKX33#=eP-d(J3EQZYo|$>#W|~Bkcx@cnGK5YO07iK&ko)IfCxh zKZ%}O*d1CqMm@7A3ye|R$QccF6RU<upaXw5bi>pI*I!LLLOPh|OzkD2z zF45UY)h^C_;*3#wGV$*tW$JsJnf)cqCTbt|xS@jIsyI@sBDhWlY%nK5s|w;*UEniM z!FQG8qaPl>P}%OdLO-{{0U)nX;tVe4B=Qn|WdKB8NXs~tqQTu41kX4%Z6ob%BBk|= zNT6nmbnl9FFrAxD3yxis8o(oRVd#qK9(|u%)M5VitL$k;afH)ul}5VBLPLI1pSaP; zo1K@bQ^{Oot+8tOY@WhufzvK+ZRz>)JaTQqwB?E`lc>?ItRmTQbHc1)E7w*)PRG=# zN>)9m)J~R<&TX>VzTV{cS)Z^LAU=4BvQC3UYmml6CFp0C)}=G*ZR=D5ZIUu(P_H3pixF=fX8&>!D1)1JIVDU0MVrgce z#xI2N0i^bNB%)8CpzKUc@>NH%8!Res{^F749lcxjZ-K^X>!Wk7Lfy|3-k_{gHb99# zE5o;N`rN|m9o!dFLKomUmz=<|ci8gNU(!Y=}zbMn*|={jow!wTo|wYmX>Bkq*=({N+KsV zAH$p8-kM*kroD^8aJmQj>@WLEriqeSO%EQmdmNUqH1JXa+c8BP@{T++;tt{vNn#Gwvv_0-^s2{C~cLfhrh)4#{vw74}k+1Do0d zY45=9nZLR*6WBQtkOEjSfbOHTyqUhT^`C&|uOa5op${=15%mSvw*=RLio1alRwhxB zkR!V8kEuBlqG67wzC+smOa1FZFC9QC$H}-c^5KD8q<{tg4yss{7%tN@JM(xg(|<>8 zU*sxQ{lu&=L*DJvKiuuhmjLBDz^*#L<@T~c?+{77(B*%HZv=P?r~%~~7$NU@=^qUC zKDD zJ#LnI`lS3Fk6+*g)B|9quMSf2hW|dUzX=1r`Tm;@T|%i?f&DyAl=RR{ zzd%4Dg>~!G+YxiPPTnU+4SM|E;qn8o#18+Q8%_yL^>y@a22fgu&uQ*j`8M(px?7qS z#t#r-Ye=DY4qk*t)hK-p&evKDCzc3Zj3R;mWN&-#vqW{!6(#b@HJ^N~t$t}+sg;`q zE-gr+R{6YeX^o+6l-ab3*u_3x)(CR7tH(c zY4D33Tk3Q^UMyp@IiXMw=44V~YCl8o_9Sy91J4&52Lw2e-MSea!kJr4w;_9YlD0cJ z7i&MEonG3mV@Xpyv;?-gwB+9OH6E{dDuQ z_0un8d=GoB<3|tFeV5D54P|zf)IWZ0o@h1lklqp|FIK-25MLT1c;3?8|0Oj*s;JQA z(k*dBplog9m?;Op<~z>aJ+<`>L3-`ALWovjbigOumf0cF)k`1TsYv?=i$0^9o81-x zGvYc;f7c<~7Do*j)Et%*-}Zu?8T#mt6{u^VRvZO8nRn-v@rRH``;(a8G_pF z{D)j8gZ*#GwWzC;k&Bf*Kn&nw<>L82T1hJZ7rB;4EsYnPPt%~BVfQUY$5M1(`#DSC zp~Q3Lxa!k`1qduZ_M6Dj|4FWEMIuxq3r&r7MzX!*eCtk5>TU#p@D42Dxp<-f{4%5v zOf_m}Fq>x{OD}L!ym%xd&`dmoWl2b*DNv@j>YlE47(Ye5O2J*hZs9jQ9B!zQpjP`y zT{N=;f~2rp>h{O7qh4vwM8Ezz>0wh0M_H2pw$JD>rF2b6E7RIc{+T1D7!{caBj>JE@ynRmROS46nm6*XTBZvwxQ)FA&Fh*{US;D(e_Rj zE$Ph(|E29;a~Al)!$H%VRlxljNw9grjkyLu;bNgR!%{k^h80O!^zaMG&b{BU3rk|@M%0CM2W;IDWht6+7iZvF7ah(_ z00STTEAsCRyxD0jefMW3$RbuQ!}=ajIBWV)R`0C~QHAH7zgW2m7i}Pf*k5=086Q+C z7|MG$Y)THQR^f8B@$P&)5?nk#P?Oj5=s)b>pk!!P4F3XmxdF15F;OAX$x+P9W7kQo zLN+S<2-k&4`}i$Tp5G*zpz&Rp`#-39aR8v)S0fIN_$NNxo8~X@e;*m*578~gpNmKj z2?&V)e{&g`Ih#0HIsE6;D4RJu*aMvZ=Y#40Qghe**7;F$BmJnkF@up;bvGV`lh~!| zr4;AG1VN-2503{E(xQ`Rn`&tp%p+-)q{tJtr>*{D=5GH2l2Z`z;wmG^C#WoIYydJb zlM3YbXu`SrG-u`Eq51xrA@B$0jIT5143b4_HHjcxh|+MR5DyYZHVp-6M_?0O!`$^C z5kN6*gmFzq&jlfI5nV$&8bm+})56>t!6DuyA`!qk@kc2X&K6Npo-#16IJ{Pl*x97D zSG629SRHqGMVgIQl?El+CR)kF>DQ-SksVv=p41MXy4xuXFo91UZ0>KPgTsT4Wd{vY zb&jG9Hf|cTq&fw1J$Utz%4saKyRek;=x#!ei`!>(!q8vse3*u2i5)8f<$$iC`^v4* z9$UdSW-uX(g^$ncu2S)7Giyz<;Xq0<5*Jy}}=|%gd-vH}oP?GV^ib zmwpW0Ie|2$8{8u@Y1e9ang9Oq&(D5}h;8S(TScZP(<_BhGDYV2*j4Pa4wZ9mZlt#o zFe%N^fGYpSP18RXUoUE)RpmX_!K3dl_gGNi`B=M>~Uq;T`yGW^HF0TZ0Wlu&(!;SNjF1Zt^G zRh`lftMBv)v=^ZrH?@X7V#v`}{-we&b(9bxGn92&hKG0Bq}nSri#|5N?TBD zj7_2HK%{;v85-$?mIi}cMa_?HEm5wQqUKRvt~p?WezD&Np^2uT`lIJo`_Xf&6*E;B zKISh8Pw3})f1hsByg9l1>EtIlRg`j~qqJ7hxpl%>es@e})p8}FJ@%Z67J}BKnwx*B zi1wZN;t`v!%ap1VASk&04c$B&Um9yHXsOQU*-QoRc-&vp(<#y0aN}{aISm$Kz_{4v zq*uJpivIa$Xr+O$k_qof4LV@3Q8(R?Yk6>$D|rV| z>FkIKO7=!qh?}nH2Z=>%nu|6sPjQ*ZFOeD7nFWa4J=K?HWmkqsDd!A0zH9P=fY;42 z5VuhJ*1656yK#>dxo|{6-~dN1I#X5%AbZ~8!OzUTrnn&Ek^Bfm0yuFJ3D=}#1#8xK z>AK`J`go9UnN86PYO9)xP*@s8H2?}v1 z_b`wfj}G-f9h8yr(C+1lc7Bw-J-+a;g1({MCR_L3fMsAkv0}X(ap`+$ zLVv62RT1`B58;00SwB;dqx^@r)ZdNcA8F?gU8r_Ika%1~GPpqXFNpMCVgLvAlmu@I zVzP%Bv^{G|{9K1m%=mp`!qEZKk%?YX{2bxG=r+(tpud51t2Yw0o|h z9y=&EJka)d8a~@_xyUpuJ#Uy@yUq+OAQ-(|jWyLzQnn(eRl zr#1Ald2Nd#dGk-dGvhE3^Hm;gNT-@bGxJwK%a3`0pStC`8iK8#Q|;K&OTFNxs+XtX zHy`U-|AGJaUA!A!we$vcmKK=Pv%QsiBAN&vt@i()bZQAuAMrn&-Q$#L=+JrL7?C!ch0`Lg#_c-Q zwXTz`0K3PacztB++9igL zheNlH?bEDlP}|x4ta<$uU5BRw=uJ;d))jA8V%9G*<1)?OZhQb>|5%T#P~*|D?yu-3 zt()V`)+U?->aKZ0d|MXcpAouem~j=6pm8t6@o-ar(YdcqNH2C6ZO(*<)AQm6iKWHF zKlA93qOMvRMaXgqe>t#n9^=O|YX{?H!@eMoc0({?Uyl}vzvZxQq9jbDb@PInf<&A+ z3!t3rj;6Gahj-A`&EO|#;B6&76AHp6&j=k(9kpUlGye)yzEJcV(ds5edOA?2MRpR> z##0&zp&uKecHlg}RAwS9?+0*WBtD7a5pt}Ray*5heOv0viw+kRNpIGTR~~EJ+E?%D z33pS+5r=aC0?U`%b80PUQDSK{Z-(!s$O;dZO-hGV!3`%C6d%W!XmyjN>J7t=M$-lI zN0vrchgrv`NFY=50YH^FX{hfWthM-TZUiy%l*mFd&N<`hkUBLu^X#0CUO5p*18u0k zMplak*%In$aS0-57ke5e<;#Qqc<4go;yzMklE+hF5SBsLq0aGDTWbpkwzFyfpUUn; z*UfZ)1B08mHs&ys(L?}OwWzQY-YajHL{dHDS_PxoP-jshR1;1tsIj#XcQbFaGB+Ke2fOW^|HviWVPoJUTC z*+3?W?4X+Oc*k3dZkWq}s%WliupT$PKWPDt)IjCom#wh0oe6ZOsIc@Vh(4g9SV@{$ zJaym}0@-d+k*`B&wlaevOE*!Ddb3zu)faYCMdScq=xwVF(J#PS^h`lEj`B_j`#OY< z8z*JJ+jJGK6EiuzF22g6_QJ_pXfDW+ZMcOi-;#-wc&K46cMQwBCWuMw#)!nmvWaI8 zgpV5uXGkeYB_qBz6&1gnHtg$M1IO)qg@h(wtzw&KKinC)2X8tH9@7gpMVBeryvl*r}~=>-bka)T6C z+L^ve5Kay!2Ui{<1EuGWy1U1S-fQnf2~CD+N>41XI4Ww`%x=@i(8G?;!)N>on(HSJ z!f9nAfG?yznP)u2WZ3)*|*S9qNF=xkW=Y#@mOj-~oLhCv3Ib_S?NmPSp zU%;>sXyYaw8|lmOZYFmivBgCYNdt$Spo4{lcY3x1{4{LZ%;JxoXhlfK}9x&U~ zB1q`)c&e1GmBr;OGmsr`n_h^1P|%JuAYfJo%$CX0-9WUk*3f1h-UM3MJtiX+f1y7W zsA)BWN^1y;;Mg?XM`97`Dtu+!G!rx zy)=&&JEGZEJCv>f*e0}IOMK8i?}+V0l9alMmg*P$18P47!_1^*HP3~Uq?2zu6FJje zELXjLGU_T^f_1p)FU!gRb4D7giuvsF?P}4{j&gublwO6|0<|p{uV$809_L2=@{vAo zV>vmkLR`op`dNXKSa^OjdC4&M|`F|x$}OC7@K7$32Nf$UPN@nU^c9+eA4@o)bhEqyqmK~>Z#v5qjE)i zsgJ|DRlkpvRZq&tqzZ8Y6++t(T~eD`mqg*0i{Px@G6(@K%D6KaH$h>mz4Dxy+X-FI zz=5^HCdG#`!Z>C$P57dvFs#Dc{wO#a9`_+n@i>IZa(xW=GCdtX+rEnH6lQW+zY^CX z`5a?Wxl@~cWZW!pFJH&;IWH)!x3%_BBJU7xZErr>v{m~ekECv9JPB6+GgZxxIq z8tnE2?Da0Vd;f8(IU$=tuqZUnZ%SdMNODbBN5J*ovG1c_WDr=1PeQl}>otbE)V}Zj zS5ljf4^ZS94|OQ%ds3Uk?7MqrwA5otn+D2==G~L?Br&O%ypJLZ!G5Qo{wBp7>^^^M zn?WI_=Pe3H9pljeirj*L^kbz*FCG42p8}be953k{Zr8Eg_A7}?7{#>Ye$k_uXrX5w z=r&L1ykm4YkIV6vm6m1bRXgDeO$wVIYf!BnXF_9`R2@TI&Q#;2kQqkC+h&%xk+P; z;#h3^QB9R%?UmpTHWN>6#|!b2g*v>sSYb#jH@`tF)8+e$@hP@pC%t;G^(Tb%6HO_v z{PnilE48OPJhKtM_}BDTk2ohA#BcUbs2%@=ubBME&9}wNzc!r1n=W#(h$E;>GSt(L9vz)|&nmUibyo zF{VGN7rEFVI}#S;!oL)f|6{`zs0F*+UxR@W`q-)6Z!5fD_u~y5ND}!}8RFef*6|TW z$wT`&Rp4LBh2O<_2Sbyqg*aSbF8}mNeoGjW#E)l_5yw+yWMj7vSr~zI&z*`Alb^P` zn6!YUbIr+%M*k_SVbtIr{Hq(24fnTE%6VeV6SvXgj=CRMG&L4*#}%@`l>=$@eR*+; zqb)J@Bl1pF;kRf^kpN!Ak{J^V&c9in7uP=Htg&`MT0pI6T-=?Yv`7nK z%ber}VP4CIV~PdL7)Q)B1eYe}4h4j8Q4A2pB3LltNiA%q3@F80KkCzV9@wRARm|yW zhBfh?JiFUG23B9)WE=&&Si8_h1E2LAr20q}eOQuV7LAa=xu+ps3LL7gqDrv@GGUTD{Z>u@`_lLAF@iZY|WQ8svgtjl}0V7zJ(OTLC)0{ z8#lEYeUNJd*%%1hEz0|8%W$Y*BE9rOy!5@}_fwC7i4hdUL(pbL3 zsO3sSGq6S!Sxtgtuv8U*r7W_b=#prU7=XL!>w*o+EgUw({)p`VjG|DsVKu#4%*v{y z=5U<{h|f}-CL>5VZe*}QnlCi*ga2%ihN5kR;^nH1I=;hg!ot2mn?KxLg(sX_R0wA8 zPjgiwyTn<8KjoHz|7g75GiT<6&2=NEmxAc$3jd`olk0DK#bKNjRBJoMrFB@|MDF<1 z9iuxR*GwBG?m-2v71qu$PKEkfDcg!sP9cDMaAfmkQMby24C#i&d1H$Q82FaQ^M z!}G;&^JUE5VZvL!3DS?Q<9hL~Xh)v)BXBM`r2dD#)N#hdvBaSx@hv?5_bbqmi1L1F z0}b{Mkv-S;Co3JdyzA6L)8s`~sZE^9m1iaH{vEdVws(-*V@^Jx`W~-LEN$zB&45@z zWg^*yP6(bxN8YI8A19qu#Wl*(gA1roS*&LWc-bF&bh;vLZ6hjc(A0u%f<;I{e<&8j zc_$wYVdxsg0i3Q7Kv(G1pFMreJwjV1uw$k+;w_MvG{`ecxE+dZt1zXeR4tdCxu6C z`w%PmGZ6iOi-yQWNVL-dPIp_WSnnS!Z?;(z2(LUU&d{dBf0I!3nui0in0rU1L+8 zYE^e6bbOe3^cw`euj(_DR__wK8GEx4xCC|zc1+%QZmGNZEUa!X-d`9_oJjcO)&}zd zPVLNn+c@39JNPm@{L)ljPe$F)iei=>`|%M;ECWNekb2wYemxf#%O)!U0P_}<21;h zdi>Q9*b}|-L_Bo*A(`t3r16EK`#2$q?FTg~uurh+Fe5Ph)h+EGJA23bg~sw7v~Ba~ z?3J@!$RWz~CF^lEm7);UHWO{Js#7`W(d!09(|mC^(tjuxs}^JEFKGR+B9y}PRR!tgEtZ;wxB02t z{hQUwO47^90=ASv5Uew`dO*)&_Xk@^ubD#NS-keNei(UFkYB(2W8+NG`*H~MP5sIF zvuxPa-0dbrSub+w{38s<25bY5Qu8DL13@1G-Dlo82j%;R9+`ojHG!^OKpnbTe4ICd zwu1`-rhpIOCwl%(6=aI+Hi0Jo3jCbp(ywj-#RgBP#}nK>^iY}$oqfI05-hFQ!Jqe8 z6NEed_9&b5{5;4V;tesJN;NcNQ7$u+tEmXOhR({DP^r(kcsMkoj`b43>i%YCcDw%K z@S?YUfDJKm&cd&XptJ(?^UIW;9aqES%hNzhE90hMmBcc7eVf=OD16&E^qBD$SU>(l zeE9uB!uy>Wll09Y$pH;6f$u8^H@DJCkCum$hny!4<5uHIU`*C7+HKh@@S-hRW%)+; z-@K<+xfJOC1A7-TF)?#?R{e2$|G%(z8x2$qw6Gm$OVSu5Ma!!pnW{7}*QTeSAn8FJ zRzp}UhGsd6WDzV`au@SLLwlR%yp5TwlV`}q+Nkd8C4yOkrKJ~H2q1&9H^{#LkY<}j#lKw?POCQ^FD-ArWB=& zbP}y4V=~SXnVs{S$;Umc1mw3s1X2(6q?r4!qzEi=g<;*LX3#TPwR~vP#ka>caTLR6 zj1$BF#v$Hvk zznGNjFhY6kFlZhhkFC?R#TVb)r#WCw*sI#><4j1Q;Wtjl|HHmB-(74gFJz_gq!iA4g7 zk?aQ5E7oBBCy_biX|(xkM$cKSUEC_0sgd|6kqMxSU-U;PGIH;$Y_Ba(=NE&Pl6?}d zs@7RA3In^=oN^2~TGaO2)};RMhj;m>fxnVcwj34T&?NsONZRb=&ziCAbC(;* zg04&!sUm6_Vo>#L*0QCME)7!w%^D4NMA5*YWn#G5h#n}!0<9X4sHT%DoXyYe)o_U~pSM-(2!~UOr%y0)y325OFxN&ZCN5ycu zDLnl1@_d}|qM|U*UwP84G+X$UKYdJB-hPu`u=n9&5uCZFuW)%6uOJ5Jqq|9A1yky< zt=($g%QNr$RRiF5fn-PZrM4JJdNxg~sm!|%7tsD)XOi=?FNLU)klK(I=D{kW1Bvi> z#JcMvB_@doGaUxvwQfhlhhvgD%YSIrQ^`6$_t&~7O13uLI+wj(`wi2i>D=)!e9rmH zSduH2AVyh=xth%kxXxuDV6)Z>!%%Urq6%y?BwOblcFm@uY}_5MTx3+NHhWhyYYX>g zy@rxcxYW9)62s)TEz?R&xiMPlO8`PPnB%SS66QnZsZgPdo_M%O0E&~|vhV~@_3X9CEbFL-t{v;R_hu$~v(93f_7-AJvxN5LM4ak@fVJyW}a zi5Mo-nDpI~TIs%M)*KP29k0SxHt%Rp?%T3&+a+srj8yEIs^Mh-k z>{Gg8f-FM14mkUENbNg9SFmr_Gd(!QCa63{?&z&+a;Y>A%_)uB9ccS$sNNyTjU0!52OH2o|Y{z{4883`Q4i$y$z5}{p{K^$mv zCgo!BM5*{vsrc~~BSFIP1&rT~oMp2`6#XBWZ~jH2-?U+%+fdUq9^J^>t%ts4yRo~% zy`ANob$ZC#FNa_GyLxap?y7H}H*vRqq&F1c_N+etSb!z&ryr=@Yd<(1+1N`B^tB5V zJikgR4qU@$nf%tz+OsE-yvRaugpmH+(CwwFEk5_It%a;DN9|3DU+E4t?|OY1{P$h# zw~QM~jtT^1zzPKPBlP*-7^Bo2Z0(Iq|3{eke-ZZ*t^dpCqfwWtUi316^%rAJGd{H` z$$+zkDGhuTL<+0MmIO~C#!X^=wUcLJy4o(o9py)v=s_kgjFJ?v5JFxWYlbUw&IZQy*I+J{6o&cR@D)zW=qn8edjW8|3YBOn(Jk(7giEuh zy2bP#F&-SGxY(jgj*K&IJqNk;wF$?^&&n^-x)%3{B~whr4Q;K={5qm zIW<=4USeoC*ca>SQawA|bZ=fB9s6#^G}LR$})lQgunj> zyr;xqTdS^>oS$ooo<$C^imMp~RIa1<4UnYxuxYA)HEXW4RFm5N# z?o0Ql2es^jI+0dDQE&D~H_yoRP!x`HhiGK3B7o~y8T zem3$zVgAV?=#y>M^`mh$II^g^PEK|7M5d=H_TB({t43l6bvaI$RD%3b!={8dt1?ZimOA)fgih6KM`LYRSB4mIoW zl#GqIfPM7!Nys#l&Q*0fP`b5g!@lNF0@BSVi`9=5sl|)5H6x4Vs?6JMlT!GqapWk8 z*lPQU_)yJ5-kgMaE>`X3-S#?<*-Gw{ zcwYU}$PxEpl1#i7l12uclWGm#0xVxVZMvFiQhi%Xkv&JOgv@b%;gT@(pM{q*vC>7J#YvfW+y$lG&hIeGio7JqiuBDwL@aa#Lp zD597ZFCD8pCb$Ycbxz8Y)P?3=%+uZ|l?KHTRM*n(aSe?l6st*+Nx35K2ISkH!a?t5 zPW2ZPn-h;kcI`y0Av4A_RT87+6*i5%nlvyMTEWeiw(V-zM;{h8y0x-Qz135%qIlv3 z!@$5x9zl0y99_4lRCV($21WJZ$f2IrPr8Tpk7G>`y=t;T=5W1weyS12Z9{-#LMPN2 z|0ZhWc0~0>Hj(;_!~u*7(wp81_U;dGsy*;;Ru=CH%Zf`?2bguT<36FGuA}w zYJbt*WG(RpI%n-%Uky?~bJrqtXba|b(Y7~qVO2&GL`1x0bQ7e3z*H^kV%j&a`9S>uuDB_KfeH(6; z6y+)ZFeOlhr$VLn7c?9ciNaqo?np{XGBL`l6p@GifSdZ=gpum&1N!Q4!;Fj`y0(;f znj*i0;5lx(Pnouqo_l=t2yYz*A7WPcj5AjZct!DXENdDu^VKjXYd-qvyqsqOZvac~^Ye3Fq&0sk+)#-J2P#&VaG_QPXhQOC8 zb}hqrE{WoR6M%cR*uO9YtdH< zL?^l?-y{Wc8v0e8=L!-~II2H;)E40Q?eBoWUt~4wsF7DRqH!yE_r;|)Nw*my5hD_hYN|7%8jMD?bo15cPkZ0zRO-?8SZw?0QyGbpeQ+|1~Na>idho=@!%clNAEyEk62Ir6Snx-WZt`C zQhs@g`gc4z!TTv+LGM8d#fQmGa{m!`<6-aObw|Qm7|KLfevF`LyS3yOtL(S6{mnH% zx}LTq{zfGJc7v|fhL!Ph5U~ytjcOj)xwUY^fS%p ztE(Rgu$9TOCzGeNnz#3@=%n$?tNFgrG@dE~_VEYy%t7H#aP*&O%0EIy&OR^L1F5D9 zJ;s?k&```;IjncUQu^crQ1?6yKNbG?L)6aD_r$(R*Ei8LRnTFVSj~k5^U68(g22I; z<|v$|YN;2qHUBc7%F?;SQjdLF$*do`9n9PPx@hDMXv zn8NSLD#-r&d#8>(2ENPg!VgN*3Vo9`?2))9mr!8(k*-c)c8-2~>EGe0uzE9_T}x=; z#O2~(R~aJ{{{N~}&C95St{yzOU^=XO6sF!pba^+Dm1g7Iy;OQ&SF(1N608py{Q;RH zQ~StyiGu_C4=CNSnIZq;w?#i^M1yg7fMf&x9*#oqMUI^`WMqlXBrL@7?Ka# z4uWzg;3#wg)!J<(a^orAVWJm~j>joRXENM5tmwEpZEdb;iCIwwegDjn#O)JjC-ViR zO;1al%zE7?=MzrHKB@C?fVN z7rOU7#hfj#C;$`I$Z)==b*8jga5fsrP7+@TFwnPv7;TeB4GILezU3L{QYiaM z7cV&F{Zz4t$BX6~%HGxM=N)vC2lojUdW_OrKt zPD!Rc;Ez;Qbxl^;*}m$1`)Pbafu59^r5fEf04UM`s-Pa0flg?DWZRQ+zmv;ypMG+! zf)jzx?dCmDZxn58M-Wy%jg7x*RorRn`o^d&2)o8Q8Jmho(yuA7{wHfkb-`S$>0nNO z*p_%O&T!yl?81T#TwC7}vaG6H;E0@sNl*44V6jyAT9dVF zG%IbJ5c%XEKi4@Hv;YbBIenXOkkcSxWo{OO=b?6YLILyyrGV9#;hO+>_rSR~x?!u5 znunDmwE_MS+g+NX;Pu7_LjR?W!F|ULH+)% z&iUDbi02-in74u#2Xga_9T_f~#jCc$6~nnSN7xMlGo-h0sNzZ7oq?A_mZ{ul<~Gd0 z3~5@^6m|`>1fngsxXn~z`3V&UhCtfZCUKR-8>b|>Mg5?G#QX;o(hjE+S6^6e0PgKO zg7M`wO5b+b#Hm&a)pq}tfW*q|y(Dx%0?Zfm(1=IF@pj0Apz%?rt16wSZeFk`cT&E> z=;-}JD9-O?CC0z#G7j_1$Tiyo)VFN>Rxdrj;F(Q1=bW-QzAJP7Z3@XR(-!=aUnom< z{baM-ni`C^>NMnhTg|)5DWkWju*hmGSG zq_Z^SN7-CFE|;hxWFTqs%7t`&i%%zzxwZkbNI8Mr2oNlxdHp86oUh2*6Djtl!jrFG z1b=Elr5+H!zHhd1)hCt}09pB=F{a%IdJrIC?v2s)pmTA2UElQud-_!rQ2GURZ1emC z&Bv#YW%eMUBNt8=RB4)466GA0xDL;kTo(H$Zs+U3+Hl8Y8BP317Vc~KNE zjf1UQ%1rWA&OuRFysfu4Xw?gOy-ojUY3-_=2jG;a9Bjb~ckli42sLumVHL-c^}#VG zt@$NEkXHh1qK81HI|1ZX|(xoNs2jUIuK=V_zLEUKl z@kHzAJ3>!@%cFT4+S%~^!_mxU&)Vg~Q7OoFKJ@!UD`&v&ncQz!r_t)I_tGv-@^VfO zu-RI^K>gphN3=)|pV9u8dsAxi6+TC&Q`Md zc!_ChkDoo-19bzd-NDwNzrWpsy!5=hi0X+TIe5mcOYK54q<16i@IOg;HFvbT=}I8)XV6f`(F~z zaUQg~zw!m_cE2s+zoXRN7BlHCFDLrX2wW*XD0YhN&Y)2%EN0Bm8~xp4A)?!OL-P*P zJhIz@aLE*SLsYwR7~}ySfZXLk#+!CWjxWHcWHa*SF=7qq7D(QZKo-sN1w5YYevn4j zp_31Yz7LD06tz-xx51roe=9p~qeIWfNQj&%EV^jsP=TmWbd9oqcXFuFq+I^Q`S+pQ zFs+}c7jP4*45(?R|NkGl{g3r0Wi+6po#5AQDIYrMHzI6e(86y{qaZ$$3tl5y5@VKj8&`B{X*PV-yxhswoc}&--2H_*|X3_}m>|*KLDv+`2NhjfxDbWq+S;cZC=^(f~%IYj7 z^$Mtovy*7e@w5(9Y2t;4$;)P{eG6a&8rn}ut)e=Tw3pC>YGg#D2_(avlAKB8Ay?+_ zLOK<8*4+CB@jB^`c3AysNHC712xsB5;Yw?ETUM7FsYp=u5%(}1;7rWCm!qGfV0v*x zV&FMGqxKlBa>f?(RE9yE%u6iA!;W&>l`h`Z%-)WFtGO+dEih9`Yba<50M}&l1|GjX zTn`a9#HyvHwNx+tIPLuoijcg-g)&ztnHuxwx4%!dq#PqFgU7OR`ZlZ81-W}6>t9s1 zZY&F{?}9*s=0#2GiNDmObsOouj#M*yH0Xwr=2>I7 zKQ`;IXj3K{-{6oSTDtTx(VG_SgyXg_r*y(0e1pE75zzd{lI!m z0?3Q8B!Hme3^fvoQ?!p=R?@zn@A3&BaDs#CC;U|k%M&DJ*e$~L1S@UTO+i(>r1J?o zaud=DDckZAWW^UsJ57u}b#M-N%CY|Yw}NmJ4uhK`U4w60&r7Py%R5h!ZU-_toF!(u zu+H!gRhM3%86V{dS$|&e{@P!6bkNwOVWtSmc8xjB}>&yI?W{nA0pFjHq&~&uC#ezfq)N|M;p1 zjAabd=QWJYr_-2rq*BD@_=xv4QA#7+U!K&ag{88_kZs~gUEY)0ua=qE_Q{Nt_L4Xw z^lS4D?!=Qz1Jy&jmrT_@p^c<;qyU+{xt|@VwZ}{dOHl8JH+(0r-QX}b6Yc14w z93JC6j5vfU&EX zKVJKG=G&9wlegj~ti&@P%kiHZ-!i4_niLt#r`(8XjW3dtGZTAHPDBL(8%K|e-^Ae~v1#orI|tA2 zqj%KjHdATpmi{KUTkYd+25C0tLlD+O%?EQV$d%vNvy=GVwM#!F5ER7e-rY`nC zIyhuzRC4-i(P}j`D<@AzO=WK(#l7BbZ^M}+nk%t^%6*j~mm_JCllS!fWVloz@ujRU z8YHgLu!5jqjmDP^Y@k7gw zY8dtBJiNxkfLPb@tHRUB{TKJ6GWWwiT_~FCL@i;b(1-C7Yy{tl-|M?+RHz0F?qfkX zEf~)++6h)gAl5!)Suvp~?Tm4P(fB3NJM_PQ*OqEl?o8l8Kz#6l4ebB?d+~qzuKh0u zH5y)q_-Z&`Jr@*BYqH5~Rtw6OUG#QY7ZjVy+VK>s>zeDoC>Fz^RAzbBa>*7hu0l80 z<_m)1V1l8+;2wX46cehclv;^G#!yl1`RzneZavY%J^QdGPhFX#alDRB_j+CP25jH< zdcAi*6N1$X)7_9p@?zjW>=FocFC?&ZPb7Hk*Gf0<@e6coeD*2yzkS^7%fC3c5k2kb z6I~^LMI1~+ADo5w*9wps2oIw4YLzF+dpL7oom-mCwK=p=P za6ow}y>zQi)rh~B#-o>l$RPc-m$=kVrN0egJT^nf%{0H&0G`r~w%bR;BKsG07mL({ zMJ5cgrIm~Vf#km$36ATs;VpDZ&mz6ovYDjbEqhc``f6T&1{)?c7KpUsRtvuN0ru%O4&%QN(VQ~mU5#;rP|rHk7UFp z?k*V;Y(Bho3znTYOmJ?2b}}8)nhR#A|YyX=moBg^aH0(0q0@Tn%HV z&YV3Nq7Cc*_TmQZ&Lxvc=fkYz*=D9#UtXt}NjJn9iMeXbQ<7S5SGK;**mpvU6jS{O zb1Ns8dRAr=^VC6B~L`+#;7o z;qFN6C*~$195+ck@H$2TBax#ecZe=?%}cvQflyjMhk0hp=3`vFyKE$9zPI#N))R~c z($g(FJi#?%D5u*7r*t}FJ~Ra~#@a-EX0+0@F{tMfJF2S)qh%7l?)|0=fB7RAvf~EU zUslw@8d48lT}jBA9DRm`-MB=TTj^1k`ff|%(ji^mS#}{C-qXg*ox@fq>&+z?m2`}` z2H({ZDmylOS43-G5*^c!+|#%yK0}s;j#hsNp(~uz_fV4Z*A8-9=~mBIVOZT2#`ZkY z=G65U?tFD12Uo^CG`oxf^j&EQI5;)yw^=Yb=7BtxSJt$Pp{eBgm8w;Suq7JVALLqnI`K?^Ow7 zflANn3(W^iT=S(mqULmZDnWuvdo%2CpGA9h&)Ly^`$UtG-Cz~Nf>Jk(Qa7<=&zw#7 z?|d@3-D)>P7&Cj^7&FOWyWG>?X~ZGX96mO(GfBoF)Kdq__m&2V_n>yw0Nk}lv_wT< z8nB^91?ij{4?bUz*VG;o>K=B&@wd^Hv-2zq5bB(I9Q#W-yfQU;HF@6L^$*ua(H@of z7tgQ-<+xzT(yuQDv@}^9=MjtY}>-PHUPm&njFDy|b2y1ixswvLDA@XaXGf z)phid*4YF|V_ZZDy{0G2ZosaWkv0E*Or5V~Hyhj?FJB1l;iNe8li~=>P#e2MfhXn9 zmQ_ELhO?_OQOSZeTZDh;_bG-tgr63r=jRnE9|2UC>jJmfjw^dl{E}lfg(z@Ze7v9q zUs+&%7!;u<7dhyqEx_cOlE%gKRdDTFS^YsTzwU(fM*ub3)(5M7jTKAWWb;=!6gdP@ zJJh=&YZ$*iNvi3Bq*>>b1{jL?$b>{l<EOZ!t69t(yEf_&+e>-6JMjGEuRe>Cz9a*!*;j=vc^s=%Yi8_;7BK(`Jfh2k0%e-Q?YA#dpeF5_C+Fz|{u6ENI20E@Tst88o{Hf!$6lOguSC42};^%DwKB#XG0+EdZGq3_@d~RDNRM*_`(Z!0+z_=N7YCqb|6M^WGQbqrM zhK2@zd{%JI=nb7M8QF1B49MRfB5u{c$XD&MH)CX| zg|)H6>u1J8o{HV+Xxj(2HgX1auUKUIezqsah5sPUKfm^Flrbi%L5w%?FZ@-*4dz!X zL~OyE9x$Hew1~mtz-S!z%dEXSl2)4d9stP5RqT!RVPvE!X;d5=Zh~8j(O{A({ZW2- zk!sXO^q5en8~b}00ux5ouu$j#0vCG)pjQY>hshNw&LIDdD~f!RzD8JTgC|I3f+>E1 zqJA+%2ATiMbRr`+Y(7g|^{xXC4qWRu@Zk~$45A@ZF;7a2=TttFL&_%PfEx{Qb{1Jd z9+l2KN$gwkCTMk-{qYCL6W;T64x;%C!@(BH%u&C}3mq;xHk_Qh-gY{DvJvn1oV8rb zXn-n}8d4bHNK%L<4(@1U=ixpKzStma^-1#eXg!NF%%3i#!VuNZ5U!=Em)cpW=MZ{i zLkyU~vovNxPQsyiI#!eQld&|&kkv~D7VtZdLl$>py7+qI;58kPV+g02VY+& zKWw}4z_&rR_;BsqXrZe*KRJK26|A$M+4{xSlZd@BG`5xV=)oQ5Nxn3J9oCo^aYMr^ zF6zGPo#Jdo-50ETenrtyz48|g2JOx5o7D)!G|Gv!6RoRPj85TsmktrYE%MQ;%+T20 z&`JP6#1W4XdRW8us$M-}2l3`^`|FPr$JJO?M;4V{$jWL)bC>z6Bk7e=Ehm~hX2+0xYQt)A@er`04}{3^N( zjL_aeIZuh0jVQ-M%Z>(j_y#}G#58~P!b+L)Y;7G1N`~}~1f#^DOd@NeI+#}Tx7|cA z;al5|@pHTR`4{&NIG;yx-rd79IbE-c@P*u;WeYX^Rf<2n7SIt6RaMp0g;O_i zLVG%g)pyKx$d7B>0rWd(9C_Ql&<-Y0tDHv!MI%sJkzU5V6(iKoCGdA4DW50;!}+@W z!81FFUts_K)DDw@`G@WQt(Z_Wa(1;cv2rxBcm01TCOj7YQA{|4QN&B;2+hyKr(;@P zfCQ(>eBU)5nA!o63+_l+PQ8pP{ytGlCMv?1vmnqmbLsL1Dy)5^;7KU`Fml z$H68=#HJnR;m?|to16Oed7RJ(fMq0~R1=y9(24rV_>eiU9do8paxKvy`SkNmBR<`&EZr5KlI^qazE80){ zTH?mv#OWljDd7MnX>3lkD$9!$%^cnJ+w;_1;Badr%34oVW2v`Wa>gyKNgEy{yl`95 z>1J#qSqP%b2E9yrt?|r&m}^)k)0ON2LsyZqiFJ=4=+9LT}$^AN@w&cZ6ADpfNj%8oymN9p;bMYlg=?anE_gy(YiF)ZU1i znN^SL(Kbb(lNrR?e3{iq6MxF7E8n+GXl42tPO`0i(=IkJrJ6xZLD7*@2T)@yPrpC~>&NPA$K@YcLKk%E)eu@6z z)v$1x!Sr2m?anOV;ih9x*4qPb>fWH^z#vx{Y$$Xa5*3V!xhGdjzrGw+GG+-^rr+EX zcZ%l?T%b`Z9oKtKGC7C&7wNj}D6^MDZWHiD>*6IjQo3(Y^_nnuj=+}dEyxu!N_hPGC1{_))FhG|Z3+`cy~x9+gHaHTY{no{Z2^ zFDDaG{u?6nvSZ||Yhc;_zerax29Lv|{~}%K{~=xP?$$nHN;lWteblX=`wi13>>U0f zT?xxslPVAYAzj&<0ea+BGEm|4c1nYZE$+iicK?vB^~Y*c(XD?hc{i6;vly9ri+)RHGJ3x)J(3H?)J}ZtY+s>!fV| z^uu>4SGn3-@N#v8AX{K8DE$(%!YHTTHJq#(Q5!ZEt(zv*Xn8$mM0frDOuN22xwa-+<5@wA@MgHOQWtyri> z{C|P2gIojufUa>X1UEd|o-1QO&^6*#z>0{DzIze>@I(`3;r7>p@qvbjg4F3L_rvaX zLu!CE$?dTge4EZYdHOrKl+8$hNM=GF%#7(f0Ps#JvK7ff)RXADdZ32UohT_FzJAAC zG%lf>njkSb+D$@4YV=O&B%PSh3v+EV@)bw&nT(C-X~I>r^C;oD4Q%uTbB42mUROK! zv;sW0lb=F@Jve-~UB*B9_y^t+yYtbzU3NcbAnShRg*+d8&ex#?x zui%M{dbugP5N8rU7{joGQ5tUh#IK|&WrLe}z?-Y^hO=h3xWJ}|yrl;GZ^|E`tYNHA zpe0Hecu(g1&r_8Dd_Vpl^LKkRVU+6Ikf}jKlROHcUtAQ-3FIh4#8`OnWTcb@L*ro! zo}2O`)fO7OO_(pVwT?UB&s1V{%-x+4c=722$;ynjl(9iErZfVevbVBn|{Y&a>p z)DzdUeTVxa>`8Ou>hcrlAH)8wnGYcvtxKNj6tx13#2r>DTKfo5AT$^9vxpNe)c#r* zuCBpVkx)oc^2^uOhE~@eW(qpvn_i{a_LEm<0hshY9xGkp&-*05Sn81;+xPEvy$ z-~5&QnHUSK9Yk~c6t%}uemz(%z4(2A)!;)p9RS5+V>L6!K3qv?)QXvFZVy;Z8?{)m zn}uv?MNcN%1X95;=%WumUSH`2RmA-@LBvjXB9s+vcntJ|UZF4CLtsf$HG@tCav zl-3Qz3v$v_)R59N{&Z41M2fbqj`ptLC7a_?9P7JXUjXu**@))I3=)Q?c*d6r8;} zU|?IUv{(6n9X_5MVgNiFqA?if{`ZR}JB-&AU_6kgJ#!8BG`1Uj?@@-V;0P<%2_GkcefQ;_IB}LnJ!W3uH4nQUqBPPasjj1%o z6QBJS7cmgHv2KCJarf#8Aryoof_;uO~x;ad%5uyrEjtbF*2J&Lhg3 zb14RcpEX(uUJpIN?GLl38iV^dYVXptJ`lu{9n|-7D(42ttyrUy88*#(dHHHCb zF6rne=eXYyPhZ_DLj;;j+!qjyf66@E7~=hRb4gS-LFvEDCHnU6PWP?KbCjzS@- zfGv?os+Ub%VP zd!nfUb$FCGD6O~vs)jrvWyhBWL$u;S6e~xiBrJ2EE1eImqsLWZ-@N~@`i(Z+i4r%y zK|!kGyNMQq0E$+Hn^{MyBBD`LDC!*NiH52gp^B`KI~pNxkquXMl@#RBvgtaq0n~-W zGH}EQ{^7@qI*uJrEa7tyiHi6ecE{zOkb0y@wS4|Z>PZx!c-EQ@H1aeob1@oUdhN!P zu0(8J;-_ivzgTH(d8&R#K7=sWgj4t3Ddw^2(N6AC4O#=>+w+t1fE(*!I*f#l#%l^x z)AXB?RLf;@Zl}%sXq5vT{&uX_89YoNV<$J$RO`+MNM@SR8KqlFN@e__MMy)Cwpy;R z#@_l~6@OMG!3m>{cz=*Y>mmin9+<0d#ks0%u*&~#fqWH0WXM&8Kt?H%GE{)zF@Q+` zKc48vVjI30BkXFuZ^XCq_|jajGZGSxdsxvbp|tNP7Hw7BlkD_YJIo|{c|v@7)zobu zOxsq$5?w+{4rmfLrZ;~8Mi2xByS$WRSU>*!!%X*N0Sz!Ez)?I@7pt$DTAJ&i4mU*# zwPgfg2LxxKhYr1LNT0K?;&)^j%@3{g8r=&An)bQym60drvBp3sinU4p9Yca zra!j%CMnVKmx^rFpfM0=gt;uHLi9gzUC22PlYUv87-`CrnNrnofID#}Ps+GvPf#3s zw))7yTQ}}+m<9-R7c7z+&xDV3oP^~|IPp!fd0;>))6fv6FFEZ8?Y2Db-BP!aAHp=N z*_79WctSAS<=BmV!{bKy`~hU9jh!xfF*SIvC4l3S12V87NfvzAhJ(0!tfr-%ux7aK zuuog)T5akUj&U=!Gz-WGBR-hFkmj?u?9-HvQ_Sb>)M*(hYszp-PTcX718_)=Sn8|r zl=&x7V^!k`oh|~X8fU-=0u@txxrZV2B3}B-xYO&f%Ja(l_BJjYI|9eWMu(HLb8%HG zBv{b?Xu|&F*<_{;?y;}wCi)aM^3>AVCp9Nxh1%t*THM-n+VIk92?5Hbl%BM??o$QF z5RFZqA=n=iG>LONK4*DNiwF0~r8spghn*S3RiXNUPA&`5P0S$HA2=!#YVd`0tQ4a5 zYDd47gEXf~)wEG@>($S)%>eIz-|M&yNR%p%Bu_1K@=*l50Pf-@TUu>$N#|1u%wbUu zlksj|`i87R*0!i_1(i@){W&G2+AYQ2&ZxxqMc#DO?x|TaKbsG34DF@^_H;+J^8;=t zi`FCgxKWcd%)`f134)i|wUr|`EDebgh9ExU558?oOVlHldxa8+W*`nFMHM)!WHcpU zG#xwG(S~W}=nT8RfQe@_dUUh~tTrob32@L}n$FXIjH>Xv!(58YLk4M3S0i zGaeyJBAUTTK4lYq&tb2!=egKe7McZtL8WB-9jRl8m&_@qU9Bj@RdTi|WGuEvIG$49 zRm3Wd<63{rc<2v0dj`ztix}q6788vxU}XKh{4RckBym+~DLdBZ@WQQ)979cpKDn5f zQKuClQ=beiQwA@Po~LS*DNxA3042Sggqiyw%*4<+TWrv`$Qn;H=>(e~>BN2DHiGdnN$QSPX*PoA)yDr1&4bNzzF!nT*J{Vg_c4D5q~DZ#iq$lq?i{FjDxKXi1SU z2Mnppt|F*Puiaw#qNK(o40EF=#HKv^Sx~)yAVk7bPVd(U^CWX6u$n$J73Z!&2iky1 zn6GZsYmJ?F5o0!tBO1dPqe*`Bsd?{^Q<8-=p*;8hhqQAKsvG`S4b4vQn#^$~=aU3Ryt;Od2f<}Yu zo+tE-T0Phj3*FC{@~UomNsaDuP1O$-;rfMP@1BaFtg(zYzk(hybuKKN)52G5?Oqs) zs~B(P4H@>niL(T~CWu-hBQlCT;Wm_PS=ZLjH;TH1xJ1F`7NChYqcVf<8UsBnalh#F z_AGH6@%$NbVF4j`FcndI6B1>|G_$y9$x_8@CpeGC&RvOR#o+n;kKaV4{U5&x$E2Lh zYoY*!<;B-7SwwaO4OpyNo4h4*Gy$dCgSmv2U!BPpt&jZ5XYWh5fCSaYzO zUv+lhD%;TNj+O*bDxDw~2yx^ozc0wk@FSove>@-+?y3yV$(d3Hg;eaOBZo!|?$rSH zd-HiCDnR@&Y>*87VFHH9s-%dxI}|~Vd1o05bX;KphMj8pczI?>urA*ALo5*psD5Q? zyS(6F^%Bf+&GYU&)$>NO3MDhjF^t7ITBu4^EXOcoruQsEYMt~E#LN>&Rnpo>;HXF4 z2okD1S%Z#rqAn^N68yFEUG*1#Y3Y30q&*CA=I^@yY*)^|Ik)!Nq=9eM%tzrvmAtiT z+<~TE6{=|lo?#3j8}%1Yj)gD!#)~!G!_r_o>x%6=dFC8fFgfN$PXm8&i?~dRN?u4h zBBqs2wUW#`&8ChkH&mGAQY|XanDq#r3iSZwmS(os#gL04GGhvt&wWVh+lbA6_1)n0 zt#1v|EJAbO4@Mv8M1|1AVW4l!gDzoH21Qk@R=WTmC%TZ1SyOZNl;9+P@n9RV`{$qi z(=jnY?Os`Y>Op8YU@>w*!7;+g0Z>zHYC4(w9F`FLvP5m%(G%mR=gSqgkv?pLouxDM8@Q!UO4{r>!EtBnVxnaQ! z3q?U`%jQ72Nf0Jh0Z?udl*VKERk`i=^Dx%_AGry1hv7Rp)0fSO{N2EOw&9;#SI)+CW!!fQ5qK`A$^O8>~w zV@tp`_4-^uw0HfuUlL}f`}TF0?A1})=qXPyu6j#Ozw4lsc9Qz0nf1=;KZOH082D+! zZEJ1u$L4v}MR6JlAI6HOpidC#$XdhvOy6vMGh`RDLHz_68H;j+Ge4+`NjCT+~w~3x~~)LK-p{WyYlP0hTqQ1tei!Et?+^O z3(Rkh?I*bA6m-|(#Ty?6kHXu{0DgI4l^auqTmC+1tcjnXs9pcISLY`g|4<~e^^yc7 zHkY{F>j~BV=vZCCfGOUS5PxPhk09uDo5q#w}X?2Bnc&x?yE{VmV(J z@O&KR!9e)OJB`SU!QcyDoW>IZ`U88lFN@#Pgdws)@IP*o(wZIA+@;GU&)KkAkyAwf z8Q_8Hel`?@=!776b~4c|U>(F~WA)g?PhMrkfB9hHVAtuK&8}0HQ+C0;tTpo=x5+J^ zU1phm*{fWYAYgNRu@Qcew4!g=HE;`a0dyHz$UUr%y~PtxcRR)xx-8}I;*fh9$&}K# zTv-Z`6OJx*)h!_KUCpI4ZIROT3-ZovkBQ1X^3SFI=J01`kbPmXjmUgCfWprw!bw8R zK!WehM{N1n$>vK4@c|6~nDYecmBOyrVb{$MdiT65Qe-=%%m17GEA)*tBj_D+!)}7( z%2zP2Z{}yXKNzgw@C7aQ2i`}Le(B2TaZ#u~8sRHoopN~BWSW%LDUf#w=r-AKYN!Z= z&YVlt1U9S-Kj;uM^1e~d z_C{G)!mQ=OR@hSH;09)aRB$5~IFJ(J1Z}N`QLCKbbooN}T(4USnj%`YQE9YxVFlUDfS#4%UibZ7G!+o;Gr43QfpOLf zl$%K0v~mFDCe}?@J06FiUnb8^;SVf)tu6n^O+@hPYxMl3zpSB6TQDR+grM$0yZ~R^ z!rwLdEWt|_;0^-mf8?+I(J03lqX%)eA^cvK_4m;w+Zrio0nT&?bFA;rXbijhcM}kn zJRtmw&q&kt4L2YW$oE236We()pya*?Zqc}aezjY$AQNl4kZT@_o;6o@uNesRsdj*ZSoAGm!{%E}(cu@!zn}V0Ux+@I0Y^fbz>yHpXZxQP>Xm`9OC#I=L-#UQ z6V?-dDY155(%P!bO;e!0q2a*C)yozZ&jine)OSpBz|;4;D2aA)8rxrdWABmG|FwHL zLgvK55oH#ePG`$unmk1zlV-b0ZUn3KcdSvyJ^eyB@WNoTCNnRO&?~f8p>F%9hT3(& zW7XMGHc{{Wfk)(t@n?Acdb_ewY4p#k_H_lbizk=BC2Q>|b~RnvZJx06kAq`r9NFYD z;jB~tynfc7o@u^}DC4%LQ@6z)g$Tz`RAcvGzTajv+UFFSI#`58+oiG~;TTg+0Rugn z-ZBv5^ECQLQwVwVEeK| zHVbiH_v$#VTPnq%&@mmMR@Jj9f7!7#fB6ZzsfHX3_T2(zCGNusRHsU?dMQAH7&BLjZE>yY8epNWp~76qvoh*Z5r(Ho zblDYxYo3}uOA@cJ*R+!nEhgx;$ak^}DdBLxHqt7@JGOFb-IjtNF3h_~9Jgp`_e4@e z#zGmSV~f3~*Fq8*vAkKzq)EOoite zqki;JEbF(RxfWd&xNR;tppFA-MC}C~&soe&R$%!rC{csJQ^2r$QN)qi1RbwlBGTRIBwoVTx>tCErj@Z^7F%NKzmQeKgNsy=RrIHuWlwam2G%Ymc^B@Qb%1;JLV|Secfa%) zyc=jEWZSA!kS@zEqOR=BDi!F09I;EN2}TyHGoD@1#}1~(JrERY{H0sjKpGww3vHq z*$|PCW)fl$-H}5-iTX>fjCLlOX!bx#=x9H$#baA&tBR1AXzNN-i10I8O%G4;XGD!m z>Y~wnJHbc`+^{uAIno2E>95p{BTvK8Nvdq8fU05=Z($xnm{$R4b~;YjZ_HNKW|(86 zzUHdTwXZIM_N4CW@piqn4&a((J%ilccMm?>TwmG|;cB-@Z#wOfVKj^>%0a$9#Kv4U1{ujJCKu~%FSSB1q?V@Q=IXAO|6jFB zrEU=h_L+&kNS4rOLEDkRw2DRPCjPVOLy~C8jPWK`joaTw0J-(6p&+rlve1Oc_5;q| zMJGS)jY%HmsIHqzS3y6R%JRim^xp&Xvngu*s%4Y~5l$Y6KluiVX?oIk#QwO)oykE` zZlfojS5ucJ6lM%yyUC-S#gv(i?Pw>L`f4p`vS7yHw|b`kME~Zuz&{Nr)v**R{ZrDB z422$F)ol>p&TzO+HOf^xZ@*qVFN)BOJU2*#m{{waG>YDXJLoG`sxUiP;U>OdHMTj~_PsI3BR=DFIVv~9nu93%#6%ro z*&tE2>`)`QL7$&L3!mkT=Ed*v{gWrmLFL80SM(71Cf6~7Z?x{X>z892ldmH7Rgi}L zq^R}FPw5Ei%6VZcRAAt;3_Fnm5H1Z2Tn;={`4Vl{-9u7K)-?sR?Nq|7Yq<-oYH+Lm!`iEhY_Xq)6(>e=PN!Z z%fe6lzzYQmmd-moQ!NbK9~&EeHA~msTVx!`C*g%%e@orknW=i`_Et9Jue>|9!mmr7 zuMYdud-G9xgZ1ONTo)(EoB4qsquZwufw%{iXqBHLMm#`HzuL1mnuZtgk3a{c~bq;jN(@Px3 z9sAvj&xq86aXoVn73aB}Mg@L=6QB2Yz^8KJ=q}+)>FdNE*04W{)ASul-i1gL7eoy-OhMm58z#G7!DC}s65zJzFgSpqk zn2tiGd=M1tZe8rD=qs(*eUO;-qLxa#^8`xiYBUlYG!R9Rqeb@nsd(PVz1ZnlLDDZA zUQw>4sL|8x{vK*BA1Yr;Nq69zp^dV#fMu+tX4J{fw<0ITQcHRo9Iq|1#J5!$ZE{UI zy!UTd)ARL%-tOXx*aNxP4t|ZUwPlg@`we$LN={~B2V-}Req|h%Q29<(Z0{ShrG6T4 z`t<|Pc@A3+UzTuIl>g`rqO^b`_LV!6C)S1~50CoY4MJIg#p^rk$OEZG;2*eZ8fQHF zkqmpfRxbXlSL0yizkOX&Hot$HWy4z}DjFMY6r7_AfF>|l3CiA69v4G4XRX8JDYU3E zJPM}H>GKsHB`4HE_G}mUx#qgo7PCAc-smR0Jni&Fd-7x!JA2Iw-9Z5*kF4yfB9^l` zF|sTXl;Xg8Cws&ho?oHdASbl(VgXf|Qj(ASC_=Oeu3Go;LxwP?F|JzAvFmyPLK!k< zV(;$)(Tow3evU<1-(d*uG*YNdUBi4Ge2(@d(d#z^Z^2c2m-@}Ce)kArB9K41NUdFb zt5+N+A%(l zVQ!_vj0$^hiO)|~bS^pTTAk$cNRCl z9lgzQQFQe#q`1IuT}7%zN6J^3Jikx^<-O=YZ8B~D4>UPzp8|%jGLMB7=6OSWQ=1ntB_du?&T^oSPqU zvOH2STjNz!27`g2%@p(8V!#KBfM|?Q{KE3q{vss9mr|C^o4>>P!hwU84{CN8PFhRu zkn0VVUJCo>0(#1+d9a+$I1B=1!Co^fWlG& zN)zCrf+gb5D$*Nq1>opIR&o~Pco$ms{8^FLD{e3<@1X>q%Y+!lzh<7IFX#tOtAIVT z_f<^y6?$7$>8n^s-uohF_(_r5OmM~UM{+8yZn1S0k57(O2TxnhBsD*&W0_+DgAD$8 za#IiHCz`RAFdOH0xMPy|BjWGDH|E0Irqt6Uc4aD#U>rWdYXfTXW{68`tiBt3gAQ|T zmyz8V_axtHp9BGR9|GU4ke|}`oA@>ug;C+2H1?WEHtKeoST+p86eQaF^eb|o;X0rU z3Rm`Y;_9LwJZ@n%T>1ARYaKay74{$KJ=VeO*JdlzmL)2S-ojk41A0C}hfW|6~;R7H!#c3z!+`x#6Q;#H@T@4qF;u+F=3>U59 z)yLzRs+c8WObxJ?vWhbsd1F;wN>Yf*owuVdqU=VwzjYUbeU|*<1mgv8Vcje z6<-a&sNRNrGYF|cHoSM@2~qS1+w#J+cq1byc5%!bZKB+OLFNKCiPFd$&wHenjMUIJ z(kWD}aF`Q?tJ$JDy`)>r;GoxmVK$|?;*zl0aZCRjd7D=nuKp6guwHJp$yKvWE8bi= zL0ORw%WkS7infR-qGzO{5xa1xFgxP3PPUlJ;dTh|2v_LNC$kW`?v&Pv{ijDcGMA&_ zp3V+)50iK~r?f`4$IgA)*0#yzs8!&5tsP+G0(0w9GL7+fCc04{&BPYzMGr@pc4!ad zgnvu@HH!Q5A9l*7h%AxWZ~P(fmkO2fy)mdaK9WK+^TQ9v&GG($qF?a%M+rX|0%GSc zD94s&h^zgh+3xw`uPny;11uvG+^@p@cL1dQR#-95fq-xGI$)y{M;no+>JJeRadPZ&uJS*7iv4at z=1;-rzyHh@{oQHFM~D?ZT47u50NPe)zV$Aj_rd%TS!EoGq99v6TlcLBB5xw%+Z$Z9 zR0UXr8q)aXKRo2ro;4mqRW8C?DyU~Y{!iE23RYVRE#J{3F>?^dHHV z<-lZ1wBWbgypL3Nl_fYAh&)|ourr(=3Ol`1tluBq9y1jr-`)JlV;1DxGHhsqETO+< zI?(76b6czdzA6lN>lw@IArPX{UQct*<3+$^OIdom-EbQ@W-CuNFS`uYx(DCb9E6EZ zQM-)^=Z#>`yrv)LlLRI9eIRb!<5vQ%{=Z+n^&6gJ+5|XlAtmaQ$IbFWFP+wa$(AY` zAK>$!&yoq=SU$1D4;RCn-dR1vv2(b#4RX+Zm-~mf{XGv{uZCCee*b#svv4}u`2yT5 zLjZ1;(ft3u^Z6g^WQ?f1(}AbJG|O?~B(uyA7F5{M4qIfRxB#K}^?w8qMlEnnt`f(R z|M43_NB90l5ZsCa0wOXbVrcMpyJFwzD$8mrwI-Jjbamj03VQXS_Ann{kZC{}$8?qc z5|zKTjs=zwTP5*Wh9y0btN@kT2w&t%bdjS$Neumr#Rji@sY3GCvmBWf+C<%IKS)`O z95(Y9sYovPz0Em*!gXfl9P6_3W~(NmCGl;Yh^-1Q za$$LM96=&-a}z(MFp>$Y;6XEx`nW{Av>&c(!4xOnXjqAJQu?5Pf2O9`2N7o8K?|{D z!^DU7>EDSWn=etKzFxSWHFd8qox;bTcSd&$0L%#~dED}gj5dKpYj(cMDT=uyFg8X#J&ES#)x7-Dk3N zwpDfxUTznbm}W9rcEUn}Ur$kJ2{ z8b|}6{9iGgY8J*OcEBxhI~PYIlYg;MS7~^80l`JXPbnV)@+J9d1sxnsK=PXxbl$g_y+<8SJ~5X1_gZR#L6 z0Z@=wG|B_CN8PB?ZsU|2%Fk6J^YMx-Mh{F%!92y+XVJ&spanc$8a=Y}w`@ zYgSMz;2TJB3RNqswt|CP-jz3<8sTIx3{rLXibQ5cVhfVN&(Rj-u-ac|7Tk2^8l$Hp zqJ!bb2-C>S$xX0{;O4$XAD8tq%JN((zszZxHgxKA57$F44?Wd5(0FOsI+j(Y550JQ1l2Vva*+4Jz{&8Sumg5}FPEuKLk@M8pm3<)~uOTncI0oY&fNRa7RWFlJSi zZfd2d$Xjg9Hd2A3&&G`&K`@EgO2C^66PZPyG{^h};aehg#J0U{>{%igGhd0PE>WER z9+VFUnd=oxS*=d*OfX#u))*Gma;>Xynxti22q!+_ZtlnjugXJ^JZ#{FVa5t05c(rW zlaMZlzLnb4!Dx_tijjsn%H2vzD+E>Eg2#97N8X5R=eO$LC@7RZ3KM2otb%Y>DARF# zGUtXFL=uwof`6*iL}3=8HiF*S@CJS19T?*3Y25MrtE3Vr_CI8v4vCXwwzbhy z>nBsz-_Sg^WJ1VYhHnW?K68hfG?_BcN+y!*{&MRaQaUO&+)Bs$%sK!+M6g+D(;M%# zip5*ts7*+=P>Y)E+n+q$C>}(Sv8H5>2dGY)pehbD8XQ#Wzj66Wp(9#=%B{B_oscYX zwd86WYKJh_C54!Q*|OHTRJ5nAd9MHQE|;D-A+a{6x?gU3!l$&n8sc4`;n z8E}-`81WZ;RJDp7;FGB<&gPrw~(cGX4 zEV!asOIRd5)uDYyJhs*LLb^Nimvy)3v&2y$$LQN1o$glxX9*ZTX;ok*f8k{DX)&>rt)@w0>ui06QPYv2z$Pv22+POCjM@{@J;uizx zoI3sdJodR2gg?2U`h*N}Gjc|2owpI`g*p?@<5-r4IG3fG+}(@il`X>mzN7_>F`mTc zqT6EEt06iEgW>v6JmEq5x`#v6J|7YJhS$olC!y+VF;p!E^~b))nZOGMLuOO|L~)y; zE(@Ppe3rniCet0EayJu6%<``NU<;PfsO&;bYkd(frr5c6j;e`P?qM!f&4=8eYv}Om z#|za|clm&tkF!~?;W~W+B=wdGKEb>w0?WLQKk5>m#S(W>>6Hc_nZ6k5sm5mJ-=bF( zt_71U)H3BULY<)Mc@iN3P%aoVGQOh7+|vDDS4dR57l`EMn?t`2vt4p(omV~j+lR^w8NI2D6HGm(Z<(4LD%~0%0fO!S!7k-ML3ZMu1)s6Yx3-y!NzHG)XI%-in z$B%e~KjwXf>D?=i{`T7Jm7VgP^?<+k=34#b+2|eQtBGm8{q8-J^AqcUfAZ!!^fBQJ z+#QCQKpVAF@t`Ld(DQwp>Xy#v*sUVKjdiapv#(<^a|cJWQxbX`0@02LmR|u;{M8Auq^^qwZr)I;x)7EU58c>xw;j@^BuZ-H+5%;wP`!&JK17~LJ z(OCw)EjY+MxN8xY&+m6V;}@LP`S6~xJHM*_FUULo@f&uu zPt)H$W4HXl`(Ma+e(3-^&v)9CuQI_;_t1&~Y8{Wyl>VK7%9$@{EDK4G&l1LWQEHZO zNm##$4L$3WuT$tR<+!ghSU+umo$&PW^{B+_-Fu0{yBX0JTSyNaal#kHgkO9Am(Bry zD1g8GhG3|npx3V=>Y@4WeP;KCUf>f3>$Bj-b?i&Z9ikz(ORi5l=3SlPy(Y*_s?(+t z-;B!hfk>Aiutj#x2uJ7e{FV;u^z6=aa`$01%##F@w;3=d^Q^2M5i zzfymEEAwMUu#+dyt*S-0x#6vu^37JbvjVg8GNO9}z}ww@ogBZduDjzZw{xzrgBJz- zk!#EkqBD1{K)7!s@co(T-A(^V$emlWQxkfdHq4DWuw`LjW9ZDQ`Oh_8m>cl9O3%5e z>+`Pa+lYeKZ_llRgnuPLZ1xGpthKsjle&4 z{44h3-w;1P%cWcCfFq3@Fe97yKaVxScDBxr21d@xF821;CN@A}`=5~be`!#QQnlSs zR8dF1648Yg@?~ox#$^RFQ85+hhS1F^S|p&Qq+81z60(Xx6D70K1tKw)=goYU%|5>E zA>Y)e5cDD(-uaO&uua)VkRq%x1LHY%w#{;Vp3>j%uXFuCIU>ZNE(F+O5Rk(NHv-ii zI0gGUFddcHsfW-gtN-C$B#?Bd!f;AUbz3PKm39!0$W+=)*lR|0Lw>=`mAEbYM@k3H zf2DL}7iH{sn2zOIEuJ@*LXYI4<(R37G;6i6;?5)o*X?G&nbkG_)EQu+ok4t`vSzVW z8d#>BsC!?)ZXX7wbZjv<)z8l6HsCrGDbhl_ZPTnT!X8*?ed*rct#iuzoA<(ZLv=TH6#=SFx0dVm4aur_8h! zHz@pU!GIeGR+@4Nwt170lrSt@dn$CT7nlNTWpNQ1t7-djx{O9zVLR8Y>ci>0aYMT? zs+qGBzi-SfGjQ@oWNg?kw7Fo)-Q@dO!r5##UCvRwV{hSG!aE!0L+)=!@2*1tFs1UI zXe3#wZ~AjqB3TB|`n>bL%!wPajxOWL(RQ6IR|TeYj2RAK^o7NM@HBF&J9bTl^o;I# zVWO@}O|~R4y5a9vN6&3m{UE}%BP^Z#jb5nrBzXrMqEh}CTXJwUzuKRWIrx^5oH;J* z;*u5{(k#uBRij_mzgRRQs+M;KJ7ukVv(!hSI5mZ6BV*8LKK<288Wm=u!sD)sgmOcuQ4)6I^U?jo2 z4i;%d7*B6k8cz>kg@={3WWPrW6HE;Z1y7kC4qsu&XMkM){>$^hqAmnzU9784`Nm#N z{=SxCBr!00FGq-P$lEvxE*=^Jn#sY*;T$AvM&y%C3N%z(4a@FZ#oJaIP^?D*t&860 zeaZEe=0;=#`N{2`4{bt8!j1Sw1ERG z%Y~0(=H4n{Ot#%Q`4rS-r4_}d6Aqeu&av_>*EP$7;~&kGgwv!}XG)i6O@0Mm$4z#d zNL3=#vS}mw9(Xr_0ju|+1NcLTvv1dJGn8)kr!?*E5F`sTKLJ9Xc+Aia~$FDZO@Kz(~5Se^D`jDG=Ubl}i_CRBU^GdlM9^C!jig3Opc{}6qm z5ZVEajGq#G)_3L5+ae?cC^oNY3WvmH<0GI)Mw0Qd;S8UN?t1aD{*c!=!(UZE>tY(L zyD~@7E}WSAPW*FM0DJJl_v7$aW>R zDRmFWAHQPakNTs=rMVv%YkHcUJ3kS*2K3ljj}x$jW-oTdeldNOR znhgaq5=^l-&XJ@8DpG3^St=OSz$H9QfMr#Uyd%+g^lGx6IdTx+63t7)OYfmO3BOT@ zTCh;-!o=#*XZ3IMv(L`u^R^6IN2aXC-~E=3>#ncIuCLdA;;*(Z!Ef@zJ^d$xf@m$F zHvx#cm|}RksKb#C;>2{l(n77u@7*j>senVmPNDa1rL0b=N?p98GgiaDz^<_WJmba_ zSLrr{0ew#&rFsNsXeCpnQ!{RdsC9=*%?^ca2fv@YUn9z`++7`W!XY)HLlqa^=m)$Q zrbm*Ce=#PEPwx_W?+yd6mp3l<*@_*WwF)XWQz8^)Eaii+e*5QRIho(P` zQtsK`w_CKybk2N9@jGh0mn#5Db+%&R+dY!CYAiW87zcETjVJ%cX3I!9wf*zV%>{u5 z=5^Uf_>h@0r#=dg^ZJXctLSG!qq3)|!c3;&H39e*W1_j*^ig18Iup%^gs~G5$YViB zrhbq%7Z){3`F7La1!Cyw45DNZbwo!SyD8FSk}e6+;xuWaCW~PJN|ZtxxMxY$_T}WK_!9RT3d|DPREnP;~u`Pax=#+ zo{ox8dsBO(vDT|*6K8FXZWw4TYKfZ~AHK43jP;Fe#Rcd+VIWY45kgHJ=KABJ`HWjw z_fk=hDf+Kif1y1?wzX8Iw7o`65w44L))^eTTw~NWu;$w)WE-Oa0qK&Fo7zs`N3{6F zCg>&)7xr3un%G0h$JDK*1w@BtAXQgXCRw>34HUX1Ki3sm=Nl-vdtotaVP2CQ(kY<} ziVE8*(TFCW$mexRCbW`bC}&)?_fvcV(RSZeEOU(iX#9PswD(F(cV&BNWzh+-soc@Z zL>C0MypsTIxL$(+M0nZmb7%%U8yaJp=pkoEM2R#Z5G> z5Jqo=`)-K)Zj6c-icDU*L>Bb>)NWfta(7Fpe2W3cFTo*td+N|?ODba6zHsHQa8Gp# zGd!qhG`X4K(YB_4`Hed6#v{Y+`Rup*84D zy76^6o04VTUig&j#GxOR+=X0Wf3znzH+0-%%^j9+mfS7b7egs$4Zz{fff`Px%-G>S zCLKl`K&6ZFx1`jiM%gC&bk1<>j|XlJ|T zQqR2780t)OoXydkh8TFE^<|%k!$S4bc-K7;l~TzKu;Q}xm{GmlD!Rg^)Sz4=@p|p1 zGd_$`6u)>^;}w0k!E>sQ*!-oOSwZs2PqF$aVi2jRhIWJA0OshWnny|lau#5r0X4xY zHguqRn9Hgj6UHKy1ipBDLU~>@WTvdZgA^pwv?t?^pw9`hN<1+yiyzU_nDsHq&YP`v zD-P$^zr(_VGc#uo$|1T@A}V9m!)rU3=*ZVla7OGI0X3~IrtIojvPUo(Q)m;5@p3nh zoMLiN#CS{O+sGo(%~!*2XU1vQ($W+FgLJ?@t+fas>ZVB{{vGMz8y;AqAE==d$yJSa_Yl_|pNp6j z9M_SD`H|#}z2efUVriZ?Ybv?H1QEU8DY2m?mMKm}K)z{ne3e%_yt91*MKQMvT8aam zwWm&k2$drM1RtAoz&9l!QW3Igdig}#blvYP8IqGVs8>}1VuJkO_0}XiijEgG-a_`3DY4f$x_E2`Qn9HMnSzi5hvkBt*~&CAIb^VAJbJ+~K0o@CK|H zRnT%q4tl^y6$ZE5!8Mn(!GuH(^4}E*U(a>G4UZMDj3x7fXH{5lesxfe%%=_aa5=-) zI11#}kkt^9bqW(Y5o-C6pSl}9d{+%vW4x^?tsAxIGjUk&GC3`GD+Rn25*y}st#Qds*GP}o{WG{6M+}9$3JB>#exQoG*5`=1VUcc$j4$Sa)xV!jXC{ygmwJ> z_X(kOHX{ZP306adSzgf8Cz(%pbqg9um+b`dAX~Nd=M-ZV@CKT5E+utRaq5A5zmy@E zaErErIS?*`E;%hhVGui)h5GqpB{m#*A5T%S#n|Jk$(MC8J9i$>a`V~79uf^)xg9wUFcw%>G?&LXZkopYJZ2S*xjJKshU3}$lvKCJd$2&{? z`u5GkMY#$a0!F}Smk)~qO5cBXZgD0;1W8OJjsUL{V2a5#BmN`-M4iC|x8VR3qDJkNzr=W@#YUFSl-m`EX zpyy~~M{h;H?Z#um5Zea@Z%+Y5Cu!>d@d&s3_H|jB!pyezVA|tXnI7%*jF#K&7HRFO zobJ7klz9juPVY?+&B&g2eWx*EqyDJ%reksv{sf`7Bn#_xR<7YX8hLhFKL_9w`NEcrQtL zXj!S-Ea`c$waT$o4Y-x`K5_-%bEBR3CFZePt-9flHdB37dJ+?sqq&sN^kd~YzHS5| z>e4Z4+1y0ooZl!CBHdtZzDE^=xd2O|;3EBf?NS*TOf84Q=|qVMp(?7Ds8+K%8A;6p z9Hqq5$vU@R`sPM%pnK19CwMMye@WCouTg>^DND{XS##3*`goTbneDI zn!PfMJJ@`G%RDtNT%E?I_TmTMu1QC@2hILv$s6y#YKcL4A7lzZy7&z6>W2P5KhFOF zrT8b8`JYc#MNxS{0P(XL6|@mLAOcNPR0LY+n}@YA}R<4 z_yg;XZENVZ5qYGBAr?}8{qi0Jvq|e3z5W+ewh0;xotmWCfONbROd{+3fw)`%GQ%*Dus6qN^u^j&^9$=d2aIW)ipl# zrn8SEjO*c99-%-OAbag~HNrNe5}r5rPxA6!h&p?55hBHsXmQwMIYzSy*X1XLo%5|| z#SK>hPSAl!*yyhHEBIeG<_sJe+DnK;4ef!2CRi>*ePnZZ#dRA4Lv-tAL&cMuhhm7Y z$wXof(fL`N5e8IUI(+KCo{i2$+(QgLzto5O&J9ky-4Po?>B6J12>MuDt->@3GODA>eo06HdB~5%ZUnzg;oVyI_Kcdl#5-oZc zA2Fk^O0$s|%7l;PoS}cOF*0BYCB53)P;|@xuf=&qCcKmm1|+7y|F5I$|0vFXu1TnE zsNt}q@(N49h6iSYDNz*WQxcNdr0JMv4pR1En#)8BGv+@>@(|<1x4B0TSKg@m*buyZ z|0u1t9m5!A=c-H&#fmPh@@byts73M5uMG?^zd}!mT_-cr{=|(>x@#`-ptFH&FD}Em&s}g& zR-_*?AnMX~M2Ire+CYFNrHf9uO;IHhB5b$Ccnnsy%st>R39G4)~BR&9VFi6ERO*H2ADk|#^;qz(Bp5xCliO8&t* zSjyW5s*{oekHV}{XzAm8L3u;G;cC%1{*nemiqlG-iG%T3Z!F8YY+tT1EsZG#xkz6%IUtfL>H*X zwEXspUaV)?;aU_Irs<7bpGl@;2-0vPV`3G?PJpT_txi9s>*|qgs`a1}eb){oNDG*X z^mq$V_eIvEgs!|r3#46t>)h-E9{Y@X1KPJyU06Pz)}Z%6j%mhQ&^(Wb?WL+E8;E3$4ECl;E7Od`~=) zpOby?`c0s3m|@DYA8N)p4Z^T8;2z0$X4V-U?(wFC{Z8 zf6(Vmh1pjO;qCiG8pCNmI8Q{H_a=-tNUzl3!SRWopxzTR-_RhlXEj#Cwln z!UlfM#$BpeBL4Y=+a4neifAk#bww6ePTnS1I)4@lcHUW{?q2F{jjZA2F_GJudwvKk zBse8M#OYi zXh%j|c{-LwRU|PvuFgMeU6e3OMo=@to7;00R?-$JOO%a%xY8S9$dTWXat$a7dWGL=1b%Cg@M zr!-xxqyasPssU-_zchwXgo3w)UF*I*d0G*4A*2=2K&i z0CG{cc^cKB7qjs|Oy1z6i4nX{bDQl9C8GcF4AXfYOW{c4`5nL!O_`2H-)rP=W)g{! zH^fTsC+|;QRCzS?#Zm@s?`N;_l3GP`m#H7_rn;@_0soCpTP%8o+S@hQjGzKt{Z$yx zDksDRAH&`T++(of9G4xk#U2MPl6&}sr^SJ_YqjgLHVWU^r+7-KXV$jR0>EF~d zbc7UTTwoif`oFhfLM~1gw!oZwNn@aJW?^b!;`smCuwVZu3|`Pin6QY(Ff_$JC2h1} z%$$QF0TxZrC5`?_`GqZu>~`*swRD>kz570b-B_mWEd@zhlFI?_P44Hczv-_Z$H(;F zAXeRK&-0FUNeBMWn|LD+35Rrf?k}cxW`?Qw#kMm6q$mbRtM(TuXrmo`%Bs$&(s7xr zePtyfiz*<-OXYsXHpW!eQq6}L9J5s~mj5P@rVjhFnD~^@z{{c*0&4M)+Uj_Cy$5dBglj(ryqfi`KEUT)6``xThx|t%tE{jz$!@zMT)$&2;IyBaeA zJPYwl@GVj|6f@lK3ZN-YqQ|B$&0)#0}ZAhvD9>`OdOFRjYx`K zZQ0t(&r$Y;&QC5tr}Vv5ZKzxq5{4G%nik}S&EfGn$=dI6y%=zsGcI3^Wq99hG0hql z)3AGMJ-4oAvb!H>2A*hd8b(tEbu}RDFt?|*G$6bdW7n(%_uH$b1cZ&vE#)YK|#zrp-# zOZ6MQe8LJ?Xp+D*A?E*l=1AJQ+F6;%+nYEVI9u4+{!^p%r-f=invxecM3f0!7_D0w- zZaMK9DHP>KOmiz$$s1`Ieo03!$Qu4`ZpWXiW~`eKRMWqr>s~yS=q4{oGu#W>WOa(h z6es)XuV{NL^NYe_bc~)gH(&YEasPO zYz;iG+N{OyWPTi{%$;S(`Wk*YAv-BwMKHgmF(1Z|xtQ+^;b~>JZLQ3yp1aziG&c;^ z6>UIIjDQ?=v?vtVQEIv|e{6Sg7-z&su&Kyqx=6*Ku3%u+4EjgJsQzK@!MkuNSvP%) zr1QACO0g9P40Xr=dr&khD&}^oGPUg37GM+GVKF4TmZbHF10m%{#e9m+H>OuwMuu6Z z5M#d%T{|i7_)GBe*0OGOM0?F4=Df9Kpf!Ve*&OOp%suYZ5^*?f*0VWjT9^DXrPjzB z1ccY#xF`q5b{P$!F+aHIVYvLHIX& zFLre`>c#`cGe-0cAKym68AeM9W1?&EOuceK71hAvM$TjW8j^+|rK-g*l^K zLV#Y&I|NAoYi}BZKT|UrGDOnTgziz~ZC%wl%Ca9AR4K14GilRBG&iSM{LDeAX#>=e z9x-*5HHh3A<8t4}-TP)8LU>20Ox=oW!&p0YabNzecDYlXM$m!ERuoX#BKyy^E9we7 z9RG)|r4Ho|)V023T@%-=!673>RASITtzA*jtHM}OgTs(Jz%qflmgPTnEiP`Hyr0sE<3Lsx4%BTzSmR6^_kW}7pZn;+nryJUSIczb6-3+ zh`%FmYbcO8^7djNby54W6&C?Rik)EeHiN&l3f$I5mi|>Jc;Q;Y$?-I$dJk z-OW%{?!QoUW7$jHjZhhJ&HH6JDDdr+ZcPP!BX@l(G`eIly}iJ$!~XoZR=KUFNC9k7 zZ5Qn2Q0#Fa5?4P*E2Cc?J#+1L*B7RDsKJROw?sL%b$(M3qi9*1KjerW> zBt^uQyDJE0rz`oyF>s> z$Q#`(E@_;xS)}kQbFve(MPAk|C0R3x4w|e-*?%LKTF>v_^5WHJFYuZ#GG3jBY1&Y2 zE|^1Uw`jS++8m%gjL6IiLvg#TqgRDP64_iDlGt7nar>p{jP58ryPZ%}T_!J`w+2!! zF%Q(obQx&W1shL1CRrd|1%E_bdd5ecSGaknd2C_JJ-5YJz`3biTwrb*8UKPRH7Kx1 zzasPGpIff^xoL6?o4?Z-mE&kIYwvpYkP@IVJbm`+yTv%v(u3tHF7ro|GZCRV2FcSc zpWk3%-=mhL7DCOPNA*EeS-DSi%{Ds;*TGAS~TGgEAgoIJ0Y zkXz}PMEpr3XiRPr`t9*)oN;C>82N|saGTC*vHn?$a9Y-68bv0_B32jiSNq&#GF@UR zi}jFdS}b|xTPV2P!4ej4>{xSmXu9Ruz?rZScsT9ZB0z(? z%FrJ^Swj)U1mX{Z+=ZCe`E8kavXUKg%Om$MWs$%#ya`3V`TMs{A%h5xA{Q)v5FK^Jl`OnU4`b} z!vb#72{%@;ec3^JrS7gIDjW*pGRL5YKVoOkjd?P~_jYIST2$9qbc^ytKI;`(y}?4A z%c|7c#KiAnFS%VOZa@^TKD)}(Rb;YSQ&aS)Iz6+1RB~x@ywj&l4@Y4lEx-rJLJG^roI6VW#XUewlb1oe^Ru+X5Xvqy8m}p_1ykDB4netTEr0?w+dTx+b z-fpC^!jd5bB3l9{s04KiG~T$6KIF6Zu$H8RN`mGxIRY%`*y0O}=Hf+lHg|DUx#t$< zaMQ&&DTKi;C%b+0gtc%dWUBX*d)PeL*TJLu+(2E61*mJ~b`q?|3s?khUQ{AhKL-z; zX0)Qbs3Kutef`Ez)L{@0W8sYPP)5_$&1cx7RvC;QO3f8vj(JrMuBr3~ZrIFf?^B$q z0Bl%;x_&=8*qOCVHP8 znoy+8x#iAnT4U0EcopFh&uzomD5?JhC!G^kukrF-kw)!)^Wea%MQ0y{^(A}Oet9yV zfi_onycU~3B3yj2Ybu6X1i-4m2~k{Yx7N`H?!efgIPUdk<5u8_Fd}^3D9elk;%s%=v{J&jo2R(EKW1Kl5Rv|5%#Dv2OWx)YzkUXS;x1rVDBi?RwC{SLlLug9O<&yW%phI z$AxV4>PO@sp!fp;D%R1?YA>aEMeAKEh{*&ACETgH5Voe+9E~dd9`(4q|? z!Hh46&8?^qwcKpZ8=vVeh9Z<`VCE0dJh&bS{^i{x_U7QoTv@U;A?%8)*jRP6HG9L; zx7_Y?+M%>*a(@fBr!ArvA+b$5#8Nu{ff*XY`%T;D+r3zWxH#!8-BVEq zb>qx^lX&TzD3+8{cG_k4_;*hGRYm2?$wOmc(TJCDjKIzVMJ=da7!#nV)g%utQu*YB z_6KVlC~9r_#;_D6G&UIHo)=>=P!VmTVwxwUSBx$>75Ovdkdx-p6Z@j8)BpTf-g3UT z0g769|1D~9wyr!66I%MwZ;#|^Y=t81b^p7l#mLTh!iD_p+a%NfN_z(`uiBazIomm^ z+8Y}OrESm#S!qT%870_nG~#~?VG8PWSg!$_h?H%8$VwU}wkE;$Du zqUvNMeuIGkWF&r0%R+LjnYc}nrP#n~j(*1h@iIYKMXV=*Aq0+0^#%jBDM23<9cC;G z3&bQNpAT9A*oWzUYFxvJx46VJH$S=3!WbRW>i?G-s$@Ae#Z`+X5|kBb^wE3;zm zlq^y_+}mVo;+Ck3RwyVkq}tY`VOoa&6$D1Nutr?SggG83=6oXur z58bn)?+AKifq3&ebW_ug_{KdCA%$zCQ&*1L1L_iXty#}^e5YOF8Pr7{H7vxAtIuPK zDhRPmXeWflbz=MbrRiDiTAz_A*f+qkN(CNm9d01G}qdPb%V=N7dEzfJmg7D!VPQ^ef zO#TcT5LQ7Sh77&XK!Kf!}_@oEd~RS zyXaCQBTV!B91WHTIhtven$W06x(93wt;?_s8JWSoHv^PUg^Sg*sb$jFhV+BU%+Q_QngQ&7jun>Hj~;&uZP z)>W$po8OL^3<9kF)Cf9rJ1w-;(fgQ|jpA@tb8*-Zd^H#GhBT5c<+Pf&GDXE0rb-o# z6llcp8WJ!iv1+v5Yb2yz8m!1G0dIxyKumXL$l*-g360%O(tJf?&Aeo3Hd6NYI>JbH zqyA&DN?gW9eWq&i;)~4dQ8S5D8PTOM{0q6=JMly)S`!V{`dnd&V53TXAf`KVqql+v zi0SrNE;R)MkXS~g{}-k^`{p2_!by@8=dmzf!!(-`H3s5}_jQVkim2$#v`o6SaO)y#%#va=zllHZ5#nEP+1`Lh7A4^(I2vE2oULX_ClFX zlb2chC>Yu6#iX_1G|-c7*5e7^n<}lxlQ*-ZL2`>iky!E7^Bdn_2e*Xk>F2EIBTEz4 z@>yw1*P@*Y{=V+=mPrrhM9VtOMYf8a8fREsQDMuJl{yQtlF=Lolsxv|HdZ89Vy>6F zEiCJD2+}YTvZ-L=Y$1Hc70W&ilGS-(RJ-cJxAfJIK;nnY72?B(FWw)M?-@oSB%JMn zZnYuUWCNtCEyka06pd}J3itxzK-)gnV*LSyiyb;C5|xvl=07WC3?n~&H?d6}BV`#u zH?pF~mKu5t4F_>Sr4DqVBS>MWZAB^%dG(hYn43Chg$^DT!;aEo;jXW7*TSi@l|?`S z9$-ptYPx!Qcu_5JEjPP;d`RlI8Mwe9AP@-nqL^`*&1|hQ?&D<_n(u#@S8qqTwNZ+8 z0px0&B-MWor?&r|^zJolMYPI#4Tdu;nfTkWNSbk62pX^$-PSzz$}>85t}_qvQ{zY{ zt0CUPt(PH1TKQy>0P2qP=aN`VQg!hw zRD7bkiJ8z-wo3=_5Prv0PCFRh)R>uou?CmOmJUE*X%b*gO1xOawpG!S12NH4i6v&O zVRW=uirMrV^z_`JZ2fb$F!G>Zydr>v_sY42m2za3rl~@pMqEJX;&DXW)d?dnkVxz? z<`cT9C&c%=C(r}Tjj4&Hjqd=6@a_lY$CMrpTvz*bmqYs6Fbp66?Z-@sO!n9ciQ>HM zdi9CF`Nf$zA<}J@A4=S!wLo;3*i7=nI$p7m+ibSP_ECm0Q+77`K?}+2LSK_Wq!50r zhr12{Y8lNF;n&zh6D@>+{by;~AnLXNKk^ShE?pL^DPI?s6qz(*3K@io0#D59GxY*O z{!TW!O@<9dA08}rgp=6lS5-ny0j_sp{cM$zenwUjCn8G2{5$c4kg;rq$5PIC7R6B0 zRUEjU{NRyw;S8jx^Tc|=P@!^x8W%>`;OZi3wDz3pha2A?!#|2xz_ywukV2%uEhO^2 zI734Cie@Q(pyeD?@ZhSi>O++=Z&-_O|JYaK7=u9T)|#wZPZy!!5%sH{&HV=eDq2YV zM0qrw3>oUCE4OASGY9MAFeXNVhHsbY*w!)tPhQ)O8V|^`Kg~A zTBH2p*)0e_xD7Rk(Ck*9VmIuQt=}cC-!`({pP&{xAP;AGs z+KuOkpD3ogs{rH|B#qtgxgc{wH<=~}a{bAUvzNP!n&RW<)wpqggd2xjhIKGhnXjj| z^p4Z`s!yT^xfWdDZUy605lJrp5fh3A_pjsp%1oFF!z&MPfNNh5??sNhhzLCtb>xg~ z==LiCVf=NPnwN&3M8(-L4{jlx0+DVd`n7Wkq$yEan-sL6~DN|=&Ayy>y zXXIlP3XRK9cAZF^84t!O0b@j=K8Cd8eUt8aDtW_j;yBU$y6j#EX9cL3hM@=2f|^;A zcd_VOQMImnNPk2iKm+w;(E~NvtXhqg(7$$Zbg7?ve=8CmD z0{t);JpNz;XURPSZMF|?miT#TQmU&GE2?C-6lf##;gBI!;b!hN;|Ua2@)RGHz;0OgZb=OjMYM%+oyRN*ELUp4(V6BBK9f0B2;iur zUa%F6rypE@6K445Z|FOCEN9$g-EX*YTZR*1jo?I{>nC>1hq%m3yX?c?Ef^28IKkTY z<}Yjkwk4)C0+LXBx*GO94B-RG$OE*ZF^YffcP&`Dh4g13sqlCtNry)hI2F?w6_*^- zJNwB?KR76vH^Y+?R8A|>i_i8|?rY1UWAbz*P0RyUx`U?vFw^A2ofQIP!IzwXt?^+T zp)w4`7$q6{=XabUA5rp2J`02c!2J?C3fJzIV;!FBla|TP{;Oln9Dy|^DCvbM9cPa3 zhq_|oK#9;IlQ2!m5KmaUfHtB8yY|6sUR<^`isL5O|6uVJIj+bfcN9Q3k!<0E);Qe; zMd<{^$x{(KJ&%syBQiprO55iKAas_UZ^%cMrK~dT9FCRxCO5Cyy58`vEkDcY_?2{$ zIsU*wi(zhM7dLoWVr~E^JD;!I*c$tU;2)DX3+v2=^A}#+Jeaz& zIHMF@Z5x~$y^(fDQx^6?ltSM(7PHnN2UfT(rW<6#7h~$vl;@r41olPC3BK@HD3>?1 z4T_599hlYqq%;Ie`?A1~6!)v!QV*is`<(5S#+pJ~u8~g=I(lYq$wF)IUVh#~ymXg^ z7B~48QftXAvH20d2(R)EFAubN^SOmjZY#N2+#FO8mU=zfp#2ttR;uF4!7}lb?1$zJ z4FMf(u&t#fByG`FRr;tB_Qa-e4e&`fu^~BrqZ_vBT53j{I1B7z?JxfDmpu5_IHATq z#spPSXOp`jUVcxnWXOoPp9K|^S^XkK7w?1m3TF|I)o0y5Fz>Na^9A8?X!Q5G7lSve z^&Q*Bqy_6$DU%4F-hGwjGb_d<)rZ=Nre@7z0>s=Ip3SxyuD>TdtvdL0)_~Pcg!U}E zXCPhMNSR`5GWyIpNT-md(CUb&FJp6VFE00RF~Wz*;#<)kyAW#BF^cE*gt3jVaOk`9 zd>k}#bBr81f8>AJuLQ^+@7PoFp$7CX3SIW8k3juw(I7j_?uy&Ty$-N#NAG2#udMIb z{`KzkU7Np+Jvq4v%^sJ-mYh2cv{mp*4zt2lH;sKEx%JS$G|+hnfH6kfef>9rdlDY1 ziJfx@hD$ebHfh0oOZ#y)DUN$7ennR&@Bc$`a^^%8Lp?Ij$%HMSb0XoUz@R)jA>#ZA zjGs_U$wVtyJTgzq#4KPvD%F#Vp4D7BMcvpWjqkh^u0{1nSG+%~T#h|~|=^@io@!%O28=8HeBIh?e1 zTv?%14EIKsnFJ)lLV!S-pwS_5^N2ENp%zRpz zPaD_Xhn7`9r&EFV1eL|LY^_ezygSi0Tx}_JOT!Ea#Ul!n4qB7N1QlW>wJf5XF0 zK$rKFRYI>LbqP5>ZukKSG;2)^p7`w#2 zrFy^~*&o0@$)RGF+ai6(7azdr^R^w3yV9af*a_KqgIiS~u2kUBEwlwFYku2&M%e(4 zm)_B8UF1DRoS$#!Gl6^-^_2XHwAIz+4ZFLl)9;!nX_$Oq5(B!md#r%Wcnw z9(pMks`^_a3w%%6RKT?L+)bCjm4lG&z@Oqz&d~NV%bOR*AU{?uJ^RRZNLQP{mA#N2 zzzFocz!xACl>dvcPA9Wf06jP1yKY1C@kR~%4oE$afZ7q@U8hgi_nxJHmK;IVediW_ zoA8CR&;`gq^$!)imhfwZwWrcvNGF56#~ZORr+1Q;kedlmb%F#0E%y84kAt|G%rdAbDKG*?HxIUCvp5y#0ub_VkobF_i3=`j=@NidMzS`7_)n`2?>#QRf&bm4 z7e*p1f%<9E_y0_T{0~j~|6Y#zuO|JtE%Fa3N)EX=MnKp-7$7J%xp^jCh!ky;M1%me zNH9O|KT%PU;zsf!-{G`0F93W{4&)&11aVBI(EL}6x17VzT3@&K>uvz=E1H6EF*pmj zmE0@8G#&~gwYa#9R({$C?(y)3ae*bC1V+_?OgeMsrwobJi?<;o$ZB{}Mgw6yze>C1 zg(`qn;l#&EyXi9wN~>z6LW1+yB7O{>$2EnhBDaoktJTexxf_hvZSLJgyrZ>iig_1g zg8D~u)Q7pwp(()Dw^&=VDx`|8jo3IS1VnFf}`mKwsV>?}# z?U$A?BX*4}9lIjDf{UWqhZ&I1O9m^gkzGaH1ooW6LkgaK-{#};7`P`leZgfbt=Ff7 zR;Ni?>V4oNmK(uA=d#TWCrmdqHllE%6rB?(SSs0uDU2ts7vK~uu6{uc-2^prR?77D zqM}&PeFg6@4Zeg}KJJxDp$myF_i*tk4zX%2TexKn!G=Lya*}cny)F-F`O&i12H=~_~Rs{fK+`dw9p{BU6*+pMI9;eK_c5TT|-8B z`rSq8ww6dm7L*7v_DNJ(y4j0j&)GesDmnZRIKt5kVXGPRzZN)k<~R7rR@FMTQC&hg*EM)w@Spwp+t}aCH>RZGkit z@~}`;*Q@mSi9U#;&zEP{di3UUenj~d1|}`#W&MOj%t&Ep_0?j;=>+=n1Xxc|Bk9!x z@Ik`epN>1f(V|`s7KpYFF|L0qT}lTZyrnO|ks=RbY+s_W>|G ztGOKbZ{e76OOa)7AH^vtQuIA*I#o5qG z@;!jIzieH3f#=IF;%#LYfhBvOYwQv+@SsPK}IKh&A) z@LFf`A@JskfY6SRCNaj?xL7J^A2-hLi&SFtWt|`xwID73sG8Q^1Wd~BV~*T;SQC2= zq{cWi73S$Y;$#~>MmSf8;ApeOVT>Xy-LIN`{Vtt-HOKG4|zUp1KOnf3|B}OYpE79^TR zmIouK6Rm|hgq?t4@L!LTshcK*vy~#MZifmiuQX~a2%_62>uN?(_=MfmBH_m(rmnE( z19CQM2R}OGGKGP@r9utsCrCqEjGeQiT4H*+aSZL&*sIKTtZHFrT+DVV#5{qE6G%Ez z%~MA_madAU%Xm&OoJ!Kk!6#xjgfhd#*y7A2MY@|{T*yFss$yOZP;NNKkto~8rwlLb$U`S=(PRELuO@E?Du!Ieb?&-k`u&ck;}r0K9it4 zrVjQ{JmlHPPeIX8cNRlVKd2(BY`ThFmMJpClp`2c)(*YFKbAVDCoCo?|Qq+FRxE4Qgu_HC%YCS(ZKxr4Pu<|r`tC_s&B} z&(vXhltS`Xdw`v4yYVQw?s&24T0D()6${+>2ric+c(4woWKr^cqeu z2)>RlI{-Y!mUN%`Mv45_)%1_E37P7NG1O37jOC20D4a+>yU1ObgiOc5v2}$!T!YfF z(_AKUnFxM~S8Tj2yUV~Fi=JLY+Vb9!%|`yf&J2@Y7nz`k6vzjtUKFBaWH@73b)FR{ zFZ1t@HS8U?{s=jSrlQRbY30sHw}^buJi5snG7&YaWSz-a){2lRSnF9VPQqilc;F-!%%i<11%aXz}i|5!wwOq z2*t}^Tu8XZR;2A=)h>{#Nbjov#{-U3`hg-EMEtN453#?i@$y4Rv5J3_!2J!6uo}T2!aLWwCB#%gb$^bcu zXOLSD)s;ioJLwxDyzkKDxr~QEKJx=~m@g^cDH8nM4m(m8;ZIbftlm9Z&MO1&AhNvV z9z<3jFsHKS4VE#w0o^TSXFT)N-d4ks#P~028#&-}x_DMsTtR8<=R=yBIqB^pWzdRE z3a${hyb#Y&C+0wCP9%k5?@P*C_!!PiJ3UWBn>w`hiMo zH78G8*oBXn)y9>SYE|cgig9&3RadTg5a-Hpr(&?dNEHNzFid`?5~xY91rzN2$pRfq zfcGpBI_xUg%T2uHe8~mItLJDrXgQ;bkXJ|BJ^bU(_&fH*=6+Kw=4bjkINLYQqERG7 zfc=hf3+7?9ipOm?5Y|b+MwzX$ZHZOQ2A)z(ByX9yanR_24LIV6Y!wzl3*18T+I>6o zdNB*|n4mh14ZNHlIxVl~35qKYoN>OdU=E*+tU$^%Og@qdEnQOGzp1*GXK|-<^bH75 zq5zES!5$Alan!Rh=M{#h3K`y|-N9*h*O#^i!3 z*A$30CTmG3@T)2Tnl&VfcPLcT1kG6%#w<@DV6LD2|Kfk`reM zH^ic>Aho3JrWrVhZxz!@4)l~_U&Thk!kt+AJ;8AtEQ}RpX2X$LT!M`o+oN9q|7%tD zy6z$X%LiiIHXG=g7Hc!zg$L;=6WezAgjf^%Ds?ZAoAu3nDY(N?XLOjQJp-!_;E^Wu z+3;_QJF$~BpJaa9j>Q=E1b@I#!Zjs_WYCwgit`_xQ{+JlWYS5q5nlIrDfw8IRaz@P z(ZlR;$Z{S*Re6lJ4){wZDnaalTUwR~EepQaQsD2UnWP2C;S+yCJy_)r5H(3MiuN2l zhQb#TUI;pvl@QEqYU?j5Ra|*SdZy6#c{X&d3{BXg+`>*7dq|709-ws(2Sx;T`Pktp zhWMe{6+?EBP9;$)<*WbAUI2Tl(j zBKW3vSf$7KpQ$5R&NsLttUAdi`OKWgV{7j20J+;|U6Rs;yIw9~uRdb)?h zxquR4&HM(j$~KcAH18IqmUH0Ql)@)LjQreQc0ps; zGlh#m(mTXt(?xg7p{65y3!kJkq@~7^C(b;qxVf*C%d`@(JUf!}TB5jyJ$IOBIjC%U>EqqJA)HF?T{O$$~>58WiVCy4y^VrJa6qjpoSsBGb50f1eG0YcB;{X zdaUzro`>b&e1e1`v8&6xp1-4#hiT_AB}?Q<>O!FXbXxH}y%B@*Guv8VZW%>|WbcJF zX5Bv7$1rBJLQzaB$G7*_V7a*iQL*#h*-qR?3lu%DG{GDJOIG;R0_3oL6L3p*_|-z- z=lIP8$%pvWV&pJ={~}dR*&jEC)Gw~-Z2{R%dTGDa4YH%`u0Z@=!fEhW-?!L8lX!iX z-Ym`mB5f3I$=siEikC%;*;BI<6WzYMDh;NA-Iwc|uva<7uN(5v_MDx_<}o$g1n7 zw3Ccyttfsyf;?XObLx5UxYRhjP|UjBetlA_&PYpW>KB7~g8DysIa78?X`j4HsQLXU zo&2%XUl@d+SoE^WQg#^M+EIaXL{F+AL&Z*fLKk4sF z5aS};P#^;0pH&!q=n9$Ks}U*n!B@*KJnv=hgE zyZEB2t@}p;kL=FZUd2?}ScG{zk&|0d z59dx$RZ1y~t6+&@#-kq|3{Qc;GlqO*$HRJZn1BH41!@NMQ4nN5%l~UApou;lAZwjP zoaYDb(j8h@JNc91B=_1!&UG`~rE1uu@%a63l~e0D*3I%q<<$GJBC-8nNKyYKbjm6t z|G1Hmdk>{wZ-@#0gd;#g0jJ&(pvTUYHwhI2w7I8W$o1`*x^AY){_VLd|6iuadN=A^ zN=enFyV}#!t>qehx9OwdX}fJe6$MOTzzVo*&i{oVSV>`8gD!v$9+PT2limsy&6n>F zj%-63vzMIq53RaWA!+1I%t5xe=w=aPIb`eKFvzd6lvHm?QhXG+vc%OgP%VY+H>4jM zDF5A)tcD<3M@jEd^es*sEIH>)w)V%$;x$A#rMDr=lYCi)+GNLR3(fzof#CThC@N@f z=|;=kN)iD9vc}UOf*k}ka#|aZA9;&h*xX@oXgkgiAtWyH64V3+(ToW52RxuRxE!8! zt>)^D=u$zQu4f}x}T%u>tKf$EK*NwG^+WJ2dN1i13@swc08@Qfz&smlm*wxzwGFqkiSiu`cuw}45kS(sE>G1ZSk^SoSCF5<68~4%W1=m(#xbDR5%w;{qhway@R{Gqd zkC?fE!T_j>`JxU1)8vxB!?{!9hEcEvDG0x%-ql2YAS@4U)CbVNckj?_3W~-L0|gTD zf7Iyzi@*P$)Qo>oPLq<}9p}YRhI&*pITS_qj>NRJ2o!Eef15+nRtXb8vIj_V0d81? zq)l*brJ4l(6zzPsDz~x_J+semK9vwVRMHhkk$C>b4z9J&tm_NTj<@UYo^Mcwm}{E@ zd)5egn;%W>kAt^K!|q5Qt5F8~aBibv&xQT*?RR}!xn!B4MStD4=x=P3`#hw|AbUU z9>`F|ntDZ+x(g~#l1of-n5C7nCo87;JsvjmF7zgveawktTR~Sa)X8Ik&|{G|t*(C1 zRl}pyYHPEz@UP0{sWj{=S|(Mam?pwnl$xu+HK&Sp+~kO1wODB)5K~X#4;L@`39GO+ z=nxNTdL;Hc5q2L;S0E%T$w6^J9RAYBzPy!vVKaD>dK7k`M+(y5mJe4nX&_}CoXafo&*Ba>zcM!2`s`=ydGCQdoc2R6ESb9t^0?D%wpV_a zn=KtiC>$AsbV9t(8P3$+!hq?Uwg8h#j@Ez<%?&4ee0Us{$iuuk9!G^iBk&3hCj|k; zox3`vVJvyuxZYFv>$u*iL`cQMY6eOJf+qYW=kWwG$@kSg)LFG%t!o1llZPDD?8mSp zKxOhLrcED;VNcr565{|;R^5|nzgu!OXx2({>A0gK|08I8sOF*d+^SWbd)98@a(wD0R&X`B-+1H{i53b_}ULGRO=f zW3E4w7j&TprUQ4R(V#)hAEvePMwPgPZn3nVLoNQH7UKGH%(cwAJYG3w_v@N-rq4mx z853J))m2$`8jzpPd+npFbD3>3zSxRnQy;U8J|vPfPWn$l)2J z92i=sHc}z_K?Qb4prui`p{;`8J&bK5ump^$ji^tiYLTvYxPKR_C`cgf4Kx729q#`j zWA{ITGXAqr|977wOZ`8%n0o*C2gyiQ1pp8sM&d5WXu?ZbQ6McAR%8c26lSEzS0$N@ z-N>${h#J+r)T>lI%T%oD)aq!pYX311;;m_U;XgH&bY1s1FBNQOq-|QWVb{N{Z%=i5 zT>E){-+w-bq2qgk^qv|DA|PI<+M5C3!|qfQjNQ}`Tn+IiY54m9ZczL2SMMMTg4C&Z zdn@rhfji(|!S5*FJ^$X|q)>+kX?tt*_Nj#$cPNE2;;7t_Kxn9Ifg+0@8y1F&9^Ido zI14#ELk!z%0K|be#KTj)X(K?}zlZ8ZAR3{UUX-VJk%9oyPdLzr-@%6J#vEw5u^D~E z1>H`f-&Vd+L7*-wzn1ReFYTlHP`LV#;Pa)y=Z}-hNhb0u-!TI+5!3l4X5G;&9Fg9R zyScEwNXF2@ZnP$*v~67&^%b~fo1R?tgE1}!&94Kb#>8EiYtCLvlM!jL0w%T?kF?~> zKLPdLj;$AaOzeu78*?)ci=^ApCQoWg9NNxiXk z8p3G?dsJDNiF8&uUe1fv+G7kTHVTMmyZZFH0xa4#fQ4ztJfB%mCt&4J*(*BC5G+&;$oLtfhKs z>rD%Cm!3LFB4tBxTJ~s{#i}n&OJr;vPZHFpwMwUH^SDEM)#10ESE7u{&P)nv)wT7A zgY`6)v~kYwaQc%T;ZRL!tFOw1u6d@jr4p`hoChN2BvaH;N`Ic;TbqpMDjnsrJji5h zVhQPKrN(GyV`8H5R69YKv)GgM;h>9uSu0k?J28{2=Br0U28t%T9ADmXkbvGrRymwXC9R2jA-yE8$?PUprpRy+X9)XD4`jyMrlnJb&|EgU{)Mq zyDV=zpwjJrwi5|%q9!E$G~7h`YSM790CZ44EHf3k*G4J!vdI{T~cp`IkiRRG|*Vh;*&WbGTI+=5znio^pl$o`l4bz zK*lzs)WJ_*Tp1|#^TWlynds=7x06EU8)T-^36=$P>`c<=+&+u9bRlMAPEiDJ(VT5N zaaV#3uB=wCXk{Hf|Ch3W-)KB}BQvQ(NHhgSS)T-ls(rR>kI0$or73)Sr=>GlIjHy( zu19?~n3YN|gnwlCkczI?O7#QLPwB=C)$gyqCi?!}wAsD^UD*rvSM?8thJ2FN+JovV zC^4M+U|;h?dyq|MYKzf&{ooz`Yg{eM&rW)nCxx{SalXw-06*zA@8&VtWE{}3QM)Ci z%Duf9=(}R@o)t<@;g(2GDa|m_u^ZvAZsVQwDRZ>`|D87 zQ}CUu@$M2D2`YUG6fLc?^e>C~B4xd?SLKB{WYkI1XsXYQ);(G(ajq}<8JSh+jFKx) zWX!TWQ-Y!fQ%_cATT=ESqRTfGV2d5<)Xx+5{f?byU~?8zGsHN*+hXfeTdHj@&1wu{|Ah=k|4 zRmvB3aN%5t>4w!S6W=BIdPjr5X*B$o*}F0oyH9B2ubclya_%&q!hKOrkYue2U^Xgc ztpQw`TXPYXP3#^PwN2C+75AFdpmcZYMgZyC9SG{{pxEPuF$Y8@9Eknu!Fe{8L#=41 zd!CMTN*MV|#t)b|*&P4bOOnc17Yr(w=F3<3XUl?jS?w&9lMgJJo0w!?KVle_Q5qF? zj|7!{9}V@o|NV5@0UE|Bgf(B>XsWj_LkN2TOv5hIt|ETlE}SE66sw^!4#v!{X`JOT zai8cl=VH_w-{WqB``DW;B`{lWj|t5pMVWD|ZYK%1ol1UUo#(YtSeoQ*C2$ReF=w-d z#0vy1GZikP76=g{GYabEu<JAJq{$QG1oTdIoDrI@c0eQ%AHE?n3`eEiv3lNROtB?f%=GI!gJXI_(zh1 z%HhdLl2&QMI>cPHn?9n+k(jIxs;v0KE6|#2qAX2oCNrc|!hK z9zej^0)*teF@89c7-!IF^b~)BTfiHMiYbC7ctuk;N1mMQa!X^!gb2IHQIJBWHj;(1 zcgystiel4Z3UqK{BMMSU?NMNX>Woq+SEf#*aRzn#c{PRxC4q(+Yf6e(X@0&cdk0LB z249LaH@ka2^t_9Xy`!7AGx|V57*t_~aKLLLv24sJ6(;BFs#uAvv@2x{d=@`pA2$gm;D}_?=ynsJ9rcKcir5MIDhTO zg*Bks4K&9A@#wb$UOAU>haEjOK;9bLU`RLwx*jz`2Q~IM&#WhQn5Q-cui)>EAxduC zFk$VA3@R)f!C@~8Js4AIj3X8b3gr$nTdQ4bOw5b92c6htD6S$0>c}alAyR|9`aL66VcjIjlk_VRdR>v zfwNMgpy&5`xC6@#sXC8l;5j6TE5kB zWJ%_$V_Sw_Md%Ag~*yjt$lc(~Gj=u`nu3PF?Gd^HR9l#y4f7qPZ z#XiZ!#(c`ah~H#6D9%(i4)09c8Oy{=agpcV9T#X%)2mLbm^KRggL-Z(zN1e$9c(sA z7U6Y(p~j?8GDNacz#*I4F-UzJM61VHb7tWK!%ReTG!Gi3Kgs2*rKODZiW#37&3P~2 z-^AYFvDMetn&iI`FzZ4Z!=3*hUPlzRdNx(>~QTlG=y63-@^oix(jjLaoY&T z)$ttn>?^JmAe|JU3`)Fyikev=di7v`Xz2m3&W;wOr3>-sk1W(x#<6fFRNC3&U<_5- zK`ywN1>e-1mG-zr)UvG^q;M=BtffsRt`*|}O-wsqPdC@sjAc8iiwh17qS-bOn;?K@ zku|>>*zopy9{b=dK8h4j1ptiicggjR8RVJ|rS0v@JRzD3$gojB{d&-c!yuat?nu3DY>69yz-w|lmB5}>c8jZ{Oh(~l%nZ1uZ9rJ&k2YuP9pcav4~b%`lk)PDn$rs zkyQ(dGe`E9Mps9Z&jF+vhlG>FIpqAQz007eK9>rozn{FcFgvlU64%^#!Md6!d$oPr~Kw|rqz;x-w@PMVQ=FcaloN#|S) zZu#&e7=^v4m2{9>sgrupOQ|zQ01scnK*F2wC(*97PY>l`p(3pIFwOei5UVo`+#!pzoLYqQLPnFd zLz5aMgxMBFV%h1mnruNPGmi9D$e5hntRrit<>bA{Bo%s)@Uc(8>iST9Tys!LHybxA z6>+GhC@e4bBbMt?h09oFF1$eoQO&LpbMB^u=N&3tXC`v=_%Mk++^9)EUC}YmaJ}^9 z{Cfcn?D}2u`I1Q7RAld}lxLBW96IdujarJx+mQ29KCzT)@kTbwSRWmh+&d%8rx`la zy6K{WNL$)uVC;xE zYDj2D{e;Vvp*xJ@jcU#cV{E<2yw|8B3gRal@h;wLri{MP#8gZ&lD6!U_VN@m)5*Hd zMUQ;rSz4I#jV$e^sBjZg9yR&ABS=57_gP$v$r$fRu}N*?tc_>?$KOt4zrW3Xd$Aom zN+0qL*KUILU~#epkYsckxGq26Cja?~Hep*aqsXUpc5?yh(Oc&N__plWcMf&NjJ?h#3>jyE5+5;!Sbl4oH9I4q9_Yv6~w$ zs=;NuA01uaStVgmP;Lb2wr}s;dgHuM=&4zLy;~iPT6xwQ1MlKZg6|DO9WC$d9X{>s z9SzP!<{;NU>N*y1-Nv~~lRC*XhK9Q-T&ox zu94Xh;B=hPE?xV36!ucI|LAL2(nNrPF)W| zi2;JJl>yjFV2pR9`&kI9X`f#``nq}Blyv1S(g8>(N-%1t4WeFnrvWH^kTBmhLo|iE zF5;HeL6*`D9P#CSG}UrO=*<=heZdYc?6Q~4=y~)_b!>|2O>|J_KF43Pc=qjq`5$Tp zFunMe3+Oe+H>1UbL|1I#5sG-tJ7d-p?;>#zil>y6d`z9s3;M%=Jugzv-!&?h7wPI( z>3FmOKe4o!Er^A+4z6TbL3?+s!*(ZASX2S$aI_{8Pj4`p|^zhB);M}{RZ zzA1)KUQc3fx_^D>Q|;joYUZ#luVxk_bIgS)QZ{3ct`GQspVEAB4lYNC008@p|D%8Zzdxn_ zx772Ge_v>aDtl663$H5O|jVOr`WlapBm@Othx6%;mVP`!a z$cx}A1QF4Y0J2fo$ZGR5(u!0sD4b`r79Sg5$U;acX`!Sv*9a;7b-$G{JCVUIO09 zC&pWcC;-dC(nn|bk<>ffTOM`HVvc(=WQzzhTTv{kvo$j1Mv0#NR#cf`H07&Sz?}c@^$XvednyigPNdT^)7Ek*~W8*=v|;wvJJw2+`)s}PIFN-&GCUSi|_x{}aX{M$J;(992j zxyxw91%e|c1Un4F%YeWR79R(JMmP0i;O9+>>Y&JvwnQwZl3p!Dj0Sn@YX@AK`{hnl zj5P`(VCyJjcZNL=v=7OOnGyM82p79WDFQ(H_@c21hh5*3-@(_f?hUJwoXN4WkZC(3bVI1Gy*$}pWW0NVE|l=ER{53 z%7(yaaKjJ3o;8$_zOyiy3aH}aDYmoUf9OD@7Plt@TUaS0b&jp0LB|gzT4<~9lPvYG z6%mR^#eT4Aua+}sBWQu_!Jg$tT6!!5bF*sx6o&);D!}5X>oxiG(MDbaSx_+IwG@en zDudj)AH|~_+EXbRi%&WU8#b)9>xLrFGgWgAPJh(w#6P_C=9XX?LA308Ykcqv#pv-x;^QV$E(po zLrg@#Kk`%$5sE#&GHd3+cwmNgq;i^#=BXuW{h;qWo*UHIV@9BU=!>i(MLccFRVc%4 z?K1P27CmFD98&`r4fKke&t~+urW)&^l;hS5>8`UM&~f zrFI-a1a`8)5T4uF%^m#t#B|JrwL~$`Eb~%pheKbSA$t`Ct;=S)oGB9fJo}8Oav_)94sr`wWuX*Md9dz z@65%w%Uj#qd~8b`3#~Tm!RF2Z)ovgGe}Tc_1k8NYj+=`;Dy%(QbX_Ag$T#+t(ncUV zX03F~@j`D);0PKjZ_eokEX~pk12CN1{-m^#JYMHCTNh#xKXj^G^1j+_oxOA8Wz{2`t;a9$1II&gnlY=$ESBWX;t66fg zP>m)B(a5;B;tB1nIUTaL)JyieyvT14#lpo^RyWpbq*f!2V&{a4-RKzjWZUO`+0GER zCs&qEI5)`@U-8wM`|HkxI7bI1eOb@qceqT5q>w{%(8Q_jBw2^dX2EUfWq9ku2#cK*5X1vhyOcKo>e-tq8$ z@P{SSVh!QM@WYs4cX4=4)x(_&l>Rh3g*WjwNY4mVAk`NG+6E1@Z5$}Re~Rs>kmYNQ z5V***Pos7oWxG^fDGaQIZPHP{bAu386t!~*On>A0cng&`_IC9TV2xbq_|G;U6z*Y1 z>Qc50VDZhSqu%y`7m8={PY`IA;rEco)&o@62)^8@|u_3fPxR2NZzdXa}L1-|+T z$UlbrS6_O5f&hK$2QdE9nT-0Q=lWyehe8>ivKv9JU{4FdaDvsNZmZ4@NDaroqH|u! z*O;J&NyG{}3xnl!yM2xIr{SIS$rH;5V{J@*a-I8PZn+`k!HIl~n+lUq?$P4}8Hn5} z!AR+r<^h)0GV=50Z z@AGjx(2_1QrB^%Fm8K&o8Stvzk zGo=Sju#e3Mm9wT9g1&OJBwlyIzd!7Q!NzqQ%xDk~FqcK&)cu7#03*oT2DNG)m7`w` z0P7x5>V7o!Uv25O6YdNT*qbU=Oje}$f; zJ|b1@Ks}^AvPz-5=p>~avQ&qhT#Ssz^y6R`TH`m}&R{(tDTGEMrrM}S@(h)u?RS37U_XYR_!eS&v2{;Wn%H$fxk=lFM zNHu~xBzzO97;-}!2$V}m6`MTa>Jl%3C|0y@mm78tH^LGvtFpxH&L6ml50vwa4i;Nu zYlD%@dOBjreVQDv)h{{42?ln$uy2yXA2Vnr>#tpIFZIllTQ*vS{h3G=%d+ibN<8pr z=@Bi7Ik5IS$8ootKYhXp=Dx!j&Enb=mDTwDOc;tk_=g^%K*E|+YD4J-1r^3iF89{! zBS^fA!aXuPCYIePpTsD?@et%SI7Vp|QOUI9kaoDVPoGm*RlM}gVIJo>UstrqT-AD8 zrbGdnw%GL_&%Nk+8TQo{IC&S0?HgmI5{-g0zR$+Op06i>5{NwjGn4wl9cnpWCYqi# z5;;?9gtaN7Feb$*g^>rz+F1=?2MrzIUNYt#H|W(Uf`%cqvB!_;zQNqIbZC_4@K5k5 z@`EJ0YNwoOf6%I9mAwCxg*su89O3&h${pq#O??c+E2b&*E$;8wHM6o%c|#-1n@igI)=!QsMFHl<7Y^FnnQGx{-=53b)5r#K zKU$Adu7~OpWkhONXJVXPvNIup2O^TU6EtKSX!rKgY`&1%WPNck>y45=MZDr^U^ifw zUOUnpAH)Yw@2WQOIVYoB-tacZyToqUM?J2Cq9HZyM}Fy&9{M!!KkD5s>E2LmiL~U} zJ#rCnbM;G@T4#2tdCc%ipO9K?H2VdOo!1C9m;55U8i(8x5!OA|c0TFev^MfHQhzYj znsA4l_XV{w**51FDCT8BGT6`&&~S#jSsN3RXQ9>=uuFk1FLSpb-!@a*B9iwzpa9q5 zBF}5f!A;u{#7+Y=gAp)|LZckqjmO;2?FV_p`~YP`M&$i0u%NhBO%u21-l#7!nf@Qj z&N0Zfw@=q?W4CSFwr$(C-MxG7wr$(CZQHhOoc_OOrl#htnp1Nsl~gKOm3+v`%F6Zp z?)%DW{Id7-8FMdGW z5(m3US<8egNAg~VUcT>tMcs{=?uljH9RczAOTr%6koq;$;Z;DELZcn1yhA6Gy{k6C z#0?a$h2Vj96iB38$SqRYkBe^s4TrWjkBgmze+mJIwvPk>=XHAu1DCv`go|B;cxLGx zSg}L(!19u`w;osxzuowfwA&8%dR;M$J?lzNv?h4!&L=r+{Tyc{iR_(Q_^3ch>0N;m zd*lH_%7qo>@|VfYM-{h z%HgPPz{pR1!{aBAAUlvC$MNj1)qUCBV0vDdU+*q$30?k(hw0?FT_S4p&8MuMU>dI) z0XN%w6WxDNrf2Wfv+u$RZ^rlOOWg0EExz3WxkAXaa&-@A6u0vFbv3(16!##lt=2kw zz*&ud688dFJEd6f13l9*-6~y74X5atTtew?qcuK3Q|lzGKZ$!&^YQrw9--rZOWXtH zCgBxBr@pOU1gy@Ol1-QZUrx)LSb16Aiijuj#m(0iR>;a&qfm@-7h2aBR(J%JiHxP~ z2auh@=uiu`SsN}pcxL~44`^B1va^vC?zZ^t!o*wyedRv-(-KDdD_{6Kih& z?oqEKR#!Oh6E{t$|LKLmauAOw@*9hBg=$2S5AYzHa7b^E;uHJ$6|>+2 zsN6RYtjmYd8LYl9jz!gOlnM5pmi2>?IvKX6e`5+ zIt1gt^7arURU^Q+8cYOCeGgo;zqm4fS>5v^_fGGAUo*dfvk$ttyd+{B_ytw}D{s#W z8zVOj{3_DH53=`ce=}7ETYC}EG0v)jlU#$i70X5ZEeD^FT7iCM*(aa~a+q9e-=hJuq`~+}ss@Il2Qp;I>0{1pfn)k<7 zOUfBb7vMSm+`o0lYmcnptiJyEo*GWF$d>eiYTm1jJku^|?>8Ko0Ptmif1H!^fX?~t zO^rnE(~=EP>FmF-J?Po;w`K5)0um>Hk6;~LlSp^F$dFrQ@^9c4N`GHo>HXX#1VEew zK9E`O_C3DC9aOmjaZIF&`F7|=S>o`&0FCkS`2C@48(3z$p#7Z=Y#h|Yb-h&B$}fHN z&82Y!^!hkwiYvN`Z?}4;y5K6MwKgv|H~f>fms7*IgBFrZwY(o3FukU^pCssPp1s#k z|H-3xrR%<9C@-!3@j;!QDdfP~NWU$ikg}6E<()F_f{C=2A7P}_KWEF*eBd6?cIQB= zg;*=BPyWJX@~!Z})qU+tntw6)BJ{%*F=%(e6+gp;vnTs1Y))FfUE}*VpP3rX=^^P) zskjd&0KiWs8o=0u&eYMs#n#-;)Wng_!ob3b&gAE+;$&cE@-KP0owb;Yfun(~v&ny0 zISMtv-I0zle5aF~>%2j$KD1g zfEK$PEVg?hc=eE#dw@sB(%(vV9IuY7VJz;QTT#(l(x;YWo~!*^Q(U`SeZZ}=-t`;T zCwK65k7BH5FMEhEE`+w<6B=g-kl+E9`zu2j~fH; zfxppvMiB=fLR_b!!C1u`?31SOEwz)8Ot#WBtHjY1@JqYY`Mhf37Tz0DYuXQg`bFS* z1Lf))r*B@Uk#?MPm1{dKwGVE|qv`B#;08F|0-wOW@;-fdVn1iI%tM5%hzUSNof#^l zEQsQ8V?Ke2NWnuN{gx}+UN3U%Zi8_oy;f1PA@|P{tCSd%~oY~>6dUJUmn-c zW*5*cY|c*?ZFa`mn=zxW@%^m4x&{8t{M^kVA5C03tg%eV!%S`)RF+f~(w z$r0vbS5lsNTdjSiGR$Pv0}NiwUeRH5qbRCRMt>TI%QVk$fn#x9a7L2X;dZBtL^`og zieRm+Xy2;7^@&t@QAProi4f_#TUnWD*u|On=p#Z{aZiYO3`rDmVh>;&xsVdDVG?;w z@X@Y<%6zI^Tc`{%()t#+*2-*Wr+IDP%hRvFJJKOsGL>`!yUvd=mR;R+#A_+SS4AUk z;*do?IovL$w14Q!+-g8n(_aLN+7HOG#6_Q$!q z`XpsF%0OKtm{~G)5=%{}MS>Iu4(tW38PN?oESQ4Vy%QmIYqM-ut7Jqr*fdUS1+NNi z`S4tgX2-zd@`$QdIBM61`zW&hA}h|1a&$UXg|VUeux%zHDhv1P%VN*Zd1{}Qn^lhLcDJ2MYi}s^qIsWP^qdY~mN@`+N`?F$fP%}4M?<7aQqUQ=y z+Y+frVt6Vdi*7^Wf?SjS7-|5hH_TSjTRN}+l=o8f8@)>e^N5tW>B$C=3jx`lNf5}@ zAG6NtpWv11q^_sBLV<$9>4Mli^{{a$`h}ajw_49nkvO2l2Y-+22JsF9H|~CW-K^3>gt`-fj^XQAp zZPxT0etrAJK)ylQ_b-9J_4WX?F3@Jc+M=H7NGKNrSdW-47m+!5x6l>EnD5L?=>c&f z#;fXDwM(OQlBa9sBrQqPNtMDT?!@--NsbUWXiaS`u=aKNvfUO=uz8l);YEFmaorlxBV92aV3G$^HowR9GpD(yLWIQ;8iVgv$`@A0=B(ERPr0GCfCdlo(GHDQz zC>^2PA*Z^j&Utq_4?iDJ%N6Mv{L(AP%gk#k65vY&6Sae z=8WSuFx`zaB@&>`Cuz*XmiHGx9M>$f(JghPlMGDNjd<5;BEeyK-zy|_sH;)hxKgS% z>)bHRF^WSemJ7f~^{+u0W*?VJdeK)5XaD1;O z?QQ0ikTX#BUvjGnDJUfIVkb?@H5(DV?yK6nV{UL^s#k)YY#MYo*RmzmW&JgGooRu} ze8}e=##Abg*SXHyuG;X}R9v!{ZNX)!0GpEyFfBis{NgU|k;yqDP|nW52)6ZXtLOyF zi_RM^+7?u|Ufg$iDc{`V0`V6T>)R0M*Gcn7k*loRrdt=g$Qk7N z0l5UGG#~4?*-UsYbtD^=2|1;dC8;V-+=1;b^9LK`;}El8INVb#ymxLvav$;{hQn56 z_2ljENDPRW)wV#nI;$MpbEWlHVvx=~;EuhV>Ede9GFfLCm*qi5_iMys*Ge;H8O23N z+7``$R8cIPsn<%JaZ|sqAJ8<-u@*ZT2ZjbR_D&f~s&d1FQqT+dlvH%8s7vnun*z+by;9mcC8qzmpN ztI8TKT2wTRq+xFzVb`69k1VMh^Ar%Sa-ThYO{jh=-p#tEbAfv$WGr~Wqbco+l8|CBNz&w}}2U897_9KHhPkk{CI**p$O~2I9E?OT0>UF-vw^!{bTZ{^Y^^ z!S~-6EPwUlG4cM4l|=q5TGIc|XPy62+5ATV&wm>x$xOD=!V*Cq{>B3Gui?-6UAeZT zIYW$Ju40LTDFsX;u1TIOUxvaqiijF7)3!RZQ@M5~j&Q;=R`%mVREu`PGA z0+lJPwa66e07E>n&XS-qjkA1xDyK1DjX2^x)UB}$%`r`a;61-o$3;#BW>RrU4Yf#p z<9Am4%=!r#hSqAFL8|4FW~1`_T3n67wU(0Q;GZ}pn#DCbtFcS0((|Gux>$CiQdDN5 z;xwhMI{OmJBP{CN;aL{5196pVrEC?NNbKFpI*qmYQ0c_equVA@>WQ61%h-A_IHJV5 z115Dv)qKLeG*u$~LS>eE&2k4ZAA!wyW+QaX$Omf3#}7T4<1Zv~fh|faQ-x~fhd2R3 zMAxPABwcw$&p}HT(}LFSfZZV(eyV{jh523~~Qii(8Kc51c z*o!o~8!aU?jMMM@buvO;hGNqvsy=^c-*B%C*s>ZjzJ*yHtR0l8_M}^DXavPR=vOsq zO`tq=8b&bh=~lTn9j$)(|r8^yVNsHFwHf>;u792d}1aMGL_D^yKS z%L8ht4CpoawiEwY9wZh{R3({AvUbWU0BBXxN5cZGAP1xziJ4ULdXJ!V=k z_MVc2U$N60c|rUMSoNtf9@XL#T=~77Nr84j zW}Ft-qfAP+@j;&e7yKh=Tsd(;H~}iAnmE*FYYDKuqh42Y?@Vf-Gk_%9sJzrt7hE_d z$*{!+>}EREzHu;7oW>;`=TMdidyX@zyo;;^`_lQiYOiRX6~g;%8Kua8{Ae4xlIc06 zZHCNoCj(bxGs@9;M!E4gyI;gDgJxx#DVbn>*31vh&r-*Xz;K&ncOfoxwxI;=88y8f z+b_P4?Zch@C&26!vZhja`}$M4h)4X1da)`mQj(we3t9049)vYJ?-KG)^eEbot=p>< zbS4;|(D*$Ra^#WM9pU(N(w&Ia&fgRCWK*c)>4gp=ibp_<$vQ?~2n6G(X4exOYe=$T z6H=@gBrj@$lXW5%9AePVF~#6(T*F!MG6v1n-C%EkosRav#natkFA~z-A)Sr%z{SHo zexRLvCvvsJp@VQIkf(!cARXam=<-7b!ebM6i||$vXe(e+>89*AnU}l8o(+UMlqcEsX5I(%O$O0$A?;HBWor{G!cuM0i&DLIlFE9>q9yYMl_6&(ohtB(ZOUN>a>>JiL zvCt}hadZl(zii-CgLuXp< z69$ERZtidBq!_;P#$I$b%acZ{VuJhINr-A68k`77Cod^R2yC)7+W!^eO}kjH!wL`U zZ+5?SzKbAd;tLLvH9?Yy@+}nrJ5B(fg~<2iEl_~#c;t{YB$BhRw}zOX)cTZR^(TM% z1%}`zG@7GgqNm3M*@NyYz~0pa^SeOvlTLZV$>MDga}3+cyKU?oa~GAe1;+@-4c+6J zMi>cxE&VySFx-+#(6lG2Xw6YaazyneM z?LiAvzAPy&v^81IFYonL?a`i>RaDgF(bm<$qoJv->Zk!e`(1UWwmLbHWzKxvZC-P2 za~!|>9skVtAE*B~>uzKS+v2(r3&3bq)hTx45NTl*0m1)b8#X4WI%z;Ak2eeMj^Ts0 zs)NlJl*XBQ#+cnqAZC$lj|VVnc_xF;whIP12`cwH32{dsQ+42r*SE3@54UQj1wzrq zoeuVO3VTX{j@2RR;uf@O=iA%FpgLvtJ*o(IC@$G4wzP(&s6oD;A$CdbveZp2&Q&B9 zsie8gb;zVW&S@hZz;-Ak{A?uymX5N8SiquzB*zx{3XdYb2)gX+(Cb zv}Q>8*SQhpHG2-uTp#2n%=Oml)|m6_RBQkS*4*6ifh-DPL2=50dUh=<6ZNR`cpx-uWLizCZ;FLMmCf}+12S1 zRpvhO`VKn_5*W<(%4#i3Aw01X1I=vF;G=MM(w{HMW?wDsNnF`+b2(8lYcAT!K<{MV z73bXS*g~G43H8{5BDK}H-@s}upfr-?RtdEx0}F~p!?JLd6HIWA#9sTgfa5~MLwC-7 zlSCo(97m2C{gs3#xB;F!8bS>HHKOHri(N!C2|tgLpWv48*zfeJ+fC>?x@?6T7gVTF z-u@}pl!d)94oHbGmP0u>W)N+z1Nel( z;r*ISpB0Vpv9ZkHgTX+fTfi~b^N+ht2Q{D^)hkXN_8}GyxD*i<61*G`i30_zsBW&; z;?%Y#sX^ZIwsozCg7tlLRM*SP(ublG^=&PzshD#EoFYRxtP$n{hLmCAXmG1h;T(&o z%P>NJx=biBlhrwMn%V2D%)FLWQw-JP=fyO$xWfD*GczN7e8$~qwwr7&XJ$0})e%C4 zqp&&tRAM1DZOO+!PYrTV)m1OzZivx3t+7?_5+t<#C>WJnh%cb>`fe1*>M2nn0x!5v zwW(#TIr1%7R#$a@gUzEQ5N#yU)(ZxCAxkHl>StU9CDHPbLW0O~Fm2w%SC@`TY3r-W zloWc_@hp{|pdJSu{MqNV6SJ3)rPg4ltdp9>J?$}hJXFZH&()knS zu*s>cCIRg}>Cx}R3{};(F~xeNliIG2IE`%vwmi|oy0BhMX!6f zISPL3MjpJESO>l>#TU#7B1vy3(%;BX8f)qqK8ZpYjGrJi___^(rM=dzM^aw3I*H*qMh*N-!fv$GoMBAR?SCSA*`8loJwg> zPoOf4W%cecVD6GRE{HGZ{VIXZwSAC9h1TUVr8oK?dyHhJWdi!*I2pBZbuAG_w1`-; z8oZXCmS>=(!YtqB@r-g?q^EM5QkqwZ*lx-g_`!$DrVT{YeT-zgMYJS5QxBhXbjgBP z+n5Bd>WgWj5;drsX`l^x=8h9(I(J@Qs;$3Ir%3u(@M$L6Y11@MRAb%+tBM@S_U6)9|GaKzs!^ z!77|`d`hPxzUsST#y4@2boXM&U*z+BQd`i!cA`rksCb7OPUcoMR`{ys%0I~GGbHK? zZ+rv`mtW#3{laJ=DxfL-(uRH+d89xke-8ww^kw(*&>x=N#9hM%6OLBRAsL2*8cxRW z8)<6R^Bp6Nf0F8QYP7BX4K};BeYg;;;4w(iB&6gMI_#pX13d%}HJ214X;G4TAPIIY z&asM}h9l#Cr+XBZ<|_!l9f}&^;&=yyQ?ybA(BvjzICj6Om@;(h>_p)nXp`U4xTfc> zheBGye}4{s=x3zI_XO@IGA&wY9Vk35CN&pVvAu{){hI~8-Qxdrm~#zzXZGsEp&ROT z@O(BMt|v_5!LXT7AC_@b9&z1x-lXv@PH4$->n=k(bS_uaWUgSH*Ji4y>acV8x;nl# zfR@s>jp$k`;;mp8)@o08Cvt9x&{2g&_NgSA%e_9B{;^lN`K2)CNh{fU!Rr~mlGcoH z(1{mGh7{7(spS4Da~Y^(Jxn>zdEIYq>FH=9^IRTE5qErUX=z2*GC&n-;e|_AT3V@* zm8nMZd9)Do7(WKfk=wFT1Yx>LY-RQ-U(fWsN844J#`kfsvPJE){Y0)kzJo2 z6rPV_qE9^E*h`?XN6w%g8E*v&< zi20HkpW)Kpho57cGf44xJxOA-D zUTZdOX4-pQqr)#t;MY6BI>D`<59yab!Fc$kdVaf6hg<{qFCSdoT$Xc5X{)_}pg4om z=9NMzqhuB4jc`!%3Wa&)4X`L^y2bw4G^211T99t0o~Nni5<^i*G-AGlQORtw@5Zyi z0iC@9i_>g_$@f?gr=oy?w=Rxc@c&*1^n?E}q4NRAD2t5c700V%ef;Hvo(AHF{xf)_ z{0v*hnW&qyLU`o{iN?`qnD~p_BH7fVlXJ;DsD(Zmz-1F%=9%)sfr8orxsF>2=M^T6 zA#E4s>i);}q41V6t%_WIlM`KY2|mpK-clW>SRMCLAcKh#P zlHfcwPd(Ir&R7%`&Wv^Je(e{U+116J0T5fKmDrkt)z?NI?9?Cr8D7zRGaLsE^ zj?b3eB(N?=L$uIbsQ~{@a{N-E@6ef} z#jIb0S0)?q%2HDxyD@I~m_IwPn&+~zs2|X3>!s7m1} z(?^<1kY2GJJHpS`LxfNf!}||r8!-PI`YG*znMi@wA7boN+95NO0a!v*9 ziX}8JP^~a}xyk!lVbx&|Zifo6~Zf&SPHO%uakI@PWyQ7}MsyoDt zgOn*2iw+n)6sm4e17IJS`Q=fw3IbuXP*%?207K-~k%oV;@)MWLyL*O9MlnN^Oe4}N zXIbjdA~Y)$;x8cb8Lb-Bp8^$HS<)!l`^6hYC3hhyVKSt%S6tZ11ig`ag^*vAA-)uX z;!|M#T7hV50mvGl%w~d+S^&*v{$9fO0yNKQxF2z3oO^J}qV;%i`t(8DznK|O-;T|n z*7L&J-b!*=hbIs|x_GMRE@BaPW(Grz7lDT32T2c3qFKOpT%Ex&Y|SgH&}}nlz7sb_ z&9-$K3J13tX1uZH2H{w`XSC^-P~n`7F|-|F+o0!13fef9$m}o32-^6j6+t$>ayK$G zwP}RVz||gd+aT;U3Q}{)X{B;vWn{GG^p2*kxBYJEz{RN7ajzDt`)JLOfO|Thzrg9+ zI;|<^SZSZTSP%3GuIb;lWl!W?K>iceqai4#0QyM+)n!Kj=#ySma4k>u$pZI@fqk@1 zsPlybReMb!_YGk1NF5e}w|9~y1owr&BfuB*aQ~q52I-Tno1=D8{DAd_@frISx>bPY z0r&X_ZAMC4touN8gU<0qtuHdGCk*X$+W!Eh{22tahpN*~vckT8{})Nj2ef_zPM$xd zUhykbWooD&lWB)+q4+8EH$SK^&j@#*j!XwGR_F45wNnDLO3nugdv{?!rk~I>Iz5;! zB7JbC(UOsDM{`Tn7YvJOSw@b?Cyn#x)ExKtk>tBOLquu=fX_DCk9D(WTMlvu{sXY; z2FM$@ZEFJXMgx3j`yB|z3&2rd62R{Sp8f)GgrtOD(h0zO3h)KKg|CAJ9cTr*W}+GF z9id|xd_gYGrTK8->m!xY8V4}$wCf1B5faN6MiCPQ#<)3j~5puU&v+6s% zlg>Tp#trDlvvHPDiECw?CrpoL|7xu0#W$0AFxzRdoRMa|0ErRmTkl#zUN+o`nh`?fg-v@2AP ztXpdO0B6CwM4pn(Sw!c(&!cp7xDPjTOqT7eo_+R~`4>KmJpid6xez;frMebV!!Iry zWWE|9r`Gf+sKw$)(*3H#g|>d&YCt6~;0q;QFZBlVT;WB#*awiZ*|f1opahk;1AVZG znw!U>Jx2N<@y(z;2752CKL&RP4ZbDoMD2>x`ojC~t2I6oxVNQ0kZ1V^^05EUkVn|U z$=T7u(8bwA#K_UZ-r2-h&h%%9=)ZVRp*plX_CNWu>R4AX_;I-#DPPM+ZK=A*T5Kbt z2$~q<{YT){-;8TG8dlY=gHu(t6GZ&K0SM@TB(U#fdx#x0xgt#>q@ZjBHPc9Qb0J|R zB+XBVNJwPN(@FW?vQuZTF3eSRIi8B!pR>HCTEBUoGn}R~kGFj9&V?J&y7%%;X_eF| zj_(|+ZWIvSy)Vof|13G0aHx&iHTJRa-Z`Ygn2IY=O-kcU@2wtki}@ZynPffC-sQU0 z!W_R-^gw@f#Gp}htccyWOvpt$W-?15>r&2-2Y9tUs6r$p9%^6bgJR3K^ootQDC`z# z66%tiH&blr{!~T=_}Fmh<=t><94lHZNzGGCf>~@4Op032-rtgLQIFB7wMc=fk#`J= zowrPgWqKF({Ups~(rhiN;sVtqof=`h3QixL@a&Se;P~|VxeC$l?rB$7ZTW2Wc?4I> zZyJ5u(%wyK5J^5hLI>}P-F&Ri}dB#84$7OA`-H=84;mz7y5!Dgt<#hQvRqJbP%PY zFUFTLm{ajo49;;X-I8ilw9>y$)|Qy^yBCIDivQxwL$Kn6!7eR8jVzAP*CSj^jvDM~ zp+~!~s-4x|C}gmak7ig@PnRjqtJRlV5Ep15MCSRmtawVyZ)-1OcM^Kin_G|+1y2_A zU`$^o5}6al7cZn5`XF43LbKG0P^JmBuJi2gPFVU^5AZz%SB~LTlqOun<#(lP&dH)^ zZwqO%VSj|hEpV?qB}OFPiL-nY{Lcw11(N*W?-VMYJhIdU^8xj2oFub$l|In)Af+GR z6JR0H^$+k7=EOj~V6t|RZIygx&wqiU0ljBk8K!9Jb+DvBr+2rpn_epL#gC1!yrrcn z8Ya?2e6z}Uqj6wyV<#%-L6FsQ)sBUKrTr8i^Y>MadWvwLG>%KtFp7FrKH>G5Y2=q(|k^_Y4_O_m_1S!9*Y()+VuHS9!?*{s!Q-BDO?7GMC9+ zb2AR8vtmbi%>Z@*faAu-iO`g)qH&L{NN5eS*uqx%dDXxQCw9zHc4t@htji>ukfHq2 zNO4a8ARcKFozM_k1C+>fpVL?Yo>nQLIQ_CqiGaoxT-YiyiGISQf}nhezD1A9s88bj zzo!XZBpKLPGN-YZLi#7T7>UBlp8Rqy{qUARuvZ4*05-sMWr48 zkThA*;2~<9n#Hjm<4M?fiJ1Be`W%vYOlppGfA{*Bi}=S?Yoayts#c~j%no8U_v>jp zV_8(IC2*+Pa1iNNbI}req(D+>W!?&me$^3zuQr6Jw@3>bFssR@x+Pv=Q9>4KUgZpQ zZiv^As%Tdd(OOTgC|6GdbFKr3`8ps(tob`ZMx?WF3MYEoyIyBz)1xe@BFGm4NQkzZ=`J1Uh zlX9o)O+amRI44cD`dvb0&T3b2WiH|h#QZgTr7}lJOo-RY3yW=T*MZf14F3%-{@-tm zY$-GwiKYD?K~4wb3%2?hrvXW&GH3U}!R1@27Ve|qK%k+}7i$uOOZ-*CYb4usA2HbW z8OJuzwA0}v;9@Sem7Mx|dQpl8aeZ`RYN1LN2bO6V_IjOv_+1i|)O|D4OKTQ|l@bp* zSIi8)ADynl?N}>Fk7fuC#&WyvphuT9q(7BB{4zpHroW=fR3f4>Fj z%6XGMgb#^4hpv!NeuMLqFFY>v0EG1uhkFt6l`l+w5eVd~nA3O*{Q*8gP<+&X)s#IG z`SNbWQj;>Cf&2mdqzmWvy<@Wo&91?Yp#ePLA6{zgK2T0o8kg0qj^H9`eDZkD$ zPc3lrl;`TFd=x0XFtucbCwNfYI4V+uVw~R72NCu|jgHsW*fr_dNoK8I$?Y|W;l7x`MT5!cyo^&iiVqM?2S zxza~Ci3+E@FD($}g${}K5HV`N^F{C?m5<`dzVX>gh0ynZ3;gF^tF*85cP-U}_R165$1Ig(3fS4_n>LLs?J z!mji|>y7Uq!5Ud?KNusUL0F8n$c%H72O(C zHL(qr&03l*Nh<@FMTl3x!$o_gpz&a#4mRE_HpGx%V;~bM`*$%~174Ac()}!=rqtv> zLwH$<@D`e~u-9#zmhhl6&B^C~upf~h_R|V?|MQ0=!<{rWAJIch;4bq7E!NbM{O?Q- z2U?aPoLtJpMvN8QBF~#(y8kwHh{4~y6w%RZ4jd+Pka>3!ciCu6XQHB8%bN^YjD(?w zUG6zH3}=veBafn=Czy)t94=l|;hWZBJkeHc-D);>dtHkKuEExI;F{WbR<0OMc=X%Z zqFb++PJA`tz_<$qyz$VxD9wVm#rHAxhT@7#O1fXL)7JLZ;+T{%J^zf!#_wg2yGbiE zXA$XrW87tvvaDB5xRzsP=bWvjIs9BKmU zpqt}8D=Y4*BBm>j5#+V!S8v*DCN zV+v5qW6D0Y(41M`m?NK{F|#m0h@8)>Z)iz*!u{@tPnS!}r8ZYp1YIMh25oaF}5 z!~v@Gx6OkSR$)a5{;9pooK)@?pL5uuqbkH~ZZlrFmL!diR2T;&fv* z>BGy!2C{}muO=~wVmFH&h^shm-%>bp6cABhxi>L{_VjlH2u4Tm(F9ZZPC5 zr^H``1;J`VO-P?H(8grJKvRg)l)FTPhO`NGcA2vsn#yI4;j2c7)qsyxA%J?4=VcDv z*9C6S9e(vUeKQ)CFNR?Yvn-cxHo10QM_GzYS}2rH!!$vc&j*xB4t;v>J09jc;Fhw) zSUwkJF#N3Nqu6=QwWZyLh(*0ianaoFhI!Jq%!V;i#ZmGoEsf>}{>WQjT^Xup{uUJA zUM9>tCPGBRPXqb~{+Llr;oIaW*#oh9{aNi(5fo`eNoD#d^9b@2)vG7EQ=JGGE*W}8 zW_d;fmXIpx2H~sB^@WVnFX@KiD;NBP@?8`BJbbZ$e_V!W1nw5B8mOKrztQs7Lpim3 zgj93p*$b(XC+(8kf1q(|L;RsXN|7?r3KpZGq{6DVkj7yw^z&(ACIH3i>r}teZ=XSQ z93!q!3(yc2iZTF=#$(JOD8n4ftuZaKDR>2(l=6`f|)g$ zxUDI{K#Tdj!e2r~drwXcaJp_5(dgG2Wj1l!4w~Q0E>ZJAKlmqojZ?rUSpF|73%kH3 z`GN|o%ObnZ6Nn9F@uZ+leW?x7!g%1?7xm;X%Xf`4L+eK<0@sy+sQLzx5H+aUJ8Bz~ zGn*!-qdikDGuw0Ey)shc3ESOHsL{J(M^cZK>ZzJ%2(RkNsw0Ez@ftg{C!9VwzxE^J zBl&m;P9QT59-+mDWb&6Te~kmJ*rJcgnv(cw&4@hr3pGciDbobtG7<5HyEwR2+G%b1uH){Qn zS$$#0CqMXyrT!Ti<*%BjUb6bG>pH<0!7EyiIfpRQVuwUHvL+kWx9Oy3JY~#W@<&uy zhi8ljSbLf`Pg6_vu-*{`nx()imA$V}Fw0j^7Lz_q2eCdJ)v)zIhP$1H(Y%G_s453< z{F~LuTV$z6)L`Q6Eh9RU0nl3&S&u4P&$cY|&T|J~QyX9xsE*w+;7bSW%{31on9ffP zy(U2SH7xxJ(8ys4Kjj^OcQ4pK_-ErE`~zDvR}c0M*s%;eLdh1R36Po!J`!&F5B|yG zU)$h6@&tN~Ws6~Divb0wqlb@4w#MfZeUSm1y8^2lKpK%lXvXDV+vHaT68FKyr%S)4 zEXtmx!sX}O;7{ZB|6uyufEEwN8leQR7zaL3x%U`(pvzKmEW{c~>rlDt{_|w_aNQJ# zBg>z-S~mpLfcu;C9^eg>R?-Gi_j(T!sX9443KA7EyX|ExJ{Za zzuHQ+4G&DcxX+UTMpgl){@){a~>)>>lOV;M!Ozh zHA_L^PVd}KWvO}ajn-vI@<1Bh&@5V~9siVxR5zPt4!9RBP6#w8Kkp~&L`TBI)z5RdTpFPv_AN+&nLMb-Ypj`n7+~1`zDt6o$>p zW9?Dmv0|zYRS>a8nnsVk;MVSc08EAGV*obiNTNJJlDSOjs``jT7VXUyngJsV4q{GU z(Jj@WM6^0A0z{v{s)D&$f$ z|Ej0Fr?VaL!apFpCYK)rANb9{nijr||CO}2MNF6q9z=N@ukPvI7jq5*Qy8^0*Cd26 z9Crd&H@nCwP`~Wv=uAr{G?}l5c zROB{UM7$OIE@yQ1n@~@`GzHlMxzH7oyqUaNx&&pcvjCgu{L%-|=tbHVuAGV99)+Y$ zsv93!Cw+~tbmoh?iajGX@;2G$>(7+X~w%RE=& z5($Tu{nz_sNjwQ=h;^vWJ5;>S+!$*Wu#jawZYV4K6nC8w5(Kg87rsA;y{%x6#8SJJ ztI=v-j0k-~OUs3&&}+auhiL?og8aj;4ygw+{pl8uE6gZfcr_FWPtlkW+jur1Om5Lt!~ z0DaLKkBMw(9SGVnz$YijcdBa=CWRi#?mEl@DJTmL4-RfDa126a>5i7GIoPq}m(U@? z(*DTwGV?}7T=G^aG&h9v{x<9GBWKzo?jh>>TEwa!BPZYTp0}>{Op4ml9-AVusoB@3 zrrX*!+0XAI{&X5|S+y%&?DWbOpSa9=DhyyPIXRKQZ}-Y%hqsAzE5Y3CP};gWIJtzP zBagQp@0*6CZ;}{BuZ(wENeAk{xH<_dk-gk zJak0ZSvu6~ucQ1U<;$|#4Xu9{zH+P(7OQ~r`9{MzPPFho)Dl-McZD#sKvjxCZ4OY) zp#}NdAv$W+WI+nOkfZU&`TSn*$V(9Om`Xl4a(5AM8D=cfnlLjiciA;BGEd2mg>}3z zkr%#cO`l)KZuqo`C@FeJnz^J_EVLZoNQ0I4mqv+;Not}zsKIRu;vCzFnz9P6nl!&F zk}X%B6LCoyB&PqC*LRnCCwh){!3z>n*OtP@i#eUku81 zs3>8LB{N(|Y2fL?9kt}CI6J1iAjKkO3SB(03Ea5YUKk%bbRYiaew&_q9X2D8IvdHQ zE*OI_QV0tHD%(L^F~G6WZCJ}zer zbC!GKc&a*yK&-#9IZcqSmAMYlIUl&#xEQj-ucP=! zR!9N^US_mZ#58Q?DRa&@TlNu3y1gxVk9EPU%$iK@{-`)G5dx71%BIAq77F>%v;hwo zrL&pZ8ClqDCJ4u=h&n4dWT}Ue3@eFACzMPDSfx`A@~HtP>LAEJ4px)g8zI?M2N9V7 zW)=U5HR*mszxfte)(+Otc!8uhW@F2X>iQpxYK~Op%u$BZ`my_<2&o%6=F)GGKkrd8 zn2h>Ki?FM4SoIP+Oc(To!~2X8s_{WB=@Lf-Z4)L297(C^N`Ejf+1b|4gHI%I37;zL zQo?oEfAo74MXl*0$;=9~0Tr_7y@BHAB0B`62fr#*jLa&1ApQ?!_ZS^nzqX4$9dvAU zY*jk8&5mt59d+!EZQHhO+qP|^lT-by_3pjbJH~s?K3{9hPgVE$*O>Rb=I`|v1Y;^xAn5mp*q4xHgKGf=76UzcE*6!t#{JR2 z@nsVs72~2EY>LCF+8~!)i4H^+9O}{eg7Nah&QFecq;H+#lH&GcIZ-Q`B-4 z``-kCSk-egY}MsJ1nL#^NNMTuiXA?R>M0Pbnr8cpRe_0N#H|Em|B01&Z+Lua`U?6W z^uXm@i5Rq0W|1~^#N~ER>7ZijU~QgUH`;2M1_Ak9HSehHJT+gmp-6RYo(pMD2A|(e zU`NF&IAIZV0Gz?h-e+mW=&sx?SG2*j(xO@ivD~deTKfw)b$M7F>+Bx{cFz-MyA%te z_`m2-U&vDnziK={Vt@_g_e=mr#sDv zFI5{MTWr-c+s-Wwp($k!?{Co{4a%Py{El`@NB)(%2LT6;dtCiyeC`@WmKEKZq2Ytm|)?D%#&N)n=5;6%z^= zJoY?N=9uVh;~-?xMNLfu6Kwu+7| z4qY=|WIG2~$<8jy#&|11A=rPFsWt=4RG;Q5fMu%Wz%o_1*)K)SLMYCmh~m4nHv&dW zu3ZtNoHUmh^NvIisic=Xcdx0Sy{8bH^d{iL6|(LV7t-%D*} zE0*4OVLrDHi3*e5ck#H`$x7iHIl4OfPGZJwO2jxuhuf%X)|xPgg4}i4SWGo9tmduF zVTb-R?hmRK+eB-;cB7Xy#_Ylinsvr!5r&rj^6nOj)Lix?hVv?lXr_~Pz8A~z5hkO1 ztakYWHs8Z>6oB?%E=rriI=PNx+dn`Q4SLnuGgIfd>{)V+vYlTa;zN!Z)Zvy}#}2bq zRow)oZ_bX_=8J&gohP>zLAHRFB?)Od9^ibjVC2&x9g43jV;%Q5r6{mp1 zD?l9BcSyR{<4pM(?`QL5Un!qpV;@d>jSA@wqgq zNnOnz?W-Tkp%fj0CUV3F9Wzy+Y+)ho0i^Y?S@JIK1rh|4MSIM-@Z_mN&Cv=9OIe<4 zQJ8-00!q}eEvVLvm`Ig4;|G4PvQhLQCy>{Di!59@`HHiDrErnk-$3=26u1x_nH&t zkiz#ON}FXnjEJ?JNj_zPnc7u%swK8{5@3y zao%4i(3I6vs+t5yjt%eaaFo+k4*lWlcEYkiF5(&mLn{QyP0??uUYTnVVO=5WUY-&7 z#UJLB2xWemrdohYD^_H8*42ZUwpDkzrqUYxQO&CppXSiuo}OLp57R6%m%x{vnKjoq z%(Ulm1J9HxEtn!Ke}Rnu zg8C%LKH7J$zWvy3hd*<-a>QRXu$1i1%z_DW!7!c>oA*qobe6v-Q)tT;u(q=AYR zuUKVaB83@x4_uk1j6v(iHyY`bOWcJ!2e6*P2v>gyiW(bqnj%gOb8HKqgq#l8`8I(r zOs1!47!Or2U22u`fDkvmXe)XNf?NYAJLKPdQ9DwxgPLK~hv0u$nDL0&{zO<)BEa{B zudn^^4$FueWLnIUs*uI5O1>4#sGqGF_Zx_s+e$%jE;G*;?Nc31m4c9!cdToZU{m?n z<6c@fXa+R}398&6R?FCSl3~xpfo%U4)CfMb0P;P2(-2CA$SS{U=?edn!30c|_n;+A z(e#<@51eezzNqkKgzMq8s<^EDT{W1vhDYDabNy=|hxn>TU%<1AXP=D6s%M|bbNp){ zb@HkYT^yN5O6S)t0}rkn$DOqR8|Y5Um`_>`{gB3~B9Syc^sAy1ARFo-UE@2&!onvC z@j{V`Y#xb9zfxdZ{)6D)OS+LZpOAIWCsynEH=glf28#u;r#jGY4JUb;>WJa(ugCOd ze)4U+FtsY*5>E5}l;eovy#_l+)5qS=pqEpKTHeb;T87CN@oMyN8qOC)S*Y2EFP@T^ ze%OsVr;BG?5-R8M_$e_)pn8EyJbeefmHyj;j;dkZYmrYOCP7c&IZyH( z(>Ta72Nqj5?zHrToKP!F&3U*Tg$Qt-xm(g+xI}QKrVkYB&FK&|i;hD(ej3`hT7|M^ z9*1`MXWITZETnSN0Nc<7oQYKv}pI3iFzwCewup5A0H< zZG&$)q1q>dY>wl>%|H1fVu0?6m1*qK4FhoZOXM*BQ=6K~R{Tw(>rys8X|bZRl8AK^ z?64xq%dt_dYLd-99)&wiC0P}RCF%E&*TOh|8e!0%`s$Jww1)9{zVkp*+-B`N$CZY` z(s(vZV)8%-aa}Rb82Fk6P-M|Gf_@vq>7Rs+8bpa-3|tX{WzV4136;9c!D=R8YRDml zG2Y=mx(cTG`lvc9dYjG(y@Ng`5Pqeh-xBldq=VhwQTBh^b+)@+1S(cP1KQUcE!=xP zTsl;M@h~H01i~$*sHe9^{AFv1S>~y|KR!JDPSnD5gpEhPpM_9&qkf)^DNy%Urav5n zW&h#oT}P^=#*;gd63_!_h5NmD1agmpuP<6+aNZk_GtK1m!r=C4ZaINKJx_gTK9RMo zS=VPVQ329L1NodJZP{sHDLn#HwohE%v%L?Oo3ghqQEXyrvJYirT^Mc{_i4HA1Di9VFp40 z{b$Y;*qFaPs`dZsQPs0$!f`fHysm0L?IGBXsy$zUQA3kUsQ7n}>YL-#;Zs&t7syXT zcQ}Z07ng&bh9Kp@9l6m->vay(WwpwAp%Kjx@dt_ZSZGy2EZUP2GL;5ly{CgFCG0ke z2;`yKQTnzli?Y?(C2eCRrdXB=L+bL-27R^W2)YKqu1x{DFoWlJIu~OX`C$FnRt|LS zo`(AOyH43*g{tKl_<}^nYpknK@c{K`$opP$L32wNlEw(;&|v>1zvc-|-%>Kui{Px# z?J}bJW}`iV;m`Pfyp(18(Qm|)N{r*h)9n(K&YaE-6@7P;3yXyl5uinhRi|aR3`*9z zJqS>bD#m?YzdP$V;!H zCZpAxNHXEA4ZM8mA+u6^*5FI}v5mcTUZOru0 zyWyQut7h}^lH*k_{LdkS}Y!7~Jrlgp7OJbe|u;eZ<(btDkz!EK$WbxTDN z#9xEZa`gBwL7QP=Husy>(b0&o$X zCho~JfWPA+`>u~imn8b}-&#~*>h#>RfOESAT&;iqpXXN0*hbFA+}hzk42+04D_P*{ z*F&G~@%TIl8rj<<%EH$g@poUo91wjWE|ka<%@3&>Zqy!)-HX(^3MTl0`Xw&}C|?+- zci=a~qYWE7o0^(>)p0RB9bHXrfgDy_=z%0gwtsDJ-_UXwLw%xFvFBsax+-7Bx6m!{s*wBgtmsu&hBn|!7Lj|vuo)#h zz92VgBQLKJg#7>%l3T+09X-GeM)rYW`LopY(HmMFRuTAHG2UOXXKM1@T~!nd1xlPr zhQs_h{Gp8db=Dq-Q-{%Lgt|4OZ=>~3XDwan==TP)Gg?H!EXEqonQYDM=gcux9V2CJ z<1yGjrOWYvjg`ov$G`N(JP)`a7K+6G0E7$@IWwKO=p$m0=X?(snVL>#7^e+mB%*B94 zxI)8Y@Ca~oe4|AuPAs7dN+`-GisR2HNzqXyoWdqlp+rQIH!3NyvpnzrT=`?LRqPu8 z2fZ3N=w$!jgZ{50IYCKV5tRWL8VZ2W07vFSLVl?iA^Lsn*Zw=4-qFI?e`lb`fh}O0 z^#tg+yi@Bw{5_I?wXaZ7=83;myW87La&SDEn0mjzKjL+B>YzvW-GTi0k_cuS_#r;J z*l3=?G@dxRAbu-Ktl;?*n{h{ya*T`z7~97x*!bGlKcvL95RdLzT3$Nz>xbo@zoMvS zV9FVqLXp9TD5n5Q0Le*dzpXhbmYd`OUf@mR`3|2jW+}z4Qu>V|av3G{Z8WPE@o?+` z%btZfZXA78VBrW(FK0?Ri=h{B?^NRSu0ljto(CNRO-i%9!F{ZWBzY*HwrcN(Y@F%_ zZhBKCCt#V6YdFbiG$>k46wG{L-#H-exFrS;*Bq*y9;Tp0h5W$>vZ~a}K$2gZoDf(5f$9NzuUA4G^kC zkuO89Fs{XewIq8{Ka;VpM0uIC)V$i7eOj^5#n@OVBoQ9Ee4J}lt;hMI6TL1lsC^a5 zb+lz<@=+L~l3L-C7p!y7Ahq$^_}h))Va>GIyU-uuz)QE;fhHuq4E=&Nj;}5fdsm&t zfd!S3r)b`GP|{RE_|py3x+Qc|@7t8H9-=h_*{dL5p_Q{&=?91hh*sN-sqk|034X$L zN=>hQo7l}bqcrrEzjPZN#>nu9%6>9;PRI^7sD4Hd-+`sEcbRV7M<3c5!2(BQh#C0N zH$Ryd&XAJ9CtEgjFC{TvG!R0lLooq~!S)PQS3R@^ypMl6a=-o(0BPyXIRA=)SkEHvGq2}> zN{MXNohai(G16`Fw;{L2LQ_L?%0f2mzFzrf7={w4c!QaxRYeD)`_^OURo~x!T%J1U zAD_!9Y*&9GuROc%V-6 zBRTJ7f*_1Ob^JQa`*mMKC`OQ!6=YEj{zqx*)?WZU02$S*l44u-d6JBAQrn;$HPhgK|00+z;o*RbE~fnxE(9P@DSUOVeg`)ENy4MP0P|W>f$=$LfoS=|E)XB*T7v3YNZl#TZ~U zt^@@NTDoTN%k4~z;Ccddg{~~rd`p`2nkX>}ivp{1nR|qQV%?mCua4}lC?NOlNL-xmrnslVEaZ`exuLy%!x27JMWLu&@J4^59>SM|)-X_}y1|vQ zwE-EX5|DnQN6u_dKWj+=Z(q%0V`BJ8%VWazyVRNdcu{7kg>8l^8~bKHprwT0`rCR+ z^|-*5VJJ!BTjeBtzHY1CJkEn=!U=bfxoL`nKeZa@BLTUXAMNxC=E zl)#$I&y=J%kCzdX4tPh2AF}%ay@3Kv5+i%kkdgy_Pz-p^?1mU{8JlcKFivdbXccIp z5<+LmVE!tqjk9lIKISr3u~$@lbX%?XgPettg9q`3ljA^y^*99Hk|e;v6uu#GR!iH> zo2BHZ(TTnK503U>80c`Lgwdj6HX(UE@gMLlEQ7vh27gXe3md>!QguQrF3RD?@MAgl z>t3DZ`6|yy@D`yu=nTwc6`gA-9*QD_wXn@NGVa|}6@Kl_s}&|8nH7zxT2F4xQjI{F z;LOSd%3B<8hNS|Y%SRmZGva)c0tj13B2S>Ln)xbeCa1~wRz5cPsa6G8m2?J#A)8iL!}^afh+AUFW!P{JQ_bNl6;)?A zHG;jZkU{VG=I~?YP+-F%QeB17`kH@Hky_~|e~>Rh*)z1)slzQ?Y1YmWKqnMd`MkUC z)-E9Q$8^Ci=5v9sZpkx?mynlzO^d&Ae|On4jaSj`(=(WC!Y-)FjhI|-Y!u(l6hJ3{ zI=MxtaU!H^yK0~O6D`*LEsfuBa(uwu@HKc{)ufT0=J;2K@yhKh<7~y6kP5f5W{G#se_B zGoiE?MN3U-u%>Jm8(EKzw(1esZwu_W9S?%qg$L+N0c&m>K<{J?eP~?Y1sqogPbY?d zl~U&>^VCTGXc49}Ut!o$kM(}0OZi^Y#Q5W_$#$Q$nO29Jg=w(WDx(<-9WX0o^}U2C zh0W4r9eXo2zaoIliT%>czO|0t-LQDqI9F8%3dROI-GXkIM2uX|$kiHvG}UHA$!+I$ z-_bZX`eUx{do4_OB={H+xLppKvLsF0SP8czEH9uq3WLZdrLNhS?0(IO59!6Oz zWt)9{l#kn>bUZA%T*{|&4&P#Oq0UHg>d2DNYB2q9rKbz?ME?j$>1+A^fJQiunRSYi z68Tl_ZSyZyIqK?zaJw%tpO@o^#dVuH z)2?+(TwU#>!~N9CsoIw!9V9sgl&fmdKPQdcSJD|2dr_2Envx`sM*F@m?%IjWm*nb5 zsot=*YY$YoP)m%y(2w&hTJxYdT7#YMtp3I@T)B2qW=elk=!ZMhny+;#a=lY(lTwuB zOpE@6%ADK^FrDIor`3-VKQ0-lGzh6=i^=8o`Ro!|I@oWm*i+@I7tT%U$k#?@S`i*w zjp}Y6Ij9CfuJ`o>SH2#b!$UVaVllz@P@8xOQ&96u8tFk$vQ5t&*Ql)8qXU_NIvS(1 z!J7Br7(#Deu8|WdkTJFDBeiVjH(H{l(byX~%}JSTx{m{!%@CYoN5Y|yUykpk4n@N* z=`F($p*>o9_!V#!In59p{fVq}{~~icvu$||WL~fAl<)##yO4%8ixKd|s&h=>PUWRI zc7K|N3FY-3S#W9uVwv@5`PZOWu@hj%)Uqs(Dla&OS-FsHJskHrS^N-3t{iR{ofb;| z#co+Ynxd6eq;*0yRADzWxn}!DyJ0<0m`8O72qjFMlZ<(BK7(;H^L_Qo*O$wjm{tmTpxE03 zJrA4C@5g^niv1R;ui^X*UI=?Wq=1kLE~76hyLKCmrsD<5L`@&4PGOsfxQ5XAEVZUj z@r5k4p!l#Z%4chG24S4kXCl8Q;b;0-g#)r|d|IrT&^!mGbg)p+``z^3PUZ+WRyUy? zn;}GCIyM9QR)14lBXye-)sd6E6^zHiRGx%@U{7py)5FeBi4+-rmno+_DwF9P2M#nZ zIHpHKeHXYooWpSaOE2b({&wCMPq@20$eNrtLQcvCJwB7He$Zca)m$Dhu!2^IhREW5 zyN!KXI<*A$b@Lg$C@t-1`wr?5l(xxUI1B`^&}(F;f>H;HN9Tk`@Re{gnF0mcYLhe- zg1*WOhyGp zZh9FZ#)RI7V=JBhBNB01360&Pl`s_REG})2Nae$}N3)5)=;B@sL&F&=cNq`O zSqEu;vB@2*tR7k_?lAA;U?>r6HGp)*B^1n-5V))Yck@NV-4-;M?MJZV*|%CvNnG|L z%%-=NJ)%YB;kLY@^`Yw#ki{UZwUwNM6>HD@;c4;4l;J)$zoJcZ@?NZs!}vVhjmelO zYZuvvbq80xTyospEOovfeosK0Vym@xdAMAO~>VGUzGLke^ag>3OC@QFVABaz**+S!@g0!GIxmp3+KQxxcMvbiB=@ZFKGL~48IR#irZju6H}A)m^PMa2k7wBK zv}f{2Kef;uNnGUb7Xx+8*c3t)P>Cx|y(S6LkrOPeODpSo_Hh$5tXB`ao*vy5EUW~z zGb{Uhl-a|Bdbe(H1>aZ==+W<&?rY^WwRG9eUq)5bd#G1(b1K%{Hdc#c6`36C3bF1r ztxwoI-Ts_}=^phB(zSCfu?EYh0G5=JG> z5Jw|VVKKBdkBdt)(8-g?eTG$1Io8COGFN&2B1DNIRUxMa=9OEyP9=u!DqJRV4 zt+cIAxc7D^Zl19gc+p7RzMqpnF0{}-p4LRtMC@gu1T5D|N-HpFZ%^&uwN8XLVW5-x zP2eop@i>GMV#D=PQdN!-JdtjAMYLymw|~K4S*eGY!|;(?>2xQH~IG4kg^?Akhx3_pf>dA#y51&MSValdkF?7Od6d8(75aY zrzrv-8OqtHye>)-h=-oFt%#mgb7aq~=DogxZJTsw@+4$hHEo(AZt95 z>Di&6knJ&-fUFT>s@G6rl^PpKS*Trp++XbRUfLqETJa-yao`)-so(J;xcB-#osh$f1?GF0169iI;#hV=w!c*K6S znR_c!@h%JEy}BnZ*yU$*K~rE${8jDUhFmu7g45;+s+N$w;j`Fbz!WThnnhY9#M!%)Jq7jh&B2u-dC>6c(m8hcC~4<5-w` zJG-5NZ&KpzBRJ3kjgWLPPhtZcT)ec>1l=5LC@#zfwxZbOrk7S{XF8kAc z_R~+;n>?G(4@>s5JlJ99=Jjdp{!{$bJN9^LJ9nCoqLQvKvw%Vj{}~=Kbz9%7Ug&D> z4s$DleOqv=KsTN{31tp^qE${B{L4ld;>|d1sTjMvXY!>J+{4dF&o4;>_BXo!xjOM) zfo^>OX2dUnh$zMX%_;ovhc0nHY)~11JM2ln3zBGNZXwqOiPd^;xZ)-q6yj)KLF$-d zOqt2bInJNw3#&LYHUsDg9w5AE2CVsnq@|=NknY@hpC2aNbszua!XaI1BjFl(lz8U3 z=dn~ksqz{_Cb3QI-URb>5V@e!$y4K?!(;xH1{Y+em0e(69q4t&T=`{%DjsM6OoPXVLo!~%j*iG(V(a=-5UZ5% z!5*%soMtBi_ zCa9RvddIkF+JBlNu{fNTtNm$E5TqR_MWILOP@ibWg|;Z!+o6S^dIoO(8L4rlGnbyX zEX?S{eB?0~W3Y4THbO|IVQ@e}ELGge)B9Ky?sxF5!GU7>^8LkOP~Jt2A7Axb<6$Y5 z`i3Z1PW*hWW6FPHk4wF9;tSZRQpi!G-%3yzon7 z0^L}PY6}oh=7+YgfF%`ID4KdAklDjD=91`P3TqsBPYavt+yZwLNI8Bd$eo2r*-kHR z-^>gIL{(BnVCWK0$a*`J+{VC1Zy76LXSormwFvUb*0XI5;gPH2A2iXdD~Jx2dpldh ztCUDx@oz4cb8(+GTW)Nk9|0IegOBl8h>Omom6U37|XC zP<#E$yfRKdiFt$Qa_3@p4SDy>wdg*VXp@X<4KLTKU5wHly^~0I$*u~{TFFH>*zNg( z(ItraD!$`!L}s%92#BH)=#`#x0s&ECiH-2{&6Sl&hn2~hd@O>+r~U{RZ^5l(sY0oV z@S@nSiuu%u*pYA+KtL1`k%Abqep!GdA>0hBaJT^oh!%<7*K>TeA`7bTFa8%Gica3e zfV>0*M4PJdBKgHUx-%oc1f?wQO3Ve{=Z|O32!n>-*#iMloRI6?vx2 zV21&C)ZBxa=Z}1o$igqbGECjHmww}8Nm=HNzS^Hn3e9?R$#@Y=@kRE^d9<&hj^m%*G)C2!)+ zty`p!8qf^eJ4#}y$QFe~+cKzD$Q)jmMcZ}y@|jY>hO1#H<0V^am{v#&0$dl$O@gK* zE$wS!bt+5rv38D9L5{p%>PK=OgnQE~`s$6Et+bipX=gDGy$|lmp(yD+$vPUyp9tmvLnq}Nj5~#CVfyiF5hzq}LM}#QxORANsv<&vwQ`-)< zgv8{M3@|_2rVuLUVbI!?+P7mtiKL7?3-aU*b&EfwyJ+@`NU#o4`Drk4PR)#y^|%*K zlhC;HL;b?M&0!*RP6x3k@d@Mc!q%=DY@(=zIfgmvEf`-Zheq{*dT2fQFf|fc&A;@} zDTe|~n;tAXV;@YpUqAn)hnjKnvYY(XLxU(RE3>40)2)JdX)+e|Mp9_|aRT|1GjR;F`5y?5r`qr&B*Q9cC}x&N(UG@o|9w;r3{td-(l|OY0*V13s3Z~ApQ!X z8UF~PFhD_6d9^Z*M)My*l(>|cyA`lrfMKhn%sz?xZ$T8$h!)AD=lmXVI0^%P* z)Yi*1?Yc6=eVf>eqX!1q!fe&-&98adIhxd%EeG#@nmN*x@ag%FAR4us&`j9dx#S5{ zB;xeuLjx>#74W2|Xc$$4(LnV8Q)53}DNNCt%-Z50<4V%vz7X&FZrv)>KbZBs_Tv!3 zN*6rMfU%bgp2?94{%Br|H37`V$@MfBe^uU`5(0F@Z8K=M1>Ucw8&*b)bSS4SipfzE z3FU;Bh`h*Y1`XrPNGoACh_7l?m{pbGNs#T$1U@}ULZVRvxL2J3fteLe!O{<>&$$>j3cJ5NP884Y~ z8#gT8WQ|bpHM~@zvmwp8@)@J zMX3@HB;w)^br=S`%x8hG!Tm-6cw~(}-MuM!{oaZKt;}|glQew>d+{_?z$&IRBxFn8&D{j`cyew{&A9mr%X4fKWW5xq z-qnpBsFvK4JUH0VBy<)srv;hgEds%l(eB=Ec@x*G);xF(Z_ayAYrWaroYWUmn5Ag2 z;0gJdcdBy9(`zdAGY0o|1m^nW%0U zvg+#`_}tO<%i2|D5VEECXqJm%Fo=b_`7FbK$cXNQ;m$ETd}8F8G@G?{QSKB`nezu( z9|tFhrsk}A`PmM0fN<#1^PTIayZgTL5nZruKdBYN#h50lo0s-Yhwugl4zBdzcg)P< zssRVTl6wrAK$`hLx>3NTrfPCSIgLk*`IRO=tz!V)p|l<5=|HEN_IKY!$0;MPXv)N) zCYM;rFZ8g)cIDvymJYveUpR-g7&!w@Bum?n1^o92@C_#YRP#u{gydeS0ev7np0 zQcIZ6qN$^!uyi-G`lqzpEX)b$)b{o#BHnx>FA_%XI)t>1R08T=F$wlwCMz-C=9pXa za9Q3~M@?yy?O_^liW(LDuXs=|DmeUW>;Y%WJ9_}W(c(X}(>Q=XkspXYS?~=mw{oZ1 z_$RfSXp~3w0`c9zNwx0WITzolB2;QsDDlf9k=n8otIF%H1aI1@1$!=FEt-1Bkd+I4 zY+^O1bG2J}9jP0}2%)4Ma0-y^4S-!mobwI4n<3VamIlfZfD$ze7eH=@%iH&CkPQ&G zDgm}x-jnk@;(Pe(cD%c*Ze6UuYUsc%z-Pa0nvNK#hQj1{S<*ViaB!nC+WTd4C`}*3 zT48spBME&SV2G$)b>`HcrZ1Ead6S9y=73r#pYkS?_BF!bRSy3o4W2Xnq=tJ=46YSo zuBp|@BMouw;%f)aPv_?4%X|H{vFxYc7uJ|-@&t|UaBg(VmlKASZ*Z+Z9{dZPJkl0o zVAaUV4%f78Vsp;31mF}tT}%JrqM>`}@FqLni zJW+CJC3Sj!GIa9DG2X2y^%mvZN18Bfs@0{mPR`+`Um0&i93B4(y#D2I$jBpd=a!m^ zPdd(bn3^K)kZ{@p+3dlAse!1J6Iof? zp^9L4Y%NqwMj*(0!J6X{(NC$QSgy|r?zV+FlPLbKf%O5sm!N3ZB{&8OYtaLQBS!6-^CDwT zt#nI%+OoO?ny@Z1YYYN-Y3w7U8>z)Hpw!yha8)R><&dUWf{$B*`(1+z>3s#KmsUfW z#LrHc(|=cF>NVP-E6Pi|9>R5*Wb9?45*sCL6r4~R5 zc}(1`V_YrUoX(1){lN z)+^H|frDZt`Oiiq#MHJZDF*1{^{35n+h}EN5C*6p_(jxF|D}f30o720h~0mwp;nd-ac076e6(?gPOH zjsxHiR)P`i^TXhd9dfFi^9g$n4}10c>8vmf138USa%|8I4_K+>^NaTLJzD@Dl7Q%$g-Q&N`NM3ptvdawX~* zhrfrl9{fWb^y*@@-F?dPLTRM)5??fmas)$R8}1j~1L9?R};E)w0iPLb+S z95IEdB16VIQh5?>>Cp!H|SWIiOTiVArmY+kD#}*Nb{kdUV||TiEA8*E$4* z)E)d5AVzLL{y~j}ZI1616li~@T)^%Reei3v^B;a0eg_s5JN$kJ-(kZNFT)%r)+OW< zT7LyGe*jfSAar#7-EQ<(4V9sL12Mk?HNXF>hQjg@TK%Jjk|9Q`mj7D~MF*;(xBsDr zDjSXcOAVa@J5KsvYH04i)X-DdhFRE$({?#;h`(xR#r;2O=q*qLMVVicyOrku!}4!6 z^kN+Lfi3&b;y-FASn=8&zo#eg8ar9wHCLzqsG+eGk#m35P(>PcU_P#KNw;<8}`V+<(u zD2PTk8x2^rIRcT`3ngB#zifo{{$DlJ(iZw(Yy?f@3=$8#K=!_B4*x1m3->^7aT9Cw}pb?kQG184-oZAy@V5W(M_TQ-d#Wwn zrD6?TsQL1o{}&H_d;+B#B94!0xvQiFG2tUIl4mtECOiIt9!itFJpgac?g!9 zUDV5&HEunp$O+>obqS>K$d4B(L(PSZ zLSV{cq+#66GuX9|;U2T|6N0nbQw2ygC)RO=$0^3tlos+rL z-QqZK>I(d5%9v$JyKkqu`Y>6>Vl>w>AWn)bRUZp;2yCn6Xjys{=sPi3XKrdx4K(jC ztdd2fOy+e{Iz@m$9(r8VQV?%){-nw4FF>v~SU;55bdQJO6s!7$qFYJXGErhY+wlf> z!5last!x`4LR$gYIYE(*%t+FkzzHxIr23{buW@PYd&{$-63`?$40S8viC`F?t)+sl zt70oawwan|B@r=V1z*L4J1hPhMBW0_Uw*V{AAt*h5^<hlp0nUNqcW#t8GJf$z+f zfKx0tOHmdZD`S)8$>@H$;ma+7k9z%m&I{azraKGEQQi{iKO#(V7z|;Sd+HZ6agK~k z+p)8)3`2+|A=Fd*f)GE-FZ^+L7bOY?tlpq2*7S_*S?gG;twlwr9*Z7c3U=5&ze5}~ zDes@UAgXp58dG?;kt4MW3$#~Q1j`WqWD{zdqCHd`94IupAHNx}tFdL&lr206Pau=< zoZDD4yQn`et3Wz}Ejaw39Vk?t02)i&ZQnSv`2BTWzg+r+#R55QIy^&90ozG~XpcurSJQ*%crY zb#p2*G>L*0W@o}axY7@RTYf2*4?a;D->=Wrq!Qwfe2UjY%|&quV=3>EMBkdemi7os ziK)E4Xu9zaytDHOjChXZW;Is}3uE!X+)}*pgy-uag7+G>8-6178VS{0^*RXjD$rX^PHt#Z6rF7AF;@o70I04t zlEi6N4%r5tJLpS-d2NIj6&6271a9p*V|zvM(SIMit-|sq?aA`ro)WyqBp(`OeAihu zYX;W_e3b6kJhugUZ)cT5vLIq(JMHM=%E%T@nm6NrE?yI8mF-}=zGJFVs{zR>lZj`9 zKk0Y_TQQ(BS~~`;eqTGbIot%+8llt7PUe}Am6A+Q#lk)!v}(>qpbo`upu>P2G~sb2 z?Kxb7ZX1$oD-o`vsj36Lc77Z7@Lzh0=ewy&Wyy1Hk;62{Mt6q`LXv&5G{=U26jCR^ z7c4DbG-o76F#l|!J3uv|=k)(7Voj;Kf7c60w!}5gwbrNl$(oT9n#g;aU4eRT6T0i| z;^xbt7w2)tLt+;bJFZmSaY0IcRXR~RZwV$sebDG9X*>$iadS~#FPlKwzboCPAwupaBV+ld*2kDGA*?*sLVGor$CQUD&ROw2 zbbZ2@rOk(E`VJR$W<8uH<-Cq<16OajZ0t@ zumbUz$^`CAP0{IGTA8viXRXm0gTu;FJJEPwo9Tf1_(p-t(^ZG0+5dpZ5be-CTp4Cl zIGje(0B+qLUGP!nW*dF1xZ#%2GtP1+GUn)#IUY@WU#3Zfo|V>6 zCRSyF3Am52Oa{Uv3|n=G;1Z*qW3gDB&J99}_p5M%Komqi|5lnDLyvlYjou*nEoX4} zpzK6^VMzq+JuxIJb8H_cs5peyV_N5qT;NG_zarId_qC%5JWM;QoIIV{rPS5p#-Q9; z;6Wd(1wtW~;+G0R7eO2XxFF8@!~)s)18-gOW`D*uF&b902iO#O8iHXAlIP zNVe4n_Fh5!F0WV>VR)Ca5WpUa0kJVeZ#AYbbtQp200-e-6CI`kF~m^UWGJr{jEOfu z<@l|qJdd5lf8s6EGk5Pd&Tn56BJ7v--mtCF1=e)I4F(IFp)oszMke7ZqxlosRd&}& z0czPsAzpJg&-EgD{z_7KP1OH~v2*^;)>rXdgR8?np zW7ZSQ8U0xsDctd05Ajey(8aI%5F*fP$RHP`^Nt&H@W^_YQo)RBCIn8az%+ltaWJl#PU&U#F_ zb;`4(Ha%GvCii@48{~K8Ph6BcO%%Af_#|#A3ikj7VD9SO*z`%K;{gdfmClx)Wf;=5 zcJT-t#MEb^?QYdC-o_kbW_g8;19rWbl?U0@LtZTZSu}iNd#QYp!cMxOn?u-=`ZAWz zkzn5l#OMG?gHhMR%;DnIehP77XN{84Khd+D{p4)r;9XsCnja<(RBeGWqi8Qhg1tgR zwa~Yg>7LBcymx6>1{cMIN&7?lDD--0^pq)uF?r8RXS*Pci;YWDby;j2C((~jswd5LgfC9E?0OqT%Yc$D0>tc!WV8qgGd zygTAtK5g-Pm$Y`D-2Q54Yl9)WL{b+5gVT+W#N}9-e^WI`?P6a0L3WJF$w3!l>7;sqj92i zUrzRgk?aH(g!#2mSfO5``f`}$gS>l-tg~i==dp?gmkUNQ?z~}tHlvVwHmdVg7Duq_ z1kGeBcz|kQ;go{WnEvh&Ei{{7=ueG}wUsU(vIWQOY3c4=TRTUM;d(lZGJv&`Xl9sL zKb;iyZ{~O)RSnbwGU2~3T^2~F{gd(4!P)vsOR@al2XYw^fQW**nYFQ+x0Ct5gr(9_ zKmMa}HsITeF93KyD5@;1H1dU*hoXf|MhBHiiH;IR`yFPtT}IDSf8s;&YG_Sr#QuN; zOkkZS%mAGrJB{$4oA=#5PlDbnWgBs3IqWS<`=2$qBtJ{4@C?72}@Nu zjXO)-$ohl)gll*!eJ!S7WcJCm;fuJl=YnygEQhh9ckihKS9_?X9Ai#

piiJd4|c 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 EabYf1HvS``H0Lws%Zae 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%0Akol)Te0k27e6yi+v9J=Y@% zWh)}DM~S+Yw7)~Rvnqu1?5}Ee{7o_xqeSO9N^T@4*fp8Flt&J7u6So>hy4^Ctn+#dN)H~Evi__OPuC;pPJgi`kkqW20rn}T1S$d*ptA6+aT+wq_Fy{Wr7H*g1v+jG#8t0&Vx ztQq?oc^E^o{`NJ{28H=M0%u+8HZ(n|34uZ+7!Z#cNQU4^X^_ZOoiofbOdiTXOfQ!- zI~x*4M$UGhkeEo(D2Gb)GBau6?#4!2tMj(7={>`Lyz!OmJ>7OZ-C)mu>q^od(t}b& z!Jwu|yJ(ucEUp~9>XE0|3L^a z3=hM(jC6~m)g!jMjANundvQMgtQ@*B!ILJ{Ae8B5$szssJJlxT>>1Db7emP}v>%_P zRe85s2ryGDWOZDLwnY(q`JI(>Zldc{&a^gJC6{xL@b(D5cME$4Y>{_KMz~K*KZaQF zUZal-eA|9=>>ISfA25VaN{IT5!7Va>5!S5p{?qpRz zA9G^q47V1lc~5Eq>ZxclcTN-} z8rGTlngDlfNH=lz^#M$!scTJ%H5bZ#2p;v}9*KgXAaLD;}3nwb} z{L%qOdb<$UcP+en=_=aTYKxzba*9hjt5-abN#%_Qe&81aUh^g}v8xzS=Rn$r!gJLz z0gko|LxCByiNU)O8L^a*e{_@aKOi5oEpssD=Cu}Cuy{k9Cm10^2MMJ zqwAZa(*f)Et78Rf9>WFQs{SMb7M>AtkOcxB%MKe%RdegY zP1lo9rYjD#Fzt|5gm@lQTZAoXIvO`5TGZC)6P?@@!rX;UTE`B2sg5TXb##B|H?b6k zr{pAgTpzwoW?p4x-F}+fMQ`!&w}*Q>c&VWB5?b^n6mRy{Tr&79O&f$EorT(2oi4 zNy{RIe%9=KTGMNuBF8 zoz--S-4YzAamS$0?S74h{2UKsGag0Y64j>+LPq{YZcx4Ex4a%VDw!d&ClaXoB{VwX z;#)+@PExsVQHmN8%ZtEDPfJwJOwUM0ThN@&^0kL?O&CL2)!E<%aHy>tI;d#?VL6tz zsLGl))iT(1RHNaq`k5geO>7Hx8?35^lE9$Th>Em;5B-h%!8giD>^uE~`-!_zyJpGlHA-JdAgY^FCpl(YxAGh6{uy!Rh> zE1WVGx|87%3;KP_MShKw8)9Org{3KHs7wk{nV8ETPdoma!k#LdnEC=?OK86+5_n@Q_fgQJO3^JyM^ut87AJ7CvVWi zU!$=;r4;SrM{bR@N~pHRo2WUJ4OOhROtWRrT3w<1b{J~9o~;ShRjXg+^KLmdx($9f zv;5>L#k+HAyEQ`rf9v}lkIFFejrQ-sAft}s*HAOY$6hd=R-zU-8c@q0s)0HXZ_^= z(EU(o8m+{-Cr&KIV?xfzt2q9<$x4}}PnPw~MR|MYlE z);_d~+p5}itTzgP{Ccm3vCL&^&ED)(Td>ynd{Z zrt^}bNYg&2^bYy;mvmggF0(+zyu=A{vb0DEbX(R7Mo7_HJ<8urp5KHrQ_G}EzJmx` zXSB-HDgwRHF6T#zG-j$Pcf6G^x@s`nC3lxT;p5jwyt^XY%qmmvfxC8w9&tK~K7>6Qhi@;wWyqy((kiEHJ%kTa;Hp+M z{(=`B6}@Mo#n(5GYF_f0$8$H~2M+o>kg8q@go@7tG&#k2W%npQ%5FqhJhX~6o=IW+ zlP$+QYf+$XMpR$H35@|>pCJqvgc_&pe(9EaC{;h%VhWkE zVy6_jWc`+@-?2A{7i#Ec-n<;C5~(Wr4I><=m*AkqOVC{*?*vvi7%$LO$V<9Bh99qm zW*GK(lD?h4!aG;mFZLu_s%`FHmiTH14D{GC@YmlZoXUCl@~;sN-H_%-T6&R~-lBS& zZ8M(n>&`|Cd(=_9Bf%102p#9-wL4lX!Sl0TOS^OG*^Llw`m8K7i@o_L@X^tkYb>XH zm?e3c`NZg5zZvZiJagkvA=dN4yE#Ym4rdNMov%`6cJeTLBBb?Gb2 zhAY3RKnmJ~Iq)rG57b5D4==4a1?uk2X#?axu+3pU3K0~_QiJ>O3my`BI|E&d2*xGV zTTffyG&<_NmC>iKAo}*0P)4_&xyG_9=r<-Ey+gFl4Zq1QJ1d;JkG?I!u_3pDGIe=i zl4Ik!z;`MgA50&X7eg+>LbqexZKKxNP?;)w9m!Jwob5iFokJ+0(u>FDF z?u4y;9t`pK&KL)S&AgtI)!OS=gCze?sH;u4P=($R*mXn#UzNXeC;$mLFvfgonpAf% z^f`gfZ49M=8qltl{0|pF9?0*mdr-a=rECovxy?!0@bB&DJlfKF-|S;_9=3(-5{GDWcNaeP+_`2w!|w% zaIAqB-ORmGu-))IeGUHh*}$WG@D_OUA03;Px4KQk*^WN9(x^)Y!sZTmFT~ZWG2JgKk3nsH`PGK9@w&!My`KeLZjkL=VE58`5S7s(| zNZSh?J>4%yfJ-U{q{a5_KgeIvKgb_>N}*iRH}XfBbVM?1h5Qfl*F;>wgRG|OviJ}3 z$B`-W5Ax?Df9B{g6WZXDDFC|B)R_5ykw3_u$BkoFXV83KU}tnRD*vP-s(&GWQwN#* z|3dy4`-AEa6!#Yzcd+Pf@k-Y-$@d%Yv8K2sCcI3)qjT=~rM8vZlRaQJdPuq@%j8SWp*d`+NSLw*+gUA{>5I{03pj`{QM*tV{|NSg zvbHR+yw`|wScr3IM$yd*xi6@P_+s|^ac;tLb|B(*|1j=sQkB#H#b{jGQ646*8HVCt zKSkB0_X?w)%al9yMTz|hy`Q4u*dt5}5@pr*Ug|RM*xG?dy7Z@8d2Pq$hHc8O%Ca$9 z%}4qOC(A}7o(`$b&7HYQUa){D*BWW=FY-@VBso_o7$JRY+G(QdQWXb~=I`v>u$rtG z`(Xy_dxdM5zqr0L$({OSlxPvpGa35>L_iVuymoZnQi2|IqC-&&+7D}zE*L)mNtfxf zWz4-gY!iHsa82*G@^`BptB-#Ay``Ib;V-CmY+_nimoNb;)m))R%DQ>t()EhILMg(kZWiZ+Hi+(DrebLQE@y~Afb#j4{}L=L zhsu_4bQ!$+%BFCdMq`;O>cl5Z^s&?cg=_V^(W=%(XoRJEy+0>^>c%*e3`#)ri3Tc! z?hvAL{cO8Qi`qqraul|9guv$t{QmS^{bYi3V9g0M7l?J{mx-H2-Qi2&-W}rlU}Z}6 zI4qyk$^&WfD8_Flm;U(*!CyApK9#sNEgWjP@T(X}q-1Z|xsgE6T`LXi8oRVc6G5G`!s_=#`@&4ByHls9mbe8t*LAvHjg~oljmnWPMLjo1#nus_6kclX!8t zVX<%ZEJfJHIl|W5vJ$Bl#b`)teRy&6;9zn{PJVi03v@6gW;8vg-)f=kvSW(o6P~wTt=?3vc>Xo=iKi_^K?xf?Mq)YloiT<3z$biE^pS8mRIzl4DvFuQv)bv_Ay1hQH{7bP{2-l?yyQK&Y4I92e0K2YH$ zlAP)X1iiAwhs}Ey`#C&~gtY!0FU{S#(2?bZ)!oDDMHhj8MyXC2@L_H53q1bvGp^Ti z0P{^^0QUu61GMvAS227{X*sv{AsGExZNWKb>Jc}G#_))Y;bb!SNk=!3hBxr18vv;1 z7}25WKbb#*e=&cnpz)v!AkV)30fvZz3?kslxsW@55qkZ5KX$A8kb3n3(7V}J5qG)- z1eZMm7lB46zgKYo#r*AoJ0&p26TX02-hyZW5yAci@|**{GY;7{A*$9RlJN zqLtMm{i!kg8S}ZRRKEaUMxwpE8o&Xh=uIHl(>r8=sq;u9+=kEZo$@7kt)^d=qF0u7 z9QrCV`?)}9Rg8I-641_$xVSwF_LBH7D={WM0SQm+XC3cLQhX5O! zd9GwBU*y%4>doC=&gv0dWsrI06K^F%>elrftU(56{N$x6Pfk7G;%a~^MvRKdJcxT+ zH&CzFZ>|U?3ban%fG4nM zhz>^uptpms@WW||gR?@R8UYIZrVha zWVRhepzvVLAWQHk@ZTU3Xg(C8Zv?4;AC)`K#87-RV}!6furS?NgZs~k2R_1mX@?UU zXg=%_WvUPC#VaVzXl|*LnOVHxU4s_O)xEJ1zOmQ%8G?IRTY~*mL$$?uFqO5%Sy1vQF#$>x0p>b z^Bc#g01gT0GZw&oj-^d64{zyltW#t24X^2X} zBkZi@#S*_h)Q2VGT#hbv_*%6I>urM1+{sTv39dO7EUz-K10N3CPi|Keabi3kFSz^9 zjxJPPu`LJIM?G4a;h~;8rdk>JS?0a;8Q9Cr8J4gJ0~IIn<`Y2y!|~Tw-|J;q_dodZ zq_S9#%5qPa>F6zE?~8^_4rnD@D#J6>w%66OM^jSU=mqE;HYSw>3_ySg7Rf}0T&7Rf zbmqs;R2NmDSYn(4?dbcoNy>{3qh9IFm}Ba-(0w#^CXKu&rbk>&Q~kYsqU;jF=&Trw z;o^8%BGhEgb;fL+TSkv}>SQKN*ht#AX7=QqCSq)6$z?2!Lmj+>)}<`qNnzH;%rydKERy*60MRlLE3Am4h8^)%<*`c+`Sa7G;l01W(D( z3g0g?ByM<`50?|!+2ZQDs;d21)3E1fI87!jGZ#8nq{V|?g&C&SnyIzFABnaKaV1lg z^8)hHzb>G~I0gjh$?bO`Je$xpOZSXyOqrAa>~V{7zfaWw#j1M&q+fD_t}CB`{mP73 z+rg(#<*n9i8d7Gf%xq1_3gawNb8X#P8>R-ZS(mEVTZW-+hOs8K>W`bJBy}r|qR{3w zN#JN0{tspE6eQXfZP})cowjXTJ8j!LZQHhO+qP}n)=t}*mFLv0in_Pnt%&!xR>b^V zF(cL(t@kF-;*VG63dKjt91!&N0m7$jhdQlttMDPk>s;Cjz(eUBtxN8zF+_JuMaV$) z0@RF58WXO2z((mEv2zwT=DhBH$h>-H2xTM^x`QG znK6Ow7i^dYr8TGV^+)Hfd=%V@ zJq5SEgTaC@Lvk&pi_y|}dLj5!*x}s2kTGB-5j^!$II4HrfJsLumt<<&@(`oDzOc=W z_(_OCazCoUo1=ZqlTz7^XF4Rz%V?B=gzx3a*kp~I9?%^6JSD0)GA$63HVv&+z5FhC zc&)5@J{vskO@q4{LK!7OC?LFL&lrVmZ)(tC8JMUmgfO4MgLH2z7uG_7#AGRb@gjo| zGp&?i0ei|WTGhi;Z8%uWTbCy;CXQ;R&4~7of%xAwWjKQ{6%M?D&d-?3S@|}4n~lu{ zYnrqR6X3r}gLW64Mz2J*mU^6(zrp+e-1C!s*z^7d<>U~h34qt$O)mG z6mq1TWV}HCX72aCpI8gdq}yu;I$1+3A9d{ZYBus?ipJkw3lcU#JGvdZXaLKe!d%D@ z#uFeutQ?+_{E>$;hktwR3I5Xi{w!VHG``8CQ_3WvvUJ3_FC z9f-*rDV(=Xv4|t|3Ahnx5o1BmKc2G)HlmA3Jj3#?10DFjDn+gG-Tvp9Bc(;fvgGW2F9 z8T~F4vb}`Et4URwKxwL*B%}oh?Z|{gYd$iQu9E*6e=mqeE4%zI0ri>1HH74Tv{|N- zA4Mj2R1vRM^Yts)2H7Qb;&Nkd`pl&q`lxyE9Y}C)8h$gv_(GT%{8rc;shL$OcS!*x zCfMGDq(4$Bv0&)Jm2qW|^~opwbH>YcW|XT5nmt`#gYb<6+ik)>zqTo36Ny>S)M>tw@$FZ#wH-*)WU5MY7^zmeIO@R^Vh{zu0gLXGP~?E z5ZhE`#(qli+EUnNzbz2U#93nTxc#y-zZSqpvHLFn5`xIJ-*aSWB5j=DstWfou5ez% z1v!8=zxz(~? ztP>4#c`*$gKSo~DRl=wltBB;RVI46GB|0`%kTf9N8|C>2r>Uj%d{;lZl=6S-ID!UV zpQAczxpzUgc=qt-)V{(*U21xG69qs**&J*LNNOVC^?xJ+&oRUd6Tfy^M(Cw(Ipm)i!SO`2?NYtr66Ve&Ag~x7TRITdx)U`EocM)* zf6wpn2*Dd8`t@Iyv@{}>E{IS70BM;2&n2y*k-f8-fswd_jisLBkAF?nNzY!-+R@19 zf7ag=sY9wEpZt_`I=dtrMb<`%aLdc1DX9o&AkT#&+fG3xf)du1;W&e%XJxHTP7)3l zi!Z#3xwnX4Q#HBp5auc+tUpLBym!YpKXqSs)a~#wQG4tQxNfF>zkEG&eO;%Ih~{wn zAoMy=Xy6jy*4*!bb!PKy_Z~#?-nb$axH5Tcj^IAuyF$nO*f;=e?)NTT z!96+?E;}b8HayY$I@S(&E^fE&El1wbM}c_>UHLq?cZCbk$Xq>pBjDX{34Vb70bWlK zyvIF*7j`0!u~`|nxx*ndy6M`r0G*4w{Ytb0OV&Pv`4DOLkmBY9ST#81L_UP95L*gT4S3JY@&g+<||#KJ6W)Ds{V zN9SMb<(pB}SJTPMVu!XKE+#f;;64f|g5`SQ=1QFSq=@yLgL--|q3ncU3B{ElW^wF^ z!iNOP%n9v-d29|XW7(AXoXC->oPZaZ9dI3CqG8wvwoy^V62`SHTtXGr!q~CMa42S4 z1ND0IDvHYriUi%L!ZT_D;Xy`<^TI>>Bce^NS_;ybOAjm|9-2R}Gu+h3S2lvYxCB@T zj^DZ)V5>%B>fi-!g@Py}Fa8_c8fgaY zbYhD2T$~&KAaa4|7;qIw4BhPuV~EEc#H-TBVOge8IxTwqB(1ijSUUw1S{cmZvCzun zn#HTWxp9%hZYVMf4IV9he#(p2GcY+mzFZ85%R+7ERm^)=O7_TK;npS&kkgRb?^v9lQ%+$ z0!MgB<0FQ#RIq(pi(CLHDdr(S^(b|GoJ7cDv{l+A3Bo2}hrL@AT-_tNA4Y?woj5&x zx_V;K(ju-7nSu8gk_F69@YHOV9zC_==6-&Kipz;gG~7z1Ju?LPwUYqJ-C}wiHNKFd zoSmbdxwqef9JWkZdKlXM+!Cd3HVxR(MIwR_6U^ZvG zGwPyZFYmH!Fo-Ac&S9-lwK3YPpABT_?na@Rd%wYtu)7wG1}HmB1@JQW%Y2O*C2}Ag zgA9TqoSB$%>P;^GC>I$a;FC7y8uEqL8y`O!4{9|E3zR#W4CDiX5A_DA)Y4JC7ahwi zcXEl@G99vj0iAmpf8;NV7rB@ULV)~T>r6meYc6zwxCA9w zged?E`z74oq`N0CpfS2oFDTKqL%4uZxsn^4WNw$+PIj~>aMDFs=K7&g4g!V>!XhsSI^ZLVPrQ45PLONKd7n$zEOQKiKV(d5dm=D?tms>l5C z&R+PkbjdY`iEt4jfH>B9+K9db)tx!OLn_{HxlBd!4Kr6C)ud{Fv#aNIi!s{uhtJcU zw6Tf2xbyY6cH4XN{6FS1x`mmjXfTgvN*A<}#cu;Sj19Ee1O*Am!|X^wx=t+=3b@sM zSX3}VoZoJAKVrPL_{3=?@dlupMk*LJX8H=v!w@VTz_>78lg=b!qt8Yw&w`ii6apRC z(Nj?@mZ!M&OVn0%8S3kwC@VMV-RG;>4W_4*1R!W3eTFc^A6_J-ush!Fz(@Sx@~5!wx$1^4OPuV2C2Q(=vG6|%=^wpX&? z;^%~dijo~CPB9QAi|v{nj2eLAG0#0kqTO2DIHkUQuD66;x9_?uB|pR$DvA z6a@OIoDHdUKTFYn|A#~cseWR=8BE8Wb>1B5!!U!yFeS+_X2~$CsqoRbUNVKGF5{T@ zAwR}6jLy3GVpPOKyj{U!(DI~SWYXLkyYN;&es$YI^0H}8hc8Ms_YkV??#A}EHK+hD zM(_ci8F?J1fG;Dk8)e20c}IxJI1!O{!_dl%l*(?f0Re7d-GP8}xF^nAO6y7qwO1PT zcT-o|e9QpA*@$`pxxQmQA|e=$oqsg@RXezRGR@qS8DbkwFM)~6sy!iwXs9-8EFBhk zI{2u{r73D|JX$R`mK+sgY=O6y(?#=?oyIjW>T*;8A=ZM}zyoJ$Ql&cG@J~ihN?C>q z(%22SMIPd8a7ctp!kls6sYG7=WR>^VWr()N-aH48wNkd`xKh{QI(f;nmf$82-C zPbhx-v$+QMufM z?YqUo=wJI$R(IN8+Gl2!Q4bw527-k}nzU?*dma|WUj3*)%6;)~|@;mF9S?c`^dHzG$@n1M?O%Ehx?8KTVC~^~1>-e(ngY`>v%Pi^VJYPlYFJTw zqc|Pg$o4KZ&0jL>%u;MLu14A122umdb_=Xskgzy{18%TcYZ_^^?heH?v`ZDjdy``v zm~ReE@p?zFBZ)J4^$VKJ+OQ)Vwp{9LRrR)~Z8qdC6KbsU%{TM+Oo|5cUX#=or)#Ap z*Zy_2_!8OVGY~B*E)w~<1RI=Qiu@1A!pYpN6 zR-g+`=lN2yY(TP9XG!Hrv{>1DRkUFbOJ|#IqDtnI4OuMIdMU6b34k!)Cf&hVK?+lL zfkv)Uf@1oSUi_^x&Bhy>zUhjw#nI$xlEt-M#*b_Cc1|;m^BfD0tQb!9Cbp5 z)y|ru0#vYNo>}TmQRV96uGH!JTS#A#(!tKHJY@DRQSB|GcdaKW0z~WTMvdQO?L4v} z;|bRs!I!izD5H85Vm4v$ljm<`TMCI%v@WGtzfbY)oQsH)4<)wPQCmOq_KFQW+FT=! zq#H8ii^gSJ+i#usV}sIDAGtO-6j7gaoYvtc#2hdBbZA?;oNEr;I&%+DI?D_EfjLq2 z{{ECga#FvlCH0F)k04$R?w#81os~*H)+_A{;owlIE!n~2{e2Ppx?z}P=bgKy;9a=o zxH=!*j|?ubS^;KjW*MuDEiq9a5D)V68CGYetr#{eOKr++sk&?l#;Lwxq@hF2Rx&Ti z*#4PJWIsskPLxL^D|)l7*w#iFPnQ8_rbjVrEqegBZQB7Kq=`)!eauBu%_Wg(KC@AF zv^6|gF5Xb}lBZ~@U@yPlIHfbiuRW@aJPSIQ!fKP0OTHmxVB$j;ZhfH@(;%72Vm^Iq zx{QN&@T|;B`GXp=IU>P=XR9ct5E6XreZ!|n`BaZBk0Ca?)_(C@S%vyuEu<`~UAxU8}&(+>uL@t_yb7x;@*)RJb< z488wGBNdQ!j|h(-`%Nz@?Z#&z-#R7ht7P@7 zWIXDik3?F`HpGnnhqCZTiNJO#Bi@)8?^S$D{%(}W9m9HjjJz`Mo6>9Rj3>9;p#gKG z56nTh=Yy!LF8voB%5BVE<4i_x2Gn*m#5L7jx5^Ev+E#pu{ME`d#5LAkxAG0+o;z96 zi4T7NdvxbDO6S2MSP3O>2>G#+#?;ktY|Dyv}QP0xI z;Xi^j|HtA^4cb%7D2b0O;gFPxZHAZpA&K|?iW!oosH}+O0YPkj{w$zTzcX!0<*0v?b8Xdjxt>SbABvAx+`Q34Ik8O23i)yOCb#VIbytF^Ut{Hh(Q_1obm-jV`Fk&jKYd_` zfU}Qfo?5}RPKplUs%T!5R4zHi)++Yc?q5#3|A*67LAsUrm(w=a*CDy6kbHp(S{db3 zm1t1;;k1{m-BNeAUvwi^+7@1}n^ZIL}`evN^lA5MD;$|kN)#f-D4O$AklcFKwI zBEFbwkf&4HeM8Wb=Q>u0c#zA;q(h)F$pzXdrk^eFl=CX7PbcU(s5E4UHUO*0E##s} z*7;869|!v?fTG*VKMr=8Q)Aba=xOI-NtZ~{$<^9!F5|(sgSTNJLNjw?>NNV)e(kwK zTUR+IDIO<7c3j?EVR+0jAvH;+pqe394@!>wl1TGhV4iFpcx|4t5SUZjpQrME~g^)%qS=q%F&!HT4^qsDxOis z{hg1~cHvgGndy+g!8WH?>+$o?WQq(lb9qCLgTBfBUlJ|enp#u1Dn^ZD1qIDGDMtd` zihTO*von+P*{C&{QVI(dmJ+>wRGa>&B#A8JtBm^Q6blAqAu7ihA#d@Yx2xV0g2cmj zj{O!Q2zvIDNAVzh>VhR;idos#ui8yj4)rJt=XHIQy3>K}L3qtCW^FK+U>h7} z(l#0BAwqr#ZE%|38RGww&{i-1A%?O_mSukkZLWU_?S6`ZKOIyjN=9Za4KlmAyrd#` zecI&ibyO)*C6$JxB+inG?F^V~usG0D)6EROIh?ES;~vE={X1do5P2Zvl1uwPrBq#S zkF$_ekKI5?hMP}GUGejyi}eExn&DQ>-L?#;9uvf48!l%{iWD%ZIVOT!3t}@9U^y)Z z*2S9J7Q+9260f{o(-{`ZqFO939~q#9#mC1r%6*ZBOQn_?iZKOWunW#Q5rQEQXkag= zADL8jg+>-~5c497J>mI&_*Ta_5{pugx(#I?1Pw?@-ihf+N#2j~ml79GHn%psiyGKb zO)wt?+?i)+(lS$h)ZcjkcDHzEi@BPz-ZwcC!$Xib(yw;H1( zK3FaMh>+%gL`ZR2T$~=wy~srAnY{CNoeUoqS69}!=%rig9Zu)#dwWg6b%cmv*gWpY znrx(2U1hY_SrHB@hPoAAtub21jW#+1g5(Z6o3W*~;;Se^L3>4sCd$;mpw`^x+te~w z?M0NG*4A`YP%hZ$6A+()JIx+u~GG z6^C}MavN}9hN=$UoMzqAPV4vsHIw-VY^Ac5b(1fb#!2^7Qd6#W*{wWh%q97|Y7G3= zg*;?GsUCbsk}rf2M1o#lc;Gm7QLLeJ=ol(a@R!s{UK+*yVWvK+mFoT zWx65F7}iSJ&gsovC3m{5n8XAdVM^hw&=vMODNuJH33RmO-0^oeu_BjW+$AO_u{sAF zSLvmje^@BIK$4K`QNq(Pk&#%MLi-T5gtp`Z9;%i`p_*8T#$2g?unDPe%cL>nwSTlhu<8Dy$O_wOOzsq4(YUZps zVSkC7FO+q%e&p*!V`44Va3tW7;riWG@jJ?SH#3WlYQzHpSDHRI2>9?IQ0J0Tv_GbxD@Ca<3m zX&)1mNqqRrl~WCga%UbN{7KnK#~jQb1RUchuYgH9yWq%QwGt<1CX`*H@RT31Fggln zMqi=*ct#qIbDi*jU#Sv3`v+1HblF}(p9hp( z^N8-?roI0JEM}wlqRNU-a^P}jG^}ddSb)M;;U?NFb3xCP_6M?zet8jm+QM9*b@|1~ zGbxU19V*-_A~6^KWqlf)B$x@X?*HxKFzW*T+VIhaUDLaD=kahdM3*1mnQA?$P9pQR zH1w?Dv|(w>jL?+nntzOX086Tv(MZlJugFGH!ES5i{Z!9-3tGJ05~ytstGlQ=qScD_ zp6^r#p|cW|@Ku&CmwmJ^?R~p!_I+x|173vfjMpV>IkN$AmmM>l04cPSZPs~F{OnKL zY>;G@^}NTz!_!t@{H{ERJa+%c+|`7pbBHq7NE4GX|FBFwGhGqy?O;CoHg@=yDW_$* z0Mg(Y-qhe7x)$eQhqmK5mE+-b@|<;UPiw&-<&cJ-J?BU@?(_65^~FwirS;ao{mKRV(c%bO5RuplhR zP?iXkU6%Pi?OHYZ0yzSoc7a_MdCjkhf}UdSx^W%b4C+GO!qkfd-Rb)sa?R1i5`zw`%>} zx9xz;DQRA;nS7Sa#w~)flysovlw3Ze*18kZ?B{Rl0w~U-?LX&fUYvv+4DLKH^0)un z%C`sWt3HGmK<0;J!YhPV&v5tUg^~8>i^NEyGJgS6&XJ*$vqX4q3yi?ltCN68Y?N&1 z)WNZ66wpeQ^ljJ6Gcji}nPYVxM||f`7@?dn5OnG@%MtbsZI5eF`|p z_1aVwFH;rwlpuj$Ap7+<|Ila>b$5#_QJO~BShIq)2k|%LF6TaHR7Et2174oLwl~lD6n;7dBq;zTo1KI`!1N<}>Ev^XP?-#)y6#EUR%ub>D-{k)MSoejR-9%N37D80Bfcs-HrN4zfCfcdX@ zJv;6{#OolHTiRe1NOI(%C+0#AT0;BWLW(&40<$Zt;R|BtXE2$*RoQ=o&FIh z8E5USoMFibZHK$Xg(`vNSKs$-lf-d{6 zV|5GA4yc|U{TP%g2tT#G$C0$*JMyXGqpus-y*9E~12kD3i}P*~C@Hy@LPw!81&Y-` znW{JmfZ2!sTKlaA_Kci{A2>?I>Noad;-L1T2UW>S*%~y?V_qV?pf{-z&CQ3)SFp58 zJo*1v*v;c`^puRFl1IDgDO*Gu3|-L6mc=Nze+CtbXGk5WCqkhygs!1LIZ%CkTfFmJV=dg!~xoZvH_FemQWGP4J}+zGsULF}In{}9<}59&F9 z{}S245^-;wKScIl*dHQW@Q29uANrTbhHeDYaWVQKvdzk^6R+8;yyDkK%(ieD@&&aS z_`NdZ2H;q@`L*d5li-|=*tH+gtQzi^=Ht_ftn z0rVcJ!$a}*PqPK#z7Tl$`GOzrAGF^fy>fJNRZmMEu--5}B{zTf z92#%Z*}tR=L}mAeqn*z99imh`gMju@b=gQ%+EyI=B8mNg{?mw)A4aKL0t!`?7UaWZ z(k)XYb{6sV8`PU;lsiaUx*Ip6YvrKEArX2h_XCB!r)U7vM{ow69!v+3J|xRv*+8bV zr8V!Cn#H6%GgtVN#_@A{o_ph1;@y=YGOZE7YX@x?imrD@7J3){1Mr7-_5^Oy6 z2jAU!2ZHecu>Ye0;ByL3e*rj3QuqX%RDv$n z93U+Yc=TRnYU&}T`?p5tZ`xbXJ0u@`8Xf$507Gv$d^+|Y+4@Ze%s&+N53HR-Wc{zO zofHU`=TNnoa;7sI(%T`;2M`6S-y^{!uFF+y-e}1-UEBl=gsK z@_ptdd*tZ=muA@i_Hsg8RF%Jz{{0E-{(S`fqlmbignVqt{hY@8N5T$Vy76|b%)|W{vUEnbiOU*dJ7jmmmT z*JgL0TJt|=p9oz5$!<7fn>xh{6cd6`uxciK?82lpwv)<+A@k6Il_Nl!U2IjLP-n7) zW6qR0VYtmRpdK8(3YyQDT7eS$@t!{)U%|seu;eSTPZ(a2WZM`o#pWxZPM_X~cuf@D zN%J@Of1bZ9MzIx{fdBx+!U6!W{P*+s{~g5_wl=i2F|+>9Eh$;a?+P0t$iqD>U;)2| z_mUqFCDyef#qG?@1fdBS2F-r-x$C7%u zk%R#u`Hy0$mGruh_iCv$pt3hj^c(*15r4@1w;=9d7TS%{gRj(_*QuANtvS=otNLqG zX)wmTG%ZrWm%H`I+J?`?1FEMIhHDsMs_YWD&S8LW-S&gos^a_TvyX~zTjgmmpQ2SN z5D#zp1P@S+QP(`T6sO|#(=TL9;#pJ$R=*BckA1We4Gt2sAlQ+LU;NR=clwf+9-@s15Ophf4}N%ek! zM=?)#_zg9W+nt|wWH?>drhWy@jxb0ciPEaxh-rK#zM$Uthb8kWx-TDqels#>i_9+R z0lr-xiJ?;TCQ&cE*k#iO1s=AA^VvU(k#1X{@SU+#;80{t9afX=%0Zqk#b{0)nNq1U zsKqSrgd<*AYmfv*^`<-A=0(pGsk%xrm z2%cM(VebrmydMu8cF*khH%|ED7OhWRa}W9)ul*yP_+tg&C%pJKQP@|J?-z;Z=JI#g z`PuN;+lb+ti@1+YzRy_1H>2>wp{EFNXG)=N!l~}C3GH=qA{*{*7om=Df5*|SN@1s( zuZvLUHAyE*jOY#`oo>q+Eg{br7cDa|2Z6QssjTC7TZR4z&ncz0-e2{IA_(`508(wT z+;*N}LvWVEqs9P?2Ln?>rj_%?>%q?}pZ|P!LAO?jy}|(itdjx&F#i9zyZ(>Cw1%4} z^0Si<-=#!@MwB%%EwF%AyvjV3foIsCz(^}S0xB^GXiKVstvF^=mWJ)Lq1a}bnnm%A zMzaNM;<|*@urMSIK`W5d*j;v~b!O=e_9~B6s|8l^j0ds#VsMhY9X6(p^sFq+3W!gZ?{0#* z4EgYRi_RA;+%HxMAJLu2*`51qzME;jy9-VE$?mC%Yo$5Tw-U-XlecQ2orrk__p0!1 z!SDWo9*GAV&yQ=VPp|AxU-=Jufp3JOq=y{DPjZQ>2VKNZ7vSGI#}z)*bF!FX5P!MG%|h*P&qYWfl!cF<*l5^nXQlYAKMU zpLV!*nI7v=T+_wyXs%~+7N@NnN?a~D4Jc4g%}Vp%_60Ja4#87|sV~eP{Ol^b zvCj7*<5ZZI<;AG@Dhun&b7$eYD;?}UyeOm@`a0)^or&QvH0C*E078=FnYS1Z1cO-; zrHg*ImjAjxopU6(x=Um%D+XEOh?&yRpyVx0Lsg>^Z8X!atH|GCQB3&V43pr00&jJ5Wkn1X`g4R$&q_B zT=!LZ2hf)g2Uu8Fh?$Ao^(UcjNNuWit>Ew(*b*U}sA)zaEG-f{0sR`CQM>9I8rfFT z_Ywc<2J{*qoow5X?8=9d!31_Wf|Sl-u6MUrt1|{+LAJns(oX|5R0gg13rS?*w(KOO z+e(5)J3%)8WD`h{Ww!cSwpx6a-h^|zxXc^8nV6NK#8d$Fk>iI6Ny1WW z$gILwPC>Agag$QK%}(cOg)=AhuC-9=1?BkCxx~QJ z;g<+gsEgMIi~5=ntT3nvkPH~J7qqZAD|GUsRnS<-kP;0t`wg3ij$-Uk#uOrWl+~Be zphAYfIr!nuQAu|-%usxa+pnL{uf^{#s`9{z$@mY+0O$*ljC&1#jPyZ_g=&WQvBWuR8%ID(N z#|+S|^<&A;A$jaf(Lw4jxRhU;0l%%Fr2wR+SO{^<3$F4 ziKT81)ikVA-VARZ#UrY3jq(;Oe%Alh62WEY4$c#O!E2zt+o5R7@d$=b_2|%Mva0Wl zBVKAoR(bA>89b!Rea2JHVn95#4pmnr(^;f~xM5_MvPuxaTZd#!XM4*z2k+mJ_6H4V z6Zm^W-gogN%9?TfQdl!6#W;a_DsGH8fBkCr}mC8vo*Y_kNkpT{ih^ zE9pGVwIX8Q)}qVu!i%a5)DrA>5_o9$!(qH$7Pen+GE>0aEWheQ0{J}{?Y@XeCt$r&DTbKacJ47ZwlB0BNq5~Xs^>PKFtvrgJ%FR!gOD2i-#)Z&U?D0fCOKso zF$NVI#vA{hT1>vGc7#ZF7q<>{ArNmm*2e~sHnC5*zkh1`rTu-orfKO(k0nhsR6vXw z)fXT(d;@+9m+BF%=|@Aeo_iP=uEfM^)cn2pC&i8QSd3dHoj z&J7)8<_7DQYuheZNc06LDJo>zz!ZWda7ONRRK1*CI{q1dpQH4GU3Tu4Ny#bwFv!el zM7`XsG^?b2LCI&_`>z2yn*z!3R;xD>O%W?Vm^{4GeAS|)S0j$=bqEXi8APceI5JLA z%SO!$#haO!Uawu|jk?=Ngpv;unKtM~c_y$D~GD8SR-hfPSR5LKQ zW@(x?BHPZo%>WBIoouQ=|0_`&P#Z z`(ps5d!$OccMq9!Ts}?~-5i)dCaZJy;BB!%BV|o{L}OUY1_Y-S9nr7M!LF#rU=6db zic+pJu!68H>L5af3juDJ{ut4{F!7fIiBgW-Lf-V zJlFK(K}=3dqafK^^~wc1&}jgIcPFY`Cx1N>^a{XYEl9;JkcOr`UXh)Bh-o6LkdmqO z=?!ZGXWO8%$~;D!0r(Bp4+yWtC^3|i$vI6j?c7yY2r*94;enSkOpFu!GF*(B-WYC1 z+PxAFLvH_^mr+-o>nHpgE8e|`{Ac(4QZ~F1*?Gn4jYYq7KfXBxkL2+IGX2opS;*3T zWA@3ZoPE&99wT_cv%wwp_9Jv0Lm@S%H7eeU;qrBzt)Z9N_{$~Z;4tNuE{ze>y6}w( z(&0$un6CK#5ejV1xRqz;@}U<5kY~}nd$KsN904&;n7uo@)qOM9Ri@f6vOW16fjAFT zk}G!q+l8B&lRrf!cdY8bIj`v1Q{E3k+nln$q2A+=Opg@WJk#LB8;!uph&^*V*4=V4 zG`HDhHTTF`;Oa4Tir&z_vkNvr@{gWAE2C7){Mb5<&!QU~Pq-iNtRFengAcdPNVEN9 z*lrVH{k&;RckU>wLhX$DJ-KS+vyRWIKS!WjTz(&7{B4k}?i_`pMc#}@+8p<{FmOZW z;{PT+_?unw^~oJJv#|A4Xp-%G84`8Pk*4};z5h*e!yP^(PX3J?z6lL+g%&q=ET1)v z-#V=jtmA*+0~g)_AncRvozd-^c*E%29s2cv@`mXZrZYl~Hap-59(gOid4nzRh>hOS(;>K0FMnlP|UsC3fw<>zM#RodGVg)$`r@ZY(dD`I2M{)|Hpk4|gmV!5W zP9njT+@ITLZoqXhgkCq6F z4W&vPr6=?r6uA)siLZHIstL>fSo8<%X?BsU+rkfCT7`-H#z_3cJ|;^Yh=nBPPS{S6T;)ksN>`Q;+X+@57TqH zV+E!aM_joQ__;E@F@vmKb4&S zT`hyMt)-2g;r|zYo0X#J@`K<0Q_gwbjN1CEtGjgzI4BHh6dcRBxQi=C&ZIrPw(T)Hqa)f{v5VZSiG0=sjgnk@8nEOi!7g$alB zM;0r&_B?o_>HMO24AWV~Jo+4J;->8(cJNe2G-1W+ZUfq>1ZM-4Wyg+cK8ecwIG)0? zN70Mbap_K)N|%wR$HL@{{P)1=uz)^Lzy3N_UDjsnG*jjVn+2=ofK3~74=OCJ@s3sP z1{JOY-KIc*+wR@Mxw^VC?0n&}y|jsbt#dCxmw-L$?i=_=gDKnhKW#aNnuU2)=DUI^jV(geD zEl9p1TC#<{c=fbo{xFG2J&x#`#v?6ss&UJKL{f;Yc0xwP?__=RWwSOGySc=oj&nD0 zPU^06SDcQv)+uL0oSJj0wV^~4NN%axh4*$K*bV+&m^Oca4nlytAq18 zxH3J_PJRi;gV*JO3ZwNkIB-_;7aZ2VkE7uO`%~M+97Ji_*9w*=S$S1|G3~TDQ&;CY zn6WRLor27$2Hzrry}p-p*j>O+riI-FJOCH4(*1*h=_s=qd+?UrqM08Y@`RJ)^;TM) z)SbZB*yX*iuCbEGC=tSyQtD!Q8lCwEs5%kvlZG3U9 zk1#y4v+t?xAWsT#vq0!Vbn}UVhCuEfaNE5Zos!#Mv8soaAlDmX8}aWbT?VqXEKnQb zs}KYQfR>Fs1c74+T;O(?+aks^ldP;L_&J1W?`~0PP}>)Mth`nC3KpHn9BaNQ1F8Nk zG{X~dY{k1rw)DF={d%A^bi-US+mN8wa;@8)@hRcA#iLZ@uCK8!Afm4_cgKXCU%o2~ zeH{Wi;(c8HE@B=oRtH7L=%OP5sWKqhDnvcl?h=x+vUxF|(U%{hzan}jHCpHP=(>*S zq&2v{62?7Y23^4pYAE;lFz$wvUeP0u1_*1)cB4k_7;wi%S}F0&lzDOK@g~AOBSUU; z`aQxuVTN3FnZ9h0f8K<8GX~8-ZP^?ud;&(lkPby4S6NB{uJpL{Lde}CzU|72-R z?9CkA{;yyyxF^yI#y6kl{Ma3WB@0YLKRy12FF-~xCR3zEKxl255jOUEe1l^%d?DA- z6^OHGI`bCM+5e#I9AiX_y7xY|ZQHhO+qP{RcWm3X?K`$@^Ny$UZ<{o4n>K0Amv!*r zBxmoFwf9=j^Rr93Nmpz4zbG=*P;J z$Ll?G8UPEUKB$gI3w+&xHE0M(_e^+=2d{s(U_Z7m!X261z1eqrIR6dm9U9wr zel=8fkJA?KZvro+WSRw$y>U6^ue@t{eWu>UtFEnw zdkN9Xy2Y()^B*#4FneG(rE(;=WrBu^NV1?RlXBL#jxbnOk(yVS zl7;=MX(g2YgrX|1l(dh?M_5IMRA;hOUfaUr+r%+cbL%8d@Q#&uNU>qCfm}#&pIa>& zF|jukPUcjI^i}_p_;S=jq#3UT49f}hNm@x69>!lORO(h=K>c&g;839?bmHx~S1cc0 zNOb}i)&uGh4z=YllXEn4>4k~GsGaFrm4LXM9JU!=mveHcI^Fm_c*$}E|FTf<8Gz|HEPqC9iSUFfJlcGKDq(D%0u|wV~Qg#i_(m$0p zJAWskO~XjzBSLUf#iUENhFh$`ARwQrVmBSPi|P2DcVSm(pQ@j^xy#4!2xz_W^p z3|mhKnW>YtC*`#kv_0 z^+<_`zf2`7hhcMT!oEU2NQLy`Tos2YMcl0!HEHT#+gz-K@@qQ{i<`Ruyim z?dJiMSyD?PxDQ>xUF3V;PXQ4iev}4aZc(17F_HgL)7~SO`?!b>L*QHH!!(|Nm}L%>)I`=fH`#dQ|*}({aWKd zyp}}h?^EbsA0UT;ue3b!uFt z#1K}N>y&YeUJbzl`OzM*ztW<%Y-gHbmC}*^n>vX{V?Ob^>!Q*rSiP;f^Fj3Q$G7|} zqsm6PGe`6v&=>thP?Z(ts@8~p0{@T<`p@6)F4dTwf`*~0$GRqxveU}$7gOV<{3Ckw zM$6Ql1pN#*RGtu&g(Si%lb)(cIsZ10HXAc6s}cILY!&tBMz?<;u@&A8+h z?sN-c_gz`JSPv>l!|C}Uh%Lu@)FaaszW`!R&A$pJxy}>_q^ndQq{L5*!E|Ylbdv-K zy9VWcHIiocwaa*@U$_2eqmE4DE3bYI!wjR#dhJ@%Nf#T>hc{7=>)^U~7xT%q+2XlK zZHs04w|o&zZ9KA=)5w@My|e%!xda3~NbhN)4ED~xtg3#Jt9d&7=fMBd`NPpk^IauY z+1OiGZIL5hiIy=4cg6sl+~0`mpSckilXa)za}>(Hnlm%@N_7_Kuv}9;KW7c(&}hF+ zGAR8@l1xU8W0n2PTXp#*DQHD_KDe<31>p;*8d;;$38EwTY*5=_am429nOgSW%CIOe z9d0u4A+9_F|Kd$>Ap%1HDA%YRfStlW}n--g&_K#alhe*b&pBl~S6$TxQ%O z(A%H?p`fMnZR%A%lxzE|lpLAOx+80~s#6sx-TawE(r97SZ_bS#iCi6x-EV3A{ z$;-#Jw56YM&mYgRBcka)zv(_BJMcrST0-+DeFJ!?8!GN=p0&PIZC({WETQlY((nXn2iY1vBj02A0)`!t8ataME658z<%+5+!ep1i zoMP1!51W|8xwi=+ZgyiP8GGeE9~=422>|Ts0a7=!EWa?`CHc)_3nS(ut;R7Nh9S8Q z+AnX#MQcUa%_NT80Kjv$38x}aa7f)c1bTJyEdU9$TZ}ma?t=nSIs&v30%EuvmK!$B z#w>e~)3y1-Ds^Otffp|g)E-~DF_d9v4JxIyB%P+P+e3vU$m!TN7?qNc>+nQfdv^K5 z%C@DA?gv_o)7_zfA~|k(PSOSb1_ z(I&;1qz|i2OsQfG1?kk>{AHI+i8J><_1Va{be3l`<9vL~PEz4{;D|Fc1e}T0%r2YR z)dM+|1?AjLIN|Y)H7?J%uHWR7xzZO1tA}e+Z65o<%L=KQ?JM8vTXRK}exD>i-`Bs; zXZv$Wgsj0Ah$c7xpgT025oX^BMcpW3?|}|yIGmAb zDQb`SM%$oi+^hm!Jz;-^m2Gu>%fLBldd!B;@R2f0+osXRZj2zoILW*bH57Nm2?aC| zxY4TOA*knK19kJhxNLGp$FhaOfys9Lq>;L6M#^*DO_o%3r1_kE*=sbtf94l_WiOOZ z&6tRImP@JKD&Vih`EqPUY2GExq+XoMd+wx%#=M~ViRa?xO-*$Q~s7#U=@ zr5x(dQzhN~O6KYuUwxCPb|f3F>&d9iVzhvLtBJOh(_hN5)n=wsb}`rTtFy-eAn>M} z)br@#g~VFXNwZ9LX>lr}^+FdYRn5H-v*u`7@L;Ydr=t#?%1N7@7E9*G4ZG!)R=e7? zs8Ui$@qLE6*)sZew*r~0!HfjaEaLfs`~=%aPLYzvlGKzsn#|{9^&^8=d-uavVvamD zEzefzQwaOPuFV~fniKcdi)yw$L;M_13A^3E{+nu3^@mz5Q?b-tYHFwDS{;Yu3#gvk zaYpq$kwRR??1uH{?rUXg#cGHv>7@r-rthzSzTFNP}8{CHPc^m4IXMI%r5wox+Q2i z+wX@4mkN_|vzathQ+1)Km=k*tx+UQRcR*i6-6>~m`~tQcQ6}(pugo*yd zPKT78ctszG&!Trw>wH*%Pes^{jljboF&j8IBUMi(I}ZI>MVg3SH)K11j&9HJd}DH^ z>wQ5s<>LkM*)*~c`rAb}a~y!`h!xW`hemylkVTUERx%iW=)>oQ?X$Wya?puqB4(wH zlD|j0OMVf0vzJzTq>=2E`=pN97WPZP1H9zrrtg_@@40fIlk{M@2Uq8iZn58y7+>}% z3qNclCo;i(9^3lA&%VFQFZLPL9L{4O^vJ)LvAoMCv7h7&aAV~n9_SM3X$^5BA6?Sy zEZ@0>dte^uF5b-?@aD0HIl|V_4<`*zS$Y_AoIa8@cYh<)NpvTMu_NA*wLGK1FE8>Q z)L45Y6?kP9f7&iBCf{0iy7I!FU7Q0hFAB@@${LNX<}F<8A}{>#p8p6I@uM!}SuNx( zobOgG{t%!4Nd8_q%UfQ#?M1jXU-*d@`>|2zb3unVaQu_oo%qdP-$&*Wk3T&p{`6V1 zFLDK+kAA)?%jFVXKoK{Ce9otwqoQN`9i>d`oA|==KsRl zO@5K^Q(yo9zuMXV+uo|+|M`Rd9|l@ADzeJIBzRxsAZQYx19U`w1Yjf(u=pe$hRjki#-6ezX!h+(gO-;LjP7r&q12b=+N7AZ>o z(SB&SDLgd{abWUP;8Bi>$vel*crtYXOd89DX8r+iM)87#ZK!I)j!`=2 zc(ZDP&6%>P3U{(=&Ecc^5Q96arkU!vdoijA<9tdhX(5S9r#C^hNqQ-T+;GKSLj9PJ zxa+^-IwC}*_Jx1$E|KgzPlpliAq`AlIH8{+uX!@n9qj=3Z7$#OqmeS#m<_ObQ4GZ4 zNmLAVfAJY*())@kY4q?^t|JQETI_x#)V5-QPxC99w`L1lW#B?RXUu1 z8ZF{bk6W|rXqtX1iVFThDD_}9lE>y%w!%QPvH?_+5(}yp>GvwqeV9}&i>C_E1g=|zIBR?vb6@$+xSb)GBhpjMe!-ATMAlV)jRk7)KjeNa#tv*4Le3jaCEAzxW$-F4ROumzSyuouB&ZdHP0v>fwi2dVGgwq)U&i>K6}+g3Kw*J`hJMK zryBa@uRPZa5}qut4M|hU`f*~YIdx#6{A@x_sP!Y1n>;B$Mg66eB#)D`!v#FELosfq zpVy6CVT#QHJA7gYlr(6R6KjhwivqZO8pl~)P-h6EZ>HUI%Y(%j!_AyJMc5kY+40)1 z5_kQ1FE6c&t8}7OA$bnY9g&fr@!a4ozLz+lJh}UuFXc{hD_(C))0q>8Wd_M5T0f>S zl5`D)Rv&f>UDOBgpD6{+7bZx;Z#wbvJ6u`*zeeo;YOE{Kg!Vx>b<9hU(>iZP5(@C> z13mMF02~cqB+zS=XIcpqh$pCXiEci_d$^8NKUn*->zF;<@YwG;)*z|9A!G!S&Oz!J zjLdF#$s(J{;+nK%t5xQD*lLMwH1;-obK%mqflU7Qw#>e7?61#{eQ(B_*&lTOe^;xK z7Nq{`^3PYw?fDVV(Hi&4c<65(>0cwwnbJ`jDfShErhX4i@?c5Dlxb#-dDI?Ki1I17 zEEyHaJ1+cQ%qAR7A(ftLkh3S8P_xgAFfS={Nj&nIZ&38{$Du+!5?;}uQmTmb_lr1s zm3K`1v?o@f)>IMgiZAdK-Q7XyS)o3rh;>RHEmzJtEY`&xdYLT?)k$VOEbOnIz`GQK z>{Q*7V81xW`$eo5$)=K!HmF`((&pT@slR`a?2faF-J1l;6Uius*eo}`nq%!1J<4G0 zQc8uJ(GtzlVBDqalrwpmwy0&(SU!%*KaL4*m0q`HJjK#0Xgnmit3i#tnCiQVTyYow z{wdH{h`7iJvwRYHl5No7sUVH@_-g_M;(a*ZFE}l85m_`Zu@}F>Z7^yKNGpoc4tbap z`I?_Affw8yG^AxBVLrJ_k+pm}+DB<*L6)ATwe|;$Uzf8@ofL<~0fn~F4p@Zs$gOjwSA(Y@SP#%sC5|-XGq`mW2G@YRq`+bk zOt78Gc{C7n-pDslfyr?AeWhf2Yr$FKR#qz@s?!^}Gj*xp4NE*F2)c495deQ}6KyMZ z?&APbIJ(6Gkr!CL=LM(XlPCYr&{z?y6LW zb+Z7T%mrDh;c58-qF%0O(yxdpTC8hi+v$O@2L3B_odg^=5mB;;Mzl!R-(3YBJ5z^t zvCyV)L(GDpO2wpv5w+}JSJ8dF<6M2R`Fu&3Xwmlhc4UljxC50Ah?*IseCx=h4RpzN zDH?7}KMj|IJwdZ8{&M11;I!B-TF+{P)dA}<5nqy;D{R-apREFW8+r49-{~l`nG};) z=(Z#=W z#YoDvq<-UWfHlpm8*x_HpxBlvH%A0WJ|FKoR86w_YCEy6Akz*a-#Ck_3FUT?gXl#2 zwm7;_kQ+n=2B9205%#Ez9D~bigW!||3&n_mxt2hBHsLlLz$(CLN%wl-644dJlM<%R9Uz}ZEfk(X_jEMNxnC>y@B@Ln zRaZVp%*T_$5QoQXDxfx1EiOfe*$v946|b{JL@1c7tb}lfM0sZV9k;QZExqCKx~|K}k~>Z^LTw|+ z^=h6VHRw9~n{%*|=>Hb6qR%5!Bg`@%K3U{Nj8aH}ouGxgxU@FghSI7|qG=y_RTC$KbRVoDY~5Srs#LmEAXS3ZAm1IGD(!7M+P924Ok-5ZXmBzF5q8Pl9etm z*It=jWgUG{Jpg>enIm@N$V;@G7UhbrDQbvQ@Fkxuw_zHAJ2xIB#}57>F((??v0yV- zg<09USree~BT1DP$#yoSWm~Bg9T_N<{&O9uSQ35)EV1vzs%jFInhcs@l>Xq7Fp@dQ zOZCr5qr`nN5XE({#WJp1_I|ESD*P-!O40JB3>fN?ig#9`tIPs#uOse5bwjjE^_>FS z;*2rHi?aHG5Kn=l&?!04_Tfy7JASL*O63o8<|d}3KnHh?(t2?c6>n@<5>N5M;S+{a z(zgi`P52vxk=}a|!5ANx6PEPfm7<=`!-nIm!~5b-Fvhh~@{dD9XC*>>>eIs{6u-F1 z+hcAOKm6TD>lucNUko*fJM_Ru=pLTN)+kJID{Z9<`F6b4$_Ha7=P4(c_g^JDYF*|%H#5?oN!{EK_jU#5 zkvtN^jl7m|$vUaD7<3>m#rNkcG5alak|dQx{g0e$fgqH)8@T}Qgt3g# z!i$agrP2&h`*E)g zMR@dwU6yfiqMo%@Hj&~ogcYlJl;B2@wOe-lBnd`IQKxiMvo@-meEyubx;uePpz#dY zvn}k=KY-+j5!Em1B`?K6@=<1ww03{iEXxtpuxMJ8NOO0Dsnoltq z0|u$qN*ZZ_=5Dt)*6U!efT~k}r@tI25$Y|Tgt~Dgac!xh=_R$#;8^uoBk9axM~(qJ z_Jd3oT5m|mbS{3=XWdkXV`|^%Y??)(7_7k3&%85~uAYOg%<@FcYH3^?K=SlM`Rg6i z-=(Q^3iYX;`=9?HpozJ&7=<4KbJ0m&Koq-f2-cv_jxi?3&Ce$c$`2U@-M{wF5iGhd z5^@7w#W;ZDjYZh0ttaEb`>=^k#n)NR?>PDFWHwYWq)I@jx6AD(jiWnp!7p~-pIZ>t zlzJIKDPq2nq?Peph-5IwJy^n@{L5St{U~nAdzfPvY{I|19n?p>M`;w}qh#-2kM+ab zHj|_PNiwT>j%f;WB5Wz`T8Wn|qIXWZ#|53YPYgU3HXb%hgiS{Q)MSaJIT1u-w?r@* z-{xg!mX$KXlzurr;*TgR>KA&8gBmiUysxRWfF8Ag z7G+JJ*msGOl1=K`tj)DSTg5A%L^T<1`S9BSX$<&5u%Y!(v<*ta%7RS;?f?3ItvynfSxs^7mv&Rsv zCdH7%WSMHUJUb?YYR0Binx(9#w?Hidn{IB42dmT*-Gc96Y@*Fy!+*ocRIn*IYJNu!il-)B zCKGT>3t+4=(3*7-(JGW2bYC(~qx4`%htxNUPxhfox+~ZlEasT`NpnvRVCXFym4`3x zP0HuGg}!*j<*zTIbxE@RCz>kNQ?byp>&$##auln)Ci(+QPP2VdnX&MCW8V!n%P9zL zC3Yz&Ds;HzJY;3JM0A&l&!5U5a!c<`Nh`pVACPG`o4%SGq&Mr!yb;5KFw^I<1|8p^ zlTsD;$V$_Wu>8dh9-slsU-;33J8-rOy6POhR@7hy4y%+s{THGeo?@=7Kb0td zVYku4Get9r{|*qs593#)vBN)3smcc1lIj)CJg0#px&`JxoVLI%IA=1p+lwlD+^XQSll-9)4Nz8E&&e#v zM!erMi>~Bne1cJm6|7uD0gP77Vqtru6b7*`&^|G#+QbP9A&NA$FjUx|DPv%JQe-vkxv+)%p(sd#&qi`UxSambjw zKq#~`uxSFEKSx!(GR>s9ibX#Y%h;u00!ph8A_E?3KIgH;D^2BP(&3ZQHtCqqw8JW` zNwe5wuuWS>owNwZYT>25gNB{A-98ghBeP0Cml+~W|p zU76gR9lzGi+Iy)E+Qc20p5e?Chx4DpOTU;|r1X~P*N^;$vEL`{p4=6v|03I&J3R+` zN3$)SYR=K?r~bx0il9HZf1?}xl&%l$6M4B`y?aYTbp8&@ExKK7JGVc_KMxExuhVjCLR#ICNHUO|1)6Xk$D*4PodnNYL{4(B zsfciW$Fi`|))GFppSKBJ*+bOu2z@A(%VNKlr5Zhx)v8d~L9HCVTFbE9`{9zi@H;iM zy-|Nj%f0<1Egv)60kC3x8Qkb>T4KmVd8=914?KmH7AlcVaf%e8_(x}bc*>jip~N1q zZ|-~ZgX6qA__ZeFRtijo8#~LEPrdhLizYx%K-{Pg=Z&5bSzvzqVEfb?oP}T2B*wnf z#_Rfm&VUJ@!`kUG%2!MQs!7mmQgR7X^*aoCu04<=; z0pPo;3_rC4kS#9D?I)}4H~d6ss1K-{1v+bC^n1mE>PO`NB=!2afmC%B?lIIU|JjHA zjkaORX*KhE`M@qN>B@i3&nu?w$~Sp&kJNW^H?Dej2nc?;8n0m(mfEv<&K5`g-X1NW&pIewtC+uzDoY9e}DS^mWmy2d9Y$cYR%yokBWKyt{GULh} z8&$39mp}ZeHPd$N=cdNk)bEPdHk-Mgg(Y=qUozNbaE;m8*=w*UByAOpR9S7FcbeJm z=C#pIZ)yJJMeS@p ze;2O~Q@5Z0`_C;Nae#!MxL*d>j^_W9!zF2JXl|-x>TK`o^y~0%>tORgJ^gi{eN;x4 z`OVoT$g;9l!$1%M?+}ELAR_V+^au$=k|1ahfUVX@up8^wW4+lWhS8~>YhLNJ@Yjg6 z6z>HAD3`=9H!FQDY^|-WYij&9tg2|E-gBI9x3|08Ps4PV_03NHW;>too14vKXLP+E ztjgm61t1T*p=U?Ta@?D*zdqXdtbhXLIqyHd3c=BgqBq@>&gi=xP}4c=Gs}DiC8=|I z#wM+DzLApm9UQG@e}*Nob9g2t?K(Z6k@p=Q{iFK??#+qyKl+r{d4x;*zA9YMPxoO* zFFnwAzUTeSLf&;B!0;hM-+pzM``#ahZ+{a$`R1qd9VUL|!2UV^O{D0_{SJa8Un`J* zuEF|$7_WxpJ_lhV^cg=^*I#3m0iKEDkq07!lVIeSI42HcwOX3I{2voC|bS7#S0iB(qf(RiY-c zWeguY=wy$NK+4inp|_2=)bqwCB;^>1(Pz+@Gs^c((8!x|jNne4b?3-WI7MlZA2}-y zO#sQEIXN?uM6+p1H4&dpoE@W-v!e(;aUtGdFT1h3mj(5Uy+#_}u|f)_yn+$^+w4p9 zpW8rvS_TX#4q{)01yKt+3wW1wx6f6Hd+$bX1fIFaubu+Sr7O(NN+hcOeYa?{<_J&Sh-a_MRe>Gb20TTIF3 zLlx=V_1h*(#@Cx7DJzqDZFK_)VJ|EX^~e5a!@YM zuVx%K51@;FFjY)7&8^p58|JYe6amUIynxfQZQ$=f09Nw#G#DzbwXvbRcn7=g1jK>M zS30vUD@vMIi`;oHOO$sF?A}0E_ELlmck)2%ZfxvdKD~euP5JN|=1H~ArMhD==TVfy z`cb&s6txYP{?2DB@3q~cB+yPMWm!9*l)?C`)z0TKe_RFUpkX; zsjXu1-5L})Da$n661QuWEesu0^eY`VtV+2PreWzT`UM^Qx(SIAT-vJk?(zXADC zMATXeTFZ3yaq_w-FE1{bm&^rZ?14@#l?AZ9^>KeljFn_u{#59!RldaMth*}dW=69v z;qAI?I7u$3#;XY9V)?P0fVe1H+X^blDq0V(ZX-iwEtg!8r(zdEpZkNQcDMABXBS_;>b45M#5atGykT!YFumsGNiq<3~3ExLph5pNi#xP#1>=cPWf z3lvxga;<_A911x(MmqQx+6U2yaQTj%`o1RlRY5aM_1tPtA*YlZfeE;p9fvh(F=|G_ z+G8qXBwaA=wl;D{#W57wc*&W>IC}M1S)yMv2p-g~yFcSMk&q>JMDj}_>$-X^(!0vd z0~eNkiayt{@hws&D_Wvi1Ss<}I^yNFxD)QLKD@34cyIs~R?6$pv|7%AOSH1)9p;9OZZn*YzFb*?-m>$9yA&?QYu`@L2E#K?I!0;b?-5nJ*B%x zr)8`GQoRe=&LiKUkm?FRy2xuR%Rj!AUKmIi7(uqYOjS<^IcMK2EKEP)^53ALk-?{bEf zx5@?|+cVa9<#+4!g~p`9@SndY){MU9yw{Yaq`l39U~=8YC>P07bf3t__LX7CQ(JN> zX&T`1!laUKKQ5%}O!y|s2btvD6l+^+A!jea7tu2ouC)pGXXrz{X(6p1MD_5Ln&-BUp2<~5^WCVWmOYAOAN-3>65h9? z4}Z0M`<-qbrx$qSY-g1Z$z#=dt)trQj5VH4{ks?wslZ^s+)cj>!8nnVRev!AILv0{btoLbq4q*5g4 zI>hyJni8FVcTSfUjx5=2yiL z(xfdHkk?7jL^v38;3MzHDE&|$tX-*tE`tl4gWv%AllwLv95;`yUqt(aN%NKW7<>ei z5-vPtFV&Oiqp1WCdI>BM%Q#1nL}^f`P};$2O9sR^CntLn8Ux}CQlAxY&Q9O*6vXDd z2mMk~9WwNlEjSJ(p|<)DMiL&|MQBV$Cq~;3xV;z!+w1%n5G-@K4hciruIT}aw zTB28Sh-Q##qBMvt{y^7ot03BHR2o2%7V5fCqzA%<#MDZJ9QhO`Wmj?D5h))C&+l7X zq1oB|!<;E`@S{N4zdhoO?Uhh37p5IYvW6B_4^)0dZIG*07ZGq->_=)9Er380j==Ov z-?!0fdPIu9sL~8{x460^AeSFlg!;^uws(Ah^r`H3YZnjvx330jw|^vO*N*R3-A^dD zWAMQIx_P{%;Jx2Z2(6OUuVb~)r zMXz9w%&{aWYee;B< z2Gs#?j&(ufp$`tv0PwH+c0EM)JrA}657Dg%<12GU!wki}^iR866m~)xN$zjXi^t#f zJ$>^lUsQWzH)BsH*g48WYDKc`I!0? zg$JB#9R&C*8x>~UbMV}JFU2q)tm!=H;!vI{692SEGX@!bK~N!UA0w~JCs}Vi6aRuKa(|6eWY!2R z8$P_+gMF9X@tggrGQd8~rnKQhIHx6oF`JyZeZ8)B&5_yCj@`BE=DMWc?B>o$!BIeU zx6)Xd8HO#&uDHhYb-%L(c_{C$JrJK3)ar0y58;TW(QZ4_t2bO7_WDfaDn01Nayb6T zbW*kIO0f;7)^C$*D=B-6RE@|3`BJlc&*0gQuY zz2Br3Do;OV!V~NqUPvgbF2edoQqK#r z%7<&(=SI36yPO~VoZkA_TJFwCoHy_q*|~6Ok1b90d5~V_WS3zk7x*ic(ff_nZHSS~ zxf@Tw%)5Bva>uAHN&z`b9tdKvlVHVq3^|Yx z>3kTb*Jl6_A9w(<0ARYJ_bJtOpxUEBD+Cp_$Wtn-zRd4Nz-YlDrq}rplDeYCfSSxO z8l+A=>n_89R_y&!jm_y)w$3u;jLRsBxZu}bsLk>>wxo?sQX77SeK@0{=w6j6U8*hF z>CJsS%SU+CS47rZ23M!L5Zf}Se~6o3cg7W|;3{jP)9@Px9ulG96`pMsN#Z0nag}!% z4ZOJ~V5da3n2g@g1F*Q&^y)<~_Zs;{ICUl$ZMeQNvc!!Kd5 zPwc9%w5)xmi=W)6e)7})46YBYA=hzKpOI@j4f7A?sNX`eeL2D#9B8WsE0+8B;a~so}c)bX&;5lSsH_F0tqJsA?H=;ZJ3qYbj z8Hs+oHxlgDz@SGPiN5h@;LyWCp-&x-F1bJ0^Kg{t=4dv;$2r7XfgigG+>f((;SoBQ zq9}NhVj2L!fI(%zAsaMF2U#=(TN?CXnyHwBk(~pRF2GJ564Ma2>&P)HN&3vm+fq#L z9ueDUN1yB6kg@(prDdi*q+YB#4-2V3#BSqx1nxE<7G`C&-}*Od%<%OFBb}zksY;S# z5#gj_sw6Z0pSYjYN*T-_a!0e7Qbe_a z6AIaC``jdh8(L!IloP2`6Ae`BeEEquF7p-RQaeQ}Fuyc8$#v)W$gYlhd@;g~m3atQ za08f{(1zHQkA05fz}TWe4w5*|-U=%tP-gip_~W3@=a4Uj{eY94-EPemZSH4`X^!y% z7jlkshlzqAQwYv|CXWEL8M<_-GD%(@k*Q-tSXEgze z!IJ5-r!;5^zLc>V_S7*g+cLJIOR3<0egz~;k#^)VH7sFzIJ~Kj@oa>o8df%Y151SN z>#A=kF}q+z)E@;4y36p$ZGz{3=}Uu8dgXR&kkMG~Lb#3{tpg0kc`RytkhM{jd|($7VKaY9UPy1WI2&<6IK?=K0zQajb=KMi$-qhnVV`eY38K` z)zKQ}RGTZxxtgYsEBt0Nsr$?4;Yo7}{XN)^J(EwBbLHOdA5Pz+|79CIV7)c7@G$`(1cRN>W5DbK)C9cm@56e}WY42{ezQ6yfU5lOK? z(Y9t&Y+{Wnvxif3P7j)0dk~?-dgj~Aqi7TB8EFzw=@si4b{1Shh~*k*7E&n?VD9maFHA|)lip3gsnp_TwH9~Djs0htEnbCmEgwtizbqaMz87rp&eU zEem(UfAq;$l=H0rfc{g|p~56=WCiGH?`&$ zm|9v%TM%=scr$)z(Q1=cMSnGBW#{*TloU#~$(7+`PtEdag_Bl8`?v;ohOSwIS^3zW zMFafAaXmECyx}X*6yzc^4e3w&M>g4`y0{1E6MWGt-Pu9L6dWGq^;s6sxv%OeMZ{Of z8-*gaC5pD#nQJJg@klRLkFe`+U-kErXP4p5nyoYCHPI_}2O9;uGX|&YSXVql2B5og z_iwFHv+OAJ45JMMRvFOE$6fXKbT*~IXNB8}apAIi1uhgON$8aM5T+oYgk!%(#XzY8 zN?iBqdlnkPer33E!O;#lI#P7UT-gBZ2B17KDM#m(dkAtw_Uf?o2PPi4W&PY7s5@iG zd(PQ%d{-h-K5~V`PGp{ySk;4?Dw)BqM;|MDJXif%Fo>w5pdPq%B(v^JJCV^7AIpb? zJfL}_%J--b33Z0Zej#Q1wPUOYgilf84YD}HaQ7td!_aG#dP6M-@@MGc4mrYmUgBh6I8bifVTR+3Sl&3w z3|AIH%kx2q;_!A3I$_wj>PC6SJEFG}Vc77#fGAP=A^S{y`OW9F#)0t`t>-H=W)k%IV88PBaG z%F>UG!qg?}!pQenggxLKotJV|6+6J?qINrUHz2!G>?~HTv7}sUHHNc87hBLVic@HT znTDW_%qZiNjs(m!0=L8j?AidCh5(q9y;N~cK%LUARa?T6{|>H)C6by61RuCV929I@4S`Tn2f-%!ZKF%&toUPP>W3szxv?8Ay|0ABjaBp9VQj%_fkFrEQdfGEPGs zzgi-|<3!L@E$~}JvflvEv;)v2)DeurDo&YEfsE%=BlsaC z$Xf@vx6=1O*ep&e(+I?A&FOYiS|H5h8R7EON6xpm0eDL!n+0zhBdbf=Sdl8|q*VwV zLbF1ryr257Sd`5q3J1IVGBLk4SlmcqRasjAigg!?)n1@{+o}9bt=uk1%LbS=9fZ{) z5Gzp#*C>>04b2Lkl2aSbH3Idb0mQW(cF6&0iNOaWxKc@51gd2Y%5^E=cDcu?JXEdx z5aPlEyBs?)-!7nrvHVnFQ;c$HK&?EbRpCQoqQj{?xJH2-d5Mxuft^%v8^k|WkT-5) zW(=C7)}j{mf~A7G6M;Ulqp$-_ox}`j7z;Zhs|DUQ0$tix%?gxhb;T?#K2iFZjk;yS z1`tro0@y-2ak{mClQGLTZi6mq!zLHis%Ob8ZOWEy)~bmt$RiVWm>MQaml77psz!iE zi9mfKI6#X!ei2e48aBQvmS7L-{0LPuFx!&b)8^swC`sKUr<)CE$qv}Udf~`66u4oc zD|f@LADdL+RJ*`!-3RRG2awF4t#dtm^Ny`scLZNy8rP%jZ|Y-B>lN9VAoiKNWgnG5 zw>DJ$43vT_^qw1MKNod>EJ*z<*d8xfKfV;XcRA|b8!*=a{={$S+DK0(khTV}k-)Of zP4nkfk8%l!dT9zQq*EELr620qD#$AqcIhKAf5$w~tIvg=zFw$DuQSb`W_Jp@deIwN zuzMNo0C-eyhxCoas6!X(NU$?WD&W83tN@!fzy}8Zdg|)qpK5YQ34WVazkIw5 z7!l;1FWnIQ-->peANu}SZ}i>auA0W3JrwLd+W1`W5xT0{v#{yN@EZ7=I87AXNH6*$ zy>=n8MzJrwSWR9=19%~I74!+ zjEvdAl?_3wR&-74 zMiu*oZpBL>nqbGj!X684zNOh0=Z_A*mDBrsUvqQvbx!ZZ^)epMhno!M)6bnB&t2y% z?4Pc0umIS0#4)Sv>X8Z#BMwgV>GX)C0woyz!%pxu9*wxVLu?(dh-vo!M%gdAyY**JyS>AHh9MO9NE`@U-7cZS9!*cDB0v2J;SCNZF**oz-|0SkHl^Ka7HkB zMvlm7F3Xx_xqV*{v%5dL9t{!6IGNII-Odk*Gi%_c(y*1pE!pshmxjwIL0wJiYzxd^GjqaEzcU6vp9=6!T8@(S+61z)$6^y zLaxxlOXsr2l1QXslK`Kj~O2WJaAdHdIEjt>MD{ z6~mle!4?f=M zB`7Tpm$k}$H~Lnh2qjEROpMHF$TH=2mrLD>9?JnrdGO_e%m1YBp;Zo}~rgnz! znQ>PmH|Y0PBWFUTpj?D?$^1~qjndLiX;nA}ma3j-1IJ0F(?wq`u zPKQj{xub@)gVZq^6Za485-z7>9}6Is5G=falaSgWlAckGRGrVqswR;wR;r#yVCr2m z{w7^h7~O7ch9@)uOUnmu+7?dW6G542w8TVOqv zTO{?~m_{nUc>eMiV*Z|i_~W(*a=M%mP*ne;k=~nq(Q0MK$SG4DMaNWqoqhC`+sdew zn~o^<8+BCw{So%pOYM&*Te@z**LUq6zK8mdw}*acEaTdBVR0Ux`ar*0z0I4&!pfWr za+qDQ=gKaz(P+J_{SvrmStEO0=WaCbQdFMyLw5^-dWEgB<+9A@fE7hufxQf^bfc4E zG)KBtyb^E430e;cN_EYVa1aU`3TJXam`-_Nu2u?NcSWTi7|)v@BaZ~taM8qRTt2PXwt55Eg{1|30i z`tSLM46f&hGOq4XNxcmr-Y0bqa$)B}7oz0&lC8;V7Yj;_pT&1cfmCrFE)NtIan94Q zC`IVO->fQWwSNd|`IuD&J92OFx22EsT!F`oQjU$yz^F1U~uRN$ul%uX6Pmzhgvuw{_9Q>h=&$!&TrQT9Q#B z_?EWS{&2@gHn#!F)qvQCxCU?^n+;(-G23-dudGAH{jF@%a=@js$87*Q`7~LfFC)S< zYj(v0sr(AsjB+P`fZeuo5U;M-W|nmZHh^$O-zKV84NH0*#Xqu7KgdFx_jA3T7s9#y ztF3Qc;Enx9jtzi|Um=VY??#Bn9XwCC;R^9^F)&An*Uc@vOx-US-)l(Uo1=~t53lP* z?j*gr`kE56kr7~F@s%}u@m3-N$4V2Bv`HM7t^eyc$dJ)#*ka-ZY@Gb>~I86HFt zzTY6R(=GHiBl&=Tx9_bnu1#}Oi*%h8;=X!}+ph?DjTZRKcDt2$6Sp&gcb&naFCF6J zE70XHt4H9 zn2@$=fUlAFKD7s=iO+9cV5B`-A-ld2u6vPkULlLCzAdkTXd{1g51zV>G{*&r+MIknM zRdTiqUe-%a;JIx=gEca1Ki(0EnLDN+ZK$D>!lQTpzwcwRXJLdr{sICD{|UVy`aj!e-TVO!p)oU%K{ylsW@E5j4Q3@KExalW~f-w-K$u&aGdv{EBV=swA z@j~Ui?zRv7Mls^t;u94&>&vGo21{I7=V%zJ7QzC4^u8OF5xvQ3 z2_fuB%VUg-6x$jyAn_r1w>?uSc!>Fax%&_Nm|38^G4dkC14N_0x#}|&){>?MpYAA# z5dAPn81bQuP2JGMeQ1L0!)Ev5vA!kVj=M6x*BE6ZoQmO}e$CgkKwPfgi# z+bK-DAMHsmmBXy~#~KCv6c7U|MbY&+cP7Df<@Di^nKZ^@A7)K7ZiK^7^{q2;MFub= z)wU+dS8g_K>7JhAx0HuYJ<-b{b)_RRHy`fXU#l#?caHL%4%oyDFGAhN>fq25=v(I=t4yz zytJ?NM>czHye614l zma0V!bc!RwHW)TW6OcZ?&W+Y7_39dl6#X~%Iy7CM#^VrdSgZp{X(xA1xmrv0-y!34>k_9vZwnWGFuReMCrBFD)Tqr`8T}S&XcllkNFU zO2ezPAj5M_TY;JLU`*Q_!;BWI^U}oh1c}l~8zYIS^{i(0Y{?5Xqcs|BM&W){4xavO@Y)_v+)$4pvNESmk$3;aa*O&t@0sI7M+-wMU#v+JrG(6(%GsoMLHLy~q+5@7YAp-yRHwo1xJJYblBABukqI6H3rx5S=9& zkvb}mk+J;Y!Z}ED7A>Q)PIaVJsVf_6y;m7%V?UVwHD(z~M;z$HpJMg1p%t7cI-7L0 zxr_v7D#q@LhRzasZvvnV|D;pGXt7e0dkd2wxFD|@J zNRymN_gJl*gxS>{nzl5{P65iF^*~rgWSj!i7!?McS-kup@HqO#s5(Apa#YXND5bLF#M@dpi zSD)hKnbE`smrYa5UC$8kCs|$NnjdYoCv4{F7Vg)~43)8v?5cgZC|;JTi88GmV{EjD z)oE$;CKp}9!CF$?G77n6>r|s^5SW2JYI#!_q!i*R!{{VBT5+vs7TT(&PT0mP7Hpm( z0cbS77Lz}`Agy(FZM*Lq+My2rg%YiF3Hc12P?wyICp-4->R+qc#m%0=C#fWBs&)0e zXZtjBI{T_&dLsD&PW^H(^henaib)h4N9I_|l_{DS7Jl;B&KvcvF=QXYHZ6= z5$tkQbvE4Qa4}iAGt|;i!WWb7Np6)DWIR&*ZNiYG+mBh}ZVkOFPHLu&oV4cK{1sVaZILoDllBml<^`)eG6QJY) zoe@haDOu|nrKVNwvlM0yNfS5qQS49u*&wiXED4ETZeGJPr|#@9Yxw8xklLyWuw$)J zYhIdX8f+K0mi2D!RuV+7O}Rw{)gM~%NYe#-I)4cZigt_)s(C5(c%W{>!bb6djvsqz z3A*0tq?uA)8QH-$w4r#x>#5rTe&pX-qu&YPl;I(hD*fMS%c(Hg7${b@`-+CbX%~9;9L-~d3Q@_Ca zO?cBmo{)XS!StfT_#?phn>6XF25rX*gRU4P$M93WFn<;9)V*L!3oUJG2>$Xv25~1F z66l|}fd~B_l!f{NEuEjQEXE96;9gx&)3FyP(N@lM*QBvt>N?*qvE??mVH#5Imr=mY z{nfJ7?^nBuIvBAhVlBmO8%c|bvadEiucEzPh-JyVZm+nq2Zbhw?UP$8`Bo%CTNcB| zNSP8xuyIBMNf=IE{qAQ+X-J>NXM(n&XIppaLd2Gpzw47CXW(ZI3j^I9s1rCdL1j?; zcS-z2Yhv1d;WF`W8vAKJF7pMv$c_Dnc^Drj!I#^o`k<&3NlA#VtvN%c8qLR$9yS#Q zWm%Kk?YB7L`GctEXcJp`1!8aK)F73Rho>u^^W;ljNX`sG_enUaW8F%00)1^nYlJ8P zU1jN)06GdFI-*>PbLBGbNEee;e)%Yg!ikN$%I&vs{;?%l7;}!1&@|8>klKPJraXm* zQl+8;ww?No(}qxYFkI9Iu#IkhTJh@9H=1jh{L^(~U20t~^ z_z=*tV*nAD2R8>fK}i0|X?cIi9~(4+fQhzVuK0;LN;GaofNL&~wM1`FFGH~zhrS@A zjlvyOt#$qMoPu=0BV5fn3rid}QH~1AF#0CBmksjTkZ}2a984ILtc;Mo*`a zIN?wbe7>3Y4hjC80E-7oL631cFK~MX0v*xfP#hsrOLbEXehn4XD!5!+~sS zh|cHs9B*zastRiLd2cOJ?pvO)B89s)c5l+^OzKwkj_5fS(T(9c=1Y|qlDW=8S_EhxknI)j< z0(31)A#O(<3FD4PQb`tI+ZZPFLeQBJXaolv2gf`B(7(|0$WB!10%H{_F(3VT>WS=3Sp)gleeh9F{BPU|;aPAMud zm0-3#(U~LNoqNL;DJ%yRo5q}rG-|}s$mO|a4%vOaB#)N}IVg%)>+@45$(;21F7N*= zUJXkLPcq*?WbMtdjcE1*g3zuVgau9f`Wmiuyl_XEVQg|$`gkt-Jy$2N7>@GoT~Xg9 z)*XKb3pl9(H%3!OWLf0+ho?D*ds?|LJF%V$6()*S_<9Ej7*&hi2AgI&IxCv7ytQi* z5U5Z2IYBerYJBj81cC2QM)e7rd4dP$@9W+6L+j2FKe*qx!%)M&xzU-`0Abrfw9YPT zYx+GgqSzZ}Y{}twC9!*l+g}R>wIz;3b4R_Wjhv!7id&L9{&tcezy-+Sbx^Ih#vPC z`ul{`wTP^D9#|DS6i>jW`b<21wVBdh1|d;n?@asc-oVVd6rnh@(45ms3AJ$H0q#uS zGEhe!J|PhMwoAV@R6PDq?nQd~OpQEX&pQ@Ue1XLL5$ZUA;96Z`Yggn%Q>Q!XLH)YV`bMkuceh*;j=hA36qExeI{tl^BY!i# z;Vs8rwcHMg+z0-Dy8*iiU4H(wP53{qvHy$4M$X2ezn;_*0j1%z@T~CbFw%i3it8fgFz?)*n*w)+ zG)TNm!kEsaDThgNGi}7k6JHW1TMZux66bK3Y;nk!iz03hDw?birqb5?AZ6cjbFU(<1Tm*Pf zpK-mIF(vX9Z${B41%kY0sOtdZEJZh7j9e3{W{NL$#Ch6q9t*Pv|50=eDpFBA(F$GQ zaoTZOWUEl88H$lnuhuv1eIerT#}Nm%VNne~Sm%M(m#RF(%hZqx!p2K%)=u_863 zt1iXA<(SjlrT{98TFPx<)4E<0FDyB?gBK$}6V*V;bwR_2?|wmILuj66#+2+hkdc`v zpihh!bqH(!FVw)J%m|&;XlQAgp*pj3kM-Kpm`b`KjJP6J3##mlbp({}ruCcFG|fu2 z%+L+2e+eB^tNER}uA*xOg7;c|h%Vzst+2$WIXspd+Qk2G&u2y@3{udyk~gl)TGDOg zxY=l@_>Ry@rPoqqyDG2xRlQlA{N2q~BeVDNTain?B!Df#hK_@TjJC61->8T^=M!J= z;cT?R_5&O~XkQA79l2vaJQa@cBHFc#cQN3OJ<`-vf^n8?IQ+=xw>$|sXI{I``-P+~oqDhpP5-qL%sd-;CuCmlZsnjwDU*#N8 zB55|pW0sA-ZARlr+pU^+a1$0TU2&T_E@im-BUK)zynSd~O`S!DuIR#@tCr(T z)4!gDtTGg2AR{@c`wcEK%h9JS*hgz=_YPhId6w;JEHPatYP%(iBbn_xT(b`K!RrB~ z#4I^+57ueVR8HMfcLF(l{GaSH1{*QGUlZA z$E#uM0@fx~UDirzurOL)dBB{Uwq8byO)>th$%i&~;U(PKbn9c>5pXFx3p^}18+9)5 zb>2L%IM!SHEROV8{lrE`O2>i?OA0?R9@c-#=h5rqdvE?jKL0UzaC#6wNf_tZL-~Aj zN#w-=oOF6TJMT~6cFgIOTlfs=%6L(Nvjby*&@G_^mk4Vi21pgtQhxn1t53g!v$l4GI@CoyLE?_Yu`ZR*CzqCwC zSKlHX6a(vF?L%#~K+wx>wFRXQ_XoH`grj!#KwOh}mZ5YB?|Lyy=@AWd3h1fzPlR+4 zbBte;*!RS);NN&@d8~|0PDW57eOfkFoko&S%}`sT~;;3@~H(OCo-sCa@Pwju?>Oqj|D|+7l(kN4EILl;0() z9PT6DH&l;~h+uqUb`_0|D1(3r&4{+CO*XJhc zx*mPk>*vO=eGa<&xQ@B;Be)g^y%zHLYWyXBH+@IxZg-;|!@_Q+!nal+|MJmZvJV?J zYw71z(VjAi&V->WdK5i_FWkLAnY=WEXP z|Fa7J&&gJa2BZ&;y61PUTS=l?k+myF9KEJnwYIe;AJT*)4K-hiL{w3#%|sZ*PlRUE zlit6~W?J%JLJaK$0t2RTcsf_REpP_^;?7rpVQFDmtSLwkBIN(Vf+(Q{T|>zKwl{NY z=H{-FsP*IT_PqA~8FPL6UUzQ!9lFc&iHe*Ljqol~Ngx$!prAmfheL&|6GMqALfRrJ zHBcPcN0RX1-9hXLQnLRQPl>!UQtaQBDphfuITx(U3TG`@q_C0}&CwuyLD!s6vx^9eUUoI zCkR@QpMUoyJdQYouIjoys-WMRKO{U>ju%z#Ev#k1Q~e}T#Hc$b+)z$7x`H1e>Vq6r zS`}%WSbv>>#8Ev|$Vb{e>JB~ZjnvSVHCskla}$9iR~e+U?$OxtdixwA90hj73f5xG zHwpndbqeHg0!fOp#JX)DgD(>X&}gHV}_Ba zmZdfD6pOg*cYV#)V@4r9_*oN6LA4eH5YK`j;cvZu^mdH47`SWGtnW z7ey*G61y309KKYZfA}hAIvHF;hqB1V?dX*gfoj}_j61SgMaGiQK!c7SL9^KNcSEi` ztX@P278@I;JgZcu5{;k?vJP1h3))gkDYTtc`w5W#>s`@hBg6h%;JY@2E7F)^PLH%& zELaNog{MnAs)1sug2j~7%Xp5W2}>Ht$jgwdnJ8R|4J{IM$>Gf+buwKxsWBYY7?Kqs zDm*_SR}w&X<_yC$q1jscV0BF0id<J(hMp5cmgI+Wtgku zWJ*_IAx(ib-%7;8%6g4@a5dyx|2wSk%z%lhmMjc41i9rzLjdwx)qojCPo_{i52{~x z<#1s@f(J)ANVpPB97c)u{vvvi(*RYj(hGlGUAd9>3+=3-UCy($cWeMX)ZLu zkGbAcIht8`eHmvD_;(gK_Cu+W3MQC0(}0RzAh$;wY2=u}6r>PPFh zA}y7F!aPeU)t#K_Abm>lhJON61Lv)n5uO>wIz-}|X$l7$iLJF18j+9Dj_>4+}<=yA^czpgiX6r_CJL|0HMA+X= zNn9-I)*>YvUHVm=3sqTvg=wOz9Lt#r*0MWNCPl86a3WNn2Ks>64iY%9arPYK@XR0N zCYNNN`t0^*re+1|$9p);i2-PV3wfC$6Rg+;G|Z5~cB)5XN}1{tC8}#w81-sc*`_5g zoIV`-hYA&k5~ns=83=ryqHd7Jo0{>$s@i$@#!$%H8OZ13U43SOiySIP0?M!~08Uk@ z<|C2EBNe`7(Z@BbGb-A2HB0KX{8El^CIwND zk}~5)QXxCP8apqU7Mt)Gsw3;x@en+P-n#6U@v- zhnLzT%+%ZIkKX7SObIv5D!86_`0F8pbiLOfZ{j7|rCqA-IW^K!!p`x~x5G?@6~vLNO+3&+5;#y}tQ zo$wp(^$|PudSY_fVzfHmtVief*{mTMsQ+wAfEuc5+t#{B*u7%3^L2MfI#0BFW;>hj{xalO5^E*l3)A7OyWh!V zc;?cQ%q~v_xh#Nka<-KMJrE!2{iDMI^A;iIZHey=r}XRX=NW0_gW@Y%QYD$u$$1WT zn!P|@z@Ci+CGZr^*v=-7Go|;m-Eke~Oz>0cJ{;8|Sf2%-#4OuYLW6 zUhGzoDaGG=JO4Z}`;f|0T_r zCJVEQt$7?rKH|f#w|syi3HQA&++?YxFGLeOHxy+9wB$790f{fjZfNKSLZh7zIAQV8 zgbKy9!!hs@REL0T&>Q_c(Z(g~?;3OqM&??*JPm_~JfcFCJWq>l93K;lIJV%9E{t_%KS|Jl3YaZC#+kFJV1e(2q+c)TF= zGb_W79&RMCGneZGJ|^HXJbz`^diqn*%MPcBe^>{zUCK||70Fkqa9Hj9Bu{3fRUQ*P zh`iG?+X#{EfVNC4uauO$qQEsw&EwqM;wP%6Qv$Y#hflofO@6L6PWQ#B_c7BXrxiL6TAwC}?j5gc=7P)~M<)Ll96ql%fP%>O{`q!cYiuT_M zv8&kGJj3IZyU9oYT)jLcJ;?SEQmsoZz4)QBQ+k?UZbQn7e@&@(Xy*au6{Sn-MidjD zAT!M^nWcS*eWrh|v@CKAT#)EiqM@~e&UK)0WZSsJ^zEVKh;2)IS(2ciF&ZN$WI?x* z9&o=>9mCB}ASa0B4RSf*)Y9|{mle?ynqI{>Uz*;<|4DLv6p6mPd=w$^nSRX~`7FPQ zQ)g1`PaMekWcI*qBl&dRJnnSjIobm(v(tU4026SfpC646oD$l@CG>9U>H`|Uo#7jX8FXAENEnEYaV#wdHn1}bJT zaCeprTGlZdr7?*%F?M^&hKrUlT(3SDHOyjFFDV$Vm_=HA)MDqci3fb5b}Oe3yl?}2 zkR#M2Aaq>OH(bBfgJEYqH*S;bH=^q+d&5ffgmUgs_TEfE0Mrmb@_^djfSr3$kIi&~ zU3&&^M&D;@1Ms}?w^cp*vYH(xHv?~0+zth*6n%65t)|%D+b}Jx%^%A3?f)X_=V9wd zrao(`XE4vUMzVaux_IpJ3TV|&hB4R17vZAU7=_t$%50ez0V;W-k0s^nnLo$c3lz3V z^H+V(6jT6gaDwjKTCSE4v(UEt3mgrlwK6idMih@cB{wZ)&Wr-8^l6ts=SzL3<|tQm z+ZFI5<1HGQo94S~iO5DreE84*>5o5Yvm^8N1h*R(@rCz#p=q*Qx3}CQ7W;q$e9S)| z`@pj)qaW_>8@%@*iy}`?AmR^Q@VXu$LJxfB-moj(wtqdx{P6pY`%T@U2j(n3LBvm{ zH(&jP^(~U3Ks_|wFD*{eaWLb)TuNt0?vZ0F%Uy?t3IRE<>8dSoT(3+l(yqM5H%AJg z6I-t+D8^1!mR3zqWYPK1q>s%T?87uSD!`l>>IAFN1&LuzT8-cz6sJvuC~b{-g>6gB zEym&s!6kdx1(&nxnohLAyfRqvMBA!P#3fsV&_Up6P~Px2x=Q)m#6v4q(j!u_1nvujDxkS+@I7ZSMQVd$IRsy~ zh-fs3+q^%7Zx;yZzqA0~BwW;phoR?aoJR_nmcueRp4wawOz$U7^CzrG7grmi`xlv{*1 zILg?1&#Pts)lS&~ttElaN^`FHjFnDZ&pqQd{j&^TYZS2k0#aNS%+7JMy#jgB_3Z17 z;h0YFi;%GO?<7cFCEPlDaeHO_%tlUdbtCWwI(Xs{&|~zILwG>e(4$MB<4!;nC*XH& zsr&i$8_0dLeL|;?|4MVRNMiW|soF~Pw5hO!_cPynfb68f#qRjo^Nc}t*jpH*`b~lbjC|0rKQCn&y?9kdm1d>j0aL`0Xqdt9^3HYYh<-g_S@Q_$E-~O z)~mPw;~e~?^?{oy_oG)lQ_{l$Wfpyi!q;-$=$7yn@zyZKU?uZ85aSFfwh;1r!y5Ur z@bde=jneC?|2m2MgbP6ZjMM)wLmW{zLmSus5&9x#Xl!Zo!zTR~@I5uzR{keqVC37| zm&5CiEoBK`u;^S1MED6ba)Jm1i7b|^NY=}^O`Wu+(?;Ss-kg0&WWd}Y{7yL2G#>gv zKT_6_n|ay&_teGY<~2XR-!G5AV>B^F+r0wN-wT`?9wu`_X@Qu~@wX!Mtd~{U78x0= zwzr#eY%yeK$)=>*d@u*o} z-u=FM5#rRo&XVTpB2=D}&8Fn&NbA)L}w7`+BlIE#v{^la~B5Pal{%LYX;-yNP+5sN*# z-WI7%c4W(;QGW{)&1Sa`2NGD}_3 zLskisE7v1)n7!B>KCv5u8qwwt!6AkkCn5=h?c5~f$V2fdf2^-)r<~>soU(E}Lt!g@ znE`bePky&o>3|S$clO4+-Yu)(HMs)09B}15M7w{v&BbI+zX_cYg%eX;USWz?-52P8 zeJxafi2e}%;pH4KfPk3)&%?$4@45V6CX4Ej9w>*He&n5FHf+cc(IAO{P++p`5Yd__ zu!@ABVY);l2-v2fv7e{0zub+}lSGzPyeKui)!ow9(x-ydO-Qv?4e2$_&xY6QThG3& z*WWW{e>?x}%=3A=9p!y@Uem*WzaRVt%8l?Bn~VFTvWwn%g3!Yd#l4Ey6ZLQ;rW=qZ z@~H6G#vGb(PzRw$zgCaPR*zb!D&*w*JU>EerOU)>J*;dXC5ilW#d zZ?Zw3v%u1e4ydR5AJ;pAzy*p89 zR<-JRn9X4_YB^I_m$lO_PjfA`YlBRvY>$%w9p{+SJvwad;_~tBEvbvYiw~MVw)R#% z0k^|rHGXxuv=~rA>X3#x&2x1#Ssl`QsYpd8JmT!MGqO+r^z;d{N%!`g za#~}LCmw5O&V-6>g({ae_XnC!Byg6p{O$5t-oeX}US1#$56}(nEGp z*2WIjq>E>q1*}uL_I5jq_{67TAy{kKiHGI6ciYR~Py@Vf!w4U;6DQOMVKC;|a`u!F zB^OP(xU;y*KIrs-t#V3XzF>H^s8OjZwo^-HYp?z8_AUsq&xqYerd3+o%hYz&>wntJ z0I2Cqkkqw&g1vuVnc_^6uY%FCMCoC+n@tmqFls5lUSyx!lkv2qgc-`ioA5#wlNji= zm4)saHzlg1EZss$=G;U89008e?@E6DvXi`WMPciD<*Zs{<7`*^wy}{SrDWw^Q_#eZ z@sLwgZABw-`DH0g94E;FbOZhH%dcD-yF3(StYa71Pb!6^z%##$W4qbg{R4m$p zlUM)hDnQYjb-a1$(%LC_ROtxRw)sVLQHlf~Vd-ZJSrT1T%+-Cdjx9}(o_emWAm9A7 z#&fBwIoaZA7H{4ZhIrffn6f#Vu%`V^gc*}6<6n#fAlj;IeP$~{jtDC^LHE!Ly* zO4K&<&aRWy>>s-p;j~uCQ3ZKryZlV#^p=&`26bswxgm0`S1yAu76E<(?c-60rX&(y>X+aKfd$`$Vrzh!O7o#!Ocd3Nn^i<%ntwIrrT$?2k%mx|gjSkL|01nYmpe8F2Nt5(yx@g@2YAox%q_=t$~QO*4Fi?XgasPd2j#NQ&L z@-N*mf9L?{Z=~7s8f#2pdx1H>u-NOv|16!D8P{Ou?dI^+;kEXn*^7v_s=Q>@Gzhx; z+SggEv+he!ZvImHQxONoVG-VPKZr?yhEyBdk!p5sk=NdkhO9Jj(PVs*+mNbiaNNRu z&|DoAy0HAi;z#!H&GzLK>R*5!LW9?JOe9I}iA?s*)R>--c7~1jFPLB4A8hPuKx*F= z`D{ahSz3tPz#c;T)K(Na#|HCCt#IXb0v^Pu!8VG6-KHJ*5r*t<)-g->0$$2a+==Mt z3wcnB*QcG(Yq$)hc-eL8=l}XY*9W3HIU!X7S{fDQ7g9XdF%?Cz^cAvKiYa^5>)*fY z1z@%-JM0;xoAlLGkC9Lc9{*6ndehEeAZiCweJgg_X^^PfTV_OKY?|6~u6_*|Eg-Ti zNwutO?|c94kX~#zm875_>IHGKH$*$jjEUm6cp?6(-2LYf7lz&6&}$Pl*z~k!EXG8N z4#j5Vt0B*P`7b8RNm-xjYx%ZdtNzD6(l*~=IP;_D${y1~xfRYXW zG=h)`kKW;-!!{utw}LqHbdJrCx+4*f#)JS2A8OK4GJ{Au(z*$3l6Ae6a>m&EM#%SSk_qq zZYWdY#r1lY#pB1OD2rjQE&wAJBi4@cVJs?N&RSBH`=LEbiCbRq>^X6(0;ME`UsduO z;YWa93t*m}rjU-li5eO!gV}v`QL7fj9-Fk&u&>CT%?-hQ*@_h}80lep12{FBEOQN@lg0^g8^*EgW7A+=4jzTt=^jhsV% z3o;8k!3~exwFiU-UdtVe3fzVCa0=>5Fkm>I6Mf$hWAz@_5d;dMGqX;(Wey>ktRE%< zMs1Q2YZRi&DD_*G%M@OhOUxn-f!bk<)PnG-9>yu=<%fu?I1zKy0^}ia%#5)A{*XLo zCE_V2!qY+`zr%y7q<#X~mb~<d><%vp*)5vyFdx;2eb$P2GpyDT~;f~h>*C`DY zmjtC5)T0^TdkS={@ZB)Su~?Iv05iR7k!+4r;->1s;!3ekyXwpU6zC*x8yE!b(NzR1mmDIHl?up6(!VOk?W zuJBWiibuROnMi)0iD2)6CRYdrz*|UB+L}Quiwilf$q)!#28lr#>7h)JFKJdwB+oAS z%W#8+GLM?r4^DoD_~VuP^i2CB&X4+wsJmcC{ssFh2Gm1+4*Qzy`FW@NBXXgeBgXq1 z`oG>uKUD&TC4c%P4y6ApcJ#jzY=Y)arlz((ftmlMT`Ebp_C^-{!H$yAfKOXwr7P4x zs}*FGO%Q1cL!}@~ROXc|A=PQKO|tAZFP5iDL9T;(7FXW1_yb-bJUsFE1MtU3n3G*Q z$K0&2^bam3$0n~ojwh#0zu(@U`G0lA)gfaH2;p3lApi#Sc%rKynN*?-6<^Xq+R0?( zGR_A!5ROV2T0`1tCv)h!!kZn zPFHHD;@yuQ`E)T?y5_Qz3VsIdIy#Z0t>~HqNSIVhv%$AY2SOs0D$+=s z{F#fD%cH^$x{{R&(mkd+$EoF(Xm46TW12+ZXSP#p@39~|3s9bVMPa|mGX=Hvj_v+c ztH_Lyu`+*!sPHb#H<}Gub9?;79AsE+b2*{COP}#jUT3b%fDT-almx!gWsz>#Rn;}p zm>Sm(ZsP9J9=C+vT@S^#3#3iMswq}ww)c8$=U#FGX6qbdNz<0s6`pYmch}D8MCjFh zOH|w08*;x((`5QMIISt&5nAe`Z)}-&TK=HBgtL&$q=U3W_cqX`0@QX{j63i&H$oV3 z=hmXWbd1`N#<7M4s(eDs(E{jB)$gpk2(k`~0PBYb z845+aKydT@!4JTMoWw~9djfs#tN*Hl>2kC+ddHD!|X9zBKVK~P`gA<~eAphj@6{auL07Lz@ zW`A%4iDnPbQPwyCyv3%PCI>m?89PGi)e=dm2LaR?so0vgZ(V}j{l8^%!tBaL5*4d? zQFPgJ+K+!iTR5CjvT%gfQwdX4xAC-qDdzg^FD>O&7ngWV>Ha8p0ZQ$0S5KJXKK z!lS;C6Tgw)y5)cWiD7AO{_;!xvX}U7h5Afp@fY$JtMJzs`F1b<_744$5%C8@2mV}O znjflpgfD;i7I^blIhipKJL@HBr5EItmPc%VR==nV1|KTIe2d8C4|f~X!+(SOy8nC*E1nXxH!lQvg}Xsp5%rad>#19bW5y@LPO`fTT08}t1`7}NgLXr}-3 z%;0}ppZ_OgydZ$Wn+$&S!x-mUdbU_fN>}`HO_vn{QBuf~VmS+28f>@3xxlpMi96A6 z2nEIO6DO<{79eOQO4FF!2%rAreLUIR`#%_C!YB0~#yFT3oE3Z>SX#KSyr#%G@c41_ zDq$lrF%s(>5pyNo?*geVw?%wS!JkI8&ebeJNU4Re{+3R08NQ;Z)b9cw*ov~Grm07* zq3_7lCGzq2Gpgno%6q!Ht+i?F!$lq{v@I`5U~_Fqpl2z%9IK@=Pq7>Qg$l~1>8vur1y+48<7uu zgIeu(?4!ODj^8{+nc`XJN68sDJcy&NVZbGEN+xT8F~&w6>Hh&48&+D?xjtlt6EblT z8GB+x$xcLsc5+LCQ5sL`3{}7&UD>BwzB9)GY;c-ge$47)cUCzJP*|iWj-lpM6M}(G zZxVI{OPSt083`j5Z-vCC2JyXrs*|`q#-Ar@qxu?UO3;SB{~x~2F*vfn(blnTb26FO zwry)-+fF9N1RdK+$F^-J6WcZ>&dvY5Z=HM2t#i(|uIj3;{?PU7z1LpPv#9%44*<~J z1fO)bka1(9as5hGDZiY4JC8?c@2ta5!8T2OT}TBeoFCS3LxXE5?RMUy_suq@y=Pcm zbSpYeAFM>2mDFsQT|rf61h7Rk?w=;xp&QRkOBt&*k z{{Q=JN#@L>(_j6zBc}f~8vSp7PX8%|(uDURIB?9FmC`=3d#JI&#m7H;W;LE42JmL??%LeETCeccaPxiTGdq0sznh%kaNhF&TWXIwTl9t~(CEQIy;y?k{9Ql8WhSx()e-NgpC^fEeqz?}MBE{k>koj2%=#slcX5TB+smvJF+ zWrRBgs~CO+^@NsP>=2Y==IHs0XfK!b4)Z6;IDCj#+&vb2A_Zp09|dQwLWhDJET@FN zNmtPh-ADaA2OUDDYK&VoOrNyAa?_mgA}#VsZOcU|=MdJjqSm=-e2)T{Zs{E)T;0m+ z?*f&FP6Cw&lW_KNz3P_SMXloOKglK=na|^aJUx6qS@%tlPhOk!osz%24@`K#8j{?{ zCg?-lp^vpM6MBh4{z8?6*#mmL3q7kd8};R#3i|L=q-+4CZPqz%g7B30E*`^=Q;o9q z&E{w)%OzbbW&=>WFGHeaX^V5UCVd8Q?qUfAJEW76XQe>}H`n}hZCjGx`?QmP_#h6g zDUUMoCGkU8nnE{th@0Qq0w%R1_qK3iEum6VQkRiJNy*tQ=p+Kjv*wH<8c;8#fZohE zoG=Wd!|UD_w8ZXOy7)7DvW<&|EGL@0>zFKa29;3*q3GJ&31B0xRR^wmL0%p|icHbS zM@jiGV=S4-EB3Yu0eVpFMx2pY`t3yd+P``9jvUC+=7g$$`KT%sro9;_4;gXyEOIKk zSR0e*rU@ALM_EVgIDE^BqzGN&Hmky4E|!@8o;&8Jq+zD2z*Jf*WGrW=8F!`*kLiLZ zCu0601bhyO>D1$hPg7N`QK%o}dM;bvtJp4YWc{;dpV&Sw%)d$0gR$d|1omE2F`H_b zT97m-y_IY_cCmx8?~e4x59eCahQ!Rawd`2VN%xQ*=yfLwc`Ke&HyyBSu$b%IAF12y zqeqJ>Q!OzA%XVo(eIbc-x~pCryqSPYIHS^u7YL!Er@>q{=x)Wx2aC68V(Ik_iqeJ# z0#~T`3mfOjr|@D&Dt6I3FLZX1?NeN-$-WH6{1b||q0~s5wa(88F`l96kqi3AG((Viv zA0l2&Nt&w7F@OTRO&cwpw$6mC~h$<9qsl=oN+d9a#$MEweK$g~SC@V=N&^u}H zyM5LPz*9lk*@8OOG%jNJ(8Nj=oh=EMd==*nI`=`;zpn(apUX35otfug&!=eRh^Dw3Zevf`H!#M}p%!VJC&^ z{pg^s^{xK8AFmpct9ESNPI`$G|LJy#ql4zCSoBVI>6DCF4@M1H@%!QOrdcd7XOI{UbCAB~?0b>c`!kj`r8hjh zh*OW_X7glA2}v?90o${dRrk9KS3F=m(jP}}R4yrw!P0Bw{TvQ#Ax_fpb-^xw*9~22 zA^BwgHhn?m*QIn?8DdHWwK+XhW(syJi$VhkCZ_(;2!#elEK5dKq*E5NszejKhpkwD zJq27`{5pm0v+ z4bVrdaf0x!Tl6I~4JnOL=p;6e_%8%8D8PqO1fU8 z%QAqQh86aRMZ1`}@UZI~QkeC_5ZW!*oWdasmb{W8|8`t`2l{)ClHXo9(8UC>);M6` zOKLDa1*@3z zj#b|f@9C3J*a=VJNn?-zT5}tAO^B?Aa9ZWjJYM-rH*`Ke4$f0a+%eIof@WXLl$v1u zdolDa86}z${dyR`SW)Ds!R$}FR;Kyy?v?HVVAxnnaZDSECI@Z-BzRLQk)$cq`MT{r zljU~>wGbD9`L}+fD=H`nLFd9sbX?Hk9mJ83SmHam?b{ht2>lz0^9 zaX5d%sl_DO%jqbl_a0n08&GxjHq0waT@Ea*L?T>aQt?t2dYjqR+x3p<1PxTLUDS?t zYfOG9e1=orAAhIhB7e#kv`HP3Q78XmER%LimESau~ex zJynxr%O94g!hLtx4EKYz1O_`XBBol;I#=YWj#eSIE&>L>hB4=_Cj)SpeuSD9K4yzpn9IOD41J>`v5WNY3SX;e7 zl>xaT1r@2BE828!eZ3PZ>ff2b+k`?+Y-$4dBsufZE%!(;N~uZLa;J?`;Jm7e29Nx_ zyz&Mt`roiw_Bi4Tc@oRQ-;8a++Ll})h6zhE;-^6RRj8+S1EKa4@?2HAsC%WCRL_#I z<2}H)r+&@2#yX(A^2W_M)WP9CBGMsqhE@YwkL==zmX)~RT&W8@E?+TVdZxg^5A_yL zUFPN@0Pu39j7yv{g_}NFazk%)$}Lmx&|Tl2F+DSga7b}HVdxcl_}I|E4(oV^+g2Dn zWVwlJbv;{E(EVyT?l?~hF^N_tY7G#!yk};Gx5F;XFhqbZCFEa}3Nr+2PPNGBnU`ld z`D$y$=OlaCtk)aR<`gr4xC4@Bu_qmFDJG@|mb71;+-zZ-^?S83pQ*R<(%|@jiX|)j zFMBJ;&VM=pl*i?Zwzp!-icJKTe#wVKd61KP7!?@;^6)V;6ZH)>3PZ&fN=r(sbH0h( zf(HV^&NgdTc7CC6a&!tiHH!Un$$Oeegn2S`W!|B}b?cHdmMX9T+nkSC@`w$;u+}2A zgN|ikQ07rF@K8XpX0jSkwkX0OD#Jl&PKZu_joSx~yU+TDAO7u^A?gVro}jj;SeRQn zKI61xSeRu}QQ}Of6hC>G<*2y}zNLK;7@1S}Q5`Q0jiG&>femp(Jmw1u~?CI zP4MkWP~Fm9viZ_$2UoB)YXV7>dd#ygjV!XZ$Kg6!k$!SQ`>|ppp$2}9Wad(E{%(kq z?}1r9hc$!Opi26qW_EQgb%*pB-eu{%Z>xzNBU$-rX?1c3DrH?*jgey-$L|>LP*_cn za}dW5nS&YE1?RI$=G(};7E>*&@imgU@T9(5PWGnfHF2O9%iTc}|8SMi;ya$n9;A)r zl3)7lNKM1Hh%kLiC{m~i%U0xFIULE|%llyt*c<)Exmnh$xq|nbjy=|A1KYUCZ zbu@a8$k78PFQjFZL|NzV3r1j+l)RQqWqP`HUn5-3F6qGo&IFW~!TN<57r8byXQjDu zpqJlqR8pnYwbyWntIZ>S<`#i=v0{o|uQ6h?oQlgzf#Av*@@w{(ZswkH|*d9 z-s@{mvmIZuoD!fvC*T2U=?-oQ4tN9;{K+CCDNb@bmQlVgNKcc>@ri!;k@hi&RW}J+ zLY;D~`D+n$1``tG9ZsPrDNw6@WP?yt^7-jAUUgzg^a9La4PaTv~f1}Ipj=Zw3O-zu77cj-GOo#`$4qbFbf0cIB zv;lvGx&g=4OQd?lguL{LPv^V|irc&7$@MS<@v=(GNxC0D(zWZw=3!HEU+cL#|MZ{_ zXPO<}+dOVE*BhO@WAPC;d1h?ZHlH-0KL|Rbf9+t($4TL@?@6|UuONb%J@=Sh={W}} zcc5&W&?HU^A`c(M^PL-yHoYPQzhA{eJfpoJNu0DI)<6I7kIuLn%D(rU~yo zO?j|jgF2yK!IQLfI=l3CXCTQJEir_-c|`$0#1ITHP`um##Z~28M8oH4Z+meT`^^z$ zBj23M_P^`iQ}5HQN7>#}J`aBtc%?+G2PoJ7g+z*CB*LU*z=`JRU_|UHYcmw0DfMkB zOSNq5V+{l`+nF@~z{aG=kMCBKEj`R0j{b>=Y&~A0^d~98t5y(Aqs1~Hm-(fkB|fA4 zTP>g%`{s0TU8nCyaxzSH23DkLyoG~GkS2WuCK1`}gif<${(^|G%e9k!geFCV{H8e@ zhu(U{ju{=tS&P>6B-^^~_#{;GF&4gO#@V;p2#iUXjW#kawcTzcM>rw$$RJD5( zFcOYcAjc!L+Lni~lsKjFo>d>VvT(Y>V0UexJM3dvZY0{}CWO+3^L&*(8JSMP_KB$>_s+v}Z)>&(Tp&m^E z!qmx_pq(Z{MI1d4XvJBnjd^F9uzzlNU?Gecyi1BxXx?`XZyzIKrne|Z)xP>neIA^S zJBy2QR`0>`omYM!Qb`W+8#<})8259GPmD{ys%k_q7#}y1KW%9_)o|J2yN&SJms6-+ zG{~wC?5Aa&iQ-tD_(DNAl#=Z_BVQZnj8z(Cif)n&^(Iba=)D|_g50Dx1m@!|?-SVn zMIwRULotHUAOD3!DsBIRL>lGsHoqyz;9lk9*h>c- zEtmKGF8df>Q9jW-4I#Prg4+(s9YaXLoqoTIw7)<#_fI*7WPz+Qr1$B+_6T}2E_42A zF&++;GzkK_KD~dCOT-A8+ln7G0lCU;IEx?N`kh?G2gW8*PFc;pTvgnjOdT7MaA8Hg zmBZqn_q|!6jT-qvYJOcfc^Kgn7`aUSo<7b>wN5`5+8W4tO$P6Edfm;8M$m_l_dj&j z$y02Zs3E)9f6&NQlp_Kw#fTbDd@ibN zaI>__VrYQVBpaJ(jIpEFyisSGQ|HOGcCI+7UXzGojKjjqQv_1ropNSmVo268L^4vV~8kSg9X7FV^(5Z7W@N6ytmWj0 zNb}*c?r7IhW4m-<#SOg7(BKhFsDKl!Rf_rke;~=Y6S~3?Va&bwRKQ>%%ntM9c`G<9 zGOq>}^nj37FeGyf_3IkL?;m{M=L;-EWd}@OK={5U+LV`i-7^GzQdq!QiRYgajY3l^ z!@#X{Ij+u=HD!--q;|onjU*JyX|c4EW@sql5*;b*Qg)^!bH?iF)sr{_A>FS7c|3dd zu3sj#Bqv`n!gKR<|IQ}}^v3s@4f(9yW4)(*QCl2VC-mu@eX|2<31_WP3 zFGw@{%CD1`93dWAF!NDuQvELQ!H9OApf~Xn?~uXzdwU{4bD40$<(TvBGUddRsH11{ zx2&fi(DEffmLUR~;>M2XO%s?h=xyQ^vh)_n;TJA{H(;sNk;2bl(SN1^v%}ZS89(rP zeLl&f)^36bhy$vxRAI#T8;R}xrZ_$Y#&wt~m`Qm(*a(>+NA}ik9OudJn9!7+NV9@O zO+M=<*oeyyfWxZIF6c?K;`~0B4}PX|ni>A-s$XU!v$Qq%!|`3W-E66w#MNAn!B|de zgqtX_O>3Ox)DRZbUXiZ?zY+EJy03(3g#L4WFxxutplw&GVTscq+xX@r)Dpd|iK9^A zuvF%;eeiX#-;=t5@2&lo+~)%nNht8X!*f#CAKlR>jYW<ScSiCErf?RxoB6=82 zC#mP#kx4zh>8EXGMk~SwKgkb2F120SA*D3BJOo!ff}|16J}Z+B<`ZHJtv; zcjR}pCF>zaky(*nCuba|5QVa1Eq$iU%!PF$6Ow4eLGJg%M$ie4LenN*T@*bNO>Wz@ zu;r_8&PDxqX|Zwhr%N1`c3eF6&X=@y^2@Ojr8;QD!`8f%z7TY2zJzoI4}YWs-IOUX zow8-WD9M$&U|m*-7rW@knV*+YivOY{``;KpWF-Fpn5g%6K{D-yyMJxkNdADDuN+!G zrgfGaoOd>E;@SruvsB`3;iCNFi?RGx5i?I3jb!j*pq;|~MM+NlLrD@~0?3a{!>wI( zkVK4<30c3eB&fn&Ph2ER7jLBg9fWZ?t`aZN0-l=lDH+;!nSy2`T(pmlwXMoo9;c>m zb1cZM(%D%8C=9e138!lDzq8V`f~|17L7LaNW+oC=TyptE*Swer?NfO-ExaMcr6c~K zkqU8%TuSjwN~=yW>;1*0=NO|b8@I_Usyn5bz%mup_ZGt#_X0hsA`AP$$H|6Y+7?WW z^VGMq+<3V}9)1G6F2uYye3}#)xyPhl)p>)j$M1l?U%sh3t@+6eiVvd=*G54&69)8k zsVfMkxrNnn+o$u&{%p&hj<6`rvI)cV4$LA35fq9JX_hzBxoJQ=9_Inj=v?HvFap)M z^ND6I#*C^5!x{eFABBJDzj2iVjPI3#v9WH+C$M&TU`QMF^T-_IdCW`YXSB+&|M>!`>c33d7SHd3XJSsx4SYfx=o2+{7vF&R zv9r4kC-lnX3LL05tGa@k4rs57Gs;%En8|tUijb?`LY>6-&`ftAbi?ixW^0Dojf~<9 z6SbHe`NZxCy|lAW=1zXo z4rOYB1d9;s2<+bqGx}Oc?thq->rZGf>5>DBOPUtujd04Z9kZ=%S@Z(Pe~r&vQfp>y zyPzR01%c~?@99Di$Z>LuU$H?J$Fa}rw<7tH?H|K$-@3qGVKzwL(7-P)7DB1**sn`zQ0D4T;u?DkGF9vM zE3awi6G6{8ipX#PrxCxP0D+(Y$=HQwZ+OH!O1J__{9wo;xUttFiODF9S-g&^Et{>s zWFE)0&JU-OU3^?iCaNt;iSR6P7SH$_CQ-5QQp7zMQl9|qDJCiyc9yV5){t5d@(ga~ z4f^%E^L1HwNO87A_U*Ub7f6Wy-(QquZ*_P0_8rA~8-qySmf@rB%Xvy4au>*5ZTC+} z?_S9Bo)4>^$7H3Fe$iKVFOvjMz}ia7gPed$0K*Me8`eilUU$+3m)wT;O1wfs*zm=C z!ws1phg?SLs-Y4O@Z^Q1!86?}^|ZdE3&Ry&Wqew>IyZow2E=Qv0%VyHW+kceqI8gh zgn~K^&RjL@Ijc#5b$EmOD2$~KR13){jQogN7E-FpfFs> zX*7XrLP76P>_uQd$wH5s6ds|VX}&IaP_0xTy0o~^DP9!5fmp39B;MTAghVqIx7j+K?}z(+Y<0hN&{S-}IA=~7q=3J|tK(0krZ^1D0^cf&CZuS5FJ z(9LGT%GNQYM%(Pgx0rf1jg`t3*z5AUU;e#yhwg-xRBNPF84KxK&P_m@W-|^D+X?XF zK?3V8i~7UAC(uZX=M5qkX~k^*;Xlp{Cgt1NnnB+ldAIe23Ctx0gbSON=sl#*QS6;6 z^!$WBcOXm}2}k5XS}BkLDbYFT5bqe-De#5kNG8qXfE=z@jTFKWKM>qe@O{kjZof;# z$(@}Ba9mf2+nlMZK|CG+$*&U6UvGh1zsrbtqd$|tP12z*6jtYSNucihyMG>neK<}V z5e*qnCKIyeU7Q47I%edC&D--V9Z{K7(icLAAVa=t&aVCR$+8C-8SwwB6#B{J#c0@$dq$m0z`AUAF(V zS|;S+pls^kWa|9Y`2AP9^Z#O#)+?Iu%WW1gWV8=HV{ zf_vaAIJ4=NQpVGyK#8uFcn8QnOc>CxZ-QuyQUpfe$+iTSdUd3v?KZ;8rs&CfBcTKt za$h_eqNDJ~somu4JY6h;kJtNVxyHkXMs?ZGh3lJB{cO*e@Z@V5eyeR*I0f26sX^Wq z2~Xa^VHHnpSh}N=L0*wA7R>Aea$*HQuZ`uQB+Jt9(RGJWHp3QPT4~bHEaXlvT=qm| zM2_TJFgsJ#I>tVz=<9wD)Z*U}*xYG^mw zEB;x1bV&m~0km716B!tGNxt-nhh&zkg_bEmk@Vk1t)l5NMJM;QIUM8cIvAVOLAG%E zhXt5DgL)l$UOXmCrgOUV{aJImN@E*N!89_ZA#7( zXa5x zW9fq%v<&Nig3YVcn(D&Ve6wGV@no!|6Q_nfgex|I|R zqCHj(c>lC-#e+6Zl#eFsX7EAs%OG!P^HvF2s#%6FjV1a<@Ox{C;*UhRO9?gFWtOi` zYgm0_K=q!g9u;HQ#T*2e0iXIinMR_H28v_1^0v*97660V!`VL3E%U(1#zB-fPAEleFqR}5@& z@lGkmBabiFMbKi;+RfrMm{x}E8^vy-_-nz{2TzZb9M4rXgqsAJ#E`}YGT?O-KYK;Z ztd?2C_dq-S(3EwbqQ}PEGIhh6j*(!ao-R%55Gb|WHoupbZQ?Qi-Rm3V*BuE3OETAH znAEt3=p9vdYqo?`ou&p;CXVac^oGN!u0BgT1n;o$F4+9&ejLXgdI;()-22?1UW0gn zR|iD}ZYXc{WI-I~(pS$(>|-*kk`Ny?9*PyVD;z@Q?_SQ(WHZ4@42f!a4Z+5UZL*)lK74<)a^yQ>DtkB_ z)#h;yc1MCDV`mMF)d{Bm+8I6X5DU z?pdKkkv*5Yvtk<3~Z?!wVbnBFi|W=%TgA`@Q*TJ*h-7D!Tay0`ib_ipPQ7)B8-T{>(mt?$ z(oXXFVNe~-!+wrF#)-{W?(z$aT90Ou4@2>ZJIQZiX^(KJ&fsqN8J*g7)-9=PAV*XL zx%7_$X_EamK4zEHLB=zd1F&f&cpbSZ|F-x^J;{Dgq2Sb!f024iUZKnd1M!~@&^;qx z@P4I09LScQ4@bVSmoVwr>+H3bEEE1Cq{~_}&ElXD?DL>yuAajEaNtlX*@rCT7K`|f zx7r8oR`Ko8lb^1AD3!)FxTb?5md{TtUKk#ogn&PFf=Rn3B^cFyR*v~T50t3k8Z81Pe)e|m1h)4D@ALSi}@_V$-vN`KF%%pDW z<_GStLJ|I3YG2s3ax``OFIqp{qUI-SIDb8ePt_v#Cw%Ho8s@E|erkUeI~Tv0=}>9^ zZA#Ub0@kZumnR(~f#T=zF^M8~;(jp6(C6h}Ir%B3^C@b6>DhlDlW9$R!0XN=LSjkn zVqetwjMEa6?bSIu;IBO5%)(=(oErPL*MT0l0>|ih1G; z2FY}G4XNmL<+tCcc4ZrFZY3hf{=EB99kcjjTxVRVY7GUym6__poPI_fSPG#1D8b>m z_l2!D5arWEVS`TVj`&8i{l*`cA;1{!)(U?}K;uMg!=KIA6oU3nL^0?_jC|u5TjyH- zrs*$i$4k;1UOLPI?fR)T@_;C-p?GD$YOrb{Lu-W~K5?ofw9_O6e=jq}xT{<{{oG15 z%sVOtq?!5vi6?9k*2k6x7g)n|T15lRdQAT2Z%PHw#~AQi_bK~I=q6RQt!nPa*(o&+ zD_~PM@8ixazJI34T+~0Q<&HRM0UM``UTA}D9UdNM$El{H)X9Op*#eaw?IFJUv`whfD^~B=Jz3e%qg5SKW;9~dLU+`M z9$hsj0;HJf0`;Ub)5;yTQZ&72#Ph3gkExHG2kED-r)ai5QQm0dX(w@#s*9fl*#PNgdODajR z)8wMI16N85S<^w3Xg={I2{{Ag+)8rd)eAvo8Fa>U@#qEs)=Xdyh>O) zQ@9FG=_|Xz0Ske7#w~$-aubwXa4!MGN*;YvW63Inm9;IlAiH!|_I{U2pFrV8&Z3fK znE4YEI>fED?hy|>m_?BjkYc&DT+2h=vf(s>0uy41m7ckz2zktF&iURaW$eQ|XoI%Zv60Ydat>aF3FY1bG|K}9@s?( zvBh1NYl^p`)1j)aNA`#+JEb|3GsPsp)by;{OypO^?E}~4O(}hgb(2DQqS2-l~%gAozyG5D_Ay>@fmqFBf7Mwf2qV6#d z=$6SW9Nkh2R!HlX={3*m{*q0LeN@4v=|xq`a_rIE$z0R*HWTO*cTsm0J<*GC^8T&q zUn7#gYZ9E>$|*Rqy+a!u1Jy4t_m6^uM_THizeOf@q77P2=A#D38NGE2SpU_H?y@Q5 zB?>TZ=popkf$k|8CD{0O)KfA+@US1KB0hO|?88MV zE;BJp#ziN7aA*;eiA%mfpf)8ci<8rVcr^}&K&Ms6# zASGV=?&TLkZR*sEQpqoLggl#W%WQ*}-zZGaBXyR#ihqv7Sfl?h(X@aLl5?VYx+)wt6Bf~>17FlBdvS*Y?tlqqgWUrmGm8J1l z5;yUqKr0YyD`;ACqP$kyr>Lar1BVTG%a}6}SPEE4RJ5&>_EJT~uuH}N1i{r{2S6f` z=pMuEUpea23u18wJb@Jc0UtRtI~{MiIX8jlKY?H#!W=<9k+2g?&lDU2mrQ~Na7=K7 z8`{}%X{4c#e4EZZlbd))uUJ5HlH$&-`$KLCnvNvM!VA(FvNY(`RjFrk$bDb&kIgHm z2f5C*X$ICcrhIA>=L1$0TXR>(f1!-<$*tb2@q5fDd!j*FM!~mmfF?&AUp<4!EYtTX7 zi9kLrz~6cnkjJw?nxi#e-$pmSUix`(fn3~!xNwgnD-^&HKlTe0{KeKNDMK$~C;NlE zkyj>P()nxk2q|e47E=yNwFBBNyX_CGYjZ}BLw0*Hz3a~_5R#tumj!ZmeR`iY9i2X=Bl% z9gC&ucV}Fr#R}oas=ftyiWqB&F%YjMI<|JomFs~fFD@D8oQO2gSqH7nowm;HLlv3n zQQs?fEY%5tYQRv&vKbG1-0%C-<27du*PDp&AXWiCnG2+e-YKsNwkpyFwhoV;wbFwn ztd z8`e7L5?|mu{*0t1Y3|Ho)26l#j8(@1HHodD5~~F~4nt>Q#w3b*Ka@KM++$Vs+S0|@ zqbtL-O5wT%{9SB*jsnaJj%)C8lSk&skXZscLsPA+6=oNf3OV${}i!uLA)_IJh=3QF%BeTI&(@y(3 zMM)lSzQ%REj?P|KWO&9wUgSu%NK&FxYaA=3d>nFz7AJ(6?R_QEGSNBIc=WI0$_brT zRcJz`XLg15LB7Lrhy4%l@fPbs+nu87smZ8O7$bEhk!kVHP(n{igK#6n^d-g(-tG-S zuiJ55cP>hTrLyI3`Us<@{q#jge8Y7z7xS+Lw1{iB$!F?faZ^z}D^^~vt(o+A**le$ z;r91{l?6~X#MB#WNtv}COX;b-Tk6c za#`y^w5Fw!E)-z(sn&tZqEqK1xB`0uD}r5xSk6lm zKocuY?mtGf_i>!fR*{&kKRIH=z8hM!?2zbFts6DYPef6xRrNu}G~+~8tPS`Sou(SC z#=Ca;&%Y5S-yAPT^Xa;ey9dT#U+2eYT$jb5AsuT57VTl#o&6YJGNrOnb*b2vCx}79 z&Wwh5xzs5vywAL+eci?A;$JTM?_?fq`YZDcw=GkksvGFFr=BK%_V;3W z@f9OEoT?V$)_+)!cyS;_9g$t6rkFS2%eb7JEWS4~+uOAA;j_KzGDZ#pF~bH0d=d_$ zOu9BfJl$DC)#sD)+X@A0^XSG3){Ha|=942_Xc1pODh(Ykk*4%4tmu2efb1^VUn?Bt zr36J5(hZ;~z6iFE1_iiO-^zHg%IkciWF*a;FTIc4tz zclRon2W#KBzg9R(DQA?sN!|8fxry@bBuRbB6MdJbIrvh8Qm&bE;@wI1 zCmqzIrZ4twrQU<5;v|+=5!R%ZK}du6Z5UHg`u4*wLO2mcaDF1Q^nCZ(Hy%(Cs9-_= z(0??*r#tHy;ARk#?~ySS?Ons#DH_37x>n{qzC+rn30%V5=@j3Tg1kbRWqN7 zlDlPw+l+JQlt}8hgtrgV~kB{?NC%0GY#gE=MZlEGADD$~5y}kMM>`^i@Y=QHMGlcnk9DChm@TO0_OWco} z!x6xN(Lsp1sj=@{xh+@IM!`_9UY&)y$+7QSzHQp~AWuF0DH!~h&~uB%BVp(H$XvSR z)BGusXX%v%Dk$1hoIgFU`|epn1o{Fo@962Q(2P5c^xg*sF&%7Kf_QI*+p4_!+^)yQ za%hIRBn#eV`=1YMQA=kRCrcw&7gI4~Cr<|#QG1mP1nOlOXG|AVIotBWc^*o)HHU9BFP3#ZK8EJ2{9JKO-Kb$mhh0p`l znd!?^V({|`E4Y zTI#ztZ(zYLQ=4y_cGL-t5;GQ>p)AeAo0C*sw~gcXOYsEIpc)S$_I;7LZJN4OQh5E?3c;kw4w%cr!=)#X$5&koc}psqc-&<&51jVJw3$8n=L{pt=go_F|BwD&pZuP85x9J{s-Pj5g0c$K~RAiimQIBy4Wm zLrPDIu(6-^U`P6(J=V_6c#hEYc!`7%fmMx^HiSCTpVgF!HRX1?LT>WR!zBev+>@`? zRlCsR5C7ex;gQ|z*v&dMF>)i~ni;Q4Yg)_Tm0Y50l(a$bhGU>D(!kfEoN9W8E2mSM zLYn;{BM-Z$#+enh|CFzwmK@RYHC?Gjmdjs(KpYDCIKvPb=V!jYXa&uS%}FtHrF`9M zsTYx zrJClMhFgNjP>00^yVvdCz7pOqU&-p1uLNK6>C0D=Dr!fP{Jkvjbw z)m-y7UV8Hpg+ZzV6_VlPMm({DkaQF0CWgv`oal;iQ}x=2bQ5=1j@l!v^DUa%bAxnK zY_|xrLn*d+j}B4+{)Ty=9k_ksp>rFvO$b>{IlvPBkIiJJxn(|Ed1l}&sUrrY zTCgKOkRx9?2uXTWjqrlHh zi{hdW*rJaQz{hLhXMpghsN^Ta=G{Zi|KaPLVswe$E#2LyA$;=vhgZVLCpq5?`Cq(}_CO3a#jTPi?tlxcQH80`b^3$GA6^N@|D3DsQkKoC zf6@cMO#k1x`v3W<{%_*)4`f2eG?kw`*(515TYL$U*0*%7n_G4~Rv8}z1mqA#u;zVLX}Po2N?8g6!wt<{Q{B`w70op<)wjL9ee$*w zy8WN?-OO}uHuptHYoE`P?-}=*&%ZmLvz$Ahw~e;EcQT}!!#XfPru3g{`lCiBIs^qI zNDy3;(wtQXQ7rXI`_%j@P}F@TEScmov8)T=yaDDZkF;++l9Y|bY!re2Kqlanc;{dq zef0LvLV>epN(H$_TNY^^pNul~h_|In+hhu8=#M&qW{$l0*}*KJ*grce!XnfsKWqH# zs)$Zk27Ak5=pUZ6G|!ZN0250@GhUXvL^BYUbTIc~yCjqJZ_eegwf9e_Ks6_eK(6$I}+3ErOB&#jOj*to%C=QD+N5|14$6jtD9T|GGPH|xaU&nHAAW*9H<=R2sS&CjER)bxLFOCs54!M;8VBxh_7kf#|ucV5MtaFtkE#k_ug1*g) zJ!EyK@TJDh#7xUS47 z?qo&TL8`QQucaYSEvxb3Y7dOo@tyb1BdxIpI1PZrx_+QlhqlISRX*WX=1I9$+qqZoRsK zVT1vd*_arka6kqfpAE2)SP8E#5C(H2B19sx!d=a}Fs->l#F8|Y@EK3XT}Su;l*PJ` ziPwKp;ohi#F1Sq?mXU&!u$PsFnvgGJCZ8H^pMqX!Y)|v2%V9m9qye_Q%?wU;5M*DJ zJ2+ryglc8V!c-YmBl$|=2;&kOr3M#6tK1I$%DIlJ>ZZHQ^P$B`%i_&zWAAnpSGmJn zGZjVY6ME0#jP#}D<)v9P+nMjI)?9^!uU&jbIdJ$Kk0aUW=GVWReg?VI*=eVGf4v@U zI%O<%7Pw**4tfp<7fliwqG79cW-DbXuM1FEm@0hzZ780_m#v!XfdsQ4bdKK`%+0i- z0dR{4%78VwU38}V`1%SSf^?Y)O|0Z!HMkwLk?@m1;+}bK1xY^{ zO$;ceYB#)Ysb>@ucKm2Qi>l`;=N(yPYh88vf(0B=R5X37Gb-y=MHP!qxi+y)ZqW0( zC(5O_i>e3JQvpwlgN~;F^a7(p+qllg7KfZnDb<*(!+Z zJ=kQAQx4{Kv(c4>FfK|7itHz*j@A8#rBGt2= z7tjP?o(jE9QceBlG^D0xRc%bAgtQ>pu?dcGjQL?+YR2bg{}_}JFcppy(p&O zLsU^nS1HP?qtcn2>pTHp_|cNzB|hp z{2KXvD)OWie8f{dPq$e!FB}ugBZ5GUN-BFm694`|7OG5>>gCo^1L)XKg2u^F+qVu+ zFxXki8w|gQhUN_m6aR=1A}9=3q=CbNIJP_0MogHklDc-kde4(#%|;M!Mxu79qL(Lw z`#@!0EXMjN#nV%rY*T&es{Hop8g#$Uh3?C~3wk>ikzw6~3-b;+;2m_J-}W?ySDIQt z!!9imf-IGkaIVWR%4eZmMB9{y{PxKoe0%an^9$GumwLGAUPI-Yxf8ePp7D}BK#0h_ zg05CX{lxcIC_F3i#E0{jfd2sZQz*pv5ajpQC~SIW4%Ish|NgFr)l)l1`x}wZKiPcV zjR6hzZVde=DxR1$hrb5qYYfdRmcTHJTQUo+SDxaT%U|h0egOsK3M!Oe*4P6_X2p(z zh>Y*~`x|SUOu3eE?klf>GKr~5*D!LPQq>houFUks`-$%$$NttdxwLG@V;%llXpZWv z6S=dHjQDYe`d?@=HHAJuF(ur~F+3}hEc~66;F?i9p4@x2Lod0-X`x;eyN|~1K?j0Q z0+-X_@-b!f##peqH+OB(HeUaTO;_AXRRDP zE3mGWx3By(rV}s47@UhP{^u=_e;R-K8Cs_{U!1?*H!(41oTdM+mdm5OKpuO?Qb6#9 z$ncL7he@A?GegE!?f1H(?VpnB!x`65thwz4j0^~RpWjMX9(D(h8OA8lyoKBvVBJTP zlRhGpX$TO9=6Z?iroQmZ`Nc|c+_{4!Zm2d$AB%{QDH1@l0K>}!a=8R)6K0)wZFn|h}*rlbJ=BntKA1xobjA-!Pt_Sqd|9JfEuq`G^W!PMDT?)cgLhTV-+p?N z88$NFNa4v4EDG(O1VfQk@I(ajWnvFO@Mq(`zfHjm-I+WIc?X|$&9my!G*+(EwmTeU z^{pI#miQlFC$f*PWHqGZ>9!d^k$6;uBfH!CC#ZLC!7oEC22KLnBGzRY8SaXtsFNK* z(Pl{c9`NI0Yr(sv3$9J-+#Z#XUiiE##(#3(sYvqV&MGwaG!j2p38ad$Fr{B`8P184 zr}xbh({P=q3NOX<&6e*oL70n_VU|4LJmp7RaK_kmkaHJExpsprdMJ5IVRDu-1k=BL zCCY~~@MoBPMar`oeWQ)X+Pvy_%9gtN-MWHr;1OE6UA@@XZ=LFQZU)@q^?}P$9W}g-F*BIzlC=z(hCm z*w|XL0O4pz2YNZmn02eRey5;eMs4CLwDqXTV$3o`i~Axl^dB_EnA*495826Z3L!4& z)lr`G#E$eqGL=UtUjPuDq|Trnv!_Rq`GH4uki5t#-GoWs7oE{Lw_uZVmy=i48SCJ#3IJ#U3% z0p}92iKMC!#z_ZzgRB8$Mdq;Za$>RCz}s^tEDBF&zd}TCXgShZ+-ie z*&#UJ8_)s$nA#`V^zAY&|04UkN-%Srb#xlYBc|UhmrOqx)Of70w8U~pL2HLyyqP6x z-26^4!zC{LY2p)>=0;FX%R5$qq7zumviABXudJM6e*GJa4TgD{HGkOUk78CnXgh*9_J-l?bPm0H^jF#$}PW8Ov}l zwti3YZD8hhER0|r)1FmzDgAtE{X(v?5T&eeIG=`DsxH1aD0M8>{NbNwnE$rNi3-nt zxoIzWi@{&2+-AI6yDbU0v^o{GO&lIs=4`7h8Dq5kq>eJPDKrw8_vsy7*eVo-cceMi zi1Ut!Vb6#%FR;*px)xOP1h#of4&ZFw0hE_Q2*nL!MFEAtKzxd_S zaf>@asKscf=LtZg6ee*-K{&;!rxtbnV}KRh$h7~$NtiZb{NN-MtFC_)LpwK%yoM^4 za%NF>4M{MH|2;fXx#mvfY^ZXO3wfsq41YgR7JL^6mosAfJ~3(^7Rs5o$`K@&+sDok zKf0-_MLc(wSRi^a9B#XCm)LEcb^PMXuvX<-AhfN3UxhY#r7w>J~)!u$&Q- zv^OstY_?o5iXc*U+~U-Nsq1Rij(LM!VVStYtnrTG8nyU?&sZd@%P8j?t{}|D$|AQz zxSY!7dV;C@%3+IJEGuN&Qc-iXJQce63M-3ZwWWD!V)Ft`)VekN6R**XP={_fq_H); zv}|=g9W>)Mx4Qv9oFp@uraS0?o_IWSBJ*6Yo2_?;@s6LVJvO|XskcXYy%~h}?>aR( zRZ#xL1!9iHE35jPPVwFw3ifQPCAwD#|7nf;*~Bs3C))YVjcT+foc9-a@RAc1iGO&3 zHy-!;=Yo*E7l|UynrHqWlH>GxO)~KPWw-^bb0p;PfTw9f^JuwC(!G_v!<& zdBag>=W~I_s!eYsAN|xl_EOaj**A!$3EwaV&A3IGm3sx?ep=ip|IDU6f+rIfG5!Qb zb$cavVs~YE@V2(sjp?6JVps`$P&@cZ2ebYS%Udyo?ItmRryjHE&+)LgG+wf_no#2e zPX5$B|IDrMOdCmkcu>M*wgma?CGSW3p+@8*_Y-)4>Ks74lezSyLhd`W_4mDjVLAbu z?4EKkTUzU>8M=?tm~Pw46y z&~4INmEk8y?+x@HYQ!VhiWVAvJIb>rYA2^cm4opNHDS500YhNp0dE^k&~e z-H?XS76gJE}7trN6=(&13W=+tPmMDdLfqBg%!FkUmRLdgVOJlc!KisCjmvD`X za1V$A`&r=UCK#Zu@E9lj6X*Hw6iVe`*k&rmrM=)+F1yMITnmOm+ezT(=g&PydD!8f zdn?%r?B8=>^0>u#w=7)_#45_~{(H3!i9Vf;pT|EE$y2sN1^~}{@zH71ir!!lS z2_iU6T0$!l-1!{{RLDTPHKBn7KG4>*1Wz(rt?O}?M$GN7%yN;XC%&ETr!q*&0egFY z^%rPEbS@b3Sy3bkyCd?O(^0c*e2J|V#@L^p%nfffnpuLc8TanD-o3ZlJKwKE`B`3Z z5uA@)Kj3)-WdQA;2(GIQSC%Wowj zZtUgGlgGfyoE3p1bI{u`?Bb!rEW_(*!-dXQ6f27r>7g-&7#P@fnsl+m{YpKVB5f)=HqK^j-&s;8YT>AA!KK)mP24b-EBO~0*{LxW; zv{{jNblHG_%~Z_zpch~m)Z)d!g|s%Uf=t+kPGf%)eSYvzk`^UTHu_@1Q7IZ*AD1sm z)Ftv+^n(uFYJ@B5oaENM6X8i*{tr4-D?XdyEvBXc0AK&YN%3mIOt_uGf7Bmi`4D+u zkQp;d-g-Q}&Fm#Yz)UMJ8No)j#Z*K8X0n4GJkoPoxw*&7YmBu)5R2JX)>bLyjxEMA zG(Q9E%Y(gk;~XYs$4%ulHk~&Of!NiLtW8neYf+}f7KG-S!Jy$X&-0e%ToI=cK9qiA z21fc+k)%oGVlCEaWzbn;GlNc`IKMo>*8%dU1JsqI2*-HhJf>#Ri)c2xVgiTqcUfA_ zyzCgsg`jE~vys?LM$WQU>sRV*o0T;SK5HA1+ScY9v!^#Ql5RJW*R8y4{N)6Zt;#uw zZzLmatG=|D>9vg4%mjsxyJyAHom6I1&k{3ob!xz3w8{MZs$PaS)y%MfNsgtV7v_QyCCshinREssTKCTvZY4D$@>BI-G(>DVxj~>mhFV_HjQ$u(J$OaD|#-bmh`p>(96hK$`7PsoI1 zTJnKN-st()`|nP^rR0o(TUv%29>z0|J(gc}NspOalktcpSqmyG=tvS(r(6EzvZPsJ zu&(AxH_++tG?U9lIW$v2t%D30!tcxz{vnp8M3RXLh7*EG(_18&zv-YHR|s*Pl+0Ta zZ`fN;qFZnWC3}9mwVqE%9&Sy|iIUPqYW>zP%5nxb9KstJ>qY+n(U04_(b>kS1-5V1 zK?E-)ea{&LU1v*+r=^v?2eZLB(SO!0_1$R+_=QO)wA!j03>&zpp)nS-zBZTOYx@}^ zqux-rhQ#$+~Je!HV*jw-W!x(f08$t);PMFx6S_30Q|dKrKZtf?%MjUQ`K z<@EGk`683O1x?p;sB)4=;|bkYObS*fd}!%B^FkVeC!7LL_QUu6^9r!&Vbj&FM)T5f zTT==%w|9kV0%S3%;0z1z1-{k*_W^~-ZF zDtxPQ;pZ)ILby}%Xq#+%!uyqeE#`ih7Mu_8A%Cm%!Ptu}s=N17?TF{AUJReV$G=to z8E*cpJ1(*exFg(&Hcrj76s37Dl{XAu^Rbn2BQ4Z3Co+(_n7^K%nh+Z;t3L>UBCJyL zlsxk7);$7;(OErhw+Ds`I!OG2hZ*p_Z@}}Eu)R~hAmZ$jJA&S;dj#bvdgR80Uvo;$ zQZGf}iK4!{Tq_wgrVB^SMxOg(xATT1eT4=T~t0qC{VJDHJm<3N8eCt#Ek z?M^m#-Z)HXF}1zx^Xm8Ch==laBJg%0o`)Vbzh|7cySPo855vD0OX@Pe0Mp`2%G^nh zmJ~R0${&24TD3TwjwpnV4HxKI9p@n^Yd>zU6+MVDd)dwYp7S)J_yMTC7?-I!6y-gS z)(HL;rdAg8u4ek_*%0z>f;nZP_TpE`W{Cw=bZ#tYl%+mbi2V5DCwDIs>EzM(9sg4B zA|D*Wx73@Y?sd9x;Wco4X~l!qRzyrwn3B~hrDn>LxLp$q>{D;6r{uWJdU7!Vf*`%) zEt_%D>MGjBoE7XINYh>8cp? zsscVpXiu4@wd{H9VKz96nF0yqG+-ox7z%7~KEgiDW3WJyM%?)x*O4fji2|bWqAFno zPAb5LEc{v!qp?H~VtksI^fm8f?b(*(@(&*_zIAlrbJ#-OgC*`WVb2h+l(jIf`YyS zC%r8@R*>;_ILr@2x59_m6Ye*=*zdhiR}W8JOJkjiC$GFzF>R*0daMBdgPowEsveLT zUDMPq7i;2#VvhqC@py5HFZR8h;FNcyF>eJ?|7c{nDLh@D1==IH8atYC0XX z@gAW!yiL~)Rl$H>I754+ey)n(`w#N6$B5>arb{H%M~chr;){&AC8P45rxu9zUOl-> zS{f5xxtdEFqi#Xk-`QGtqFXNM0qCsa6LZVZaQGaLtCLw}H2`#$TBR7bDD+@0_NbnS znj+)fhFmkllmoG@gUH1Wx_OLF;&EAb$?>!Ob;ypr9Zuenv?X9=Eg@b4jNl zk;VX%ai>FiS^n1SUJI6sgP~1 zi8$;p;eX7r{e?@~veiqxF~GHNsf7w%-I8+RE43<=+e$4VcCS&c4O@f4qaOGLzCR?r zAH?22Z_Ch9fUp_FEU|SF-!P~q&#=|rt%3xYUB8#7!SPGsI~6%*Sq&{NA9$k)4d)e5 z;^w#>DYtXb2r}8(cR=HN2_Ay}0cO`D{%S$r8Nvb>ode1h^E zw@o0+P616SL*k}DI?gsz8gu(n)sih^N^xvX3?K#W@E zu_2B4#a`CUy8A{YTE#Cx*u^ia7NrJbfCX&U2`C1uda%FZo_~lFuUAh+HrAkWeV*Y_jYfS@=>4FT^8%m>t@{TF1p3T1f8Ei zKpG?6C1>ZK{>bkOZlJ%gJC-%wFU{*;H&C~!;@07=D=8h-dq)gM`ByVSe|16JJ=lU`DVB*`s2tILpe&Q)p7JXv+1@;RV z7p|X7>Wk{_fpkyUpH%k6>FwFQvk3F-98&a8T^HHk96f^sMfTcy>;XIKL&5nXE&Btt z7rWsV_&tu#dLr$7Q2H+dx3uTJ$UTsYypHT+^1cb)QF%_^E&*A)B>_U&j+)gT!Bn*h zElbvoT~o}-_VY=hZgctJ&(4S%Js=uHB`U(6{Hk^zQP1AkOl_C#?xR?@|C& z*ARswQYxF5F3P=KQM0%L^ShuOe0%bcKVTFhsh)UdC!)*2?!w*q&N-BCZ-ktKc4>J$7k;A2_SR z%-cJ&XD?v_7}A$Fz^8fdYMVYmavjkwnM%P5Frr*uK_)AkXZd1x&4E_5!BlBOD$_zf zH>OjmgnpS|8wLs=ih;wyEn;2`F z`h@PPY87yx3*Dw+UaW8ba|#1?)rA7kqY3){ta*glrGo3ax~p#KVy;X_6lhQFyLC^Y zKg1^u@x@Zcl>v;)kyd@h)K5j8l>+nLOyF%i3QJ=)&CqtOUVF__Y8SpRya=tH;OQAX zvUSR$Yx}YRw5NbNi(qkQtHZ2m2v#-QIkh|PRS3P$+L4b+v;vrgFP!RsH$nZ`%kR7$2396@GKJ39{_$ zwJf=edr@~qFM+g0XlMjr2|~Wj zy)~QRtF5iAtDBeQHLaUPfIZH)-JKq9ce{1bKc;50)12mioy~r%-W^r+zE8)3KpcWL z&I6I})hPaHvHWwRXF7!Uo*2CW^r!tsK9o56r{8Ggy_X;ud{e{p9GC` z<9q9CwGBP5^mVb2cen_!6V zc$j`C;GMO}KRI@f3tWL8gcB);gJWW!BsrNzE+@ytIcX>|I$1{!Rql~v!aWj1QQ}WD zak≫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∾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

sD8W@NAH_65QsgX!KVwY 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

- z>rtC!GHRn%LC$@IlV6qmrJ&OvA2-CcKGZjh2KATIB+0g162w)vBPfGG{+a`C`Todh zjoVik(F*b~rxH#=X33F<#IO^rDNo7C&>}p^SR#V4yRT5)a$Vk{Y*i9sm9m=Md=Ypi zg%_vnwn;46m1A4B$Y^9zg$=Ps)Rfdxvmt$TsV8oB#D49_xOP9>&EaX9KswSh9^D=W zi5Iz%CgiY_b5p;~S7{iZYRsP5-mS%RRcrNYkz=dp|5y-Q4V$(h>sZOQ|FVubiZAvg zndWFwTs*X@{q+v-*yXqx30?dCNw)|37eVk@K~T127ePf>O$mV3Me}8zZ#texhZxV( zYYA$J4l9I$6S~vBkF(ue0lC64)1N_Ok3V1(HKlEWC5+3LEz42ocABd9)&NTlscvky z2}RFq^J{q|AU+a3&NnHxiDsC;d{`J~nEiz=Y=iYN7pmi{w9mE8Sd>jeL;+2+*ti?L zb$`X-ASgBiVc11S)14m9#r=#2)hInG4I&~&v^MASHK)Xfph)g4)3dY0@zJhl)b5$( z;7_H&gR}~^J`>X@5B6RnEpPs>L%%uZqQk~AM?sg(L1~*g=ALKVu}+3G^D%c_@~}}L zaUF%DI4`#YN`@0lI7En>KXD$PBWTKz?lIPmLEIdG-D7~%sq(~qD)|ClF%F?34ZdG8 zp@!GS#wmA}*}cAvS?0ua4<}O{pgG|MP?B+N2rZ?yBpac%-&TVm!V3;?3!FFUFBezD zyL%a8y!wU`0bhmJ_lmkL+5co-n*%53l4$PG#uu<=l_D|IHubRZiML6|Y?(J$Q4BT9 zB%OJLX^fNvi@a9#hBi}w!4KW z;&%RtM{2vhmhgyYE~B{G5sr-o&uuLv>`nLA8vl4@9^b=bKdFxIyXe9DK-I0rtUA9W z;)l{loudvxX7>$r1?JG`oQ7%aLhEZe%kbiUY-Qsxn4O<4q$XpuP395rOk%W=YQq#; zc;gKJBjuu~d_a2tbEl&XXQb6l5%jz~p-YrY50?!X`LqLidMbe}S45@hFrgAt(dzvVZK7k&f5vH+Pk|JB~%A2gw*X;j_J4^a)Zb@vwdW{H(W}eiGRtl$MOZJ_Z z@G)~UyZrX?bPd~$yGVOSWJ_$^BghSj!-6i`zykO>mUU{m_#?pF%d2XulMy%s$p}-2 zW3Mg?sAyK7#K&ucQ*McKc<|A?M;P_)btg?m6Q9YP7Pz*K~)>y2q0SrSuiCEJN_^H;fy$#xh*9bI{f z(VgXo*EBlrqBcKXTfkUFe-EP#*9{&p8=K60SW*c0Cc8`yZCqgAg&p{CoMGQ^B@}TN zN@tWQvI~rFx_@w1#HG=Epe8@WiZekY?K#-&tXJjyA}VTCYuj?=sCr*Y%Cm%~#RZeE z-^moCiZ|-ka*8p)Q+k$VBh_8;U}+2z;8Kt9JSm0?jnnKnGzcbUOS{_`#=)LcV`B_a z&W>;J!?3mfm4!5oVS;hm+%h&XbN8HNzPF5?RU859~7H`Dau2*+(a$ z&or`IUqk4`-UJrtV|abtg78+E?k-^y3u}HA>lSIdPpgVh>rFwsn}*Raf_T-Ff_^^5$mc;D3JT{?Jpqryt z@QKNi4dV~(2CGer;jy^H+&}iOr{Ljns=SFBp&#(bq*&czR**zM`&R(a{)O>)YPeXV z_MJB%^-0NGs|RTRGHCZVU+_eCv}$LX{moH*gv}t@?gG1sc?rdPYUWk!Nz-te(BDN; z6l>1r#Nv{*7!(XQC~_=oiivPBVE?Xq9oJNVnvuA3L|1qt^ObBGpJ|K~qBcnO_KBN?Yp-L=V)DZB$x* zAwXrNJw%W5<+z;N(WRyPx31S(^Q(-ON1m;>%d>UQ^F$Fuux`&hF0k9}L#^K);APu~ zUMaBIp*cNDXzRkA+9N)3~SIHkYQfI!!eUi;X@&2eX?6NX2e*}l<}nzUg1MU<}Jco zS>`R`TU+Mn*e8V4Arb(%Px5&D{6ewSj&~#AP3yP+cQxm)@>u1OI^^~gzy{GqOHf>X zHWXpSYZH4C4$EzRenMxyTi?CrG6;-(l7ntelQO5Zl8-#6h$fyDob@p!$x-r~$#l)j zeGjV_`Mn8>7kAMbIiL3=w)yfnG-q+%_lO%aZp&rN`o>E0{7%hitzc9dkmAI{QJxJZ9Xh z#)lcxLRvpSz>1Qnf3ayRI0B(aKJ;ot<3X^d_Ll`{GXNDsj@(2D-tpcUN8wR>$GTdTc_EnA73cx_v>jPj|WSnmtyxPWRF+mttv( z6(>OhbKyT{zeG`0sWZ3`Ojdw5g#K*4(o;A|(6T9#5tsM2aNrh5WDNw{njN)Z;G3z5fq1P0zo~bd=<^Fp9(9;H zQjOV^+Umq|RDAVNGRGxQI{eVoNhizKbZnJ*YU=7!%_7EN!|D360)^N7WktL|aSj!l zr@Gi1er=p|A}~F#0bOp1{|X_fQ-ZfryVOU6tb^S!5!aeMN(x>MOCPMVbWz%Z*331@*w(SG1j{K;@e z74lS#PA_}?CW`IPqu~3BSZ%Yj6+@0Amh+64syQ4c>}QK?<8(D@)&oiE!0*rjclgSS zO{3c-Jkm|1aFX2Wumb%pYm0h;?N$yXdRjHBUJE0l0M5Glngq~^ZHa7J2DXz2U#Av? zsiI@-c4$h1>O1Rj$fG_8Y}z06@ZZ2Qv0qch=Kwz;1^oLO0Tqw}LD`l7=bKkB1Z#ja z^;@{qRL?E;Yuag-;GwA3HICP{_U}x{t`GqWfcVW@n2%r4tbU10ppqn&|&7&KFNfWRMM_p=I2y*2VPr_kHyFMVF61|50&!l|=G zlV=b2nB|mSJH1hxDqPj1`aU9}>QatCAQFZcZs{mP<+5rdAj_#4w>YuIBqOedCM0eQ9b<^&Q;(x_cwq3p(vg%kvCrPA8&R{~$E1$UH1ZJkTK z@6}4@<=j9bk~<`A(JOISO|R_1HIeGwA0m>3A`srup$v?8IWPt@Q;CZjXm8O^6Ub#z z9h8jaPQ>jMJ*Ji=24~HwcQD&kF8d7xA|2r8f=wbg{utC*Pu$czf|d@+q^Qugi3`{# z@VRtI<}R5t7SH7%+Nfu&0f7}K8q9L6z#|VXUEBsqTHkcjzEkA(w^@C1u5KW@{Op?U zXqx*OvL3eIfO|a1lW;rocX?htIEa}smON3z?X@MciZOIf;T}k66jYAY258A96x(B7 zH4J)=Z7(*+gu*8}3vELpeoH$XAXaP8ulMW@&|0gC_Rc}P85s4FW46ejf(f4f(?A)? zqzpX$lpWAr5_p8y3#|bs?UP1t^HumVWHrj$XFSx$nR~vcfw(<18bb&d!=5Tay?EP| zaFZ6&zBHs6E@_Xa_Y84+LAw2vxVqqd?$ulKJ+0Rd;^xZm_apVI_l{EbhZR<7HSdR5 zlpowvHSk$t{au0X8+)bSXTyFmI|4ZXg6`iC@75*-z(ER!^XuHOU*L`a$QkL!JpkQ| zzlqW#e;CO8@#AY(-PIkOyi$X>vg-Hd2yjmY0+DJw;=lX>BFRD82ZUgl51oOV{u7Aw zi&^g)2t;!Jzkx{W{{uv#{C|K*+>^sM9?EF#0io`pJ#$1v*Krk{6x9C&B1wvFkzxXY zNX$xQ?VS|=14K#>G#uG7dCIN;2>g7_HvOFrgD3MZ?^wgnuhlWJ zNeM@O*nRp9!tp8nfGLlch>05k-5o!e8-c_fdzx3<;ip$TEGqr157wv;nfeiCv_;EuD(CAc)3t|c*lI`1p{|6^*-BEL~uy=v_V|knF8FrJlZsTr_}B` zK3!j$0NkjaFJaEb+kU+rC)cm;_3F|7!nx0 z_QQP==mBh9Tz6&ZGE}Prvc%q}7+ z3E>u9h7L&piccT4e~%Wcx(r^H1rAyDfGbNp|G6mr-_c_7W@e^N|JI@b_m=)?aRn}t zqJR}zRN7X4krg&YlhzIzoD~F3M+Z;Svx!JQM?sI@__Z>c%=ddSVfk{Oi4OchrGcnQL6~5q34Yu$!JLS7h4>OSs6*Oh{1Z(MA zR2kvnY0|GaZv_@?_Nh^ohnc40jhU*@EnZt`N;GI_vaTxTRVHS(9R{&-ES2c4 z%E}jEU)R7;>4sva_arlz*ibOo8KiXn%)83D(|yF?Sqgy~mbpBTIM?80Nmtwt>E}(rlI%D>Tv| zKS^~jEA zv`VmTyR-iU)(}l#mT15*O0kp8Po2qTkD&NrRV}JC2R{k-;^5^-SL+Jr=SQ_y6N5`n!0h0E@{X$k6^c{aY|*< zJ<6z=>Bnz3Z0(&H)P8k|)5+)SXBLe*4HNMnIw0&&PJ_RSGvX90aBJ)93}yz6AebO7QCff z%}oD(z2N*wD>uE=Z7AX4|Ba}r>^PtxdbELg1iyF{xLFDvN|r4w_3l4CQT&h1QrI1` zI>kSBS5&6I?5v5r#aiSjpC1l?BE@z-aEHE?+1?W41EKxkM`S_mZ=k@{B@!^b*@MW>o={-z zj=P!205P3Ss&?{iQLuK{{&W<8T+0&<+D+@H_Obr4&mQ^o@;zr@S-9J#VeY!!$^b|p z3_upNnxY%%cSQvZ9@C^>4^s6JO=?^2igH!=4bh-flWs@u(aubIGH~CvixxJw6hY%# z&i)lCRx2St&2xiebO%o`?H9fImd2nr9)Ur!>RBZ~Hot{2{b#ZC%dtGfT`?f4_ya7O zwXy#FTkQLvNHMTCsx*Nj0lIlV#(zYL3Hov82t-=Sf51h%Sen0mBK~W!6ykX<<>2|~ z_rpNR(a8w$<9oP>D}d0ORfJ&X)_f~!`Am4LP@lq+eyHd(aQF&?2pqmn6~F;hbq*S}>pN5Km;3UH)+y$r$PJ|owg%zQN&Om^Qe)lt%#ZS0RO4EsLQkGLJeoTZIz zqD?+F>pjKC^Rd-(l=1MGP0$6xJH&!Ir@`nj6&M`EE3?jFNhIBu7Y3!F6jABp`U(Ei z7$iRP7cb&v6y9)=8D7pi%p@({o%3MchsPhf$lP&bog~MeEJ(jbW3=Nte=cG%yLC+D z*hA{}DA)keoRNs!#IM~v_@1Mc-3NN1dc-( zvGuo|EoF69b<(16&scSMWJVedXUP;f9=G@A>bPRefEP%7CKZWfLcYt#4EIdm9kDM- zTSwsYTe7kX80^`s6Sh!gx=MsFpnxzdNIbeEJNsS zS*`oakSSI$E5wY7n_*Un>iV}+F95G3kiBl zLrZ6RQ{cnW1o!}|zqa-!rZ)fOid_F6uV@8iD*fxSpFwJXzZLQ?*&<@#e^&%hmlpj0 zb6E#_TY3*$n?_Yd`!zzOZ&TGZue>4|mxke#3#^Swg)Zsym8)m)^w6pB=tvYVkDqXP zZ{8uM9>mOLekJcG3`OYcs(Ki)5@e#dM9psg!basVrv zr7ft|6^4f3zvmksI#ZRkL?Y4zKjq5&)Sao;$3OOFCYz%rUgsL#9HG}FC4_d&f)HDM z8>o9Ch3yK=g3=QivTG)3D-X<+Ehc-ruJMpbfGx~~qGLSDO;)?-_O4j4U{|aNs3_O~dpqHPGe_|#iv8@pSM2fZU5q8R*n(YS?}^49)Yvt)|J{Q- zZtvUq?mPeQiI3$8@9e%aJ3Bi&ySpk&RLb`QJ}bi$!*d7Tz7e+E-<@z2@P9ouEdSTGZ^-1uxg2C9eBzI>|Z>}AY! zmyrHr*;Q{q$$)o*8tCvpjK;5m?Eg&rjD|}1lf{$xd$#v&#_q_>oL0n7h`Kjg9eh5f*z)E`LqR8S;3wi3L0B)jy2gt?d*SP8QEGa6%}VD#myY?=g|TQsKFiy1{#_X@w?Hmp&SWHScq>#v`#GL-pZlI6g1rkLfAo8 z^Jg?9K(_=L-U*TF2;3YRoA&N`VAM~VNHKn=tKT=X5PCX{1Sszu<-;I+uN zt42dTm|K0)L|s7`FR~N zWQAQ93hjh3lm#q5BtHB^LVCu;Xb9uYSm4r$OfanTSQ_xg^-lze=dBIX;?_K73b^w_ z+|bAjRabWerC$KSQ8Vh*Y?+hh%r4H^c?mQonIpHH>VMc5jNm$#thIz zpspECUZ+bCCP{@xUx}sw6tq>TeTNn zNCMhxW#N!rwYwp_?t%%I*VD+wB&eSzG}iQnVk9xLzT|#-A^-u*(yq2{pyW0ZwWT&v ztxrgOn=wi+Hh@(*U)^+r=_g=F)!<0odxiuC2a%fK*eJbEWJq)rsjzoXuAY7d?628u z-K+a~5?dZJ*|5-KCm_!NlJ@|;FOrbV4>AKYZLf%Fc=PYmqh%3!cp&oNr`qmUNZgL< zgb4Uo|ELhPj`S^aGjv%K3nP02Bdf>}eC$sW*%OP+P_$Uw*uuR^tugOy&%`1$=clp* zFG*Cs+2ZE5dp+Uackq!DHp~N;c}oHZY9gYe^lF{lyzt*U&!)5Cal&B|{2Uqno&;{C zXOUNAFfrF?&g6Kx7L8@$7jF;EvSpy=NWZAm3;f0X*H&12ZUF{wr+8b)$~kCAb!1o^ zCPp!2pM96S|3)hpLSKB(Gd3h4{WT$4HLd?$lOOK*0!&Y+sR3uURXj+{)aW3ZIdKGD z(U>C4_qK~0p10~Iw-ImZ|KL^+T+#IPh{X8CRLkZhvR70Df|?K| zYx$qc4_xyB^ss1;x8gnC3h~91W3VV=c+Gd3O zMX-qFXZvlwBrwm2IOJ#Gp*=injB`?{24mLao!o|=B)DgEv^hNY!i6BZs4!ZPsgFmK^Q+3I9o3>fCNu#vKVnmw#5%?qq#?+McyY@ z(vgT(e27tB|LNXu5LoV0+;%=)9!a8hjO(w{2Dj5B(BvNabn3`k^nbs^fAkq*A6MMg zx66`79zZZ%8nY75>HJ|NCR!=>NK8vU)lN)*hgN<;EBR&RyzwNW_1SoFL+}2*p|cOH z>N~~;@*tMaX!v6y3EMeJr|GYb(S(TaI=5a9u%@?P>E`?uO)n*}-T~7ckRk3s!)vYb z{0dtugP4U63U|&V!Oc;YrTt>;{4ZuM-Uni~BTvAO>tE-P*iv$6)2rLO+wUwIXaHvF zS-5BGuHRRa0wj%>b$mtMs$Vn%dAxd<{3%k+CLw#30TfG-1@_}OOtX5}Ji zmU>UQY)Xo)fDDMRd`83aLnMBHCOA%yNLN~?WNV($e*b)7D5R eg4}>NNFtLUaOPI6B?{y6j!b^?hqC5H4F-E)Et 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 literal 0 HcmV?d00001 diff --git a/clearing-house-edc/libs/fraunhofer/infomodel-util-4.0.4.jar b/clearing-house-edc/libs/fraunhofer/infomodel-util-4.0.4.jar new file mode 100644 index 0000000000000000000000000000000000000000..60a3775f09d27831a68b73400a8a542a4066522e GIT binary patch literal 13437 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 P`MiR7>96l+%|QPTz^>w* literal 0 HcmV?d00001 diff --git a/clearing-house-edc/settings.gradle.kts b/clearing-house-edc/settings.gradle.kts index a21f7ec..23350a5 100644 --- a/clearing-house-edc/settings.gradle.kts +++ b/clearing-house-edc/settings.gradle.kts @@ -28,8 +28,8 @@ dependencyResolutionManagement { maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots/") } - maven { - url = uri("https://maven.iais.fraunhofer.de/artifactory/eis-ids-public/") + flatDir { + dirs("libs/fraunhofer") } mavenCentral() mavenLocal() From 3fc6232a59f5c5e165d41ad86c0c2046244c6878 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Mon, 23 Oct 2023 15:16:59 -0300 Subject: [PATCH 129/183] feat (ch-edc): creating installation and tests page for the Readme.md --- clearing-house-edc/README.md | 16 +++++++-------- docs/SUMMARY.md | 1 + docs/content/admin-guide/installation.md | 26 ++++++++++++++++++++++++ docs/content/admin-guide/tests.md | 22 ++++++++++++++++++++ 4 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 docs/content/admin-guide/tests.md diff --git a/clearing-house-edc/README.md b/clearing-house-edc/README.md index 1e3803f..6862e27 100644 --- a/clearing-house-edc/README.md +++ b/clearing-house-edc/README.md @@ -1,4 +1,4 @@ -## CLEARING HOUSE +## CLEARING HOUSE EDC This repository contains the Clearing House Extension that works with the Eclipse Dataspace Connector allowing logging operations. @@ -6,13 +6,13 @@ allowing logging operations. ### Configurations It is required to configure those parameters: -| Parameter name | Description | Default value | -|----------------------------------------|-------------------------|------------------------| -| `truzzt.clearinghouse.jwt.audience` | 1 | 1 | -| `truzzt.clearinghouse.jwt.issuer` | 1 | 1 | -| `truzzt.clearinghouse.jwt.sign.secret` | 123 | 123 | -| `truzzt.clearinghouse.jwt.expires.at` | 30 | 30 | -| `truzzt.clearinghouse.app.base.url` | http://localhost:8000 | http://localhost:8000 | +| Parameter name | Description | Default value | +|----------------------------------------|----------------------------------------------|------------------------| +| `truzzt.clearinghouse.jwt.audience` | Defines the intended recipients of the token | 1 | +| `truzzt.clearinghouse.jwt.issuer` | Person or entity offering the token | 1 | +| `truzzt.clearinghouse.jwt.sign.secret` | Secret key to encode the token | 123 | +| `truzzt.clearinghouse.jwt.expires.at` | Time to token Expiration (in Seconds) | 30 | +| `truzzt.clearinghouse.app.base.url` | Base URL from the clearing house app | http://localhost:8000 | ### Build To build the project run the command below: diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 436543d..0e50308 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -2,6 +2,7 @@ # Admin Guide - [Installation](content/admin-guide/installation.md) +- [Tests](content/admin-guide/tests.md) - [Maintenance](content/admin-guide/maintenance.md) # Internals diff --git a/docs/content/admin-guide/installation.md b/docs/content/admin-guide/installation.md index 25267fe..62ba90b 100644 --- a/docs/content/admin-guide/installation.md +++ b/docs/content/admin-guide/installation.md @@ -1 +1,27 @@ # Installation + +## Clearinghouse-edc +This module contains the Clearing House Extension that works with the Eclipse Dataspace Connector +allowing logging operations. + +### Configurations +It is required to configure those parameters: + +| Parameter name | Description | Default value | +|----------------------------------------|----------------------------------------------|------------------------| +| `truzzt.clearinghouse.jwt.audience` | Defines the intended recipients of the token | 1 | +| `truzzt.clearinghouse.jwt.issuer` | Person or entity offering the token | 1 | +| `truzzt.clearinghouse.jwt.sign.secret` | Secret key to encode the token | 123 | +| `truzzt.clearinghouse.jwt.expires.at` | Time to token Expiration (in Seconds) | 30 | +| `truzzt.clearinghouse.app.base.url` | Base URL from the clearing house app | http://localhost:8000 | + +### Build +To build the project run the command below: + + ./gradlew build + + +### Running +Local execution: + + java -Dedc.fs.config=launchers/connector-local/resources/config.properties -Dedc.keystore=launchers/connector-local/resources/keystore.jks -Dedc.keystore.password=password -Dedc.vault=launchers/connector-local/resources/vault.properties -jar launchers/connector-local/build/libs/clearing-house-edc.jar diff --git a/docs/content/admin-guide/tests.md b/docs/content/admin-guide/tests.md new file mode 100644 index 0000000..e5ae585 --- /dev/null +++ b/docs/content/admin-guide/tests.md @@ -0,0 +1,22 @@ +# Tests + + +## Clearinghouse-edc + +For the test clearinghouse-edc it uses Junit 5 and Jacoco for the coverage. + +### Running Tests +To run the unit-tests execute the following command: + + ./gradlew test + + +### Test Coverage +To generate the tests coverage execute the following command: + + ./gradlew jacocoTestReport + +The coverage reports will be available in the following folders: + +- [core/build/reports/jacoco/test/html/index.html](./core/build/reports/jacoco/test/html/index.html) +- [extensions/multipart/build/reports/jacoco/test/html/index.html](./extensions/multipart/build/reports/jacoco/test/html/index.html) \ No newline at end of file From 6f273bbd5b8c0503f2061aee944b95c692a2a3f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 25 Oct 2023 16:01:18 +0200 Subject: [PATCH 130/183] fix(ch-app): Bump dependencies --- clearing-house-app/Cargo.lock | 365 +++++++++++++++++++--------------- 1 file changed, 210 insertions(+), 155 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 285beed..3a72478 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -55,21 +55,22 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" dependencies = [ "cfg-if", "getrandom", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -106,13 +107,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -193,9 +194,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.4" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "biscuit" @@ -208,7 +209,7 @@ dependencies = [ "num-bigint", "num-traits", "once_cell", - "ring", + "ring 0.16.20", "serde", "serde_json", ] @@ -221,9 +222,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "bitvec" @@ -285,9 +286,9 @@ checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" @@ -342,7 +343,7 @@ dependencies = [ "anyhow", "async-trait", "axum", - "base64 0.21.4", + "base64 0.21.5", "biscuit", "blake2-rfc", "chrono", @@ -356,7 +357,7 @@ dependencies = [ "once_cell", "openssh-keys", "rand", - "ring", + "ring 0.16.20", "serde", "serde_json", "serial_test", @@ -403,9 +404,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" dependencies = [ "libc", ] @@ -472,7 +473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.0", + "hashbrown 0.14.2", "lock_api", "once_cell", "parking_lot_core", @@ -486,9 +487,12 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] [[package]] name = "derivative" @@ -545,30 +549,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "finl_unicode" @@ -668,7 +661,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -755,9 +748,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" [[package]] name = "heck" @@ -857,7 +850,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.9", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -866,16 +859,16 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows-core", ] [[package]] @@ -926,12 +919,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.2", ] [[package]] @@ -949,7 +942,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.4", + "socket2 0.5.5", "widestring", "windows-sys", "winreg", @@ -957,9 +950,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" @@ -984,9 +977,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.148" +version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "linked-hash-map" @@ -996,15 +989,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.7" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1054,18 +1047,19 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "md-5" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6365506850d44bff6e2fbcb5176cf63650e48bd45ef2fe2665ae1570e0f4b9ca" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ + "cfg-if", "digest", ] [[package]] name = "memchr" -version = "2.6.3" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "mime" @@ -1090,9 +1084,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", @@ -1133,7 +1127,7 @@ dependencies = [ "serde_with", "sha-1", "sha2", - "socket2 0.4.9", + "socket2 0.4.10", "stringprep", "strsim", "take_mut", @@ -1198,9 +1192,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] @@ -1242,7 +1236,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75a0ec2d1b302412fb503224289325fcc0e44600176864804c7211b055cfd58" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", "byteorder", "md-5", "sha2", @@ -1255,7 +1249,7 @@ version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "cfg-if", "foreign-types", "libc", @@ -1272,7 +1266,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1311,13 +1305,13 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.4.1", "smallvec", "windows-targets", ] @@ -1360,7 +1354,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1393,6 +1387,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1401,9 +1401,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.67" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -1468,16 +1468,25 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "regex" -version = "1.9.5" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", - "regex-syntax 0.7.5", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -1491,13 +1500,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.8" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.2", ] [[package]] @@ -1508,9 +1517,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.5" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "resolv-conf" @@ -1531,12 +1540,26 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1558,7 +1581,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.18", + "semver 1.0.20", ] [[package]] @@ -1573,11 +1596,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.14" +version = "0.38.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" dependencies = [ - "bitflags 2.4.0", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -1586,12 +1609,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.7" +version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", - "ring", + "ring 0.17.5", "rustls-webpki", "sct", ] @@ -1602,17 +1625,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.4", + "base64 0.21.5", ] [[package]] name = "rustls-webpki" -version = "0.101.6" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -1635,12 +1658,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -1654,9 +1677,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "semver-parser" @@ -1666,9 +1689,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] @@ -1684,13 +1707,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1699,7 +1722,7 @@ version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.0.2", "itoa", "ryu", "serde", @@ -1771,7 +1794,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1787,9 +1810,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -1798,9 +1821,9 @@ dependencies = [ [[package]] name = "sharded-slab" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] @@ -1831,9 +1854,9 @@ checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", @@ -1841,9 +1864,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", "windows-sys", @@ -1855,6 +1878,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stringprep" version = "0.1.4" @@ -1891,9 +1920,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.37" +version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", @@ -1926,29 +1955,29 @@ checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", + "redox_syscall 0.3.5", "rustix", "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.48" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -1963,12 +1992,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -1976,15 +2006,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -2006,9 +2036,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", "bytes", @@ -2017,7 +2047,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.4", + "socket2 0.5.5", "tokio-macros", "windows-sys", ] @@ -2030,7 +2060,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] @@ -2057,9 +2087,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -2109,11 +2139,10 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "log", "pin-project-lite", "tracing-attributes", @@ -2122,20 +2151,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", "valuable", @@ -2143,12 +2172,12 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" dependencies = [ - "lazy_static", "log", + "once_cell", "tracing-core", ] @@ -2275,6 +2304,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.1" @@ -2288,9 +2323,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom", "serde", @@ -2350,7 +2385,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", "wasm-bindgen-shared", ] @@ -2372,7 +2407,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.37", + "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2428,10 +2463,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.48.0" +name = "windows-core" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" dependencies = [ "windows-targets", ] @@ -2521,6 +2556,26 @@ dependencies = [ "tap", ] +[[package]] +name = "zerocopy" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "zeroize" version = "1.6.0" From ae3e8135aca0df2c06dd7e4aeac0077f13edcb88 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Wed, 25 Oct 2023 14:39:38 -0300 Subject: [PATCH 131/183] chore (ch-edc): removing clearing-house-processors --- clearing-house-processors/.editorconfig | 105 -------- clearing-house-processors/.gitignore | 18 -- clearing-house-processors/README.md | 34 --- clearing-house-processors/bin/.gitignore | 2 - clearing-house-processors/bnd.bnd | 7 - clearing-house-processors/build.gradle.kts | 145 ----------- .../gradle/libs.versions.toml | 71 ------ .../gradle/wrapper/gradle-wrapper.jar | Bin 60756 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 - clearing-house-processors/gradlew | 240 ------------------ clearing-house-processors/gradlew.bat | 91 ------- clearing-house-processors/settings.gradle.kts | 2 - .../de/fhg/aisec/ids/clearinghouse/Utility.kt | 217 ---------------- .../clearinghouse/idscp2/CreatePidTests.kt | 124 --------- .../ids/clearinghouse/idscp2/Idscp2Client.kt | 107 -------- .../idscp2/Idscp2EndpointTest.kt | 108 -------- .../clearinghouse/idscp2/LogMessageTests.kt | 77 ------ .../ids/clearinghouse/idscp2/QueryIdTests.kt | 71 ------ .../ids/clearinghouse/idscp2/QueryPidTests.kt | 109 -------- .../clearinghouse/multipart/CreatePidTests.kt | 154 ----------- .../multipart/LogMessageTests.kt | 107 -------- .../multipart/MultipartClient.kt | 80 ------ .../multipart/MultipartEndpointTest.kt | 63 ----- .../clearinghouse/multipart/QueryIdTests.kt | 100 -------- .../clearinghouse/multipart/QueryPidTests.kt | 141 ---------- .../intTest/resources/simplelogger.properties | 1 - .../intTest/resources/ssl/client-keystore.p12 | Bin 6366 -> 0 bytes .../resources/ssl/consumer-keystore.p12 | Bin 2693 -> 0 bytes .../resources/ssl/provider-keystore.p12 | Bin 2693 -> 0 bytes .../intTest/resources/ssl/server-keystore.p12 | Bin 6366 -> 0 bytes .../src/intTest/resources/ssl/truststore.p12 | Bin 1271 -> 0 bytes .../ids/clearinghouse/ChTrustManager.java | 56 ---- .../clearinghouse/ClearingHouseConstants.java | 48 ---- .../ClearingHouseExceptionProcessor.kt | 75 ------ .../ClearingHouseInputValidationProcessor.kt | 116 --------- .../ids/clearinghouse/ClearingHouseMessage.kt | 62 ----- .../ClearingHouseOutputProcessor.kt | 108 -------- .../aisec/ids/clearinghouse/Configuration.kt | 28 -- .../clearinghouse/SharedSecretProcessor.kt | 72 ------ .../src/routes/clearing-house-routes.xml | 170 ------------- clearing-house-processors/version | 0 41 files changed, 2914 deletions(-) delete mode 100644 clearing-house-processors/.editorconfig delete mode 100644 clearing-house-processors/.gitignore delete mode 100644 clearing-house-processors/README.md delete mode 100644 clearing-house-processors/bin/.gitignore delete mode 100644 clearing-house-processors/bnd.bnd delete mode 100644 clearing-house-processors/build.gradle.kts delete mode 100644 clearing-house-processors/gradle/libs.versions.toml delete mode 100644 clearing-house-processors/gradle/wrapper/gradle-wrapper.jar delete mode 100644 clearing-house-processors/gradle/wrapper/gradle-wrapper.properties delete mode 100755 clearing-house-processors/gradlew delete mode 100644 clearing-house-processors/gradlew.bat delete mode 100644 clearing-house-processors/settings.gradle.kts delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/Utility.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/CreatePidTests.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/Idscp2Client.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/Idscp2EndpointTest.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/LogMessageTests.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/QueryIdTests.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/QueryPidTests.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/CreatePidTests.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/LogMessageTests.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/MultipartClient.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/MultipartEndpointTest.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/QueryIdTests.kt delete mode 100644 clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/QueryPidTests.kt delete mode 100644 clearing-house-processors/src/intTest/resources/simplelogger.properties delete mode 100644 clearing-house-processors/src/intTest/resources/ssl/client-keystore.p12 delete mode 100644 clearing-house-processors/src/intTest/resources/ssl/consumer-keystore.p12 delete mode 100644 clearing-house-processors/src/intTest/resources/ssl/provider-keystore.p12 delete mode 100644 clearing-house-processors/src/intTest/resources/ssl/server-keystore.p12 delete mode 100644 clearing-house-processors/src/intTest/resources/ssl/truststore.p12 delete mode 100644 clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ChTrustManager.java delete mode 100644 clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseConstants.java delete mode 100644 clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseExceptionProcessor.kt delete mode 100644 clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseInputValidationProcessor.kt delete mode 100644 clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseMessage.kt delete mode 100644 clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseOutputProcessor.kt delete mode 100644 clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/Configuration.kt delete mode 100644 clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/SharedSecretProcessor.kt delete mode 100644 clearing-house-processors/src/routes/clearing-house-routes.xml delete mode 100644 clearing-house-processors/version diff --git a/clearing-house-processors/.editorconfig b/clearing-house-processors/.editorconfig deleted file mode 100644 index 017e5db..0000000 --- a/clearing-house-processors/.editorconfig +++ /dev/null @@ -1,105 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 4 -indent_style = space -insert_final_newline = false -max_line_length = 120 -tab_width = 4 -ij_continuation_indent_size = 8 -ij_formatter_off_tag = @formatter:off -ij_formatter_on_tag = @formatter:on -ij_formatter_tags_enabled = false -ij_smart_tabs = false -ij_visual_guides = none -ij_wrap_on_typing = false - -[{*.kt,*.kts}] -ij_continuation_indent_size = 4 -ij_kotlin_align_in_columns_case_branch = false -ij_kotlin_align_multiline_binary_operation = false -ij_kotlin_align_multiline_extends_list = false -ij_kotlin_align_multiline_method_parentheses = false -ij_kotlin_align_multiline_parameters = false -ij_kotlin_align_multiline_parameters_in_calls = false -ij_kotlin_allow_trailing_comma = false -ij_kotlin_allow_trailing_comma_on_call_site = false -ij_kotlin_assignment_wrap = normal -ij_kotlin_blank_lines_after_class_header = 0 -ij_kotlin_blank_lines_around_block_when_branches = 0 -ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 -ij_kotlin_block_comment_add_space = false -ij_kotlin_block_comment_at_first_column = true -ij_kotlin_call_parameters_new_line_after_left_paren = true -ij_kotlin_call_parameters_right_paren_on_new_line = true -ij_kotlin_call_parameters_wrap = on_every_item -ij_kotlin_catch_on_new_line = false -ij_kotlin_class_annotation_wrap = split_into_lines -ij_kotlin_continuation_indent_for_chained_calls = false -ij_kotlin_continuation_indent_for_expression_bodies = false -ij_kotlin_continuation_indent_in_argument_lists = false -ij_kotlin_continuation_indent_in_elvis = false -ij_kotlin_continuation_indent_in_if_conditions = false -ij_kotlin_continuation_indent_in_parameter_lists = false -ij_kotlin_continuation_indent_in_supertype_lists = false -ij_kotlin_else_on_new_line = false -ij_kotlin_enum_constants_wrap = off -ij_kotlin_extends_list_wrap = normal -ij_kotlin_field_annotation_wrap = split_into_lines -ij_kotlin_finally_on_new_line = false -ij_kotlin_if_rparen_on_new_line = true -ij_kotlin_import_nested_classes = false -ij_kotlin_imports_layout = *,java.**,javax.**,kotlin.**,^ -ij_kotlin_insert_whitespaces_in_simple_one_line_method = true -ij_kotlin_keep_blank_lines_before_right_brace = 0 -ij_kotlin_keep_blank_lines_in_code = 1 -ij_kotlin_keep_blank_lines_in_declarations = 1 -ij_kotlin_keep_first_column_comment = true -ij_kotlin_keep_indents_on_empty_lines = false -ij_kotlin_keep_line_breaks = true -ij_kotlin_lbrace_on_next_line = false -ij_kotlin_line_break_after_multiline_when_entry = true -ij_kotlin_line_comment_add_space = true -ij_kotlin_line_comment_add_space_on_reformat = false -ij_kotlin_line_comment_at_first_column = false -ij_kotlin_method_annotation_wrap = split_into_lines -ij_kotlin_method_call_chain_wrap = normal -ij_kotlin_method_parameters_new_line_after_left_paren = true -ij_kotlin_method_parameters_right_paren_on_new_line = true -ij_kotlin_method_parameters_wrap = on_every_item -ij_kotlin_name_count_to_use_star_import = 2147483647 -ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 -ij_kotlin_packages_to_use_import_on_demand = kotlinx.android.synthetic.** -ij_kotlin_parameter_annotation_wrap = off -ij_kotlin_space_after_comma = true -ij_kotlin_space_after_extend_colon = true -ij_kotlin_space_after_type_colon = true -ij_kotlin_space_before_catch_parentheses = true -ij_kotlin_space_before_comma = false -ij_kotlin_space_before_extend_colon = true -ij_kotlin_space_before_for_parentheses = true -ij_kotlin_space_before_if_parentheses = true -ij_kotlin_space_before_lambda_arrow = true -ij_kotlin_space_before_type_colon = false -ij_kotlin_space_before_when_parentheses = true -ij_kotlin_space_before_while_parentheses = true -ij_kotlin_spaces_around_additive_operators = true -ij_kotlin_spaces_around_assignment_operators = true -ij_kotlin_spaces_around_equality_operators = true -ij_kotlin_spaces_around_function_type_arrow = true -ij_kotlin_spaces_around_logical_operators = true -ij_kotlin_spaces_around_multiplicative_operators = true -ij_kotlin_spaces_around_range = false -ij_kotlin_spaces_around_relational_operators = true -ij_kotlin_spaces_around_unary_operator = false -ij_kotlin_spaces_around_when_arrow = true -ij_kotlin_variable_annotation_wrap = off -ij_kotlin_while_on_new_line = false -ij_kotlin_wrap_elvis_expressions = 1 -ij_kotlin_wrap_expression_body_functions = 1 -ij_kotlin_wrap_first_method_in_call_chain = false -insert_final_newline = true -ktlint_code_style = official -ktlint_ignore_back_ticked_identifier = false \ No newline at end of file diff --git a/clearing-house-processors/.gitignore b/clearing-house-processors/.gitignore deleted file mode 100644 index 20e5396..0000000 --- a/clearing-house-processors/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -# Gradle -/.gradle - -# Eclipse -.metadata -.project -.settings -.classpath -*.launch - -# IDEA -*.iml -/.idea - -# Binary -/target -/build -/out \ No newline at end of file diff --git a/clearing-house-processors/README.md b/clearing-house-processors/README.md deleted file mode 100644 index 43e1697..0000000 --- a/clearing-house-processors/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Clearing House Processors - -## Building from Source -The Clearing House Processors are written in Java and require Java 17 and can be build using gradle (version 7.5+): - -``` -cd clearing-house-processors -./gradlew build -``` - -## Camel Routes -The Clearing House Processors include a file that contains the [routes](src/routes/clearing-house-routes.xml) used by [Apache Camel](https://camel.apache.org) (used in the Trusted Connector) to provide the endpoints of the Clearing House Service. The routes also contain some important steps that transform and forward data to the services of the Clearing House. - -The routes define TLS endpoints and require access to the `keystore` and `truststore` used by the Trusted Connector. Currently, the passwords for both need to be configured in the routes file. - -The routes also expect the `Logging Service` to be accessible via the docker-url `logging-service`. If this is not the case in your deployment, you will need to change this in the routes file. - -## Testing -All tests are integration tests and will try to establish a TLS connection to an instance of the Clearing House. -The tests will only run successfully if they can authenticate the peer (i.e. the Clearing House). -To set up a local test environment, the docker container running the Trusted Connector for the Clearing House -needs to be named `provider-core` and use the `provider-keystore.p12` as keystore. - -The host running the test must include the line -``` -127.0.0.1 provider-core -``` - -in its `/etc/hosts` file. Remote setups for testing will need to adapt the settings accordingly. - -To run the tests use -``` -./gradlew integrationTest -``` diff --git a/clearing-house-processors/bin/.gitignore b/clearing-house-processors/bin/.gitignore deleted file mode 100644 index d97dd0c..0000000 --- a/clearing-house-processors/bin/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/main/ -/default/ diff --git a/clearing-house-processors/bnd.bnd b/clearing-house-processors/bnd.bnd deleted file mode 100644 index 61c34cb..0000000 --- a/clearing-house-processors/bnd.bnd +++ /dev/null @@ -1,7 +0,0 @@ -Bundle-Name: IDS :: Clearing House Processors -Bundle-Description: Support Processors required by Clearing House -Export-Package: \ - de.fhg.aisec.ids.clearinghouse -Import-Package: \ - !org.checkerframework.checker*,\ - * diff --git a/clearing-house-processors/build.gradle.kts b/clearing-house-processors/build.gradle.kts deleted file mode 100644 index 4c0ad8a..0000000 --- a/clearing-house-processors/build.gradle.kts +++ /dev/null @@ -1,145 +0,0 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -import java.io.FileInputStream -import java.util.* - -plugins { - java - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.spring.dependencyManagement) - `maven-publish` -} - -group = "de.fhg.aisec.ids.clearinghouse" - -val fis = FileInputStream("../clearing-house-app/logging-service/Cargo.toml") -val props = Properties() -props.load(fis) -version = props.getProperty("version").removeSurrounding("\"") - -sourceSets{ - create("intTest"){ - } -} - -val intTestImplementation: Configuration by configurations.getting { - extendsFrom(configurations.testImplementation.get()) -} - -configurations["intTestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get()) - -val integrationTest = task("integrationTest") { - // set to true for debugging - testLogging.showStandardStreams = false - useJUnitPlatform() - - description = "Runs integration tests." - group = "verification" - - testClassesDirs = sourceSets["intTest"].output.classesDirs - classpath = sourceSets["intTest"].runtimeClasspath - shouldRunAfter("test") -} - -tasks.register("printChVersion") { - - doFirst { - println(version) - } -} - -buildscript { - repositories { - mavenCentral() - - fun findProperty(s: String) = project.findProperty(s) as String? - - maven { - name = "GitHubPackages" - - url = uri("https://maven.pkg.github.com/Fraunhofer-AISEC/ids-clearing-house-service") - credentials(HttpHeaderCredentials::class) { - name = findProperty("github.username") - value = findProperty("github.token") - } - authentication { - create("header") - } - } - } -} - -publishing { - fun findProperty(s: String) = project.findProperty(s) as String? - - publications { - create("binary") { - artifact(tasks["jar"]) - } - } - repositories { - maven { - name = "GitHubPackages" - - url = uri("https://maven.pkg.github.com/Fraunhofer-AISEC/ids-clearing-house-service") - credentials(HttpHeaderCredentials::class) { - name = findProperty("github.username") - value = findProperty("github.token") - } - authentication { - create("header") - } - } - } -} - -repositories { - mavenCentral() - // References IAIS repository that contains the infomodel artifacts - maven("https://maven.iais.fraunhofer.de/artifactory/eis-ids-public/") -} - -dependencies { - // Imported from IDS feature in TC at runtime - implementation(libs.infomodel.model) - implementation(libs.infomodel.serializer) - - implementation(libs.camel.idscp2) - implementation(libs.camel.core) - implementation(libs.camel.api) - implementation(libs.camel.jetty) - - implementation(libs.apacheHttp.core) - implementation(libs.apacheHttp.client) - implementation(libs.apacheHttp.mime) - implementation(libs.commons.fileupload) - implementation(libs.ktor.auth) - implementation(libs.ktor.auth.jwt) - compileOnly(libs.spring.context) - - testApi(libs.slf4j.simple) - testImplementation(libs.idscp2.core) - testImplementation(libs.junit5) - testImplementation(libs.okhttp3) - testImplementation(kotlin("test")) - testImplementation(libs.kotlin.serialization.json) -} - -tasks.withType { - kotlinOptions { - jvmTarget = "17" - } -} - -tasks.withType { - options.encoding = "UTF-8" - sourceCompatibility = JavaVersion.VERSION_17.toString() - targetCompatibility = JavaVersion.VERSION_17.toString() -} - -tasks.jar { - manifest { - attributes(mapOf(Pair("Bundle-Vendor", "Fraunhofer AISEC"), - Pair("-noee", true))) - } -} diff --git a/clearing-house-processors/gradle/libs.versions.toml b/clearing-house-processors/gradle/libs.versions.toml deleted file mode 100644 index 91cbd1e..0000000 --- a/clearing-house-processors/gradle/libs.versions.toml +++ /dev/null @@ -1,71 +0,0 @@ -[versions] -idscp2 = "0.17.0" -ktlint = "0.48.2" - -# Kotlin library/compiler version -kotlin = "1.8.0" -kotlinxCoroutines = "1.6.4" -kotlinxSerialization = "1.4.0" - -# HTTP client -ktor = "2.2.3" -okhttp = "4.9.1" - -# The used version of the infomodel from IESE -infomodel = "4.1.3" - -camel = "3.18.5" -slf4j = "2.0.0" -junit5 = "5.9.2" -mockito = "5.1.1" -httpcore = "4.4.15" -httpclient = "4.5.14" - -# Needed for camel multipart processor -commonsFileUpload = "1.4" - -springBoot = "3.0.2" -springframework = "6.0.4" - -[libraries] -# common libraries -slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } -slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } -camel-core = { group = "org.apache.camel", name = "camel-core", version.ref = "camel" } -camel-api = { group = "org.apache.camel", name = "camel-api", version.ref = "camel" } -okhttp3 = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } -ktor-auth = { group = "io.ktor", name = "ktor-server-auth", version.ref = "ktor" } -ktor-auth-jwt = { group = "io.ktor", name = "ktor-server-auth-jwt", version.ref = "ktor" } -spring-context = { group = "org.springframework", name = "spring-context", version.ref = "springframework"} - -# common test libraries -mockito = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } -camel-test = { group = "org.apache.camel", name = "camel-test", version.ref = "camel" } -junit5 = { group = "org.junit.jupiter", name = "junit-jupiter", version.ref = "junit5" } -kotlin-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } - -# camel-multipart-processor -camel-jetty = { group = "org.apache.camel", name = "camel-jetty", version.ref = "camel" } -camel-http = { group = "org.apache.camel", name = "camel-http", version.ref = "camel" } -apacheHttp-core = { group = "org.apache.httpcomponents", name = "httpcore", version.ref = "httpcore" } -apacheHttp-client = { group = "org.apache.httpcomponents", name = "httpclient", version.ref = "httpclient" } -apacheHttp-mime = { group = "org.apache.httpcomponents", name = "httpmime", version.ref = "httpclient" } -commons-fileupload = { group = "commons-fileupload", name = "commons-fileupload", version.ref = "commonsFileUpload" } - -# camel-processors -camel-idscp2 = { group = "de.fhg.aisec.ids", name = "camel-idscp2", version.ref = "idscp2" } -infomodel-model = { group = "de.fraunhofer.iais.eis.ids.infomodel", name = "java", version.ref = "infomodel" } -infomodel-serializer = { group = "de.fraunhofer.iais.eis.ids", name = "infomodel-serializer", version.ref = "infomodel" } - -# for tests -idscp2-core = { group = "de.fhg.aisec.ids", name = "idscp2-core", version.ref = "idscp2" } - -[bundles] -test5 = ["junit5", "mockito"] - -[plugins] -springboot = { id = "org.springframework.boot", version.ref = "springBoot" } -spring-dependencyManagement = { id = "io.spring.dependency-management", version = "1.0.13.RELEASE" } -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlin-plugin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/clearing-house-processors/gradle/wrapper/gradle-wrapper.jar b/clearing-house-processors/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 249e5832f090a2944b7473328c07c9755baa3196..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60756 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 '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/clearing-house-processors/gradlew.bat b/clearing-house-processors/gradlew.bat deleted file mode 100644 index 53a6b23..0000000 --- a/clearing-house-processors/gradlew.bat +++ /dev/null @@ -1,91 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/clearing-house-processors/settings.gradle.kts b/clearing-house-processors/settings.gradle.kts deleted file mode 100644 index 9bc9f2f..0000000 --- a/clearing-house-processors/settings.gradle.kts +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = "clearing-house-processors" - diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/Utility.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/Utility.kt deleted file mode 100644 index 3a4854b..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/Utility.kt +++ /dev/null @@ -1,217 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse - -import de.fhg.aisec.ids.clearinghouse.multipart.MultipartEndpointTest -import de.fhg.aisec.ids.idscp2.daps.aisecdaps.AisecDapsDriver -import de.fhg.aisec.ids.idscp2.daps.aisecdaps.AisecDapsDriverConfig -import de.fhg.aisec.ids.idscp2.keystores.KeyStoreUtil.loadKeyStore -import de.fraunhofer.iais.eis.DynamicAttributeToken -import de.fraunhofer.iais.eis.DynamicAttributeTokenBuilder -import de.fraunhofer.iais.eis.LogMessageBuilder -import de.fraunhofer.iais.eis.Message -import de.fraunhofer.iais.eis.QueryMessageBuilder -import de.fraunhofer.iais.eis.RequestMessageBuilder -import de.fraunhofer.iais.eis.TokenFormat -import de.fraunhofer.iais.eis.ids.jsonld.Serializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.int -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.jsonPrimitive -import okhttp3.Headers -import okhttp3.MultipartReader -import java.net.URI -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets -import java.nio.file.Path -import java.nio.file.Paths -import java.time.LocalDateTime -import java.util.Base64 -import java.util.Objects -import javax.xml.datatype.DatatypeFactory - -@Serializable -data class ChJwt(val transaction_id: String, - val timestamp: Int, - val process_id: String, - val document_id: String, - val payload: String, - val chain_hash: String, - val client_id: String, - val clearing_house_version: String) - -@Serializable -private data class ChReceipt(val data: String) - -@Serializable -data class QueryResult(val date_from: String, - val date_to: String, - val page: Int, - val size: Int, - val order: String, - val documents: List) - -@Serializable -data class OwnerList(val owners: List) - - -enum class MessageType{ - LOG, PID, QUERY -} - -class Utility { - companion object{ - - val CONNECTOR_1 = "D2:70:FE:7F:32:BB:37:BF:DF:F4:08:36:6B:F1:9E:7A:EB:A4:2D:2A:keyid:CB:8C:C7:B6:85:79:A8:23:A6:CB:15:AB:17:50:2F:E6:65:43:5D:E8" - val CONNECTOR_2 = "13:09:2E:1C:50:9B:8B:77:DE:01:1F:3B:B5:E0:D2:CC:1B:C5:88:9E:keyid:CB:8C:C7:B6:85:79:A8:23:A6:CB:15:AB:17:50:2F:E6:65:43:5D:E8" - - val STATUS_400 = "Bad Request" - val STATUS_401 = "Unauthorized" - val STATUS_403 = "Forbidden" - val STATUS_404 = "Not Found" - val STATUS_500 = "Internal Server Error" - - private val TEST_RUN_ID = (0..2147483647).random() - - private val SERIALIZER = Serializer() - - val keyStorePath: Path = Paths.get( - Objects.requireNonNull( - MultipartEndpointTest::class.java.classLoader - .getResource("ssl/client-keystore.p12") - ).path - ) - - val keyStorePathOtherClient: Path = Paths.get( - Objects.requireNonNull( - MultipartEndpointTest::class.java.classLoader - .getResource("ssl/server-keystore.p12") - ).path - ) - - val trustStorePath: Path = Paths.get( - Objects.requireNonNull( - MultipartEndpointTest::class.java.classLoader - .getResource("ssl/truststore.p12") - ).path - ) - - val password = "password".toCharArray() - - // Load certificates from local KeyStore - val ks = loadKeyStore(keyStorePath, password) - val ksOtherClient = loadKeyStore(keyStorePathOtherClient, password) - - val dapsDriver = AisecDapsDriver( - AisecDapsDriverConfig.Builder() - .setKeyStorePath(keyStorePath) - .setKeyStorePassword(password) - .setKeyPassword(password) - .setKeyAlias("1") - .setTrustStorePath(trustStorePath) - .setTrustStorePassword(password) - .setDapsUrl("https://daps-dev.aisec.fraunhofer.de/v4") - .loadTransportCertsFromKeystore(ks) - .build() - ) - - val dapsDriverOtherClient = AisecDapsDriver( - AisecDapsDriverConfig.Builder() - .setKeyStorePath(keyStorePathOtherClient) - .setKeyStorePassword(password) - .setKeyPassword(password) - .setKeyAlias("1") - .setTrustStorePath(trustStorePath) - .setTrustStorePassword(password) - .setDapsUrl("https://daps-dev.aisec.fraunhofer.de/v4") - .loadTransportCertsFromKeystore(ksOtherClient) - .build() - ) - - fun formatId(id: String): String{ - return "${id}_${TEST_RUN_ID}" - } - - fun getDapsToken(token: ByteArray = dapsDriver.token): DynamicAttributeToken{ - return DynamicAttributeTokenBuilder() - ._tokenFormat_(TokenFormat.JWT) - ._tokenValue_(String(token, StandardCharsets.UTF_8)) - .build() - } - - fun checkIdsMessage(m: String, c: Class){ - SERIALIZER.deserialize(m, c) - } - - private fun getPart(headers: Headers): String{ - val partName = headers["Content-Disposition"]!!.split(";")[1].split("=")[1] - return partName.substring(1, partName.length-1) - } - - fun getParts(reader: MultipartReader): Pair{ - var header = "" - var payload = "" - reader.use { - while (true) { - val part = reader.nextPart() ?: break - when (getPart(part.headers)){ - "header" -> { - header = part.body.readString(Charset.forName("utf-8")) - } - "payload" -> { - payload = part.body.readString(Charset.forName("utf-8")) - } - } - } - } - return Pair(header, payload) - } - - fun parseJwt(receipt: String): ChJwt{ - val data = Json.decodeFromString(receipt) - val chunks: List = data.data.split(".") - val decoder: Base64.Decoder = Base64.getUrlDecoder() - val payload = String(decoder.decode(chunks[1])) - return Json.decodeFromString(payload) - } - - fun parseQueryResult(body: String): QueryResult{ - val json = Json.parseToJsonElement(body).jsonObject - return QueryResult( - json["date_from"]!!.jsonPrimitive.content, - json["date_to"]!!.jsonPrimitive.content, - json["page"]!!.jsonPrimitive.int, - json["size"]!!.jsonPrimitive.int, - json["order"]!!.jsonPrimitive.content, - json["documents"]!!.jsonArray.map { it.toString() } - ) - } - - fun getMessage(type: MessageType, token: DynamicAttributeToken): Message{ - when (type) { - MessageType.LOG -> return LogMessageBuilder() - ._securityToken_(token) - ._issuerConnector_(URI.create("http://ch-ids.aisec.fraunhofer.de/idscp-client")) - ._issued_(DatatypeFactory.newInstance().newXMLGregorianCalendar(LocalDateTime.now().toString())) - ._senderAgent_(URI.create("http://ch-ids.aisec.fraunhofer.de/idscp-client")) - ._modelVersion_("4.0") - .build() - MessageType.QUERY -> return QueryMessageBuilder() - ._securityToken_(token) - ._issuerConnector_(URI.create("http://ch-ids.aisec.fraunhofer.de/idscp-client")) - ._issued_(DatatypeFactory.newInstance().newXMLGregorianCalendar(LocalDateTime.now().toString())) - ._senderAgent_(URI.create("http://ch-ids.aisec.fraunhofer.de/idscp-client")) - ._modelVersion_("4.0") - .build() - MessageType.PID -> return RequestMessageBuilder() - ._securityToken_(token) - ._issuerConnector_(URI.create("http://ch-ids.aisec.fraunhofer.de/idscp-client")) - ._issued_(DatatypeFactory.newInstance().newXMLGregorianCalendar(LocalDateTime.now().toString())) - ._senderAgent_(URI.create("http://ch-ids.aisec.fraunhofer.de/idscp-client")) - ._modelVersion_("4.0") - .build() - } - } - } -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/CreatePidTests.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/CreatePidTests.kt deleted file mode 100644 index a17279f..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/CreatePidTests.kt +++ /dev/null @@ -1,124 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.idscp2 - -import de.fhg.aisec.ids.clearinghouse.OwnerList -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.STATUS_400 -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.STATUS_403 -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.formatId -import de.fraunhofer.iais.eis.Message -import de.fraunhofer.iais.eis.MessageProcessedNotificationMessage -import de.fraunhofer.iais.eis.RejectionMessage -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import org.junit.Assert -import org.junit.jupiter.api.Test - -class CreatePidTests { - - @Test - fun createPid1(){ - val pid = formatId("idscp-pid1") - val owners = null - - // Test: createPid with no extra owners - succCreatePid(pid, owners) - } - - @Test - fun createPid2(){ - val pid = formatId("idscp-pid2") - val owners = listOf(Utility.CONNECTOR_2) - - // Test: createPid with an extra owner - succCreatePid(pid, owners) - } - - @Test - fun createPid3(){ - val pid = formatId("idscp-pid3") - val owners = listOf(Utility.CONNECTOR_1, Utility.CONNECTOR_2) - - // Test: createPid with duplicate self in owner list - succCreatePid(pid, owners) - } - - @Test - fun createPid4(){ - val pid = formatId("idscp-pid4") - val owners = listOf(Utility.CONNECTOR_2, Utility.CONNECTOR_2) - - // Test: createPid with duplicate other owner in owner list - succCreatePid(pid, owners) - } - - @Test - fun createPid5(){ - val pid = formatId("idscp-pid5") - val owners = null - - // Preparation: create PID - succCreatePid(pid, owners) - - // Test: Try to create existing PID (to which user has access) - failCreatePid(pid, owners, STATUS_400) - } - - @Test - fun createPid6(){ - val pid = formatId("idscp-pid6") - val owners = null - - // Preparation: create PID - succCreatePid(pid, owners, client = 2) - - // Test: Try to create existing PID (to which user has access) - failCreatePid(pid, owners, STATUS_403) - } - - @Test - fun createPid7(){ - val pid = formatId("idscp-pid7") - val owners = "{\"owners\": [\"${Utility.CONNECTOR_2}\",]}" - - // Test: createPid with invalid owner list - val (resultMessage, resultPayload, _) = Idscp2EndpointTest.pidMessage(pid, owners) - - // check IDS message type - Assert.assertTrue(resultMessage is RejectionMessage) - // createPid returns the created PID, but in quotes - val p = String(resultPayload!!) - Assert.assertEquals("Unexpected status message", STATUS_400, p) - } - - companion object{ - - fun succCreatePid(pid: String, owners: List?, client: Int = 1){ - val (resultMessage, resultPayload, _) = callCreatePid(pid, owners, client) - - // check IDS message type - Assert.assertTrue(resultMessage is MessageProcessedNotificationMessage) - // createPid returns the created PID, but in quotes - val p = String(resultPayload!!) - val createdPid = p.substring(1, p.length-1) - Assert.assertEquals("Returned PID does not match given PID!", pid, createdPid) - } - - fun failCreatePid(pid: String, owners: List?, em: String){ - val (resultMessage, resultPayload, _) = callCreatePid(pid, owners) - // check IDS message type - Assert.assertTrue(resultMessage is RejectionMessage) - // payload = http status code message - val p = String(resultPayload!!) - Assert.assertEquals("Unexpected status code message", em, p) - } - - private fun callCreatePid(pid: String, owners: List?, c: Int = 1): Triple?> { - var list = "" - if (owners != null) { - list = Json.encodeToString(OwnerList(owners)) - } - return Idscp2EndpointTest.pidMessage(pid, list, client=c) - } - } - -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/Idscp2Client.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/Idscp2Client.kt deleted file mode 100644 index af4e85d..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/Idscp2Client.kt +++ /dev/null @@ -1,107 +0,0 @@ -/*- - * ========================LICENSE_START================================= - * idscp2-examples - * %% - * Copyright (C) 2021 Fraunhofer AISEC - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * =========================LICENSE_END================================== - */ -package de.fhg.aisec.ids.clearinghouse.idscp2 - -import de.fhg.aisec.ids.idscp2.api.FastLatch -import de.fhg.aisec.ids.idscp2.api.configuration.Idscp2Configuration -import de.fhg.aisec.ids.idscp2.api.connection.Idscp2ConnectionAdapter -import de.fhg.aisec.ids.idscp2.api.raregistry.RaProverDriverRegistry -import de.fhg.aisec.ids.idscp2.api.raregistry.RaVerifierDriverRegistry -import de.fhg.aisec.ids.idscp2.applayer.AppLayerConnection -import de.fhg.aisec.ids.idscp2.defaultdrivers.remoteattestation.dummy.RaProverDummy2 -import de.fhg.aisec.ids.idscp2.defaultdrivers.remoteattestation.dummy.RaVerifierDummy2 -import de.fhg.aisec.ids.idscp2.defaultdrivers.securechannel.tls13.NativeTLSDriver -import de.fhg.aisec.ids.idscp2.defaultdrivers.securechannel.tls13.NativeTlsConfiguration -import de.fraunhofer.iais.eis.Message -import de.fraunhofer.iais.eis.ids.jsonld.Serializer -import org.slf4j.LoggerFactory -import java.nio.charset.StandardCharsets - -class Idscp2Client constructor( - private val configuration: Idscp2Configuration, - private val nativeTlsConfiguration: NativeTlsConfiguration -) { - - init{ - // register ra drivers - RaProverDriverRegistry.registerDriver( - RaProverDummy2.RA_PROVER_DUMMY2_ID, ::RaProverDummy2, null - ) - - RaVerifierDriverRegistry.registerDriver( - RaVerifierDummy2.RA_VERIFIER_DUMMY2_ID, ::RaVerifierDummy2, null - ) - } - - fun send(message: Message, headers: Map?, payload: ByteArray?): Triple?>{ - var resultMessage: Message? = null - var resultPayload: ByteArray? = null - var resultHeaders: Map? = null - - // Use this latch for waiting - val latch = FastLatch() - - val secureChannelDriver = NativeTLSDriver() - val connectionFuture = secureChannelDriver.connect(::AppLayerConnection, configuration, nativeTlsConfiguration) - connectionFuture.thenAccept { connection: AppLayerConnection -> - LOG.info("Client: New connection with id " + connection.id) - connection.addConnectionListener(object : Idscp2ConnectionAdapter() { - override fun onError(t: Throwable) { - LOG.error("Client connection error occurred", t) - } - - override fun onClose() { - LOG.info("Client: Connection with id " + connection.id + " has been closed") - latch.unlock() - } - }) - - connection.addIdsMessageListener { c: AppLayerConnection, m: Message?, data: ByteArray?, headers: Map -> - resultMessage = m - resultHeaders = headers - resultPayload = data - headers.forEach { (name, value) -> - LOG.debug("Found header '{}':'{}'", name, value) - } - LOG.debug("All headers logged!") - LOG.info("Received IDS message: " + Serializer().serialize(m)) - LOG.info("with payload: " + String(data!!, StandardCharsets.UTF_8)) - c.close() - } - - connection.unlockMessaging() - LOG.info("Send Message ...") - connection.sendIdsMessage(message, payload, headers) - LOG.info("Local DAT: " + String(connection.localDat, StandardCharsets.UTF_8)) - }.exceptionally { t: Throwable? -> - LOG.error("Client endpoint error occurred", t) - latch.unlock() - null - } - - // Wait until error or connection close - latch.await() - return Triple(resultMessage, resultPayload, resultHeaders) - } - - companion object { - private val LOG = LoggerFactory.getLogger(Idscp2Client::class.java) - } -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/Idscp2EndpointTest.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/Idscp2EndpointTest.kt deleted file mode 100644 index 65ddd82..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/Idscp2EndpointTest.kt +++ /dev/null @@ -1,108 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.idscp2 - -import de.fhg.aisec.ids.clearinghouse.MessageType -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.idscp2.api.configuration.AttestationConfig -import de.fhg.aisec.ids.idscp2.api.configuration.Idscp2Configuration -import de.fhg.aisec.ids.idscp2.defaultdrivers.remoteattestation.dummy.RaProverDummy2 -import de.fhg.aisec.ids.idscp2.defaultdrivers.remoteattestation.dummy.RaVerifierDummy2 -import de.fhg.aisec.ids.idscp2.defaultdrivers.securechannel.tls13.NativeTlsConfiguration -import de.fraunhofer.iais.eis.DynamicAttributeTokenBuilder -import de.fraunhofer.iais.eis.Message -import de.fraunhofer.iais.eis.TokenFormat - -class Idscp2EndpointTest { - - companion object { - - private val localAttestationConfig = AttestationConfig.Builder() - .setSupportedRaSuite(arrayOf(RaProverDummy2.RA_PROVER_DUMMY2_ID)) - .setExpectedRaSuite(arrayOf(RaVerifierDummy2.RA_VERIFIER_DUMMY2_ID)) - .setRaTimeoutDelay(300 * 1000L) // 300 seconds - .build() - - // create idscp2 config - private val settings = Idscp2Configuration.Builder() - .setAckTimeoutDelay(500) // 500 ms - .setHandshakeTimeoutDelay(5 * 1000L) // 5 seconds - .setAttestationConfig(localAttestationConfig) - .setDapsDriver(Utility.dapsDriver) - .build() - - private val settingsOtherClient = Idscp2Configuration.Builder() - .setAckTimeoutDelay(500) // 500 ms - .setHandshakeTimeoutDelay(5 * 1000L) // 5 seconds - .setAttestationConfig(localAttestationConfig) - .setDapsDriver(Utility.dapsDriverOtherClient) - .build() - - // create secureChannel config - - private val nativeTlsConfiguration = NativeTlsConfiguration.Builder() - .setKeyStorePath(Utility.keyStorePath) - .setKeyPassword(Utility.password) - .setKeyStorePassword(Utility.password) - .setTrustStorePath(Utility.trustStorePath) - .setTrustStorePassword(Utility.password) - .setCertificateAlias("1") - .setHost("tc-core-server") - .build() - - val client = Idscp2Client(settings, nativeTlsConfiguration) - - fun getMessage(type: MessageType, client: Int = 1): Message{ - return when (client){ - 2 -> Utility.getMessage(type, - Utility.getDapsToken(Utility.dapsDriverOtherClient.token) - ) - else -> Utility.getMessage(type, Utility.getDapsToken()) - } - } - - fun getInvalidMessage(type: MessageType): Message{ - val invToken = DynamicAttributeTokenBuilder() - ._tokenFormat_(TokenFormat.JWT) - ._tokenValue_("This is not a valid token!") - .build() - return Utility.getMessage(type, invToken) - } - - fun logMessage(pid: String, payload: String, authenticated: Boolean = true, client: Int = 1): Triple?> { - val m = if (authenticated){ - getMessage(MessageType.LOG, client) - } else{ - getInvalidMessage(MessageType.LOG) - } - val header = mapOf("ch-ids-pid" to pid) - val p = payload.toByteArray() - return Idscp2EndpointTest.client.send(m, header, p) - } - - fun pidMessage(pid: String, payload: String, authenticated: Boolean = true, client: Int = 1): Triple?> { - val m = if (authenticated){ - getMessage(MessageType.PID, client) - } else{ - getInvalidMessage(MessageType.PID) - } - val header = mapOf("ch-ids-pid" to pid, "Content-Type" to "application/json" ) - val p = payload.toByteArray() - return Idscp2EndpointTest.client.send(m, header, p) - } - - fun queryMessage(pid: String, id: String?, payload: String, authenticated: Boolean = true, client: Int = 1, page: Int = 1, size: Int = 100, sort: String = "desc"): Triple?> { - val m = if (authenticated){ - getMessage(MessageType.QUERY, client) - } else{ - getInvalidMessage(MessageType.QUERY) - } - val header = if (id != null){ - mapOf("ch-ids-pid" to pid, "ch-ids-id" to id, "Content-Type" to "application/json" ) - } - else{ - mapOf("ch-ids-pid" to pid, "ch-ids-page" to page.toString(), "ch-ids-size" to size.toString(), "ch-ids-sort" to sort, "Content-Type" to "application/json" ) - } - val p = payload.toByteArray() - return Idscp2EndpointTest.client.send(m, header, p) - } - } -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/LogMessageTests.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/LogMessageTests.kt deleted file mode 100644 index 9b7483b..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/LogMessageTests.kt +++ /dev/null @@ -1,77 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.idscp2 - -import de.fhg.aisec.ids.clearinghouse.ChJwt -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.STATUS_400 -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.STATUS_403 -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.formatId -import de.fhg.aisec.ids.clearinghouse.idscp2.CreatePidTests.Companion.succCreatePid -import de.fraunhofer.iais.eis.* -import org.junit.Assert -import org.junit.jupiter.api.Test - -class LogMessageTests { - @Test - fun logMessage1(){ - val pid = formatId("idscp-log1") - val payload = "This message is logged" - - // create Pid - succCreatePid(pid, null) - - // test: Logging to existing Pid - succLogMessage(pid, payload) - } - - @Test - fun logMessage2() { - val pid = formatId("idscp-log2") - val payload = "This message is logged" - - succLogMessage(pid, payload) - } - - @Test - fun logMessage3(){ - val pid = formatId("idscp-log3") - val payload = "" - - // test: Logging an empty payload - failLogMessage(pid, payload, STATUS_400) - } - - @Test - fun logMessage4(){ - val pid = formatId("idscp-log4") - val payload = "This message is logged" - - // create Pid - succCreatePid(pid, null, client = 2) - - // test: Logging to existing Pid - failLogMessage(pid, payload, STATUS_403) - } - - companion object{ - - fun failLogMessage(pid: String, payload: String, em: String) { - val (resultMessage, resultPayload, _) = Idscp2EndpointTest.logMessage(pid, payload) - // check IDS message type - Assert.assertTrue(resultMessage is RejectionMessage) - // payload = http status code message - val p = String(resultPayload!!) - Assert.assertEquals("Unexpected status code message", em, p) - } - - fun succLogMessage(pid: String, payload: String, c: Int = 1): ChJwt { - val (resultMessage, resultPayload, resultHeaders) = Idscp2EndpointTest.logMessage(pid, payload) - // check IDS message type - Assert.assertTrue(resultMessage is MessageProcessedNotificationMessage) - // check the pid from receipt in the payload. Does pid match with the given pid? - val receipt = Utility.parseJwt(String(resultPayload!!)) - Assert.assertEquals("Returned PID does not match given PID!", pid, receipt.process_id) - return receipt - } - } - -} \ No newline at end of file diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/QueryIdTests.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/QueryIdTests.kt deleted file mode 100644 index ffeb75d..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/QueryIdTests.kt +++ /dev/null @@ -1,71 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.idscp2 - -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.STATUS_404 -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.formatId -import de.fhg.aisec.ids.clearinghouse.idscp2.LogMessageTests.Companion.succLogMessage -import de.fraunhofer.iais.eis.RejectionMessage -import de.fraunhofer.iais.eis.ResultMessage -import org.junit.Assert -import org.junit.jupiter.api.Test - -class QueryIdTests { - - @Test - fun queryId1(){ - val pid = formatId("idscp-qid1") - - // create Pid with one document - val message = "This is the first message" - val receipt = LogMessageTests.succLogMessage(pid, message) - - // Test: query existing document - succQueryId(pid, receipt.document_id) - } - - @Test - fun queryId2(){ - val pid = formatId("idscp-qid2") - - // create Pid with one document - val message = "This is the first message" - succLogMessage(pid, message) - - // Test: query non-existing document - failQueryId(pid, "unknown-id", STATUS_404) - } - - @Test - fun queryId3(){ - val pid1 = formatId("idscp-qid2_with_doc") - val pid2 = formatId("idscp-qid2_without_doc") - - // create one Pid with one document and another with no documents - val message = "This is the first message" - val receipt = succLogMessage(pid1, message) - CreatePidTests.succCreatePid(pid2, null) - - // Test: query existing document in wrong pid - failQueryId(pid2, receipt.document_id, STATUS_404) - } - - companion object{ - - fun failQueryId(pid: String, id: String?, em: String) { - val (resultMessage, resultPayload, resultHeaders) = Idscp2EndpointTest.queryMessage(pid, id, "") - // check IDS message type - Assert.assertTrue(resultMessage is RejectionMessage) - // payload = http status code message - val p = String(resultPayload!!) - Assert.assertEquals("Unexpected status code message", em, p) - } - - fun succQueryId(pid: String, id: String): String { - val (resultMessage, resultPayload, resultHeaders) = Idscp2EndpointTest.queryMessage(pid, id, "") - // check IDS message type - Assert.assertTrue(resultMessage is ResultMessage) - //TODO: can't serialize json. array is of type "message + payload + payload type" - val p = String(resultPayload!!) - return p - } - } -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/QueryPidTests.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/QueryPidTests.kt deleted file mode 100644 index 50430ba..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/idscp2/QueryPidTests.kt +++ /dev/null @@ -1,109 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.idscp2 - -import de.fhg.aisec.ids.clearinghouse.QueryResult -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.STATUS_403 -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.STATUS_404 -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.formatId -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.parseQueryResult -import de.fhg.aisec.ids.clearinghouse.idscp2.CreatePidTests.Companion.succCreatePid -import de.fhg.aisec.ids.clearinghouse.idscp2.LogMessageTests.Companion.succLogMessage -import de.fhg.aisec.ids.clearinghouse.idscp2.QueryIdTests.Companion.failQueryId -import de.fraunhofer.iais.eis.ResultMessage -import org.junit.Assert -import org.junit.jupiter.api.Test - -class QueryPidTests { - @Test - fun queryPid1(){ - val pid = formatId("idscp-qpid1") - - // create Pid - succCreatePid(pid, null) - - // Test: query existing Pid with no documents - val result = succQueryPid(pid) - Assert.assertEquals("Should receive empty array!", 0, result.documents.size) - } - - @Test - fun queryPid2(){ - val pid = formatId("idscp-qpid2") - - // create Pid with three messages - val messages = listOf("This is the first message", "This is the second message", "This is the third message") - messages.forEach{ - succLogMessage(pid, it) - } - - // Test: query existing Pid with three documents - val result = succQueryPid(pid) - Assert.assertEquals("Should receive array of size three!", 3, result.documents.size) - } - - @Test - fun queryPid3(){ - val pid = formatId("idscp-qpid3") - val owners = listOf(Utility.CONNECTOR_1) - - // create Pid with other user, but user 1 is also authorized - succCreatePid(pid, owners, client = 2) - - // add three messages - val messages = listOf("This is the first message", "This is the second message", "This is the third message") - messages.forEach{ - succLogMessage(pid, it, c = 2) - } - - // Test: query existing Pid with user (who did not create pid, but is authorized) - val result = succQueryPid(pid) - Assert.assertEquals("Should receive array of size three!", 3, result.documents.size) - } - - @Test - fun queryPid4(){ - val pid = formatId("idscp-qpid4") - - // Test: query non-existing Pid - failQueryPid(pid, STATUS_404) - } - - @Test - fun queryPid5(){ - val pid = formatId("idscp-qpid5") - - // create Pid with other user - succCreatePid(pid, null, client = 2) - - // Test: query existing Pid with user (for which he is not authorized) - failQueryPid(pid, STATUS_403) - } - - @Test - fun queryPid6(){ - val pid = formatId("idscp-qpid6") - - // create Pid - succLogMessage(pid, "This is the log message!") - - // Test: query non existing page results in empty array - val result = succQueryPid(pid, 2) - Assert.assertEquals("Should receive empty array!", 0, result.documents.size) - } - - companion object{ - - fun failQueryPid(pid: String, em: String) { - return failQueryId(pid, null, em) - } - - fun succQueryPid(pid: String, page: Int = 1, size: Int = 100, sort: String = "desc"): QueryResult { - val (resultMessage, resultPayload, resultHeaders) = Idscp2EndpointTest.queryMessage(pid, null, "", page=page, size=size, sort=sort) - // check IDS message type - Assert.assertTrue(resultMessage is ResultMessage) - // check the pid from receipt in the payload. Does pid match with the given pid? - return parseQueryResult(String(resultPayload!!)) - } - } - -} \ No newline at end of file diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/CreatePidTests.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/CreatePidTests.kt deleted file mode 100644 index a0e4c17..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/CreatePidTests.kt +++ /dev/null @@ -1,154 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.multipart - -import de.fhg.aisec.ids.clearinghouse.OwnerList -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.formatId -import de.fhg.aisec.ids.clearinghouse.multipart.MultipartEndpointTest.Companion.client -import de.fhg.aisec.ids.clearinghouse.multipart.MultipartEndpointTest.Companion.otherClient -import de.fraunhofer.iais.eis.MessageProcessedNotificationMessage -import de.fraunhofer.iais.eis.RejectionMessage -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import okhttp3.MultipartReader -import okhttp3.Response -import org.junit.Assert -import org.junit.jupiter.api.Test - -class CreatePidTests { - - @Test - fun createPid1(){ - val pid = formatId("mp-pid1") - val owners = null - - // Test: createPid with no extra owners - succCreatePid(pid, owners) - } - - @Test - fun createPid2(){ - val pid = formatId("mp-pid2") - val owners = listOf(Utility.CONNECTOR_2) - - // Test: createPid with an extra owner - succCreatePid(pid, owners) - } - - @Test - fun createPid3(){ - val pid = formatId("mp-pid3") - val owners = listOf(Utility.CONNECTOR_1, Utility.CONNECTOR_2) - - // Test: createPid with duplicate self in owner list - succCreatePid(pid, owners) - } - - @Test - fun createPid4(){ - val pid = formatId("mp-pid4") - val owners = listOf(Utility.CONNECTOR_2, Utility.CONNECTOR_2) - - // Test: createPid with duplicate other owner in owner list - succCreatePid(pid, owners) - } - - @Test - fun createPid5(){ - val pid = formatId("mp-pid5") - val owners = null - - // Preparation: create PID - succCreatePid(pid, owners) - - // Test: Try to create existing PID (to which user has access) - failCreatePid(pid, owners, 400) - } - - @Test - fun createPid6(){ - val pid = formatId("mp-pid6") - val owners = null - - // Preparation: create PID - succCreatePid(pid, owners, client=2) - - // Test: Try to create existing PID (to which user has access) - failCreatePid(pid, owners, 403) - } - - @Test - fun createPid7(){ - val pid = formatId("mp-pid7") - val owners = "{\"owners\": [\"${Utility.CONNECTOR_2}\",]}" - - // Test: createPid with invalid owner list - val call = client.newCall(MultipartClient.pidMessage(pid, owners)) - val response = call.execute() - - // check http status code - Assert.assertEquals("Unexpected http status code!", 400, response.code) - // check IDS message type - val parts = Utility.getParts(MultipartReader(response.body!!)) - Utility.checkIdsMessage(parts.first, RejectionMessage::class.java) - response.close() - } - - @Test - fun createPid8(){ - val pid = formatId("mp-pid8") - - // Test: Create Pid without matching aki:ski in certificate - failEarlyCreatePid(pid, null, 401) - } - - - companion object{ - - fun succCreatePid(pid: String, owners: List?, client: Int = 1){ - val response = callCreatePid(pid, owners, client) - val parts = Utility.getParts(MultipartReader(response.body!!)) - // check http status code - Assert.assertEquals("Unexpected http status code!", 201, response.code) - // check IDS message type - Utility.checkIdsMessage(parts.first, MessageProcessedNotificationMessage::class.java) - // createPid returns the created PID, but in quotes - val createdPid = parts.second.substring(1, parts.second.length-1) - Assert.assertEquals("Returned PID does not match given PID!", pid, createdPid) - response.close() - } - - fun failCreatePid(pid: String, owners: List?, code: Int){ - val response = callCreatePid(pid, owners) - val parts = Utility.getParts(MultipartReader(response.body!!)) - // check http status code - Assert.assertEquals("Unexpected http status code!", code, response.code) - // check IDS message type - Utility.checkIdsMessage(parts.first, RejectionMessage::class.java) - response.close() - } - - private fun callCreatePid(pid: String, owners: List?, c: Int = 1): Response { - var list = "" - if (owners != null) { - list = Json.encodeToString(OwnerList(owners)) - } - val call = when (c) { - 1 -> client.newCall(MultipartClient.pidMessage(pid, list, client=c)) - else -> otherClient.newCall(MultipartClient.pidMessage(pid, list, client=c)) - } - return call.execute() - } - - fun failEarlyCreatePid(pid: String, owners: List?, code: Int){ - var list = "" - if (owners != null) { - list = Json.encodeToString(OwnerList(owners)) - } - val call = client.newCall(MultipartClient.pidMessage(pid, list, client=2)) - val response = call.execute() - // check http status code and message - Assert.assertEquals("Unexpected http status code!", code, response.code) - Assert.assertEquals("Unexpected message", "Unauthorized", response.message) - } - } -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/LogMessageTests.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/LogMessageTests.kt deleted file mode 100644 index 6f0d800..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/LogMessageTests.kt +++ /dev/null @@ -1,107 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.multipart - -import de.fhg.aisec.ids.clearinghouse.ChJwt -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.formatId -import de.fhg.aisec.ids.clearinghouse.multipart.CreatePidTests.Companion.succCreatePid -import de.fhg.aisec.ids.clearinghouse.multipart.MultipartEndpointTest.Companion.client -import de.fhg.aisec.ids.clearinghouse.multipart.MultipartEndpointTest.Companion.otherClient -import de.fraunhofer.iais.eis.MessageProcessedNotificationMessage -import de.fraunhofer.iais.eis.RejectionMessage -import okhttp3.MultipartReader -import org.junit.Assert -import org.junit.jupiter.api.Test - -class LogMessageTests { - @Test - fun logMessage1(){ - val pid = formatId("mp-log1") - val payload = "This message is logged" - - // create Pid - succCreatePid(pid, null) - - // test: Logging to existing Pid - succLogMessage(pid, payload) - } - - @Test - fun logMessage2(){ - val pid = formatId("mp-log2") - val payload = "This message is logged" - - // test: Logging to non-existing Pid - succLogMessage(pid, payload) - } - - @Test - fun logMessage3(){ - val pid = formatId("mp-log3") - val payload = "" - - // test: Logging an empty payload - failLogMessage(pid, payload, 400) - } - - @Test - fun logMessage4(){ - val pid = formatId("mp-log4") - val payload = "This message is logged" - - // create Pid - succCreatePid(pid, null, client=2) - - // test: Logging to existing Pid - failLogMessage(pid, payload, 403) - } - - @Test - fun logMessage5(){ - val pid = formatId("mp-log5") - val payload = "This message is logged" - - // Test: Logging without matching aki:ski in certificate - failEarlyLogMessage(pid, payload, 401) - } - - companion object{ - - fun failEarlyLogMessage(pid: String, payload: String, code: Int){ - val call = client.newCall(MultipartClient.logMessage(pid, payload, client=2)) - val response = call.execute() - // check http status code and message - Assert.assertEquals("Unexpected http status code!", code, response.code) - Assert.assertEquals("Unexpected message", "Unauthorized", response.message) - } - - fun failLogMessage(pid: String, payload: String, code: Int){ - val call = client.newCall(MultipartClient.logMessage(pid, payload)) - val response = call.execute() - // check http status code - Assert.assertEquals("Unexpected http status code!", code, response.code) - // check IDS message type - val parts = Utility.getParts(MultipartReader(response.body!!)) - Utility.checkIdsMessage(parts.first, RejectionMessage::class.java) - } - - fun succLogMessage(pid: String, payload: String, c: Int = 1): ChJwt { - val call = when (c) { - 1 -> client.newCall(MultipartClient.logMessage(pid, payload, client=c)) - else -> otherClient.newCall(MultipartClient.logMessage(pid, payload, client=c)) - } - val response = call.execute() - // check http status code - Assert.assertEquals("Unexpected http status code!", 201, response.code) - // check IDS message type - val parts = Utility.getParts(MultipartReader(response.body!!)) - Utility.checkIdsMessage(parts.first, MessageProcessedNotificationMessage::class.java) - // check the pid from receipt in the payload. Does pid match with the given pid? - val receipt = Utility.parseJwt(parts.second) - Assert.assertEquals("Returned PID does not match given PID!", pid, receipt.process_id) - response.close() - return receipt - } - - } - -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/MultipartClient.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/MultipartClient.kt deleted file mode 100644 index f98a2ed..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/MultipartClient.kt +++ /dev/null @@ -1,80 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.multipart - -import de.fhg.aisec.ids.clearinghouse.MessageType -import de.fraunhofer.iais.eis.Message -import de.fraunhofer.iais.eis.ids.jsonld.Serializer -import okhttp3.Headers -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.MultipartBody -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody - -class MultipartClient { - - companion object{ - private val SERIALIZER = Serializer() - private var JSON = "application/json; charset=utf-8".toMediaTypeOrNull()!! - - private val BASE_URL = "https://tc-core-server:9999/" - private val LOG_URL = "messages/log/" - private val QUERY_URL = "messages/query/" - private val PROCESS_URL = "process/" - - private fun makePart(name: String, payload: String, ctJson: Boolean): MultipartBody.Part{ - var headers = Headers.Builder().add("Content-Disposition", "form-data; name=\"$name\"") - val body = if (ctJson){ - payload.toRequestBody(JSON) - } - else{ - payload.toRequestBody() - } - - return MultipartBody.Part.create(headers.build(), body) - } - - private fun makeRequest(url: String, m: Message, payload: String, ctJson: Boolean): Request{ - val requestBody = MultipartBody.Builder() - .setType(MultipartBody.ALTERNATIVE) - .addPart(makePart("header", SERIALIZER.serialize(m), ctJson)) - .addPart(makePart("payload", payload, ctJson)) - .build() - - return Request.Builder() - .header("Authorization", "Bearer " + m.securityToken) - .url(url) - .post(requestBody) - .build() - } - - fun logMessage(pid: String, payload: String, authenticated: Boolean = true, client: Int = 1): Request{ - val m = if (authenticated){ - MultipartEndpointTest.getMessage(MessageType.LOG, client) - } else{ - MultipartEndpointTest.getInvalidMessage(MessageType.LOG) - } - val url = "$BASE_URL$LOG_URL$pid" - return makeRequest(url, m, payload, false) - } - - fun queryMessage(pid: String, id: String?, payload: String, authenticated: Boolean = true, client: Int = 1, page: Int = 1, size: Int = 100, sort: String = "desc"): Request{ - val m = if (authenticated){ - MultipartEndpointTest.getMessage(MessageType.QUERY, client) - } else{ - MultipartEndpointTest.getInvalidMessage(MessageType.QUERY) - } - val url = if (id == null) "$BASE_URL$QUERY_URL$pid?page=$page&size=$size&sort=$sort" else "$BASE_URL$QUERY_URL$pid/$id" - return makeRequest(url, m, payload, false) - } - - fun pidMessage(pid: String, payload: String, ctJson: Boolean = true, authenticated: Boolean = true, client: Int = 1): Request{ - val m = if (authenticated){ - MultipartEndpointTest.getMessage(MessageType.PID, client) - } else{ - MultipartEndpointTest.getInvalidMessage(MessageType.PID) - } - val url = "$BASE_URL$PROCESS_URL$pid" - return makeRequest(url, m, payload, ctJson) - } - - } -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/MultipartEndpointTest.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/MultipartEndpointTest.kt deleted file mode 100644 index 2e7b895..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/MultipartEndpointTest.kt +++ /dev/null @@ -1,63 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.multipart - -import de.fhg.aisec.ids.clearinghouse.MessageType -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.idscp2.keystores.PreConfiguration -import de.fraunhofer.iais.eis.DynamicAttributeTokenBuilder -import de.fraunhofer.iais.eis.Message -import de.fraunhofer.iais.eis.TokenFormat -import okhttp3.OkHttpClient -import javax.net.ssl.SSLContext - -class MultipartEndpointTest { - - companion object { - private val trustManager = PreConfiguration.getX509ExtTrustManager( - Utility.trustStorePath, - "password".toCharArray() - ) - - private val keyManagers = PreConfiguration.getX509ExtKeyManager( - "password".toCharArray(), - Utility.keyStorePath, - "password".toCharArray(), - ) - - private val keyManagersOtherClient = PreConfiguration.getX509ExtKeyManager( - "password".toCharArray(), - Utility.keyStorePathOtherClient, - "password".toCharArray(), - ) - - private val sslContext = SSLContext.getInstance("TLS").apply { - init(keyManagers, arrayOf(trustManager), null) - } - - private val sslContextOtherClient = SSLContext.getInstance("TLS").apply { - init(keyManagersOtherClient, arrayOf(trustManager), null) - } - - val client = OkHttpClient.Builder() - .sslSocketFactory(sslContext.socketFactory, trustManager) - .build() - - val otherClient = OkHttpClient.Builder() - .sslSocketFactory(sslContextOtherClient.socketFactory, trustManager) - .build() - - fun getMessage(type: MessageType, client: Int = 1): Message { - return when (client) { - 2 -> Utility.getMessage(type, Utility.getDapsToken(Utility.dapsDriverOtherClient.token)) - else -> Utility.getMessage(type, Utility.getDapsToken()) - } - } - - fun getInvalidMessage(type: MessageType): Message { - val invToken = DynamicAttributeTokenBuilder() - ._tokenFormat_(TokenFormat.JWT) - ._tokenValue_("This is not a valid token!") - .build() - return Utility.getMessage(type, invToken) - } - } -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/QueryIdTests.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/QueryIdTests.kt deleted file mode 100644 index 816247d..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/QueryIdTests.kt +++ /dev/null @@ -1,100 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.multipart - -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.formatId -import de.fhg.aisec.ids.clearinghouse.multipart.CreatePidTests.Companion.succCreatePid -import de.fhg.aisec.ids.clearinghouse.multipart.LogMessageTests.Companion.succLogMessage -import de.fhg.aisec.ids.clearinghouse.multipart.MultipartEndpointTest.Companion.client -import de.fraunhofer.iais.eis.RejectionMessage -import de.fraunhofer.iais.eis.ResultMessage -import okhttp3.MultipartReader -import org.junit.Assert -import org.junit.jupiter.api.Test -import java.net.ProtocolException - -class QueryIdTests { - - @Test - fun queryId1(){ - val pid = formatId("mp-qid1") - - // create Pid with one document - val message = "This is the first message" - val receipt = succLogMessage(pid, message) - - // Test: query existing document - succQueryId(pid, receipt.document_id) - } - - @Test - fun queryId2(){ - val pid = formatId("mp-qid2") - - // create Pid with one document - val message = "This is the first message" - succLogMessage(pid, message) - - // Test: query non-existing document - failQueryId(pid, "unknown-id", 404) - } - - @Test - fun queryId3(){ - val pid1 = formatId("mp-qid2_with_doc") - val pid2 = formatId("mp-qid2_without_doc") - - // create one Pid with one document and another with no documents - val message = "This is the first message" - val receipt = succLogMessage(pid1, message) - succCreatePid(pid2, null) - - // Test: query existing document in wrong pid - failQueryId(pid2, receipt.document_id, 404) - } - - @Test - fun queryId4(){ - val pid = formatId("mp-qid4") - - // create Pid with one document - val message = "This is the first message" - val receipt = succLogMessage(pid, message) - - // Test: query existing document - failEarlyQueryPid(pid, receipt.document_id, 401) - } - - companion object{ - fun failEarlyQueryPid(pid: String, id: String, code: Int){ - val call = client.newCall(MultipartClient.queryMessage(pid, id, "", client=2)) - val response = call.execute() - // check http status code and message - Assert.assertEquals("Unexpected http status code!", code, response.code) - Assert.assertEquals("Unexpected message", "Unauthorized", response.message) - } - - fun failQueryId(pid: String, id: String, code: Int){ - val call = client.newCall(MultipartClient.queryMessage(pid, id, "")) - val response = call.execute() - // check http status code - Assert.assertEquals("Unexpected http status code!", response.code, code) - // check IDS message type - val parts = Utility.getParts(MultipartReader(response.body!!)) - Utility.checkIdsMessage(parts.first, RejectionMessage::class.java) - response.close() - } - - fun succQueryId(pid: String, id: String): String{ - val call = client.newCall(MultipartClient.queryMessage(pid, id, "")) - val response = call.execute() - // check http status code - Assert.assertEquals("Unexpected http status code!", response.code, 200) - // check IDS message type - val parts = Utility.getParts(MultipartReader(response.body!!)) - Utility.checkIdsMessage(parts.first, ResultMessage::class.java) - //TODO: can't serialize. json array is of type "message + payload + payload type" - response.close() - return parts.second - } - } -} diff --git a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/QueryPidTests.kt b/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/QueryPidTests.kt deleted file mode 100644 index 1b47499..0000000 --- a/clearing-house-processors/src/intTest/java/de/fhg/aisec/ids/clearinghouse/multipart/QueryPidTests.kt +++ /dev/null @@ -1,141 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse.multipart - -import de.fhg.aisec.ids.clearinghouse.QueryResult -import de.fhg.aisec.ids.clearinghouse.Utility -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.CONNECTOR_1 -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.formatId -import de.fhg.aisec.ids.clearinghouse.Utility.Companion.parseQueryResult -import de.fhg.aisec.ids.clearinghouse.multipart.CreatePidTests.Companion.succCreatePid -import de.fhg.aisec.ids.clearinghouse.multipart.LogMessageTests.Companion.succLogMessage -import de.fhg.aisec.ids.clearinghouse.multipart.MultipartEndpointTest.Companion.client -import de.fraunhofer.iais.eis.RejectionMessage -import de.fraunhofer.iais.eis.ResultMessage -import okhttp3.MultipartReader -import org.junit.Assert -import org.junit.jupiter.api.Test -import java.net.ProtocolException - -class QueryPidTests { - @Test - fun queryPid1(){ - val pid = formatId("mp-qpid1") - - // create Pid - succCreatePid(pid, null) - - // Test: query existing Pid with no documents - val result = succQueryPid(pid) - Assert.assertEquals("Should receive empty array!", 0, result.documents.size) - } - - @Test - fun queryPid2(){ - val pid = formatId("mp-qpid2") - - // create Pid with three messages - val messages = listOf("This is the first message", "This is the second message", "This is the third message") - messages.forEach{ - succLogMessage(pid, it) - } - - // Test: query existing Pid with three documents - val result = succQueryPid(pid) - Assert.assertEquals("Should receive empty array!", 3, result.documents.size) - } - - @Test - fun queryPid3(){ - val pid = formatId("mp-qpid3") - val owners = listOf(CONNECTOR_1) - - // create Pid with other user, but user 1 is also authorized - succCreatePid(pid, owners, client=2) - - // add three messages - val messages = listOf("This is the first message", "This is the second message", "This is the third message") - messages.forEach{ - succLogMessage(pid, it, c=2) - } - - // Test: query existing Pid with user (who did not create pid, but is authorized) - val result = succQueryPid(pid) - Assert.assertEquals("Should receive empty array!", 3, result.documents.size) - } - - @Test - fun queryPid4(){ - val pid = formatId("mp-qpid4") - - // Test: query non-existing Pid - failQueryPid(pid, 404) - } - - @Test - fun queryPid5(){ - val pid = formatId("mp-qpid5") - - // create Pid with other user - succCreatePid(pid, null, client=2) - - // Test: query existing Pid with user (for which he is not authorized) - failQueryPid(pid, 403) - } - - @Test - fun queryPid6(){ - val pid = formatId("mp-qpid6") - - // create Pid - succLogMessage(pid, "This is the log message!") - - // Test: query non existing page results in empty array - val result = succQueryPid(pid, 2) - Assert.assertEquals("Should receive empty array!", 0, result.documents.size) - } - - @Test - fun queryPid7(){ - val pid = formatId("mp-qpid7") - - // create Pid - succLogMessage(pid, "This is the log message!") - - // Test: Query pid without matching aki:ski in certificate - failEarlyQueryPid(pid, 401) - } - - companion object{ - - fun failEarlyQueryPid(pid: String, code: Int){ - val call = client.newCall(MultipartClient.queryMessage(pid, null, "", client=2)) - val response = call.execute() - // check http status code and message - Assert.assertEquals("Unexpected http status code!", code, response.code) - Assert.assertEquals("Unexpected message", "Unauthorized", response.message) - } - - fun failQueryPid(pid: String, code: Int){ - val call = client.newCall(MultipartClient.queryMessage(pid, null, "")) - val response = call.execute() - // check http status code - Assert.assertEquals("Unexpected http status code!", code, response.code) - // check IDS message type - val parts = Utility.getParts(MultipartReader(response.body!!)) - Utility.checkIdsMessage(parts.first, RejectionMessage::class.java) - response.close() - } - - fun succQueryPid(pid: String, page: Int = 1, size: Int = 100, sort: String = "desc"): QueryResult{ - val call = client.newCall(MultipartClient.queryMessage(pid, null, "", page=page, size=size, sort=sort)) - val response = call.execute() - // check http status code - Assert.assertEquals("Unexpected http status code!", 200, response.code) - // check IDS message type - val parts = Utility.getParts(MultipartReader(response.body!!)) - Utility.checkIdsMessage(parts.first, ResultMessage::class.java) - val result = parseQueryResult(parts.second) - response.close() - return result - } - } -} diff --git a/clearing-house-processors/src/intTest/resources/simplelogger.properties b/clearing-house-processors/src/intTest/resources/simplelogger.properties deleted file mode 100644 index eafa2b0..0000000 --- a/clearing-house-processors/src/intTest/resources/simplelogger.properties +++ /dev/null @@ -1 +0,0 @@ -org.slf4j.simpleLogger.defaultLogLevel=debug \ No newline at end of file diff --git a/clearing-house-processors/src/intTest/resources/ssl/client-keystore.p12 b/clearing-house-processors/src/intTest/resources/ssl/client-keystore.p12 deleted file mode 100644 index 3a2ac465b6d4d623fd1430aeeefe03d6ad6169b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6366 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 diff --git a/clearing-house-processors/src/intTest/resources/ssl/provider-keystore.p12 b/clearing-house-processors/src/intTest/resources/ssl/provider-keystore.p12 deleted file mode 100644 index 7c45bd3087048bbef49cfcbf4d4cd40a338a82c2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2693 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 diff --git a/clearing-house-processors/src/intTest/resources/ssl/server-keystore.p12 b/clearing-house-processors/src/intTest/resources/ssl/server-keystore.p12 deleted file mode 100644 index 6c361effd41e5160328cef2941f122550764fcc4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6366 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

jX!>gfq{eeUruNXf&c&j diff --git a/clearing-house-processors/src/intTest/resources/ssl/truststore.p12 b/clearing-house-processors/src/intTest/resources/ssl/truststore.p12 deleted file mode 100644 index 80d7234ab4e1c20386686017fd5f455a56941141..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1271 zcmVuf&}vd0Ru3C1gr)LDuzgg_YDCD0ic2eodkjenJ|I`l`w(?kp>AWhDe6@ z4FLxRpn?Q~FoFbr0s#Opf&_O42`Yw2hW8Bt2LUi<1_>&LNQUwClCSwATSID2r7n1hW8Bu2?YQ! z9R>+thDZTr0|Wso1P}#Lx&!}6!U2L_%`U>I7|no!1OU)SBrm;M^GOqb07eyhdNpHG zi_|aBy3!6|8{cFpCTW7D+Xm=B@u9AG7a$+?IH<>g+-oO3rT3v0njQ4z{;liiBcOLO z#Lj6%z8rhLB*CP3{+hP*PIBn$q`D%%od$yBPI5NY5M-lQ@VTnLyx|JT2=u&dlE=K3 zjyr;>6!U!y0f%qC8ri)-@J6hl?9b9mzRVXG?NgBU+YF$MYEEFzXNsj3n*Wxq;D2kM zTyA6~KzfD8bSEpS`Q%nx8yJw5q&c`(d%r1&^NAr4L$}ity%LUe9mYYLEDvKP;=uBs zIhf@zeIBETfkI>0*2-aYzBF|g>nG8DQoZ=!R=1?x-l&%i4Gw6emRx7_%9A=7)#8yJ z;g-FC$u^*Xh4RQdaXAgV8o`rZF}2KPitt~V8-O$9J~G*!1d=oNfAvs)>3%9~^RtW# zXT~S?qiK#&xyA+fFzG9bBWJ1Hm8hBSBc!MX-!eoQ1rvXzTX0LoL?9g37zY)#tYN!R z=}Xk!J%6hQq(h{cm%3jp%z||R{)82A3hE!x;wq(+i2s6u%dp>ugxvLZ6+iJxN^qM) zH^@{#Uqel|CU~Yyg^OV>?ysl)89naH;Y40HgOF1*a0lN*Bo@kjFN1Di2anB4Q*17!qLUtd1|Aii3(L_Z}K=)+IrM5X%^N#6{aLz&m`@>NGoxQj?h|<5)!@!<(y^yRe zqz32Sn(!EG50JI&CBt;a8b%mPQWE)th;)qS$sFdVb72j zoJvINP<9Z%KGjV?PK~27x1+7c$Pu9+tr&CKGM=dS!jdy?DPA_yFm6Sx5w|p+w$S)2 zwwMy%c9=Gl4#F|+l?{`Kt%HtY`XeqwcKPVOs6QkPMjo$pT*9*Jk-z=PMcib`<={mo z;0eDUm?J8XnI7Gn;&el~P^w2OVY4JwfYKLP#=NXtr*Q22(5~ECIVA~hTeE+|3+4cxLHy6pL*@~j_{)7tvs~d z=q5ZV@+XH!P0JNwIrsJ2c@SRs^1;48iAZd&{$sNrY@r@&d(w7&wI4h?Ok0Am*?f1J z`q^by>j)yzrzg7^S%*Ze>e%mh<6~i6uDY3cPn0z$GBBt2;-qwhl#l#4D`m%=T_G69d zBcTX2WB*LV+6-lq#$6j@1!PQ%aEbbcfutL)oQOwMl>T7YS$^SBb|xgzhADx=<9>^1 z+bKCm3PF+EP egetIn.setHeader(IDS_HEADER, message) - } - - // clean up headers - egetIn.removeHeader(IDS_PROTOCOL) - egetIn.removeHeader(IDSCP_ID_HEADER) - egetIn.removeHeader(IDSCP_PID_HEADER) - egetIn.removeHeader(TYPE_HEADER) - } - - companion object { - private val LOG: Logger = LoggerFactory.getLogger(ClearingHouseExceptionProcessor::class.java) - } -} diff --git a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseInputValidationProcessor.kt b/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseInputValidationProcessor.kt deleted file mode 100644 index 38230e4..0000000 --- a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseInputValidationProcessor.kt +++ /dev/null @@ -1,116 +0,0 @@ -/*- - * ========================LICENSE_START================================= - * camel-multipart-processor - * %% - * Copyright (C) 2019 Fraunhofer AISEC - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * =========================LICENSE_END================================== - */ -package de.fhg.aisec.ids.clearinghouse - -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.CAMEL_HTTP_PATH -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDSCP_ID_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDSCP_PAGE_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDSCP_PID_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDSCP_SIZE_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDSCP_SORT_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDS_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDS_PROTOCOL -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.PROTO_IDSCP2 -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.TYPE_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.TYPE_JSON -import de.fraunhofer.iais.eis.Message -import de.fraunhofer.iais.eis.QueryMessage -import de.fraunhofer.iais.eis.RequestMessage -import org.apache.camel.Exchange -import org.apache.camel.Processor -import org.apache.http.entity.ContentType -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component - -@Component("chInputValidationProcessor") -class ClearingHouseInputValidationProcessor : Processor { - override fun process(exchange: Exchange) { - val egetIn = exchange.getIn() - val headers = egetIn.headers - val body = exchange.message.getBody(ByteArray::class.java) - - if (LOG.isTraceEnabled) { - LOG.trace("[IN] ${ClearingHouseInputValidationProcessor::class.java.simpleName}") - for (header in headers.keys) { - LOG.trace("Found header '{}':'{}'", header, headers[header]) - } - } - - // Prepare compound message for Clearing House Service API - val idsHeader = exchange.message.getHeader(IDS_HEADER) as Message - val contentTypeHeader = (headers[TYPE_HEADER] as String?) - val chMessage = ClearingHouseMessage(idsHeader, contentTypeHeader, body) - - LOG.info("idsmessage: {}", idsHeader.id) - - // Input validation: check that payload type of create pid message is application/json - if (chMessage.header is RequestMessage && idsHeader !is QueryMessage) { - val expectedContentType = ContentType.create("application/json") - if (expectedContentType.mimeType != chMessage.payloadType) { - LOG.warn("Expected application/json, got {}", chMessage.payloadType) - throw IllegalArgumentException("Expected content-type application/json") - } - } - - // Input validation: construct url from headers for IDSCP2 - if (headers[IDS_PROTOCOL] == PROTO_IDSCP2) { - if (chMessage.header is QueryMessage) { - val queryPath = if (headers.contains(IDSCP_ID_HEADER)) { - (headers[CAMEL_HTTP_PATH] as String) + "/" + (headers[IDSCP_ID_HEADER] as String) - } else { - var paginationPath = if (headers.contains(IDSCP_PAGE_HEADER)) { - (headers[CAMEL_HTTP_PATH] as String) + "?page=" + exchange.message.getHeader(IDSCP_PAGE_HEADER) - } else { - (headers[CAMEL_HTTP_PATH] as String) + "?page=1" - } - - if (headers.contains(IDSCP_SIZE_HEADER)) { - paginationPath = paginationPath + "&size=" + exchange.message.getHeader(IDSCP_SIZE_HEADER) - } - if (headers.contains(IDSCP_SORT_HEADER)) { - paginationPath = "$paginationPath?sort=desc" - } - paginationPath - } - exchange.getIn().setHeader(CAMEL_HTTP_PATH, queryPath) - } - } - - if (LOG.isTraceEnabled) { - LOG.trace("Received payload: {}", chMessage.payload) - } - - // store ids header for response processor and clean up idscp2 specific header - exchange.getIn().removeHeader(IDSCP_ID_HEADER) - exchange.getIn().removeHeader(IDSCP_PID_HEADER) - - // Remove current Content-Type header before setting the new one - exchange.getIn().removeHeader(TYPE_HEADER) - - // Copy Content-Type from payload part populate body with new payload - exchange.getIn().setHeader(TYPE_HEADER, TYPE_JSON) - exchange.getIn().body = chMessage.toJson() - } - - companion object { - private val LOG: Logger = LoggerFactory.getLogger(ClearingHouseInputValidationProcessor::class.java) - } -} diff --git a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseMessage.kt b/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseMessage.kt deleted file mode 100644 index 27ab517..0000000 --- a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseMessage.kt +++ /dev/null @@ -1,62 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse - -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.databind.ObjectMapper -import de.fraunhofer.iais.eis.Message -import org.slf4j.LoggerFactory -import java.nio.charset.Charset -import javax.xml.bind.DatatypeConverter - -class ClearingHouseMessage (var header: Message? = null, var payloadType: String? = null, var payload: String? = null){ - private var charset: String = Charset.defaultCharset().toString() - - fun toJson(): String { - val objectMapper = ObjectMapper() - objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) - return objectMapper.writeValueAsString(this) - } - - constructor(idsHeader: Message, contentTypeHeader: String?, payload: ByteArray) : this() { - this.header = idsHeader - parseContentType(contentTypeHeader) - when (this.payloadType){ - "text/plain", "application/json", "application/ld+json" -> { - this.payload = String(payload, Charset.forName(charset)) - } - else -> { - this.payloadType = "application/octet-stream" - this.payload = DatatypeConverter.printBase64Binary(payload) - } - } - } - - private fun parseContentType(contentTypeHeader: String?) { - // Parsing Content-Type and Charset - if (contentTypeHeader != null) { - val parts = contentTypeHeader.split(";") - when (parts.size){ - 1 -> { - this.payloadType = parts[0] - } - 2 -> { - this.payloadType = parts[0] - val charsetInput = parts[1].split("=") - if (charsetInput.size == 2){ - this.charset = charsetInput[1] - LOG.debug("Using Charset from Content-Type header: {}", charset) - } - } - else -> { - this.payloadType = "text/plain" - } - } - } - else{ - this.payloadType = "application/octet-stream" - } - } - - companion object { - private val LOG = LoggerFactory.getLogger(ClearingHouseMessage::class.java) - } -} diff --git a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseOutputProcessor.kt b/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseOutputProcessor.kt deleted file mode 100644 index 9375a35..0000000 --- a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/ClearingHouseOutputProcessor.kt +++ /dev/null @@ -1,108 +0,0 @@ -/*- - * ========================LICENSE_START================================= - * camel-multipart-processor - * %% - * Copyright (C) 2019 Fraunhofer AISEC - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * =========================LICENSE_END================================== - */ -package de.fhg.aisec.ids.clearinghouse - -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.CAMEL_HTTP_STATUS_CODE_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDS_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDS_PROTOCOL -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.PROTO_IDSCP2 -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.PROTO_MULTIPART -import de.fraunhofer.iais.eis.Message -import de.fraunhofer.iais.eis.MessageProcessedNotificationMessageBuilder -import de.fraunhofer.iais.eis.RejectionMessageBuilder -import de.fraunhofer.iais.eis.ResultMessageBuilder -import org.apache.camel.Exchange -import org.apache.camel.Processor -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component - -@Component("chOutputProcessor") -class ClearingHouseOutputProcessor : Processor { - - override fun process(exchange: Exchange) { - val egetIn = exchange.getIn() - val headers = egetIn.headers - if (LOG.isTraceEnabled) { - LOG.trace("[IN] ${ClearingHouseOutputProcessor::class.java.simpleName}") - for (header in headers.keys) { - LOG.trace("Found header '{}':'{}'", header, headers[header]) - } - } - - // If this property is null, the routes are not defined correctly! - val originalRequest = exchange.message.getHeader(IDS_HEADER) as Message - - val statusCode = (headers[CAMEL_HTTP_STATUS_CODE_HEADER] as Int?)!!.toInt() - // creating IDS header for the response - val responseMessage = when (statusCode) { - 200 -> ResultMessageBuilder() - ._correlationMessage_(originalRequest.id) - ._recipientAgent_(listOf(originalRequest.senderAgent)) - ._recipientConnector_(listOf(originalRequest.issuerConnector)) - 201 -> MessageProcessedNotificationMessageBuilder() - ._correlationMessage_(originalRequest.id) - ._recipientAgent_(listOf(originalRequest.senderAgent)) - ._recipientConnector_(listOf(originalRequest.issuerConnector)) - else -> RejectionMessageBuilder() - ._correlationMessage_(originalRequest.id) - ._recipientAgent_(listOf(originalRequest.senderAgent)) - ._recipientConnector_(listOf(originalRequest.issuerConnector)) - } - - // set the IDS header - egetIn.setHeader(IDS_HEADER, responseMessage) - - // idscp2 set status code - when (headers[IDS_PROTOCOL] as String){ - PROTO_IDSCP2 -> { - when(statusCode){ - 400 -> egetIn.body = "Bad Request" - 401 -> egetIn.body = "Unauthorized" - 403 -> egetIn.body = "Forbidden" - 404 -> egetIn.body = "Not Found" - 500 -> egetIn.body = "Internal Server Error" - } - } - PROTO_MULTIPART -> { - when(statusCode){ - 200, 201 -> - if (LOG.isTraceEnabled) { - LOG.trace("[OUT] ${ClearingHouseOutputProcessor::class.java.simpleName}") - LOG.trace("Message successfully processed.") - } - else -> { - egetIn.body = "" - } - } - } - } - - // Clean up the headers - egetIn.removeHeader(ClearingHouseConstants.AUTH_HEADER) - egetIn.removeHeader(ClearingHouseConstants.PID_HEADER) - egetIn.removeHeader(ClearingHouseConstants.SERVER) - egetIn.removeHeader(ClearingHouseConstants.TYPE_HEADER) - egetIn.removeHeader(IDS_PROTOCOL) - } - - companion object { - private val LOG = LoggerFactory.getLogger(ClearingHouseOutputProcessor::class.java) - } -} diff --git a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/Configuration.kt b/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/Configuration.kt deleted file mode 100644 index 54d3d05..0000000 --- a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/Configuration.kt +++ /dev/null @@ -1,28 +0,0 @@ -package de.fhg.aisec.ids.clearinghouse - -import org.slf4j.LoggerFactory - -internal object Configuration { - private val LOG = LoggerFactory.getLogger(Configuration::class.java) - private const val LOGGING_SERVICE_ID = "SERVICE_ID_LOG" - private const val TC_SERVICE_ID = "SERVICE_ID_TC" - private const val SERVICE_SHARED_SECRET = "SERVICE_SHARED_SECRET" - - val serviceIdTc: String - get() = getEnvVariable(TC_SERVICE_ID) - val serviceIdLog: String - get() = getEnvVariable(LOGGING_SERVICE_ID) - val serviceSecret: String - get() = getEnvVariable(SERVICE_SHARED_SECRET) - - - private fun getEnvVariable(envVariable: String): String { - val value = System.getenv(envVariable) - return if (value == null) { - LOG.error("Configuration invalid: Missing {}", envVariable) - "" - } else { - value - } - } -} diff --git a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/SharedSecretProcessor.kt b/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/SharedSecretProcessor.kt deleted file mode 100644 index b6d8c37..0000000 --- a/clearing-house-processors/src/main/java/de/fhg/aisec/ids/clearinghouse/SharedSecretProcessor.kt +++ /dev/null @@ -1,72 +0,0 @@ -/*- - * ========================LICENSE_START================================= - * camel-multipart-processor - * %% - * Copyright (C) 2019 Fraunhofer AISEC - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * =========================LICENSE_END================================== - */ -package de.fhg.aisec.ids.clearinghouse - -import com.auth0.jwt.JWT -import com.auth0.jwt.algorithms.Algorithm -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.IDS_HEADER -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.SERVICE_CLAIM -import de.fhg.aisec.ids.clearinghouse.ClearingHouseConstants.SERVICE_HEADER -import de.fraunhofer.iais.eis.Message -import org.apache.camel.Exchange -import org.apache.camel.Processor -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component -import java.util.Date - -/** - * This processor validates the JWT token in the IDS header - */ -@Component("chSharedSecretProcessor") -class SharedSecretProcessor : Processor { - override fun process(exchange: Exchange) { - val eIn = exchange.getIn() - val headers = eIn.headers - -// if (LOG.isDebugEnabled) { - LOG.debug("[IN] ${SharedSecretProcessor::class.java.simpleName}") - for (header in headers.keys) { - LOG.debug("Found header '{}':'{}'", header, headers[header]) - } -// } - - val idsHeader = exchange.message.getHeader(IDS_HEADER) as Message - //val idsHeader = exchange.getProperty(IDS_HEADER, Message::class.java) - // ?: throw RuntimeException("No IDS header provided!") - val dat = idsHeader.securityToken?.tokenValue ?: throw RuntimeException("No DAT provided!") - - val decodedDat = JWT.decode(dat) - val claimedClientId = decodedDat.subject - val now = System.currentTimeMillis() - val serviceToken = JWT.create() - .withAudience(Configuration.serviceIdLog) - .withIssuer(Configuration.serviceIdTc) - .withClaim(SERVICE_CLAIM, claimedClientId) - .withIssuedAt(Date(now)) - .withExpiresAt(Date(now + 60000)) - .sign(Algorithm.HMAC256(Configuration.serviceSecret)) - exchange.getIn().setHeader(SERVICE_HEADER, serviceToken) - } - - companion object { - val LOG: Logger = LoggerFactory.getLogger(SharedSecretProcessor::class.java) - } -} diff --git a/clearing-house-processors/src/routes/clearing-house-routes.xml b/clearing-house-processors/src/routes/clearing-house-routes.xml deleted file mode 100644 index a26975d..0000000 --- a/clearing-house-processors/src/routes/clearing-house-routes.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - java.io.IOException - java.lang.SecurityException - java.lang.IllegalArgumentException - - true - - ${exception.message} - - - - - - - - - ${exception.class} == 'java.lang.SecurityException' || ${exception.class} == 'java.lang.IllegalArgumentException' - 401 - - - ${exception.class} == 'java.io.IOException' || ${exception.class} == 'java.lang.RuntimeException' - 400 - - - 500 - Internal Server Error - - - - - idsMultipart - - - - - - - - - - java.io.IOException - java.lang.RuntimeException - java.lang.SecurityException - java.lang.IllegalArgumentException - - true - - - - - - - - - - idscp2 - - - ${exchangeProperty.ids-type} == 'RequestMessage' - - POST - /process/${headers.ch-ids-pid} - - - - - - - ${exchangeProperty.ids-type} == 'QueryMessage' - - POST - /messages/query/${headers.ch-ids-pid} - - - - - - - ${exchangeProperty.ids-type} == 'LogMessage' - - POST - /messages/log/${headers.ch-ids-pid} - - - - - - - - ${null} - - - - - - - - diff --git a/clearing-house-processors/version b/clearing-house-processors/version deleted file mode 100644 index e69de29..0000000 From d03cedeec58d7483bf299683d2a3bc965438298b Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Thu, 26 Oct 2023 15:29:41 -0300 Subject: [PATCH 132/183] feat (ch-edc): create process endpoint included --- .../edc/multipart/MultipartController.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index 0fdf8a9..a8fbb54 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -82,10 +82,23 @@ public MultipartController(@NotNull Monitor monitor, @POST @Path("messages/log/{pid}") - public Response request(@PathParam(PID) String pid, + public Response messageRequest(@PathParam(PID) String pid, @FormDataParam(HEADER) InputStream headerInputStream, @FormDataParam(PAYLOAD) String payload) { + return execute(pid, headerInputStream, payload); + } + + @POST + @Path("process/{pid}") + public Response processRequest(@PathParam(PID) String pid, + @FormDataParam(HEADER) InputStream headerInputStream, + @FormDataParam(PAYLOAD) String payload){ + return execute(pid, headerInputStream, payload); + + } + + private Response execute(String pid, InputStream headerInputStream, String payload){ // Check if pid is missing if (pid == null) { monitor.severe(LOG_ID + ": PID is missing"); @@ -208,7 +221,6 @@ public Response request(@PathParam(PID) String pid, .build(); } } - private boolean validateToken(Message header) { var dynamicAttributeToken = new DynamicAttributeTokenBuilder(). From 002845aa0729887853954118032084c6e5606354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Fri, 27 Oct 2023 14:19:25 +0200 Subject: [PATCH 133/183] feat(ch-app): Add CreateProcessResponse as JSON --- clearing-house-app/src/ports/logging_api.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/clearing-house-app/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs index 4060d31..94237f2 100644 --- a/clearing-house-app/src/ports/logging_api.rs +++ b/clearing-house-app/src/ports/logging_api.rs @@ -31,18 +31,23 @@ async fn log( } } +#[derive(serde::Serialize)] +struct CreateProcessResponse { + pub pid: String, +} + async fn create_process( ExtractChClaims(ch_claims): ExtractChClaims, axum::extract::State(state): axum::extract::State, axum::extract::Path(pid): axum::extract::Path, axum::extract::Json(message): axum::extract::Json, -) -> LoggingApiResult { +) -> LoggingApiResult { match state .logging_service .create_process(ch_claims, message, pid) .await { - Ok(id) => Ok((StatusCode::CREATED, Json(id))), + Ok(id) => Ok((StatusCode::CREATED, Json(CreateProcessResponse { pid: id }))), Err(e) => { error!("Error while creating process: {:?}", e); Err(e) From c6662bb21a6e6891f005bac52d2e6ee348af4935 Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Fri, 27 Oct 2023 18:27:11 -0300 Subject: [PATCH 134/183] chore (ch-edc): create process multipart endpoint --- .../app/delegate/CreateProcessDelegate.java | 69 +++++++++++++++++ .../edc/dto/CreateProcessRequest.java | 35 +++++++++ .../edc/dto/CreateProcessResponse.java | 29 +++++++ .../edc/handler/LogMessageHandler.java | 8 +- .../edc/handler/RequestMessageHandler.java | 69 +++++++++++++++++ .../edc/handler/LogMessageHandlerTest.java | 6 +- .../edc/multipart/MultipartController.java | 77 +++++++++++-------- .../edc/multipart/MultipartExtension.java | 4 +- .../dto/RequestValidationResponse.java | 44 +++++++++++ .../multipart/MultipartControllerTest.java | 18 ++--- 10 files changed, 305 insertions(+), 54 deletions(-) create mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/CreateProcessDelegate.java create mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/CreateProcessRequest.java create mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/CreateProcessResponse.java create mode 100644 clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandler.java create mode 100644 clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/dto/RequestValidationResponse.java diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/CreateProcessDelegate.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/CreateProcessDelegate.java new file mode 100644 index 0000000..2f51815 --- /dev/null +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/app/delegate/CreateProcessDelegate.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 truzzt GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - Initial implementation + * + */ +package de.truzzt.clearinghouse.edc.app.delegate; + +import de.truzzt.clearinghouse.edc.dto.*; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.types.clearinghouse.Context; +import de.truzzt.clearinghouse.edc.types.clearinghouse.Header; +import de.truzzt.clearinghouse.edc.types.clearinghouse.SecurityToken; +import de.truzzt.clearinghouse.edc.types.clearinghouse.TokenFormat; +import okhttp3.ResponseBody; + +public class CreateProcessDelegate implements AppSenderDelegate { + + private final TypeManagerUtil typeManagerUtil; + + public CreateProcessDelegate(TypeManagerUtil typeManagerUtil) { + this.typeManagerUtil = typeManagerUtil; + } + + public String buildRequestUrl(String baseUrl, HandlerRequest handlerRequest) { + return baseUrl + "/process/" + handlerRequest.getPid(); + } + + public CreateProcessRequest buildRequestBody(HandlerRequest handlerRequest) { + var header = handlerRequest.getHeader(); + + var multipartContext = header.getContext(); + var context = new Context(multipartContext.getIds(), multipartContext.getIdsc()); + + var multipartSecurityToken = header.getSecurityToken(); + var multipartTokenFormat = multipartSecurityToken.getTokenFormat(); + var securityToken = SecurityToken.Builder.newInstance(). + type(multipartSecurityToken.getType()). + id(multipartSecurityToken.getId()). + tokenFormat(new TokenFormat(multipartTokenFormat.getId())). + tokenValue(multipartSecurityToken.getTokenValue()). + build(); + + var requestHeader = Header.Builder.newInstance() + .context(context) + .id(header.getId()) + .type(header.getType()) + .securityToken(securityToken) + .issuerConnector(header.getIssuerConnector()) + .modelVersion(header.getModelVersion()) + .issued(header.getIssued()) + .senderAgent(header.getSenderAgent()) + .build(); + + return new CreateProcessRequest(requestHeader, handlerRequest.getPayload()); + } + + @Override + public CreateProcessResponse parseResponseBody(ResponseBody responseBody) { + return typeManagerUtil.parse(responseBody.byteStream(), CreateProcessResponse.class); + } +} diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/CreateProcessRequest.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/CreateProcessRequest.java new file mode 100644 index 0000000..ad3078a --- /dev/null +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/CreateProcessRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 truzzt GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - Initial implementation + * + */ +package de.truzzt.clearinghouse.edc.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.truzzt.clearinghouse.edc.types.clearinghouse.Header; +import org.jetbrains.annotations.NotNull; + +public class CreateProcessRequest { + + @JsonProperty("header") + @NotNull + private final Header header; + + @JsonProperty("payload") + @NotNull + private final String payload; + + public CreateProcessRequest(@NotNull Header header, @NotNull String payload) { + this.header = header; + this.payload = payload; + } +} + diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/CreateProcessResponse.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/CreateProcessResponse.java new file mode 100644 index 0000000..fb591d4 --- /dev/null +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/dto/CreateProcessResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023 truzzt GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - Initial implementation + * + */ +package de.truzzt.clearinghouse.edc.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.NotNull; + +public class CreateProcessResponse { + + @JsonProperty("pid") + @NotNull + private String pid; + + public String getPid() { + return pid; + } + +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java index 2d48528..26b4104 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandler.java @@ -20,7 +20,6 @@ import de.truzzt.clearinghouse.edc.dto.HandlerResponse; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import org.eclipse.edc.protocol.ids.spi.types.IdsId; -import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.jetbrains.annotations.NotNull; @@ -32,22 +31,17 @@ public class LogMessageHandler implements Handler { - private final Monitor monitor; private final IdsId connectorId; - private final TypeManagerUtil typeManagerUtil; private final AppSender appSender; private final LoggingMessageDelegate senderDelegate; private final ServiceExtensionContext context; - public LogMessageHandler(Monitor monitor, - IdsId connectorId, + public LogMessageHandler(IdsId connectorId, TypeManagerUtil typeManagerUtil, AppSender appSender, ServiceExtensionContext context) { - this.monitor = monitor; this.connectorId = connectorId; - this.typeManagerUtil = typeManagerUtil; this.appSender = appSender; this.context = context; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandler.java new file mode 100644 index 0000000..6a0cd0e --- /dev/null +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 truzzt GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - Initial implementation + * + */ +package de.truzzt.clearinghouse.edc.handler; + +import de.truzzt.clearinghouse.edc.app.AppSender; +import de.truzzt.clearinghouse.edc.app.delegate.CreateProcessDelegate; +import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.jetbrains.annotations.NotNull; + +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.createMultipartResponse; +import static de.truzzt.clearinghouse.edc.util.ResponseUtil.messageProcessedNotification; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.APP_BASE_URL_DEFAULT_VALUE; +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.APP_BASE_URL_SETTING; + +public class RequestMessageHandler implements Handler { + + private final IdsId connectorId; + private final AppSender appSender; + private final CreateProcessDelegate senderDelegate; + + private final ServiceExtensionContext context; + + public RequestMessageHandler(IdsId connectorId, + TypeManagerUtil typeManagerUtil, + AppSender appSender, + ServiceExtensionContext context) { + this.connectorId = connectorId; + this.appSender = appSender; + this.context = context; + + this.senderDelegate = new CreateProcessDelegate(typeManagerUtil); + } + + @Override + public boolean canHandle(@NotNull HandlerRequest handlerRequest) { + return handlerRequest.getHeader().getType().equals("ids:RequestMessage"); + } + + @Override + public @NotNull HandlerResponse handleRequest(@NotNull HandlerRequest handlerRequest) { + var baseUrl = context.getSetting(APP_BASE_URL_SETTING, APP_BASE_URL_DEFAULT_VALUE); + var header = handlerRequest.getHeader(); + + var url = senderDelegate.buildRequestUrl(baseUrl, handlerRequest); + var token = buildJWTToken(handlerRequest.getHeader().getSecurityToken(), context); + var body = senderDelegate.buildRequestBody(handlerRequest); + + var request = AppSenderRequest.Builder.newInstance().url(url).token(token).body(body).build(); + + var response = appSender.send(request, senderDelegate); + return createMultipartResponse(messageProcessedNotification(header, connectorId), response); + } +} diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index f72e158..5d9e2b9 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -12,8 +12,6 @@ import okhttp3.ResponseBody; import org.eclipse.edc.protocol.ids.spi.types.IdsId; import org.eclipse.edc.spi.EdcException; -import org.eclipse.edc.spi.http.EdcHttpClient; -import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.ServiceExtensionContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,8 +26,6 @@ import static org.mockito.Mockito.spy; class LogMessageHandlerTest { - @Mock - private Monitor monitor; @Mock private IdsId connectorId; @Mock @@ -49,7 +45,7 @@ class LogMessageHandlerTest { public void setUp() { MockitoAnnotations.openMocks(this); senderDelegate = spy(new LoggingMessageDelegate(typeManagerUtil)); - logMessageHandler = spy(new LogMessageHandler(monitor, connectorId, typeManagerUtil, appSender, context)); + logMessageHandler = spy(new LogMessageHandler(connectorId, typeManagerUtil, appSender, context)); } @Test diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index a8fbb54..4adc31d 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -18,6 +18,7 @@ import de.truzzt.clearinghouse.edc.handler.Handler; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.multipart.dto.RequestValidationResponse; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.Message; @@ -82,37 +83,51 @@ public MultipartController(@NotNull Monitor monitor, @POST @Path("messages/log/{pid}") - public Response messageRequest(@PathParam(PID) String pid, - @FormDataParam(HEADER) InputStream headerInputStream, - @FormDataParam(PAYLOAD) String payload) { - return execute(pid, headerInputStream, payload); + public Response logMessage(@PathParam(PID) String pid, + @FormDataParam(HEADER) InputStream headerInputStream, + @FormDataParam(PAYLOAD) String payload) { + var response = validaRequest(pid, headerInputStream); + if (response.fail()) + return response.getError(); + // Check if payload is missing + if (payload == null) { + monitor.severe(LOG_ID + ": Payload is missing"); + return Response.status(Response.Status.BAD_REQUEST) + .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) + .build(); + } + + return processRequest(pid, response.getHeader(), payload); } @POST @Path("process/{pid}") - public Response processRequest(@PathParam(PID) String pid, - @FormDataParam(HEADER) InputStream headerInputStream, - @FormDataParam(PAYLOAD) String payload){ - return execute(pid, headerInputStream, payload); - + public Response createProcess(@PathParam(PID) String pid, + @FormDataParam(HEADER) InputStream headerInputStream, + @FormDataParam(PAYLOAD) String payload){ + var response = validaRequest(pid, headerInputStream); + if (response.fail()) + return response.getError(); + + return processRequest(pid, response.getHeader(), payload); } - private Response execute(String pid, InputStream headerInputStream, String payload){ + private RequestValidationResponse validaRequest(String pid, InputStream headerInputStream){ // Check if pid is missing if (pid == null) { monitor.severe(LOG_ID + ": PID is missing"); - return Response.status(Response.Status.BAD_REQUEST) + return new RequestValidationResponse(Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) - .build(); + .build()); } // Check if header is missing if (headerInputStream == null) { monitor.severe(LOG_ID + ": Header is missing"); - return Response.status(Response.Status.BAD_REQUEST) + return new RequestValidationResponse(Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) - .build(); + .build()); } // Convert header to message @@ -121,9 +136,9 @@ private Response execute(String pid, InputStream headerInputStream, String paylo header = typeManagerUtil.parse(headerInputStream, Message.class); } catch (Exception e) { monitor.severe(format(LOG_ID + ": Header parsing failed: %s", e.getMessage())); - return Response.status(Response.Status.BAD_REQUEST) + return new RequestValidationResponse(Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) - .build(); + .build()); } // Check if any required header field missing @@ -137,44 +152,41 @@ private Response execute(String pid, InputStream headerInputStream, String paylo || header.getSenderAgent() == null || (header.getSenderAgent() != null && StringUtils.isNullOrBlank(header.getSenderAgent().toString())) ) { - return Response.status(Response.Status.BAD_REQUEST) + return new RequestValidationResponse(Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(header, connectorId))) - .build(); + .build()); } // Check if security token is present var securityToken = header.getSecurityToken(); if (securityToken == null || securityToken.getTokenValue() == null) { monitor.severe(LOG_ID + ": Token is missing in header"); - return Response.status(Response.Status.BAD_REQUEST) + return new RequestValidationResponse(Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, notAuthenticated(header, connectorId))) - .build(); + .build()); } // Check the security token type var tokenFormat = securityToken.getTokenFormat().getId().toString(); if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { monitor.severe(LOG_ID + ": Invalid security token type: " + tokenFormat); - return Response.status(Response.Status.BAD_REQUEST) + return new RequestValidationResponse(Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) - .build(); - } - - // Check if payload is missing - if (payload == null) { - monitor.severe(LOG_ID + ": Payload is missing"); - return Response.status(Response.Status.BAD_REQUEST) - .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) - .build(); + .build()); } // Validate DAT if (!validateToken(header)) { - return Response.status(Response.Status.FORBIDDEN) + return new RequestValidationResponse(Response.status(Response.Status.FORBIDDEN) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, notAuthenticated(header, connectorId))) - .build(); + .build()); } + return new RequestValidationResponse(header); + } + + private Response processRequest(String pid, Message header, String payload){ + // Build the multipart request var multipartRequest = HandlerRequest.Builder.newInstance() .pid(pid) @@ -221,6 +233,7 @@ private Response execute(String pid, InputStream headerInputStream, String paylo .build(); } } + private boolean validateToken(Message header) { var dynamicAttributeToken = new DynamicAttributeTokenBuilder(). diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java index 59d3cc6..a36a733 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartExtension.java @@ -17,6 +17,7 @@ import de.truzzt.clearinghouse.edc.handler.Handler; import de.truzzt.clearinghouse.edc.handler.LogMessageHandler; import de.truzzt.clearinghouse.edc.app.AppSender; +import de.truzzt.clearinghouse.edc.handler.RequestMessageHandler; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import org.eclipse.edc.connector.api.management.configuration.ManagementApiConfiguration; import org.eclipse.edc.protocol.ids.api.configuration.IdsApiConfiguration; @@ -73,7 +74,8 @@ public void initialize(ServiceExtensionContext context) { var clearingHouseAppSender = new AppSender(monitor, httpClient, typeManagerUtil); var handlers = new LinkedList(); - handlers.add(new LogMessageHandler(monitor, connectorId, typeManagerUtil, clearingHouseAppSender, context)); + handlers.add(new RequestMessageHandler(connectorId, typeManagerUtil, clearingHouseAppSender, context)); + handlers.add(new LogMessageHandler(connectorId, typeManagerUtil, clearingHouseAppSender, context)); var multipartController = new MultipartController(monitor, connectorId, diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/dto/RequestValidationResponse.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/dto/RequestValidationResponse.java new file mode 100644 index 0000000..ba89ccb --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/dto/RequestValidationResponse.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 truzzt GmbH + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * truzzt GmbH - Initial implementation + * + */ +package de.truzzt.clearinghouse.edc.multipart.dto; + +import de.truzzt.clearinghouse.edc.types.ids.Message; +import jakarta.ws.rs.core.Response; +import org.jetbrains.annotations.NotNull; + +public class RequestValidationResponse { + + private Response error; + private Message header; + + public RequestValidationResponse(@NotNull Response error) { + this.error = error; + } + public RequestValidationResponse(@NotNull Message header) { + this.header = header; + } + + public Response getError() { + return error; + } + + public Message getHeader() { + return header; + } + + public Boolean fail() { + return (error != null); + } + +} diff --git a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java index 3b7861e..7f9d106 100644 --- a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java +++ b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java @@ -106,7 +106,7 @@ public void success() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.VALID_HEADER_JSON); - var response = controller.request(pid, header, PAYLOAD); + var response = controller.logMessage(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); @@ -122,7 +122,7 @@ public void success() { public void missingPIDError() { var header = TestUtils.getHeaderInputStream(TestUtils.VALID_HEADER_JSON); - var response = controller.request(null, header, PAYLOAD); + var response = controller.logMessage(null, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -137,7 +137,7 @@ public void missingPIDError() { public void missingHeaderError() { var pid = UUID.randomUUID().toString(); - var response = controller.request(pid, null, PAYLOAD); + var response = controller.logMessage(pid, null, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -153,7 +153,7 @@ public void invalidHeaderError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_HEADER_JSON); - var response = controller.request(pid, header, PAYLOAD); + var response = controller.logMessage(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -169,7 +169,7 @@ public void missingHeaderFieldsError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.MISSING_FIELDS_HEADER_JSON); - var response = controller.request(pid, header, PAYLOAD); + var response = controller.logMessage(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -188,7 +188,7 @@ public void invalidSecurityTokenError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_TOKEN_HEADER_JSON); - var response = controller.request(pid, header, PAYLOAD); + var response = controller.logMessage(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); @@ -204,7 +204,7 @@ public void missingSecurityTokenError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.MISSING_TOKEN_HEADER_JSON); - var response = controller.request(pid, header, PAYLOAD); + var response = controller.logMessage(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -220,7 +220,7 @@ public void missingPayloadError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.VALID_HEADER_JSON); - var response = controller.request(pid, header, null); + var response = controller.logMessage(pid, header, null); assertNotNull(response); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); @@ -241,7 +241,7 @@ public void invalidMessageTypeError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_TYPE_HEADER_JSON); - var response = controller.request(pid, header, PAYLOAD); + var response = controller.logMessage(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); From 75e89cf0a13ebc432dfc59badb706ad49a118f0c Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Mon, 30 Oct 2023 19:17:03 -0300 Subject: [PATCH 135/183] feat (ch-edc): create process test included --- .../edc/multipart/MultipartController.java | 4 +- .../multipart/MultipartControllerTest.java | 85 ++++++++++++++----- .../edc/multipart/tests/TestUtils.java | 5 ++ .../headers/valid-create-process-header.json | 20 +++++ 4 files changed, 90 insertions(+), 24 deletions(-) create mode 100644 clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-create-process-header.json diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index 4adc31d..a566cd7 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -113,7 +113,7 @@ public Response createProcess(@PathParam(PID) String pid, return processRequest(pid, response.getHeader(), payload); } - private RequestValidationResponse validaRequest(String pid, InputStream headerInputStream){ + RequestValidationResponse validaRequest(String pid, InputStream headerInputStream){ // Check if pid is missing if (pid == null) { monitor.severe(LOG_ID + ": PID is missing"); @@ -185,7 +185,7 @@ private RequestValidationResponse validaRequest(String pid, InputStream headerIn return new RequestValidationResponse(header); } - private Response processRequest(String pid, Message header, String payload){ + Response processRequest(String pid, Message header, String payload){ // Build the multipart request var multipartRequest = HandlerRequest.Builder.newInstance() diff --git a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java index 7f9d106..7143fd1 100644 --- a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java +++ b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java @@ -7,6 +7,8 @@ import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; import de.truzzt.clearinghouse.edc.handler.Handler; import de.truzzt.clearinghouse.edc.handler.LogMessageHandler; +import de.truzzt.clearinghouse.edc.handler.RequestMessageHandler; +import de.truzzt.clearinghouse.edc.multipart.dto.RequestValidationResponse; import de.truzzt.clearinghouse.edc.multipart.tests.TestUtils; import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; import de.truzzt.clearinghouse.edc.types.ids.Message; @@ -23,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import java.io.*; import java.net.URI; @@ -32,13 +35,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyByte; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; public class MultipartControllerTest { private static final String IDS_WEBHOOK_ADDRESS = "http://localhost/callback"; private static final String PAYLOAD = "Hello World"; + private static final String CREATE_PROCESS_PAYLOAD = "{ \"owners\": [\"1\", \"2\"]}"; private MultipartController controller; @@ -52,6 +60,9 @@ public class MultipartControllerTest { @Mock private LogMessageHandler logMessageHandler; + @Mock + private RequestMessageHandler requestMessageHandler; + private final ObjectMapper mapper = new ObjectMapper(); @BeforeEach @@ -92,7 +103,7 @@ private T extractPayload(Response response, Class type) { } @Test - public void success() { + public void logMessageSuccess() { var responseHeader = TestUtils.getValidResponseHeader(mapper); var responsePayload = TestUtils.getValidResponsePayload(mapper); @@ -110,7 +121,7 @@ public void success() { assertNotNull(response); assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); - + var message = extractHeader(response, Message.class); assertEquals("ids:LogMessage", message.getType()); @@ -118,16 +129,43 @@ public void success() { assertNotNull(payload.getData()); } + @Test + public void createProcessSuccess() { + var responseHeader = TestUtils.getValidResponseHeader(mapper); + var responsePayload = TestUtils.getValidResponsePayload(mapper); + + doReturn(Result.success()) + .when(tokenService).verifyDynamicAttributeToken(any(DynamicAttributeToken.class), any(URI.class), any(String.class)); + doReturn(true) + .when(requestMessageHandler).canHandle(any(HandlerRequest.class)); + doReturn(HandlerResponse.Builder.newInstance().header(responseHeader).payload(responsePayload).build()) + .when(requestMessageHandler).handleRequest(any(HandlerRequest.class)); + + var pid = UUID.randomUUID().toString(); + var header = TestUtils.getHeaderInputStream(TestUtils.VALID_CREATE_PROCESS_HEADER_JSON); + + var response = controller.createProcess(pid, header, CREATE_PROCESS_PAYLOAD); + + assertNotNull(response); + assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus()); + + var message = extractHeader(response, Message.class); + assertEquals("ids:RequestMessage", message.getType()); + + var payload = extractPayload(response, LoggingMessageResponse.class); + assertNotNull(payload.getData()); + } + @Test public void missingPIDError() { var header = TestUtils.getHeaderInputStream(TestUtils.VALID_HEADER_JSON); - var response = controller.logMessage(null, header, PAYLOAD); + var response = controller.validaRequest(null, header); assertNotNull(response); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertTrue(response.fail()); - var message = extractHeader(response, RejectionMessage.class); + var message = extractHeader(response.getError(), RejectionMessage.class); assertNotNull(message.getRejectionReason()); assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); @@ -137,12 +175,12 @@ public void missingPIDError() { public void missingHeaderError() { var pid = UUID.randomUUID().toString(); - var response = controller.logMessage(pid, null, PAYLOAD); + var response = controller.validaRequest(pid, null); assertNotNull(response); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertTrue(response.fail()); - var message = extractHeader(response, RejectionMessage.class); + var message = extractHeader(response.getError(), RejectionMessage.class); assertNotNull(message.getRejectionReason()); assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); @@ -153,12 +191,12 @@ public void invalidHeaderError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_HEADER_JSON); - var response = controller.logMessage(pid, header, PAYLOAD); + var response = controller.validaRequest(pid, header); assertNotNull(response); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertTrue(response.fail()); - var message = extractHeader(response, RejectionMessage.class); + var message = extractHeader(response.getError(), RejectionMessage.class); assertNotNull(message.getRejectionReason()); assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); @@ -169,12 +207,12 @@ public void missingHeaderFieldsError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.MISSING_FIELDS_HEADER_JSON); - var response = controller.logMessage(pid, header, PAYLOAD); + var response = controller.validaRequest(pid, header); assertNotNull(response); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertTrue(response.fail()); - var message = extractHeader(response, RejectionMessage.class); + var message = extractHeader(response.getError(), RejectionMessage.class); assertNotNull(message.getRejectionReason()); assertEquals(RejectionReason.MALFORMED_MESSAGE.getId(), message.getRejectionReason().getId()); @@ -188,12 +226,12 @@ public void invalidSecurityTokenError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_TOKEN_HEADER_JSON); - var response = controller.logMessage(pid, header, PAYLOAD); + var response = controller.validaRequest(pid, header); assertNotNull(response); - assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); + assertTrue(response.fail()); - var message = extractHeader(response, RejectionMessage.class); + var message = extractHeader(response.getError(), RejectionMessage.class); assertNotNull(message.getRejectionReason()); assertEquals(RejectionReason.NOT_AUTHENTICATED.getId(), message.getRejectionReason().getId()); @@ -204,12 +242,12 @@ public void missingSecurityTokenError() { var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.MISSING_TOKEN_HEADER_JSON); - var response = controller.logMessage(pid, header, PAYLOAD); + var response = controller.validaRequest(pid, header); assertNotNull(response); - assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + assertTrue(response.fail()); - var message = extractHeader(response, RejectionMessage.class); + var message = extractHeader(response.getError(), RejectionMessage.class); assertNotNull(message.getRejectionReason()); assertEquals(RejectionReason.NOT_AUTHENTICATED.getId(), message.getRejectionReason().getId()); @@ -217,6 +255,9 @@ public void missingSecurityTokenError() { @Test public void missingPayloadError() { + doReturn(Result.success()) + .when(tokenService).verifyDynamicAttributeToken(any(DynamicAttributeToken.class), any(URI.class), any(String.class)); + var pid = UUID.randomUUID().toString(); var header = TestUtils.getHeaderInputStream(TestUtils.VALID_HEADER_JSON); @@ -239,9 +280,9 @@ public void invalidMessageTypeError() { .when(logMessageHandler).canHandle(any(HandlerRequest.class)); var pid = UUID.randomUUID().toString(); - var header = TestUtils.getHeaderInputStream(TestUtils.INVALID_TYPE_HEADER_JSON); + var header = TestUtils.getResponseHeader(new ObjectMapper(), TestUtils.INVALID_TYPE_HEADER_JSON); - var response = controller.logMessage(pid, header, PAYLOAD); + var response = controller.processRequest(pid, header, PAYLOAD); assertNotNull(response); assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); diff --git a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/tests/TestUtils.java b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/tests/TestUtils.java index 4cda33e..00e6d81 100644 --- a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/tests/TestUtils.java +++ b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/tests/TestUtils.java @@ -11,6 +11,7 @@ public class TestUtils extends BaseTestUtils { public static final String VALID_HEADER_JSON = "headers/valid-header.json"; + public static final String VALID_CREATE_PROCESS_HEADER_JSON = "headers/valid-create-process-header.json"; public static final String INVALID_HEADER_JSON = "headers/invalid-header.json"; public static final String INVALID_TYPE_HEADER_JSON = "headers/invalid-type.json"; public static final String INVALID_TOKEN_HEADER_JSON = "headers/invalid-token.json"; @@ -25,6 +26,10 @@ public static InputStream getHeaderInputStream(String path) { return new ByteArrayInputStream(json.getBytes()); } + public static Message getResponseHeader(ObjectMapper mapper, String path) { + return parseFile(mapper, Message.class, path); + } + public static Message getValidResponseHeader(ObjectMapper mapper) { return parseFile(mapper, Message.class, VALID_RESPONSE_HEADER_JSON); } diff --git a/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-create-process-header.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-create-process-header.json new file mode 100644 index 0000000..bb2eb20 --- /dev/null +++ b/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-create-process-header.json @@ -0,0 +1,20 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:RequestMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} \ No newline at end of file From 610fc8910af7edf3690989f7f24f83b6f81ce934 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Tue, 31 Oct 2023 09:40:47 -0300 Subject: [PATCH 136/183] feat (ch-edc): create process test working --- .../clearinghouse/edc/multipart/MultipartControllerTest.java | 4 ++-- .../test/resources/headers/valid-create-process-header.json | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java index 7143fd1..ef2d40b 100644 --- a/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java +++ b/clearing-house-edc/extensions/multipart/src/test/java/de/truzzt/clearinghouse/edc/multipart/MultipartControllerTest.java @@ -72,7 +72,7 @@ public void setUp() { connectorId = IdsId.Builder.newInstance().type(IdsType.CONNECTOR).value("http://test.connector").build(); typeManagerUtil = new TypeManagerUtil(new ObjectMapper()); - List multipartHandlers = List.of(logMessageHandler); + List multipartHandlers = List.of(logMessageHandler, requestMessageHandler); controller = new MultipartController(monitor, connectorId, typeManagerUtil, tokenService, IDS_WEBHOOK_ADDRESS, multipartHandlers); } @@ -131,7 +131,7 @@ public void logMessageSuccess() { @Test public void createProcessSuccess() { - var responseHeader = TestUtils.getValidResponseHeader(mapper); + var responseHeader = TestUtils.getResponseHeader(mapper, TestUtils.VALID_CREATE_PROCESS_HEADER_JSON); var responsePayload = TestUtils.getValidResponsePayload(mapper); doReturn(Result.success()) diff --git a/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-create-process-header.json b/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-create-process-header.json index bb2eb20..a1cd612 100644 --- a/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-create-process-header.json +++ b/clearing-house-edc/extensions/multipart/src/test/resources/headers/valid-create-process-header.json @@ -17,4 +17,5 @@ "ids:modelVersion":"4.1.0", "ids:issued" : "2021-06-23T17:27:23.566+02:00", "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" -} \ No newline at end of file +} + From a7103cedced9c00fed5879d137e11d45b5ae0142 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Tue, 31 Oct 2023 14:58:44 -0300 Subject: [PATCH 137/183] feat (ch-edc): RequestMessageHandler tests included --- .../handler/RequestMessageHandlerTest.java | 114 ++++++++++++++++++ .../clearinghouse/edc/tests/TestUtils.java | 54 +++++++++ .../headers/valid-create-process-header.json | 21 ++++ 3 files changed, 189 insertions(+) create mode 100644 clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandlerTest.java create mode 100644 clearing-house-edc/core/src/test/resources/headers/valid-create-process-header.json diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandlerTest.java new file mode 100644 index 0000000..5c9032b --- /dev/null +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandlerTest.java @@ -0,0 +1,114 @@ +package de.truzzt.clearinghouse.edc.handler; + +import com.auth0.jwt.JWT; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.truzzt.clearinghouse.edc.app.AppSender; +import de.truzzt.clearinghouse.edc.app.delegate.CreateProcessDelegate; +import de.truzzt.clearinghouse.edc.app.delegate.LoggingMessageDelegate; +import de.truzzt.clearinghouse.edc.dto.HandlerRequest; +import de.truzzt.clearinghouse.edc.dto.HandlerResponse; +import de.truzzt.clearinghouse.edc.tests.TestUtils; +import de.truzzt.clearinghouse.edc.types.TypeManagerUtil; +import de.truzzt.clearinghouse.edc.types.ids.SecurityToken; +import okhttp3.ResponseBody; +import org.eclipse.edc.protocol.ids.spi.types.IdsId; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static de.truzzt.clearinghouse.edc.util.SettingsConstants.APP_BASE_URL_DEFAULT_VALUE; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +class RequestMessageHandlerTest { + + @Mock + private IdsId connectorId; + @Mock + private TypeManagerUtil typeManagerUtil; + @Mock + private AppSender appSender; + @Mock + private ServiceExtensionContext context; + + @Mock + private CreateProcessDelegate createProcessDelegate; + @Mock + private RequestMessageHandler requestMessageHandler; + + private final ObjectMapper mapper = new ObjectMapper(); + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + requestMessageHandler = spy(new RequestMessageHandler(connectorId, typeManagerUtil, appSender, context)); + } + @Test + void successfulCanHandle() { + HandlerRequest request = TestUtils.getValidHandlerCreateProcessRequest(mapper); + + Boolean response = requestMessageHandler.canHandle(request); + + assertNotNull(response); + assertEquals(true, response); + } + + @Test + public void invalidMessageTypeCanHandle(){ + + HandlerRequest request = TestUtils.getInvalidHandlerRequest(mapper); + + Boolean response = requestMessageHandler.canHandle(request); + + assertNotNull(response); + assertEquals(response, false); + } + + @Test + public void successfulHandleRequest(){ + HandlerRequest request = TestUtils.getValidHandlerRequest(mapper); + doReturn(JWT.create().toString()) + .when(requestMessageHandler).buildJWTToken(any(SecurityToken.class), any(ServiceExtensionContext.class)); + + doReturn(TestUtils.getValidCreateProcessResponse(TestUtils.getValidAppSenderRequest(mapper).getUrl(), mapper)) + .when(createProcessDelegate).parseResponseBody(any(ResponseBody.class)); + + doReturn(APP_BASE_URL_DEFAULT_VALUE+ "/process/" + request.getPid()) + .when(createProcessDelegate) + .buildRequestUrl(any(String.class), any(HandlerRequest.class)); + + doReturn(TestUtils.getValidCreateProcessRequest(request)) + .when(createProcessDelegate).buildRequestBody(any(HandlerRequest.class)); + + HandlerResponse response = requestMessageHandler.handleRequest(request); + + assertNotNull(response); + assertEquals(response.getHeader().getType(), "ids:MessageProcessedNotificationMessage"); + } + + @Test + public void missingSubjectBuildJwtToken() { + EdcException exception = assertThrows(EdcException.class, () -> requestMessageHandler.buildJWTToken( + TestUtils.getInvalidTokenHandlerRequest(mapper) + .getHeader() + .getSecurityToken(), context)); + + assertEquals("JWT Token subject is missing",exception.getMessage()); + } + @Test + public void successfulBuildJwtToken() { + doReturn("1").when(context).getSetting(anyString(), anyString()); + var response = requestMessageHandler.buildJWTToken( + TestUtils.getValidHandlerRequest(mapper) + .getHeader() + .getSecurityToken(), context); + + assertNotNull(response); + } +} \ No newline at end of file diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java index f1a4458..44e236d 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/tests/TestUtils.java @@ -3,6 +3,8 @@ import com.auth0.jwt.JWT; import com.fasterxml.jackson.databind.ObjectMapper; import de.truzzt.clearinghouse.edc.dto.AppSenderRequest; +import de.truzzt.clearinghouse.edc.dto.CreateProcessRequest; +import de.truzzt.clearinghouse.edc.dto.CreateProcessResponse; import de.truzzt.clearinghouse.edc.dto.HandlerRequest; import de.truzzt.clearinghouse.edc.dto.LoggingMessageRequest; import de.truzzt.clearinghouse.edc.dto.LoggingMessageResponse; @@ -23,7 +25,9 @@ public class TestUtils extends BaseTestUtils { public static final String TEST_BASE_URL = "http://localhost:8000"; private static final String TEST_PAYLOAD = "Hello World"; + private static final String TEST_CREATE_PROCCESS_PAYLOAD = "{ \"owners\": [\"1\", \"2\"]}";; private static final String VALID_HEADER_JSON = "headers/valid-header.json"; + private static final String VALID_CREATE_PROCESS_HEADER_JSON = "headers/valid-create-process-header.json"; private static final String INVALID_HEADER_JSON = "headers/invalid-header.json"; private static final String INVALID_TYPE_HEADER_JSON = "headers/invalid-type.json"; private static final String INVALID_TOKEN_HEADER_JSON = "headers/invalid-token.json"; @@ -32,6 +36,10 @@ public static Message getValidHeader(ObjectMapper mapper) { return parseFile(mapper, Message.class, VALID_HEADER_JSON); } + public static Message getValidCreateProcessHeader(ObjectMapper mapper) { + return parseFile(mapper, Message.class, VALID_CREATE_PROCESS_HEADER_JSON); + } + public static Message getInvalidTokenHeader(ObjectMapper mapper) { return parseFile(mapper, Message.class, INVALID_TOKEN_HEADER_JSON); } @@ -90,6 +98,15 @@ public static LoggingMessageResponse getValidLoggingMessageResponse(String url, } } + public static CreateProcessResponse getValidCreateProcessResponse(String url, ObjectMapper mapper) { + try { + return mapper.readValue(getValidResponse(url).body().byteStream(), CreateProcessResponse.class); + + } catch (IOException e) { + throw new EdcException("Error parsing response", e); + } + } + public static LoggingMessageRequest getValidLoggingMessageRequest(HandlerRequest handlerRequest) { var header = handlerRequest.getHeader(); @@ -120,6 +137,36 @@ public static LoggingMessageRequest getValidLoggingMessageRequest(HandlerRequest return new LoggingMessageRequest(requestHeader, handlerRequest.getPayload()); } + public static CreateProcessRequest getValidCreateProcessRequest(HandlerRequest handlerRequest) { + + var header = handlerRequest.getHeader(); + + var multipartContext = header.getContext(); + var context = new Context(multipartContext.getIds(), multipartContext.getIdsc()); + + var multipartSecurityToken = header.getSecurityToken(); + var multipartTokenFormat = multipartSecurityToken.getTokenFormat(); + var securityToken = SecurityToken.Builder.newInstance(). + type(multipartSecurityToken.getType()). + id(multipartSecurityToken.getId()). + tokenFormat(new TokenFormat(multipartTokenFormat.getId())). + tokenValue(multipartSecurityToken.getTokenValue()). + build(); + + var requestHeader = Header.Builder.newInstance() + .context(context) + .id(header.getId()) + .type(header.getType()) + .securityToken(securityToken) + .issuerConnector(header.getIssuerConnector()) + .modelVersion(header.getModelVersion()) + .issued(header.getIssued()) + .senderAgent(header.getSenderAgent()) + .build(); + + return new CreateProcessRequest(requestHeader, handlerRequest.getPayload()); + } + public static ResponseBody getValidResponseBody(){ return ResponseBody.create( MediaType.get("application/json; charset=utf-8"), @@ -134,6 +181,13 @@ public static HandlerRequest getValidHandlerRequest(ObjectMapper mapper){ .payload(TEST_PAYLOAD).build(); } + public static HandlerRequest getValidHandlerCreateProcessRequest(ObjectMapper mapper){ + return HandlerRequest.Builder.newInstance() + .pid(UUID.randomUUID().toString()) + .header(getValidCreateProcessHeader(mapper)) + .payload(TEST_PAYLOAD).build(); + } + public static HandlerRequest getInvalidTokenHandlerRequest(ObjectMapper mapper){ return HandlerRequest.Builder.newInstance() .pid(UUID.randomUUID().toString()) diff --git a/clearing-house-edc/core/src/test/resources/headers/valid-create-process-header.json b/clearing-house-edc/core/src/test/resources/headers/valid-create-process-header.json new file mode 100644 index 0000000..a1cd612 --- /dev/null +++ b/clearing-house-edc/core/src/test/resources/headers/valid-create-process-header.json @@ -0,0 +1,21 @@ +{ + "@context":{ + "ids" : "https://w3id.org/idsa/core/", + "idsc" : "https://w3id.org/idsa/code/" + }, + "@type":"ids:RequestMessage", + "@id":"https://w3id.org/idsa/autogen/logMessage/9fdba4ad-f750-4bbc-a7f0-f648ac853508", + "ids:securityToken": { + "@type" : "ids:DynamicAttributeToken", + "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlMyNTYifQ.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCIsInN1YiI6IjkyOjE0OkU3OkFDOjEwOjIyOkYyOkNDOjA1OjZFOjJBOjJCOjhEOkRCOjEwOkQ2OjREOkEwOkExOjUzOmtleWlkOkNCOjhDOkM3OkI2Ojg1Ojc5OkE4OjIzOkE2OkNCOjE1OkFCOjE3OjUwOjJGOkU2OjY1OjQzOjVEOkU4In0.Qw3gWMgwnKQyVatbsozcin6qtQbLyXlk6QdaLajGaDmxSYqCKEcAje4kiDp5Fqj04WPmVyF0k8c1BJA3KGnaW3Qcikv4MNxqqoenvKIrSTokXsA7-osqBCfxLhV-s2lSXVTAtV_Q7f71eSoR5j-7nPPX8_nf4Xup4_VzfnwRmnuAbLfHfWThbupxFazC34r3waXCltOTFVa_XDlwEDMpPY7vEPeaqIt2t6ofVGo_HF86UB19liL-UZvp0uSE9z2fhloyxOrx9B_xavGS7pP6oRaumSJEN_x9dfdeDS98HQ_oBSSGBzaI4fM7ik35Yg42KQwmkZesD6P_YSEzVLcJDg", + "ids:tokenFormat" : { + "@id" : "idsc:JWT" + } + }, + "ids:senderAgent":"http://example.org", + "ids:modelVersion":"4.1.0", + "ids:issued" : "2021-06-23T17:27:23.566+02:00", + "ids:issuerConnector" : "https://companyA.com/connector/59a68243-dd96-4c8d-88a9-0f0e03e13b1b" +} + From 4a23bb8a3e77e87d4de014e2d2f3756bc979193a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 2 Nov 2023 10:08:04 +0100 Subject: [PATCH 138/183] Feat(ch-app): Merge util modules together into a single util module --- clearing-house-app/src/model/crypto.rs | 2 +- clearing-house-app/src/model/document.rs | 2 +- clearing-house-app/src/model/mod.rs | 1 - clearing-house-app/src/model/util.rs | 5 ----- clearing-house-app/src/util.rs | 16 ++++++++++++++++ 5 files changed, 18 insertions(+), 8 deletions(-) delete mode 100644 clearing-house-app/src/model/util.rs diff --git a/clearing-house-app/src/model/crypto.rs b/clearing-house-app/src/model/crypto.rs index 6c9407c..0079362 100644 --- a/clearing-house-app/src/model/crypto.rs +++ b/clearing-house-app/src/model/crypto.rs @@ -1,5 +1,5 @@ use crate::crypto::generate_random_seed; -use crate::model::util::new_uuid; +use crate::util::new_uuid; use hkdf::Hkdf; use sha2::Sha256; use std::collections::HashMap; diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index 5104db7..a71b2c0 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -1,6 +1,6 @@ use crate::model::constants::SPLIT_CT; use crate::model::crypto::{KeyEntry, KeyMap}; -use crate::model::util::new_uuid; +use crate::util::new_uuid; use aes_gcm_siv::aead::Aead; use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; use base64::Engine; diff --git a/clearing-house-app/src/model/mod.rs b/clearing-house-app/src/model/mod.rs index 9b1d734..a0aade6 100644 --- a/clearing-house-app/src/model/mod.rs +++ b/clearing-house-app/src/model/mod.rs @@ -5,7 +5,6 @@ pub(crate) mod doc_type; pub(crate) mod document; pub mod ids; pub(crate) mod process; -pub(crate) mod util; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum SortingOrder { diff --git a/clearing-house-app/src/model/util.rs b/clearing-house-app/src/model/util.rs deleted file mode 100644 index 60d2efb..0000000 --- a/clearing-house-app/src/model/util.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// Returns a new UUID as a string with hyphens. -pub fn new_uuid() -> String { - use uuid::Uuid; - Uuid::new_v4().hyphenated().to_string() -} diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index 0fe8524..3d80724 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -58,3 +58,19 @@ pub(super) async fn shutdown_signal() { info!("signal received, starting graceful shutdown"); } + +/// Returns a new UUID as a string with hyphens. +pub fn new_uuid() -> String { + use uuid::Uuid; + Uuid::new_v4().hyphenated().to_string() +} + +#[cfg(test)] +mod test { + #[test] + fn test_new_uuid() { + let uuid = super::new_uuid(); + assert_eq!(uuid.len(), 36); + assert_eq!(uuid.chars().filter(|&c| c == '-').count(), 4); + } +} \ No newline at end of file From ba9ad09ff9491f291518b2f8dc236312ae226959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 2 Nov 2023 10:35:27 +0100 Subject: [PATCH 139/183] Feat(ch-app): Add test for parse_date --- clearing-house-app/src/crypto.rs | 10 +- clearing-house-app/src/db/key_store.rs | 6 +- clearing-house-app/src/main.rs | 3 +- clearing-house-app/src/model/document.rs | 1 - clearing-house-app/src/model/ids/message.rs | 23 ++-- clearing-house-app/src/model/mod.rs | 48 +++++--- clearing-house-app/src/ports/doc_type_api.rs | 4 +- clearing-house-app/src/ports/logging_api.rs | 4 +- clearing-house-app/src/ports/mod.rs | 5 +- .../src/services/document_service.rs | 44 +++++-- .../src/services/keyring_service.rs | 111 ++++++++++++------ .../src/services/logging_service.rs | 38 +++--- clearing-house-app/src/util.rs | 2 +- 13 files changed, 186 insertions(+), 113 deletions(-) diff --git a/clearing-house-app/src/crypto.rs b/clearing-house-app/src/crypto.rs index 01db133..4024ba5 100644 --- a/clearing-house-app/src/crypto.rs +++ b/clearing-house-app/src/crypto.rs @@ -226,8 +226,7 @@ mod test { let mut okm = [0u8; super::EXP_BUFF_SIZE]; let mut restored_okm = [0u8; super::EXP_BUFF_SIZE]; - kdf - .expand(salt.as_bytes(), &mut okm) + kdf.expand(salt.as_bytes(), &mut okm) .expect("kdf expansion failed"); restored_kdf .expand(salt.as_bytes(), &mut restored_okm) @@ -248,9 +247,10 @@ mod test { ], ); - let key_map = super::generate_key_map(msk.clone(), dt.clone()).expect("key_map generation failed"); - let restored_key_map = - super::restore_key_map(msk, dt, key_map.clone().keys_enc.unwrap()).expect("key_map restoration failed"); + let key_map = + super::generate_key_map(msk.clone(), dt.clone()).expect("key_map generation failed"); + let restored_key_map = super::restore_key_map(msk, dt, key_map.clone().keys_enc.unwrap()) + .expect("key_map restoration failed"); assert_eq!(key_map, restored_key_map); } diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index ad85931..96a2dd8 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -1,11 +1,11 @@ use super::DataStoreApi; use crate::db::init_database_client; +#[cfg(doc_type)] +use crate::model::constants::MONGO_PID; use crate::model::constants::{ FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, MONGO_COLL_DOC_TYPES, - MONGO_COLL_MASTER_KEY, MONGO_ID + MONGO_COLL_MASTER_KEY, MONGO_ID, }; -#[cfg(doc_type)] -use crate::model::constants::MONGO_PID; use crate::model::crypto::MasterKey; use crate::model::doc_type::DocumentType; use anyhow::anyhow; diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index 75f5284..e4f90fe 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -87,8 +87,7 @@ async fn main() -> Result<(), anyhow::Error> { let app_state = AppState::init(&conf).await?; // Setup router - let app = ports::router() - .with_state(app_state); + let app = ports::router().with_state(app_state); // Bind port and start server let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index a71b2c0..4eff06e 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -46,7 +46,6 @@ impl DocumentPart { let nonce = GenericArray::from_slice(nonce); let cipher = Aes256GcmSiv::new(key); - let pt = format_pt_for_storage(&self.name, &self.content); match cipher.encrypt(nonce, pt.as_bytes()) { Ok(ct) => Ok(ct), diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index b5c6970..ede1b1f 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -139,7 +139,6 @@ impl Default for IdsMessage { } impl IdsMessage { - pub fn restore() -> IdsMessage { IdsMessage { type_message: MessageType::LogMessage, @@ -283,10 +282,7 @@ impl TryFrom for Document { // correlation_message if let Some(s) = m.correlation_message { - doc_parts.push(DocumentPart::new( - CORRELATION_MESSAGE.to_string(), - s, - )); + doc_parts.push(DocumentPart::new(CORRELATION_MESSAGE.to_string(), s)); } // issued @@ -309,18 +305,12 @@ impl TryFrom for Document { // transfer_contract if let Some(s) = m.transfer_contract { - doc_parts.push(DocumentPart::new( - TRANSFER_CONTRACT.to_string(), - s, - )); + doc_parts.push(DocumentPart::new(TRANSFER_CONTRACT.to_string(), s)); } // content_version if let Some(s) = m.content_version { - doc_parts.push(DocumentPart::new( - CONTENT_VERSION.to_string(), - s, - )); + doc_parts.push(DocumentPart::new(CONTENT_VERSION.to_string(), s)); } // security_token @@ -340,7 +330,12 @@ impl TryFrom for Document { } // pid - Ok(Document::new(m.pid.unwrap(), DEFAULT_DOC_TYPE.to_string(), -1, doc_parts)) + Ok(Document::new( + m.pid.unwrap(), + DEFAULT_DOC_TYPE.to_string(), + -1, + doc_parts, + )) } } diff --git a/clearing-house-app/src/model/mod.rs b/clearing-house-app/src/model/mod.rs index a0aade6..653e01e 100644 --- a/clearing-house-app/src/model/mod.rs +++ b/clearing-house-app/src/model/mod.rs @@ -14,19 +14,22 @@ pub enum SortingOrder { Descending, } +/// Parses a date string into a `chrono::NaiveDateTime` object. If `to_date` is true, the time will be set to 23:59:59, otherwise it is 00:00:00. pub fn parse_date(date: Option, to_date: bool) -> Option { - let time_format = if to_date { "23:59:59" } else { "00:00:00" }; + // If it is a to_date, we want to set the time to 23:59:59, otherwise it is 00:00:00 + let time: chrono::NaiveTime = if to_date { + chrono::NaiveTime::from_hms_opt(23, 59, 59).expect("23:59:59 is a valid time") + } else { + chrono::NaiveTime::from_hms_opt(0, 0, 0).expect("00:00:00 is a valid time") + }; match date { Some(d) => { debug!("Parsing date: {}", &d); - match chrono::NaiveDateTime::parse_from_str( - format!("{} {}", &d, &time_format).as_str(), - "%Y-%m-%d %H:%M:%S", - ) { - Ok(date) => Some(date), + match chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d") { + Ok(date) => Some(date.and_time(time)), Err(e) => { - error!("Error occurred: {:#?}", e); + error!("Parsing date '{d}' failed: {:#?}", e); None } } @@ -54,12 +57,6 @@ pub fn validate_and_sanitize_dates( .expect("00:00:00 is a valid time") - chrono::Duration::weeks(2); - println!("date_to: {:#?}", date_to); - println!("date_from: {:#?}", date_from); - - println!("Default date_to: {:#?}", default_to_date); - println!("Default date_from: {:#?}", default_from_date); - match (date_from, date_to) { (Some(from), None) if from < now => Ok((from, default_to_date)), (Some(from), Some(to)) if from < now && to <= now && from < to => Ok((from, to)), @@ -70,11 +67,15 @@ pub fn validate_and_sanitize_dates( #[cfg(test)] mod test { + #[test] fn validate_and_sanitize_dates() { // Setup dates for testing let date_now = chrono::Local::now().naive_local(); - let date_now_midnight = date_now.date().and_hms_opt(0, 0, 0).unwrap(); + let date_now_midnight = date_now + .date() + .and_hms_opt(0, 0, 0) + .expect("00:00:00 is a valid time"); let date_from = date_now_midnight - chrono::Duration::weeks(2); let date_to = date_now_midnight - chrono::Duration::weeks(1); @@ -126,4 +127,23 @@ mod test { .is_err() ); } + + #[test] + fn parse_date() { + let wrong_date = Some("2020-13-01".to_string()); + let valid_date = Some("2020-01-01".to_string()); + let valid_date_parsed = chrono::NaiveDate::from_ymd_opt(2020, 1, 1).expect("This is valid"); + let day_start_time = chrono::NaiveTime::from_hms_opt(0, 0, 0).expect("This is valid"); + let day_end_time = chrono::NaiveTime::from_hms_opt(23, 59, 59).expect("This is valid"); + + assert!(super::parse_date(wrong_date, false).is_none()); + assert_eq!( + super::parse_date(valid_date.clone(), false), + Some(valid_date_parsed.and_time(day_start_time)) + ); + assert_eq!( + super::parse_date(valid_date, true), + Some(valid_date_parsed.and_time(day_end_time)) + ); + } } diff --git a/clearing-house-app/src/ports/doc_type_api.rs b/clearing-house-app/src/ports/doc_type_api.rs index eb4df3e..fd38a01 100644 --- a/clearing-house-app/src/ports/doc_type_api.rs +++ b/clearing-house-app/src/ports/doc_type_api.rs @@ -1,6 +1,6 @@ -use axum::http::StatusCode; use crate::model::constants::DEFAULT_PROCESS_ID; use crate::ports::ApiResponse; +use axum::http::StatusCode; use crate::model::doc_type::DocumentType; use crate::services::keyring_service::KeyringServiceError; @@ -81,7 +81,7 @@ async fn get_doc_type( match state.keyring_service.get_doc_type(id, pid).await { Ok(dt) => match dt { Some(dt) => Ok((StatusCode::OK, Json(Some(dt)))), - None => Ok((StatusCode::OK, Json(None))) + None => Ok((StatusCode::OK, Json(None))), }, Err(e) => { error!("Error while retrieving doctype: {:?}", e); diff --git a/clearing-house-app/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs index 94237f2..bfcbf2b 100644 --- a/clearing-house-app/src/ports/logging_api.rs +++ b/clearing-house-app/src/ports/logging_api.rs @@ -1,7 +1,7 @@ -use axum::http::StatusCode; -use axum::Json; use crate::model::claims::ExtractChClaims; use crate::{model::claims::get_jwks, model::SortingOrder, AppState}; +use axum::http::StatusCode; +use axum::Json; use biscuit::jwk::JWKSet; use crate::model::ids::message::IdsMessage; diff --git a/clearing-house-app/src/ports/mod.rs b/clearing-house-app/src/ports/mod.rs index 3636fbb..8360709 100644 --- a/clearing-house-app/src/ports/mod.rs +++ b/clearing-house-app/src/ports/mod.rs @@ -20,9 +20,8 @@ pub(crate) fn router() -> axum::routing::Router { /// Router for the logging service #[cfg(not(doc_type))] pub(crate) fn router() -> axum::routing::Router { - axum::Router::new() - .merge(logging_api::router()) + axum::Router::new().merge(logging_api::router()) } /// Result type alias for the API -pub(crate) type ApiResult = Result<(axum::http::StatusCode, axum::response::Json), E>; \ No newline at end of file +pub(crate) type ApiResult = Result<(axum::http::StatusCode, axum::response::Json), E>; diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 3c1d930..a5a53af 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -41,7 +41,9 @@ impl axum::response::IntoResponse for DocumentServiceError { fn into_response(self) -> axum::response::Response { use axum::http::StatusCode; match self { - Self::DocumentAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::DocumentAlreadyExists => { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } Self::MissingPayload => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::DatabaseError { source, @@ -51,12 +53,18 @@ impl axum::response::IntoResponse for DocumentServiceError { format!("{}: {}", description, source), ) .into_response(), - Self::ChainHashError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), + Self::ChainHashError => { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } Self::KeyringServiceError(e) => e.into_response(), Self::InvalidDates => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), - Self::CorruptedCiphertext(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), - Self::EncryptionError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), + Self::CorruptedCiphertext(e) => { + (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() + } + Self::EncryptionError => { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } } } } @@ -88,7 +96,10 @@ impl DocumentService { .collect(); // If the document contains more than 1 payload we panic. This should never happen! - assert!(payload.len() <= 1, "Document contains two or more payloads!"); + assert!( + payload.len() <= 1, + "Document contains two or more payloads!" + ); if payload.is_empty() { return Err(DocumentServiceError::MissingPayload); } @@ -162,7 +173,10 @@ impl DocumentService { Ok(_b) => Ok(receipt), Err(e) => { error!("Error while adding: {:?}", e); - Err(DocumentServiceError::DatabaseError { source: e, description: "Error while adding document".to_string() }) + Err(DocumentServiceError::DatabaseError { + source: e, + description: "Error while adding document".to_string(), + }) } } } @@ -201,10 +215,10 @@ impl DocumentService { // Validation of dates with various checks. If none given dates default to date_now (date_to) and (date_now - 2 weeks) (date_from) let Ok((sanitized_date_from, sanitized_date_to)) = validate_and_sanitize_dates(parsed_date_from, parsed_date_to, None) - else { - debug!("date validation failed!"); - return Err(DocumentServiceError::InvalidDates); - }; + else { + debug!("date validation failed!"); + return Err(DocumentServiceError::InvalidDates); + }; //new behavior: if pages are "invalid" return {}. Do not adjust page //either call db with type filter or without to get cts @@ -233,7 +247,10 @@ impl DocumentService { Ok(cts) => cts, Err(e) => { error!("Error while retrieving document: {:?}", e); - return Err(DocumentServiceError::DatabaseError { source: e, description: "Error while retrieving document".to_string() }); + return Err(DocumentServiceError::DatabaseError { + source: e, + description: "Error while retrieving document".to_string(), + }); } }; @@ -368,7 +385,10 @@ impl DocumentService { } Err(e) => { error!("Error while retrieving document: {:?}", e); - Err(DocumentServiceError::DatabaseError {source: e, description: "Error while retrieving document".to_string()}) + Err(DocumentServiceError::DatabaseError { + source: e, + description: "Error while retrieving document".to_string(), + }) } } } diff --git a/clearing-house-app/src/services/keyring_service.rs b/clearing-house-app/src/services/keyring_service.rs index df08149..8a19257 100644 --- a/clearing-house-app/src/services/keyring_service.rs +++ b/clearing-house-app/src/services/keyring_service.rs @@ -28,13 +28,27 @@ impl axum::response::IntoResponse for KeyringServiceError { fn into_response(self) -> axum::response::Response { use axum::http::StatusCode; match self { - Self::KeymapGenerationFailed => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), - Self::KeymapRestorationFailed => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), + Self::KeymapGenerationFailed => { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } + Self::KeymapRestorationFailed => { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } Self::DocumentTypeNotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), - Self::DatabaseError { source, description } => - (StatusCode::INTERNAL_SERVER_ERROR, format!("{}: {}", description, source)).into_response(), - Self::DecryptionError => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(), - Self::DocumentTypeAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::DatabaseError { + source, + description, + } => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("{}: {}", description, source), + ) + .into_response(), + Self::DecryptionError => { + (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() + } + Self::DocumentTypeAlreadyExists => { + (StatusCode::BAD_REQUEST, self.to_string()).into_response() + } } } } @@ -81,13 +95,19 @@ impl KeyringService { } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: "Error while retrieving document type".to_string() }) + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while retrieving document type".to_string(), + }) } } } Err(e) => { error!("Error while retrieving master key: {}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: "Error while retrieving master key".to_string() }) + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while retrieving master key".to_string(), + }) } } } @@ -150,7 +170,10 @@ impl KeyringService { } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: "Error while retrieving document type".to_string() }) + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while retrieving document type".to_string(), + }) } } } @@ -198,7 +221,10 @@ impl KeyringService { } Err(e) => { warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: "Error while retrieving document type".to_string() }) + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while retrieving document type".to_string(), + }) } } } @@ -231,18 +257,22 @@ impl KeyringService { .await { Ok(true) => Err(KeyringServiceError::DocumentTypeAlreadyExists), // BadRequest - Ok(false) => { - match self.db.add_document_type(doc_type.clone()).await { - Ok(()) => Ok(doc_type), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: "Error while adding doctype".to_string() }) - } + Ok(false) => match self.db.add_document_type(doc_type.clone()).await { + Ok(()) => Ok(doc_type), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while adding doctype".to_string(), + }) } - } + }, Err(e) => { error!("Error while adding document type: {:?}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: "Error while checking doctype".to_string() }) + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while checking doctype".to_string(), + }) } } } @@ -259,24 +289,32 @@ impl KeyringService { .await { Ok(true) => Err(KeyringServiceError::DocumentTypeAlreadyExists), - Ok(false) => { - match self.db.update_document_type(doc_type, &id).await { - Ok(id) => Ok(id), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: "Error while storing document type!".to_string() }) - } + Ok(false) => match self.db.update_document_type(doc_type, &id).await { + Ok(id) => Ok(id), + Err(e) => { + error!("Error while adding doctype: {:?}", e); + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while storing document type!".to_string(), + }) } - } + }, Err(e) => { error!("Error while adding document type: {:?}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: "Error while checking doctype".to_string() }) + Err(KeyringServiceError::DatabaseError { + source: e, + description: "Error while checking doctype".to_string(), + }) } } } #[cfg(doc_type)] - pub(crate) async fn delete_doc_type(&self, id: String, pid: String) -> Result { + pub(crate) async fn delete_doc_type( + &self, + id: String, + pid: String, + ) -> Result { match self.db.delete_document_type(&id, &pid).await { Ok(true) => Ok(String::from("Document type deleted!")), // NoContent Ok(false) => Err(KeyringServiceError::DocumentTypeNotFound), @@ -284,8 +322,7 @@ impl KeyringService { error!("Error while deleting doctype: {:?}", e); Err(KeyringServiceError::DatabaseError { source: e, - description: format!("Error while deleting document type with id {}!", - id), + description: format!("Error while deleting document type with id {}!", id), }) } } @@ -302,13 +339,21 @@ impl KeyringService { Ok(dt) => Ok(dt), Err(e) => { error!("Error while retrieving doctype: {:?}", e); - Err(KeyringServiceError::DatabaseError { source: e, description: format!("Error while retrieving document type with id {} and pid {}!", id, pid) }) + Err(KeyringServiceError::DatabaseError { + source: e, + description: format!( + "Error while retrieving document type with id {} and pid {}!", + id, pid + ), + }) } } } #[cfg(doc_type)] - pub(crate) async fn get_doc_types(&self) -> Result, KeyringServiceError> { + pub(crate) async fn get_doc_types( + &self, + ) -> Result, KeyringServiceError> { match self.db.get_all_document_types().await { //TODO: would like to send "{}" instead of "null" when dt is not found Ok(dt) => Ok(dt), diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 26010c3..84e2643 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -56,21 +56,15 @@ impl axum::response::IntoResponse for LoggingServiceError { format!("{}: {}", description, source), ) .into_response(), - Self::UserNotAuthorized => { - (StatusCode::FORBIDDEN, self.to_string()).into_response() - } - Self::InvalidRequest => { - (StatusCode::BAD_REQUEST, self.to_string()).into_response() - } + Self::UserNotAuthorized => (StatusCode::FORBIDDEN, self.to_string()).into_response(), + Self::InvalidRequest => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::ProcessAlreadyExists => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } Self::ProcessDoesNotExist(_) => { (StatusCode::NOT_FOUND, self.to_string()).into_response() } - Self::ParsingError(_) => { - (StatusCode::BAD_REQUEST, self.to_string()).into_response() - } + Self::ParsingError(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::DocumentServiceError(e) => e.into_response(), } } @@ -120,7 +114,9 @@ impl LoggingService { }?; // Check if process exists and if the user is authorized to access the process - if let Err(LoggingServiceError::ProcessDoesNotExist(_)) = self.get_process_and_check_authorized(&pid, user).await { + if let Err(LoggingServiceError::ProcessDoesNotExist(_)) = + self.get_process_and_check_authorized(&pid, user).await + { // convenience: if process does not exist, we create it but only if no error occurred before info!("Requested pid '{}' does not exist. Creating...", &pid); // create a new process @@ -265,11 +261,10 @@ impl LoggingService { } } } - Err(e) => - Err(LoggingServiceError::DatabaseError { - source: e, - description: "Error while getting process".to_string(), - }) + Err(e) => Err(LoggingServiceError::DatabaseError { + source: e, + description: "Error while getting process".to_string(), + }), } } @@ -372,7 +367,11 @@ impl LoggingService { } /// Checks if a process exists and the user is authorized to access the process - async fn get_process_and_check_authorized(&self, pid: &String, user: &str) -> Result { + async fn get_process_and_check_authorized( + &self, + pid: &String, + user: &str, + ) -> Result { match self.db.get_process(pid).await { Ok(Some(p)) if !p.is_authorized(user) => { warn!("User is not authorized to read from pid '{}'", &pid); @@ -382,9 +381,7 @@ impl LoggingService { info!("User authorized."); Ok(p) } - Ok(None) => { - Err(LoggingServiceError::ProcessDoesNotExist(pid.clone())) - } + Ok(None) => Err(LoggingServiceError::ProcessDoesNotExist(pid.clone())), Err(e) => { error!("Error while getting process '{}': {}", &pid, e); Err(LoggingServiceError::DatabaseError { @@ -396,7 +393,6 @@ impl LoggingService { } } - #[cfg(test)] mod test { use super::LoggingService; @@ -407,4 +403,4 @@ mod test { assert!(LoggingService::check_for_default_pid(DEFAULT_PROCESS_ID).is_err()); assert!(LoggingService::check_for_default_pid("not_default").is_ok()); } -} \ No newline at end of file +} diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index 3d80724..bcbab1c 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -73,4 +73,4 @@ mod test { assert_eq!(uuid.len(), 36); assert_eq!(uuid.chars().filter(|&c| c == '-').count(), 4); } -} \ No newline at end of file +} From 812f3e868bfb4c17c5a18765bacaf7826ef99532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 2 Nov 2023 12:02:21 +0100 Subject: [PATCH 140/183] fix(ch-app): Fix all clippy warnings --- clearing-house-app/src/db/doc_store.rs | 32 +++++----- clearing-house-app/src/db/key_store.rs | 3 +- clearing-house-app/src/db/process_store.rs | 2 +- clearing-house-app/src/model/claims.rs | 11 +++- clearing-house-app/src/model/document.rs | 59 ++++++++++--------- clearing-house-app/src/model/ids/message.rs | 3 +- clearing-house-app/src/model/ids/mod.rs | 10 +++- clearing-house-app/src/model/process.rs | 11 ++-- clearing-house-app/src/ports/logging_api.rs | 17 +++--- .../src/services/document_service.rs | 16 ++--- .../src/services/logging_service.rs | 7 +-- 11 files changed, 92 insertions(+), 79 deletions(-) diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 39f5ab6..449872b 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -160,7 +160,7 @@ impl DataStore { match coll.update_one(query, doc! { "$push": { - MONGO_DOC_ARRAY: mongodb::bson::to_bson(&bucket_update).unwrap(), + MONGO_DOC_ARRAY: mongodb::bson::to_bson(&bucket_update)?, }, "$inc": {"counter": 1}, "$setOnInsert": { "_id": format!("{}_{}", doc.pid.clone(), doc.ts), MONGO_DT_ID: doc.dt_id.clone(), MONGO_FROM_TS: doc.ts}, @@ -280,8 +280,7 @@ impl DataStore { page: u64, size: u64, sort: &SortingOrder, - date_from: &chrono::NaiveDateTime, - date_to: &chrono::NaiveDateTime, + (date_from, date_to): (&chrono::NaiveDateTime, &chrono::NaiveDateTime), ) -> anyhow::Result> { debug!( "...trying to get page {} of size {} of documents for pid {} of dt {}...", @@ -289,7 +288,7 @@ impl DataStore { ); match self - .get_start_bucket_size(dt_id, pid, page, size, sort, date_from, date_to) + .get_start_bucket_size(dt_id, pid, page, size, sort, (date_from, date_to)) .await { Ok(bucket_size) => { @@ -318,22 +317,22 @@ impl DataStore { let pipeline = vec![ doc! {"$match":{ - MONGO_PID: pid.clone(), - MONGO_DT_ID: dt_id.clone(), - MONGO_FROM_TS: {"$lte": date_to.timestamp()}, - MONGO_TO_TS: {"$gte": date_from.timestamp()} + MONGO_PID: pid.clone(), + MONGO_DT_ID: dt_id.clone(), + MONGO_FROM_TS: {"$lte": date_to.timestamp()}, + MONGO_TO_TS: {"$gte": date_from.timestamp()} }}, - doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, - doc! {"$skip" : skip_buckets}, + doc! {"$sort": {MONGO_FROM_TS: sort_order}}, + doc! {"$skip": skip_buckets}, // worst case: overlap between two buckets. - doc! {"$limit" : 2}, - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$limit": 2}, + doc! {"$unwind": format ! ("${}", MONGO_DOC_ARRAY)}, doc! {"$replaceRoot": { "newRoot": "$documents"}}, doc! {"$match":{ - MONGO_TS: {"$gte": date_from.timestamp(), "$lte": date_to.timestamp()} + MONGO_TS: {"$gte": date_from.timestamp(), "$lte": date_to.timestamp()} }}, - doc! {"$sort" : {MONGO_TS: sort_order}}, - doc! {"$skip" : start_entry as i32}, + doc! {"$sort": {MONGO_TS: sort_order}}, + doc! {"$skip": start_entry as i32}, doc! { "$limit": size as i32}, ]; @@ -369,8 +368,7 @@ impl DataStore { page: u64, size: u64, sort: &SortingOrder, - date_from: &chrono::NaiveDateTime, - date_to: &chrono::NaiveDateTime, + (date_from, date_to): (&chrono::NaiveDateTime, &chrono::NaiveDateTime), ) -> anyhow::Result { debug!("...trying to get the offset for page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); let sort_order = match sort { diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs index 96a2dd8..335011c 100644 --- a/clearing-house-app/src/db/key_store.rs +++ b/clearing-house-app/src/db/key_store.rs @@ -59,8 +59,7 @@ impl KeyStore { debug!("Database empty. Need to initialize..."); debug!("Adding initial document type..."); match serde_json::from_str::( - &crate::util::read_file(FILE_DEFAULT_DOC_TYPE) - .unwrap_or(String::new()), + &crate::util::read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or_default(), ) { Ok(dt) => match keystore.add_document_type(dt).await { Ok(_) => { diff --git a/clearing-house-app/src/db/process_store.rs b/clearing-house-app/src/db/process_store.rs index 1d8aff6..bea5f43 100644 --- a/clearing-house-app/src/db/process_store.rs +++ b/clearing-house-app/src/db/process_store.rs @@ -192,7 +192,7 @@ impl ProcessStore { trace!("didn't find process"); Ok(false) } - _ => Err(anyhow!("User '{}' could not be authorized", &user).into()), + _ => Err(anyhow!("User '{}' could not be authorized", &user)), } } diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index f5b3a07..f51fa86 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -45,7 +45,13 @@ where .await .map_err(|err| err.into_response())?; if let Some(token) = parts.headers.get(SERVICE_HEADER) { - let token = token.to_str().unwrap(); + let token = token.to_str().map_err(|_| { + ( + axum::http::StatusCode::BAD_REQUEST, + format!("Invalid token in {SERVICE_HEADER}"), + ) + .into_response() + })?; debug!("...received service header: {:?}", token); match decode_token::(token, app_state.service_config.service_id.as_str()) { @@ -106,7 +112,8 @@ pub fn get_jwks(key_path: &str) -> Option> } pub fn get_fingerprint(key_path: &str) -> Option { - let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); + let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path) + .unwrap_or_else(|_| panic!("File exists at '{key_path}' and is a valid RSA keypair")); if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { let pk_modulus = a .as_ref() diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index 4eff06e..383231b 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -105,18 +105,22 @@ impl Document { for part in self.parts.iter() { // check if there's a key for this part - if !keys.contains_key(&part.name) { - error!("Missing key for part '{}'", &part.name); - anyhow::bail!("Missing key for part '{}'", &part.name); - } - // get the key for this part - let key_entry = keys.get(&part.name).unwrap(); - let ct = part.encrypt(key_entry.key.as_slice(), key_entry.nonce.as_slice()); - if ct.is_err() { - warn!("Encryption error. No ct received!"); - anyhow::bail!("Encryption error. No ct received!"); - } - let ct_string = hex::encode_upper(ct.unwrap()); + let key_entry = match keys.get(&part.name) { + Some(key_entry) => key_entry, + None => { + error!("Missing key for part '{}'", &part.name); + anyhow::bail!("Missing key for part '{}'", &part.name); + } + }; + // Encrypt part + let ct_string = match part.encrypt(key_entry.key.as_slice(), key_entry.nonce.as_slice()) + { + Ok(ct) => hex::encode_upper(ct), + Err(e) => { + error!("Error while encrypting: {}", e); + anyhow::bail!("Error while encrypting: {}", e); + } + }; // key entry id is needed for decryption cts.push(format!("{}::{}", key_entry.id, ct_string)); @@ -200,22 +204,23 @@ impl EncryptedDocument { } // get key and nonce let key_entry = keys.get(ct_parts[0]); - if key_entry.is_none() { - anyhow::bail!("Key for id '{}' does not exist!", ct_parts[0]); - } - let key = key_entry.unwrap().key.as_slice(); - let nonce = key_entry.unwrap().nonce.as_slice(); - - // get ciphertext - //TODO: use error_chain? - let ct = hex::decode(ct_parts[1]).unwrap(); - - // decrypt - match DocumentPart::decrypt(key, nonce, ct.as_slice()) { - Ok(part) => pts.push(part), - Err(e) => { - anyhow::bail!("Error while decrypting: {}", e); + if let Some(key_entry) = key_entry { + let key = key_entry.key.as_slice(); + let nonce = key_entry.nonce.as_slice(); + + // get ciphertext + //TODO: use error_chain? + let ct = hex::decode(ct_parts[1])?; + + // decrypt + match DocumentPart::decrypt(key, nonce, ct.as_slice()) { + Ok(part) => pts.push(part), + Err(e) => { + anyhow::bail!("Error while decrypting: {}", e); + } } + } else { + anyhow::bail!("Key for id '{}' does not exist!", ct_parts[0]); } } diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index ede1b1f..ee06824 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -264,6 +264,7 @@ impl TryFrom for Document { type Error = serde_json::Error; fn try_from(m: IdsMessage) -> Result { + use serde::ser::Error; let mut doc_parts = vec![]; // message_id @@ -331,7 +332,7 @@ impl TryFrom for Document { // pid Ok(Document::new( - m.pid.unwrap(), + m.pid.ok_or(serde_json::Error::custom("PID missing"))?, DEFAULT_DOC_TYPE.to_string(), -1, doc_parts, diff --git a/clearing-house-app/src/model/ids/mod.rs b/clearing-house-app/src/model/ids/mod.rs index ad494b3..0882585 100644 --- a/clearing-house-app/src/model/ids/mod.rs +++ b/clearing-house-app/src/model/ids/mod.rs @@ -13,8 +13,16 @@ pub struct InfoModelComplexId { impl std::fmt::Display for InfoModelComplexId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use serde::ser::Error; + match &self.id { - Some(id) => write!(f, "{}", serde_json::to_string(id).unwrap()), + Some(id) => write!( + f, + "{}", + serde_json::to_string(id).map_err(|e| std::fmt::Error::custom(format!( + "JSON serialization failed: {e}" + )))? + ), None => write!(f, ""), } } diff --git a/clearing-house-app/src/model/process.rs b/clearing-house-app/src/model/process.rs index d2f095c..9ea3c5b 100644 --- a/clearing-house-app/src/model/process.rs +++ b/clearing-house-app/src/model/process.rs @@ -57,10 +57,13 @@ impl DataTransaction { self.clone(), ); - let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path).unwrap(); - println!("decoded JWS:{:#?}", &jws); + let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path) + .unwrap_or_else(|_| panic!("File exists at '{key_path}' and is a valid RSA keypair")); + debug!("decoded JWS:{:#?}", &jws); Receipt { - data: jws.into_encoded(&keypair).unwrap(), + data: jws + .into_encoded(&keypair) + .expect("Encoded JWS with keypair"), } } } @@ -73,7 +76,7 @@ impl From for DataTransaction { match r.data.unverified_payload() { Ok(d) => d, Err(e) => { - println!("Error occured: {:#?}", e); + println!("Error occurred: {:#?}", e); DataTransaction { transaction_id: "error".to_string(), timestamp: 0, diff --git a/clearing-house-app/src/ports/logging_api.rs b/clearing-house-app/src/ports/logging_api.rs index bfcbf2b..cb60f9a 100644 --- a/clearing-house-app/src/ports/logging_api.rs +++ b/clearing-house-app/src/ports/logging_api.rs @@ -1,7 +1,6 @@ use crate::model::claims::ExtractChClaims; use crate::{model::claims::get_jwks, model::SortingOrder, AppState}; use axum::http::StatusCode; -use axum::Json; use biscuit::jwk::JWKSet; use crate::model::ids::message::IdsMessage; @@ -23,7 +22,7 @@ async fn log( .log(ch_claims, state.signing_key_path.as_str(), message, pid) .await { - Ok(id) => Ok((StatusCode::CREATED, Json(id))), + Ok(id) => Ok((StatusCode::CREATED, axum::Json(id))), Err(e) => { error!("Error while logging: {:?}", e); Err(e) @@ -47,7 +46,10 @@ async fn create_process( .create_process(ch_claims, message, pid) .await { - Ok(id) => Ok((StatusCode::CREATED, Json(CreateProcessResponse { pid: id }))), + Ok(id) => Ok(( + StatusCode::CREATED, + axum::Json(CreateProcessResponse { pid: id }), + )), Err(e) => { error!("Error while creating process: {:?}", e); Err(e) @@ -78,13 +80,12 @@ async fn query_pid( params.page, params.size, params.sort, - params.date_to, - params.date_from, + (params.date_to, params.date_from), pid, ) .await { - Ok(result) => Ok((StatusCode::OK, Json(result))), + Ok(result) => Ok((StatusCode::OK, axum::Json(result))), Err(e) => { error!("Error while querying: {:?}", e); Err(e) @@ -104,7 +105,7 @@ async fn query_id( .query_id(ch_claims, pid, id, message) .await { - Ok(result) => Ok((StatusCode::OK, Json(result))), + Ok(result) => Ok((StatusCode::OK, axum::Json(result))), Err(e) => { error!("Error while querying: {:?}", e); Err(e) @@ -116,7 +117,7 @@ async fn get_public_sign_key( axum::extract::State(state): axum::extract::State, ) -> super::ApiResult, &'static str> { match get_jwks(state.signing_key_path.as_str()) { - Some(jwks) => Ok((StatusCode::OK, Json(jwks))), + Some(jwks) => Ok((StatusCode::OK, axum::Json(jwks))), None => Err("Error reading signing key"), } } diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index a5a53af..73c6207 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -187,12 +187,10 @@ impl DocumentService { pub(crate) async fn get_enc_documents_for_pid( &self, ch_claims: ChClaims, - doc_type: Option, page: Option, size: Option, sort: Option, - date_from: Option, - date_to: Option, + (date_from, date_to): (Option, Option), pid: String, ) -> Result { debug!("Trying to retrieve documents for pid '{}'...", &pid); @@ -202,6 +200,7 @@ impl DocumentService { page, size, sort ); + let dt_id = String::from(DEFAULT_DOC_TYPE); let sanitized_page = Self::sanitize_page(page); let sanitized_size = Self::sanitize_size(size); @@ -227,10 +226,6 @@ impl DocumentService { sanitized_page, sanitized_size, &sanitized_sort ); - let dt_id = match doc_type { - Some(dt) => dt, - None => String::from(DEFAULT_DOC_TYPE), - }; let cts = match self .db .get_documents_for_pid( @@ -239,8 +234,7 @@ impl DocumentService { sanitized_page, sanitized_size, &sanitized_sort, - &sanitized_date_from, - &sanitized_date_to, + (&sanitized_date_from, &sanitized_date_to), ) .await { @@ -337,8 +331,8 @@ impl DocumentService { &id, &pid ); - if hash.is_some() { - debug!("integrity check with hash: {}", hash.as_ref().unwrap()); + if let Some(hash) = hash { + debug!("integrity check with hash: {}", hash); } match self.db.get_document(&id, &pid).await { diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 84e2643..d1a3328 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -274,8 +274,7 @@ impl LoggingService { page: Option, size: Option, sort: Option, - date_to: Option, - date_from: Option, + (date_to, date_from): (Option, Option), pid: String, ) -> Result { debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); @@ -298,12 +297,10 @@ impl LoggingService { .doc_api .get_enc_documents_for_pid( ChClaims::new(user), - None, Some(sanitized_page), Some(sanitized_size), Some(sanitized_sort), - date_from, - date_to, + (date_from, date_to), pid.clone(), ) .await From 918a9035ac1e61a0faa8716143f25886d049dae2 Mon Sep 17 00:00:00 2001 From: dhommen Date: Thu, 2 Nov 2023 14:50:55 +0100 Subject: [PATCH 141/183] feat(ch-app): feature flag sentry --- clearing-house-app/Cargo.lock | 328 +++++++++++++++++++++++++++++++++ clearing-house-app/Cargo.toml | 2 + clearing-house-app/src/main.rs | 5 + 3 files changed, 335 insertions(+) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 3a72478..db42cd0 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -358,6 +358,7 @@ dependencies = [ "openssh-keys", "rand", "ring 0.16.20", + "sentry", "serde", "serde_json", "serial_test", @@ -396,6 +397,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -485,6 +496,16 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + [[package]] name = "deranged" version = "0.3.9" @@ -529,6 +550,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-as-inner" version = "0.4.0" @@ -563,6 +593,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + [[package]] name = "finl_unicode" version = "1.2.0" @@ -857,6 +899,19 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -1143,6 +1198,24 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -1287,6 +1360,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "serde", + "winapi", +] + [[package]] name = "overload" version = "0.1.1" @@ -1521,6 +1605,44 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64 0.21.5", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "resolv-conf" version = "0.7.0" @@ -1650,6 +1772,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1666,6 +1797,29 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "0.9.0" @@ -1687,6 +1841,114 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" +[[package]] +name = "sentry" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0097a48cd1999d983909f07cb03b15241c5af29e5e679379efac1c06296abecc" +dependencies = [ + "httpdate", + "native-tls", + "reqwest", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-debug-images", + "sentry-panic", + "sentry-tracing", + "tokio", + "ureq", +] + +[[package]] +name = "sentry-backtrace" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a7b80fa1dd6830a348d38a8d3a9761179047757b7dca29aef82db0118b9670" +dependencies = [ + "backtrace", + "once_cell", + "regex", + "sentry-core", +] + +[[package]] +name = "sentry-contexts" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7615dc588930f1fd2e721774f25844ae93add2dbe2d3c2f995ce5049af898147" +dependencies = [ + "hostname", + "libc", + "os_info", + "rustc_version 0.4.0", + "sentry-core", + "uname", +] + +[[package]] +name = "sentry-core" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f51264e4013ed9b16558cce43917b983fa38170de2ca480349ceb57d71d6053" +dependencies = [ + "once_cell", + "rand", + "sentry-types", + "serde", + "serde_json", +] + +[[package]] +name = "sentry-debug-images" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fe6180fa564d40bb942c9f0084ffb5de691c7357ead6a2b7a3154fae9e401dd" +dependencies = [ + "findshlibs", + "once_cell", + "sentry-core", +] + +[[package]] +name = "sentry-panic" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323160213bba549f9737317b152af116af35c0410f4468772ee9b606d3d6e0fa" +dependencies = [ + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-tracing" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38033822128e73f7b6ca74c1631cef8868890c6cb4008a291cf73530f87b4eac" +dependencies = [ + "sentry-backtrace", + "sentry-core", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sentry-types" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e663b3eb62ddfc023c9cf5432daf5f1a4f6acb1df4d78dd80b740b32dd1a740" +dependencies = [ + "debugid", + "hex", + "rand", + "serde", + "serde_json", + "thiserror", + "time", + "url", + "uuid", +] + [[package]] name = "serde" version = "1.0.189" @@ -1935,6 +2197,27 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "take_mut" version = "0.2.2" @@ -2063,6 +2346,16 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-openssl" version = "0.6.3" @@ -2267,6 +2560,15 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2310,6 +2612,19 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +dependencies = [ + "base64 0.21.5", + "log", + "native-tls", + "once_cell", + "url", +] + [[package]] name = "url" version = "2.4.1" @@ -2319,6 +2634,7 @@ dependencies = [ "form_urlencoded", "idna 0.4.0", "percent-encoding", + "serde", ] [[package]] @@ -2389,6 +2705,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.87" diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index cfaf038..dc5b925 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -56,6 +56,7 @@ async-trait = "0.1.73" # Helper for working with futures futures = "0.3.28" thiserror = "1.0.48" +sentry = { version = "0.31.7", optional = true } [dev-dependencies] # Controlling execution of unit test cases, which could interfere with each other @@ -67,3 +68,4 @@ tempfile = "3.8.0" default = [] # Enables the doc_type API doc_type = [] +sentry = ["dep:sentry"] diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index e4f90fe..e59ecdb 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -77,6 +77,11 @@ impl AppState { /// Main function: Reading config, initializing application state, starting server #[tokio::main] async fn main() -> Result<(), anyhow::Error> { + #[cfg(feature = "sentry")] + let _guard = sentry::init(("https://347cc3aa30aa0c07d437da8c780838d3@o4506146399322112.ingest.sentry.io/4506155710480384", sentry::ClientOptions { + release: sentry::release_name!(), + ..Default::default() + })); // Read configuration let conf = config::read_config(None); config::configure_logging(&conf); From 6136bada6d12d7194fca6308108ec9fc0d528110 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 8 Nov 2023 13:36:24 +0100 Subject: [PATCH 142/183] chore(docs): update README.md --- README.md | 123 ++++++------------- docker/application.yml | 14 --- docker/daps_cachain.crt | 112 ----------------- docker/docker-compose.yml | 12 ++ docker/document-api-multistage.Dockerfile | 23 ---- docker/document-api.Dockerfile | 24 ---- docker/entrypoint.sh | 4 - docker/keyring-api-multistage.Dockerfile | 23 ---- docker/keyring-api.Dockerfile | 24 ---- docker/logging-service-multistage.Dockerfile | 23 ---- docker/logging-service.Dockerfile | 24 ---- docs/SUMMARY.md | 1 + docs/old_README.md | 101 +++++++++++++++ package.json | 8 +- 14 files changed, 156 insertions(+), 360 deletions(-) delete mode 100644 docker/application.yml delete mode 100644 docker/daps_cachain.crt create mode 100644 docker/docker-compose.yml delete mode 100644 docker/document-api-multistage.Dockerfile delete mode 100644 docker/document-api.Dockerfile delete mode 100755 docker/entrypoint.sh delete mode 100644 docker/keyring-api-multistage.Dockerfile delete mode 100644 docker/keyring-api.Dockerfile delete mode 100644 docker/logging-service-multistage.Dockerfile delete mode 100644 docker/logging-service.Dockerfile create mode 100644 docs/old_README.md diff --git a/README.md b/README.md index 6fa2b14..3dd805b 100644 --- a/README.md +++ b/README.md @@ -1,101 +1,50 @@ -# IDS Clearing House -The IDS Clearing House Service is a prototype implementation of the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) component of the [Industrial Data Space](https://internationaldataspaces.org/). - -Data in the Clearing House is stored encrypted and practically immutable. There are multiple ways in which the Clearing House enforces Data Immutability: -- Using the `Logging Service` there is no way to update an already existing log entry in the database -- Log entries in the database include a hash value of the previous log entry, chaining together all log entries. Any change to a previous log entry would require rehashing all following log entries. -- The connector logging information in the Clearing House receives a signed receipt from the Clearing House that includes among other things a timestamp and the current chain hash. A single valid receipt in possession of any connector is enough to detect any change to data up to the time indicated in the receipt. - -## Architecture -The IDS Clearing House Service currently implements the [`Logging Service`](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md). Other services that comprise the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) may follow. The Clearing House Service consists of two parts: - -1. [`Clearing House App`](clearing-house-app) -2. [`Clearing House Processors`](clearing-house-processors) - -The `Clearing House App` is a REST API written in [Rust](https://www.rust-lang.org) that implements the business logic of the Clearing House. The `Clearing House Processors` is a library written in Java that integrates the `Clearing House App` into the [Trusted Connector](https://github.com/industrial-data-space/trusted-connector). The `Clearing House Processors` provide the `multipart` and `idscp2` endpoints described in the [IDS-G](https://github.com/International-Data-Spaces-Association/IDS-G/tree/main). These are used by the IDS connectors to interact with the Clearing House. Both `Clearing House App` and `Clearing House Processors` are needed to provide the `Clearing House Service`. - -## Requirements -- [OpenSSL](https://www.openssl.org) -- [MongoDB](https://www.mongodb.com) -- ([Docker](https://www.docker.com)) -- [Trusted Connector](https://github.com/industrial-data-space/trusted-connector) - -## Trusted Connector -The Clearing House Service API requires a Trusted Connector [Trusted Connector](https://github.com/industrial-data-space/trusted-connector) (Version 7.1.0+) for deployment. The process of setting up a Trusted Connector is described [here](https://industrial-data-space.github.io/trusted-connector-documentation/docs/getting_started/). Using a docker image of the Trusted Connector should be sufficient for most deployments: - -`docker pull fraunhoferaisec/trusted-connector-core:7.2.0` - -The Clearing House Processors are written in Java for use in the Camel Component of the Trusted Connector. To configure the Trusted Connector for the Clearing House Service API, it needs access to the following files inside the docker container (e.g. mounted as a volume): -- `clearing-house-processors.jar`: The Clearing House Processors need to be placed in the `/root/jars` folder of the Trusted Connector. The jar file needs to be [build](clearing-house-processors#building-from-source) from the Clearing House Processors using `gradle`. -- [`clearing-house-routes.xml`](clearing-house-processors/src/routes/clearing-house-routes.xml): The camel routes required by the Clearing House need to be placed in the `/root/deploy` folder of the Trusted Connector. -- [`application.yml`](docker/application.yml): This is a new configuration file of Trusted Connector 7.0.0+. The file version in this repository enables the use of some of the environment variables documented in the next section. - -Besides those files that are specific for the configuration of the Clearing House Service API, the Trusted Connector requires other files for its configuration, e.g. a truststore and a keystore with appropriate key material. Please refer to the [Documentation](https://industrial-data-space.github.io/trusted-connector-documentation/) of the Trusted Connector for more information. Also, please check the [Examples](https://github.com/industrial-data-space/trusted-connector/tree/master/examples) as they contain up-to-date configurations for the Trusted Connector. +

Welcome to ids-basecamp-clearinghouse 👋

+

+ Version + + Documentation + + + License: Apache--2.0 + +

+ +> The ids-basecamp-clearingHouse is a implementation of the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) component of the [International Data Space](https://internationaldataspaces.org/). + +## Install + +```sh +npm install +``` -#### Environment Variables -The Clearing House Processors can override some standard configuration settings of the Trusted Connector using environment variables. If these variables are not set, the Clearing House Processors will use the standard values provided by the Trusted Connector. Some of the variables are mandatory and have to be set: -- `TC_DAPS_URL`: The url of the DAPS used by the Clearing House. The Trusted Connector uses `https://daps.aisec.fraunhofer.de/v3` as the default DAPS url. -- `TC_KEYSTORE_PW`: The password of the key store mounted in the Trusted Connector. Defaults to `password`. -- `TC_TRUSTSTORE_PW`: The password of the trust store mounted in the Trusted Connector. Defaults to `password`. -- `TC_CH_ISSUER_CONNECTOR`(mandatory): Issuer connector needed for IDS Messages as specified by the [InfoModel](https://github.com/International-Data-Spaces-Association/InformationModel) -- `TC_CH_AGENT`(mandatory): Server agent needed for IDS Messages as specified by the [InfoModel](https://github.com/International-Data-Spaces-Association/InformationModel) -- `SERVICE_SHARED_SECRET`(mandatory): Shared secret, see Configuration section -- `SERVICE_ID_TC` (mandatory): Internal ID of the `Trusted Connector` that is used by the `Logging Service` to identify the `Trusted Connector`. -- `SERVICE_ID_LOG` (mandatory): Internal ID of the `Logging Service`. +## Usage +```sh +npm start +``` +## Run tests -#### Example Configuration (docker-compose) -``` -tc-core: - container_name: "tc-core" - image: fraunhoferaisec/trusted-connector-core:7.1.0 - tty: true - stdin_open: true - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./data/trusted-connector/application.yml:/root/etc/application.yml - - ./data/trusted-connector/allow-all-flows.pl:/root/deploy/allow-all-flows.pl - - ./data/trusted-connector/ch-ids.p12:/root/etc/keystore.p12 - - ./data/trusted-connector/truststore.p12:/root/etc/truststore.p12 - - ./data/trusted-connector/clearing-house-processors-0.10.0.jar:/root/jars/clearing-house-processors.jar - - ./data/trusted-connector/routes/clearing-house-routes.xml:/root/deploy/clearing-house-routes.xml - environment: - TC_DAPS_URL: https:// - SERVICE_SHARED_SECRET: - SERVICE_ID_TC: - SERVICE_ID_LOG: - - ports: - - "8443:8443" - - "9999:9999" - - "29292:29292" +```sh +npm test ``` -## Docker Containers -The dockerfiles located [here](docker/) can be used to create containers for the services of the [`Clearing House App`](clearing-house-app). There are two types of dockerfiles: -1. Simple builds (e.g. [dockerfile](docker/keyring-api.Dockerfile)) that require you to build the Service APIs yourself using [Rust](https://www.rust-lang.org) -2. Multistage builds (e.g. [dockerfile](docker/keyring-api-multistage.Dockerfile)) that have a stage for building the rust code - -To build the containers check out the repository and in the main directory execute +## Authors -`docker build -f docker/ . -t ` +👤 **Maximilian Schönenberg** +👤 **Daniel Hommen** -### Container Dependencies -![Container Dependencies](doc/images/ch_container_dependencies.png) -### Configuration -Please read the configuration section of the service ([`Logging Service`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#logging-service), [`Document API`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#document-api), [`Keyring API`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#keyring-api)) you are trying to run, before using `docker run` oder `docker-compose`. All Containers build with the provided dockerfiles require at least one volume: -1. The configuration file `Rocket.toml` is expected at `/server/Rocket.toml` +## 🤝 Contributing -Containers of the Keyring API require an additional volume: +Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/truzzt/ids-basecamp-clearinghouse/issues). -2. `/server/init_db` needs to contain the `default_doc_type.json` +## Show your support -Containers of the Logging Service require an additional volume: +Give a ⭐️ if this project helped you! -3. The folder containing the signing key needs to match the path configured for the signing key in `Rocket.toml`, e.g. `/sever/keys` +## 📝 License -## Shared Secret -The Clearing House services use signed JWTs with HMAC and a shared secret to ensure a minimal integrity of the requests received. The `Trusted Connector` as well as the services ([`Logging Service`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#logging-service), [`Document API`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#document-api), [`Keyring API`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#keyring-api)) need to have access to the shared secret. +This project is [Apache--2.0](https://github.com/truzzt/ids-basecamp-clearinghouse/blob/development/LICENSE) licensed. -For production use please consider using additional protection measures. +*** +_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ \ No newline at end of file diff --git a/docker/application.yml b/docker/application.yml deleted file mode 100644 index 786732c..0000000 --- a/docker/application.yml +++ /dev/null @@ -1,14 +0,0 @@ -logging: - level: - root: info - -ids-multipart: - daps-bean-name: rootDaps - -connector: - # Used as default for IDSCP2 DAPS instances which have not been explicitly configured. - daps-url: ${TC_DAPS_URL:} - # Used for IDS Messages issuerConnector field. - connector-url: ${TC_CH_ISSUER_CONNECTOR:} - # Used for IDS Messages senderAgent field. - sender-agent: ${TC_CH_AGENT:} diff --git a/docker/daps_cachain.crt b/docker/daps_cachain.crt deleted file mode 100644 index 1611cee..0000000 --- a/docker/daps_cachain.crt +++ /dev/null @@ -1,112 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDZzCCAk+gAwIBAgIJALIB7y7FZtiHMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNV -BAYTAkRFMRMwEQYDVQQKDApGcmF1bmhvZmVyMR4wHAYDVQQDDBVJRFMgUm9vdCBU -ZXN0IENBIDIwMTgwHhcNMTgxMDMxMTYxMjAyWhcNMzgxMDI2MTYxMjAyWjBCMQsw -CQYDVQQGEwJERTETMBEGA1UECgwKRnJhdW5ob2ZlcjEeMBwGA1UEAwwVSURTIFJv -b3QgVGVzdCBDQSAyMDE4MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -sh2FI432kI1BYWCs1CBPgJakTBsQt/BP7DQJ2lULlBKhtJpJM85tD1dXgIhLoBFi -5cMQPCX8kEy7TQLEOB4pPvqvUk+Tc48bNA39F4p3nOLZzlYREaTHui9r+FDvHy7j -TJGuRuDNKbP8qOAAd8Ci63e9iabSTL1wXLuZFnkEEHKraCwUQhzck5Dk7ll5Vncw -StishI+zk47uOiM3v0yMO4TSRyxFvwKfHB09hKLbLg6fr+62IqLEQWU/r0qo9h8e -4N16HdXLojyTam36JPDfExB2yHwMAe5fqtzwknWy0VfkrI2+KjN004EmjhsD9tL0 -dKS+MSRARDQYuHenSpG6XQIDAQABo2AwXjAdBgNVHQ4EFgQUl+fsV0kEi4xmZ+NX -uXYPdqDwY5AwHwYDVR0jBBgwFoAUl+fsV0kEi4xmZ+NXuXYPdqDwY5AwDwYDVR0T -AQH/BAUwAwEB/zALBgNVHQ8EBAMCAUYwDQYJKoZIhvcNAQEFBQADggEBAIgDI5Zq -xvrI0ihxtgdnr/p7Nx71imoEIquGDWR+W9smzUGcY5zkAX9V2zm/vydNhRrA3iNn -/+bmsM/xUYk4J6Oju8czYE1EQ/43OUB3iODzdzdAUspaa4MQORi0qaHehLrOzf28 -RVDPzLhhy64Z7muvhiKlWlwlLMqJSKIrJpeCSYTQZJFLLvu2atktoFJok477z1cX -ZpV2P1ODUyVETlA2WpOkb1g+BeaboDcODV3lwHA0kToDnjCeVo1rzs9ghjIpxvmg -gWMVSvLUgHTqUq1ruyqg9dJ7kN4rxpmTYBhVzYueVCfaRNQ+kKkUOyNEIAH9/c2g -lmTm5QRelsvLnbk= ------END CERTIFICATE----- -Certificate: - Data: - Version: 3 (0x2) - Serial Number: 1 (0x1) - Signature Algorithm: sha256WithRSAEncryption - Issuer: C=DE, O=Fraunhofer, CN=IDS Root Test CA 2018 - Validity - Not Before: Oct 31 16:12:04 2018 GMT - Not After : Oct 29 16:12:04 2026 GMT - Subject: C=DE, O=Fraunhofer, CN=IDS Test SubCA 2018 - Subject Public Key Info: - Public Key Algorithm: rsaEncryption - Public-Key: (2048 bit) - Modulus: - 00:ab:24:8c:56:4f:da:26:8a:74:d1:c1:da:e6:fe: - c6:2e:1d:cc:98:c9:43:c7:a0:7a:59:cf:59:e7:7c: - 4e:c0:5e:2a:6b:dd:b4:e6:b0:ae:20:c0:20:d6:aa: - 37:25:5f:22:98:62:ba:bc:d4:76:fc:c8:72:fc:a7: - 64:48:36:b7:35:33:58:69:46:19:b7:51:70:34:3c: - b0:82:58:75:33:29:56:6f:c9:e7:f5:1e:22:fc:14: - db:cf:78:9c:a9:22:9d:a8:5b:f7:52:56:82:4d:51: - 89:9e:26:ef:14:8f:30:5b:f8:58:87:5b:83:e1:80: - b2:e2:0f:6c:7a:76:a1:b8:92:2e:e6:53:50:c3:c1: - 20:41:66:0d:0d:f9:c2:f8:d3:34:76:ef:d8:5e:45: - 15:39:88:20:c9:fb:34:88:61:c4:66:a0:c3:10:58: - c0:ad:86:6b:1f:8f:4b:ca:0a:47:6f:44:84:c3:59: - 92:aa:66:15:21:14:79:25:01:a7:3d:16:9e:59:01: - c3:69:eb:24:47:7a:84:ed:81:8d:c4:f9:4b:4b:75: - 8b:09:0a:54:11:77:81:db:12:be:50:bd:6b:86:95: - fa:66:6f:b4:3f:45:4e:0b:af:1a:0f:51:57:1a:2b: - ef:bc:ec:5c:d6:a2:37:f1:95:9f:ca:4a:73:30:b6: - ed:eb - Exponent: 65537 (0x10001) - X509v3 extensions: - X509v3 Subject Key Identifier: - CB:8C:C7:B6:85:79:A8:23:A6:CB:15:AB:17:50:2F:E6:65:43:5D:E8 - X509v3 Authority Key Identifier: - keyid:97:E7:EC:57:49:04:8B:8C:66:67:E3:57:B9:76:0F:76:A0:F0:63:90 - - X509v3 Basic Constraints: critical - CA:TRUE - X509v3 Key Usage: - Digital Signature, Certificate Sign, CRL Sign - X509v3 CRL Distribution Points: - - Full Name: - URI:http://crl.aisec.fraunhofer.de/ids.crl - - Authority Information Access: - CA Issuers - URI:http://downloads.aisec.fraunhofer.de/rootcacert2016.cert - - Signature Algorithm: sha256WithRSAEncryption - 11:95:1a:ed:ec:b4:2e:3d:fd:7b:fd:a0:fd:7c:68:08:42:6b: - 5f:ad:84:cd:1a:53:b7:90:22:09:fe:d5:26:68:d6:0a:6e:b4: - 05:f1:85:8b:d5:6b:26:52:d3:75:00:6b:06:fa:d0:43:14:7d: - 19:53:e9:09:0a:97:14:c6:fe:14:12:c8:7c:d0:c7:50:8d:67: - db:63:de:aa:49:ce:6b:6d:07:8a:ae:9e:39:c5:91:ef:af:85: - 06:fb:51:70:d2:c4:f6:b4:07:26:b1:da:e3:ac:b5:38:9e:61: - cf:bf:4e:47:9b:1d:51:6a:4c:6f:b9:a0:b8:a6:a8:b0:da:e4: - 60:e3:29:85:5c:ad:2d:65:29:60:7b:e5:16:a0:1f:7c:c6:62: - ed:fe:48:04:81:3f:9f:3b:e3:9b:d8:8b:78:1b:5a:8b:f1:46: - 5e:39:f3:bf:e2:8e:68:62:cd:ec:fa:17:98:80:5b:8a:5d:89: - 61:2e:f2:bb:68:7b:9c:ec:7e:e5:c1:6f:b1:03:0c:81:fe:45: - 6f:32:01:31:0a:dc:25:83:90:11:96:a3:ba:e4:8a:d9:58:20: - f4:85:21:e9:7b:00:d2:11:df:bc:e6:8c:bb:5e:f8:31:18:60: - 40:9d:66:9f:af:6a:99:8b:42:8a:9f:f3:6b:c4:f4:be:a5:01: - 24:b8:f4:26 ------BEGIN CERTIFICATE----- -MIID7jCCAtagAwIBAgIBATANBgkqhkiG9w0BAQsFADBCMQswCQYDVQQGEwJERTET -MBEGA1UECgwKRnJhdW5ob2ZlcjEeMBwGA1UEAwwVSURTIFJvb3QgVGVzdCBDQSAy -MDE4MB4XDTE4MTAzMTE2MTIwNFoXDTI2MTAyOTE2MTIwNFowQDELMAkGA1UEBhMC -REUxEzARBgNVBAoMCkZyYXVuaG9mZXIxHDAaBgNVBAMME0lEUyBUZXN0IFN1YkNB -IDIwMTgwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrJIxWT9ominTR -wdrm/sYuHcyYyUPHoHpZz1nnfE7AXipr3bTmsK4gwCDWqjclXyKYYrq81Hb8yHL8 -p2RINrc1M1hpRhm3UXA0PLCCWHUzKVZvyef1HiL8FNvPeJypIp2oW/dSVoJNUYme -Ju8UjzBb+FiHW4PhgLLiD2x6dqG4ki7mU1DDwSBBZg0N+cL40zR279heRRU5iCDJ -+zSIYcRmoMMQWMCthmsfj0vKCkdvRITDWZKqZhUhFHklAac9Fp5ZAcNp6yRHeoTt -gY3E+UtLdYsJClQRd4HbEr5QvWuGlfpmb7Q/RU4LrxoPUVcaK++87FzWojfxlZ/K -SnMwtu3rAgMBAAGjgfAwge0wHQYDVR0OBBYEFMuMx7aFeagjpssVqxdQL+ZlQ13o -MB8GA1UdIwQYMBaAFJfn7FdJBIuMZmfjV7l2D3ag8GOQMA8GA1UdEwEB/wQFMAMB -Af8wCwYDVR0PBAQDAgGGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly9jcmwuYWlz -ZWMuZnJhdW5ob2Zlci5kZS9pZHMuY3JsMFQGCCsGAQUFBwEBBEgwRjBEBggrBgEF -BQcwAoY4aHR0cDovL2Rvd25sb2Fkcy5haXNlYy5mcmF1bmhvZmVyLmRlL3Jvb3Rj -YWNlcnQyMDE2LmNlcnQwDQYJKoZIhvcNAQELBQADggEBABGVGu3stC49/Xv9oP18 -aAhCa1+thM0aU7eQIgn+1SZo1gputAXxhYvVayZS03UAawb60EMUfRlT6QkKlxTG -/hQSyHzQx1CNZ9tj3qpJzmttB4qunjnFke+vhQb7UXDSxPa0Byax2uOstTieYc+/ -TkebHVFqTG+5oLimqLDa5GDjKYVcrS1lKWB75RagH3zGYu3+SASBP58745vYi3gb -WovxRl4587/ijmhizez6F5iAW4pdiWEu8rtoe5zsfuXBb7EDDIH+RW8yATEK3CWD -kBGWo7rkitlYIPSFIel7ANIR37zmjLte+DEYYECdZp+vapmLQoqf82vE9L6lASS4 -9CY= ------END CERTIFICATE----- diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..b944d58 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,12 @@ +version: "3.8" + +services: + app: + build: ../clearing-house-app/ + environment: + NODE_ENV: production + + edc: + build: ../clearing-house-edc/launchers/connector-prod/ + environment: + NODE_ENV: production \ No newline at end of file diff --git a/docker/document-api-multistage.Dockerfile b/docker/document-api-multistage.Dockerfile deleted file mode 100644 index 9de74c6..0000000 --- a/docker/document-api-multistage.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM rust as builder -WORKDIR app -COPY LICENSE clearing-house-app ./ -RUN cargo build --release - -FROM ubuntu:20.04 - -RUN apt-get update \ -&& echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \ -&& apt-get --no-install-recommends install -y -q ca-certificates gnupg2 libssl1.1 libc6 - -# trust the DAPS certificate -COPY docker/daps_cachain.crt /usr/local/share/ca-certificates/daps_cachain.crt -RUN update-ca-certificates - -RUN mkdir /server -WORKDIR /server - -COPY --from=builder /app/target/release/document-api . -COPY docker/entrypoint.sh . - -ENTRYPOINT ["/server/entrypoint.sh"] -CMD ["/server/document-api"] \ No newline at end of file diff --git a/docker/document-api.Dockerfile b/docker/document-api.Dockerfile deleted file mode 100644 index 73eabf3..0000000 --- a/docker/document-api.Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -######################################################################################### -# -# Builds minimal runtime environment for the document-api -# Copyright 2019 Fraunhofer AISEC -# -######################################################################################### -FROM ubuntu:20.04 - -RUN apt-get update \ -&& echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \ -&& apt-get --no-install-recommends install -y -q ca-certificates gnupg2 libssl1.1 libc6 - -# trust the DAPS certificate -COPY docker/daps_cachain.crt /usr/local/share/ca-certificates/daps_cachain.crt -RUN update-ca-certificates - -RUN mkdir /server -WORKDIR /server - -COPY clearing-house-app/target/release/document-api . -COPY docker/entrypoint.sh . - -ENTRYPOINT ["/server/entrypoint.sh"] -CMD ["/server/document-api"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh deleted file mode 100755 index a645310..0000000 --- a/docker/entrypoint.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -set -e - -exec "$@" diff --git a/docker/keyring-api-multistage.Dockerfile b/docker/keyring-api-multistage.Dockerfile deleted file mode 100644 index 572d7f2..0000000 --- a/docker/keyring-api-multistage.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM rust as builder -WORKDIR app -COPY LICENSE clearing-house-app ./ -RUN cargo build --release - -FROM ubuntu:20.04 - -RUN apt-get update \ -&& echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \ -&& apt-get --no-install-recommends install -y -q ca-certificates gnupg2 libssl1.1 libc6 - -# trust the DAPS certificate -COPY docker/daps_cachain.crt /usr/local/share/ca-certificates/daps_cachain.crt -RUN update-ca-certificates - -RUN mkdir /server -WORKDIR /server - -COPY --from=builder /app/target/release/keyring-api . -COPY docker/entrypoint.sh . - -ENTRYPOINT ["/server/entrypoint.sh"] -CMD ["/server/keyring-api"] \ No newline at end of file diff --git a/docker/keyring-api.Dockerfile b/docker/keyring-api.Dockerfile deleted file mode 100644 index ad0146a..0000000 --- a/docker/keyring-api.Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -######################################################################################### -# -# Builds minimal runtime environment for the keyring-api -# Copyright 2019 Fraunhofer AISEC -# -######################################################################################### -FROM ubuntu:20.04 - -RUN apt-get update \ -&& echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \ -&& apt-get --no-install-recommends install -y -q ca-certificates gnupg2 libssl1.1 libc6 - -# trust the DAPS certificate -COPY docker/daps_cachain.crt /usr/local/share/ca-certificates/daps_cachain.crt -RUN update-ca-certificates - -RUN mkdir /server -WORKDIR /server - -COPY clearing-house-app/target/release/keyring-api . -COPY docker/entrypoint.sh . - -ENTRYPOINT ["/server/entrypoint.sh"] -CMD ["/server/keyring-api"] diff --git a/docker/logging-service-multistage.Dockerfile b/docker/logging-service-multistage.Dockerfile deleted file mode 100644 index 82d7f82..0000000 --- a/docker/logging-service-multistage.Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -FROM rust as builder -WORKDIR app -COPY LICENSE clearing-house-app ./ -RUN cargo build --release - -FROM ubuntu:20.04 - -RUN apt-get update \ -&& echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \ -&& apt-get --no-install-recommends install -y -q ca-certificates gnupg2 libssl1.1 libc6 - -# trust the DAPS certificate -COPY docker/daps_cachain.crt /usr/local/share/ca-certificates/daps_cachain.crt -RUN update-ca-certificates - -RUN mkdir /server -WORKDIR /server - -COPY --from=builder /app/target/release/logging-service . -COPY docker/entrypoint.sh . - -ENTRYPOINT ["/server/entrypoint.sh"] -CMD ["/server/logging-service"] \ No newline at end of file diff --git a/docker/logging-service.Dockerfile b/docker/logging-service.Dockerfile deleted file mode 100644 index 72040c0..0000000 --- a/docker/logging-service.Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -######################################################################################### -# -# Builds minimal runtime environment for the Trackchain API -# Copyright 2019 Fraunhofer AISEC -# -######################################################################################### -FROM ubuntu:20.04 - -RUN apt-get update \ -&& echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections \ -&& apt-get --no-install-recommends install -y -q ca-certificates gnupg2 libssl1.1 libc6 - -# trust the DAPS certificate -COPY docker/daps_cachain.crt /usr/local/share/ca-certificates/daps_cachain.crt -RUN update-ca-certificates - -RUN mkdir /server -WORKDIR /server - -COPY clearing-house-app/target/release/logging-service . -COPY docker/entrypoint.sh . - -ENTRYPOINT ["/server/entrypoint.sh"] -CMD ["/server/logging-service"] diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b8b4fcf..0e2176e 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -13,5 +13,6 @@ - [Communication](content/internals/communication.md) - [Functionality](content/internals/functionality.md) - [Proposal](Proposal.md) +- [oldReadme](old_README.md) diff --git a/docs/old_README.md b/docs/old_README.md new file mode 100644 index 0000000..6fa2b14 --- /dev/null +++ b/docs/old_README.md @@ -0,0 +1,101 @@ +# IDS Clearing House +The IDS Clearing House Service is a prototype implementation of the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) component of the [Industrial Data Space](https://internationaldataspaces.org/). + +Data in the Clearing House is stored encrypted and practically immutable. There are multiple ways in which the Clearing House enforces Data Immutability: +- Using the `Logging Service` there is no way to update an already existing log entry in the database +- Log entries in the database include a hash value of the previous log entry, chaining together all log entries. Any change to a previous log entry would require rehashing all following log entries. +- The connector logging information in the Clearing House receives a signed receipt from the Clearing House that includes among other things a timestamp and the current chain hash. A single valid receipt in possession of any connector is enough to detect any change to data up to the time indicated in the receipt. + +## Architecture +The IDS Clearing House Service currently implements the [`Logging Service`](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md). Other services that comprise the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) may follow. The Clearing House Service consists of two parts: + +1. [`Clearing House App`](clearing-house-app) +2. [`Clearing House Processors`](clearing-house-processors) + +The `Clearing House App` is a REST API written in [Rust](https://www.rust-lang.org) that implements the business logic of the Clearing House. The `Clearing House Processors` is a library written in Java that integrates the `Clearing House App` into the [Trusted Connector](https://github.com/industrial-data-space/trusted-connector). The `Clearing House Processors` provide the `multipart` and `idscp2` endpoints described in the [IDS-G](https://github.com/International-Data-Spaces-Association/IDS-G/tree/main). These are used by the IDS connectors to interact with the Clearing House. Both `Clearing House App` and `Clearing House Processors` are needed to provide the `Clearing House Service`. + +## Requirements +- [OpenSSL](https://www.openssl.org) +- [MongoDB](https://www.mongodb.com) +- ([Docker](https://www.docker.com)) +- [Trusted Connector](https://github.com/industrial-data-space/trusted-connector) + +## Trusted Connector +The Clearing House Service API requires a Trusted Connector [Trusted Connector](https://github.com/industrial-data-space/trusted-connector) (Version 7.1.0+) for deployment. The process of setting up a Trusted Connector is described [here](https://industrial-data-space.github.io/trusted-connector-documentation/docs/getting_started/). Using a docker image of the Trusted Connector should be sufficient for most deployments: + +`docker pull fraunhoferaisec/trusted-connector-core:7.2.0` + +The Clearing House Processors are written in Java for use in the Camel Component of the Trusted Connector. To configure the Trusted Connector for the Clearing House Service API, it needs access to the following files inside the docker container (e.g. mounted as a volume): +- `clearing-house-processors.jar`: The Clearing House Processors need to be placed in the `/root/jars` folder of the Trusted Connector. The jar file needs to be [build](clearing-house-processors#building-from-source) from the Clearing House Processors using `gradle`. +- [`clearing-house-routes.xml`](clearing-house-processors/src/routes/clearing-house-routes.xml): The camel routes required by the Clearing House need to be placed in the `/root/deploy` folder of the Trusted Connector. +- [`application.yml`](docker/application.yml): This is a new configuration file of Trusted Connector 7.0.0+. The file version in this repository enables the use of some of the environment variables documented in the next section. + +Besides those files that are specific for the configuration of the Clearing House Service API, the Trusted Connector requires other files for its configuration, e.g. a truststore and a keystore with appropriate key material. Please refer to the [Documentation](https://industrial-data-space.github.io/trusted-connector-documentation/) of the Trusted Connector for more information. Also, please check the [Examples](https://github.com/industrial-data-space/trusted-connector/tree/master/examples) as they contain up-to-date configurations for the Trusted Connector. + +#### Environment Variables +The Clearing House Processors can override some standard configuration settings of the Trusted Connector using environment variables. If these variables are not set, the Clearing House Processors will use the standard values provided by the Trusted Connector. Some of the variables are mandatory and have to be set: +- `TC_DAPS_URL`: The url of the DAPS used by the Clearing House. The Trusted Connector uses `https://daps.aisec.fraunhofer.de/v3` as the default DAPS url. +- `TC_KEYSTORE_PW`: The password of the key store mounted in the Trusted Connector. Defaults to `password`. +- `TC_TRUSTSTORE_PW`: The password of the trust store mounted in the Trusted Connector. Defaults to `password`. +- `TC_CH_ISSUER_CONNECTOR`(mandatory): Issuer connector needed for IDS Messages as specified by the [InfoModel](https://github.com/International-Data-Spaces-Association/InformationModel) +- `TC_CH_AGENT`(mandatory): Server agent needed for IDS Messages as specified by the [InfoModel](https://github.com/International-Data-Spaces-Association/InformationModel) +- `SERVICE_SHARED_SECRET`(mandatory): Shared secret, see Configuration section +- `SERVICE_ID_TC` (mandatory): Internal ID of the `Trusted Connector` that is used by the `Logging Service` to identify the `Trusted Connector`. +- `SERVICE_ID_LOG` (mandatory): Internal ID of the `Logging Service`. + + +#### Example Configuration (docker-compose) +``` +tc-core: + container_name: "tc-core" + image: fraunhoferaisec/trusted-connector-core:7.1.0 + tty: true + stdin_open: true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./data/trusted-connector/application.yml:/root/etc/application.yml + - ./data/trusted-connector/allow-all-flows.pl:/root/deploy/allow-all-flows.pl + - ./data/trusted-connector/ch-ids.p12:/root/etc/keystore.p12 + - ./data/trusted-connector/truststore.p12:/root/etc/truststore.p12 + - ./data/trusted-connector/clearing-house-processors-0.10.0.jar:/root/jars/clearing-house-processors.jar + - ./data/trusted-connector/routes/clearing-house-routes.xml:/root/deploy/clearing-house-routes.xml + environment: + TC_DAPS_URL: https:// + SERVICE_SHARED_SECRET: + SERVICE_ID_TC: + SERVICE_ID_LOG: + + ports: + - "8443:8443" + - "9999:9999" + - "29292:29292" +``` + +## Docker Containers +The dockerfiles located [here](docker/) can be used to create containers for the services of the [`Clearing House App`](clearing-house-app). There are two types of dockerfiles: +1. Simple builds (e.g. [dockerfile](docker/keyring-api.Dockerfile)) that require you to build the Service APIs yourself using [Rust](https://www.rust-lang.org) +2. Multistage builds (e.g. [dockerfile](docker/keyring-api-multistage.Dockerfile)) that have a stage for building the rust code + +To build the containers check out the repository and in the main directory execute + +`docker build -f docker/ . -t ` + +### Container Dependencies +![Container Dependencies](doc/images/ch_container_dependencies.png) + +### Configuration +Please read the configuration section of the service ([`Logging Service`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#logging-service), [`Document API`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#document-api), [`Keyring API`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#keyring-api)) you are trying to run, before using `docker run` oder `docker-compose`. All Containers build with the provided dockerfiles require at least one volume: +1. The configuration file `Rocket.toml` is expected at `/server/Rocket.toml` + +Containers of the Keyring API require an additional volume: + +2. `/server/init_db` needs to contain the `default_doc_type.json` + +Containers of the Logging Service require an additional volume: + +3. The folder containing the signing key needs to match the path configured for the signing key in `Rocket.toml`, e.g. `/sever/keys` + +## Shared Secret +The Clearing House services use signed JWTs with HMAC and a shared secret to ensure a minimal integrity of the requests received. The `Trusted Connector` as well as the services ([`Logging Service`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#logging-service), [`Document API`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#document-api), [`Keyring API`](https://github.com/Fraunhofer-AISEC/ids-clearing-house-service/tree/architecture-revamp/clearing-house-app#keyring-api)) need to have access to the shared secret. + +For production use please consider using additional protection measures. diff --git a/package.json b/package.json index 7bfc48c..2b719db 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,15 @@ "doc": "doc" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "cd clearing-house-app && cargo test", + "start": "docker compose -f docker/docker-compose.yml up -d" + }, + "bugs": { + "url": "https://github.com/truzzt/ids-basecamp-clearinghouse/issues" }, "keywords": [], "repository": "https://github.com/truzzt/ids-basecamp-clearinghouse", - "author": "", + "author": "Maximilian Schönenberg, Daniel Hommen", "license": "Apache-2.0", "devDependencies": { "@semantic-release/changelog": "^6.0.3", From ed94ba5d6888f30ec9157aee78623645d3b00ff1 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Fri, 10 Nov 2023 12:57:37 -0300 Subject: [PATCH 143/183] feat (ch-edc): Excluding the coverage from dto classes --- clearing-house-edc/core/build.gradle.kts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 8fe966c..0982d79 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -54,4 +54,11 @@ tasks.jacocoTestReport { xml.required = true } dependsOn(tasks.test) + classDirectories.setFrom( + files(classDirectories.files.map { + fileTree(it) { + exclude("**/dto/**") + } + }) + ) } From a4b77c03220ca2706ac6b579e6299746014081c9 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Fri, 10 Nov 2023 16:18:00 -0300 Subject: [PATCH 144/183] feat (ch-edc): Excluding the coverage from some types directory --- clearing-house-edc/core/build.gradle.kts | 5 ++++- .../de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java | 2 +- .../clearinghouse/edc/types/{ids => }/util/VocabUtil.java | 2 +- .../clearinghouse/edc/types/ids/util/VocabUtilTest.java | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) rename clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/{ids => }/util/VocabUtil.java (95%) diff --git a/clearing-house-edc/core/build.gradle.kts b/clearing-house-edc/core/build.gradle.kts index 0982d79..957d08f 100644 --- a/clearing-house-edc/core/build.gradle.kts +++ b/clearing-house-edc/core/build.gradle.kts @@ -57,7 +57,10 @@ tasks.jacocoTestReport { classDirectories.setFrom( files(classDirectories.files.map { fileTree(it) { - exclude("**/dto/**") + exclude( + "**/dto/**", + "**/types/clearinghouse/*", + "**/types/ids/*") } }) ) diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java index b8f1b46..c659df6 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/SecurityToken.java @@ -15,7 +15,7 @@ package de.truzzt.clearinghouse.edc.types.ids; import com.fasterxml.jackson.annotation.JsonProperty; -import de.truzzt.clearinghouse.edc.types.ids.util.VocabUtil; +import de.truzzt.clearinghouse.edc.types.util.VocabUtil; import org.jetbrains.annotations.NotNull; import java.net.URI; diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/util/VocabUtil.java similarity index 95% rename from clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java rename to clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/util/VocabUtil.java index 15a7680..8d18760 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtil.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/util/VocabUtil.java @@ -12,7 +12,7 @@ * truzzt GmbH - EDC extension implementation * */ -package de.truzzt.clearinghouse.edc.types.ids.util; +package de.truzzt.clearinghouse.edc.types.util; import java.net.MalformedURLException; import java.net.URI; diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtilTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtilTest.java index 5d8da42..6e6063a 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtilTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/types/ids/util/VocabUtilTest.java @@ -1,5 +1,6 @@ package de.truzzt.clearinghouse.edc.types.ids.util; +import de.truzzt.clearinghouse.edc.types.util.VocabUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; From dc18f0f977e3b6abb505afc1e6b28256aaf2a7fd Mon Sep 17 00:00:00 2001 From: Glaucio Jannotti Date: Tue, 21 Nov 2023 16:34:44 -0300 Subject: [PATCH 145/183] fix (ch-edc): dsp token format --- .../de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java | 7 ++++++- .../clearinghouse/edc/multipart/MultipartController.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java index 56d86cd..1bb0602 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/types/ids/TokenFormat.java @@ -21,7 +21,8 @@ public class TokenFormat { - public static final String JWT_TOKEN_FORMAT = "idsc:JWT"; + public static final String JWT_TOKEN_FORMAT_IDS = "idsc:JWT"; + public static final String JWT_TOKEN_FORMAT_DSP = "https://w3id.org/idsa/code/JWT"; @JsonProperty("@id") @NotNull @@ -30,4 +31,8 @@ public class TokenFormat { public URI getId() { return id; } + + public static boolean isValid(String id) { + return id.equals(JWT_TOKEN_FORMAT_IDS) || id.equals(JWT_TOKEN_FORMAT_DSP); + } } diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index a566cd7..d1532c2 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -168,7 +168,7 @@ RequestValidationResponse validaRequest(String pid, InputStream headerInputStrea // Check the security token type var tokenFormat = securityToken.getTokenFormat().getId().toString(); - if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { + if (!TokenFormat.isValid(tokenFormat)) { monitor.severe(LOG_ID + ": Invalid security token type: " + tokenFormat); return new RequestValidationResponse(Response.status(Response.Status.BAD_REQUEST) .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) From d046826132c1e6cc3e60f2c31e2d4f8c397fe01b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 22 Nov 2023 09:15:01 +0000 Subject: [PATCH 146/183] feat(docker): Optimised docker image with distroless image --- clearing-house-app/Dockerfile | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/clearing-house-app/Dockerfile b/clearing-house-app/Dockerfile index 25444a3..8cf3d6a 100644 --- a/clearing-house-app/Dockerfile +++ b/clearing-house-app/Dockerfile @@ -1,5 +1,5 @@ # Use an official Rust runtime as a parent image -FROM rust:latest +FROM rust:latest AS build-env # Set the working directory inside the container WORKDIR /usr/src/chapp @@ -9,15 +9,17 @@ COPY Cargo.toml Cargo.lock config.toml ./ # Copy the source code into the container COPY src ./src -COPY init_db ./init_db -COPY keys ./keys -COPY certs ./certs # Build the Rust application with dependencies (this helps to cache dependencies) RUN cargo build --release +FROM gcr.io/distroless/cc-debian12 + # Expose any necessary ports (if your Rust app listens on a port) -# EXPOSE 8000 +EXPOSE 8000 + +WORKDIR /app +COPY init_db /app/init_db -# Run the Rust application when the container starts -CMD ["cargo", "run", "--release"] +COPY --from=build-env /usr/src/chapp/target/release/clearing-house-app /app/ +CMD ["/app/clearing-house-app"] From 293500d45f2bccbae47d4ae0dfdbf01851ea4f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 22 Nov 2023 10:09:17 +0000 Subject: [PATCH 147/183] feat(ch-app): Add docs for installation of ch-app --- clearing-house-app/Dockerfile | 1 + docs/SUMMARY.md | 1 + .../admin-guide/ch-app_installation.md | 57 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 docs/content/admin-guide/ch-app_installation.md diff --git a/clearing-house-app/Dockerfile b/clearing-house-app/Dockerfile index 8cf3d6a..7006a9d 100644 --- a/clearing-house-app/Dockerfile +++ b/clearing-house-app/Dockerfile @@ -20,6 +20,7 @@ EXPOSE 8000 WORKDIR /app COPY init_db /app/init_db +COPY config.toml /app/ COPY --from=build-env /usr/src/chapp/target/release/clearing-house-app /app/ CMD ["/app/clearing-house-app"] diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index b8b4fcf..a8dc421 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -2,6 +2,7 @@ # Admin Guide - [Installation](content/admin-guide/installation.md) + - [Clearinghouse-App](content/admin-guide/ch-app_installation.md) - [Tests](content/admin-guide/tests.md) - [Maintenance](content/admin-guide/maintenance.md) diff --git a/docs/content/admin-guide/ch-app_installation.md b/docs/content/admin-guide/ch-app_installation.md new file mode 100644 index 0000000..bf7683f --- /dev/null +++ b/docs/content/admin-guide/ch-app_installation.md @@ -0,0 +1,57 @@ +# Clearinghouse App Installation + +The Clearinghouse App (`ch-app`) comes pre-packaged in a docker container. + +## Releases + +For existing releases visit [ids-basecamp-clearinghouse/ch-app Releases](https://github.com/truzzt/ids-basecamp-clearinghouse/pkgs/container/ids-basecamp-clearing/ch-app). + +## Usage + +Starting the `ch-app` Docker-only, use the following command and adapt it to your needs. + +```sh +docker run -d \ + -p 8000:8000 \ + -v ${PRIVATE_KEY_PATH}:/app/keys/private_key.der:ro \ + -e CH_APP_PROCESS_DATABASE_URL='mongodb://mongohost:27017' \ + -e CH_APP_KEYRING_DATABASE_URL='mongodb://mongohost:27017' \ + -e CH_APP_DOCUMENT_DATABASE_URL='mongodb://mongohost:27017' \ + -e CH_APP_CLEAR_DB='false' \ + -e CH_APP_LOG_LEVEL='INFO' \ + -e SERVICE_ID_LOG='1' \ + -e SHARED_SECRET='123' \ + --name ch-app \ + ghcr.io/truzzt/ids-basecamp-clearing/ch-app:${TAG} +``` + +The following example starts the `ch-app` together with a `mongodb` also running on docker (good for local development): + +```sh +# Create a docker network +docker network create testch +# Start mongodb +docker run -d -p 27017:27017 --net=testch --name mongohost mongo +# Start ch-app +docker run -d \ + -p 8000:8000 --net=testch \ + -v ${PRIVATE_KEY_PATH}:/app/keys/private_key.der:ro \ + -e CH_APP_PROCESS_DATABASE_URL='mongodb://mongohost:27017' \ + -e CH_APP_KEYRING_DATABASE_URL='mongodb://mongohost:27017' \ + -e CH_APP_DOCUMENT_DATABASE_URL='mongodb://mongohost:27017' \ + -e CH_APP_CLEAR_DB='false' \ + -e CH_APP_LOG_LEVEL='INFO' \ + -e SERVICE_ID_LOG='1' \ + -e SHARED_SECRET='123' \ + --name ch-app \ + ghcr.io/truzzt/ids-basecamp-clearing/ch-app:${TAG} + +# --- +# Cleanup +docker rm -f mongohost ch-app +docker network rm testch +``` + +## Build + +To build the ch-app yourself change into the `/clearing-house-app` directory and run `docker build -t ch-app:latest .`. \ No newline at end of file From ffdfbadd10769b99f392617f0d691fcd45dcdafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 2 Nov 2023 17:07:19 +0100 Subject: [PATCH 148/183] feat(ch-app): Remove Blockchain, add integration tests --- clearing-house-app/Cargo.lock | 299 ++++++++---------- clearing-house-app/Cargo.toml | 11 +- clearing-house-app/src/db/doc_store.rs | 18 +- clearing-house-app/src/lib.rs | 85 +++++ clearing-house-app/src/main.rs | 89 +----- clearing-house-app/src/model/claims.rs | 49 ++- clearing-house-app/src/model/document.rs | 70 +--- clearing-house-app/src/model/ids/message.rs | 3 +- clearing-house-app/src/model/ids/mod.rs | 15 +- clearing-house-app/src/model/mod.rs | 2 +- clearing-house-app/src/model/process.rs | 4 - .../src/services/document_service.rs | 31 +- .../src/services/logging_service.rs | 81 ++--- clearing-house-app/src/services/mod.rs | 4 +- clearing-house-app/src/util.rs | 4 +- clearing-house-app/tests/README.md | 5 + clearing-house-app/tests/log.rs | 87 +++++ clearing-house-app/tests/public_key.rs | 21 ++ 18 files changed, 432 insertions(+), 446 deletions(-) create mode 100644 clearing-house-app/src/lib.rs create mode 100644 clearing-house-app/tests/README.md create mode 100644 clearing-house-app/tests/log.rs create mode 100644 clearing-house-app/tests/public_key.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index db42cd0..ea752e8 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -96,15 +96,6 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" -[[package]] -name = "arrayvec" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" -dependencies = [ - "nodrop", -] - [[package]] name = "async-trait" version = "0.1.74" @@ -113,7 +104,7 @@ checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -238,16 +229,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "blake2-rfc" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" -dependencies = [ - "arrayvec", - "constant_time_eq", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -345,13 +326,13 @@ dependencies = [ "axum", "base64 0.21.5", "biscuit", - "blake2-rfc", "chrono", "config", "futures", "generic-array", "hex", "hkdf", + "hyper", "mongodb", "num-bigint", "once_cell", @@ -366,6 +347,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "tower", "tracing", "tracing-subscriber", "uuid", @@ -373,9 +355,9 @@ dependencies = [ [[package]] name = "config" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" dependencies = [ "async-trait", "lazy_static", @@ -385,12 +367,6 @@ dependencies = [ "toml", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "convert_case" version = "0.4.0" @@ -415,9 +391,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fbc60abd742b35f2492f808e1abbb83d45f72db402e14c55057edc9c7b1e9e4" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -492,9 +468,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "debugid" @@ -579,9 +555,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" dependencies = [ "libc", "windows-sys", @@ -649,9 +625,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -664,9 +640,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -674,15 +650,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -691,38 +667,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -748,9 +724,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -765,9 +741,9 @@ checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "h2" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" dependencies = [ "bytes", "fnv", @@ -775,7 +751,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", @@ -843,9 +819,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -974,9 +950,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", "hashbrown 0.14.2", @@ -1017,9 +993,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -1032,9 +1008,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.149" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "linked-hash-map" @@ -1044,9 +1020,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" @@ -1150,9 +1126,9 @@ dependencies = [ [[package]] name = "mongodb" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22d517e7e678e1c9a2983ec704b43f3b22f38b1b7a247ea3ddb36d21578bf4e" +checksum = "e7c926772050c3a3f87c837626bf6135c8ca688d91d31dd39a3da547fc2bc9fe" dependencies = [ "async-trait", "base64 0.13.1", @@ -1216,12 +1192,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "nom" version = "7.1.3" @@ -1318,9 +1288,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.57" +version = "0.10.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" +checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -1339,7 +1309,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1350,9 +1320,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.93" +version = "0.9.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" +checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" dependencies = [ "cc", "libc", @@ -1395,7 +1365,7 @@ checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall", "smallvec", "windows-targets", ] @@ -1438,7 +1408,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -1543,15 +1513,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -1718,9 +1679,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.20" +version = "0.38.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ce50cb2e16c2903e30d1cbccfd8387a74b9d4c938b6a4c5ec6cc7556f7a8a0" +checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" dependencies = [ "bitflags 2.4.1", "errno", @@ -1731,9 +1692,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" dependencies = [ "log", "ring 0.17.5", @@ -1743,9 +1704,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ "base64 0.21.5", ] @@ -1843,9 +1804,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "sentry" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0097a48cd1999d983909f07cb03b15241c5af29e5e679379efac1c06296abecc" +checksum = "6ce4b57f1b521f674df7a1d200be8ff5d74e3712020ee25b553146657b5377d5" dependencies = [ "httpdate", "native-tls", @@ -1862,9 +1823,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a7b80fa1dd6830a348d38a8d3a9761179047757b7dca29aef82db0118b9670" +checksum = "58cc8d4e04a73de8f718dc703943666d03f25d3e9e4d0fb271ca0b8c76dfa00e" dependencies = [ "backtrace", "once_cell", @@ -1874,9 +1835,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7615dc588930f1fd2e721774f25844ae93add2dbe2d3c2f995ce5049af898147" +checksum = "6436c1bad22cdeb02179ea8ef116ffc217797c028927def303bc593d9320c0d1" dependencies = [ "hostname", "libc", @@ -1888,9 +1849,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f51264e4013ed9b16558cce43917b983fa38170de2ca480349ceb57d71d6053" +checksum = "901f761681f97db3db836ef9e094acdd8756c40215326c194201941947164ef1" dependencies = [ "once_cell", "rand", @@ -1901,9 +1862,9 @@ dependencies = [ [[package]] name = "sentry-debug-images" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fe6180fa564d40bb942c9f0084ffb5de691c7357ead6a2b7a3154fae9e401dd" +checksum = "afdb263e73d22f39946f6022ed455b7561b22ff5553aca9be3c6a047fa39c328" dependencies = [ "findshlibs", "once_cell", @@ -1912,9 +1873,9 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323160213bba549f9737317b152af116af35c0410f4468772ee9b606d3d6e0fa" +checksum = "74fbf1c163f8b6a9d05912e1b272afa27c652e8b47ea60cb9a57ad5e481eea99" dependencies = [ "sentry-backtrace", "sentry-core", @@ -1922,9 +1883,9 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38033822128e73f7b6ca74c1631cef8868890c6cb4008a291cf73530f87b4eac" +checksum = "82eabcab0a047040befd44599a1da73d3adb228ff53b5ed9795ae04535577704" dependencies = [ "sentry-backtrace", "sentry-core", @@ -1934,9 +1895,9 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e663b3eb62ddfc023c9cf5432daf5f1a4f6acb1df4d78dd80b740b32dd1a740" +checksum = "da956cca56e0101998c8688bc65ce1a96f00673a0e58e663664023d4c7911e82" dependencies = [ "debugid", "hex", @@ -1951,9 +1912,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.189" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" dependencies = [ "serde_derive", ] @@ -1969,22 +1930,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.189" +version = "1.0.193" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.107" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ - "indexmap 2.0.2", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -2056,7 +2017,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2110,9 +2071,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" @@ -2182,9 +2143,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.38" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -2232,13 +2193,13 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.8.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", "windows-sys", ] @@ -2260,7 +2221,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2319,9 +2280,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.33.0" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ "backtrace", "bytes", @@ -2337,13 +2298,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2450,7 +2411,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] @@ -2465,9 +2426,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", @@ -2476,9 +2437,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", @@ -2614,9 +2575,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3" +checksum = "7830e33f6e25723d41a63f77e434159dad02919f18f55a512b5f16f3b1d77138" dependencies = [ "base64 0.21.5", "log", @@ -2639,9 +2600,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.5.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" dependencies = [ "getrandom", "serde", @@ -2682,9 +2643,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2692,24 +2653,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" dependencies = [ "cfg-if", "js-sys", @@ -2719,9 +2680,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2729,28 +2690,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", @@ -2886,26 +2847,26 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.15" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81ba595b9f2772fbee2312de30eeb80ec773b4cb2f1e8098db024afadda6c06f" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.15" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772666c41fb6dceaf520b564b962d738a8e1a83b41bd48945f50837aed78bb1d" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn 2.0.39", ] [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index dc5b925..6319cd4 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -12,9 +12,9 @@ edition = "2021" # JWT biscuit = "0.6.0" # Database -mongodb = { version = ">= 2.7.0" , features = ["openssl-tls"]} +mongodb = { version = ">= 2.7.0", features = ["openssl-tls"] } # Serialization -serde = { version = ">1.0.184", features = ["derive"] } +serde = { version = "> 1.0.184", features = ["derive"] } serde_json = "1" # Error handling anyhow = "1" @@ -26,7 +26,6 @@ aes = "0.8.3" aes-gcm-siv = "0.11.1" hkdf = "0.12.3" sha2 = "0.10.7" -blake2-rfc = "0.2.18" ring = "0.16.20" # Fixed size arrays generic-array = "0.14.7" @@ -54,7 +53,7 @@ axum = { version = "0.6.20", features = ["json", "http2"] } # Helper to allow defining traits for async functions async-trait = "0.1.73" # Helper for working with futures -futures = "0.3.28" +futures = "0.3.29" thiserror = "1.0.48" sentry = { version = "0.31.7", optional = true } @@ -62,7 +61,9 @@ sentry = { version = "0.31.7", optional = true } # Controlling execution of unit test cases, which could interfere with each other serial_test = "2.0.0" # Tempfile creation for testing -tempfile = "3.8.0" +tempfile = "3.8" +tower = { version = "0.4", features = ["util"] } +hyper = { version = "0.14.27", features = ["full"] } [features] default = [] diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index 449872b..a7c1b2b 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -11,7 +11,7 @@ use anyhow::anyhow; use futures::StreamExt; use mongodb::bson::doc; use mongodb::options::{ - AggregateOptions, CreateCollectionOptions, IndexOptions, UpdateOptions, WriteConcern, + AggregateOptions, CreateCollectionOptions, UpdateOptions, WriteConcern, }; use mongodb::{bson, Client, IndexModel}; @@ -86,7 +86,7 @@ impl DataStore { }; // This purpose of this index is to ensure that the transaction counter is unique - let mut index_options = IndexOptions::default(); + /*let mut index_options = IndexOptions::default(); index_options.unique = Some(true); let mut index_model = IndexModel::default(); index_model.keys = doc! {format!("{}.{}",MONGO_DOC_ARRAY, MONGO_TC): 1}; @@ -107,7 +107,7 @@ impl DataStore { debug!("... failed."); return Err(anyhow!("Failed to create index")); } - } + }*/ // This creates a compound index over pid and the timestamp to enable paging using buckets let mut compound_index_model = IndexModel::default(); @@ -163,7 +163,7 @@ impl DataStore { MONGO_DOC_ARRAY: mongodb::bson::to_bson(&bucket_update)?, }, "$inc": {"counter": 1}, - "$setOnInsert": { "_id": format!("{}_{}", doc.pid.clone(), doc.ts), MONGO_DT_ID: doc.dt_id.clone(), MONGO_FROM_TS: doc.ts}, + "$setOnInsert": { "_id": format!("{}_{}_{}", doc.pid.clone(), doc.ts, crate::util::new_uuid()), MONGO_DT_ID: doc.dt_id.clone(), MONGO_FROM_TS: doc.ts}, "$set": {MONGO_TO_TS: doc.ts}, }, update_options).await { Ok(_r) => { @@ -326,14 +326,14 @@ impl DataStore { doc! {"$skip": skip_buckets}, // worst case: overlap between two buckets. doc! {"$limit": 2}, - doc! {"$unwind": format ! ("${}", MONGO_DOC_ARRAY)}, + doc! {"$unwind": format! ("${}", MONGO_DOC_ARRAY)}, doc! {"$replaceRoot": { "newRoot": "$documents"}}, doc! {"$match":{ MONGO_TS: {"$gte": date_from.timestamp(), "$lte": date_to.timestamp()} }}, doc! {"$sort": {MONGO_TS: sort_order}}, doc! {"$skip": start_entry as i32}, - doc! { "$limit": size as i32}, + doc! {"$limit": size as i32}, ]; let coll = self @@ -483,8 +483,6 @@ mod bucket { pub struct DocumentBucketUpdate { pub id: String, pub ts: i64, - pub tc: i64, - pub hash: String, pub keys_ct: String, pub cts: Vec, } @@ -494,8 +492,6 @@ mod bucket { DocumentBucketUpdate { id: doc.id.clone(), ts: doc.ts, - tc: doc.tc, - hash: doc.hash.clone(), keys_ct: doc.keys_ct.clone(), cts: doc.cts.to_vec(), } @@ -512,8 +508,6 @@ mod bucket { dt_id, pid, ts: bucket_update.ts, - tc: bucket_update.tc, - hash: bucket_update.hash.clone(), keys_ct: bucket_update.keys_ct.clone(), cts: bucket_update.cts.to_vec(), } diff --git a/clearing-house-app/src/lib.rs b/clearing-house-app/src/lib.rs new file mode 100644 index 0000000..7f8cfc7 --- /dev/null +++ b/clearing-house-app/src/lib.rs @@ -0,0 +1,85 @@ +#[macro_use] +extern crate tracing; + +use crate::db::doc_store::DataStore; +use crate::db::key_store::KeyStore; +use crate::db::process_store::ProcessStore; +use crate::model::constants::ENV_LOGGING_SERVICE_ID; +use crate::util::ServiceConfig; +use std::sync::Arc; + +mod config; +mod crypto; +mod db; +pub mod model; +mod ports; +mod services; +pub mod util; + +/// Contains the application state +#[derive(Clone)] +pub struct AppState { + #[cfg_attr(not(doc_type), allow(dead_code))] + pub keyring_service: Arc, + pub logging_service: Arc, + pub service_config: Arc, + pub signing_key_path: String, +} + +impl AppState { + /// Initialize the application state from config + async fn init(conf: &config::CHConfig) -> anyhow::Result { + trace!("Initializing Process store"); + let process_store = + ProcessStore::init_process_store(&conf.process_database_url, conf.clear_db) + .await + .expect("Failure to initialize process store! Exiting..."); + trace!("Initializing Keyring store"); + let keyring_store = KeyStore::init_keystore(&conf.keyring_database_url, conf.clear_db) + .await + .expect("Failure to initialize keyring store! Exiting..."); + trace!("Initializing Document store"); + let doc_store = DataStore::init_datastore(&conf.document_database_url, conf.clear_db) + .await + .expect("Failure to initialize document store! Exiting..."); + + trace!("Initializing services"); + let keyring_service = Arc::new(services::keyring_service::KeyringService::new( + keyring_store, + )); + let doc_service = Arc::new(services::document_service::DocumentService::new( + doc_store, + keyring_service.clone(), + )); + let logging_service = Arc::new(services::logging_service::LoggingService::new( + process_store, + doc_service.clone(), + )); + + let service_config = Arc::new(util::init_service_config( + ENV_LOGGING_SERVICE_ID.to_string(), + )?); + let signing_key = util::init_signing_key(conf.signing_key.as_deref())?; + + Ok(Self { + signing_key_path: signing_key, + service_config, + keyring_service, + logging_service, + }) + } +} + +pub async fn app() -> anyhow::Result { + // Read configuration + let conf = config::read_config(None); + config::configure_logging(&conf); + + tracing::info!("Config read successfully! Initializing application ..."); + + // Initialize application state + let app_state = AppState::init(&conf).await?; + + // Setup router + Ok(ports::router().with_state(app_state)) +} \ No newline at end of file diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index e59ecdb..56bf783 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -1,104 +1,25 @@ #![forbid(unsafe_code)] #![warn(clippy::unwrap_used)] -#[macro_use] -extern crate tracing; - -use crate::db::doc_store::DataStore; -use crate::db::key_store::KeyStore; -use crate::db::process_store::ProcessStore; -use crate::model::constants::ENV_LOGGING_SERVICE_ID; -use crate::util::ServiceConfig; use std::net::SocketAddr; -use std::sync::Arc; - -mod config; -mod crypto; -mod db; -mod model; -mod ports; -mod services; -mod util; - -/// Contains the application state -#[derive(Clone)] -pub(crate) struct AppState { - #[cfg_attr(not(doc_type), allow(dead_code))] - pub keyring_service: Arc, - pub logging_service: Arc, - pub service_config: Arc, - pub signing_key_path: String, -} - -impl AppState { - /// Initialize the application state from config - async fn init(conf: &config::CHConfig) -> anyhow::Result { - trace!("Initializing Process store"); - let process_store = - ProcessStore::init_process_store(&conf.process_database_url, conf.clear_db) - .await - .expect("Failure to initialize process store! Exiting..."); - trace!("Initializing Keyring store"); - let keyring_store = KeyStore::init_keystore(&conf.keyring_database_url, conf.clear_db) - .await - .expect("Failure to initialize keyring store! Exiting..."); - trace!("Initializing Document store"); - let doc_store = DataStore::init_datastore(&conf.document_database_url, conf.clear_db) - .await - .expect("Failure to initialize document store! Exiting..."); - - trace!("Initializing services"); - let keyring_service = Arc::new(services::keyring_service::KeyringService::new( - keyring_store, - )); - let doc_service = Arc::new(services::document_service::DocumentService::new( - doc_store, - keyring_service.clone(), - )); - let logging_service = Arc::new(services::logging_service::LoggingService::new( - process_store, - doc_service.clone(), - )); - - let service_config = Arc::new(util::init_service_config( - ENV_LOGGING_SERVICE_ID.to_string(), - )?); - let signing_key = util::init_signing_key(conf.signing_key.as_deref())?; - - Ok(Self { - signing_key_path: signing_key, - service_config, - keyring_service, - logging_service, - }) - } -} /// Main function: Reading config, initializing application state, starting server #[tokio::main] async fn main() -> Result<(), anyhow::Error> { #[cfg(feature = "sentry")] let _guard = sentry::init(("https://347cc3aa30aa0c07d437da8c780838d3@o4506146399322112.ingest.sentry.io/4506155710480384", sentry::ClientOptions { - release: sentry::release_name!(), - ..Default::default() + release: sentry::release_name!(), + ..Default::default() })); - // Read configuration - let conf = config::read_config(None); - config::configure_logging(&conf); - - info!("Config read successfully! Initializing application ..."); - - // Initialize application state - let app_state = AppState::init(&conf).await?; // Setup router - let app = ports::router().with_state(app_state); + let app = clearing_house_app::app().await?; // Bind port and start server let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); - info!("Starting server: Listening on {}", addr); + tracing::info!("Starting server: Listening on {}", addr); Ok(axum::Server::bind(&addr) .serve(app.into_make_service()) - .with_graceful_shutdown(util::shutdown_signal()) + .with_graceful_shutdown(clearing_house_app::util::shutdown_signal()) .await?) } diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index f51fa86..d6b96cb 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -4,7 +4,6 @@ use anyhow::Context; use axum::extract::FromRef; use axum::response::IntoResponse; use num_bigint::BigUint; -use ring::signature::KeyPair; use std::env; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -75,8 +74,9 @@ pub fn get_jwks(key_path: &str) -> Option> .unwrap_or_else(|_| panic!("Failed to load keyfile from path {key_path}")); if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { + use ring::signature::KeyPair; let pk_modulus = BigUint::from_bytes_be( - a.as_ref() + a .public_key() .modulus() .big_endian_without_leading_zero(), @@ -112,6 +112,7 @@ pub fn get_jwks(key_path: &str) -> Option> } pub fn get_fingerprint(key_path: &str) -> Option { + use ring::signature::KeyPair; let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path) .unwrap_or_else(|_| panic!("File exists at '{key_path}' and is a valid RSA keypair")); if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { @@ -135,6 +136,50 @@ pub fn get_fingerprint(key_path: &str) -> Option { } } +pub fn create_token< + T: std::fmt::Display + Clone + serde::Serialize + for<'de> serde::Deserialize<'de>, +>( + issuer: &str, + audience: &str, + private_claims: &T, +) -> String { + let signing_secret = match env::var(ENV_SHARED_SECRET) { + Ok(secret) => biscuit::jws::Secret::Bytes(secret.to_string().into_bytes()), + Err(_) => { + panic!( + "Shared Secret not configured. Please configure environment variable {}", + ENV_SHARED_SECRET + ); + } + }; + let expiration_date = chrono::Utc::now() + chrono::Duration::minutes(5); + + let claims = biscuit::ClaimsSet:: { + registered: biscuit::RegisteredClaims { + issuer: Some(issuer.to_string()), + issued_at: Some(biscuit::Timestamp::from(chrono::Utc::now())), + audience: Some(biscuit::SingleOrMultiple::Single(audience.to_string())), + expiry: Some(biscuit::Timestamp::from(expiration_date)), + ..Default::default() + }, + private: private_claims.clone(), + }; + + // Construct the JWT + let jwt = biscuit::jws::Compact::new_decoded( + From::from(biscuit::jws::RegisteredHeader { + algorithm: biscuit::jwa::SignatureAlgorithm::HS256, + ..Default::default() + }), + claims, + ); + + jwt.into_encoded(&signing_secret) + .unwrap() + .unwrap_encoded() + .to_string() +} + pub fn decode_token serde::Deserialize<'de>>( token: &str, audience: &str, diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index 383231b..77358ed 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -3,8 +3,6 @@ use crate::model::crypto::{KeyEntry, KeyMap}; use crate::util::new_uuid; use aes_gcm_siv::aead::Aead; use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; -use base64::Engine; -use blake2_rfc::blake2b::Blake2b; use chrono::Local; use generic_array::GenericArray; use std::collections::HashMap; @@ -79,7 +77,6 @@ pub struct Document { pub dt_id: String, pub pid: String, pub ts: i64, - pub tc: i64, pub parts: Vec, } @@ -132,16 +129,11 @@ impl Document { self.pid.clone(), self.dt_id.clone(), self.ts, - self.tc, key_ct, cts, )) } - pub fn get_formatted_tc(&self) -> String { - format_tc(self.tc) - } - pub fn get_parts_map(&self) -> HashMap { let mut p_map = HashMap::with_capacity(self.parts.len()); for part in self.parts.iter() { @@ -150,13 +142,12 @@ impl Document { p_map } - pub fn new(pid: String, dt_id: String, tc: i64, parts: Vec) -> Document { + pub fn new(pid: String, dt_id: String, parts: Vec) -> Document { Document { id: Document::create_uuid(), dt_id, pid, ts: Local::now().timestamp(), - tc, parts, } } @@ -166,7 +157,6 @@ impl Document { pid: String, dt_id: String, ts: i64, - tc: i64, parts: Vec, ) -> Document { Document { @@ -174,7 +164,6 @@ impl Document { dt_id, pid, ts, - tc, parts, } } @@ -186,8 +175,6 @@ pub struct EncryptedDocument { pub pid: String, pub dt_id: String, pub ts: i64, - pub tc: i64, - pub hash: String, pub keys_ct: String, pub cts: Vec, } @@ -229,42 +216,15 @@ impl EncryptedDocument { self.pid.clone(), self.dt_id.clone(), self.ts, - self.tc, pts, )) } - pub fn get_formatted_tc(&self) -> String { - format_tc(self.tc) - } - - pub fn hash(&self) -> String { - let mut hasher = Blake2b::new(64); - - hasher.update(self.id.as_bytes()); - hasher.update(self.pid.as_bytes()); - hasher.update(self.dt_id.as_bytes()); - hasher.update(self.get_formatted_tc().as_bytes()); - hasher.update(self.ts.to_string().as_bytes()); - hasher.update(self.hash.as_bytes()); - hasher.update(self.keys_ct.as_bytes()); - let mut cts = self.cts.clone(); - cts.sort(); - for ct in cts.iter() { - hasher.update(ct.as_bytes()); - } - - let res = base64::engine::general_purpose::STANDARD.encode(hasher.finalize()); - debug!("hashed cts: '{}'", &res); - res - } - pub fn new( id: String, pid: String, dt_id: String, ts: i64, - tc: i64, keys_ct: String, cts: Vec, ) -> EncryptedDocument { @@ -273,8 +233,6 @@ impl EncryptedDocument { pid, dt_id, ts, - tc, - hash: String::from("0"), keys_ct, cts, } @@ -295,29 +253,3 @@ pub fn restore_pt_no_dt(pt: &str) -> anyhow::Result<(String, String)> { fn format_pt_for_storage(field_name: &str, pt: &str) -> String { format!("{}{}{}", field_name, SPLIT_CT, pt) } - -fn format_tc(tc: i64) -> String { - format!("{:08}", tc) -} - -#[cfg(test)] -mod test { - /// Purpose of this test case: The `base64::encode` function has been deprecated in favor of - /// `base64::engine::Engine::encode`. This test case ensures that the new function works as - /// expected. - #[test] - fn hash() { - let doc = super::EncryptedDocument::new( - String::from("id"), - String::from("pid"), - String::from("dt_id"), - 42, - 12, - String::from("keys_ct"), - vec![String::from("ct1"), String::from("ct2")], - ); - - let hash = doc.hash(); - assert_eq!("X/BsEutzaPbi555duyusiD9z5aUCwE7oNIMteMtdYLEAqJ7FJ0Ln13J3t1Qw8MMJhLCb9rRE8bRbqHtV4mYqRA==", hash); - } -} diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index ee06824..10e5ed4 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -123,7 +123,7 @@ impl Default for IdsMessage { pid: None, model_version: "".to_string(), correlation_message: None, - issued: InfoModelDateTime::new(), + issued: InfoModelDateTime::default(), issuer_connector: InfoModelId::new("".to_string()), sender_agent: "https://w3id.org/idsa/core/ClearingHouse".to_string(), recipient_connector: None, @@ -334,7 +334,6 @@ impl TryFrom for Document { Ok(Document::new( m.pid.ok_or(serde_json::Error::custom("PID missing"))?, DEFAULT_DOC_TYPE.to_string(), - -1, doc_parts, )) } diff --git a/clearing-house-app/src/model/ids/mod.rs b/clearing-house-app/src/model/ids/mod.rs index 0882585..5f48bdb 100644 --- a/clearing-house-app/src/model/ids/mod.rs +++ b/clearing-house-app/src/model/ids/mod.rs @@ -33,6 +33,7 @@ impl InfoModelComplexId { InfoModelComplexId { id: Some(id) } } } + impl From for InfoModelComplexId { fn from(id: String) -> InfoModelComplexId { InfoModelComplexId::new(id) @@ -61,6 +62,7 @@ impl std::fmt::Display for InfoModelId { Ok(()) } } + impl From for InfoModelId { fn from(id: String) -> InfoModelId { InfoModelId::SimpleId(id) @@ -74,8 +76,8 @@ pub enum InfoModelDateTime { Time(chrono::DateTime), } -impl InfoModelDateTime { - pub fn new() -> InfoModelDateTime { +impl Default for InfoModelDateTime { + fn default() -> InfoModelDateTime { InfoModelDateTime::Time(chrono::Local::now()) } } @@ -94,9 +96,9 @@ impl std::fmt::Display for InfoModelDateTime { pub struct InfoModelTimeStamp { //IDS name #[serde( - rename = "@type", - alias = "type", - skip_serializing_if = "Option::is_none" + rename = "@type", + alias = "type", + skip_serializing_if = "Option::is_none" )] pub format: Option, //IDS name @@ -112,6 +114,7 @@ impl Default for InfoModelTimeStamp { } } } + impl std::fmt::Display for InfoModelTimeStamp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match serde_json::to_string(&self) { @@ -133,7 +136,7 @@ The message classes relevant for the Connector to Connector communication are li available in the Information Model can be found here. Based on [v4.2.0](https://github.com/International-Data-Spaces-Association/InformationModel/blob/v4.2.0/taxonomies/Message.ttl) -*/ + */ #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub enum MessageType { #[serde(rename = "ids:Message")] diff --git a/clearing-house-app/src/model/mod.rs b/clearing-house-app/src/model/mod.rs index 653e01e..f2aaa61 100644 --- a/clearing-house-app/src/model/mod.rs +++ b/clearing-house-app/src/model/mod.rs @@ -1,4 +1,4 @@ -pub(crate) mod claims; +pub mod claims; pub mod constants; pub(crate) mod crypto; pub(crate) mod doc_type; diff --git a/clearing-house-app/src/model/process.rs b/clearing-house-app/src/model/process.rs index 9ea3c5b..cda0edf 100644 --- a/clearing-house-app/src/model/process.rs +++ b/clearing-house-app/src/model/process.rs @@ -31,12 +31,10 @@ pub struct Receipt { #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct DataTransaction { - pub transaction_id: String, pub timestamp: i64, pub process_id: String, pub document_id: String, pub payload: String, - pub chain_hash: String, pub client_id: String, pub clearing_house_version: String, } @@ -78,12 +76,10 @@ impl From for DataTransaction { Err(e) => { println!("Error occurred: {:#?}", e); DataTransaction { - transaction_id: "error".to_string(), timestamp: 0, process_id: "error".to_string(), document_id: "error".to_string(), payload: "error".to_string(), - chain_hash: "error".to_string(), client_id: "error".to_string(), clearing_house_version: "error".to_string(), } diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 73c6207..926e170 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -23,8 +23,6 @@ pub enum DocumentServiceError { source: anyhow::Error, description: String, }, - #[error("Error while creating the chain hash!")] - ChainHashError, #[error("Error while retrieving keys from keyring!")] KeyringServiceError(#[from] crate::services::keyring_service::KeyringServiceError), #[error("Invalid dates in query!")] @@ -53,9 +51,6 @@ impl axum::response::IntoResponse for DocumentServiceError { format!("{}: {}", description, source), ) .into_response(), - Self::ChainHashError => { - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() - } Self::KeyringServiceError(e) => e.into_response(), Self::InvalidDates => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), @@ -130,7 +125,7 @@ impl DocumentService { }?; debug!("start encryption"); - let mut enc_doc = match doc.encrypt(keys) { + let enc_doc = match doc.encrypt(keys) { Ok(ct) => { debug!("got ct"); Ok(ct) @@ -141,31 +136,9 @@ impl DocumentService { } }?; - // chain the document to previous documents - debug!("add the chain hash..."); - // get the document with the previous tc - match self.db.get_document_with_previous_tc(doc.tc).await { - Ok(Some(previous_doc)) => { - enc_doc.hash = previous_doc.hash(); - } - Ok(None) => { - if doc.tc == 0 { - info!("No entries found for pid {}. Beginning new chain!", doc.pid); - } else { - // If this happens, db didn't find a tc entry that should exist. - return Err(DocumentServiceError::ChainHashError); - } - } - Err(e) => { - error!("Error while creating the chain hash: {:?}", e); - return Err(DocumentServiceError::ChainHashError); - } - } - // prepare the success result message - let receipt = - DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id, &enc_doc.hash); + DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id); trace!("storing document ...."); // store document diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index d1a3328..213699e 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -74,7 +74,6 @@ impl axum::response::IntoResponse for LoggingServiceError { pub struct LoggingService { db: ProcessStore, doc_api: Arc, - write_lock: Arc>, } impl LoggingService { @@ -82,7 +81,6 @@ impl LoggingService { LoggingService { db, doc_api, - write_lock: Arc::new(tokio::sync::Mutex::new(())), } } @@ -133,64 +131,31 @@ impl LoggingService { // transform message to document debug!("transforming message to document..."); - let mut doc = Document::try_from(m).map_err(LoggingServiceError::ParsingError)?; - - // lock write access - let _x = self.write_lock.lock().await; - match self.db.get_transaction_counter().await { - Ok(Some(tid)) => { - debug!("Storing document..."); - doc.tc = tid; - // TODO: ChClaims usage check - match self - .doc_api - .create_enc_document(ChClaims::new(user), doc.clone()) - .await - { - Ok(doc_receipt) => { - debug!("Increase transaction counter"); - match self.db.increment_transaction_counter().await { - Ok(Some(_tid)) => { - debug!("Creating receipt..."); - let transaction = DataTransaction { - transaction_id: doc.get_formatted_tc(), - timestamp: doc_receipt.timestamp, - process_id: doc_receipt.pid, - document_id: doc_receipt.doc_id, - payload, - chain_hash: doc_receipt.chain_hash, - client_id: user.to_owned(), - clearing_house_version: env!("CARGO_PKG_VERSION").to_string(), - }; - debug!("...done. Signing receipt..."); - Ok(transaction.sign(key_path)) - } - Ok(None) => { - unreachable!("increment_transaction_counter never returns None!") - } - Err(e) => { - error!("Error while incrementing transaction id!"); - Err(LoggingServiceError::DatabaseError { - source: e, - description: "Error while incrementing transaction id!" - .to_string(), - }) // InternalError - } - } - } - Err(e) => { - error!("Error while creating document: {:?}", e); - Err(LoggingServiceError::DocumentServiceError(e)) - } - } + let doc = Document::try_from(m).map_err(LoggingServiceError::ParsingError)?; + + debug!("Storing document..."); + // TODO: ChClaims usage check + match self + .doc_api + .create_enc_document(ChClaims::new(user), doc.clone()) + .await + { + Ok(doc_receipt) => { + debug!("Creating receipt..."); + let transaction = DataTransaction { + timestamp: doc_receipt.timestamp, + process_id: doc_receipt.pid, + document_id: doc_receipt.doc_id, + payload, + client_id: user.to_owned(), + clearing_house_version: env!("CARGO_PKG_VERSION").to_string(), + }; + debug!("...done. Signing receipt..."); + Ok(transaction.sign(key_path)) } - Ok(None) => unreachable!("get_transaction_counter never returns None!"), Err(e) => { - error!("Error while getting transaction id!"); - Err(LoggingServiceError::DatabaseError { - source: e, - description: "Error while getting transaction id".to_string(), - }) // InternalError + error!("Error while creating document: {:?}", e); + Err(LoggingServiceError::DocumentServiceError(e)) } } } diff --git a/clearing-house-app/src/services/mod.rs b/clearing-house-app/src/services/mod.rs index a5f1e20..98888ee 100644 --- a/clearing-house-app/src/services/mod.rs +++ b/clearing-house-app/src/services/mod.rs @@ -15,16 +15,14 @@ pub struct DocumentReceipt { pub timestamp: i64, pub pid: String, pub doc_id: String, - pub chain_hash: String, } impl DocumentReceipt { - pub fn new(timestamp: i64, pid: &str, doc_id: &str, chain_hash: &str) -> DocumentReceipt { + pub fn new(timestamp: i64, pid: &str, doc_id: &str) -> DocumentReceipt { DocumentReceipt { timestamp, pid: pid.to_string(), doc_id: doc_id.to_string(), - chain_hash: chain_hash.to_string(), } } } diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index bcbab1c..3c39ce3 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -1,7 +1,7 @@ use anyhow::Context; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub(crate) struct ServiceConfig { +pub struct ServiceConfig { pub service_id: String, } @@ -33,7 +33,7 @@ pub(super) fn init_signing_key(signing_key_path: Option<&str>) -> anyhow::Result } /// Signal handler to catch a Ctrl+C and initiate a graceful shutdown -pub(super) async fn shutdown_signal() { +pub async fn shutdown_signal() { let ctrl_c = async { tokio::signal::ctrl_c() .await diff --git a/clearing-house-app/tests/README.md b/clearing-house-app/tests/README.md new file mode 100644 index 0000000..52e2628 --- /dev/null +++ b/clearing-house-app/tests/README.md @@ -0,0 +1,5 @@ +# Integration Tests + +Prerequisites: + +- MongoDB running on `localhost:27017` \ No newline at end of file diff --git a/clearing-house-app/tests/log.rs b/clearing-house-app/tests/log.rs new file mode 100644 index 0000000..610785c --- /dev/null +++ b/clearing-house-app/tests/log.rs @@ -0,0 +1,87 @@ +#![cfg(test)] + +use axum::http::{Request, StatusCode}; +use tower::ServiceExt; +use clearing_house_app::model::ids::message::IdsMessage; +use clearing_house_app::model::ids::{InfoModelDateTime, InfoModelId, MessageType}; +use clearing_house_app::model::ids::request::ClearingHouseMessage; +use clearing_house_app::util::new_uuid; +use clearing_house_app::model::{claims::create_token, constants::SERVICE_HEADER}; +use clearing_house_app::model::claims::ChClaims; + +#[tokio::test] +#[ignore] +async fn log_message() { + std::env::set_var("SERVICE_ID_LOG", "test"); + std::env::set_var("SHARED_SECRET", "test"); + std::env::set_var("CH_APP_LOG_LEVEL", "TRACE"); + std::env::set_var("CH_CLEAR_DB", "false"); + + let app = clearing_house_app::app().await.unwrap(); + + let pid = new_uuid(); + + let msg = ClearingHouseMessage { + header: IdsMessage { + context: Some(std::collections::HashMap::from([ + ("ids".to_string(), "https://w3id.org/idsa/core/".to_string()), + ( + "idsc".to_string(), + "https://w3id.org/idsa/code/".to_string(), + ), + ])), + type_message: MessageType::Message, + id: Some(new_uuid()), + pid: None, + model_version: "".to_string(), + correlation_message: None, + issued: InfoModelDateTime::default(), + issuer_connector: InfoModelId::new("".to_string()), + sender_agent: "https://w3id.org/idsa/core/ClearingHouse".to_string(), + recipient_connector: None, + recipient_agent: None, + transfer_contract: None, + content_version: None, + security_token: None, + authorization_token: None, + payload: None, + payload_type: None, + }, + payload: Some("test".to_string()), + payload_type: None, + }; + + let claims = ChClaims::new("test"); + + // Log + let response = app.clone() + .oneshot(Request::builder() + .uri(format!("/messages/log/{}", pid)) + .method("POST") + .header("Content-Type", "application/json") + .header(SERVICE_HEADER, create_token("test", "test", &claims)) + .body(serde_json::to_string(&msg).unwrap().into()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::CREATED); + + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert!(!body.is_empty()); + println!("Receipt: {:?}", body); +/* + // Query + let query_resp = app + .oneshot(Request::builder() + .uri(format!("/messages/query/{}", pid)) + .method("POST") + .header("Content-Type", "application/json") + .header(SERVICE_HEADER, create_token("test", "test", &claims)) + .body(serde_json::to_string(&msg).unwrap().into()).unwrap()) + .await + .unwrap(); + + let body = hyper::body::to_bytes(query_resp.into_body()).await.unwrap(); + println!("Query: {:?}", body); + */ +} \ No newline at end of file diff --git a/clearing-house-app/tests/public_key.rs b/clearing-house-app/tests/public_key.rs new file mode 100644 index 0000000..f76626d --- /dev/null +++ b/clearing-house-app/tests/public_key.rs @@ -0,0 +1,21 @@ +use axum::body::Body; +use axum::http::{Request, StatusCode}; +use tower::ServiceExt; + +#[tokio::test] +#[ignore] +async fn retrieve_public_key() { + std::env::set_var("SERVICE_ID_LOG", "test"); + + let app = clearing_house_app::app().await.unwrap(); + + let response = app + .oneshot(Request::builder().uri("/.well-known/jwks.json").body(Body::empty()).unwrap()) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert!(!body.is_empty()); +} \ No newline at end of file From bcc6a5604162d6d4166f00e57587e9bab049c565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 22 Nov 2023 11:32:48 +0100 Subject: [PATCH 149/183] fix(ch-app): Fix integration test case log --- clearing-house-app/tests/log.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clearing-house-app/tests/log.rs b/clearing-house-app/tests/log.rs index 610785c..64a50bb 100644 --- a/clearing-house-app/tests/log.rs +++ b/clearing-house-app/tests/log.rs @@ -15,7 +15,7 @@ async fn log_message() { std::env::set_var("SERVICE_ID_LOG", "test"); std::env::set_var("SHARED_SECRET", "test"); std::env::set_var("CH_APP_LOG_LEVEL", "TRACE"); - std::env::set_var("CH_CLEAR_DB", "false"); + std::env::set_var("CH_APP_CLEAR_DB", "false"); let app = clearing_house_app::app().await.unwrap(); @@ -69,7 +69,7 @@ async fn log_message() { let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); assert!(!body.is_empty()); println!("Receipt: {:?}", body); -/* + // Query let query_resp = app .oneshot(Request::builder() @@ -83,5 +83,5 @@ async fn log_message() { let body = hyper::body::to_bytes(query_resp.into_body()).await.unwrap(); println!("Query: {:?}", body); - */ + } \ No newline at end of file From 124911e0314329b1053e3407c7c2b1ec073d799e Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 22 Nov 2023 22:57:49 +0100 Subject: [PATCH 150/183] chore: update README.md --- .gitignore | 1 + README.md | 12 +++++++++--- docker/docker-compose.yml | 12 ------------ 3 files changed, 10 insertions(+), 15 deletions(-) delete mode 100644 docker/docker-compose.yml diff --git a/.gitignore b/.gitignore index 734100a..b076f47 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ node_modules/ **/*.iml .vscode/ book/ +ca/ \ No newline at end of file diff --git a/README.md b/README.md index 3dd805b..d503156 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,24 @@ ## Install ```sh -npm install +npm run install ``` ## Usage ```sh -npm start +npm run start ``` ## Run tests +### CH_APP ```sh -npm test +npm run test:app +``` + +### CH_EDC +```sh +npm run test:edc ``` ## Authors diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index b944d58..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,12 +0,0 @@ -version: "3.8" - -services: - app: - build: ../clearing-house-app/ - environment: - NODE_ENV: production - - edc: - build: ../clearing-house-edc/launchers/connector-prod/ - environment: - NODE_ENV: production \ No newline at end of file From 34e2b9ad64c1e95e969450c412745412b852d716 Mon Sep 17 00:00:00 2001 From: dhommen Date: Wed, 22 Nov 2023 23:08:12 +0100 Subject: [PATCH 151/183] fix: removed workingdir since cd is used --- .github/workflows/release-publish.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index bba3ad3..4d0754f 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -58,9 +58,6 @@ jobs: runs-on: ubuntu-latest needs: release if: ${{ needs.release.outputs.new_tag_version != '' }} - defaults: - run: - working-directory: ./clearing-house-edc permissions: contents: read packages: write @@ -81,4 +78,4 @@ jobs: - name: Push Docker image env: DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} - run: docker push ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$DOCKER_IMAGE_TAG \ No newline at end of file + run: docker push ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$DOCKER_IMAGE_TAG From ef0fd60c908a519c2b5a250dcf77ea2a6ce8b3a4 Mon Sep 17 00:00:00 2001 From: dhommen Date: Thu, 23 Nov 2023 09:04:46 +0100 Subject: [PATCH 152/183] add tests scripts to package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b719db..0498a89 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "doc": "doc" }, "scripts": { - "test": "cd clearing-house-app && cargo test", + "test:app": "cd clearing-house-app && cargo test", + "test:edc": "cd clearing-house-edc && ./gradlew test", "start": "docker compose -f docker/docker-compose.yml up -d" }, "bugs": { From cef068b2e41916a05101dab5e3255114a49a95c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 23 Nov 2023 10:04:34 +0100 Subject: [PATCH 153/183] feat(ch-app): Add and debug integration test --- clearing-house-app/Cargo.toml | 2 + clearing-house-app/src/db/doc_store.rs | 4 +- clearing-house-app/src/lib.rs | 2 +- clearing-house-app/src/model/claims.rs | 17 ++-- clearing-house-app/src/model/ids/mod.rs | 10 +-- clearing-house-app/src/model/mod.rs | 2 +- .../src/services/document_service.rs | 3 +- .../src/services/logging_service.rs | 5 +- clearing-house-app/tests/log.rs | 89 ++++++++++++++----- clearing-house-app/tests/public_key.rs | 9 +- 10 files changed, 95 insertions(+), 48 deletions(-) diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 6319cd4..58f3fd7 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -54,7 +54,9 @@ axum = { version = "0.6.20", features = ["json", "http2"] } async-trait = "0.1.73" # Helper for working with futures futures = "0.3.29" +# Helper for creating custom error types thiserror = "1.0.48" +# Optional: Sentry integration sentry = { version = "0.31.7", optional = true } [dev-dependencies] diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/doc_store.rs index a7c1b2b..455c8a5 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/doc_store.rs @@ -10,9 +10,7 @@ use crate::model::SortingOrder; use anyhow::anyhow; use futures::StreamExt; use mongodb::bson::doc; -use mongodb::options::{ - AggregateOptions, CreateCollectionOptions, UpdateOptions, WriteConcern, -}; +use mongodb::options::{AggregateOptions, CreateCollectionOptions, UpdateOptions, WriteConcern}; use mongodb::{bson, Client, IndexModel}; #[derive(Clone, Debug)] diff --git a/clearing-house-app/src/lib.rs b/clearing-house-app/src/lib.rs index 7f8cfc7..3e48c3d 100644 --- a/clearing-house-app/src/lib.rs +++ b/clearing-house-app/src/lib.rs @@ -82,4 +82,4 @@ pub async fn app() -> anyhow::Result { // Setup router Ok(ports::router().with_state(app_state)) -} \ No newline at end of file +} diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index d6b96cb..30958fb 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -75,12 +75,8 @@ pub fn get_jwks(key_path: &str) -> Option> if let biscuit::jws::Secret::RsaKeyPair(a) = keypair { use ring::signature::KeyPair; - let pk_modulus = BigUint::from_bytes_be( - a - .public_key() - .modulus() - .big_endian_without_leading_zero(), - ); + let pk_modulus = + BigUint::from_bytes_be(a.public_key().modulus().big_endian_without_leading_zero()); let pk_e = BigUint::from_bytes_be( a.as_ref() .public_key() @@ -225,3 +221,12 @@ pub fn decode_token serde::Deserialize<'d .with_context(|| "Failed validating JWT")?; Ok(decoded_jwt.payload()?.private.clone()) } + +#[cfg(test)] +mod test { + #[test] + fn get_fingerprint() { + let fingerprint = super::get_fingerprint("keys/private_key.der").unwrap(); + assert_eq!(fingerprint, "Qra//29Frxbj5hh5Azef+G36SeiOm9q7s8+w8uGLD28"); + } +} diff --git a/clearing-house-app/src/model/ids/mod.rs b/clearing-house-app/src/model/ids/mod.rs index 5f48bdb..f72ed4c 100644 --- a/clearing-house-app/src/model/ids/mod.rs +++ b/clearing-house-app/src/model/ids/mod.rs @@ -5,9 +5,9 @@ pub mod request; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)] pub struct InfoModelComplexId { - //IDS name + /// IDS name #[serde(rename = "@id", alias = "id", skip_serializing_if = "Option::is_none")] - // Correlated message, e.g. a response to a previous request + /// Correlated message, e.g. a response to a previous request pub id: Option, } @@ -96,9 +96,9 @@ impl std::fmt::Display for InfoModelDateTime { pub struct InfoModelTimeStamp { //IDS name #[serde( - rename = "@type", - alias = "type", - skip_serializing_if = "Option::is_none" + rename = "@type", + alias = "type", + skip_serializing_if = "Option::is_none" )] pub format: Option, //IDS name diff --git a/clearing-house-app/src/model/mod.rs b/clearing-house-app/src/model/mod.rs index f2aaa61..a204098 100644 --- a/clearing-house-app/src/model/mod.rs +++ b/clearing-house-app/src/model/mod.rs @@ -4,7 +4,7 @@ pub(crate) mod crypto; pub(crate) mod doc_type; pub(crate) mod document; pub mod ids; -pub(crate) mod process; +pub mod process; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum SortingOrder { diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 926e170..b9f254c 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -137,8 +137,7 @@ impl DocumentService { }?; // prepare the success result message - let receipt = - DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id); + let receipt = DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id); trace!("storing document ...."); // store document diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 213699e..658dc1c 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -78,10 +78,7 @@ pub struct LoggingService { impl LoggingService { pub fn new(db: ProcessStore, doc_api: Arc) -> LoggingService { - LoggingService { - db, - doc_api, - } + LoggingService { db, doc_api } } pub async fn log( diff --git a/clearing-house-app/tests/log.rs b/clearing-house-app/tests/log.rs index 64a50bb..dc57a8d 100644 --- a/clearing-house-app/tests/log.rs +++ b/clearing-house-app/tests/log.rs @@ -1,13 +1,16 @@ #![cfg(test)] use axum::http::{Request, StatusCode}; -use tower::ServiceExt; +use biscuit::jwa::SignatureAlgorithm::PS512; +use biscuit::jws::Secret; +use clearing_house_app::model::claims::{ChClaims, get_fingerprint}; use clearing_house_app::model::ids::message::IdsMessage; -use clearing_house_app::model::ids::{InfoModelDateTime, InfoModelId, MessageType}; use clearing_house_app::model::ids::request::ClearingHouseMessage; -use clearing_house_app::util::new_uuid; +use clearing_house_app::model::ids::{IdsQueryResult, InfoModelDateTime, InfoModelId, MessageType}; use clearing_house_app::model::{claims::create_token, constants::SERVICE_HEADER}; -use clearing_house_app::model::claims::ChClaims; +use clearing_house_app::util::new_uuid; +use tower::ServiceExt; +use clearing_house_app::model::process::Receipt; #[tokio::test] #[ignore] @@ -20,6 +23,7 @@ async fn log_message() { let app = clearing_house_app::app().await.unwrap(); let pid = new_uuid(); + let id = new_uuid(); let msg = ClearingHouseMessage { header: IdsMessage { @@ -31,7 +35,7 @@ async fn log_message() { ), ])), type_message: MessageType::Message, - id: Some(new_uuid()), + id: Some(id.clone()), pid: None, model_version: "".to_string(), correlation_message: None, @@ -53,35 +57,72 @@ async fn log_message() { let claims = ChClaims::new("test"); - // Log - let response = app.clone() - .oneshot(Request::builder() - .uri(format!("/messages/log/{}", pid)) - .method("POST") - .header("Content-Type", "application/json") - .header(SERVICE_HEADER, create_token("test", "test", &claims)) - .body(serde_json::to_string(&msg).unwrap().into()).unwrap()) + // Send log message + let response = app + .clone() + .oneshot( + Request::builder() + .uri(format!("/messages/log/{}", pid)) + .method("POST") + .header("Content-Type", "application/json") + .header(SERVICE_HEADER, create_token("test", "test", &claims)) + .body(serde_json::to_string(&msg).unwrap().into()) + .unwrap(), + ) .await .unwrap(); + // Check status code assert_eq!(response.status(), StatusCode::CREATED); - + // get body let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); assert!(!body.is_empty()); - println!("Receipt: {:?}", body); - // Query + // Decode receipt + let receipt = serde_json::from_slice::(&body).unwrap(); + println!("Receipt: {:?}", receipt); + let decoded_receipt = receipt.data + .decode(&Secret::rsa_keypair_from_file("keys/private_key.der") + .expect("Loading key successfully"), PS512) + .expect("Decoding JWS successful"); + let decoded_receipt_header = decoded_receipt + .header() + .expect("Header is now already decoded"); + + assert_eq!(decoded_receipt_header.registered.key_id, get_fingerprint("keys/private_key.der")); + + let decoded_receipt_payload = decoded_receipt + .payload() + .expect("Payload is now already decoded"); + println!("Decoded Receipt: {:?}", decoded_receipt); + + assert_eq!(decoded_receipt_payload.process_id, pid); + assert_eq!(decoded_receipt_payload.payload, "test".to_string()); + + // --------------------------------------------------------------------------------------------- + + // Query ID let query_resp = app - .oneshot(Request::builder() - .uri(format!("/messages/query/{}", pid)) - .method("POST") - .header("Content-Type", "application/json") - .header(SERVICE_HEADER, create_token("test", "test", &claims)) - .body(serde_json::to_string(&msg).unwrap().into()).unwrap()) + .oneshot( + Request::builder() + .uri(format!("/messages/query/{}", pid)) + .method("POST") + .header("Content-Type", "application/json") + .header(SERVICE_HEADER, create_token("test", "test", &claims)) + .body(serde_json::to_string(&msg).unwrap().into()) + .unwrap(), + ) .await .unwrap(); + assert_eq!(query_resp.status(), StatusCode::OK); let body = hyper::body::to_bytes(query_resp.into_body()).await.unwrap(); - println!("Query: {:?}", body); + assert!(!body.is_empty()); -} \ No newline at end of file + let ids_message = serde_json::from_slice::(&body).unwrap(); + println!("IDS Query Result: {:?}", ids_message); + let query_docs = ids_message.documents; + assert_eq!(query_docs.len(), 1); + let doc = query_docs.first().expect("Document is there, just checked").to_owned(); + assert_eq!(doc.payload.expect("Payload is there"), "test".to_string()); +} diff --git a/clearing-house-app/tests/public_key.rs b/clearing-house-app/tests/public_key.rs index f76626d..a4c19dc 100644 --- a/clearing-house-app/tests/public_key.rs +++ b/clearing-house-app/tests/public_key.rs @@ -10,7 +10,12 @@ async fn retrieve_public_key() { let app = clearing_house_app::app().await.unwrap(); let response = app - .oneshot(Request::builder().uri("/.well-known/jwks.json").body(Body::empty()).unwrap()) + .oneshot( + Request::builder() + .uri("/.well-known/jwks.json") + .body(Body::empty()) + .unwrap(), + ) .await .unwrap(); @@ -18,4 +23,4 @@ async fn retrieve_public_key() { let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); assert!(!body.is_empty()); -} \ No newline at end of file +} From d73f4b03683efedcc4c6e792b7686a14bb8f1f47 Mon Sep 17 00:00:00 2001 From: dhommen Date: Thu, 23 Nov 2023 10:36:19 +0100 Subject: [PATCH 154/183] docs: update documentation with quick start guide --- README.md | 15 ++--- docs/SUMMARY.md | 1 + docs/content/admin-guide/quick_start.md | 85 +++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 docs/content/admin-guide/quick_start.md diff --git a/README.md b/README.md index d503156..ad64e86 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,11 @@ > The ids-basecamp-clearingHouse is a implementation of the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) component of the [International Data Space](https://internationaldataspaces.org/). -## Install +## Quick Start +Please refer to the quick start [example]() -```sh -npm run install -``` - -## Usage -```sh -npm run start -``` +## Documentation +Please refer to the [documentation](https://truzzt.github.io/ids-basecamp-clearinghouse/) ## Run tests @@ -53,4 +48,4 @@ Give a ⭐️ if this project helped you! This project is [Apache--2.0](https://github.com/truzzt/ids-basecamp-clearinghouse/blob/development/LICENSE) licensed. *** -_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ \ No newline at end of file +_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 860494e..cb5ba21 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,6 +1,7 @@ # Summary # Admin Guide +- [Quick Start](content/admin-guide/quick_start.md) - [Installation](content/admin-guide/installation.md) - [Clearinghouse-App](content/admin-guide/ch-app_installation.md) - [Tests](content/admin-guide/tests.md) diff --git a/docs/content/admin-guide/quick_start.md b/docs/content/admin-guide/quick_start.md new file mode 100644 index 0000000..e8102ba --- /dev/null +++ b/docs/content/admin-guide/quick_start.md @@ -0,0 +1,85 @@ +# Quick Start + +## Prerequesits +To run the quick start example please ensure to have a working DAPS. + +### Private Key +You will need the private key in the following formats: +* .jks +* .der + +### Environment +```.env +VERSION=v1.0.0-alpha.5 +SERVICE_ID_LOG=1 +SHARED_SECRET=changethis +KEY_PASSWORD=password +DAPS_URL= +DAPS_JWKS_URL= +API_KEY=changethis +``` + +## docker-compose.yml +```sh +docker compose up +``` + +```docker-compoye.yml +version: "3.8" + +services: + ch-app: + image: ghcr.io/truzzt/ids-basecamp-clearing/ch-app:$VERSION + environment: + CH_APP_PROCESS_DATABASE_URL: mongodb://mongodb:27017 + CH_APP_KEYRING_DATABASE_URL: mongodb://mongodb:27017 + CH_APP_DOCUMENT_DATABASE_URL: mongodb://mongodb:27017 + CH_APP_CLEAR_DB: false + CH_APP_LOG_LEVEL: INFO + SERVICE_ID_LOG: $SERVICE_ID + SHARED_SECRET: $SHARED_SECRET + volumes: + - ./YOUR_PRIVATE_KEY.der:/app/keys/private_key.der:ro + + ch-edc: + image: ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$VERSION + environment: + WEB_HTTP_PORT=11001 + WEB_HTTP_PATH=/api + WEB_HTTP_DATA_PORT=11002 + WEB_HTTP_DATA_PATH=/api/v1/data + WEB_HTTP_IDS_PORT=11003 + WEB_HTTP_IDS_PATH=/api/v1/ids + EDC_IDS_ID=urn:connector:example-connector + EDC_IDS_TITLE='truzzt Test EDC Connector' + EDC_IDS_DESCRIPTION='Minimally configured Open Source EDC built by truzzt.' + EDC_IDS_ENDPOINT=http://ch-edc:11003/api/v1/ids + IDS_WEBHOOK_ADDRESS=http://ch-edc:11003 + EDC_IDS_CURATOR=https://truzzt.com + EDC_IDS_MAINTAINER=https://truzzt.com + EDC_CONNECTOR_NAME=truzzt-example-connector + EDC_HOSTNAME=ch-edc + EDC_API_AUTH_KEY=$API_KEY + EDC_WEB_REST_CORS_ENABLED='true' + EDC_WEB_REST_CORS_HEADERS='origin,content-type,accept,authorization,x-api-key' + EDC_WEB_REST_CORS_ORIGINS='*' + EDC_VAULT=/resources/vault/edc/vault.properties + EDC_OAUTH_TOKEN_URL=$DAPS_URL + EDC_OAUTH_PROVIDER_JWKS_URL=$DAPS_JWKS_URL + EDC_OAUTH_CLIENT_ID=$CLIENT_ID + EDC_KEYSTORE=/resources/vault/edc/keystore.jks + EDC_KEYSTORE_PASSWORD=$KEY_PASSWORD + EDC_OAUTH_CERTIFICATE_ALIAS=1 + EDC_OAUTH_PRIVATE_KEY_ALIAS=1 + TRUZZT_CLEARINGHOUSE_JWT_AUDIENCE=$SERVICE_ID + TRUZZT_CLEARINGHOUSE_JWT_ISSUER=ch-edc + TRUZZT_CLEARINGHOUSE_JWT_SIGN_SECRET=$SHARED_SECRET + TRUZZT_CLEARINGHOUSE_JWT_EXPIRES_AT=30 + TRUZZT_CLEARINGHOUSE_APP_BASE_URL=ch-edc:8080 + volumes: + - ./YOUR_PRIVATE_KEY.jks:/resources/vault/edc/keystore.jks + + mongodb: + image: mongo +``` + From e77e17af221bf2ce5704a460ed5ba89801bfb8c0 Mon Sep 17 00:00:00 2001 From: dhommen Date: Thu, 23 Nov 2023 10:36:52 +0100 Subject: [PATCH 155/183] chore: add ch-app int test script and docs script --- package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/package.json b/package.json index 0498a89..6e63ac4 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,10 @@ "doc": "doc" }, "scripts": { + "test:app:int": "cd clearing-house-app && cargo test -- --ignored", "test:app": "cd clearing-house-app && cargo test", "test:edc": "cd clearing-house-edc && ./gradlew test", + "docs": "mdbook serve", "start": "docker compose -f docker/docker-compose.yml up -d" }, "bugs": { From 147a7e5bf9b91f837f8fe89e3962d0177ba65e85 Mon Sep 17 00:00:00 2001 From: dhommen Date: Thu, 23 Nov 2023 10:39:23 +0100 Subject: [PATCH 156/183] fix quickstart link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad64e86..c97db1e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ > The ids-basecamp-clearingHouse is a implementation of the [Clearing House](https://github.com/International-Data-Spaces-Association/IDS-RAM_4_0/blob/main/documentation/3_Layers_of_the_Reference_Architecture_Model/3_5_System_Layer/3_5_5_Clearing_House.md) component of the [International Data Space](https://internationaldataspaces.org/). ## Quick Start -Please refer to the quick start [example]() +Please refer to the quick start [guide](https://truzzt.github.io/ids-basecamp-clearinghouse/content/admin-guide/quick_start.html) ## Documentation Please refer to the [documentation](https://truzzt.github.io/ids-basecamp-clearinghouse/) From 11a7314f2bfc9236561770623a98239bf71b088e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 23 Nov 2023 10:40:36 +0100 Subject: [PATCH 157/183] feat(ch-app): Use JWKS from endpoint to validate receipt --- clearing-house-app/tests/log.rs | 47 ++++++++++++++++---------- clearing-house-app/tests/public_key.rs | 3 ++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/clearing-house-app/tests/log.rs b/clearing-house-app/tests/log.rs index dc57a8d..652971b 100644 --- a/clearing-house-app/tests/log.rs +++ b/clearing-house-app/tests/log.rs @@ -2,11 +2,12 @@ use axum::http::{Request, StatusCode}; use biscuit::jwa::SignatureAlgorithm::PS512; -use biscuit::jws::Secret; +use biscuit::jwk::JWKSet; +use hyper::Body; use clearing_house_app::model::claims::{ChClaims, get_fingerprint}; use clearing_house_app::model::ids::message::IdsMessage; use clearing_house_app::model::ids::request::ClearingHouseMessage; -use clearing_house_app::model::ids::{IdsQueryResult, InfoModelDateTime, InfoModelId, MessageType}; +use clearing_house_app::model::ids::{IdsQueryResult, InfoModelId, MessageType}; use clearing_house_app::model::{claims::create_token, constants::SERVICE_HEADER}; use clearing_house_app::util::new_uuid; use tower::ServiceExt; @@ -22,6 +23,26 @@ async fn log_message() { let app = clearing_house_app::app().await.unwrap(); + // Prerequisite JWKS for checking the signature + let response = app + .clone() + .oneshot( + Request::builder() + .uri("/.well-known/jwks.json") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + assert!(!body.is_empty()); + let jwks = serde_json::from_slice::>(&body).expect("Decoded the JWKSet"); + + // --------------------------------------------------------------------------------------------- + // Create a message let pid = new_uuid(); let id = new_uuid(); @@ -36,20 +57,10 @@ async fn log_message() { ])), type_message: MessageType::Message, id: Some(id.clone()), - pid: None, - model_version: "".to_string(), - correlation_message: None, - issued: InfoModelDateTime::default(), - issuer_connector: InfoModelId::new("".to_string()), + model_version: "test".to_string(), + issuer_connector: InfoModelId::new("test-connector".to_string()), sender_agent: "https://w3id.org/idsa/core/ClearingHouse".to_string(), - recipient_connector: None, - recipient_agent: None, - transfer_contract: None, - content_version: None, - security_token: None, - authorization_token: None, - payload: None, - payload_type: None, + ..Default::default() }, payload: Some("test".to_string()), payload_type: None, @@ -82,8 +93,7 @@ async fn log_message() { let receipt = serde_json::from_slice::(&body).unwrap(); println!("Receipt: {:?}", receipt); let decoded_receipt = receipt.data - .decode(&Secret::rsa_keypair_from_file("keys/private_key.der") - .expect("Loading key successfully"), PS512) + .decode_with_jwks(&jwks, Some(PS512)) .expect("Decoding JWS successful"); let decoded_receipt_header = decoded_receipt .header() @@ -122,7 +132,10 @@ async fn log_message() { let ids_message = serde_json::from_slice::(&body).unwrap(); println!("IDS Query Result: {:?}", ids_message); let query_docs = ids_message.documents; + + // Check the only document in the result assert_eq!(query_docs.len(), 1); let doc = query_docs.first().expect("Document is there, just checked").to_owned(); assert_eq!(doc.payload.expect("Payload is there"), "test".to_string()); + assert_eq!(doc.model_version, "test".to_string()); } diff --git a/clearing-house-app/tests/public_key.rs b/clearing-house-app/tests/public_key.rs index a4c19dc..3aba051 100644 --- a/clearing-house-app/tests/public_key.rs +++ b/clearing-house-app/tests/public_key.rs @@ -1,5 +1,6 @@ use axum::body::Body; use axum::http::{Request, StatusCode}; +use biscuit::jwk::JWKSet; use tower::ServiceExt; #[tokio::test] @@ -23,4 +24,6 @@ async fn retrieve_public_key() { let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); assert!(!body.is_empty()); + let jwks = serde_json::from_slice::>(&body).expect("Decoded the JWKSet"); + println!("JWKS: {:?}", jwks); } From 2779f6c5fc2f550e9e35af9c60b2ca7426d52036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 23 Nov 2023 10:41:30 +0100 Subject: [PATCH 158/183] feat(ch-app): Removed certs folder --- clearing-house-app/certs/daps-dev.der | Bin 796 -> 0 bytes clearing-house-app/certs/daps.der | Bin 796 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 clearing-house-app/certs/daps-dev.der delete mode 100644 clearing-house-app/certs/daps.der diff --git a/clearing-house-app/certs/daps-dev.der b/clearing-house-app/certs/daps-dev.der deleted file mode 100644 index bf5eca335b9710b0ba41a0ba3caf58f5f1c918e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 796 zcmXqLVwNyyVw$&rnTe5!iILHOi;Y98&EuRc3p0~}iy^lGCmVAp3!5;Li>o2O0WXNd z#lu!oR9aP4V#otj4HV$!VNOXcGZZrr0r8l5cnvL$^bE}`^bGZkj7<&X#CeU34J?c- z4NOffjm)EfTr(io0?IY;BFPpR11X3t!YH;_85$b^K@+1A(2<-#AGCevWn^SzWngY% z7g|VCw^Cc3w7m~eoykqzx?|jk6Am; zyvKU})-vaKN5=A{JwFw;l}dh?_2;;AgI9Aa&r*&PGg}%`l3nbkD$DJi)vm-{p(G%+ zES-Jkgfcl!gF8nVV_7;}7O!(&yKRT*<+>G<9=6-N$y_M%J3T47cKQbygGo)Q>9q$J zu6)h({kX}Y%W~aCf-{@E@;9z->3f)N`&yw&>_gy;|JmCm!na9ApNO5At8hW`zFUaw z&yK@6?JcW%w(d&%r}?$V{ZGBU;j8l@cRy|YymTk`$;V=c9dz88x%@lIG#}Y?2e#N) zmHMxnts*=ne)$h~_QLgleFCdInV1|fpxHm9AGR|Jo?heTWgl%kDLu$P90bG z3K=MYbSQ&VNEnDUi1??~?&WxQ%V0&Pq<%t6WX#!9C(=IX?y~e%*!rV3cXs2ShdJ{ diff --git a/clearing-house-app/certs/daps.der b/clearing-house-app/certs/daps.der deleted file mode 100644 index bf5eca335b9710b0ba41a0ba3caf58f5f1c918e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 796 zcmXqLVwNyyVw$&rnTe5!iILHOi;Y98&EuRc3p0~}iy^lGCmVAp3!5;Li>o2O0WXNd z#lu!oR9aP4V#otj4HV$!VNOXcGZZrr0r8l5cnvL$^bE}`^bGZkj7<&X#CeU34J?c- z4NOffjm)EfTr(io0?IY;BFPpR11X3t!YH;_85$b^K@+1A(2<-#AGCevWn^SzWngY% z7g|VCw^Cc3w7m~eoykqzx?|jk6Am; zyvKU})-vaKN5=A{JwFw;l}dh?_2;;AgI9Aa&r*&PGg}%`l3nbkD$DJi)vm-{p(G%+ zES-Jkgfcl!gF8nVV_7;}7O!(&yKRT*<+>G<9=6-N$y_M%J3T47cKQbygGo)Q>9q$J zu6)h({kX}Y%W~aCf-{@E@;9z->3f)N`&yw&>_gy;|JmCm!na9ApNO5At8hW`zFUaw z&yK@6?JcW%w(d&%r}?$V{ZGBU;j8l@cRy|YymTk`$;V=c9dz88x%@lIG#}Y?2e#N) zmHMxnts*=ne)$h~_QLgleFCdInV1|fpxHm9AGR|Jo?heTWgl%kDLu$P90bG z3K=MYbSQ&VNEnDUi1??~?&WxQ%V0&Pq<%t6WX#!9C(=IX?y~e%*!rV3cXs2ShdJ{ From 0d8398932fb4fde1b454d2117ef567cc85ddc0c0 Mon Sep 17 00:00:00 2001 From: dhommen Date: Thu, 23 Nov 2023 10:50:26 +0100 Subject: [PATCH 159/183] fix: quick start docker-compose.yml snytax --- docs/content/admin-guide/quick_start.md | 64 ++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/content/admin-guide/quick_start.md b/docs/content/admin-guide/quick_start.md index e8102ba..65c48b7 100644 --- a/docs/content/admin-guide/quick_start.md +++ b/docs/content/admin-guide/quick_start.md @@ -44,38 +44,38 @@ services: ch-edc: image: ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$VERSION environment: - WEB_HTTP_PORT=11001 - WEB_HTTP_PATH=/api - WEB_HTTP_DATA_PORT=11002 - WEB_HTTP_DATA_PATH=/api/v1/data - WEB_HTTP_IDS_PORT=11003 - WEB_HTTP_IDS_PATH=/api/v1/ids - EDC_IDS_ID=urn:connector:example-connector - EDC_IDS_TITLE='truzzt Test EDC Connector' - EDC_IDS_DESCRIPTION='Minimally configured Open Source EDC built by truzzt.' - EDC_IDS_ENDPOINT=http://ch-edc:11003/api/v1/ids - IDS_WEBHOOK_ADDRESS=http://ch-edc:11003 - EDC_IDS_CURATOR=https://truzzt.com - EDC_IDS_MAINTAINER=https://truzzt.com - EDC_CONNECTOR_NAME=truzzt-example-connector - EDC_HOSTNAME=ch-edc - EDC_API_AUTH_KEY=$API_KEY - EDC_WEB_REST_CORS_ENABLED='true' - EDC_WEB_REST_CORS_HEADERS='origin,content-type,accept,authorization,x-api-key' - EDC_WEB_REST_CORS_ORIGINS='*' - EDC_VAULT=/resources/vault/edc/vault.properties - EDC_OAUTH_TOKEN_URL=$DAPS_URL - EDC_OAUTH_PROVIDER_JWKS_URL=$DAPS_JWKS_URL - EDC_OAUTH_CLIENT_ID=$CLIENT_ID - EDC_KEYSTORE=/resources/vault/edc/keystore.jks - EDC_KEYSTORE_PASSWORD=$KEY_PASSWORD - EDC_OAUTH_CERTIFICATE_ALIAS=1 - EDC_OAUTH_PRIVATE_KEY_ALIAS=1 - TRUZZT_CLEARINGHOUSE_JWT_AUDIENCE=$SERVICE_ID - TRUZZT_CLEARINGHOUSE_JWT_ISSUER=ch-edc - TRUZZT_CLEARINGHOUSE_JWT_SIGN_SECRET=$SHARED_SECRET - TRUZZT_CLEARINGHOUSE_JWT_EXPIRES_AT=30 - TRUZZT_CLEARINGHOUSE_APP_BASE_URL=ch-edc:8080 + WEB_HTTP_PORT: 11001 + WEB_HTTP_PATH: /api + WEB_HTTP_DATA_PORT: 11002 + WEB_HTTP_DATA_PATH: /api/v1/data + WEB_HTTP_IDS_PORT: 11003 + WEB_HTTP_IDS_PATH: /api/v1/ids + EDC_IDS_ID: urn:connector:example-connector + EDC_IDS_TITLE: 'truzzt Test EDC Connector' + EDC_IDS_DESCRIPTION: 'Minimally configured Open Source EDC built by truzzt.' + EDC_IDS_ENDPOINT: http://ch-edc:11003/api/v1/ids + IDS_WEBHOOK_ADDRESS: http://ch-edc:11003 + EDC_IDS_CURATOR: https://truzzt.com + EDC_IDS_MAINTAINER: https://truzzt.com + EDC_CONNECTOR_NAME: truzzt-example-connector + EDC_HOSTNAME: ch-edc + EDC_API_AUTH_KEY: $API_KEY + EDC_WEB_REST_CORS_ENABLED: 'true' + EDC_WEB_REST_CORS_HEADERS: 'origin,content-type,accept,authorization,x-api-key' + EDC_WEB_REST_CORS_ORIGINS: '*' + EDC_VAULT: /resources/vault/edc/vault.properties + EDC_OAUTH_TOKEN_URL: $DAPS_URL + EDC_OAUTH_PROVIDER_JWKS_URL: $DAPS_JWKS_URL + EDC_OAUTH_CLIENT_ID: $CLIENT_ID + EDC_KEYSTORE: /resources/vault/edc/keystore.jks + EDC_KEYSTORE_PASSWORD: $KEY_PASSWORD + EDC_OAUTH_CERTIFICATE_ALIAS: 1 + EDC_OAUTH_PRIVATE_KEY_ALIAS: 1 + TRUZZT_CLEARINGHOUSE_JWT_AUDIENCE: $SERVICE_ID + TRUZZT_CLEARINGHOUSE_JWT_ISSUER: ch-edc + TRUZZT_CLEARINGHOUSE_JWT_SIGN_SECRET: $SHARED_SECRET + TRUZZT_CLEARINGHOUSE_JWT_EXPIRES_AT: 30 + TRUZZT_CLEARINGHOUSE_APP_BASE_URL: ch-edc:8080 volumes: - ./YOUR_PRIVATE_KEY.jks:/resources/vault/edc/keystore.jks From 5121c31f981849c834be153f26bd97a1403fda96 Mon Sep 17 00:00:00 2001 From: dhommen Date: Thu, 23 Nov 2023 12:02:29 +0100 Subject: [PATCH 160/183] docs: update quick start and readme --- README.md | 2 ++ docs/SUMMARY.md | 3 ++- ...installation.md => ch-edc_installation.md} | 0 docs/content/admin-guide/quick_start.md | 23 +++++++++++++++---- 4 files changed, 22 insertions(+), 6 deletions(-) rename docs/content/admin-guide/{installation.md => ch-edc_installation.md} (100%) diff --git a/README.md b/README.md index c97db1e..07adb80 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ npm run test:edc 👤 **Maximilian Schönenberg** 👤 **Daniel Hommen** +👤 **Glaucio Jannotti** +👤 **Augusto Leal** ## 🤝 Contributing diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index cb5ba21..6d52145 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -2,8 +2,9 @@ # Admin Guide - [Quick Start](content/admin-guide/quick_start.md) -- [Installation](content/admin-guide/installation.md) +- [Installation]() - [Clearinghouse-App](content/admin-guide/ch-app_installation.md) + - [Clearinghouse-EDC](content/admin-guide/ch-edc_installation.md) - [Tests](content/admin-guide/tests.md) - [Maintenance](content/admin-guide/maintenance.md) diff --git a/docs/content/admin-guide/installation.md b/docs/content/admin-guide/ch-edc_installation.md similarity index 100% rename from docs/content/admin-guide/installation.md rename to docs/content/admin-guide/ch-edc_installation.md diff --git a/docs/content/admin-guide/quick_start.md b/docs/content/admin-guide/quick_start.md index 65c48b7..daae246 100644 --- a/docs/content/admin-guide/quick_start.md +++ b/docs/content/admin-guide/quick_start.md @@ -8,15 +8,26 @@ You will need the private key in the following formats: * .jks * .der +The .jks should be generated from the MDS Portal + +To generate the .der key run +```sh +openssl genpkey -algorithm RSA \ + -pkeyopt rsa_keygen_bits:4096 \ + -outform der \ + -out private_key.der +``` + ### Environment ```.env -VERSION=v1.0.0-alpha.5 -SERVICE_ID_LOG=1 +VERSION=1.0.0-beta.1 +SERVICE_ID=1 SHARED_SECRET=changethis KEY_PASSWORD=password DAPS_URL= DAPS_JWKS_URL= API_KEY=changethis +CLIENT_ID= ``` ## docker-compose.yml @@ -44,8 +55,8 @@ services: ch-edc: image: ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$VERSION environment: - WEB_HTTP_PORT: 11001 - WEB_HTTP_PATH: /api + WEB_HTTP_MANAGEMENT_PORT: 11001 + WEB_HTTP_MANAGEMENT_PATH: / WEB_HTTP_DATA_PORT: 11002 WEB_HTTP_DATA_PATH: /api/v1/data WEB_HTTP_IDS_PORT: 11003 @@ -66,6 +77,7 @@ services: EDC_VAULT: /resources/vault/edc/vault.properties EDC_OAUTH_TOKEN_URL: $DAPS_URL EDC_OAUTH_PROVIDER_JWKS_URL: $DAPS_JWKS_URL + EDC_OAUTH_ENDPOINT_AUDIENCE: idsc:IDS_CONNECTORS_ALL EDC_OAUTH_CLIENT_ID: $CLIENT_ID EDC_KEYSTORE: /resources/vault/edc/keystore.jks EDC_KEYSTORE_PASSWORD: $KEY_PASSWORD @@ -75,9 +87,10 @@ services: TRUZZT_CLEARINGHOUSE_JWT_ISSUER: ch-edc TRUZZT_CLEARINGHOUSE_JWT_SIGN_SECRET: $SHARED_SECRET TRUZZT_CLEARINGHOUSE_JWT_EXPIRES_AT: 30 - TRUZZT_CLEARINGHOUSE_APP_BASE_URL: ch-edc:8080 + TRUZZT_CLEARINGHOUSE_APP_BASE_URL: http://ch-app:8000 volumes: - ./YOUR_PRIVATE_KEY.jks:/resources/vault/edc/keystore.jks + - ./vault.properties:/resources/vault/edc/vault.properties mongodb: image: mongo From 60e45f0dd820b5123ee40c00bd66420dce4dcb5a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 23 Nov 2023 11:29:51 +0000 Subject: [PATCH 161/183] chore(release): 1.0.0-beta.1 [skip ci] # 1.0.0-beta.1 (2023-11-23) ### Bug Fixes * **app:** Fix build on development branch ([32bfea3](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/32bfea389a3f0f43907f3c5e7afa66105f25cf60)) * **app:** Fix build on development branch ([851146e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/851146eb3c546f6813d3209beee367b84ee1ffaa)) * **app:** Fix warnings and build on development branch ([89f39f7](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/89f39f784180b4bd26813f33e7787d0744fe975c)) * **ch-app:** Add error log and removed assert ([0d07fe5](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/0d07fe55c3a83a2b4d22adde2e7c70ddc44b2c06)) * **ch-app:** Bump dependencies ([6f273bb](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/6f273bbd5b8c0503f2061aee944b95c692a2a3f1)) * **ch-app:** Fix all clippy warnings ([812f3e8](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/812f3e868bfb4c17c5a18765bacaf7826ef99532)) * **ch-app:** Fix integration test case log ([bcc6a56](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/bcc6a5604162d6d4166f00e57587e9bab049c565)) * **ch-app:** Fix security issue through updating dependencies ([2613559](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/26135597ccc4a8f9f040f496732fb7e275504ce9)) * **ch-app:** Reenable new serde crates, due to resolved issues with precompiled binaries ([e2784b9](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/e2784b9b642987cc1ddb9ffa2ca7057cb6382d25)) * **ch-app:** Updated dependencies to fix security vulnerability ([fe19cdf](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/fe19cdf8c153a1108759a27f689ed3fdc2197ff4)) * **ch-edc:** add missing vault filesystem ([e845269](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/e845269a2149f9b02b5dac71c4f40649052a8d12)) * **ch-edc:** add multistage dockerfile ([8e8026e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/8e8026e39059debc5df27f24b58829c081c58da0)) * **ci:** Delete .github/workflows/rust.yml to fix failing CI ([3a8d5a1](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/3a8d5a15c08151ea2d43f70d7a25ecb4f4555424)) * **ci:** disable rust workflow (dublicate build) ([9af75cf](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/9af75cf760173fda5d1fad4bf4ddbefd21224413)) * **ci:** Fix rust.yml workflow ([0a474c0](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/0a474c0904a74f258978b1bd0ed2278edd8c8db1)) * **ci:** Fix unauthorized push ([57d4e02](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/57d4e02ebee80c04f359d577fd87af2a70e0b7ce)) * **ci:** Fix unauthorized push ([453ce88](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/453ce8810ddd5970f0d7c349f142ea5f24db8b8a)) * **ci:** simplified ch-edc docker build ([f0cb1e1](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f0cb1e149160b945e6e03d2426e6b40165c6fb55)) * **ci:** updated test job to run from root ([04cecce](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/04cecce30c0c787847ca199788d40e1daf07092f)) * **config:** Fixed config and added unit test to verify correct functionality ([76765e6](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/76765e687c3cac025f33fd902d28a6caec764e2f)) * **core:** Disable integration tests, fix warnings and make the build reproducible ([ecd3078](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/ecd3078b92d8061588f58537133c5b56074b91f9)) * **core:** Disable integration tests, fix warnings and make the build reproducible ([c69b246](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/c69b246cf365c06ccfb23bdf0c85f0506f4a023e)) * quick start docker-compose.yml snytax ([0d83989](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/0d8398932fb4fde1b454d2117ef567cc85ddc0c0)) * removed workingdir since cd is used ([34e2b9a](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/34e2b9ad64c1e95e969450c412745412b852d716)) * **tests:** add __ENV for hostname and token ([209244c](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/209244c551e8e9fd4eed5e00b620a271e5fd57e9)) * updating .gitignore to exclude vscode files ([1ce073f](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/1ce073fef0b2e70d97c58d1b14a7dec104bed3a1)) ### Features * AppSender, LoggingMessageDelegate, LogMessageHandler tests implemented ([5127591](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/5127591162bec3ee6e92227ffbb80f36ffa08f62)) * basic endpoint functions working ([f1726e7](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f1726e74574a596e1216d4cf468af1ccfd07443e)) * **ch-app:** Add and debug integration test ([cef068b](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/cef068b2e41916a05101dab5e3255114a49a95c8)) * **ch-app:** Add CreateProcessResponse as JSON ([002845a](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/002845aa0729887853954118032084c6e5606354)) * **ch-app:** add Dockerfile and GH action ([f64aa14](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f64aa14c802e91a34b85437d07d79eba756ea504)) * **ch-app:** Add docs for installation of ch-app ([293500d](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/293500d45f2bccbae47d4ae0dfdbf01851ea4f03)) * **ch-app:** Added tests, refactored unwrap ([b3f8ede](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/b3f8edec027aa8168f64fd552ec7bed0e7f4ac30)) * **ch-app:** Bump Cargo edition to 2021 and remove unused imports ([007281f](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/007281f3e7f436606c04c41edab917c432e7e0c8)) * **ch-app:** Bump Cargo edition to 2021 and remove unused imports ([6a3934e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/6a3934e089f775bf434821d0e672e63daf34676c)) * **ch-app:** Created services for Keyring- and Document-Service inside logging service and adjusted the handlers ([4bb512f](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4bb512f68f1137a3c89cca7bbd4ee6055525b1ed)) * **ch-app:** Created services for Keyring- and Document-Service inside logging service and adjusted the handlers ([f1a8e59](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f1a8e5969006156c931ce39a7225b8e3acea56a5)) * **ch-app:** feature flag sentry ([918a903](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/918a9035ac1e61a0faa8716143f25886d049dae2)) * **ch-app:** Finished error-handling in keyring service and introduces 'doc_type' feature ([387498c](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/387498c15ff2bd8c2890625dd92d8d3be1250b42)) * **ch-app:** Finished refactoring document-service error-handling ([8965f5e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/8965f5e8a1ccbfdf8c36040f3736a3dd7fee7929)) * **ch-app:** Refactor logging-api to use a service as well ([4259c65](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4259c65cfca978f3ad77c8d37fec85bd3fbaa90f)) * **ch-app:** Refactor logging-api to use a service as well ([f1beee0](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f1beee0bd6ed48277d02a385b25d232f7ee5740a)) * **ch-app:** Remove Blockchain, add integration tests ([ffdfbad](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/ffdfbadd10769b99f392617f0d691fcd45dcdafb)) * **ch-app:** Removed ApiResponse, fixed warnings and hid more doc_type related functions ([fc710b7](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/fc710b7afc2f8ff28729ee88315fd74777476c05)) * **ch-app:** Removed certs folder ([2779f6c](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/2779f6c5fc2f550e9e35af9c60b2ca7426d52036)) * **ch-app:** Setup tracing as logger and replace rocket as logger; setup config ([c9d8e6f](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/c9d8e6f99fba95ab83816911293cc1885f866fae)) * **ch-app:** Setup tracing as logger and replace rocket as logger; setup config ([356665a](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/356665a46bd6de165b0fd227b845d10d6e1fcb0e)) * **ch-app:** Use JWKS from endpoint to validate receipt ([11a7314](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/11a7314f2bfc9236561770623a98239bf71b088e)) * **ci:** add test job for CH app ([807bcdf](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/807bcdf5fad95456dfcd008fcee990983facd711)) * create connector and extension modules ([fa47ff8](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/fa47ff8f18feeefd77fdcf6be9cfe266981f358b)) * Create TestUtils with mock and start to create application tests ([f1612e0](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f1612e027f9815ad9525c7f78aab876baf1f64a1)) * **doc:** Add internal description to docs ([4e89ba6](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4e89ba6755095d30d23df8caec3463561112cafe)) * **docker:** Optimised docker image with distroless image ([d046826](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/d046826132c1e6cc3e60f2c31e2d4f8c397fe01b)) * **docs:** add d2 diagramming integration to workflow ([24e87ef](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/24e87efc96516a22dc1edc4d89662cebd537d2bf)) * **docs:** add mdbook for documentation ([0cf4ada](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/0cf4adaa5494a8ae3bc679ee0387b90bc3079e38)) * **docs:** Enable GitHub Pages generation ([36bfaa3](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/36bfaa3f569ee86be8f8cc072cb951aeaca8e295)) * externalization of environments variables ([f8e187e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f8e187e59c32483c8250252683804f0b86643de7)) * readme added ([4d382b5](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4d382b5877dda24b6143b08a47549d3c29a61d71)) * release action ([98f1448](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/98f1448795003bf6fc823fccda7f0e14fe8b7cb0)) * release action ([4710fc0](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4710fc0bde1a63ca6af2042a56b81b68c73860b1)) * **release:** add more release types ([cd59461](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/cd59461fb2dfa5b8c95c80fbaa3bafd511e036c0)) * semantic-release ([6fb29ff](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/6fb29ff39a86a34e2bda5ac400b1114643b4f906)) * starting create objects and method ([f13f15e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f13f15e7e35c866f011a4474bc3bd5722d8a40b9)) * **tests:** add load tests ([a88175b](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/a88175bb083ce0091459e8b47c4c27ac042f782b)) * **tests:** add smoke tests ([e31f806](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/e31f8066b08ebac341aa3b081056bbd110b72680)) --- CHANGELOG.md | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b27d34c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,74 @@ +# 1.0.0-beta.1 (2023-11-23) + + +### Bug Fixes + +* **app:** Fix build on development branch ([32bfea3](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/32bfea389a3f0f43907f3c5e7afa66105f25cf60)) +* **app:** Fix build on development branch ([851146e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/851146eb3c546f6813d3209beee367b84ee1ffaa)) +* **app:** Fix warnings and build on development branch ([89f39f7](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/89f39f784180b4bd26813f33e7787d0744fe975c)) +* **ch-app:** Add error log and removed assert ([0d07fe5](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/0d07fe55c3a83a2b4d22adde2e7c70ddc44b2c06)) +* **ch-app:** Bump dependencies ([6f273bb](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/6f273bbd5b8c0503f2061aee944b95c692a2a3f1)) +* **ch-app:** Fix all clippy warnings ([812f3e8](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/812f3e868bfb4c17c5a18765bacaf7826ef99532)) +* **ch-app:** Fix integration test case log ([bcc6a56](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/bcc6a5604162d6d4166f00e57587e9bab049c565)) +* **ch-app:** Fix security issue through updating dependencies ([2613559](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/26135597ccc4a8f9f040f496732fb7e275504ce9)) +* **ch-app:** Reenable new serde crates, due to resolved issues with precompiled binaries ([e2784b9](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/e2784b9b642987cc1ddb9ffa2ca7057cb6382d25)) +* **ch-app:** Updated dependencies to fix security vulnerability ([fe19cdf](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/fe19cdf8c153a1108759a27f689ed3fdc2197ff4)) +* **ch-edc:** add missing vault filesystem ([e845269](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/e845269a2149f9b02b5dac71c4f40649052a8d12)) +* **ch-edc:** add multistage dockerfile ([8e8026e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/8e8026e39059debc5df27f24b58829c081c58da0)) +* **ci:** Delete .github/workflows/rust.yml to fix failing CI ([3a8d5a1](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/3a8d5a15c08151ea2d43f70d7a25ecb4f4555424)) +* **ci:** disable rust workflow (dublicate build) ([9af75cf](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/9af75cf760173fda5d1fad4bf4ddbefd21224413)) +* **ci:** Fix rust.yml workflow ([0a474c0](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/0a474c0904a74f258978b1bd0ed2278edd8c8db1)) +* **ci:** Fix unauthorized push ([57d4e02](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/57d4e02ebee80c04f359d577fd87af2a70e0b7ce)) +* **ci:** Fix unauthorized push ([453ce88](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/453ce8810ddd5970f0d7c349f142ea5f24db8b8a)) +* **ci:** simplified ch-edc docker build ([f0cb1e1](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f0cb1e149160b945e6e03d2426e6b40165c6fb55)) +* **ci:** updated test job to run from root ([04cecce](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/04cecce30c0c787847ca199788d40e1daf07092f)) +* **config:** Fixed config and added unit test to verify correct functionality ([76765e6](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/76765e687c3cac025f33fd902d28a6caec764e2f)) +* **core:** Disable integration tests, fix warnings and make the build reproducible ([ecd3078](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/ecd3078b92d8061588f58537133c5b56074b91f9)) +* **core:** Disable integration tests, fix warnings and make the build reproducible ([c69b246](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/c69b246cf365c06ccfb23bdf0c85f0506f4a023e)) +* quick start docker-compose.yml snytax ([0d83989](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/0d8398932fb4fde1b454d2117ef567cc85ddc0c0)) +* removed workingdir since cd is used ([34e2b9a](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/34e2b9ad64c1e95e969450c412745412b852d716)) +* **tests:** add __ENV for hostname and token ([209244c](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/209244c551e8e9fd4eed5e00b620a271e5fd57e9)) +* updating .gitignore to exclude vscode files ([1ce073f](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/1ce073fef0b2e70d97c58d1b14a7dec104bed3a1)) + + +### Features + +* AppSender, LoggingMessageDelegate, LogMessageHandler tests implemented ([5127591](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/5127591162bec3ee6e92227ffbb80f36ffa08f62)) +* basic endpoint functions working ([f1726e7](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f1726e74574a596e1216d4cf468af1ccfd07443e)) +* **ch-app:** Add and debug integration test ([cef068b](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/cef068b2e41916a05101dab5e3255114a49a95c8)) +* **ch-app:** Add CreateProcessResponse as JSON ([002845a](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/002845aa0729887853954118032084c6e5606354)) +* **ch-app:** add Dockerfile and GH action ([f64aa14](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f64aa14c802e91a34b85437d07d79eba756ea504)) +* **ch-app:** Add docs for installation of ch-app ([293500d](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/293500d45f2bccbae47d4ae0dfdbf01851ea4f03)) +* **ch-app:** Added tests, refactored unwrap ([b3f8ede](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/b3f8edec027aa8168f64fd552ec7bed0e7f4ac30)) +* **ch-app:** Bump Cargo edition to 2021 and remove unused imports ([007281f](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/007281f3e7f436606c04c41edab917c432e7e0c8)) +* **ch-app:** Bump Cargo edition to 2021 and remove unused imports ([6a3934e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/6a3934e089f775bf434821d0e672e63daf34676c)) +* **ch-app:** Created services for Keyring- and Document-Service inside logging service and adjusted the handlers ([4bb512f](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4bb512f68f1137a3c89cca7bbd4ee6055525b1ed)) +* **ch-app:** Created services for Keyring- and Document-Service inside logging service and adjusted the handlers ([f1a8e59](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f1a8e5969006156c931ce39a7225b8e3acea56a5)) +* **ch-app:** feature flag sentry ([918a903](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/918a9035ac1e61a0faa8716143f25886d049dae2)) +* **ch-app:** Finished error-handling in keyring service and introduces 'doc_type' feature ([387498c](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/387498c15ff2bd8c2890625dd92d8d3be1250b42)) +* **ch-app:** Finished refactoring document-service error-handling ([8965f5e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/8965f5e8a1ccbfdf8c36040f3736a3dd7fee7929)) +* **ch-app:** Refactor logging-api to use a service as well ([4259c65](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4259c65cfca978f3ad77c8d37fec85bd3fbaa90f)) +* **ch-app:** Refactor logging-api to use a service as well ([f1beee0](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f1beee0bd6ed48277d02a385b25d232f7ee5740a)) +* **ch-app:** Remove Blockchain, add integration tests ([ffdfbad](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/ffdfbadd10769b99f392617f0d691fcd45dcdafb)) +* **ch-app:** Removed ApiResponse, fixed warnings and hid more doc_type related functions ([fc710b7](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/fc710b7afc2f8ff28729ee88315fd74777476c05)) +* **ch-app:** Removed certs folder ([2779f6c](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/2779f6c5fc2f550e9e35af9c60b2ca7426d52036)) +* **ch-app:** Setup tracing as logger and replace rocket as logger; setup config ([c9d8e6f](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/c9d8e6f99fba95ab83816911293cc1885f866fae)) +* **ch-app:** Setup tracing as logger and replace rocket as logger; setup config ([356665a](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/356665a46bd6de165b0fd227b845d10d6e1fcb0e)) +* **ch-app:** Use JWKS from endpoint to validate receipt ([11a7314](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/11a7314f2bfc9236561770623a98239bf71b088e)) +* **ci:** add test job for CH app ([807bcdf](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/807bcdf5fad95456dfcd008fcee990983facd711)) +* create connector and extension modules ([fa47ff8](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/fa47ff8f18feeefd77fdcf6be9cfe266981f358b)) +* Create TestUtils with mock and start to create application tests ([f1612e0](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f1612e027f9815ad9525c7f78aab876baf1f64a1)) +* **doc:** Add internal description to docs ([4e89ba6](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4e89ba6755095d30d23df8caec3463561112cafe)) +* **docker:** Optimised docker image with distroless image ([d046826](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/d046826132c1e6cc3e60f2c31e2d4f8c397fe01b)) +* **docs:** add d2 diagramming integration to workflow ([24e87ef](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/24e87efc96516a22dc1edc4d89662cebd537d2bf)) +* **docs:** add mdbook for documentation ([0cf4ada](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/0cf4adaa5494a8ae3bc679ee0387b90bc3079e38)) +* **docs:** Enable GitHub Pages generation ([36bfaa3](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/36bfaa3f569ee86be8f8cc072cb951aeaca8e295)) +* externalization of environments variables ([f8e187e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f8e187e59c32483c8250252683804f0b86643de7)) +* readme added ([4d382b5](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4d382b5877dda24b6143b08a47549d3c29a61d71)) +* release action ([98f1448](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/98f1448795003bf6fc823fccda7f0e14fe8b7cb0)) +* release action ([4710fc0](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/4710fc0bde1a63ca6af2042a56b81b68c73860b1)) +* **release:** add more release types ([cd59461](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/cd59461fb2dfa5b8c95c80fbaa3bafd511e036c0)) +* semantic-release ([6fb29ff](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/6fb29ff39a86a34e2bda5ac400b1114643b4f906)) +* starting create objects and method ([f13f15e](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/f13f15e7e35c866f011a4474bc3bd5722d8a40b9)) +* **tests:** add load tests ([a88175b](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/a88175bb083ce0091459e8b47c4c27ac042f782b)) +* **tests:** add smoke tests ([e31f806](https://github.com/truzzt/ids-basecamp-clearinghouse/commit/e31f8066b08ebac341aa3b081056bbd110b72680)) From c920b825219edeae317d874f6cb723d1016ecabc Mon Sep 17 00:00:00 2001 From: "D.Hommen" <75446820+dhommen@users.noreply.github.com> Date: Mon, 4 Dec 2023 11:11:37 +0100 Subject: [PATCH 162/183] fix: disable tokenFormat check --- .../edc/multipart/MultipartController.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java index 0fdf8a9..be1fcd9 100644 --- a/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java +++ b/clearing-house-edc/extensions/multipart/src/main/java/de/truzzt/clearinghouse/edc/multipart/MultipartController.java @@ -139,13 +139,13 @@ public Response request(@PathParam(PID) String pid, } // Check the security token type - var tokenFormat = securityToken.getTokenFormat().getId().toString(); - if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { - monitor.severe(LOG_ID + ": Invalid security token type: " + tokenFormat); - return Response.status(Response.Status.BAD_REQUEST) - .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) - .build(); - } + //var tokenFormat = securityToken.getTokenFormat().getId().toString(); + //if (!tokenFormat.equals(TokenFormat.JWT_TOKEN_FORMAT)) { + // monitor.severe(LOG_ID + ": Invalid security token type: " + tokenFormat); + // return Response.status(Response.Status.BAD_REQUEST) + // .entity(createFormDataMultiPart(typeManagerUtil, HEADER, malformedMessage(null, connectorId))) + // .build(); + //} // Check if payload is missing if (payload == null) { From af59cd7a4dd41dfcacc79f1ce0f788f6d2080dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 2 Jan 2024 10:29:38 +0100 Subject: [PATCH 163/183] chore: gitignore, logging, code fmt --- .gitignore | 6 ++++-- clearing-house-app/config.toml | 2 +- .../src/services/logging_service.rs | 6 +++--- clearing-house-app/tests/log.rs | 21 ++++++++++++------- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index b076f47..fb4e6cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -.settings/* +/**/.settings +/**/.classpath .project target data @@ -8,4 +9,5 @@ node_modules/ **/*.iml .vscode/ book/ -ca/ \ No newline at end of file +ca/ +.DS_Store diff --git a/clearing-house-app/config.toml b/clearing-house-app/config.toml index 457bd54..62a4c3d 100644 --- a/clearing-house-app/config.toml +++ b/clearing-house-app/config.toml @@ -4,4 +4,4 @@ process_database_url= "mongodb://localhost:27017" keyring_database_url= "mongodb://localhost:27017" clear_db = true signing_key = "keys/private_key.der" # Optional -performance_tracing = true \ No newline at end of file +performance_tracing = false diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 658dc1c..589265d 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -88,7 +88,7 @@ impl LoggingService { msg: ClearingHouseMessage, pid: String, ) -> Result { - trace!("...user '{:?}'", &ch_claims.client_id); + trace!("...user '{}'", &ch_claims.client_id); let user = &ch_claims.client_id; // Add non-InfoModel information to IdsMessage let mut m = msg.header; @@ -241,7 +241,7 @@ impl LoggingService { ) -> Result { debug!("page: {:#?}, size:{:#?} and sort:{:#?}", page, size, sort); - trace!("...user '{:?}'", &ch_claims.client_id); + trace!("...user '{}'", &ch_claims.client_id); let user = &ch_claims.client_id; // Check if process exists and if the user is authorized to access the process @@ -291,7 +291,7 @@ impl LoggingService { id: String, _message: ClearingHouseMessage, ) -> Result { - trace!("...user '{:?}'", &ch_claims.client_id); + trace!("...user '{}'", &ch_claims.client_id); let user = &ch_claims.client_id; // Check if process exists and if the user is authorized to access the process diff --git a/clearing-house-app/tests/log.rs b/clearing-house-app/tests/log.rs index 652971b..8d5c615 100644 --- a/clearing-house-app/tests/log.rs +++ b/clearing-house-app/tests/log.rs @@ -3,15 +3,15 @@ use axum::http::{Request, StatusCode}; use biscuit::jwa::SignatureAlgorithm::PS512; use biscuit::jwk::JWKSet; -use hyper::Body; -use clearing_house_app::model::claims::{ChClaims, get_fingerprint}; +use clearing_house_app::model::claims::{get_fingerprint, ChClaims}; use clearing_house_app::model::ids::message::IdsMessage; use clearing_house_app::model::ids::request::ClearingHouseMessage; use clearing_house_app::model::ids::{IdsQueryResult, InfoModelId, MessageType}; +use clearing_house_app::model::process::Receipt; use clearing_house_app::model::{claims::create_token, constants::SERVICE_HEADER}; use clearing_house_app::util::new_uuid; +use hyper::Body; use tower::ServiceExt; -use clearing_house_app::model::process::Receipt; #[tokio::test] #[ignore] @@ -66,7 +66,7 @@ async fn log_message() { payload_type: None, }; - let claims = ChClaims::new("test"); + let claims = ChClaims::new("69:F5:9D:B0:DD:A6:9D:30:5F:58:AA:2D:20:4D:B2:39:F0:54:FC:3B:keyid:4F:66:7D:BD:08:EE:C6:4A:D1:96:D8:7C:6C:A2:32:8A:EC:A6:AD:49"); // Send log message let response = app @@ -92,14 +92,18 @@ async fn log_message() { // Decode receipt let receipt = serde_json::from_slice::(&body).unwrap(); println!("Receipt: {:?}", receipt); - let decoded_receipt = receipt.data + let decoded_receipt = receipt + .data .decode_with_jwks(&jwks, Some(PS512)) .expect("Decoding JWS successful"); let decoded_receipt_header = decoded_receipt .header() .expect("Header is now already decoded"); - assert_eq!(decoded_receipt_header.registered.key_id, get_fingerprint("keys/private_key.der")); + assert_eq!( + decoded_receipt_header.registered.key_id, + get_fingerprint("keys/private_key.der") + ); let decoded_receipt_payload = decoded_receipt .payload() @@ -135,7 +139,10 @@ async fn log_message() { // Check the only document in the result assert_eq!(query_docs.len(), 1); - let doc = query_docs.first().expect("Document is there, just checked").to_owned(); + let doc = query_docs + .first() + .expect("Document is there, just checked") + .to_owned(); assert_eq!(doc.payload.expect("Payload is there"), "test".to_string()); assert_eq!(doc.model_version, "test".to_string()); } From 2ca4dfae59aa65061f818d579d81eb7f09325576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 2 Jan 2024 10:34:34 +0100 Subject: [PATCH 164/183] fix(ch-app): Fix 3 vulnerabilitites: GHSA-rjhf-4mh8-9xjq, GHSA-xphf-cx8h-7q9g, GHSA-3mv5-343c-w2qg --- clearing-house-app/Cargo.lock | 438 +++++++++++++++++++--------------- 1 file changed, 252 insertions(+), 186 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index ea752e8..7a9fa20 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -55,9 +55,9 @@ dependencies = [ [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", "getrandom", @@ -92,19 +92,19 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "bson" -version = "2.7.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58da0ae1e701ea752cc46c1bb9f39d5ecefc7395c3ecd526261a566d4f16e0c2" +checksum = "88c18b51216e1f74b9d769cead6ace2f82b965b807e3d73330aabe9faec31c84" dependencies = [ "ahash", "base64 0.13.1", @@ -302,7 +302,7 @@ dependencies = [ "iana-time-zone", "num-traits", "serde", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -375,9 +375,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -385,9 +385,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" @@ -460,7 +460,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.2", + "hashbrown 0.14.3", "lock_api", "once_cell", "parking_lot_core", @@ -484,9 +484,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] @@ -555,12 +555,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f258a7194e7f7c2a7837a8913aeab7fd8c383457034fa20ce4dd3dcb813e8eb8" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -610,9 +610,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] @@ -625,9 +625,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -640,9 +640,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -650,15 +650,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -667,38 +667,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -735,9 +735,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" @@ -766,9 +766,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "heck" @@ -790,9 +790,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hkdf" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ "hmac", ] @@ -830,9 +830,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http", @@ -853,9 +853,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", @@ -868,7 +868,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -890,9 +890,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.58" +version = "0.1.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -930,9 +930,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -955,7 +955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.3", ] [[package]] @@ -975,7 +975,7 @@ checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ "socket2 0.5.5", "widestring", - "windows-sys", + "windows-sys 0.48.0", "winreg", ] @@ -987,15 +987,15 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" dependencies = [ "wasm-bindgen", ] @@ -1008,9 +1008,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "linked-hash-map" @@ -1020,9 +1020,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" [[package]] name = "lock_api" @@ -1088,9 +1088,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "mime" @@ -1115,20 +1115,20 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "mongodb" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c926772050c3a3f87c837626bf6135c8ca688d91d31dd39a3da547fc2bc9fe" +checksum = "46c30763a5c6c52079602be44fa360ca3bfacee55fca73f4734aecd23706a7f2" dependencies = [ "async-trait", "base64 0.13.1", @@ -1254,18 +1254,18 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -1288,9 +1288,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.59" +version = "0.10.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" dependencies = [ "bitflags 2.4.1", "cfg-if", @@ -1309,7 +1309,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] @@ -1320,9 +1320,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.95" +version = "0.9.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" dependencies = [ "cc", "libc", @@ -1367,7 +1367,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -1387,9 +1387,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" @@ -1408,7 +1408,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] @@ -1425,9 +1425,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "polyval" @@ -1455,9 +1455,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" dependencies = [ "unicode-ident", ] @@ -1470,9 +1470,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1568,9 +1568,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.22" +version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ "base64 0.21.5", "bytes", @@ -1631,16 +1631,16 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.5" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" dependencies = [ "cc", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1679,25 +1679,25 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.25" +version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.21.9" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629648aced5775d558af50b2b4c7b02983a04b312126d45eeead26e7caa498b9" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.5", + "ring 0.17.7", "rustls-webpki", "sct", ] @@ -1717,7 +1717,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.5", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -1729,17 +1729,17 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1754,7 +1754,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.5", + "ring 0.17.7", "untrusted 0.9.0", ] @@ -1912,38 +1912,38 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" +checksum = "8bb1879ea93538b78549031e2d54da3e901fd7e75f2e4dc758d760937b123d10" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.194" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.110" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" dependencies = [ "indexmap 2.1.0", "itoa", @@ -2017,7 +2017,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] @@ -2092,7 +2092,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2143,9 +2143,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" dependencies = [ "proc-macro2", "quote", @@ -2193,35 +2193,35 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ "cfg-if", "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] @@ -2236,9 +2236,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" dependencies = [ "deranged", "itoa", @@ -2256,9 +2256,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" dependencies = [ "time-core", ] @@ -2280,9 +2280,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.35.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" dependencies = [ "backtrace", "bytes", @@ -2293,7 +2293,7 @@ dependencies = [ "signal-hook-registry", "socket2 0.5.5", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2304,7 +2304,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] @@ -2319,9 +2319,9 @@ dependencies = [ [[package]] name = "tokio-openssl" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08f9ffb7809f1b20c1b398d92acf4cc719874b3b2b2d9ea2f09b4a80350878a" +checksum = "6ffab79df67727f6acf57f1ff743091873c24c579b1e2ce4d8f53e47ded4d63d" dependencies = [ "futures-util", "openssl", @@ -2411,7 +2411,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] @@ -2500,9 +2500,9 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typed-builder" @@ -2532,9 +2532,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" [[package]] name = "unicode-ident" @@ -2575,9 +2575,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7830e33f6e25723d41a63f77e434159dad02919f18f55a512b5f16f3b1d77138" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" dependencies = [ "base64 0.21.5", "log", @@ -2588,12 +2588,12 @@ dependencies = [ [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna 0.4.0", + "idna 0.5.0", "percent-encoding", "serde", ] @@ -2643,9 +2643,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2653,24 +2653,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" dependencies = [ "cfg-if", "js-sys", @@ -2680,9 +2680,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2690,28 +2690,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" dependencies = [ "js-sys", "wasm-bindgen", @@ -2719,9 +2719,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.2" +version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "widestring" @@ -2753,11 +2753,11 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.51.1" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets", + "windows-targets 0.52.0", ] [[package]] @@ -2766,7 +2766,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -2775,13 +2784,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -2790,42 +2814,84 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winreg" version = "0.50.0" @@ -2833,7 +2899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ "cfg-if", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2847,22 +2913,22 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.26" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.26" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.46", ] [[package]] From 965b4c2cbba0580006f9e40834470f3e225354b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 2 Jan 2024 11:02:37 +0100 Subject: [PATCH 165/183] feat(ch-app): Implement #91 --- clearing-house-app/src/lib.rs | 2 +- clearing-house-app/src/util.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/clearing-house-app/src/lib.rs b/clearing-house-app/src/lib.rs index 3e48c3d..1c4a77c 100644 --- a/clearing-house-app/src/lib.rs +++ b/clearing-house-app/src/lib.rs @@ -1,12 +1,12 @@ #[macro_use] extern crate tracing; +use std::sync::Arc; use crate::db::doc_store::DataStore; use crate::db::key_store::KeyStore; use crate::db::process_store::ProcessStore; use crate::model::constants::ENV_LOGGING_SERVICE_ID; use crate::util::ServiceConfig; -use std::sync::Arc; mod config; mod crypto; diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index 3c39ce3..41b9146 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -1,4 +1,5 @@ use anyhow::Context; +use crate::model::claims::get_fingerprint; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ServiceConfig { @@ -25,7 +26,7 @@ pub(super) fn init_service_config(service_id: String) -> anyhow::Result) -> anyhow::Result { let private_key_path = signing_key_path.unwrap_or("keys/private_key.der"); - if std::path::Path::new(&private_key_path).exists() { + if std::path::Path::new(&private_key_path).exists() && get_fingerprint(private_key_path).is_some() { Ok(private_key_path.to_string()) } else { anyhow::bail!("Signing key not found! Aborting startup! Please configure signing_key!"); From c24e7c4d39fda5f45c384bef5259e98e81471a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Tue, 2 Jan 2024 11:21:39 +0100 Subject: [PATCH 166/183] Fix(gh-pages): Fix permission issue --- .github/workflows/docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d211040..2fea6be 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,3 +32,6 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./book + +permissions: + contents: write From 842ff0058b0b6d1ca4b3d62a6747d0bfcf025bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 1 Feb 2024 12:28:20 +0200 Subject: [PATCH 167/183] feat(ch-app): Add postgres implementation (#96) * feat(ch-app): finalize postgres implementation * feat(ch-app): finalize postgres implementation with relations * feat(ch-app): implement all feature flags for both mongodb and postgres * fix(ci) Set postgres build as the default build * fix(ch-app): Fix test build * fix(ch-app): Fix test case 'model::test::validate_and_sanitize_dates' and set 'postgres' as a default feature * feat(ch-app): Add embedded migration and some log messages * Update postgres_document_store.rs * Update postgres_process_store.rs --- clearing-house-app/Cargo.lock | 1023 +++++++++++++---- clearing-house-app/Cargo.toml | 28 +- clearing-house-app/build.rs | 5 + clearing-house-app/config.toml | 4 +- .../migrations/20240102094054_init.down.sql | 7 + .../migrations/20240102094054_init.up.sql | 44 + clearing-house-app/src/config.rs | 24 +- clearing-house-app/src/crypto.rs | 257 ----- clearing-house-app/src/db/key_store.rs | 286 ----- clearing-house-app/src/db/mod.rs | 53 +- .../db/{doc_store.rs => mongo_doc_store.rs} | 372 +++--- ...rocess_store.rs => mongo_process_store.rs} | 93 +- .../src/db/postgres_document_store.rs | 192 ++++ .../src/db/postgres_process_store.rs | 136 +++ clearing-house-app/src/lib.rs | 81 +- clearing-house-app/src/main.rs | 9 +- clearing-house-app/src/model/crypto.rs | 91 -- clearing-house-app/src/model/doc_type.rs | 25 - clearing-house-app/src/model/document.rs | 245 +--- clearing-house-app/src/model/ids/message.rs | 175 +-- clearing-house-app/src/model/mod.rs | 11 +- clearing-house-app/src/model/process.rs | 26 +- clearing-house-app/src/ports/doc_type_api.rs | 119 -- clearing-house-app/src/ports/mod.rs | 11 - .../src/services/document_service.rs | 185 +-- .../src/services/keyring_service.rs | 369 ------ .../src/services/logging_service.rs | 29 +- clearing-house-app/src/services/mod.rs | 9 +- clearing-house-app/src/util.rs | 11 +- clearing-house-app/tests/log.rs | 19 +- clearing-house-app/tests/public_key.rs | 4 +- tests/smoke.js | 4 - 32 files changed, 1539 insertions(+), 2408 deletions(-) create mode 100644 clearing-house-app/build.rs create mode 100644 clearing-house-app/migrations/20240102094054_init.down.sql create mode 100644 clearing-house-app/migrations/20240102094054_init.up.sql delete mode 100644 clearing-house-app/src/crypto.rs delete mode 100644 clearing-house-app/src/db/key_store.rs rename clearing-house-app/src/db/{doc_store.rs => mongo_doc_store.rs} (72%) rename clearing-house-app/src/db/{process_store.rs => mongo_process_store.rs} (63%) create mode 100644 clearing-house-app/src/db/postgres_document_store.rs create mode 100644 clearing-house-app/src/db/postgres_process_store.rs delete mode 100644 clearing-house-app/src/model/crypto.rs delete mode 100644 clearing-house-app/src/model/doc_type.rs delete mode 100644 clearing-house-app/src/ports/doc_type_api.rs delete mode 100644 clearing-house-app/src/services/keyring_service.rs diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 7a9fa20..d689c26 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -17,42 +17,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm-siv" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "polyval", - "subtle", - "zeroize", -] - [[package]] name = "ahash" version = "0.8.7" @@ -75,6 +39,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -104,7 +74,26 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-write-file" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" +dependencies = [ + "nix", + "rand", ] [[package]] @@ -115,18 +104,19 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axum" -version = "0.6.20" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +checksum = "1236b4b292f6c4d6dc34604bb5120d85c3fe1d1aa596bd5cc52ca054d13e7b9e" dependencies = [ "async-trait", "axum-core", - "bitflags 1.3.2", "bytes", "futures-util", - "http", - "http-body", - "hyper", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", "itoa", "matchit", "memchr", @@ -143,23 +133,28 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "axum-core" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" dependencies = [ "async-trait", "bytes", "futures-util", - "http", - "http-body", + "http 1.0.0", + "http-body 1.0.0", + "http-body-util", "mime", + "pin-project-lite", "rustversion", + "sync_wrapper", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -185,9 +180,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "biscuit" @@ -213,9 +214,12 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -294,45 +298,30 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.48.5", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", + "windows-targets 0.52.0", ] [[package]] name = "clearing-house-app" version = "0.10.0" dependencies = [ - "aes", - "aes-gcm-siv", "anyhow", "async-trait", "axum", - "base64 0.21.5", + "base64 0.21.7", "biscuit", "chrono", "config", "futures", - "generic-array", - "hex", - "hkdf", - "hyper", + "hyper 1.1.0", "mongodb", "num-bigint", "once_cell", @@ -343,7 +332,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2", + "sqlx", "tempfile", "thiserror", "tokio", @@ -367,6 +356,12 @@ dependencies = [ "toml", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.4.0" @@ -391,31 +386,51 @@ checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] -name = "crypto-common" -version = "0.1.6" +name = "crc" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" dependencies = [ - "generic-array", - "rand_core", - "typenum", + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", ] [[package]] -name = "ctr" -version = "0.9.2" +name = "crossbeam-utils" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "cipher", + "generic-array", + "typenum", ] [[package]] @@ -482,6 +497,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.11" @@ -522,10 +548,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +dependencies = [ + "serde", +] + [[package]] name = "encoding_rs" version = "0.8.33" @@ -563,6 +605,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "fastrand" version = "2.0.1" @@ -587,6 +646,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + [[package]] name = "fnv" version = "1.0.7" @@ -665,6 +735,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -679,7 +760,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -724,9 +805,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -741,16 +822,35 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "h2" -version = "0.3.22" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap 2.1.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", + "http 1.0.0", "indexmap 2.1.0", "slab", "tokio", @@ -769,18 +869,34 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.3", +] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" [[package]] name = "hex" @@ -806,6 +922,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "hostname" version = "0.3.1" @@ -828,6 +953,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http-body" version = "0.4.6" @@ -835,7 +971,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.0.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +dependencies = [ + "bytes", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -861,9 +1020,9 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", - "http", - "http-body", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -875,6 +1034,26 @@ dependencies = [ "want", ] +[[package]] +name = "hyper" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.2", + "http 1.0.0", + "http-body 1.0.0", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "tokio", + "want", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -882,12 +1061,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper", + "hyper 0.14.28", "native-tls", "tokio", "tokio-native-tls", ] +[[package]] +name = "hyper-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.0.0", + "http-body 1.0.0", + "hyper 1.1.0", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.59" @@ -958,15 +1155,6 @@ dependencies = [ "hashbrown 0.14.3", ] -[[package]] -name = "inout" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" -dependencies = [ - "generic-array", -] - [[package]] name = "ipconfig" version = "0.3.2" @@ -985,6 +1173,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "itertools" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -993,9 +1190,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -1005,12 +1202,32 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "libc" -version = "0.2.151" +version = "0.2.152" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] [[package]] name = "linked-hash-map" @@ -1020,9 +1237,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -1192,6 +1409,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.4.2", + "cfg-if", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1216,11 +1444,28 @@ dependencies = [ name = "num-bigint" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ - "autocfg", + "byteorder", + "lazy_static", + "libm", "num-integer", + "num-iter", "num-traits", + "rand", + "smallvec", + "zeroize", ] [[package]] @@ -1233,6 +1478,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -1240,6 +1496,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1267,19 +1524,13 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - [[package]] name = "openssh-keys" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c75a0ec2d1b302412fb503224289325fcc0e44600176864804c7211b055cfd58" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "byteorder", "md-5", "sha2", @@ -1288,11 +1539,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.62" +version = "0.10.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cfg-if", "foreign-types", "libc", @@ -1309,7 +1560,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1320,9 +1571,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.98" +version = "0.9.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" dependencies = [ "cc", "libc", @@ -1370,6 +1621,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pathdiff" version = "0.2.1" @@ -1385,6 +1642,15 @@ dependencies = [ "digest", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -1408,7 +1674,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -1424,23 +1690,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] -name = "pkg-config" -version = "0.3.28" +name = "pkcs1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] [[package]] -name = "polyval" -version = "0.6.1" +name = "pkcs8" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", + "der", + "spki", ] +[[package]] +name = "pkg-config" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1455,9 +1730,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.74" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2de98502f212cfcea8d0bb305bd0f49d7ebdd75b64ba0a68f937d888f4e0d6db" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -1524,13 +1799,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", + "regex-automata 0.4.4", "regex-syntax 0.8.2", ] @@ -1545,9 +1820,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" dependencies = [ "aho-corasick", "memchr", @@ -1572,15 +1847,15 @@ version = "0.11.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", - "h2", - "http", - "http-body", - "hyper", + "h2 0.3.24", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.28", "hyper-tls", "ipnet", "js-sys", @@ -1643,6 +1918,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1664,7 +1959,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.20", + "semver 1.0.21", ] [[package]] @@ -1679,11 +1974,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", @@ -1708,7 +2003,7 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", ] [[package]] @@ -1792,9 +2087,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" [[package]] name = "semver-parser" @@ -1804,9 +2099,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "sentry" -version = "0.31.8" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce4b57f1b521f674df7a1d200be8ff5d74e3712020ee25b553146657b5377d5" +checksum = "ab18211f62fb890f27c9bb04861f76e4be35e4c2fcbfc2d98afa37aadebb16f1" dependencies = [ "httpdate", "native-tls", @@ -1823,9 +2118,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.31.8" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cc8d4e04a73de8f718dc703943666d03f25d3e9e4d0fb271ca0b8c76dfa00e" +checksum = "cf018ff7d5ce5b23165a9cbfee60b270a55ae219bc9eebef2a3b6039356dd7e5" dependencies = [ "backtrace", "once_cell", @@ -1835,9 +2130,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.31.8" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6436c1bad22cdeb02179ea8ef116ffc217797c028927def303bc593d9320c0d1" +checksum = "1d934df6f9a17b8c15b829860d9d6d39e78126b5b970b365ccbd817bc0fe82c9" dependencies = [ "hostname", "libc", @@ -1849,9 +2144,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.31.8" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901f761681f97db3db836ef9e094acdd8756c40215326c194201941947164ef1" +checksum = "5e362d3fb1c5de5124bf1681086eaca7adf6a8c4283a7e1545359c729f9128ff" dependencies = [ "once_cell", "rand", @@ -1862,9 +2157,9 @@ dependencies = [ [[package]] name = "sentry-debug-images" -version = "0.31.8" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdb263e73d22f39946f6022ed455b7561b22ff5553aca9be3c6a047fa39c328" +checksum = "d8bca420d75d9e7a8e54a4806bf4fa8a7e9a804e8f2ff05c7c80234168c6ca66" dependencies = [ "findshlibs", "once_cell", @@ -1873,9 +2168,9 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.31.8" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74fbf1c163f8b6a9d05912e1b272afa27c652e8b47ea60cb9a57ad5e481eea99" +checksum = "e0224e7a8e2bd8a32d96804acb8243d6d6e073fed55618afbdabae8249a964d8" dependencies = [ "sentry-backtrace", "sentry-core", @@ -1883,9 +2178,9 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.31.8" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82eabcab0a047040befd44599a1da73d3adb228ff53b5ed9795ae04535577704" +checksum = "087bed8c616d176a9c6b662a8155e5f23b40dc9e1fa96d0bd5fb56e8636a9275" dependencies = [ "sentry-backtrace", "sentry-core", @@ -1895,9 +2190,9 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.31.8" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da956cca56e0101998c8688bc65ce1a96f00673a0e58e663664023d4c7911e82" +checksum = "fb4f0e37945b7a8ce7faebc310af92442e2d7c5aa7ef5b42fe6daa98ee133f65" dependencies = [ "debugid", "hex", @@ -1912,38 +2207,38 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b114498256798c94a0689e1a15fec6005dee8ac1f41de56404b67afc2a4b773" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb1879ea93538b78549031e2d54da3e901fd7e75f2e4dc758d760937b123d10" +checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.194" +version = "1.0.195" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3385e45322e8f9931410f01b3031ec534c3947d0e94c18049af4d9f9907d4e0" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] name = "serde_json" -version = "1.0.110" +version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fbd975230bada99c8bb618e0c365c2eefa219158d5c6c29610fd09ff1833257" +checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ "indexmap 2.1.0", "itoa", @@ -1953,9 +2248,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" dependencies = [ "itoa", "serde", @@ -1997,9 +2292,9 @@ dependencies = [ [[package]] name = "serial_test" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +checksum = "953ad9342b3aaca7cb43c45c097dd008d4907070394bd0751a0aa8817e5a018d" dependencies = [ "dashmap", "futures", @@ -2011,13 +2306,13 @@ dependencies = [ [[package]] name = "serial_test_derive" -version = "2.0.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2031,6 +2326,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.8" @@ -2060,6 +2366,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "slab" version = "0.4.9" @@ -2071,9 +2387,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" @@ -2106,6 +2422,238 @@ name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +dependencies = [ + "ahash", + "atoi", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.1.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlformat", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", + "webpki-roots", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +dependencies = [ + "atomic-write-file", + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.4.2", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.4.2", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "tracing", + "url", + "urlencoding", + "uuid", +] [[package]] name = "stringprep" @@ -2143,9 +2691,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.46" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89456b690ff72fddcecf231caedbe615c59480c93358a93dfae7fc29e3ebbf0e" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -2221,7 +2769,7 @@ checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2304,7 +2852,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2339,6 +2887,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -2411,7 +2970,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] @@ -2532,9 +3091,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -2552,14 +3111,16 @@ dependencies = [ ] [[package]] -name = "universal-hash" -version = "0.5.1" +name = "unicode-segmentation" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "untrusted" @@ -2579,7 +3140,7 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "log", "native-tls", "once_cell", @@ -2598,11 +3159,17 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "uuid" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom", "serde", @@ -2643,9 +3210,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2653,24 +3220,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.39" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -2680,9 +3247,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2690,28 +3257,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.89" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "web-sys" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -2723,6 +3290,12 @@ version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" + [[package]] name = "widestring" version = "1.0.2" @@ -2928,7 +3501,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.46", + "syn 2.0.48", ] [[package]] diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 58f3fd7..1af3c5d 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" # JWT biscuit = "0.6.0" # Database -mongodb = { version = ">= 2.7.0", features = ["openssl-tls"] } +mongodb = { version = ">= 2.7.0", features = ["openssl-tls"], optional = true } # Serialization serde = { version = "> 1.0.184", features = ["derive"] } serde_json = "1" @@ -20,15 +20,8 @@ serde_json = "1" anyhow = "1" # Time handling chrono = { version = "0.4.26", features = ["serde", "clock", "std"], default-features = false } -hex = "0.4.3" # Encryption and hashing -aes = "0.8.3" -aes-gcm-siv = "0.11.1" -hkdf = "0.12.3" -sha2 = "0.10.7" ring = "0.16.20" -# Fixed size arrays -generic-array = "0.14.7" # Config reader config = { version = "0.13.3", default-features = false, features = ["toml"] } # Logging/Tracing @@ -39,9 +32,9 @@ rand = "0.8.5" # lazy initialization of static variables once_cell = "1.18.0" # Base64 encoding -base64 = "0.21.2" +base64 = "0.21.7 " # UUID generation -uuid = { version = "1.4.1", features = ["serde"] } +uuid = { version = "1", features = ["serde", "v4"] } # Big integer handling (RSA key modulus and exponent) num-bigint = "0.4.3" # Generating fingerprint of RSA keys @@ -49,7 +42,7 @@ openssh-keys = "0.6.2" # Async runtime tokio = { version = ">= 1.32.0", features = ["macros", "rt-multi-thread", "signal"] } # HTTP server -axum = { version = "0.6.20", features = ["json", "http2"] } +axum = { version = "0.7.4", features = ["json", "http2"] } # Helper to allow defining traits for async functions async-trait = "0.1.73" # Helper for working with futures @@ -57,18 +50,19 @@ futures = "0.3.29" # Helper for creating custom error types thiserror = "1.0.48" # Optional: Sentry integration -sentry = { version = "0.31.7", optional = true } +sentry = { version = "0.32.1", optional = true } +sqlx = { version = "0.7.3", features = ["runtime-tokio-rustls", "postgres", "chrono", "uuid"], optional = true } [dev-dependencies] # Controlling execution of unit test cases, which could interfere with each other -serial_test = "2.0.0" +serial_test = "3" # Tempfile creation for testing tempfile = "3.8" tower = { version = "0.4", features = ["util"] } -hyper = { version = "0.14.27", features = ["full"] } +hyper = { version = "1", features = ["full"] } [features] -default = [] -# Enables the doc_type API -doc_type = [] +default = ["postgres"] sentry = ["dep:sentry"] +mongodb = ["dep:mongodb"] +postgres = ["dep:sqlx"] diff --git a/clearing-house-app/build.rs b/clearing-house-app/build.rs new file mode 100644 index 0000000..d506869 --- /dev/null +++ b/clearing-house-app/build.rs @@ -0,0 +1,5 @@ +// generated by `sqlx migrate build-script` +fn main() { + // trigger recompilation when a new migration is added + println!("cargo:rerun-if-changed=migrations"); +} diff --git a/clearing-house-app/config.toml b/clearing-house-app/config.toml index 62a4c3d..c61e1b9 100644 --- a/clearing-house-app/config.toml +++ b/clearing-house-app/config.toml @@ -1,7 +1,5 @@ log_level = "INFO" # TRACE, DEBUG, INFO, WARN, ERROR -document_database_url= "mongodb://localhost:27017" -process_database_url= "mongodb://localhost:27017" -keyring_database_url= "mongodb://localhost:27017" +database_url= "postgres://my_user:my_password@localhost:5432/ch" clear_db = true signing_key = "keys/private_key.der" # Optional performance_tracing = false diff --git a/clearing-house-app/migrations/20240102094054_init.down.sql b/clearing-house-app/migrations/20240102094054_init.down.sql new file mode 100644 index 0000000..1d2918c --- /dev/null +++ b/clearing-house-app/migrations/20240102094054_init.down.sql @@ -0,0 +1,7 @@ +-- Add down migration script here +DROP TABLE IF EXISTS documents; +DROP TABLE IF EXISTS process_owners; +DROP INDEX IF EXISTS client_id_idx; +DROP TABLE IF EXISTS clients; +DROP INDEX IF EXISTS process_id_idx; +DROP TABLE IF EXISTS processes; \ No newline at end of file diff --git a/clearing-house-app/migrations/20240102094054_init.up.sql b/clearing-house-app/migrations/20240102094054_init.up.sql new file mode 100644 index 0000000..7aa6982 --- /dev/null +++ b/clearing-house-app/migrations/20240102094054_init.up.sql @@ -0,0 +1,44 @@ +-- Add up migration script here +CREATE TABLE processes +( + id SERIAL PRIMARY KEY, + process_id VARCHAR UNIQUE NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_processes_process_id ON processes (process_id); + +CREATE TABLE clients +( + id SERIAL PRIMARY KEY, + client_id VARCHAR UNIQUE NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_clients_client_id ON clients (client_id); + +CREATE TABLE process_owners +( + process_id INTEGER NOT NULL REFERENCES processes (id), + client_id INTEGER NOT NULL REFERENCES clients (id), + PRIMARY KEY (process_id, client_id) +); + +CREATE TABLE documents +( + id UUID PRIMARY KEY, + process_id INTEGER NOT NULL REFERENCES processes (id), + created_at TIMESTAMP NOT NULL, + model_version VARCHAR NOT NULL, + correlation_message VARCHAR, + transfer_contract VARCHAR, + issued JSONB, + issuer_connector JSONB NOT NULL, + content_version VARCHAR, + recipient_connector JSONB, + sender_agent VARCHAR, + recipient_agent JSONB, + payload BYTEA, + payload_type VARCHAR, + message_id VARCHAR +); diff --git a/clearing-house-app/src/config.rs b/clearing-house-app/src/config.rs index 0074ead..b1bcaf8 100644 --- a/clearing-house-app/src/config.rs +++ b/clearing-house-app/src/config.rs @@ -1,9 +1,7 @@ /// Represents the configuration for the application #[derive(Debug, serde::Deserialize)] pub(crate) struct CHConfig { - pub(crate) process_database_url: String, - pub(crate) keyring_database_url: String, - pub(crate) document_database_url: String, + pub(crate) database_url: String, pub(crate) clear_db: bool, #[serde(default)] pub(crate) log_level: Option, @@ -101,23 +99,17 @@ mod test { #[test] #[serial] fn test_read_config_from_env() { - std::env::set_var("CH_APP_PROCESS_DATABASE_URL", "mongodb://localhost:27117"); - std::env::set_var("CH_APP_KEYRING_DATABASE_URL", "mongodb://localhost:27118"); - std::env::set_var("CH_APP_DOCUMENT_DATABASE_URL", "mongodb://localhost:27119"); + std::env::set_var("CH_APP_DATABASE_URL", "mongodb://localhost:27117"); std::env::set_var("CH_APP_CLEAR_DB", "true"); std::env::set_var("CH_APP_LOG_LEVEL", "INFO"); let conf = super::read_config(None); - assert_eq!(conf.process_database_url, "mongodb://localhost:27117"); - assert_eq!(conf.keyring_database_url, "mongodb://localhost:27118"); - assert_eq!(conf.document_database_url, "mongodb://localhost:27119"); + assert_eq!(conf.database_url, "mongodb://localhost:27117"); assert!(conf.clear_db); assert_eq!(conf.log_level, Some(super::LogLevel::Info)); // Cleanup - std::env::remove_var("CH_APP_PROCESS_DATABASE_URL"); - std::env::remove_var("CH_APP_KEYRING_DATABASE_URL"); - std::env::remove_var("CH_APP_DOCUMENT_DATABASE_URL"); + std::env::remove_var("CH_APP_DATABASE_URL"); std::env::remove_var("CH_APP_CLEAR_DB"); std::env::remove_var("CH_APP_LOG_LEVEL"); } @@ -130,9 +122,7 @@ mod test { let file = tempfile::Builder::new().suffix(".toml").tempfile().unwrap(); // Write config to file - let toml = r#"process_database_url = "mongodb://localhost:27019" -keyring_database_url = "mongodb://localhost:27020" -document_database_url = "mongodb://localhost:27017" + let toml = r#"database_url = "mongodb://localhost:27019" clear_db = true log_level = "ERROR" "#; @@ -144,9 +134,7 @@ log_level = "ERROR" let conf = super::read_config(Some(file.path())); // Test - assert_eq!(conf.process_database_url, "mongodb://localhost:27019"); - assert_eq!(conf.keyring_database_url, "mongodb://localhost:27020"); - assert_eq!(conf.document_database_url, "mongodb://localhost:27017"); + assert_eq!(conf.database_url, "mongodb://localhost:27019"); assert!(conf.clear_db); assert_eq!(conf.log_level, Some(super::LogLevel::Error)); } diff --git a/clearing-house-app/src/crypto.rs b/clearing-house-app/src/crypto.rs deleted file mode 100644 index 4024ba5..0000000 --- a/clearing-house-app/src/crypto.rs +++ /dev/null @@ -1,257 +0,0 @@ -use crate::model::crypto::MasterKey; -use crate::model::crypto::{KeyEntry, KeyMap}; -use crate::model::doc_type::DocumentType; -use aes_gcm_siv::aead::Aead; -use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; -use anyhow::anyhow; -use generic_array::GenericArray; -use hkdf::Hkdf; -use rand::{RngCore, SeedableRng}; -use sha2::Sha256; -use std::collections::HashMap; -use std::sync::Mutex; - -const EXP_KEY_SIZE: usize = 32; -const EXP_NONCE_SIZE: usize = 12; -const EXP_BUFF_SIZE: usize = 44; - -fn initialize_kdf() -> (String, Hkdf) { - let salt = generate_random_seed(); - let ikm = generate_random_seed(); - let (master_key, kdf) = Hkdf::::extract(Some(&salt), &ikm); - (hex::encode_upper(master_key), kdf) -} - -/// Generates a random seed with 256 bytes. -pub fn generate_random_seed() -> Vec { - // Init crypto RNG once lazy - static RNG: once_cell::sync::Lazy> = - once_cell::sync::Lazy::new(|| Mutex::new(rand::rngs::StdRng::from_entropy())); - // Create a buffer to fill with random bytes - let mut buf = [0u8; 256]; - - // Fill buffer with random bytes in a block, so the mutex is locked for a short time. - { - RNG.lock() - .expect("This mutex locking is fine, because it will be released immediately after use and this is the only place of usage. So no deadlock possible.") - .fill_bytes(&mut buf); - } - - buf.to_vec() -} - -fn derive_key_map(kdf: Hkdf, dt: DocumentType, enc: bool) -> HashMap { - let mut key_map = HashMap::new(); - let mut i = 0; - dt.parts.iter().for_each(|p| { - let mut okm = [0u8; EXP_BUFF_SIZE]; - if kdf.expand(p.name.clone().as_bytes(), &mut okm).is_ok() { - let map_key = match enc { - true => p.name.clone(), - false => i.to_string(), - }; - key_map.insert( - map_key, - KeyEntry::new( - i.to_string(), - okm[..EXP_KEY_SIZE].to_vec(), - okm[EXP_KEY_SIZE..].to_vec(), - ), - ); - } - i += 1; - }); - key_map -} - -pub fn generate_key_map(mkey: MasterKey, dt: DocumentType) -> anyhow::Result { - debug!("generating encryption key_map for doc type: '{}'", &dt.id); - let (secret, doc_kdf) = initialize_kdf(); - let key_map = derive_key_map(doc_kdf, dt, true); - - debug!("encrypting the key seed"); - let msk_kdf = restore_kdf(&mkey.key)?; - let mut okm = [0u8; EXP_BUFF_SIZE]; - msk_kdf - .expand(hex::decode(mkey.salt)?.as_slice(), &mut okm) - .map_err(|_| anyhow!("Error while generating key"))?; - - match encrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], secret) { - Ok(ct) => Ok(KeyMap::new(key_map, Some(ct))), - Err(e) => { - error!("Error while encrypting key seed: {:?}", e); - Err(anyhow!("Error while encrypting key seed!")) - } - } -} - -pub fn restore_key_map( - mkey: MasterKey, - dt: DocumentType, - keys_ct: Vec, -) -> anyhow::Result { - debug!("decrypting the key seed"); - let kdf = restore_kdf(&mkey.key)?; - let mut okm = [0u8; EXP_BUFF_SIZE]; - if kdf - .expand(hex::decode(mkey.salt)?.as_slice(), &mut okm) - .is_err() - { - return Err(anyhow!("Error while generating key")); - } - - match decrypt_secret(&okm[..EXP_KEY_SIZE], &okm[EXP_KEY_SIZE..], &keys_ct) { - Ok(key_seed) => { - // generate new random key map - let mut km = restore_keys(&key_seed, dt)?; - km.keys_enc = Some(keys_ct); - Ok(km) - } - Err(e) => { - error!("Error while decrypting key ciphertext: {}", e); - Err(anyhow!("Error while decrypting keys")) - } - } -} - -pub fn restore_keys(secret: &String, dt: DocumentType) -> anyhow::Result { - debug!("restoring decryption key_map for doc type: '{}'", &dt.id); - let kdf = restore_kdf(secret)?; - let key_map = derive_key_map(kdf, dt, false); - - Ok(KeyMap::new(key_map, None)) -} - -fn restore_kdf(secret: &String) -> anyhow::Result> { - debug!("restoring kdf from secret"); - let prk = hex::decode(secret).map_err(|e| { - error!("Error while decoding master key: {}", e); - anyhow!("Error while encrypting key seed!") - })?; - - match Hkdf::::from_prk(prk.as_slice()) { - Ok(kdf) => Ok(kdf), - Err(e) => { - error!("Error while instantiating hkdf: {}", e); - Err(anyhow!("Error while encrypting key seed!")) - } - } -} - -pub fn encrypt_secret(key: &[u8], nonce: &[u8], secret: String) -> anyhow::Result> { - // check key size - if key.len() != EXP_KEY_SIZE { - error!( - "Given key has size {} but expected {} bytes", - key.len(), - EXP_KEY_SIZE - ); - Err(anyhow!("Incorrect key size")) - } - // check nonce size - else if nonce.len() != EXP_NONCE_SIZE { - error!( - "Given nonce has size {} but expected {} bytes", - nonce.len(), - EXP_NONCE_SIZE - ); - Err(anyhow!("Incorrect nonce size")) - } else { - let key = GenericArray::from_slice(key); - let nonce = GenericArray::from_slice(nonce); - let cipher = Aes256GcmSiv::new(key); - - match cipher.encrypt(nonce, secret.as_bytes()) { - Ok(ct) => Ok(ct), - Err(e) => Err(anyhow!("Error while encrypting {}", e)), - } - } -} - -pub fn decrypt_secret(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result { - debug!("key len = {}", key.len()); - debug!("ct len = {}", ct.len()); - let key = GenericArray::from_slice(key); - let nonce = GenericArray::from_slice(nonce); - let cipher = Aes256GcmSiv::new(key); - - debug!("key: {}", hex::encode_upper(key)); - debug!("nonce: {}", hex::encode_upper(nonce)); - - debug!("ct len = {}", ct.len()); - debug!("nonce len = {}", nonce.len()); - match cipher.decrypt(nonce, ct) { - Ok(pt) => { - let pt = String::from_utf8(pt)?; - Ok(pt) - } - Err(e) => Err(anyhow!("Error while decrypting: {}", e)), - } -} - -#[cfg(test)] -mod test { - use crate::model::crypto::MasterKey; - use crate::model::doc_type::{DocumentType, DocumentTypePart}; - - #[test] - fn test_generate_random_seed() { - for _ in 1..100 { - let seed = super::generate_random_seed(); - // Check length of seed - assert_eq!(256, seed.len()); - // Check that the seed is not all zeros - assert_ne!(0, seed.iter().map(|b| *b as usize).sum::()); - } - } - - #[test] - fn encrypt_decrypt_secret() { - let seed = super::generate_random_seed(); - let key = &seed[..super::EXP_KEY_SIZE]; - let nonce = &seed[..super::EXP_NONCE_SIZE]; - let secret = "This is a secret".to_string(); - - let ct = super::encrypt_secret(key, nonce, secret.clone()).expect("Encryption failed"); - let pt = super::decrypt_secret(key, nonce, &ct).expect("Decryption failed"); - - assert_eq!(secret, pt); - } - - #[test] - fn restore_kdf() { - let salt = "abcdefghijklmnopqrstuvwx"; - let (secret, kdf) = super::initialize_kdf(); - let restored_kdf = super::restore_kdf(&secret).expect("kdf restoration failed"); - - let mut okm = [0u8; super::EXP_BUFF_SIZE]; - let mut restored_okm = [0u8; super::EXP_BUFF_SIZE]; - kdf.expand(salt.as_bytes(), &mut okm) - .expect("kdf expansion failed"); - restored_kdf - .expand(salt.as_bytes(), &mut restored_okm) - .expect("restored_kdf expansion failed"); - assert_eq!(restored_okm, okm); - } - - #[test] - fn restore_key_map() { - let msk: MasterKey = MasterKey::new_random(); - let dt: DocumentType = DocumentType::new( - "test".to_string(), - "hello_world".to_string(), - vec![ - DocumentTypePart::new("0".to_string()), - DocumentTypePart::new("1".to_string()), - DocumentTypePart::new("2".to_string()), - ], - ); - - let key_map = - super::generate_key_map(msk.clone(), dt.clone()).expect("key_map generation failed"); - let restored_key_map = super::restore_key_map(msk, dt, key_map.clone().keys_enc.unwrap()) - .expect("key_map restoration failed"); - - assert_eq!(key_map, restored_key_map); - } -} diff --git a/clearing-house-app/src/db/key_store.rs b/clearing-house-app/src/db/key_store.rs deleted file mode 100644 index 335011c..0000000 --- a/clearing-house-app/src/db/key_store.rs +++ /dev/null @@ -1,286 +0,0 @@ -use super::DataStoreApi; -use crate::db::init_database_client; -#[cfg(doc_type)] -use crate::model::constants::MONGO_PID; -use crate::model::constants::{ - FILE_DEFAULT_DOC_TYPE, KEYRING_DB, KEYRING_DB_CLIENT, MONGO_COLL_DOC_TYPES, - MONGO_COLL_MASTER_KEY, MONGO_ID, -}; -use crate::model::crypto::MasterKey; -use crate::model::doc_type::DocumentType; -use anyhow::anyhow; -use futures::TryStreamExt; -use mongodb::bson::doc; -use std::process::exit; - -#[derive(Clone, Debug)] -pub struct KeyStore { - pub(crate) client: mongodb::Client, - database: mongodb::Database, -} - -impl DataStoreApi for KeyStore { - fn new(client: mongodb::Client) -> KeyStore { - KeyStore { - client: client.clone(), - database: client.database(KEYRING_DB), - } - } -} - -impl KeyStore { - pub async fn init_keystore(db_url: &str, clear_db: bool) -> anyhow::Result { - debug!("Using database url: '{:#?}'", &db_url); - - match init_database_client::(db_url, Some(KEYRING_DB_CLIENT.to_string())).await { - Ok(keystore) => { - debug!("Check if database is empty..."); - match keystore - .client - .database(KEYRING_DB) - .list_collection_names(None) - .await - { - Ok(colls) => { - debug!("... found collections: {:#?}", &colls); - if !colls.is_empty() && clear_db { - debug!("Database not empty and clear_db == true. Dropping database..."); - match keystore.client.database(KEYRING_DB).drop(None).await { - Ok(_) => { - debug!("... done."); - } - Err(_) => { - debug!("... failed."); - return Err(anyhow!("Failed to drop database")); - } - }; - } - if colls.is_empty() || clear_db { - debug!("Database empty. Need to initialize..."); - debug!("Adding initial document type..."); - match serde_json::from_str::( - &crate::util::read_file(FILE_DEFAULT_DOC_TYPE).unwrap_or_default(), - ) { - Ok(dt) => match keystore.add_document_type(dt).await { - Ok(_) => { - debug!("... done."); - } - Err(e) => { - error!( - "Error while adding initial document type: {:#?}", - e - ); - return Err(anyhow!( - "Error while adding initial document type" - )); - } - }, - _ => { - error!("Error while loading initial document type"); - return Err(anyhow!( - "Error while loading initial document type" - )); - } - }; - debug!("Creating master key..."); - // create master key - match keystore.store_master_key(MasterKey::new_random()).await { - Ok(true) => { - debug!("... done."); - } - _ => { - error!("... failed to create master key"); - return Err(anyhow!("Failed to create master key")); - } - }; - } - debug!("... database initialized."); - Ok(keystore) - } - Err(_) => Err(anyhow!("Failed to list collections")), - } - } - Err(_) => Err(anyhow!("Failed to initialize database client")), - } - } - - /// Only one master key may exist in the database. - #[tracing::instrument(skip_all)] - pub async fn store_master_key(&self, key: MasterKey) -> anyhow::Result { - tracing::debug!("Storing new master key..."); - let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); - tracing::debug!("... but first check if there's already one."); - let result = coll - .find(None, None) - .await - .expect("Error retrieving the master keys") - .try_collect() - .await - .unwrap_or_else(|_| vec![]); - - if result.len() > 1 { - tracing::error!("Master Key table corrupted!"); - exit(1); - } - if result.len() == 1 { - tracing::error!("Master key already exists!"); - Ok(false) - } else { - //let db_key = bson::to_bson(&key) - // .expect("failed to serialize master key for database"); - match coll.insert_one(key, None).await { - Ok(_r) => Ok(true), - Err(e) => { - tracing::error!("master key could not be stored: {:?}", &e); - panic!("master key could not be stored") - } - } - } - } - - /// Only one master key may exist in the database. - #[tracing::instrument(skip_all)] - pub async fn get_msk(&self) -> anyhow::Result { - let coll = self.database.collection::(MONGO_COLL_MASTER_KEY); - let result = coll - .find(None, None) - .await - .expect("Error retrieving the master keys") - .try_collect() - .await - .unwrap_or_else(|_| vec![]); - - if result.len() > 1 { - tracing::error!("Master Key table corrupted!"); - exit(1); - } - if result.len() == 1 { - Ok(result[0].clone()) - } else { - tracing::error!("Master Key missing!"); - exit(1); - } - } - - // DOCTYPE - pub async fn add_document_type(&self, doc_type: DocumentType) -> anyhow::Result<()> { - let coll = self - .database - .collection::(MONGO_COLL_DOC_TYPES); - match coll.insert_one(doc_type.clone(), None).await { - Ok(_r) => { - tracing::debug!("added new document type: {}", &_r.inserted_id); - Ok(()) - } - Err(e) => { - tracing::error!("failed to log document type {}", &doc_type.id); - Err(e.into()) - } - } - } - - //TODO: Do we need to check that no documents of this type exist before we remove it from the db? - #[cfg(doc_type)] - pub async fn delete_document_type(&self, id: &String, pid: &String) -> anyhow::Result { - let coll = self - .database - .collection::(MONGO_COLL_DOC_TYPES); - let result = coll - .delete_many(doc! { MONGO_ID: id, MONGO_PID: pid }, None) - .await?; - if result.deleted_count >= 1 { - Ok(true) - } else { - Ok(false) - } - } - - /// checks if the model exits - #[cfg(doc_type)] - pub async fn exists_document_type(&self, pid: &String, dt_id: &String) -> anyhow::Result { - let coll = self - .database - .collection::(MONGO_COLL_DOC_TYPES); - let result = coll - .find_one(Some(doc! { MONGO_ID: dt_id, MONGO_PID: pid }), None) - .await?; - match result { - Some(_r) => Ok(true), - None => { - tracing::debug!( - "document type with id {} and pid {:?} does not exist!", - &dt_id, - &pid - ); - Ok(false) - } - } - } - - #[cfg(doc_type)] - pub async fn get_all_document_types(&self) -> anyhow::Result> { - let coll = self - .database - .collection::(MONGO_COLL_DOC_TYPES); - let result = coll - .find(None, None) - .await? - .try_collect() - .await - .unwrap_or_default(); - Ok(result) - } - - pub async fn get_document_type(&self, dt_id: &String) -> anyhow::Result> { - let coll = self - .database - .collection::(MONGO_COLL_DOC_TYPES); - tracing::debug!("get_document_type for dt_id: '{}'", dt_id); - match coll.find_one(Some(doc! { MONGO_ID: dt_id}), None).await { - Ok(result) => Ok(result), - Err(e) => { - tracing::error!("error while getting document type with id {}!", dt_id); - Err(e.into()) - } - } - } - - #[cfg(doc_type)] - pub async fn update_document_type( - &self, - doc_type: DocumentType, - id: &String, - ) -> anyhow::Result { - let coll = self - .database - .collection::(MONGO_COLL_DOC_TYPES); - match coll.replace_one(doc! { MONGO_ID: id}, doc_type, None).await { - Ok(r) => { - if r.matched_count != 1 || r.modified_count != 1 { - tracing::warn!( - "while replacing doc type {} matched '{}' dts and modified '{}'", - id, - r.matched_count, - r.modified_count - ); - } else { - tracing::debug!( - "while replacing doc type {} matched '{}' dts and modified '{}'", - id, - r.matched_count, - r.modified_count - ); - } - Ok(true) - } - Err(e) => { - tracing::error!( - "error while updating document type with id {}: {:#?}", - id, - e - ); - Ok(false) - } - } - } -} diff --git a/clearing-house-app/src/db/mod.rs b/clearing-house-app/src/db/mod.rs index 055c68f..2e0beef 100644 --- a/clearing-house-app/src/db/mod.rs +++ b/clearing-house-app/src/db/mod.rs @@ -1,19 +1,23 @@ -pub(crate) mod doc_store; -pub(crate) mod key_store; -pub(crate) mod process_store; +#[cfg(feature = "mongodb")] +pub(crate) mod mongo_doc_store; +#[cfg(feature = "mongodb")] +pub(crate) mod mongo_process_store; +#[cfg(feature = "postgres")] +pub(crate) mod postgres_document_store; +#[cfg(feature = "postgres")] +pub(crate) mod postgres_process_store; -use mongodb::options::ClientOptions; -use mongodb::Client; +use crate::model::document::Document; +use crate::model::process::Process; +use crate::model::SortingOrder; -pub trait DataStoreApi { - fn new(client: Client) -> Self; -} - -pub async fn init_database_client( +#[cfg(feature = "mongodb")] +pub async fn init_database_client( db_url: &str, client_name: Option, -) -> anyhow::Result { - let mut client_options = match ClientOptions::parse(&db_url.to_string()).await { +) -> anyhow::Result { + let mut client_options = match mongodb::options::ClientOptions::parse(&db_url.to_string()).await + { Ok(co) => co, Err(_) => { anyhow::bail!("Can't parse database connection string"); @@ -21,6 +25,27 @@ pub async fn init_database_client( }; client_options.app_name = client_name; - let client = Client::with_options(client_options)?; - Ok(T::new(client)) + mongodb::Client::with_options(client_options).map_err(|e| e.into()) +} + +pub(crate) trait ProcessStore { + async fn get_processes(&self) -> anyhow::Result>; + async fn delete_process(&self, pid: &str) -> anyhow::Result; + async fn exists_process(&self, pid: &str) -> anyhow::Result; + async fn get_process(&self, pid: &str) -> anyhow::Result>; + async fn store_process(&self, process: Process) -> anyhow::Result<()>; +} + +pub(crate) trait DocumentStore { + async fn add_document(&self, doc: Document) -> anyhow::Result; + async fn exists_document(&self, id: &str) -> anyhow::Result; + async fn get_document(&self, id: &str, pid: &str) -> anyhow::Result>; + async fn get_documents_for_pid( + &self, + pid: &str, + page: u64, + size: u64, + sort: &SortingOrder, + date: (&chrono::NaiveDateTime, &chrono::NaiveDateTime), + ) -> anyhow::Result>; } diff --git a/clearing-house-app/src/db/doc_store.rs b/clearing-house-app/src/db/mongo_doc_store.rs similarity index 72% rename from clearing-house-app/src/db/doc_store.rs rename to clearing-house-app/src/db/mongo_doc_store.rs index 455c8a5..d6037b4 100644 --- a/clearing-house-app/src/db/doc_store.rs +++ b/clearing-house-app/src/db/mongo_doc_store.rs @@ -1,11 +1,10 @@ -use crate::db::doc_store::bucket::{restore_from_bucket, DocumentBucketSize, DocumentBucketUpdate}; -use crate::db::{init_database_client, DataStoreApi}; +use crate::db::init_database_client; +use crate::db::mongo_doc_store::bucket::DocumentBucketSize; use crate::model::constants::{ DOCUMENT_DB, DOCUMENT_DB_CLIENT, MAX_NUM_RESPONSE_ENTRIES, MONGO_COLL_DOCUMENT_BUCKET, - MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_DT_ID, MONGO_FROM_TS, MONGO_ID, MONGO_PID, MONGO_TC, - MONGO_TO_TS, MONGO_TS, + MONGO_COUNTER, MONGO_DOC_ARRAY, MONGO_FROM_TS, MONGO_ID, MONGO_PID, MONGO_TO_TS, MONGO_TS, }; -use crate::model::document::{Document, EncryptedDocument}; +use crate::model::document::Document; use crate::model::SortingOrder; use anyhow::anyhow; use futures::StreamExt; @@ -14,29 +13,23 @@ use mongodb::options::{AggregateOptions, CreateCollectionOptions, UpdateOptions, use mongodb::{bson, Client, IndexModel}; #[derive(Clone, Debug)] -pub struct DataStore { - pub(crate) client: mongodb::Client, +pub struct MongoDocumentStore { database: mongodb::Database, } -impl DataStoreApi for DataStore { - fn new(client: Client) -> DataStore { - DataStore { - client: client.clone(), +impl MongoDocumentStore { + fn new(client: Client) -> MongoDocumentStore { + MongoDocumentStore { database: client.database(DOCUMENT_DB), } } -} -impl DataStore { pub async fn init_datastore(db_url: &str, clear_db: bool) -> anyhow::Result { debug!("Using mongodb url: '{:#?}'", &db_url); - match init_database_client::(db_url, Some(DOCUMENT_DB_CLIENT.to_string())).await - { - Ok(datastore) => { + match init_database_client(db_url, Some(DOCUMENT_DB_CLIENT.to_string())).await { + Ok(docstore) => { debug!("Check if database is empty..."); - match datastore - .client + match docstore .database(DOCUMENT_DB) .list_collection_names(None) .await @@ -51,7 +44,7 @@ impl DataStore { if number_of_colls > 0 && clear_db { debug!("Database not empty and clear_db == true. Dropping database..."); - match datastore.client.database(DOCUMENT_DB).drop(None).await { + match docstore.database(DOCUMENT_DB).drop(None).await { Ok(_) => { debug!("... done."); } @@ -68,8 +61,7 @@ impl DataStore { let mut options = CreateCollectionOptions::default(); options.write_concern = Some(write_concern); debug!("Create collection {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore - .client + match docstore .database(DOCUMENT_DB) .create_collection(MONGO_COLL_DOCUMENT_BUCKET, options) .await @@ -112,8 +104,7 @@ impl DataStore { compound_index_model.keys = doc! {MONGO_PID: 1, MONGO_TS: 1}; debug!("Create unique index for {} ...", MONGO_COLL_DOCUMENT_BUCKET); - match datastore - .client + match docstore .database(DOCUMENT_DB) .collection::(MONGO_COLL_DOCUMENT_BUCKET) .create_index(compound_index_model, None) @@ -129,7 +120,7 @@ impl DataStore { } } debug!("... database initialized."); - Ok(datastore) + Ok(Self::new(docstore)) } Err(_) => Err(anyhow!("Failed to list collections")), } @@ -138,13 +129,110 @@ impl DataStore { } } + /// offset is necessary for duration queries. There, start_entries of bucket depend on timestamps which usually creates an offset in the bucket #[tracing::instrument(skip_all)] - pub async fn add_document(&self, doc: EncryptedDocument) -> anyhow::Result { + async fn get_start_bucket_size( + &self, + pid: &str, + page: u64, + size: u64, + sort: &SortingOrder, + (date_from, date_to): (&chrono::NaiveDateTime, &chrono::NaiveDateTime), + ) -> anyhow::Result { + debug!("...trying to get the offset for page {page} of size {size} of documents for pid {pid}..."); + let sort_order = match sort { + SortingOrder::Ascending => 1, + SortingOrder::Descending => -1, + }; + let coll = self + .database + .collection::(MONGO_COLL_DOCUMENT_BUCKET); + + debug!( + "... match with pid: {pid}, to_ts <= {}, from_ts >= {} ...", + date_to.timestamp(), + date_from.timestamp(), + ); + let pipeline = vec![ + doc! {"$match":{ + MONGO_PID: pid, + MONGO_FROM_TS: {"$lte": date_to.timestamp()}, + MONGO_TO_TS: {"$gte": date_from.timestamp()} + }}, + // sorting according to sorting order, so we get either the start or end + doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, + doc! {"$limit" : 1}, + // count all relevant documents in the target bucket + doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, + doc! {"$match":{ + format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TS): {"$lte": date_to.timestamp(), "$gte": date_from.timestamp()} + }}, + // modify result to return total number of docs in bucket and number of relevant docs in bucket + doc! { "$group": { "_id": {"total": "$counter"}, "size": { "$sum": 1 } } }, + doc! { "$project": {"_id":0, "capacity": "$_id.total", "size":true}}, + ]; + + let mut options = AggregateOptions::default(); + options.allow_disk_use = Some(true); + let mut results = coll.aggregate(pipeline, options).await?; + let mut bucket_size = DocumentBucketSize { + capacity: MAX_NUM_RESPONSE_ENTRIES as i32, + size: 0, + }; + while let Some(result) = results.next().await { + debug!("... retrieved: {:#?}", &result); + let result_bucket: DocumentBucketSize = bson::from_document(result?)?; + bucket_size = result_bucket; + } + debug!("... sending offset: {:?}", bucket_size); + Ok(bucket_size) + } + + #[tracing::instrument(skip_all)] + fn get_offset(bucket_size: &DocumentBucketSize) -> u64 { + (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES + } + + #[tracing::instrument(skip_all)] + fn get_start_bucket( + page: u64, + size: u64, + bucket_size: &DocumentBucketSize, + offset: u64, + ) -> u64 { + let docs_to_skip = + (page - 1) * size + offset + MAX_NUM_RESPONSE_ENTRIES - bucket_size.capacity as u64; + (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1 + } + + #[tracing::instrument(skip_all)] + fn get_start_entry( + page: u64, + size: u64, + start_bucket: u64, + bucket_size: &DocumentBucketSize, + offset: u64, + ) -> u64 { + // docs to skip calculated by page * size + let docs_to_skip = (page - 1) * size + offset; + let mut start_entry = 0; + if start_bucket > 1 { + start_entry = docs_to_skip - bucket_size.capacity as u64; + if start_entry > 2 { + start_entry -= (start_bucket - 2) * MAX_NUM_RESPONSE_ENTRIES + } + } + start_entry + } +} + +impl super::DocumentStore for MongoDocumentStore { + #[tracing::instrument(skip_all)] + async fn add_document(&self, doc: Document) -> anyhow::Result { debug!("add_document to bucket"); let coll = self .database - .collection::(MONGO_COLL_DOCUMENT_BUCKET); - let bucket_update = DocumentBucketUpdate::from(&doc); + .collection::(MONGO_COLL_DOCUMENT_BUCKET); let mut update_options = UpdateOptions::default(); update_options.upsert = Some(true); let id = format!("^{}_", doc.pid.clone()); @@ -158,11 +246,11 @@ impl DataStore { match coll.update_one(query, doc! { "$push": { - MONGO_DOC_ARRAY: mongodb::bson::to_bson(&bucket_update)?, + MONGO_DOC_ARRAY: mongodb::bson::to_bson(&doc)?, }, "$inc": {"counter": 1}, - "$setOnInsert": { "_id": format!("{}_{}_{}", doc.pid.clone(), doc.ts, crate::util::new_uuid()), MONGO_DT_ID: doc.dt_id.clone(), MONGO_FROM_TS: doc.ts}, - "$set": {MONGO_TO_TS: doc.ts}, + "$setOnInsert": { "_id": format!("{}_{}_{}", doc.pid.clone(), doc.ts.timestamp(), crate::util::new_uuid()), MONGO_FROM_TS: doc.ts.timestamp()}, + "$set": {MONGO_TO_TS: doc.ts.timestamp()}, }, update_options).await { Ok(_r) => { debug!("added new document: {:#?}", &_r.upserted_id); @@ -178,13 +266,13 @@ impl DataStore { /// checks if the document exists /// document ids are globally unique #[tracing::instrument(skip_all)] - pub async fn exists_document(&self, id: &String) -> anyhow::Result { + async fn exists_document(&self, id: &str) -> anyhow::Result { debug!("Check if document with id '{}' exists...", id); - let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.clone()}; + let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id}; let coll = self .database - .collection::(MONGO_COLL_DOCUMENT_BUCKET); + .collection::(MONGO_COLL_DOCUMENT_BUCKET); match coll.count_documents(Some(query), None).await? { 0 => { debug!("Document with id '{}' does not exist!", &id); @@ -199,15 +287,11 @@ impl DataStore { /// gets the model from the db #[tracing::instrument(skip_all)] - pub async fn get_document( - &self, - id: &str, - pid: &str, - ) -> anyhow::Result> { + async fn get_document(&self, id: &str, pid: &str) -> anyhow::Result> { debug!("Trying to get doc with id {}...", id); let coll = self .database - .collection::(MONGO_COLL_DOCUMENT_BUCKET); + .collection::(MONGO_COLL_DOCUMENT_BUCKET); let pipeline = vec![ doc! {"$match":{ @@ -215,7 +299,7 @@ impl DataStore { format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.to_owned(), }}, doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, + doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID)}}, doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, doc! {"$match":{ MONGO_ID: id.to_owned()}}, ]; @@ -223,82 +307,45 @@ impl DataStore { let mut results = coll.aggregate(pipeline, None).await?; if let Some(result) = results.next().await { - let doc: EncryptedDocument = bson::from_document(result?)?; + let doc: Document = bson::from_document(result?)?; return Ok(Some(doc)); } Ok(None) } - /// gets documents for a single process from the db - #[tracing::instrument(skip_all)] - pub async fn get_document_with_previous_tc( - &self, - tc: i64, - ) -> anyhow::Result> { - let previous_tc = tc - 1; - debug!("Trying to get document for tc {} ...", previous_tc); - if previous_tc < 0 { - info!("... not entry exists."); - Ok(None) - } else { - let coll = self - .database - .collection::(MONGO_COLL_DOCUMENT_BUCKET); - - let pipeline = vec![ - doc! {"$match":{ - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TC): previous_tc - }}, - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$addFields": {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_PID): format!("${}", MONGO_PID), format!("{}.{}", MONGO_DOC_ARRAY, MONGO_DT_ID): format!("${}", MONGO_DT_ID)}}, - doc! {"$replaceRoot": { "newRoot": format!("${}", MONGO_DOC_ARRAY)}}, - doc! {"$match":{ MONGO_TC: previous_tc}}, - ]; - - let mut results = coll.aggregate(pipeline, None).await?; - - if let Some(result) = results.next().await { - debug!("Found {:#?}", &result); - let doc: EncryptedDocument = bson::from_document(result?)?; - Ok(Some(doc)) - } else { - warn!("Document with tc {} not found!", previous_tc); - Ok(None) - } - } - } - /// gets a page of documents of a specific document type for a single process from the db defined by parameters page, size and sort #[tracing::instrument(skip_all)] - pub async fn get_documents_for_pid( + async fn get_documents_for_pid( &self, - dt_id: &String, - pid: &String, + pid: &str, page: u64, size: u64, sort: &SortingOrder, (date_from, date_to): (&chrono::NaiveDateTime, &chrono::NaiveDateTime), - ) -> anyhow::Result> { - debug!( - "...trying to get page {} of size {} of documents for pid {} of dt {}...", - pid, dt_id, page, size - ); + ) -> anyhow::Result> { + debug!("...trying to get page {page} of size {size} of documents for pid {pid}..."); match self - .get_start_bucket_size(dt_id, pid, page, size, sort, (date_from, date_to)) + .get_start_bucket_size(pid, page, size, sort, (date_from, date_to)) .await { Ok(bucket_size) => { - let offset = DataStore::get_offset(&bucket_size); - let start_bucket = DataStore::get_start_bucket(page, size, &bucket_size, offset); + let offset = MongoDocumentStore::get_offset(&bucket_size); + let start_bucket = + MongoDocumentStore::get_start_bucket(page, size, &bucket_size, offset); trace!( "...working with start_bucket {} and offset {} ...", start_bucket, offset ); - let start_entry = - DataStore::get_start_entry(page, size, start_bucket, &bucket_size, offset); + let start_entry = MongoDocumentStore::get_start_entry( + page, + size, + start_bucket, + &bucket_size, + offset, + ); trace!( "...working with start_entry {} in start_bucket {} and offset {} ...", @@ -315,8 +362,7 @@ impl DataStore { let pipeline = vec![ doc! {"$match":{ - MONGO_PID: pid.clone(), - MONGO_DT_ID: dt_id.clone(), + MONGO_PID: pid, MONGO_FROM_TS: {"$lte": date_to.timestamp()}, MONGO_TO_TS: {"$gte": date_from.timestamp()} }}, @@ -336,7 +382,7 @@ impl DataStore { let coll = self .database - .collection::(MONGO_COLL_DOCUMENT_BUCKET); + .collection::(MONGO_COLL_DOCUMENT_BUCKET); let mut options = AggregateOptions::default(); options.allow_disk_use = Some(true); @@ -344,8 +390,8 @@ impl DataStore { let mut docs = vec![]; while let Some(result) = results.next().await { - let doc: DocumentBucketUpdate = bson::from_document(result?)?; - docs.push(restore_from_bucket(pid.clone(), dt_id.clone(), doc)); + let doc: Document = bson::from_document(result?)?; + docs.push(doc); } Ok(docs) @@ -356,110 +402,10 @@ impl DataStore { } } } - - /// offset is necessary for duration queries. There, start_entries of bucket depend on timestamps which usually creates an offset in the bucket - #[tracing::instrument(skip_all)] - async fn get_start_bucket_size( - &self, - dt_id: &String, - pid: &String, - page: u64, - size: u64, - sort: &SortingOrder, - (date_from, date_to): (&chrono::NaiveDateTime, &chrono::NaiveDateTime), - ) -> anyhow::Result { - debug!("...trying to get the offset for page {} of size {} of documents for pid {} of dt {}...", pid, dt_id, page, size); - let sort_order = match sort { - SortingOrder::Ascending => 1, - SortingOrder::Descending => -1, - }; - let coll = self - .database - .collection::(MONGO_COLL_DOCUMENT_BUCKET); - - debug!( - "... match with pid: {}, dt_it: {}, to_ts <= {}, from_ts >= {} ...", - pid, - dt_id, - date_from.timestamp(), - date_to.timestamp() - ); - let pipeline = vec![ - doc! {"$match":{ - MONGO_PID: pid.clone(), - MONGO_DT_ID: dt_id.clone(), - MONGO_FROM_TS: {"$lte": date_to.timestamp()}, - MONGO_TO_TS: {"$gte": date_from.timestamp()} - }}, - // sorting according to sorting order, so we get either the start or end - doc! {"$sort" : {MONGO_FROM_TS: sort_order}}, - doc! {"$limit" : 1}, - // count all relevant documents in the target bucket - doc! {"$unwind": format!("${}", MONGO_DOC_ARRAY)}, - doc! {"$match":{ - format!("{}.{}", MONGO_DOC_ARRAY, MONGO_TS): {"$lte": date_to.timestamp(), "$gte": date_from.timestamp()} - }}, - // modify result to return total number of docs in bucket and number of relevant docs in bucket - doc! { "$group": { "_id": {"total": "$counter"}, "size": { "$sum": 1 } } }, - doc! { "$project": {"_id":0, "capacity": "$_id.total", "size":true}}, - ]; - - let mut options = AggregateOptions::default(); - options.allow_disk_use = Some(true); - let mut results = coll.aggregate(pipeline, options).await?; - let mut bucket_size = DocumentBucketSize { - capacity: MAX_NUM_RESPONSE_ENTRIES as i32, - size: 0, - }; - while let Some(result) = results.next().await { - debug!("... retrieved: {:#?}", &result); - let result_bucket: DocumentBucketSize = bson::from_document(result?)?; - bucket_size = result_bucket; - } - debug!("... sending offset: {:?}", bucket_size); - Ok(bucket_size) - } - - #[tracing::instrument(skip_all)] - fn get_offset(bucket_size: &DocumentBucketSize) -> u64 { - (bucket_size.capacity - bucket_size.size) as u64 % MAX_NUM_RESPONSE_ENTRIES - } - - #[tracing::instrument(skip_all)] - fn get_start_bucket( - page: u64, - size: u64, - bucket_size: &DocumentBucketSize, - offset: u64, - ) -> u64 { - let docs_to_skip = - (page - 1) * size + offset + MAX_NUM_RESPONSE_ENTRIES - bucket_size.capacity as u64; - (docs_to_skip / MAX_NUM_RESPONSE_ENTRIES) + 1 - } - - #[tracing::instrument(skip_all)] - fn get_start_entry( - page: u64, - size: u64, - start_bucket: u64, - bucket_size: &DocumentBucketSize, - offset: u64, - ) -> u64 { - // docs to skip calculated by page * size - let docs_to_skip = (page - 1) * size + offset; - let mut start_entry = 0; - if start_bucket > 1 { - start_entry = docs_to_skip - bucket_size.capacity as u64; - if start_entry > 2 { - start_entry -= (start_bucket - 2) * MAX_NUM_RESPONSE_ENTRIES - } - } - start_entry - } } mod bucket { - use super::EncryptedDocument; + use super::Document; #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DocumentBucket { @@ -468,7 +414,7 @@ mod bucket { pub dt_id: String, pub from_ts: i64, pub to_ts: i64, - pub documents: Vec, + pub documents: Vec, } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] @@ -484,30 +430,4 @@ mod bucket { pub keys_ct: String, pub cts: Vec, } - - impl From<&EncryptedDocument> for DocumentBucketUpdate { - fn from(doc: &EncryptedDocument) -> Self { - DocumentBucketUpdate { - id: doc.id.clone(), - ts: doc.ts, - keys_ct: doc.keys_ct.clone(), - cts: doc.cts.to_vec(), - } - } - } - - pub fn restore_from_bucket( - pid: String, - dt_id: String, - bucket_update: DocumentBucketUpdate, - ) -> EncryptedDocument { - EncryptedDocument { - id: bucket_update.id.clone(), - dt_id, - pid, - ts: bucket_update.ts, - keys_ct: bucket_update.keys_ct.clone(), - cts: bucket_update.cts.to_vec(), - } - } } diff --git a/clearing-house-app/src/db/process_store.rs b/clearing-house-app/src/db/mongo_process_store.rs similarity index 63% rename from clearing-house-app/src/db/process_store.rs rename to clearing-house-app/src/db/mongo_process_store.rs index bea5f43..1e355e9 100644 --- a/clearing-house-app/src/db/process_store.rs +++ b/clearing-house-app/src/db/mongo_process_store.rs @@ -1,44 +1,33 @@ -use crate::db::{init_database_client, DataStoreApi}; +use crate::db::init_database_client; use crate::model::constants::{ - MONGO_COLL_PROCESSES, MONGO_COLL_TRANSACTIONS, MONGO_ID, MONGO_TC, PROCESS_DB, - PROCESS_DB_CLIENT, + MONGO_COLL_PROCESSES, MONGO_COLL_TRANSACTIONS, MONGO_ID, PROCESS_DB, PROCESS_DB_CLIENT, }; use crate::model::process::Process; -use crate::model::process::TransactionCounter; use anyhow::anyhow; use futures::TryStreamExt; use mongodb::bson::doc; -use mongodb::options::{ - CreateCollectionOptions, FindOneAndUpdateOptions, UpdateModifications, WriteConcern, -}; +use mongodb::options::{CreateCollectionOptions, WriteConcern}; use mongodb::{Client, Database}; #[derive(Clone, Debug)] -pub struct ProcessStore { - pub(crate) client: Client, +pub struct MongoProcessStore { database: Database, } -impl DataStoreApi for ProcessStore { - fn new(client: Client) -> ProcessStore { - ProcessStore { - client: client.clone(), +impl MongoProcessStore { + fn new(client: Client) -> MongoProcessStore { + MongoProcessStore { database: client.database(PROCESS_DB), } } -} -impl ProcessStore { pub async fn init_process_store(db_url: &str, clear_db: bool) -> anyhow::Result { debug!("...using database url: '{:#?}'", &db_url); - match init_database_client::(db_url, Some(PROCESS_DB_CLIENT.to_string())) - .await - { + match init_database_client(db_url, Some(PROCESS_DB_CLIENT.to_string())).await { Ok(process_store) => { debug!("...check if database is empty..."); match process_store - .client .database(PROCESS_DB) .list_collection_names(None) .await @@ -49,7 +38,7 @@ impl ProcessStore { debug!( "...database not empty and clear_db == true. Dropping database..." ); - match process_store.client.database(PROCESS_DB).drop(None).await { + match process_store.database(PROCESS_DB).drop(None).await { Ok(_) => { debug!("... done."); } @@ -67,7 +56,6 @@ impl ProcessStore { options.write_concern = Some(write_concern); debug!("...create collection {} ...", MONGO_COLL_TRANSACTIONS); match process_store - .client .database(PROCESS_DB) .create_collection(MONGO_COLL_TRANSACTIONS, options) .await @@ -82,7 +70,7 @@ impl ProcessStore { }; } debug!("... database initialized."); - Ok(process_store) + Ok(Self::new(process_store)) } Err(_) => Err(anyhow!("Failed to list collections")), } @@ -90,35 +78,10 @@ impl ProcessStore { Err(_) => Err(anyhow!("Failed to initialize database client")), } } +} - #[tracing::instrument(skip_all)] - pub async fn get_transaction_counter(&self) -> anyhow::Result> { - debug!("Getting transaction counter..."); - let coll = self - .database - .collection::(MONGO_COLL_TRANSACTIONS); - match coll.find_one(None, None).await? { - Some(t) => Ok(Some(t.tc)), - None => Ok(Some(0)), - } - } - - #[tracing::instrument(skip_all)] - pub async fn increment_transaction_counter(&self) -> anyhow::Result> { - debug!("Getting transaction counter..."); - let coll = self - .database - .collection::(MONGO_COLL_TRANSACTIONS); - let mods = UpdateModifications::Document(doc! {"$inc": {MONGO_TC: 1 }}); - let mut opts = FindOneAndUpdateOptions::default(); - opts.upsert = Some(true); - match coll.find_one_and_update(doc! {}, mods, opts).await? { - Some(t) => Ok(Some(t.tc)), - None => Ok(Some(0)), - } - } - - pub async fn get_processes(&self) -> anyhow::Result> { +impl super::ProcessStore for MongoProcessStore { + async fn get_processes(&self) -> anyhow::Result> { debug!("Trying to get all processes..."); let coll = self.database.collection::(MONGO_COLL_PROCESSES); let result = coll @@ -130,7 +93,7 @@ impl ProcessStore { Ok(result) } - pub async fn delete_process(&self, pid: &String) -> anyhow::Result { + async fn delete_process(&self, pid: &str) -> anyhow::Result { debug!("Trying to delete process with pid '{}'...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); let result = coll.delete_one(doc! { MONGO_ID: pid }, None).await?; @@ -145,7 +108,7 @@ impl ProcessStore { /// checks if the id exits #[tracing::instrument(skip_all)] - pub async fn exists_process(&self, pid: &String) -> anyhow::Result { + async fn exists_process(&self, pid: &str) -> anyhow::Result { debug!("Check if process with pid '{}' exists...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); let result = coll.find_one(Some(doc! { MONGO_ID: pid }), None).await?; @@ -162,7 +125,7 @@ impl ProcessStore { } #[tracing::instrument(skip_all)] - pub async fn get_process(&self, pid: &String) -> anyhow::Result> { + async fn get_process(&self, pid: &str) -> anyhow::Result> { debug!("Trying to get process with id {}...", pid); let coll = self.database.collection::(MONGO_COLL_PROCESSES); match coll.find_one(Some(doc! { MONGO_ID: pid }), None).await { @@ -174,31 +137,9 @@ impl ProcessStore { } } - #[tracing::instrument(skip_all)] - pub async fn is_authorized(&self, user: &String, pid: &String) -> anyhow::Result { - debug!( - "checking if user '{}' is authorized to access '{}'", - user, pid - ); - match self.get_process(pid).await { - Ok(Some(process)) => { - let authorized = process.owners.iter().any(|o| { - trace!("found owner {}", o); - user.eq(o) - }); - Ok(authorized) - } - Ok(None) => { - trace!("didn't find process"); - Ok(false) - } - _ => Err(anyhow!("User '{}' could not be authorized", &user)), - } - } - /// store process in db #[tracing::instrument(skip_all)] - pub async fn store_process(&self, process: Process) -> anyhow::Result<()> { + async fn store_process(&self, process: Process) -> anyhow::Result<()> { debug!("Storing process with pid {:#?}...", &process.id); let coll = self.database.collection::(MONGO_COLL_PROCESSES); match coll.insert_one(process, None).await { diff --git a/clearing-house-app/src/db/postgres_document_store.rs b/clearing-house-app/src/db/postgres_document_store.rs new file mode 100644 index 0000000..2ee7b75 --- /dev/null +++ b/clearing-house-app/src/db/postgres_document_store.rs @@ -0,0 +1,192 @@ +use crate::model::document::Document; +use crate::model::ids::{InfoModelDateTime, InfoModelId}; +use crate::model::SortingOrder; + +pub(crate) struct PostgresDocumentStore { + db: sqlx::PgPool, +} + +impl PostgresDocumentStore { + pub(crate) async fn new(db: sqlx::PgPool, clear_db: bool) -> Self { + if clear_db { + info!("Clearing database 'documents'"); + sqlx::query("TRUNCATE documents") + .execute(&db) + .await + .expect("Clearing database 'documents' failed"); + } + + Self { db } + } +} + +impl super::DocumentStore for PostgresDocumentStore { + async fn add_document(&self, doc: Document) -> anyhow::Result { + let doc = DocumentRow::from(doc); + + sqlx::query( + r#"INSERT INTO documents + (id, process_id, created_at, model_version, correlation_message, + transfer_contract, issued, issuer_connector, content_version, recipient_connector, + sender_agent, recipient_agent, payload, payload_type, message_id) + VALUES + ($1, (SELECT id from processes where process_id = $2), $3, $4, $5, + $6, $7, $8, $9, $10, + $11, $12, $13, $14, $15)"#, + ) + .bind(doc.id) // 1 + .bind(doc.process_id) // 2 + .bind(doc.created_at) // 3 + .bind(doc.model_version) // 4 + .bind(doc.correlation_message) // 5 + .bind(doc.transfer_contract) // 6 + .bind(doc.issued) // 7 + .bind(doc.issuer_connector) // 8 + .bind(doc.content_version) // 9 + .bind(doc.recipient_connector) // 10 + .bind(doc.sender_agent) // 11 + .bind(doc.recipient_agent) // 12 + .bind(doc.payload) // 13 + .bind(doc.payload_type) // 14 + .bind(doc.message_id) // 15 + .execute(&self.db) + .await?; + + Ok(true) + } + + async fn exists_document(&self, id: &str) -> anyhow::Result { + sqlx::query("SELECT id FROM documents WHERE id = $1") + .bind(id) + .fetch_optional(&self.db) + .await + .map(|r| r.is_some()) + .map_err(|e| e.into()) + } + + async fn get_document(&self, id: &str, pid: &str) -> anyhow::Result> { + sqlx::query_as::<_, DocumentRow>( + r#"SELECT documents.id, processes.process_id, documents.created_at, model_version, correlation_message, + transfer_contract, issued, issuer_connector, content_version, recipient_connector, + sender_agent, recipient_agent, payload, payload_type, message_id + FROM documents + LEFT JOIN processes ON processes.id = documents.process_id + WHERE id = $1 AND processes.process_id = $2"#, + ) + .bind(id) + .bind(pid) + .fetch_optional(&self.db) + .await + .map(|r| r.map(DocumentRow::into)) + .map_err(|e| e.into()) + } + + async fn get_documents_for_pid( + &self, + pid: &str, + page: u64, + size: u64, + sort: &SortingOrder, + (date_from, date_to): (&chrono::NaiveDateTime, &chrono::NaiveDateTime), + ) -> anyhow::Result> { + let sort_order = match sort { + SortingOrder::Ascending => "ASC", + SortingOrder::Descending => "DESC", + }; + + sqlx::query_as::<_, DocumentRow>( + format!( + r#"SELECT documents.id, processes.process_id, documents.created_at, model_version, correlation_message, + transfer_contract, issued, issuer_connector, content_version, recipient_connector, + sender_agent, recipient_agent, payload, payload_type, message_id + FROM documents + LEFT JOIN processes ON processes.id = documents.process_id + WHERE processes.process_id = $1 AND documents.created_at BETWEEN $2 AND $3 + ORDER BY created_at {} + LIMIT $4 OFFSET $5"#, + sort_order + ) + .as_str(), + ) + .bind(pid) + .bind(date_from) + .bind(date_to) + .bind(size as i64) + .bind(((page - 1) * size) as i64) + .fetch_all(&self.db) + .await + .map(|r| r.into_iter().map(DocumentRow::into).collect()) + .map_err(|e| e.into()) + } +} + +#[derive(sqlx::FromRow)] +struct DocumentRow { + id: uuid::Uuid, + process_id: String, + created_at: chrono::NaiveDateTime, + model_version: String, + correlation_message: Option, + transfer_contract: Option, + issued: sqlx::types::Json, + issuer_connector: sqlx::types::Json, + content_version: Option, + recipient_connector: Option>>, + sender_agent: String, + recipient_agent: Option>>, + payload: Option>, + payload_type: Option, + message_id: Option, +} + +impl From for DocumentRow { + fn from(value: Document) -> Self { + use std::str::FromStr; + Self { + id: uuid::Uuid::from_str(&value.id).unwrap(), + process_id: value.pid, + created_at: value.ts.naive_utc(), + model_version: value.content.model_version, + correlation_message: value.content.correlation_message, + transfer_contract: value.content.transfer_contract, + issued: sqlx::types::Json(value.content.issued), + issuer_connector: sqlx::types::Json(value.content.issuer_connector), + content_version: value.content.content_version, + recipient_connector: value.content.recipient_connector.map(sqlx::types::Json), + sender_agent: value.content.sender_agent, + recipient_agent: value.content.recipient_agent.map(sqlx::types::Json), + payload: value.content.payload.map(|s| s.as_bytes().to_owned()), + payload_type: value.content.payload_type, + message_id: value.content.id, + } + } +} + +impl Into for DocumentRow { + fn into(self) -> Document { + use chrono::TimeZone; + + Document { + id: self.id.to_string(), + pid: self.process_id, + ts: chrono::Local.from_utc_datetime(&self.created_at), + content: crate::model::ids::message::IdsMessage { + model_version: self.model_version, + correlation_message: self.correlation_message, + transfer_contract: self.transfer_contract, + issued: self.issued.0, + issuer_connector: self.issuer_connector.0, + content_version: self.content_version, + recipient_connector: self.recipient_connector.map(|s| s.0), + sender_agent: self.sender_agent, + recipient_agent: self.recipient_agent.map(|s| s.0), + payload: self + .payload + .map(|s| String::from_utf8_lossy(s.as_ref()).to_string()), + payload_type: self.payload_type, + id: self.message_id, + ..Default::default() + }, + } + } +} diff --git a/clearing-house-app/src/db/postgres_process_store.rs b/clearing-house-app/src/db/postgres_process_store.rs new file mode 100644 index 0000000..4ed06fc --- /dev/null +++ b/clearing-house-app/src/db/postgres_process_store.rs @@ -0,0 +1,136 @@ +use crate::model::process::Process; +use sqlx::Row; + +pub(crate) struct PostgresProcessStore { + db: sqlx::PgPool, +} + +impl PostgresProcessStore { + pub(crate) async fn new(db: sqlx::PgPool, clear_db: bool) -> Self { + if clear_db { + info!("Clearing database 'process_owners', 'clients' and 'processes'"); + sqlx::query("TRUNCATE process_owners, clients, processes CASCADE") + .execute(&db) + .await + .expect("Clearing databases 'process_owners', 'clients' and 'processes' failed."); + } + + Self { db } + } +} + +impl super::ProcessStore for PostgresProcessStore { + async fn get_processes(&self) -> anyhow::Result> { + sqlx::query_as::<_, ProcessRow>( + r#"SELECT p.process_id, p.created_at, ARRAY_AGG(c.client_id) AS owners FROM processes p + LEFT JOIN process_owners po ON p.id = po.process_id + LEFT JOIN clients c ON po.client_id = c.id + GROUP BY p.process_id, p.created_at"#, + ) + .fetch_all(&self.db) + .await + .map(|r| r.into_iter().map(|p| p.into()).collect()) + .map_err(|e| e.into()) + } + + async fn delete_process(&self, pid: &str) -> anyhow::Result { + sqlx::query("DELETE FROM processes WHERE process_id = $1 CASCADE") + .bind(pid) + .execute(&self.db) + .await + .map(|r| r.rows_affected() == 1) + .map_err(|e| e.into()) + } + + async fn exists_process(&self, pid: &str) -> anyhow::Result { + sqlx::query("SELECT process_id FROM processes WHERE process_id = $1") + .bind(pid) + .fetch_optional(&self.db) + .await + .map(|r| r.is_some()) + .map_err(|e| e.into()) + } + + async fn get_process(&self, pid: &str) -> anyhow::Result> { + sqlx::query_as::<_, ProcessRow>( + r#"SELECT p.process_id, p.created_at, ARRAY_AGG(c.client_id) AS owners FROM processes p + LEFT JOIN process_owners po ON p.id = po.process_id + LEFT JOIN clients c ON po.client_id = c.id + WHERE p.process_id = $1 + GROUP BY p.process_id, p.created_at"#, + ) + .bind(pid) + .fetch_optional(&self.db) + .await + .map(|r| r.map(|p| p.into())) + .map_err(|e| e.into()) + } + + async fn store_process(&self, process: Process) -> anyhow::Result<()> { + let process = ProcessRow::from(process); + let mut tx = self.db.begin().await?; + + // Create a process + let process_row = + sqlx::query(r#"INSERT INTO processes (process_id) VALUES ($1) RETURNING id"#) + .bind(&process.process_id) + .fetch_one(&mut *tx) + .await?; + + let pid = process_row.get::("id"); + + for o in process.owners { + // Check if client exists + let client_row = sqlx::query(r#"SELECT id FROM clients WHERE client_id = $1"#) + .bind(&o) + .fetch_optional(&mut *tx) + .await?; + + // If not, create it + let client_row = match client_row { + Some(crow) => crow, + None => { + sqlx::query(r#"INSERT INTO clients (client_id) VALUES ($1) RETURNING id"#) + .bind(&o) + .fetch_one(&mut *tx) + .await? + } + }; + + // Get id of client + let oid = client_row.get::("id"); + + // Create process owner + sqlx::query(r#"INSERT INTO process_owners (process_id, client_id) VALUES ($1, $2)"#) + .bind(pid) + .bind(oid) + .execute(&mut *tx) + .await?; + } + + tx.commit().await?; + + Ok(()) + } +} + +#[derive(sqlx::FromRow, Debug)] +struct ProcessRow { + pub process_id: String, + pub owners: Vec, +} + +impl From for ProcessRow { + fn from(p: Process) -> Self { + Self { + process_id: p.id, + owners: p.owners, + } + } +} + +impl Into for ProcessRow { + fn into(self) -> Process { + Process::new(self.process_id, self.owners) + } +} diff --git a/clearing-house-app/src/lib.rs b/clearing-house-app/src/lib.rs index 1c4a77c..57d10d2 100644 --- a/clearing-house-app/src/lib.rs +++ b/clearing-house-app/src/lib.rs @@ -1,27 +1,35 @@ #[macro_use] extern crate tracing; -use std::sync::Arc; -use crate::db::doc_store::DataStore; -use crate::db::key_store::KeyStore; -use crate::db::process_store::ProcessStore; use crate::model::constants::ENV_LOGGING_SERVICE_ID; use crate::util::ServiceConfig; +use std::sync::Arc; mod config; -mod crypto; mod db; pub mod model; mod ports; mod services; pub mod util; +#[cfg(feature = "postgres")] +type PostgresLoggingService = services::logging_service::LoggingService< + db::postgres_process_store::PostgresProcessStore, + db::postgres_document_store::PostgresDocumentStore, +>; +#[cfg(feature = "mongodb")] +type MongoLoggingService = services::logging_service::LoggingService< + db::mongo_process_store::MongoProcessStore, + db::mongo_doc_store::MongoDocumentStore, +>; + /// Contains the application state #[derive(Clone)] -pub struct AppState { - #[cfg_attr(not(doc_type), allow(dead_code))] - pub keyring_service: Arc, - pub logging_service: Arc, +pub(crate) struct AppState { + #[cfg(feature = "postgres")] + pub logging_service: Arc, + #[cfg(feature = "mongodb")] + pub logging_service: Arc, pub service_config: Arc, pub signing_key_path: String, } @@ -29,28 +37,48 @@ pub struct AppState { impl AppState { /// Initialize the application state from config async fn init(conf: &config::CHConfig) -> anyhow::Result { + #[cfg(feature = "postgres")] + let pool = async { + info!("Connecting to database"); + let pool = sqlx::PgPool::connect(&conf.database_url).await.unwrap(); + + info!("Migrating database"); + sqlx::migrate!() + .run(&pool) + .await + .expect("Failed to migrate database!"); + + pool + } + .await; + trace!("Initializing Process store"); + #[cfg(feature = "mongodb")] + let process_store = db::mongo_process_store::MongoProcessStore::init_process_store( + &conf.database_url, + conf.clear_db, + ) + .await + .expect("Failure to initialize process store! Exiting..."); + #[cfg(feature = "postgres")] let process_store = - ProcessStore::init_process_store(&conf.process_database_url, conf.clear_db) - .await - .expect("Failure to initialize process store! Exiting..."); - trace!("Initializing Keyring store"); - let keyring_store = KeyStore::init_keystore(&conf.keyring_database_url, conf.clear_db) - .await - .expect("Failure to initialize keyring store! Exiting..."); + db::postgres_process_store::PostgresProcessStore::new(pool.clone(), conf.clear_db) + .await; + trace!("Initializing Document store"); - let doc_store = DataStore::init_datastore(&conf.document_database_url, conf.clear_db) - .await - .expect("Failure to initialize document store! Exiting..."); + #[cfg(feature = "mongodb")] + let doc_store = db::mongo_doc_store::MongoDocumentStore::init_datastore( + &conf.database_url, + conf.clear_db, + ) + .await + .expect("Failure to initialize document store! Exiting..."); + #[cfg(feature = "postgres")] + let doc_store = + db::postgres_document_store::PostgresDocumentStore::new(pool, conf.clear_db).await; trace!("Initializing services"); - let keyring_service = Arc::new(services::keyring_service::KeyringService::new( - keyring_store, - )); - let doc_service = Arc::new(services::document_service::DocumentService::new( - doc_store, - keyring_service.clone(), - )); + let doc_service = Arc::new(services::document_service::DocumentService::new(doc_store)); let logging_service = Arc::new(services::logging_service::LoggingService::new( process_store, doc_service.clone(), @@ -64,7 +92,6 @@ impl AppState { Ok(Self { signing_key_path: signing_key, service_config, - keyring_service, logging_service, }) } diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index 56bf783..2588197 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -1,7 +1,7 @@ #![forbid(unsafe_code)] #![warn(clippy::unwrap_used)] -use std::net::SocketAddr; +use tokio::net::TcpListener; /// Main function: Reading config, initializing application state, starting server #[tokio::main] @@ -16,10 +16,9 @@ async fn main() -> Result<(), anyhow::Error> { let app = clearing_house_app::app().await?; // Bind port and start server - let addr = SocketAddr::from(([0, 0, 0, 0], 8000)); - tracing::info!("Starting server: Listening on {}", addr); - Ok(axum::Server::bind(&addr) - .serve(app.into_make_service()) + let listener = TcpListener::bind("0.0.0.0:8000").await?; + tracing::info!("Starting server: Listening on 0.0.0.0:8000"); + Ok(axum::serve(listener, app.into_make_service()) .with_graceful_shutdown(clearing_house_app::util::shutdown_signal()) .await?) } diff --git a/clearing-house-app/src/model/crypto.rs b/clearing-house-app/src/model/crypto.rs deleted file mode 100644 index 0079362..0000000 --- a/clearing-house-app/src/model/crypto.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::crypto::generate_random_seed; -use crate::util::new_uuid; -use hkdf::Hkdf; -use sha2::Sha256; -use std::collections::HashMap; - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct MasterKey { - pub id: String, - pub key: String, - pub salt: String, -} - -impl MasterKey { - pub fn new(id: String, key: String, salt: String) -> MasterKey { - MasterKey { id, key, salt } - } - - pub fn new_random() -> MasterKey { - let key_salt = generate_random_seed(); - let ikm = generate_random_seed(); - let (master_key, _) = Hkdf::::extract(Some(&key_salt), &ikm); - - MasterKey::new( - new_uuid(), - hex::encode_upper(master_key), - hex::encode_upper(generate_random_seed()), - ) - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq)] -pub struct KeyEntry { - pub id: String, - pub key: Vec, - pub nonce: Vec, -} - -impl KeyEntry { - pub fn new(id: String, key: Vec, nonce: Vec) -> KeyEntry { - KeyEntry { id, key, nonce } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug, PartialEq)] -pub struct KeyMap { - pub keys: HashMap, - pub keys_enc: Option>, -} - -impl KeyMap { - pub fn new(keys: HashMap, keys_enc: Option>) -> KeyMap { - KeyMap { keys, keys_enc } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyCt { - pub id: String, - pub ct: String, -} - -impl KeyCt { - pub fn new(id: String, ct: String) -> KeyCt { - KeyCt { id, ct } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyCtList { - pub dt: String, - pub cts: Vec, -} - -impl KeyCtList { - pub fn new(dt: String, cts: Vec) -> KeyCtList { - KeyCtList { dt, cts } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct KeyMapListItem { - pub id: String, - pub map: KeyMap, -} - -impl KeyMapListItem { - pub fn new(id: String, map: KeyMap) -> KeyMapListItem { - KeyMapListItem { id, map } - } -} diff --git a/clearing-house-app/src/model/doc_type.rs b/clearing-house-app/src/model/doc_type.rs deleted file mode 100644 index af9235b..0000000 --- a/clearing-house-app/src/model/doc_type.rs +++ /dev/null @@ -1,25 +0,0 @@ -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct DocumentType { - pub id: String, - pub pid: String, - pub parts: Vec, -} - -impl DocumentType { - #[cfg(test)] - pub fn new(id: String, pid: String, parts: Vec) -> DocumentType { - DocumentType { id, pid, parts } - } -} - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct DocumentTypePart { - pub name: String, -} - -impl DocumentTypePart { - #[cfg(test)] - pub fn new(name: String) -> DocumentTypePart { - DocumentTypePart { name } - } -} diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index 77358ed..cd3dc1a 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -1,83 +1,20 @@ -use crate::model::constants::SPLIT_CT; -use crate::model::crypto::{KeyEntry, KeyMap}; use crate::util::new_uuid; -use aes_gcm_siv::aead::Aead; -use aes_gcm_siv::{Aes256GcmSiv, KeyInit}; use chrono::Local; -use generic_array::GenericArray; -use std::collections::HashMap; -use uuid::Uuid; - -#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] -pub struct DocumentPart { - pub name: String, - pub content: String, -} -impl DocumentPart { - pub fn new(name: String, content: String) -> DocumentPart { - DocumentPart { name, content } - } - - pub fn encrypt(&self, key: &[u8], nonce: &[u8]) -> anyhow::Result> { - const EXP_KEY_SIZE: usize = 32; - const EXP_NONCE_SIZE: usize = 12; - // check key size - if key.len() != EXP_KEY_SIZE { - error!( - "Given key has size {} but expected {} bytes", - key.len(), - EXP_KEY_SIZE - ); - anyhow::bail!("Incorrect key size") - } - // check nonce size - else if nonce.len() != EXP_NONCE_SIZE { - error!( - "Given nonce has size {} but expected {} bytes", - nonce.len(), - EXP_NONCE_SIZE - ); - anyhow::bail!("Incorrect nonce size") - } else { - let key = GenericArray::from_slice(key); - let nonce = GenericArray::from_slice(nonce); - let cipher = Aes256GcmSiv::new(key); - - let pt = format_pt_for_storage(&self.name, &self.content); - match cipher.encrypt(nonce, pt.as_bytes()) { - Ok(ct) => Ok(ct), - Err(e) => anyhow::bail!("Error while encrypting {}", e), - } - } - } - - pub fn decrypt(key: &[u8], nonce: &[u8], ct: &[u8]) -> anyhow::Result { - let key = GenericArray::from_slice(key); - let nonce = GenericArray::from_slice(nonce); - let cipher = Aes256GcmSiv::new(key); - - match cipher.decrypt(nonce, ct) { - Ok(pt) => { - let pt = String::from_utf8(pt)?; - let (name, content) = restore_pt_no_dt(&pt)?; - Ok(DocumentPart::new(name, content)) - } - Err(e) => { - anyhow::bail!("Error while decrypting: {}", e) - } - } - } -} +use crate::model::ids::message::IdsMessage; +use uuid::Uuid; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct Document { + /// Document id #[serde(default = "new_uuid")] pub id: String, - pub dt_id: String, + /// Process ID pub pid: String, - pub ts: i64, - pub parts: Vec, + /// timestamp: unix timestamp + pub ts: chrono::DateTime, + /// Content of the document + pub content: IdsMessage, } /// Documents should have a globally unique id, setting the id manually is discouraged. @@ -86,170 +23,12 @@ impl Document { Uuid::new_v4().hyphenated().to_string() } - // each part is encrypted using the part specific key from the key map - // the hash is set to "0". Chaining is not done here. - pub fn encrypt(&self, key_map: KeyMap) -> anyhow::Result { - debug!("encrypting document of doc_type {}", self.dt_id); - let mut cts = vec![]; - - let keys = key_map.keys; - let key_ct = match key_map.keys_enc { - Some(ct) => hex::encode(ct), - None => { - anyhow::bail!("Missing key ct"); - } - }; - - for part in self.parts.iter() { - // check if there's a key for this part - let key_entry = match keys.get(&part.name) { - Some(key_entry) => key_entry, - None => { - error!("Missing key for part '{}'", &part.name); - anyhow::bail!("Missing key for part '{}'", &part.name); - } - }; - // Encrypt part - let ct_string = match part.encrypt(key_entry.key.as_slice(), key_entry.nonce.as_slice()) - { - Ok(ct) => hex::encode_upper(ct), - Err(e) => { - error!("Error while encrypting: {}", e); - anyhow::bail!("Error while encrypting: {}", e); - } - }; - - // key entry id is needed for decryption - cts.push(format!("{}::{}", key_entry.id, ct_string)); - } - cts.sort(); - - Ok(EncryptedDocument::new( - self.id.clone(), - self.pid.clone(), - self.dt_id.clone(), - self.ts, - key_ct, - cts, - )) - } - - pub fn get_parts_map(&self) -> HashMap { - let mut p_map = HashMap::with_capacity(self.parts.len()); - for part in self.parts.iter() { - p_map.insert(part.name.clone(), part.content.clone()); - } - p_map - } - - pub fn new(pid: String, dt_id: String, parts: Vec) -> Document { - Document { + pub fn new(pid: String, content: IdsMessage) -> Self { + Self { id: Document::create_uuid(), - dt_id, pid, - ts: Local::now().timestamp(), - parts, + ts: Local::now(), + content, } } - - fn restore( - id: String, - pid: String, - dt_id: String, - ts: i64, - parts: Vec, - ) -> Document { - Document { - id, - dt_id, - pid, - ts, - parts, - } - } -} - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -pub struct EncryptedDocument { - pub id: String, - pub pid: String, - pub dt_id: String, - pub ts: i64, - pub keys_ct: String, - pub cts: Vec, -} - -impl EncryptedDocument { - /// Note: KeyMap keys need to be KeyEntry.ids in this case - // Decryption is done without checking the hashes. Do this before calling this method - pub fn decrypt(&self, keys: HashMap) -> anyhow::Result { - let mut pts = vec![]; - for ct in self.cts.iter() { - let ct_parts = ct.split(SPLIT_CT).collect::>(); - if ct_parts.len() != 2 { - anyhow::bail!("Integrity violation! Ciphertexts modified"); - } - // get key and nonce - let key_entry = keys.get(ct_parts[0]); - if let Some(key_entry) = key_entry { - let key = key_entry.key.as_slice(); - let nonce = key_entry.nonce.as_slice(); - - // get ciphertext - //TODO: use error_chain? - let ct = hex::decode(ct_parts[1])?; - - // decrypt - match DocumentPart::decrypt(key, nonce, ct.as_slice()) { - Ok(part) => pts.push(part), - Err(e) => { - anyhow::bail!("Error while decrypting: {}", e); - } - } - } else { - anyhow::bail!("Key for id '{}' does not exist!", ct_parts[0]); - } - } - - Ok(Document::restore( - self.id.clone(), - self.pid.clone(), - self.dt_id.clone(), - self.ts, - pts, - )) - } - - pub fn new( - id: String, - pid: String, - dt_id: String, - ts: i64, - keys_ct: String, - cts: Vec, - ) -> EncryptedDocument { - EncryptedDocument { - id, - pid, - dt_id, - ts, - keys_ct, - cts, - } - } -} - -/// companion to format_pt_for_storage_no_dt -pub fn restore_pt_no_dt(pt: &str) -> anyhow::Result<(String, String)> { - trace!("Trying to restore plain text"); - let vec: Vec<&str> = pt.split(SPLIT_CT).collect(); - if vec.len() != 2 { - anyhow::bail!("Could not restore plaintext"); - } - Ok((String::from(vec[0]), String::from(vec[1]))) -} - -/// formats the pt before encryption -fn format_pt_for_storage(field_name: &str, pt: &str) -> String { - format!("{}{}{}", field_name, SPLIT_CT, pt) } diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index 10e5ed4..02b2837 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -1,21 +1,7 @@ -use crate::model::constants::DEFAULT_DOC_TYPE; -use crate::model::document::{Document, DocumentPart}; +use crate::model::document::Document; use crate::model::ids::{InfoModelDateTime, InfoModelId, MessageType, SecurityToken}; use std::collections::HashMap; -const MESSAGE_ID: &str = "message_id"; -const MODEL_VERSION: &str = "model_version"; -const CORRELATION_MESSAGE: &str = "correlation_message"; -const TRANSFER_CONTRACT: &str = "transfer_contract"; -const ISSUED: &str = "issued"; -const ISSUER_CONNECTOR: &str = "issuer_connector"; -const CONTENT_VERSION: &str = "content_version"; -/// const RECIPIENT_CONNECTOR: &'static str = "recipient_connector"; // all messages should contain the CH connector, so we skip this information -const SENDER_AGENT: &str = "sender_agent"; -///const RECIPIENT_AGENT: &'static str = "recipient_agent"; // all messages should contain the CH agent, so we skip this information -const PAYLOAD: &str = "payload"; -const PAYLOAD_TYPE: &str = "payload_type"; - /// Metadata describing payload exchanged by interacting Connectors. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct IdsMessage { @@ -138,16 +124,6 @@ impl Default for IdsMessage { } } -impl IdsMessage { - pub fn restore() -> IdsMessage { - IdsMessage { - type_message: MessageType::LogMessage, - //TODO recipient_agent CH - ..Default::default() - } - } -} - /// Conversion from Document to IdsMessage /// /// note: Documents are converted into LogMessages. The LogMessage contains @@ -171,73 +147,7 @@ impl IdsMessage { /// - payload_type impl From for IdsMessage { fn from(doc: Document) -> Self { - let mut m = IdsMessage::restore(); - // pid - m.pid = Some(doc.pid.clone()); - // message_id - let p_map = doc.get_parts_map(); - if let Some(v) = p_map.get(MESSAGE_ID) { - m.id = Some(v.clone()); - } - // model_version - if let Some(v) = p_map.get(MODEL_VERSION) { - m.model_version = v.clone(); - } - - // correlation_message - if let Some(v) = p_map.get(CORRELATION_MESSAGE) { - m.correlation_message = Some(v.clone()); - } - - // transfer_contract - if let Some(v) = p_map.get(TRANSFER_CONTRACT) { - m.transfer_contract = Some(v.clone()); - } - - // issued - if let Some(v) = p_map.get(ISSUED) { - match serde_json::from_str(v) { - Ok(date_time) => { - m.issued = date_time; - } - Err(e) => { - error!( - "Error while converting DateTimeStamp (field 'issued') from database: {}", - e - ); - } - } - } - - // issuer_connector - if let Some(v) = p_map.get(ISSUER_CONNECTOR) { - m.issuer_connector = InfoModelId::SimpleId(v.clone()); - } - - // content_version - if let Some(v) = p_map.get(CONTENT_VERSION) { - m.content_version = Some(v.clone()); - } - - // sender_agent - if let Some(v) = p_map.get(SENDER_AGENT) { - m.sender_agent = v.clone(); - } - - // payload - if let Some(v) = p_map.get(PAYLOAD) { - m.payload = Some(v.clone()); - } - - // payload_type - if let Some(v) = p_map.get(PAYLOAD_TYPE) { - m.payload_type = Some(v.clone()); - } - - //TODO: security_token - //TODO: authorization_token - - m + doc.content.clone() } } @@ -260,82 +170,17 @@ impl From for IdsMessage { /// - authorization_token /// - payload /// - payload_type -impl TryFrom for Document { - type Error = serde_json::Error; - - fn try_from(m: IdsMessage) -> Result { - use serde::ser::Error; - let mut doc_parts = vec![]; - - // message_id - let id = match m.id { - Some(m_id) => m_id, - None => autogen("Message"), - }; - - doc_parts.push(DocumentPart::new(MESSAGE_ID.to_string(), id)); - - // model_version - doc_parts.push(DocumentPart::new( - MODEL_VERSION.to_string(), - m.model_version, - )); - - // correlation_message - if let Some(s) = m.correlation_message { - doc_parts.push(DocumentPart::new(CORRELATION_MESSAGE.to_string(), s)); - } - - // issued - doc_parts.push(DocumentPart::new( - ISSUED.to_string(), - serde_json::to_string(&m.issued)?, - )); +impl Into for IdsMessage { + fn into(self) -> Document { + let mut m = self.clone(); - // issuer_connector - doc_parts.push(DocumentPart::new( - ISSUER_CONNECTOR.to_string(), - m.issuer_connector.to_string(), - )); + m.id = Some(m.id.unwrap_or_else(|| autogen("Message"))); - // sender_agent - doc_parts.push(DocumentPart::new( - SENDER_AGENT.to_string(), - m.sender_agent.to_string(), - )); - - // transfer_contract - if let Some(s) = m.transfer_contract { - doc_parts.push(DocumentPart::new(TRANSFER_CONTRACT.to_string(), s)); - } - - // content_version - if let Some(s) = m.content_version { - doc_parts.push(DocumentPart::new(CONTENT_VERSION.to_string(), s)); - } - - // security_token - //TODO - - // authorization_token - //TODO - - // payload - if let Some(s) = m.payload { - doc_parts.push(DocumentPart::new(PAYLOAD.to_string(), s)); - } - - // payload_type - if let Some(s) = m.payload_type { - doc_parts.push(DocumentPart::new(PAYLOAD_TYPE.to_string(), s)); - } + // Remove security tokens to protect against impersonation of other owners of the same process + m.security_token = None; + m.authorization_token = None; - // pid - Ok(Document::new( - m.pid.ok_or(serde_json::Error::custom("PID missing"))?, - DEFAULT_DOC_TYPE.to_string(), - doc_parts, - )) + Document::new(m.pid.clone().expect("Missing pid"), m) } } diff --git a/clearing-house-app/src/model/mod.rs b/clearing-house-app/src/model/mod.rs index a204098..36e06d8 100644 --- a/clearing-house-app/src/model/mod.rs +++ b/clearing-house-app/src/model/mod.rs @@ -1,7 +1,7 @@ +use std::ops::Add; + pub mod claims; pub mod constants; -pub(crate) mod crypto; -pub(crate) mod doc_type; pub(crate) mod document; pub mod ids; pub mod process; @@ -50,7 +50,7 @@ pub fn validate_and_sanitize_dates( &now, &date_from, &date_to ); - let default_to_date = now; + let default_to_date = now.add(chrono::Duration::seconds(1)); let default_from_date = default_to_date .date() .and_hms_opt(0, 0, 0) @@ -67,6 +67,7 @@ pub fn validate_and_sanitize_dates( #[cfg(test)] mod test { + use std::ops::Add; #[test] fn validate_and_sanitize_dates() { @@ -81,12 +82,12 @@ mod test { // # Good cases assert_eq!( - (date_from, date_now), + (date_from, date_now.add(chrono::Duration::seconds(1))), super::validate_and_sanitize_dates(None, None, Some(date_now)) .expect("Should be valid") ); assert_eq!( - (date_from, date_now), + (date_from, date_now.add(chrono::Duration::seconds(1))), super::validate_and_sanitize_dates(Some(date_from), None, Some(date_now)) .expect("Should be valid") ); diff --git a/clearing-house-app/src/model/process.rs b/clearing-house-app/src/model/process.rs index cda0edf..db4d6e1 100644 --- a/clearing-house-app/src/model/process.rs +++ b/clearing-house-app/src/model/process.rs @@ -43,13 +43,11 @@ impl biscuit::CompactJson for DataTransaction {} impl DataTransaction { pub fn sign(&self, key_path: &str) -> Receipt { - use crate::model::claims::get_fingerprint; - let jws = biscuit::jws::Compact::new_decoded( biscuit::jws::Header::from_registered_header(biscuit::jws::RegisteredHeader { algorithm: biscuit::jwa::SignatureAlgorithm::PS512, media_type: None, - key_id: get_fingerprint(key_path), + key_id: crate::model::claims::get_fingerprint(key_path), ..Default::default() }), self.clone(), @@ -65,25 +63,3 @@ impl DataTransaction { } } } - -// convenience method for testing -#[cfg(test)] -impl From for DataTransaction { - // TODO: It would be better to implement the TryFrom trait instead of this error DataTransaction - fn from(r: Receipt) -> Self { - match r.data.unverified_payload() { - Ok(d) => d, - Err(e) => { - println!("Error occurred: {:#?}", e); - DataTransaction { - timestamp: 0, - process_id: "error".to_string(), - document_id: "error".to_string(), - payload: "error".to_string(), - client_id: "error".to_string(), - clearing_house_version: "error".to_string(), - } - } - } - } -} diff --git a/clearing-house-app/src/ports/doc_type_api.rs b/clearing-house-app/src/ports/doc_type_api.rs deleted file mode 100644 index fd38a01..0000000 --- a/clearing-house-app/src/ports/doc_type_api.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::model::constants::DEFAULT_PROCESS_ID; -use crate::ports::ApiResponse; -use axum::http::StatusCode; - -use crate::model::doc_type::DocumentType; -use crate::services::keyring_service::KeyringServiceError; - -type DocApiResult = super::ApiResult; - -async fn create_doc_type( - axum::extract::State(state): axum::extract::State, - axum::extract::Json(doc_type): axum::extract::Json, -) -> DocApiResult { - match state.keyring_service.create_doc_type(doc_type).await { - Ok(dt) => Ok((StatusCode::CREATED, Json(dt))), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - Err(e) - } - } -} - -async fn update_doc_type( - axum::extract::State(state): axum::extract::State, - axum::extract::Path(id): axum::extract::Path, - axum::extract::Json(doc_type): axum::extract::Json, -) -> DocApiResult { - match state.keyring_service.update_doc_type(id, doc_type).await { - Ok(id) => Ok((StatusCode::OK, Json(id))), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - Err(e) - } - } -} - -async fn delete_default_doc_type( - state: axum::extract::State, - id: axum::extract::Path, -) -> DocApiResult { - delete_doc_type( - state, - id, - axum::extract::Path(DEFAULT_PROCESS_ID.to_string()), - ) - .await -} - -async fn delete_doc_type( - axum::extract::State(state): axum::extract::State, - axum::extract::Path(id): axum::extract::Path, - axum::extract::Path(pid): axum::extract::Path, -) -> DocApiResult { - match state.keyring_service.delete_doc_type(id, pid).await { - Ok(id) => Ok((StatusCode::OK, Json(id))), - Err(e) => { - error!("Error while deleting doctype: {:?}", e); - Err(e) - } - } -} - -async fn get_default_doc_type( - state: axum::extract::State, - id: axum::extract::Path, -) -> DocApiResult> { - get_doc_type( - state, - id, - axum::extract::Path(DEFAULT_PROCESS_ID.to_string()), - ) - .await -} - -//#[rocket::get("//", format = "json")] -async fn get_doc_type( - axum::extract::State(state): axum::extract::State, - axum::extract::Path(id): axum::extract::Path, - axum::extract::Path(pid): axum::extract::Path, -) -> DocApiResult> { - match state.keyring_service.get_doc_type(id, pid).await { - Ok(dt) => match dt { - Some(dt) => Ok((StatusCode::OK, Json(Some(dt)))), - None => Ok((StatusCode::OK, Json(None))), - }, - Err(e) => { - error!("Error while retrieving doctype: {:?}", e); - Err(e) - } - } -} - -//#[rocket::get("/", format = "json")] -async fn get_doc_types( - axum::extract::State(state): axum::extract::State, -) -> DocApiResult> { - match state.keyring_service.get_doc_types().await { - Ok(dt) => Ok((StatusCode::OK, Json(dt))), - Err(e) => { - error!("Error while retrieving doc_types: {:?}", e); - Err(e) - } - } -} - -pub(crate) fn router() -> axum::Router { - axum::Router::new() - .route("/", axum::routing::get(get_doc_types).post(create_doc_type)) - .route( - "/:id", - axum::routing::get(get_default_doc_type) - .post(update_doc_type) - .delete(delete_default_doc_type), - ) - .route( - "/:pid/:id", - axum::routing::get(get_doc_type).delete(delete_doc_type), - ) -} diff --git a/clearing-house-app/src/ports/mod.rs b/clearing-house-app/src/ports/mod.rs index 8360709..5ebb30b 100644 --- a/clearing-house-app/src/ports/mod.rs +++ b/clearing-house-app/src/ports/mod.rs @@ -5,20 +5,9 @@ //! the logging service. use crate::AppState; -#[cfg(doc_type)] -pub(crate) mod doc_type_api; pub(crate) mod logging_api; -/// Router for the logging service and the doc_type service -#[cfg(doc_type)] -pub(crate) fn router() -> axum::routing::Router { - axum::Router::new() - .merge(ports::logging_api::router()) - .nest("/doctype", ports::doc_type_api::router()); -} - /// Router for the logging service -#[cfg(not(doc_type))] pub(crate) fn router() -> axum::routing::Router { axum::Router::new().merge(logging_api::router()) } diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index b9f254c..84bf72a 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -1,15 +1,10 @@ -use crate::db::doc_store::DataStore; +use crate::db::DocumentStore; use crate::model::claims::ChClaims; -use crate::model::constants::{ - DEFAULT_DOC_TYPE, DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES, PAYLOAD_PART, -}; -use crate::model::crypto::{KeyCt, KeyCtList}; +use crate::model::constants::{DEFAULT_NUM_RESPONSE_ENTRIES, MAX_NUM_RESPONSE_ENTRIES}; use crate::model::document::Document; use crate::model::{parse_date, validate_and_sanitize_dates, SortingOrder}; -use crate::services::keyring_service::KeyringService; use crate::services::{DocumentReceipt, QueryResult}; use std::convert::TryFrom; -use std::sync::Arc; /// Error type for DocumentService #[derive(thiserror::Error, Debug)] @@ -23,16 +18,10 @@ pub enum DocumentServiceError { source: anyhow::Error, description: String, }, - #[error("Error while retrieving keys from keyring!")] - KeyringServiceError(#[from] crate::services::keyring_service::KeyringServiceError), #[error("Invalid dates in query!")] InvalidDates, #[error("Document not found!")] NotFound, - #[error("Key Ciphertext corrupted!")] - CorruptedCiphertext(#[from] hex::FromHexError), - #[error("Error while encrypting!")] - EncryptionError, } impl axum::response::IntoResponse for DocumentServiceError { @@ -51,28 +40,20 @@ impl axum::response::IntoResponse for DocumentServiceError { format!("{}: {}", description, source), ) .into_response(), - Self::KeyringServiceError(e) => e.into_response(), Self::InvalidDates => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), - Self::CorruptedCiphertext(e) => { - (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response() - } - Self::EncryptionError => { - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() - } } } } #[derive(Clone, Debug)] -pub struct DocumentService { - db: DataStore, - key_api: Arc, +pub struct DocumentService { + db: T, } -impl DocumentService { - pub fn new(db: DataStore, key_api: Arc) -> Self { - Self { db, key_api } +impl DocumentService { + pub fn new(db: T) -> Self { + Self { db } } #[tracing::instrument(skip_all)] @@ -83,19 +64,7 @@ impl DocumentService { ) -> Result { trace!("...user '{:?}'", &ch_claims.client_id); // data validation - let payload: Vec = doc - .parts - .iter() - .filter(|p| *PAYLOAD_PART == p.name) - .map(|p| p.content.clone()) - .collect(); - - // If the document contains more than 1 payload we panic. This should never happen! - assert!( - payload.len() <= 1, - "Document contains two or more payloads!" - ); - if payload.is_empty() { + if doc.content.payload.is_none() { return Err(DocumentServiceError::MissingPayload); } @@ -106,42 +75,12 @@ impl DocumentService { Err(DocumentServiceError::DocumentAlreadyExists) } _ => { - trace!("getting keys"); - - // TODO: This needs some attention, because keyring api called `create_service_token` on `ch_claims` - let keys = match self - .key_api - .generate_keys(ch_claims, doc.pid.clone(), doc.dt_id.clone()) - .await - { - Ok(key_map) => { - debug!("got keys"); - Ok(key_map) - } - Err(e) => { - error!("Error while retrieving keys: {:?}", e); - Err(DocumentServiceError::KeyringServiceError(e)) - } - }?; - - debug!("start encryption"); - let enc_doc = match doc.encrypt(keys) { - Ok(ct) => { - debug!("got ct"); - Ok(ct) - } - Err(e) => { - error!("Error while encrypting: {:?}", e); - Err(DocumentServiceError::EncryptionError) - } - }?; - // prepare the success result message - let receipt = DocumentReceipt::new(enc_doc.ts, &enc_doc.pid, &enc_doc.id); + let receipt = DocumentReceipt::new(doc.ts, &doc.pid, &doc.id); trace!("storing document ...."); // store document - match self.db.add_document(enc_doc).await { + match self.db.add_document(doc).await { Ok(_b) => Ok(receipt), Err(e) => { error!("Error while adding: {:?}", e); @@ -165,14 +104,10 @@ impl DocumentService { (date_from, date_to): (Option, Option), pid: String, ) -> Result { - debug!("Trying to retrieve documents for pid '{}'...", &pid); + debug!("Trying to retrieve documents for pid '{pid}'..."); trace!("...user '{:?}'", &ch_claims.client_id); - debug!( - "...page: {:#?}, size:{:#?} and sort:{:#?}", - page, size, sort - ); + debug!("...page: {page:?}, size:{size:?} and sort:{sort:?}"); - let dt_id = String::from(DEFAULT_DOC_TYPE); let sanitized_page = Self::sanitize_page(page); let sanitized_size = Self::sanitize_size(size); @@ -198,10 +133,9 @@ impl DocumentService { sanitized_page, sanitized_size, &sanitized_sort ); - let cts = match self + let docs = match self .db .get_documents_for_pid( - &dt_id, &pid, sanitized_page, sanitized_size, @@ -210,7 +144,7 @@ impl DocumentService { ) .await { - Ok(cts) => cts, + Ok(docs) => docs, Err(e) => { error!("Error while retrieving document: {:?}", e); return Err(DocumentServiceError::DatabaseError { @@ -237,54 +171,11 @@ impl DocumentService { ); // The db might contain no documents in which case we get an empty vector - if cts.is_empty() { + if docs.is_empty() { debug!("Queried empty pid: {}", &pid); Ok(result) } else { - // Documents found for pid, now decrypting them - debug!( - "Found {} documents. Getting keys from keyring...", - cts.len() - ); - let key_cts: Vec = cts - .iter() - .map(|e| KeyCt::new(e.id.clone(), e.keys_ct.clone())) - .collect(); - // caution! we currently only support a single dt per call, so we use the first dt we found - let key_cts_list = KeyCtList::new(cts[0].dt_id.clone(), key_cts); - // decrypt cts - // TODO: This method needs some attention, because keyring api called `create_service_token` on `ch_claims` - let key_maps = match self - .key_api - .decrypt_multiple_keys(ch_claims, Some(pid), &key_cts_list) - .await - { - Ok(key_map) => key_map, - Err(e) => { - error!("Error while retrieving keys from keyring: {:?}", e); - return Err(DocumentServiceError::KeyringServiceError(e)); - } - }; - debug!("... keys received. Starting decryption..."); - let pts_bulk: Vec = cts - .iter() - .zip(key_maps.iter()) - .filter_map(|(ct, key_map)| { - if ct.id != key_map.id { - error!("Document and map don't match"); - }; - match ct.decrypt(key_map.map.keys.clone()) { - Ok(d) => Some(d), - Err(e) => { - warn!("Got empty document from decryption! {:?}", e); - None - } - } - }) - .collect(); - debug!("...done."); - - result.documents = pts_bulk; + result.documents = docs; Ok(result) } } @@ -298,53 +189,13 @@ impl DocumentService { hash: Option, ) -> Result { trace!("...user '{:?}'", &ch_claims.client_id); - trace!( - "trying to retrieve document with id '{}' for pid '{}'", - &id, - &pid - ); + trace!("trying to retrieve document with id '{id}' for pid '{pid}'"); if let Some(hash) = hash { debug!("integrity check with hash: {}", hash); } match self.db.get_document(&id, &pid).await { - //TODO: would like to send "{}" instead of "null" when dt is not found - Ok(Some(ct)) => { - match hex::decode(&ct.keys_ct) { - Ok(key_ct) => { - // TODO: This method needs some attention, because keyring api called `create_service_token` on `ch_claims` - match self - .key_api - .decrypt_key_map( - ch_claims, - hex::encode_upper(key_ct), - Some(pid), - ct.dt_id.clone(), - ) - .await - { - Ok(key_map) => { - //TODO check the hash - match ct.decrypt(key_map.keys) { - Ok(d) => Ok(d), - Err(e) => { - warn!("Got empty document from decryption! {:?}", e); - Err(DocumentServiceError::NotFound) - } - } - } - Err(e) => { - error!("Error while retrieving keys from keyring: {:?}", e); - Err(DocumentServiceError::KeyringServiceError(e)) - } - } - } - Err(e) => { - error!("Error while decoding ciphertext: {:?}", e); - Err(DocumentServiceError::CorruptedCiphertext(e)) // InternalError - } - } - } + Ok(Some(ct)) => Ok(ct), Ok(None) => { debug!("Nothing found in db!"); Err(DocumentServiceError::NotFound) // NotFound diff --git a/clearing-house-app/src/services/keyring_service.rs b/clearing-house-app/src/services/keyring_service.rs deleted file mode 100644 index 8a19257..0000000 --- a/clearing-house-app/src/services/keyring_service.rs +++ /dev/null @@ -1,369 +0,0 @@ -use crate::crypto; -use crate::crypto::restore_key_map; -use crate::db::key_store::KeyStore; -use crate::model::claims::ChClaims; -use crate::model::crypto::{KeyCtList, KeyMap, KeyMapListItem}; - -#[derive(Debug, thiserror::Error)] -pub enum KeyringServiceError { - #[error("Keymap generation error")] - KeymapGenerationFailed, - #[error("Keymap restoration error")] - KeymapRestorationFailed, - #[error("Document type not found")] - DocumentTypeNotFound, - #[error("Error during database operation: {description}: {source}")] - DatabaseError { - source: anyhow::Error, - description: String, - }, - #[error("Error while decrypting keys")] - DecryptionError, - #[cfg_attr(not(doc_type), allow(dead_code))] - #[error("Document type already exists")] - DocumentTypeAlreadyExists, -} - -impl axum::response::IntoResponse for KeyringServiceError { - fn into_response(self) -> axum::response::Response { - use axum::http::StatusCode; - match self { - Self::KeymapGenerationFailed => { - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() - } - Self::KeymapRestorationFailed => { - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() - } - Self::DocumentTypeNotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), - Self::DatabaseError { - source, - description, - } => ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("{}: {}", description, source), - ) - .into_response(), - Self::DecryptionError => { - (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response() - } - Self::DocumentTypeAlreadyExists => { - (StatusCode::BAD_REQUEST, self.to_string()).into_response() - } - } - } -} - -#[derive(Clone, Debug)] -pub struct KeyringService { - db: KeyStore, -} - -impl KeyringService { - pub fn new(db: KeyStore) -> KeyringService { - KeyringService { db } - } - - #[tracing::instrument(skip_all)] - pub async fn generate_keys( - &self, - ch_claims: ChClaims, - _pid: String, - dt_id: String, - ) -> Result { - trace!("generate_keys"); - trace!("...user '{:?}'", &ch_claims.client_id); - match self.db.get_msk().await { - Ok(key) => { - // check that doc type exists for pid - match self.db.get_document_type(&dt_id).await { - Ok(Some(dt)) => { - // generate new random key map - match crypto::generate_key_map(key, dt) { - Ok(key_map) => { - trace!("response: {:?}", &key_map); - Ok(key_map) - } - Err(e) => { - error!("Error while generating key map: {}", e); - Err(KeyringServiceError::KeymapGenerationFailed) - } - } - } - Ok(None) => { - warn!("document type {} not found", &dt_id); - Err(KeyringServiceError::DocumentTypeNotFound) - } - Err(e) => { - warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while retrieving document type".to_string(), - }) - } - } - } - Err(e) => { - error!("Error while retrieving master key: {}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while retrieving master key".to_string(), - }) - } - } - } - - #[tracing::instrument(skip_all)] - pub(crate) async fn decrypt_keys( - &self, - ch_claims: ChClaims, - _pid: Option, - key_cts: &KeyCtList, - ) -> Result, KeyringServiceError> { - trace!("decrypt_keys"); - trace!("...user '{:?}'", &ch_claims.client_id); - debug!("number of cts to decrypt: {}", &key_cts.cts.len()); - - // get master key - match self.db.get_msk().await { - Ok(m_key) => { - // check that doc type exists for pid - match self.db.get_document_type(&key_cts.dt).await { - Ok(Some(dt)) => { - let mut dec_error_count = 0; - let mut map_error_count = 0; - // validate keys_ct input - let key_maps: Vec = key_cts - .cts - .iter() - .filter_map(|key_ct| match hex::decode(key_ct.ct.clone()) { - Ok(key) => Some((key_ct.id.clone(), key)), - Err(e) => { - error!("Error while decoding key ciphertext: {}", e); - dec_error_count += 1; - None - } - }) - .filter_map(|(id, key)| { - match restore_key_map(m_key.clone(), dt.clone(), key) { - Ok(key_map) => Some(KeyMapListItem::new(id, key_map)), - Err(e) => { - error!("Error while generating key map: {}", e); - map_error_count += 1; - None - } - } - }) - .collect(); - - let error_count = map_error_count + dec_error_count; - - // Currently, we don't tolerate errors while decrypting keys - if error_count > 0 { - Err(KeyringServiceError::DecryptionError) - } else { - Ok(key_maps) - } - } - Ok(None) => { - warn!("document type {} not found", &key_cts.dt); - Err(KeyringServiceError::DocumentTypeNotFound) - } - Err(e) => { - warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while retrieving document type".to_string(), - }) - } - } - } - Err(e) => { - error!("Error while retrieving master key: {}", e); - Err(KeyringServiceError::DecryptionError) - } - } - } - - #[tracing::instrument(skip_all)] - pub async fn decrypt_key_map( - &self, - ch_claims: ChClaims, - keys_ct: String, - _pid: Option, - dt_id: String, - ) -> Result { - trace!("decrypt_key_map"); - trace!("...user '{:?}'", &ch_claims.client_id); - trace!("ct: {}", &keys_ct); - // get master key - match self.db.get_msk().await { - Ok(key) => { - // check that doc type exists for pid - match self.db.get_document_type(&dt_id).await { - Ok(Some(dt)) => { - // validate keys_ct input - let keys_ct = hex::decode(keys_ct).map_err(|e| { - error!("Error while decoding key ciphertext: {}", e); - KeyringServiceError::DecryptionError - })?; - - match restore_key_map(key, dt, keys_ct) { - Ok(key_map) => Ok(key_map), - Err(e) => { - error!("Error while generating key map: {}", e); - Err(KeyringServiceError::KeymapRestorationFailed) - } - } - } - Ok(None) => { - warn!("document type {} not found", &dt_id); - Err(KeyringServiceError::DocumentTypeNotFound) - } - Err(e) => { - warn!("Error while retrieving document type: {}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while retrieving document type".to_string(), - }) - } - } - } - Err(e) => { - error!("Error while retrieving master key: {}", e); - Err(KeyringServiceError::DecryptionError) - } - } - } - - #[tracing::instrument(skip_all)] - pub(crate) async fn decrypt_multiple_keys( - &self, - ch_claims: ChClaims, - pid: Option, - cts: &KeyCtList, - ) -> Result, KeyringServiceError> { - self.decrypt_keys(ch_claims, pid, cts).await - } - - #[cfg(doc_type)] - pub(crate) async fn create_doc_type( - &self, - doc_type: crate::model::doc_type::DocumentType, - ) -> Result { - debug!("adding doctype: {:?}", &doc_type); - match self - .db - .exists_document_type(&doc_type.pid, &doc_type.id) - .await - { - Ok(true) => Err(KeyringServiceError::DocumentTypeAlreadyExists), // BadRequest - Ok(false) => match self.db.add_document_type(doc_type.clone()).await { - Ok(()) => Ok(doc_type), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while adding doctype".to_string(), - }) - } - }, - Err(e) => { - error!("Error while adding document type: {:?}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while checking doctype".to_string(), - }) - } - } - } - - #[cfg(doc_type)] - pub(crate) async fn update_doc_type( - &self, - id: String, - doc_type: crate::model::doc_type::DocumentType, - ) -> Result { - match self - .db - .exists_document_type(&doc_type.pid, &doc_type.id) - .await - { - Ok(true) => Err(KeyringServiceError::DocumentTypeAlreadyExists), - Ok(false) => match self.db.update_document_type(doc_type, &id).await { - Ok(id) => Ok(id), - Err(e) => { - error!("Error while adding doctype: {:?}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while storing document type!".to_string(), - }) - } - }, - Err(e) => { - error!("Error while adding document type: {:?}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while checking doctype".to_string(), - }) - } - } - } - - #[cfg(doc_type)] - pub(crate) async fn delete_doc_type( - &self, - id: String, - pid: String, - ) -> Result { - match self.db.delete_document_type(&id, &pid).await { - Ok(true) => Ok(String::from("Document type deleted!")), // NoContent - Ok(false) => Err(KeyringServiceError::DocumentTypeNotFound), - Err(e) => { - error!("Error while deleting doctype: {:?}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: format!("Error while deleting document type with id {}!", id), - }) - } - } - } - - #[cfg(doc_type)] - pub(crate) async fn get_doc_type( - &self, - id: String, - pid: String, - ) -> Result, KeyringServiceError> { - match self.db.get_document_type(&id).await { - //TODO: would like to send "{}" instead of "null" when dt is not found - Ok(dt) => Ok(dt), - Err(e) => { - error!("Error while retrieving doctype: {:?}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: format!( - "Error while retrieving document type with id {} and pid {}!", - id, pid - ), - }) - } - } - } - - #[cfg(doc_type)] - pub(crate) async fn get_doc_types( - &self, - ) -> Result, KeyringServiceError> { - match self.db.get_all_document_types().await { - //TODO: would like to send "{}" instead of "null" when dt is not found - Ok(dt) => Ok(dt), - Err(e) => { - error!("Error while retrieving default doc_types: {:?}", e); - Err(KeyringServiceError::DatabaseError { - source: e, - description: "Error while retrieving all document types".to_string(), - }) - } - } - } -} diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index 589265d..a82b39f 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -5,7 +5,7 @@ use crate::model::{ }; use std::sync::Arc; -use crate::db::process_store::ProcessStore; +use crate::db::{DocumentStore, ProcessStore}; use crate::model::{ ids::{message::IdsMessage, request::ClearingHouseMessage, IdsQueryResult}, process::{DataTransaction, OwnerList, Receipt}, @@ -70,14 +70,14 @@ impl axum::response::IntoResponse for LoggingServiceError { } } -#[derive(Clone, Debug)] -pub struct LoggingService { - db: ProcessStore, - doc_api: Arc, +#[derive(Debug)] +pub(crate) struct LoggingService { + db: T, + doc_api: Arc>, } -impl LoggingService { - pub fn new(db: ProcessStore, doc_api: Arc) -> LoggingService { +impl LoggingService { + pub fn new(db: T, doc_api: Arc>) -> LoggingService { LoggingService { db, doc_api } } @@ -128,10 +128,9 @@ impl LoggingService { // transform message to document debug!("transforming message to document..."); - let doc = Document::try_from(m).map_err(LoggingServiceError::ParsingError)?; + let doc: Document = m.into(); debug!("Storing document..."); - // TODO: ChClaims usage check match self .doc_api .create_enc_document(ChClaims::new(user), doc.clone()) @@ -351,15 +350,3 @@ impl LoggingService { } } } - -#[cfg(test)] -mod test { - use super::LoggingService; - use crate::model::constants::DEFAULT_PROCESS_ID; - - #[test] - fn check_for_default_pid() { - assert!(LoggingService::check_for_default_pid(DEFAULT_PROCESS_ID).is_err()); - assert!(LoggingService::check_for_default_pid("not_default").is_ok()); - } -} diff --git a/clearing-house-app/src/services/mod.rs b/clearing-house-app/src/services/mod.rs index 98888ee..c5595d5 100644 --- a/clearing-house-app/src/services/mod.rs +++ b/clearing-house-app/src/services/mod.rs @@ -7,7 +7,6 @@ use crate::model::document::Document; pub(crate) mod document_service; -pub(crate) mod keyring_service; pub(crate) mod logging_service; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] @@ -18,9 +17,13 @@ pub struct DocumentReceipt { } impl DocumentReceipt { - pub fn new(timestamp: i64, pid: &str, doc_id: &str) -> DocumentReceipt { + pub fn new( + timestamp: chrono::DateTime, + pid: &str, + doc_id: &str, + ) -> DocumentReceipt { DocumentReceipt { - timestamp, + timestamp: timestamp.timestamp(), pid: pid.to_string(), doc_id: doc_id.to_string(), } diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index 41b9146..5af5d60 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -1,4 +1,3 @@ -use anyhow::Context; use crate::model::claims::get_fingerprint; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -6,12 +5,6 @@ pub struct ServiceConfig { pub service_id: String, } -/// Reads a file into a string -pub(crate) fn read_file(file: &str) -> anyhow::Result { - std::fs::read_to_string(file) - .with_context(|| format!("Failed to read contents of file '{}'", file)) -} - pub(super) fn init_service_config(service_id: String) -> anyhow::Result { match std::env::var(&service_id) { Ok(id) => Ok(ServiceConfig { service_id: id }), @@ -26,7 +19,9 @@ pub(super) fn init_service_config(service_id: String) -> anyhow::Result) -> anyhow::Result { let private_key_path = signing_key_path.unwrap_or("keys/private_key.der"); - if std::path::Path::new(&private_key_path).exists() && get_fingerprint(private_key_path).is_some() { + if std::path::Path::new(&private_key_path).exists() + && get_fingerprint(private_key_path).is_some() + { Ok(private_key_path.to_string()) } else { anyhow::bail!("Signing key not found! Aborting startup! Please configure signing_key!"); diff --git a/clearing-house-app/tests/log.rs b/clearing-house-app/tests/log.rs index 8d5c615..77527aa 100644 --- a/clearing-house-app/tests/log.rs +++ b/clearing-house-app/tests/log.rs @@ -10,7 +10,6 @@ use clearing_house_app::model::ids::{IdsQueryResult, InfoModelId, MessageType}; use clearing_house_app::model::process::Receipt; use clearing_house_app::model::{claims::create_token, constants::SERVICE_HEADER}; use clearing_house_app::util::new_uuid; -use hyper::Body; use tower::ServiceExt; #[tokio::test] @@ -29,7 +28,7 @@ async fn log_message() { .oneshot( Request::builder() .uri("/.well-known/jwks.json") - .body(Body::empty()) + .body(axum::body::Body::empty()) .unwrap(), ) .await @@ -37,7 +36,9 @@ async fn log_message() { assert_eq!(response.status(), StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); assert!(!body.is_empty()); let jwks = serde_json::from_slice::>(&body).expect("Decoded the JWKSet"); @@ -77,7 +78,7 @@ async fn log_message() { .method("POST") .header("Content-Type", "application/json") .header(SERVICE_HEADER, create_token("test", "test", &claims)) - .body(serde_json::to_string(&msg).unwrap().into()) + .body(serde_json::to_string(&msg).unwrap()) .unwrap(), ) .await @@ -86,7 +87,9 @@ async fn log_message() { // Check status code assert_eq!(response.status(), StatusCode::CREATED); // get body - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); assert!(!body.is_empty()); // Decode receipt @@ -123,14 +126,16 @@ async fn log_message() { .method("POST") .header("Content-Type", "application/json") .header(SERVICE_HEADER, create_token("test", "test", &claims)) - .body(serde_json::to_string(&msg).unwrap().into()) + .body(serde_json::to_string(&msg).unwrap()) .unwrap(), ) .await .unwrap(); assert_eq!(query_resp.status(), StatusCode::OK); - let body = hyper::body::to_bytes(query_resp.into_body()).await.unwrap(); + let body = axum::body::to_bytes(query_resp.into_body(), usize::MAX) + .await + .unwrap(); assert!(!body.is_empty()); let ids_message = serde_json::from_slice::(&body).unwrap(); diff --git a/clearing-house-app/tests/public_key.rs b/clearing-house-app/tests/public_key.rs index 3aba051..e429685 100644 --- a/clearing-house-app/tests/public_key.rs +++ b/clearing-house-app/tests/public_key.rs @@ -22,7 +22,9 @@ async fn retrieve_public_key() { assert_eq!(response.status(), StatusCode::OK); - let body = hyper::body::to_bytes(response.into_body()).await.unwrap(); + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); assert!(!body.is_empty()); let jwks = serde_json::from_slice::>(&body).expect("Decoded the JWKSet"); println!("JWKS: {:?}", jwks); diff --git a/tests/smoke.js b/tests/smoke.js index 2c9aaff..ba7df1b 100644 --- a/tests/smoke.js +++ b/tests/smoke.js @@ -15,10 +15,6 @@ export default () => { 'ch-app GET jwks is status 200': (r) => r.status === 200, }); - const doctypeRes = http.get(`${url}/doctype`); - check(doctypeRes, { - 'ch-app GET doctype is status 200': (r) => r.status === 200, - }); const logMessageRes = http.post(`${url}/messages/log/6`, JSON.stringify(logMessage(), null, 2), { headers: header() }); check(logMessageRes, { From 60379b464c8e00591555462cce1d4820619b274f Mon Sep 17 00:00:00 2001 From: dhommen Date: Fri, 2 Feb 2024 17:31:34 +0100 Subject: [PATCH 168/183] fix(ci): change docker image tag to reflect new repo and impl job matrix --- .github/workflows/release-publish.yml | 40 +++++++++++---------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 4d0754f..1cc92f2 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -32,29 +32,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx semantic-release - - publish-ch-app: - needs: release - if: ${{ needs.release.outputs.new_tag_version != '' }} - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Login to GitHub Container Registry - run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - - name: Build Docker image - env: - DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} - run: cd clearing-house-app && docker build -t ghcr.io/truzzt/ids-basecamp-clearing/ch-app:$DOCKER_IMAGE_TAG . - - - name: Push Docker image - env: - DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} - run: docker push ghcr.io/truzzt/ids-basecamp-clearing/ch-app:$DOCKER_IMAGE_TAG - - publish-ch-edc: + publish-docker-images: runs-on: ubuntu-latest needs: release if: ${{ needs.release.outputs.new_tag_version != '' }} @@ -63,6 +42,16 @@ jobs: packages: write id-token: write + strategy: + matrix: + include: + - context: "ch-app" + directory: "clearing-house-app" + dockerfile: "Dockerfile" + - context: "ch-edc" + directory: "clearing-house-edc" + dockerfile: "launchers/connector-prod/Dockerfile" + steps: - name: Checkout repository uses: actions/checkout@v3 @@ -73,9 +62,12 @@ jobs: - name: Build Docker image env: DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} - run: cd clearing-house-edc && docker build -t ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$DOCKER_IMAGE_TAG -f launchers/connector-prod/Dockerfile . + run: | + cd ${{ matrix.directory }} + docker build -t ghcr.io/${{ github.repository }}/${{ matrix.context }}:$DOCKER_IMAGE_TAG -f ${{ matrix.dockerfile }} . - name: Push Docker image env: DOCKER_IMAGE_TAG: ${{ needs.release.outputs.new_tag_version }} - run: docker push ghcr.io/truzzt/ids-basecamp-clearing/ch-edc:$DOCKER_IMAGE_TAG + run: docker push ghcr.io/${{ github.repository }}/${{ matrix.context }}:$DOCKER_IMAGE_TAG + From 5b0b15cdf5f44ffe6e38b556c6573d19a9ffce7e Mon Sep 17 00:00:00 2001 From: dhommen Date: Sat, 3 Feb 2024 09:21:02 +0100 Subject: [PATCH 169/183] fix: changed repository in package.json --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6e63ac4..e36b822 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ "start": "docker compose -f docker/docker-compose.yml up -d" }, "bugs": { - "url": "https://github.com/truzzt/ids-basecamp-clearinghouse/issues" + "url": "https://github.com/ids-basecamp/clearinghouse/issues" }, "keywords": [], - "repository": "https://github.com/truzzt/ids-basecamp-clearinghouse", + "repository": "https://github.com/ids-basecamp/clearinghouse", "author": "Maximilian Schönenberg, Daniel Hommen", "license": "Apache-2.0", "devDependencies": { From b2678aaa49bb9d2d0259413567704b7670635bc1 Mon Sep 17 00:00:00 2001 From: dhommen Date: Sat, 3 Feb 2024 09:29:35 +0100 Subject: [PATCH 170/183] fix: GITHUB_TOKEN permissions for release job --- .github/workflows/release-publish.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 1cc92f2..ff15867 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -8,6 +8,10 @@ on: jobs: release: runs-on: ubuntu-latest + permissions: + contents: write + packages: write + id-token: write outputs: new_tag_version: ${{ steps.tag_version.outputs.new_tag_version }} steps: From b91926cd6dbde60e1e13813949587d3a6f3e3f4c Mon Sep 17 00:00:00 2001 From: dhommen Date: Sat, 3 Feb 2024 09:40:09 +0100 Subject: [PATCH 171/183] fix(ch-app): copy migrations in Dockerfile --- clearing-house-app/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/clearing-house-app/Dockerfile b/clearing-house-app/Dockerfile index 7006a9d..12dec4a 100644 --- a/clearing-house-app/Dockerfile +++ b/clearing-house-app/Dockerfile @@ -8,6 +8,7 @@ WORKDIR /usr/src/chapp COPY Cargo.toml Cargo.lock config.toml ./ # Copy the source code into the container +COPY migrations ./migrations COPY src ./src # Build the Rust application with dependencies (this helps to cache dependencies) From 0571bd1d720d89d9c3b9d3758d70197faca4f04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Wed, 14 Feb 2024 17:38:44 +0100 Subject: [PATCH 172/183] fix(ch-app): Fixed uuid <-> str mismatch in document, which resulted in failed query --- clearing-house-app/src/db/mod.rs | 2 +- clearing-house-app/src/db/mongo_doc_store.rs | 8 ++++---- clearing-house-app/src/db/postgres_document_store.rs | 7 +++---- clearing-house-app/src/model/document.rs | 10 ++-------- clearing-house-app/src/model/ids/message.rs | 2 +- clearing-house-app/src/services/document_service.rs | 2 +- 6 files changed, 12 insertions(+), 19 deletions(-) diff --git a/clearing-house-app/src/db/mod.rs b/clearing-house-app/src/db/mod.rs index 2e0beef..782737b 100644 --- a/clearing-house-app/src/db/mod.rs +++ b/clearing-house-app/src/db/mod.rs @@ -38,7 +38,7 @@ pub(crate) trait ProcessStore { pub(crate) trait DocumentStore { async fn add_document(&self, doc: Document) -> anyhow::Result; - async fn exists_document(&self, id: &str) -> anyhow::Result; + async fn exists_document(&self, id: &uuid::Uuid) -> anyhow::Result; async fn get_document(&self, id: &str, pid: &str) -> anyhow::Result>; async fn get_documents_for_pid( &self, diff --git a/clearing-house-app/src/db/mongo_doc_store.rs b/clearing-house-app/src/db/mongo_doc_store.rs index d6037b4..c8d4c1b 100644 --- a/clearing-house-app/src/db/mongo_doc_store.rs +++ b/clearing-house-app/src/db/mongo_doc_store.rs @@ -266,16 +266,16 @@ impl super::DocumentStore for MongoDocumentStore { /// checks if the document exists /// document ids are globally unique #[tracing::instrument(skip_all)] - async fn exists_document(&self, id: &str) -> anyhow::Result { - debug!("Check if document with id '{}' exists...", id); - let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id}; + async fn exists_document(&self, id: &uuid::Uuid) -> anyhow::Result { + debug!("Check if document with id '{}' exists...", id.to_string()); + let query = doc! {format!("{}.{}", MONGO_DOC_ARRAY, MONGO_ID): id.to_string()}; let coll = self .database .collection::(MONGO_COLL_DOCUMENT_BUCKET); match coll.count_documents(Some(query), None).await? { 0 => { - debug!("Document with id '{}' does not exist!", &id); + debug!("Document with id '{}' does not exist!", &id.to_string()); Ok(false) } _ => { diff --git a/clearing-house-app/src/db/postgres_document_store.rs b/clearing-house-app/src/db/postgres_document_store.rs index 2ee7b75..871656f 100644 --- a/clearing-house-app/src/db/postgres_document_store.rs +++ b/clearing-house-app/src/db/postgres_document_store.rs @@ -55,7 +55,7 @@ impl super::DocumentStore for PostgresDocumentStore { Ok(true) } - async fn exists_document(&self, id: &str) -> anyhow::Result { + async fn exists_document(&self, id: &uuid::Uuid) -> anyhow::Result { sqlx::query("SELECT id FROM documents WHERE id = $1") .bind(id) .fetch_optional(&self.db) @@ -141,9 +141,8 @@ struct DocumentRow { impl From for DocumentRow { fn from(value: Document) -> Self { - use std::str::FromStr; Self { - id: uuid::Uuid::from_str(&value.id).unwrap(), + id: value.id, process_id: value.pid, created_at: value.ts.naive_utc(), model_version: value.content.model_version, @@ -167,7 +166,7 @@ impl Into for DocumentRow { use chrono::TimeZone; Document { - id: self.id.to_string(), + id: self.id, pid: self.process_id, ts: chrono::Local.from_utc_datetime(&self.created_at), content: crate::model::ids::message::IdsMessage { diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index cd3dc1a..2617090 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -1,14 +1,11 @@ -use crate::util::new_uuid; use chrono::Local; use crate::model::ids::message::IdsMessage; -use uuid::Uuid; #[derive(Clone, serde::Serialize, serde::Deserialize, Debug)] pub struct Document { /// Document id - #[serde(default = "new_uuid")] - pub id: String, + pub id: uuid::Uuid, /// Process ID pub pid: String, /// timestamp: unix timestamp @@ -19,13 +16,10 @@ pub struct Document { /// Documents should have a globally unique id, setting the id manually is discouraged. impl Document { - pub fn create_uuid() -> String { - Uuid::new_v4().hyphenated().to_string() - } pub fn new(pid: String, content: IdsMessage) -> Self { Self { - id: Document::create_uuid(), + id: uuid::Uuid::new_v4(), pid, ts: Local::now(), content, diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index 02b2837..46ad6c4 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -189,6 +189,6 @@ fn autogen(message: &str) -> String { format!( "https://w3id.org/idsa/autogen/{}/{}", message, - Document::create_uuid() + uuid::Uuid::new_v4() ) } diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 84bf72a..4620224 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -76,7 +76,7 @@ impl DocumentService { } _ => { // prepare the success result message - let receipt = DocumentReceipt::new(doc.ts, &doc.pid, &doc.id); + let receipt = DocumentReceipt::new(doc.ts, &doc.pid, &doc.id.to_string()); trace!("storing document ...."); // store document From f3f0849e41ce719589c66288acca9039c2b55459 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 19 Feb 2024 10:17:15 +0000 Subject: [PATCH 173/183] chore(release): 1.0.0-beta.2 [skip ci] # [1.0.0-beta.2](https://github.com/ids-basecamp/clearinghouse/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2024-02-19) ### Bug Fixes * **ch-app:** copy migrations in Dockerfile ([b91926c](https://github.com/ids-basecamp/clearinghouse/commit/b91926cd6dbde60e1e13813949587d3a6f3e3f4c)) * **ch-app:** Fix 3 vulnerabilitites: GHSA-rjhf-4mh8-9xjq, GHSA-xphf-cx8h-7q9g, GHSA-3mv5-343c-w2qg ([2ca4dfa](https://github.com/ids-basecamp/clearinghouse/commit/2ca4dfae59aa65061f818d579d81eb7f09325576)) * **ch-app:** Fixed uuid <-> str mismatch in document, which resulted in failed query ([0571bd1](https://github.com/ids-basecamp/clearinghouse/commit/0571bd1d720d89d9c3b9d3758d70197faca4f04c)) * changed repository in package.json ([5b0b15c](https://github.com/ids-basecamp/clearinghouse/commit/5b0b15cdf5f44ffe6e38b556c6573d19a9ffce7e)) * **ci:** change docker image tag to reflect new repo and impl job matrix ([60379b4](https://github.com/ids-basecamp/clearinghouse/commit/60379b464c8e00591555462cce1d4820619b274f)) * disable tokenFormat check ([c920b82](https://github.com/ids-basecamp/clearinghouse/commit/c920b825219edeae317d874f6cb723d1016ecabc)) * GITHUB_TOKEN permissions for release job ([b2678aa](https://github.com/ids-basecamp/clearinghouse/commit/b2678aaa49bb9d2d0259413567704b7670635bc1)) ### Features * **ch-app:** Add postgres implementation ([#96](https://github.com/ids-basecamp/clearinghouse/issues/96)) ([842ff00](https://github.com/ids-basecamp/clearinghouse/commit/842ff0058b0b6d1ca4b3d62a6747d0bfcf025bb8)) * **ch-app:** Implement [#91](https://github.com/ids-basecamp/clearinghouse/issues/91) ([965b4c2](https://github.com/ids-basecamp/clearinghouse/commit/965b4c2cbba0580006f9e40834470f3e225354b6)) --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b27d34c..0b29dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +# [1.0.0-beta.2](https://github.com/ids-basecamp/clearinghouse/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2024-02-19) + + +### Bug Fixes + +* **ch-app:** copy migrations in Dockerfile ([b91926c](https://github.com/ids-basecamp/clearinghouse/commit/b91926cd6dbde60e1e13813949587d3a6f3e3f4c)) +* **ch-app:** Fix 3 vulnerabilitites: GHSA-rjhf-4mh8-9xjq, GHSA-xphf-cx8h-7q9g, GHSA-3mv5-343c-w2qg ([2ca4dfa](https://github.com/ids-basecamp/clearinghouse/commit/2ca4dfae59aa65061f818d579d81eb7f09325576)) +* **ch-app:** Fixed uuid <-> str mismatch in document, which resulted in failed query ([0571bd1](https://github.com/ids-basecamp/clearinghouse/commit/0571bd1d720d89d9c3b9d3758d70197faca4f04c)) +* changed repository in package.json ([5b0b15c](https://github.com/ids-basecamp/clearinghouse/commit/5b0b15cdf5f44ffe6e38b556c6573d19a9ffce7e)) +* **ci:** change docker image tag to reflect new repo and impl job matrix ([60379b4](https://github.com/ids-basecamp/clearinghouse/commit/60379b464c8e00591555462cce1d4820619b274f)) +* disable tokenFormat check ([c920b82](https://github.com/ids-basecamp/clearinghouse/commit/c920b825219edeae317d874f6cb723d1016ecabc)) +* GITHUB_TOKEN permissions for release job ([b2678aa](https://github.com/ids-basecamp/clearinghouse/commit/b2678aaa49bb9d2d0259413567704b7670635bc1)) + + +### Features + +* **ch-app:** Add postgres implementation ([#96](https://github.com/ids-basecamp/clearinghouse/issues/96)) ([842ff00](https://github.com/ids-basecamp/clearinghouse/commit/842ff0058b0b6d1ca4b3d62a6747d0bfcf025bb8)) +* **ch-app:** Implement [#91](https://github.com/ids-basecamp/clearinghouse/issues/91) ([965b4c2](https://github.com/ids-basecamp/clearinghouse/commit/965b4c2cbba0580006f9e40834470f3e225354b6)) + # 1.0.0-beta.1 (2023-11-23) From 679b06b95d8e7ac58019fd21e678c6725c79083e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 29 Feb 2024 08:44:23 +0100 Subject: [PATCH 174/183] feat(ch-app): Add testcontainers for Integration tests with database --- .github/workflows/test.yml | 4 +- clearing-house-app/Cargo.lock | 306 +++++++++++++++---------- clearing-house-app/Cargo.toml | 2 + clearing-house-app/tests/README.md | 2 +- clearing-house-app/tests/log.rs | 10 +- clearing-house-app/tests/public_key.rs | 10 +- 6 files changed, 202 insertions(+), 132 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f16c91..4cc5188 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,8 +24,8 @@ jobs: - name: Build and Test run: | cd clearing-house-app - cargo build --verbose - cargo test --verbose + cargo build + cargo test edc-unit-tests: runs-on: ubuntu-latest diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index d689c26..575e057 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" +checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" dependencies = [ "cfg-if", "getrandom", @@ -242,17 +242,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bollard-stubs" +version = "1.42.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" +dependencies = [ + "serde", + "serde_with", +] + [[package]] name = "bson" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c18b51216e1f74b9d769cead6ace2f82b965b807e3d73330aabe9faec31c84" +checksum = "ce21468c1c9c154a85696bb25c20582511438edb6ad67f846ba1378ffdd80222" dependencies = [ "ahash", "base64 0.13.1", "bitvec", "hex", - "indexmap 1.9.3", + "indexmap", "js-sys", "once_cell", "rand", @@ -298,9 +308,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.32" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "android-tzdata", "iana-time-zone", @@ -334,6 +344,8 @@ dependencies = [ "serial_test", "sqlx", "tempfile", + "testcontainers", + "testcontainers-modules", "thiserror", "tokio", "tower", @@ -475,7 +487,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown", "lock_api", "once_cell", "parking_lot_core", @@ -561,9 +573,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" dependencies = [ "serde", ] @@ -832,7 +844,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.11", - "indexmap 2.1.0", + "indexmap", "slab", "tokio", "tokio-util", @@ -851,19 +863,13 @@ dependencies = [ "futures-sink", "futures-util", "http 1.0.0", - "indexmap 2.1.0", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.3" @@ -880,7 +886,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -894,9 +900,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d3d0e0f38255e7fa3cf31335b3a56f05febd18025f4db5ef7a0cfb4f8da651f" +checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" [[package]] name = "hex" @@ -931,6 +937,29 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "hoot" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df22a4d90f1b0e65fe3e0d6ee6a4608cc4d81f4b2eb3e670f44bb6bde711e452" +dependencies = [ + "httparse", + "log", +] + +[[package]] +name = "hootbin" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "354e60868e49ea1a39c44b9562ad207c4259dc6eabf9863bf3b0f058c55cfdb2" +dependencies = [ + "fastrand", + "hoot", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "hostname" version = "0.3.1" @@ -1069,12 +1098,11 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdea9aac0dbe5a9240d68cfd9501e2db94222c6dc06843e06640b9e07f0fdc67" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", - "futures-channel", "futures-util", "http 1.0.0", "http-body 1.0.0", @@ -1082,14 +1110,13 @@ dependencies = [ "pin-project-lite", "socket2 0.5.5", "tokio", - "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.59" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1137,22 +1164,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.1.0" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown", ] [[package]] @@ -1175,9 +1192,9 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] @@ -1190,9 +1207,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -1208,9 +1225,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libm" @@ -1323,9 +1340,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -1343,9 +1360,9 @@ dependencies = [ [[package]] name = "mongodb" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c30763a5c6c52079602be44fa360ca3bfacee55fca73f4734aecd23706a7f2" +checksum = "de59562e5c71656c098d8e966641b31da87b89dc3dcb6e761d3b37dcdfa0cb72" dependencies = [ "async-trait", "base64 0.13.1", @@ -1468,21 +1485,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", @@ -1491,9 +1513,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -1659,18 +1681,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" dependencies = [ "proc-macro2", "quote", @@ -1712,9 +1734,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "powerfmt" @@ -1805,7 +1827,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.4", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -1820,9 +1842,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -1843,9 +1865,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.23" +version = "0.11.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" dependencies = [ "base64 0.21.7", "bytes", @@ -1865,9 +1887,11 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", "system-configuration", "tokio", "tokio-native-tls", @@ -1974,9 +1998,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.30" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322394588aaf33c24007e8bb3238ee3e4c5c09c084ab32bc73890b99ff326bca" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ "bitflags 2.4.2", "errno", @@ -2099,9 +2123,9 @@ checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "sentry" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab18211f62fb890f27c9bb04861f76e4be35e4c2fcbfc2d98afa37aadebb16f1" +checksum = "766448f12e44d68e675d5789a261515c46ac6ccd240abdd451a9c46c84a49523" dependencies = [ "httpdate", "native-tls", @@ -2118,9 +2142,9 @@ dependencies = [ [[package]] name = "sentry-backtrace" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf018ff7d5ce5b23165a9cbfee60b270a55ae219bc9eebef2a3b6039356dd7e5" +checksum = "32701cad8b3c78101e1cd33039303154791b0ff22e7802ed8cc23212ef478b45" dependencies = [ "backtrace", "once_cell", @@ -2130,9 +2154,9 @@ dependencies = [ [[package]] name = "sentry-contexts" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d934df6f9a17b8c15b829860d9d6d39e78126b5b970b365ccbd817bc0fe82c9" +checksum = "17ddd2a91a13805bd8dab4ebf47323426f758c35f7bf24eacc1aded9668f3824" dependencies = [ "hostname", "libc", @@ -2144,9 +2168,9 @@ dependencies = [ [[package]] name = "sentry-core" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e362d3fb1c5de5124bf1681086eaca7adf6a8c4283a7e1545359c729f9128ff" +checksum = "b1189f68d7e7e102ef7171adf75f83a59607fafd1a5eecc9dc06c026ff3bdec4" dependencies = [ "once_cell", "rand", @@ -2157,9 +2181,9 @@ dependencies = [ [[package]] name = "sentry-debug-images" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bca420d75d9e7a8e54a4806bf4fa8a7e9a804e8f2ff05c7c80234168c6ca66" +checksum = "7b4d0a615e5eeca5699030620c119a094e04c14cf6b486ea1030460a544111a7" dependencies = [ "findshlibs", "once_cell", @@ -2168,9 +2192,9 @@ dependencies = [ [[package]] name = "sentry-panic" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0224e7a8e2bd8a32d96804acb8243d6d6e073fed55618afbdabae8249a964d8" +checksum = "d1c18d0b5fba195a4950f2f4c31023725c76f00aabb5840b7950479ece21b5ca" dependencies = [ "sentry-backtrace", "sentry-core", @@ -2178,9 +2202,9 @@ dependencies = [ [[package]] name = "sentry-tracing" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087bed8c616d176a9c6b662a8155e5f23b40dc9e1fa96d0bd5fb56e8636a9275" +checksum = "3012699a9957d7f97047fd75d116e22d120668327db6e7c59824582e16e791b2" dependencies = [ "sentry-backtrace", "sentry-core", @@ -2190,9 +2214,9 @@ dependencies = [ [[package]] name = "sentry-types" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb4f0e37945b7a8ce7faebc310af92442e2d7c5aa7ef5b42fe6daa98ee133f65" +checksum = "c7173fd594569091f68a7c37a886e202f4d0c1db1e1fa1d18a051ba695b2e2ec" dependencies = [ "debugid", "hex", @@ -2207,9 +2231,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] @@ -2225,9 +2249,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.195" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", @@ -2236,11 +2260,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.111" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ - "indexmap 2.1.0", + "indexmap", "itoa", "ryu", "serde", @@ -2483,7 +2507,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.1.0", + "indexmap", "log", "memchr", "once_cell", @@ -2741,31 +2765,56 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.9.0" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", "windows-sys 0.52.0", ] +[[package]] +name = "testcontainers" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" +dependencies = [ + "bollard-stubs", + "futures", + "hex", + "hmac", + "log", + "rand", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "testcontainers-modules" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c391cd115649a8a14e5638d0606648d5348b216700a31f402987f57e58693766" +dependencies = [ + "testcontainers", +] + [[package]] name = "thiserror" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.56" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", @@ -2784,12 +2833,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2804,10 +2854,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -2828,9 +2879,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -3112,9 +3163,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode_categories" @@ -3136,11 +3187,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.1" +version = "2.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +checksum = "0b52731d03d6bb2fd18289d4028aee361d6c28d44977846793b994b13cdcc64d" dependencies = [ "base64 0.21.7", + "hootbin", "log", "native-tls", "once_cell", @@ -3210,9 +3262,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3220,9 +3272,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", @@ -3235,9 +3287,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" dependencies = [ "cfg-if", "js-sys", @@ -3247,9 +3299,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3257,9 +3309,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", @@ -3270,15 +3322,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.90" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "web-sys" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" dependencies = [ "js-sys", "wasm-bindgen", @@ -3286,9 +3338,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "whoami" diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 1af3c5d..1296be9 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -60,6 +60,8 @@ serial_test = "3" tempfile = "3.8" tower = { version = "0.4", features = ["util"] } hyper = { version = "1", features = ["full"] } +testcontainers = "0.15.0" +testcontainers-modules = { version = "0.3.4", features = ["postgres"] } [features] default = ["postgres"] diff --git a/clearing-house-app/tests/README.md b/clearing-house-app/tests/README.md index 52e2628..69608ed 100644 --- a/clearing-house-app/tests/README.md +++ b/clearing-house-app/tests/README.md @@ -2,4 +2,4 @@ Prerequisites: -- MongoDB running on `localhost:27017` \ No newline at end of file +- Docker Deamon running and Docker CLI installed \ No newline at end of file diff --git a/clearing-house-app/tests/log.rs b/clearing-house-app/tests/log.rs index 77527aa..785a6d8 100644 --- a/clearing-house-app/tests/log.rs +++ b/clearing-house-app/tests/log.rs @@ -13,12 +13,20 @@ use clearing_house_app::util::new_uuid; use tower::ServiceExt; #[tokio::test] -#[ignore] async fn log_message() { + // Start testcontainer: Postgres + let docker = testcontainers::clients::Cli::default(); + let postgres_instance = docker.run(testcontainers_modules::postgres::Postgres::default()); + let connection_string = format!( + "postgres://postgres:postgres@127.0.0.1:{}/postgres", + postgres_instance.get_host_port_ipv4(5432) + ); + std::env::set_var("SERVICE_ID_LOG", "test"); std::env::set_var("SHARED_SECRET", "test"); std::env::set_var("CH_APP_LOG_LEVEL", "TRACE"); std::env::set_var("CH_APP_CLEAR_DB", "false"); + std::env::set_var("CH_APP_DATABASE_URL", connection_string); let app = clearing_house_app::app().await.unwrap(); diff --git a/clearing-house-app/tests/public_key.rs b/clearing-house-app/tests/public_key.rs index e429685..29724f0 100644 --- a/clearing-house-app/tests/public_key.rs +++ b/clearing-house-app/tests/public_key.rs @@ -4,9 +4,17 @@ use biscuit::jwk::JWKSet; use tower::ServiceExt; #[tokio::test] -#[ignore] async fn retrieve_public_key() { + // Start testcontainer: Postgres + let docker = testcontainers::clients::Cli::default(); + let postgres_instance = docker.run(testcontainers_modules::postgres::Postgres::default()); + let connection_string = format!( + "postgres://postgres:postgres@127.0.0.1:{}/postgres", + postgres_instance.get_host_port_ipv4(5432) + ); + std::env::set_var("SERVICE_ID_LOG", "test"); + std::env::set_var("CH_APP_DATABASE_URL", connection_string); let app = clearing_house_app::app().await.unwrap(); From 8cfb5e18feea759aeb4425cb900453f86f07c15f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 29 Feb 2024 10:10:35 +0100 Subject: [PATCH 175/183] feat(ch-app): Add create_process test and fix an issue --- .../src/services/logging_service.rs | 30 ++- clearing-house-app/tests/create_process.rs | 233 ++++++++++++++++++ clearing-house-app/tests/log.rs | 2 +- 3 files changed, 252 insertions(+), 13 deletions(-) create mode 100644 clearing-house-app/tests/create_process.rs diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index a82b39f..de6b005 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -109,21 +109,27 @@ impl LoggingService { }?; // Check if process exists and if the user is authorized to access the process - if let Err(LoggingServiceError::ProcessDoesNotExist(_)) = - self.get_process_and_check_authorized(&pid, user).await + match self.get_process_and_check_authorized(&pid, user).await { - // convenience: if process does not exist, we create it but only if no error occurred before - info!("Requested pid '{}' does not exist. Creating...", &pid); - // create a new process - let new_process = Process::new(pid.clone(), vec![user.clone()]); + Err(LoggingServiceError::ProcessDoesNotExist(_)) => { + // convenience: if process does not exist, we create it but only if no error occurred before + info!("Requested pid '{}' does not exist. Creating...", &pid); + // create a new process + let new_process = Process::new(pid.clone(), vec![user.clone()]); - if let Err(e) = self.db.store_process(new_process).await { - error!("Error while creating process '{}'", &pid); - return Err(LoggingServiceError::DatabaseError { - source: e, - description: "Creating process failed".to_string(), - }); // InternalError + if let Err(e) = self.db.store_process(new_process).await { + error!("Error while creating process '{}'", & pid); + return Err(LoggingServiceError::DatabaseError { + source: e, + description: "Creating process failed".to_string(), + }); // InternalError + } + } + Err(e) => { + warn!("Error while checking process: {:?}", e); + return Err(e); } + Ok(_) => {} } // transform message to document diff --git a/clearing-house-app/tests/create_process.rs b/clearing-house-app/tests/create_process.rs new file mode 100644 index 0000000..d7ee88c --- /dev/null +++ b/clearing-house-app/tests/create_process.rs @@ -0,0 +1,233 @@ +#![cfg(test)] + +use axum::http::{Request, StatusCode}; +use biscuit::jwa::SignatureAlgorithm::PS512; +use biscuit::jwk::JWKSet; +use clearing_house_app::model::claims::{get_fingerprint, ChClaims}; +use clearing_house_app::model::ids::message::IdsMessage; +use clearing_house_app::model::ids::request::ClearingHouseMessage; +use clearing_house_app::model::ids::{IdsQueryResult, InfoModelId, MessageType}; +use clearing_house_app::model::process::{OwnerList, Receipt}; +use clearing_house_app::model::{claims::create_token, constants::SERVICE_HEADER}; +use clearing_house_app::util::new_uuid; +use tower::ServiceExt; + +#[tokio::test] +async fn log_message() { + const CLIENT_ID: &str = "69:F5:9D:B0:DD:A6:9D:30:5F:58:AA:2D:20:4D:B2:39:F0:54:FC:3B:keyid:4F:66:7D:BD:08:EE:C6:4A:D1:96:D8:7C:6C:A2:32:8A:EC:A6:AD:49"; + + // Start testcontainer: Postgres + let docker = testcontainers::clients::Cli::default(); + let postgres_instance = docker.run(testcontainers_modules::postgres::Postgres::default()); + let connection_string = format!( + "postgres://postgres:postgres@127.0.0.1:{}/postgres", + postgres_instance.get_host_port_ipv4(5432) + ); + + std::env::set_var("SERVICE_ID_LOG", "test"); + std::env::set_var("SHARED_SECRET", "test"); + std::env::set_var("CH_APP_LOG_LEVEL", "TRACE"); + std::env::set_var("CH_APP_CLEAR_DB", "false"); + std::env::set_var("CH_APP_DATABASE_URL", connection_string); + + let app = clearing_house_app::app().await.unwrap(); + + // Prerequisite JWKS for checking the signature + let response = app + .clone() + .oneshot( + Request::builder() + .uri("/.well-known/jwks.json") + .body(axum::body::Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + assert!(!body.is_empty()); + let jwks = serde_json::from_slice::>(&body).expect("Decoded the JWKSet"); + + // --------------------------------------------------------------------------------------------- + + // Create a process + let pid = new_uuid(); + let id = new_uuid(); + + let process_owners = OwnerList { + owners: vec![CLIENT_ID.to_string()], + }; + let process_owners_payload = serde_json::to_string(&process_owners).expect("Should serialize"); + + let msg = ClearingHouseMessage { + header: IdsMessage { + context: Some(std::collections::HashMap::from([ + ("ids".to_string(), "https://w3id.org/idsa/core/".to_string()), + ( + "idsc".to_string(), + "https://w3id.org/idsa/code/".to_string(), + ), + ])), + type_message: MessageType::RequestMessage, + id: Some(id.clone()), + model_version: "test".to_string(), + issuer_connector: InfoModelId::new("test-connector".to_string()), + sender_agent: "https://w3id.org/idsa/core/ClearingHouse".to_string(), + ..Default::default() + }, + payload: Some(process_owners_payload), + payload_type: None, + }; + + let claims = ChClaims::new(CLIENT_ID); + + // Send create process message + let response = app + .clone() + .oneshot( + Request::builder() + .uri(format!("/process/{}", pid)) + .method("POST") + .header("Content-Type", "application/json") + .header(SERVICE_HEADER, create_token("test", "test", &claims)) + .body(serde_json::to_string(&msg).unwrap()) + .unwrap(), + ) + .await + .unwrap(); + + // Check status code + assert_eq!(response.status(), StatusCode::CREATED); + + // --------------------------------------------------------------------------------------------- + + // Send authorized log message + + let log_msg = ClearingHouseMessage { + header: IdsMessage { + context: Some(std::collections::HashMap::from([ + ("ids".to_string(), "https://w3id.org/idsa/core/".to_string()), + ( + "idsc".to_string(), + "https://w3id.org/idsa/code/".to_string(), + ), + ])), + type_message: MessageType::LogMessage, + id: Some(id.clone()), + model_version: "test".to_string(), + issuer_connector: InfoModelId::new("test-connector".to_string()), + sender_agent: "https://w3id.org/idsa/core/ClearingHouse".to_string(), + ..Default::default() + }, + payload: Some("test".to_string()), + payload_type: None, + }; + + // Send log message + let log_response = app + .clone() + .oneshot( + Request::builder() + .uri(format!("/messages/log/{}", pid)) + .method("POST") + .header("Content-Type", "application/json") + .header(SERVICE_HEADER, create_token("test", "test", &claims)) + .body(serde_json::to_string(&log_msg).unwrap()) + .unwrap(), + ) + .await + .unwrap(); + + // Check status code + assert_eq!(log_response.status(), StatusCode::CREATED); + // get body + let body = axum::body::to_bytes(log_response.into_body(), usize::MAX) + .await + .unwrap(); + assert!(!body.is_empty()); + + // Decode receipt + let receipt = serde_json::from_slice::(&body).unwrap(); + println!("Receipt: {:?}", receipt); + let decoded_receipt = receipt + .data + .decode_with_jwks(&jwks, Some(PS512)) + .expect("Decoding JWS successful"); + let decoded_receipt_header = decoded_receipt + .header() + .expect("Header is now already decoded"); + + assert_eq!( + decoded_receipt_header.registered.key_id, + get_fingerprint("keys/private_key.der") + ); + + let decoded_receipt_payload = decoded_receipt + .payload() + .expect("Payload is now already decoded"); + println!("Decoded Receipt: {:?}", decoded_receipt); + + assert_eq!(decoded_receipt_payload.process_id, pid); + assert_eq!(decoded_receipt_payload.payload, "test".to_string()); + + // --------------------------------------------------------------------------------------------- + + // Query ID + let query_resp = app + .clone() + .oneshot( + Request::builder() + .uri(format!("/messages/query/{}", pid)) + .method("POST") + .header("Content-Type", "application/json") + .header(SERVICE_HEADER, create_token("test", "test", &claims)) + .body(serde_json::to_string(&log_msg).unwrap()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(query_resp.status(), StatusCode::OK); + + let body = axum::body::to_bytes(query_resp.into_body(), usize::MAX) + .await + .unwrap(); + assert!(!body.is_empty()); + + let ids_message = serde_json::from_slice::(&body).unwrap(); + println!("IDS Query Result: {:?}", ids_message); + let query_docs = ids_message.documents; + + // Check the only document in the result + assert_eq!(query_docs.len(), 1); + let doc = query_docs + .first() + .expect("Document is there, just checked") + .to_owned(); + assert_eq!(doc.payload.expect("Payload is there"), "test".to_string()); + assert_eq!(doc.model_version, "test".to_string()); + + // --------------------------------------------------------------------------------------------- + + // Send unauthorized message + let unauthorized_claims = ChClaims::new("unauthorized"); + + // Send log message + let log_response_unauth = app + .oneshot( + Request::builder() + .uri(format!("/messages/log/{}", pid)) + .method("POST") + .header("Content-Type", "application/json") + .header(SERVICE_HEADER, create_token("test", "test", &unauthorized_claims)) + .body(serde_json::to_string(&log_msg).unwrap()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(log_response_unauth.status(), StatusCode::FORBIDDEN); +} \ No newline at end of file diff --git a/clearing-house-app/tests/log.rs b/clearing-house-app/tests/log.rs index 785a6d8..564520a 100644 --- a/clearing-house-app/tests/log.rs +++ b/clearing-house-app/tests/log.rs @@ -64,7 +64,7 @@ async fn log_message() { "https://w3id.org/idsa/code/".to_string(), ), ])), - type_message: MessageType::Message, + type_message: MessageType::LogMessage, id: Some(id.clone()), model_version: "test".to_string(), issuer_connector: InfoModelId::new("test-connector".to_string()), From df0a5d40ed50ea45f90383c21666b51fb89bdddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 14 Mar 2024 09:59:53 +0100 Subject: [PATCH 176/183] feat: enable pedantic linter and fix clippy findings where appropiate --- clearing-house-app/Cargo.lock | 405 ++++++++---------- clearing-house-app/Cargo.toml | 4 +- clearing-house-app/src/config.rs | 13 +- .../src/db/postgres_document_store.rs | 102 +++-- .../src/db/postgres_process_store.rs | 48 +-- clearing-house-app/src/lib.rs | 40 +- clearing-house-app/src/main.rs | 2 +- clearing-house-app/src/model/claims.rs | 50 ++- clearing-house-app/src/model/document.rs | 1 + clearing-house-app/src/model/ids/message.rs | 78 ++-- clearing-house-app/src/model/ids/mod.rs | 16 +- clearing-house-app/src/model/mod.rs | 39 +- clearing-house-app/src/model/process.rs | 7 + .../src/services/document_service.rs | 47 +- .../src/services/logging_service.rs | 26 +- clearing-house-app/src/util.rs | 17 +- 16 files changed, 466 insertions(+), 429 deletions(-) diff --git a/clearing-house-app/Cargo.lock b/clearing-house-app/Cargo.lock index 575e057..ec9a1cd 100644 --- a/clearing-house-app/Cargo.lock +++ b/clearing-house-app/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.8" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "getrandom", @@ -62,9 +62,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "async-trait" @@ -74,7 +74,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -86,16 +86,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic-write-file" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436" -dependencies = [ - "nix", - "rand", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -112,10 +102,10 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", "http-body-util", - "hyper 1.1.0", + "hyper 1.2.0", "hyper-util", "itoa", "matchit", @@ -145,7 +135,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", "http-body-util", "mime", @@ -275,9 +265,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "byteorder" @@ -293,12 +283,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" -dependencies = [ - "libc", -] +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" @@ -308,15 +295,15 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", "serde", - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -331,7 +318,7 @@ dependencies = [ "chrono", "config", "futures", - "hyper 1.1.0", + "hyper 1.2.0", "mongodb", "num-bigint", "once_cell", @@ -772,7 +759,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -843,7 +830,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 0.2.11", + "http 0.2.12", "indexmap", "slab", "tokio", @@ -862,7 +849,7 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http 1.0.0", + "http 1.1.0", "indexmap", "slab", "tokio", @@ -900,9 +887,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5256b483761cd23699d0da46cc6fd2ee3be420bbe6d020ae4a091e70b7e9fd" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -937,29 +924,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "hoot" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df22a4d90f1b0e65fe3e0d6ee6a4608cc4d81f4b2eb3e670f44bb6bde711e452" -dependencies = [ - "httparse", - "log", -] - -[[package]] -name = "hootbin" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "354e60868e49ea1a39c44b9562ad207c4259dc6eabf9863bf3b0f058c55cfdb2" -dependencies = [ - "fastrand", - "hoot", - "serde", - "serde_json", - "thiserror", -] - [[package]] name = "hostname" version = "0.3.1" @@ -973,9 +937,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", @@ -984,9 +948,9 @@ dependencies = [ [[package]] name = "http" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1000,7 +964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http 0.2.11", + "http 0.2.12", "pin-project-lite", ] @@ -1011,18 +975,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http 1.0.0", + "http 1.1.0", ] [[package]] name = "http-body-util" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", - "futures-util", - "http 1.0.0", + "futures-core", + "http 1.1.0", "http-body 1.0.0", "pin-project-lite", ] @@ -1050,13 +1014,13 @@ dependencies = [ "futures-core", "futures-util", "h2 0.3.24", - "http 0.2.11", + "http 0.2.12", "http-body 0.4.6", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -1065,20 +1029,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5aa53871fc917b1a9ed87b683a5d86db645e23acb32c2e0785a353e522fb75" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" dependencies = [ "bytes", "futures-channel", "futures-util", "h2 0.4.2", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", "httparse", "httpdate", "itoa", "pin-project-lite", + "smallvec", "tokio", "want", ] @@ -1104,11 +1069,11 @@ checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", "futures-util", - "http 1.0.0", + "http 1.1.0", "http-body 1.0.0", - "hyper 1.1.0", + "hyper 1.2.0", "pin-project-lite", - "socket2 0.5.5", + "socket2 0.5.6", "tokio", ] @@ -1164,9 +1129,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown", @@ -1178,7 +1143,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.5", + "socket2 0.5.6", "widestring", "windows-sys 0.48.0", "winreg", @@ -1207,9 +1172,9 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "js-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1270,9 +1235,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lru-cache" @@ -1349,9 +1314,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", @@ -1426,17 +1391,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.4.2", - "cfg-if", - "libc", -] - [[package]] name = "nom" version = "7.1.3" @@ -1561,9 +1515,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.63" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ "bitflags 2.4.2", "cfg-if", @@ -1582,7 +1536,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1593,9 +1547,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.99" +version = "0.9.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" dependencies = [ "cc", "libc", @@ -1605,13 +1559,13 @@ dependencies = [ [[package]] name = "os_info" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +checksum = "52a07930afc1bd77ac9e1101dc18d3fc4986c6568e939c31d1c26657eb0ccbf5" dependencies = [ "log", "serde", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1681,22 +1635,22 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -1752,9 +1706,9 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1827,7 +1781,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.5", + "regex-automata 0.4.6", "regex-syntax 0.8.2", ] @@ -1842,9 +1796,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1865,9 +1819,9 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "reqwest" -version = "0.11.24" +version = "0.11.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6920094eb85afde5e4a138be3f2de8bbdf28000f0029e72c45025a56b042251" +checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" dependencies = [ "base64 0.21.7", "bytes", @@ -1875,7 +1829,7 @@ dependencies = [ "futures-core", "futures-util", "h2 0.3.24", - "http 0.2.11", + "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", "hyper-tls", @@ -1930,16 +1884,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -1983,7 +1938,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.21", + "semver 1.0.22", ] [[package]] @@ -2016,7 +1971,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.7", + "ring 0.17.8", "rustls-webpki", "sct", ] @@ -2036,7 +1991,7 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -2048,9 +2003,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "schannel" @@ -2073,7 +2028,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.7", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -2111,9 +2066,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" [[package]] name = "semver-parser" @@ -2231,9 +2186,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] @@ -2249,20 +2204,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.196" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] name = "serde_json" -version = "1.0.113" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "indexmap", "itoa", @@ -2272,9 +2227,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd154a240de39fdebcf5775d2675c204d7c13cf39a4c697be6493c8e734337c" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ "itoa", "serde", @@ -2336,7 +2291,7 @@ checksum = "b93fb4adc70021ac1b47f7d45e8cc4169baaa7ea58483bc5b721d19a26202212" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -2427,12 +2382,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2473,9 +2428,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ "sqlx-core", "sqlx-macros", @@ -2486,9 +2441,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ "ahash", "atoi", @@ -2497,7 +2452,6 @@ dependencies = [ "chrono", "crc", "crossbeam-queue", - "dotenvy", "either", "event-listener", "futures-channel", @@ -2531,9 +2485,9 @@ dependencies = [ [[package]] name = "sqlx-macros" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" dependencies = [ "proc-macro2", "quote", @@ -2544,11 +2498,10 @@ dependencies = [ [[package]] name = "sqlx-macros-core" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ - "atomic-write-file", "dotenvy", "either", "heck", @@ -2571,9 +2524,9 @@ dependencies = [ [[package]] name = "sqlx-mysql" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64 0.21.7", @@ -2615,9 +2568,9 @@ dependencies = [ [[package]] name = "sqlx-postgres" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64 0.21.7", @@ -2643,7 +2596,6 @@ dependencies = [ "rand", "serde", "serde_json", - "sha1", "sha2", "smallvec", "sqlx-core", @@ -2656,9 +2608,9 @@ dependencies = [ [[package]] name = "sqlx-sqlite" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", "chrono", @@ -2715,9 +2667,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" dependencies = [ "proc-macro2", "quote", @@ -2765,9 +2717,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.10.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", @@ -2794,38 +2746,38 @@ dependencies = [ [[package]] name = "testcontainers-modules" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c391cd115649a8a14e5638d0606648d5348b216700a31f402987f57e58693766" +checksum = "1d0334776e1e8ee7c504a922c5236daf865ffe413aa630d84ae91dcce0b10bc3" dependencies = [ "testcontainers", ] [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -2890,7 +2842,7 @@ dependencies = [ "num_cpus", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2 0.5.6", "tokio-macros", "windows-sys 0.48.0", ] @@ -2903,7 +2855,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -3021,7 +2973,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] @@ -3154,9 +3106,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -3187,12 +3139,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "ureq" -version = "2.9.5" +version = "2.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b52731d03d6bb2fd18289d4028aee361d6c28d44977846793b994b13cdcc64d" +checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35" dependencies = [ "base64 0.21.7", - "hootbin", "log", "native-tls", "once_cell", @@ -3260,11 +3211,17 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3272,24 +3229,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -3299,9 +3256,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3309,28 +3266,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -3344,9 +3301,13 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "whoami" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +dependencies = [ + "redox_syscall", + "wasite", +] [[package]] name = "widestring" @@ -3382,7 +3343,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -3400,7 +3361,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", + "windows-targets 0.52.4", ] [[package]] @@ -3420,17 +3381,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", ] [[package]] @@ -3441,9 +3402,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" @@ -3453,9 +3414,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" @@ -3465,9 +3426,9 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" @@ -3477,9 +3438,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" @@ -3489,9 +3450,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" @@ -3501,9 +3462,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" @@ -3513,9 +3474,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] name = "winreg" @@ -3553,7 +3514,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.52", ] [[package]] diff --git a/clearing-house-app/Cargo.toml b/clearing-house-app/Cargo.toml index 1296be9..2ea06ca 100644 --- a/clearing-house-app/Cargo.toml +++ b/clearing-house-app/Cargo.toml @@ -1,6 +1,8 @@ [package] name = "clearing-house-app" version = "0.10.0" +license = "Apache-2.0" +repository = "https://github.com/ids-basecamp/clearinghouse" authors = [ "Mark Gall ", "Georg Bramm ", @@ -32,7 +34,7 @@ rand = "0.8.5" # lazy initialization of static variables once_cell = "1.18.0" # Base64 encoding -base64 = "0.21.7 " +base64 = "0.21.7" # UUID generation uuid = { version = "1", features = ["serde", "v4"] } # Big integer handling (RSA key modulus and exponent) diff --git a/clearing-house-app/src/config.rs b/clearing-house-app/src/config.rs index b1bcaf8..d35d280 100644 --- a/clearing-house-app/src/config.rs +++ b/clearing-house-app/src/config.rs @@ -1,3 +1,5 @@ +use std::fmt::Display; + /// Represents the configuration for the application #[derive(Debug, serde::Deserialize)] pub(crate) struct CHConfig { @@ -33,15 +35,16 @@ impl From for tracing::Level { } } -impl ToString for LogLevel { - fn to_string(&self) -> String { - match self { +impl Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { LogLevel::Trace => String::from("TRACE"), LogLevel::Debug => String::from("DEBUG"), LogLevel::Info => String::from("INFO"), LogLevel::Warn => String::from("WARN"), LogLevel::Error => String::from("ERROR"), - } + }; + write!(f, "{str}") } } @@ -119,7 +122,7 @@ mod test { #[serial] fn test_read_config_from_toml() { // Create tempfile - let file = tempfile::Builder::new().suffix(".toml").tempfile().unwrap(); + let file = tempfile::Builder::new().suffix(".toml").tempfile().expect("Failure to create tempfile"); // Write config to file let toml = r#"database_url = "mongodb://localhost:27019" diff --git a/clearing-house-app/src/db/postgres_document_store.rs b/clearing-house-app/src/db/postgres_document_store.rs index 871656f..a4a6a23 100644 --- a/clearing-house-app/src/db/postgres_document_store.rs +++ b/clearing-house-app/src/db/postgres_document_store.rs @@ -25,14 +25,14 @@ impl super::DocumentStore for PostgresDocumentStore { let doc = DocumentRow::from(doc); sqlx::query( - r#"INSERT INTO documents + r"INSERT INTO documents (id, process_id, created_at, model_version, correlation_message, transfer_contract, issued, issuer_connector, content_version, recipient_connector, sender_agent, recipient_agent, payload, payload_type, message_id) VALUES ($1, (SELECT id from processes where process_id = $2), $3, $4, $5, $6, $7, $8, $9, $10, - $11, $12, $13, $14, $15)"#, + $11, $12, $13, $14, $15)", ) .bind(doc.id) // 1 .bind(doc.process_id) // 2 @@ -61,26 +61,32 @@ impl super::DocumentStore for PostgresDocumentStore { .fetch_optional(&self.db) .await .map(|r| r.is_some()) - .map_err(|e| e.into()) + .map_err(std::convert::Into::into) } async fn get_document(&self, id: &str, pid: &str) -> anyhow::Result> { sqlx::query_as::<_, DocumentRow>( - r#"SELECT documents.id, processes.process_id, documents.created_at, model_version, correlation_message, + r"SELECT documents.id, processes.process_id, documents.created_at, model_version, correlation_message, transfer_contract, issued, issuer_connector, content_version, recipient_connector, sender_agent, recipient_agent, payload, payload_type, message_id FROM documents LEFT JOIN processes ON processes.id = documents.process_id - WHERE id = $1 AND processes.process_id = $2"#, + WHERE id = $1 AND processes.process_id = $2", ) - .bind(id) - .bind(pid) - .fetch_optional(&self.db) - .await - .map(|r| r.map(DocumentRow::into)) - .map_err(|e| e.into()) + .bind(id) + .bind(pid) + .fetch_optional(&self.db) + .await + .map(|r| r.map(DocumentRow::into)) + .map_err(std::convert::Into::into) } + /// Get documents for a process + /// + /// # Lints + /// + /// Disabled `clippy::cast_possible_wrap` because cast is handled + #[allow(clippy::cast_possible_wrap)] async fn get_documents_for_pid( &self, pid: &str, @@ -96,27 +102,35 @@ impl super::DocumentStore for PostgresDocumentStore { sqlx::query_as::<_, DocumentRow>( format!( - r#"SELECT documents.id, processes.process_id, documents.created_at, model_version, correlation_message, + r"SELECT documents.id, processes.process_id, documents.created_at, model_version, correlation_message, transfer_contract, issued, issuer_connector, content_version, recipient_connector, sender_agent, recipient_agent, payload, payload_type, message_id FROM documents LEFT JOIN processes ON processes.id = documents.process_id WHERE processes.process_id = $1 AND documents.created_at BETWEEN $2 AND $3 - ORDER BY created_at {} - LIMIT $4 OFFSET $5"#, - sort_order - ) - .as_str(), + ORDER BY created_at {sort_order} + LIMIT $4 OFFSET $5") + .as_str(), ) - .bind(pid) - .bind(date_from) - .bind(date_to) - .bind(size as i64) - .bind(((page - 1) * size) as i64) - .fetch_all(&self.db) - .await - .map(|r| r.into_iter().map(DocumentRow::into).collect()) - .map_err(|e| e.into()) + .bind(pid) + .bind(date_from) + .bind(date_to) + .bind(cast_i64(size)?) + .bind(cast_i64((page - 1) * size)?) + .fetch_all(&self.db) + .await + .map(|r| r.into_iter().map(DocumentRow::into).collect()) + .map_err(std::convert::Into::into) + } +} + +/// Cast u64 to i64 with out-of-range check +fn cast_i64(value: u64) -> anyhow::Result { + if value > i64::MAX as u64 { + Err(anyhow::anyhow!("size out-of-range")) + } else { + #[allow(clippy::cast_possible_wrap)] + Ok(value as i64) } } @@ -161,29 +175,29 @@ impl From for DocumentRow { } } -impl Into for DocumentRow { - fn into(self) -> Document { +impl From for Document { + fn from(value: DocumentRow) -> Self { use chrono::TimeZone; - Document { - id: self.id, - pid: self.process_id, - ts: chrono::Local.from_utc_datetime(&self.created_at), + Self { + id: value.id, + pid: value.process_id, + ts: chrono::Local.from_utc_datetime(&value.created_at), content: crate::model::ids::message::IdsMessage { - model_version: self.model_version, - correlation_message: self.correlation_message, - transfer_contract: self.transfer_contract, - issued: self.issued.0, - issuer_connector: self.issuer_connector.0, - content_version: self.content_version, - recipient_connector: self.recipient_connector.map(|s| s.0), - sender_agent: self.sender_agent, - recipient_agent: self.recipient_agent.map(|s| s.0), - payload: self + model_version: value.model_version, + correlation_message: value.correlation_message, + transfer_contract: value.transfer_contract, + issued: value.issued.0, + issuer_connector: value.issuer_connector.0, + content_version: value.content_version, + recipient_connector: value.recipient_connector.map(|s| s.0), + sender_agent: value.sender_agent, + recipient_agent: value.recipient_agent.map(|s| s.0), + payload: value .payload .map(|s| String::from_utf8_lossy(s.as_ref()).to_string()), - payload_type: self.payload_type, - id: self.message_id, + payload_type: value.payload_type, + id: value.message_id, ..Default::default() }, } diff --git a/clearing-house-app/src/db/postgres_process_store.rs b/clearing-house-app/src/db/postgres_process_store.rs index 4ed06fc..3c3edf5 100644 --- a/clearing-house-app/src/db/postgres_process_store.rs +++ b/clearing-house-app/src/db/postgres_process_store.rs @@ -22,15 +22,15 @@ impl PostgresProcessStore { impl super::ProcessStore for PostgresProcessStore { async fn get_processes(&self) -> anyhow::Result> { sqlx::query_as::<_, ProcessRow>( - r#"SELECT p.process_id, p.created_at, ARRAY_AGG(c.client_id) AS owners FROM processes p + r"SELECT p.process_id, p.created_at, ARRAY_AGG(c.client_id) AS owners FROM processes p LEFT JOIN process_owners po ON p.id = po.process_id LEFT JOIN clients c ON po.client_id = c.id - GROUP BY p.process_id, p.created_at"#, + GROUP BY p.process_id, p.created_at", ) - .fetch_all(&self.db) - .await - .map(|r| r.into_iter().map(|p| p.into()).collect()) - .map_err(|e| e.into()) + .fetch_all(&self.db) + .await + .map(|r| r.into_iter().map(std::convert::Into::into).collect()) + .map_err(std::convert::Into::into) } async fn delete_process(&self, pid: &str) -> anyhow::Result { @@ -39,7 +39,7 @@ impl super::ProcessStore for PostgresProcessStore { .execute(&self.db) .await .map(|r| r.rows_affected() == 1) - .map_err(|e| e.into()) + .map_err(std::convert::Into::into) } async fn exists_process(&self, pid: &str) -> anyhow::Result { @@ -48,22 +48,22 @@ impl super::ProcessStore for PostgresProcessStore { .fetch_optional(&self.db) .await .map(|r| r.is_some()) - .map_err(|e| e.into()) + .map_err(std::convert::Into::into) } async fn get_process(&self, pid: &str) -> anyhow::Result> { sqlx::query_as::<_, ProcessRow>( - r#"SELECT p.process_id, p.created_at, ARRAY_AGG(c.client_id) AS owners FROM processes p + r"SELECT p.process_id, p.created_at, ARRAY_AGG(c.client_id) AS owners FROM processes p LEFT JOIN process_owners po ON p.id = po.process_id LEFT JOIN clients c ON po.client_id = c.id WHERE p.process_id = $1 - GROUP BY p.process_id, p.created_at"#, + GROUP BY p.process_id, p.created_at", ) - .bind(pid) - .fetch_optional(&self.db) - .await - .map(|r| r.map(|p| p.into())) - .map_err(|e| e.into()) + .bind(pid) + .fetch_optional(&self.db) + .await + .map(|r| r.map(std::convert::Into::into)) + .map_err(std::convert::Into::into) } async fn store_process(&self, process: Process) -> anyhow::Result<()> { @@ -72,7 +72,7 @@ impl super::ProcessStore for PostgresProcessStore { // Create a process let process_row = - sqlx::query(r#"INSERT INTO processes (process_id) VALUES ($1) RETURNING id"#) + sqlx::query(r"INSERT INTO processes (process_id) VALUES ($1) RETURNING id") .bind(&process.process_id) .fetch_one(&mut *tx) .await?; @@ -81,7 +81,7 @@ impl super::ProcessStore for PostgresProcessStore { for o in process.owners { // Check if client exists - let client_row = sqlx::query(r#"SELECT id FROM clients WHERE client_id = $1"#) + let client_row = sqlx::query(r"SELECT id FROM clients WHERE client_id = $1") .bind(&o) .fetch_optional(&mut *tx) .await?; @@ -90,7 +90,7 @@ impl super::ProcessStore for PostgresProcessStore { let client_row = match client_row { Some(crow) => crow, None => { - sqlx::query(r#"INSERT INTO clients (client_id) VALUES ($1) RETURNING id"#) + sqlx::query(r"INSERT INTO clients (client_id) VALUES ($1) RETURNING id") .bind(&o) .fetch_one(&mut *tx) .await? @@ -98,12 +98,12 @@ impl super::ProcessStore for PostgresProcessStore { }; // Get id of client - let oid = client_row.get::("id"); + let client_id = client_row.get::("id"); // Create process owner - sqlx::query(r#"INSERT INTO process_owners (process_id, client_id) VALUES ($1, $2)"#) + sqlx::query(r"INSERT INTO process_owners (process_id, client_id) VALUES ($1, $2)") .bind(pid) - .bind(oid) + .bind(client_id) .execute(&mut *tx) .await?; } @@ -129,8 +129,8 @@ impl From for ProcessRow { } } -impl Into for ProcessRow { - fn into(self) -> Process { - Process::new(self.process_id, self.owners) +impl From for Process { + fn from(value: ProcessRow) -> Self { + Self::new(value.process_id, value.owners) } } diff --git a/clearing-house-app/src/lib.rs b/clearing-house-app/src/lib.rs index 57d10d2..62b648c 100644 --- a/clearing-house-app/src/lib.rs +++ b/clearing-house-app/src/lib.rs @@ -1,3 +1,7 @@ +#![forbid(unsafe_code)] +#![warn(clippy::all, clippy::pedantic, clippy::unwrap_used)] +#![allow(clippy::module_name_repetitions)] + #[macro_use] extern crate tracing; @@ -35,22 +39,25 @@ pub(crate) struct AppState { } impl AppState { + + /// Connect to the database and execute database migrations + async fn setup_postgres(conf: &config::CHConfig) -> anyhow::Result { + info!("Connecting to database"); + let pool = sqlx::PgPool::connect(&conf.database_url).await?; + + info!("Migrating database"); + sqlx::migrate!() + .run(&pool) + .await + .expect("Failed to migrate database!"); + + Ok(pool) + } + /// Initialize the application state from config async fn init(conf: &config::CHConfig) -> anyhow::Result { #[cfg(feature = "postgres")] - let pool = async { - info!("Connecting to database"); - let pool = sqlx::PgPool::connect(&conf.database_url).await.unwrap(); - - info!("Migrating database"); - sqlx::migrate!() - .run(&pool) - .await - .expect("Failed to migrate database!"); - - pool - } - .await; + let pool = Self::setup_postgres(conf).await?; trace!("Initializing Process store"); #[cfg(feature = "mongodb")] @@ -85,7 +92,7 @@ impl AppState { )); let service_config = Arc::new(util::init_service_config( - ENV_LOGGING_SERVICE_ID.to_string(), + ENV_LOGGING_SERVICE_ID, )?); let signing_key = util::init_signing_key(conf.signing_key.as_deref())?; @@ -97,6 +104,11 @@ impl AppState { } } +/// Initialize the application +/// +/// # Errors +/// +/// Throws an error if the `AppState` cannot be initialized pub async fn app() -> anyhow::Result { // Read configuration let conf = config::read_config(None); diff --git a/clearing-house-app/src/main.rs b/clearing-house-app/src/main.rs index 2588197..d302490 100644 --- a/clearing-house-app/src/main.rs +++ b/clearing-house-app/src/main.rs @@ -1,5 +1,5 @@ #![forbid(unsafe_code)] -#![warn(clippy::unwrap_used)] +#![warn(clippy::all, clippy::pedantic, clippy::unwrap_used)] use tokio::net::TcpListener; diff --git a/clearing-house-app/src/model/claims.rs b/clearing-house-app/src/model/claims.rs index 30958fb..49a7b66 100644 --- a/clearing-house-app/src/model/claims.rs +++ b/clearing-house-app/src/model/claims.rs @@ -12,8 +12,9 @@ pub struct ChClaims { } impl ChClaims { - pub fn new(client_id: &str) -> ChClaims { - ChClaims { + #[must_use] + pub fn new(client_id: &str) -> Self { + Self { client_id: client_id.to_string(), } } @@ -29,9 +30,9 @@ pub struct ExtractChClaims(pub ChClaims); #[async_trait::async_trait] impl axum::extract::FromRequestParts for ExtractChClaims -where - S: Send + Sync, - AppState: FromRef, + where + S: Send + Sync, + AppState: FromRef, { type Rejection = axum::response::Response; @@ -42,7 +43,7 @@ where let axum::extract::State(app_state) = axum::extract::State::::from_request_parts(parts, state) .await - .map_err(|err| err.into_response())?; + .map_err(axum::response::IntoResponse::into_response)?; if let Some(token) = parts.headers.get(SERVICE_HEADER) { let token = token.to_str().map_err(|_| { ( @@ -69,6 +70,12 @@ where } } +/// Returns the `JWKSet` for the RSA keypair at `key_path` +/// +/// # Panics +/// +/// Panics if the key at `key_path` is not a valid RSA keypair or does not exist. +#[must_use] pub fn get_jwks(key_path: &str) -> Option> { let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path) .unwrap_or_else(|_| panic!("Failed to load keyfile from path {key_path}")); @@ -107,6 +114,12 @@ pub fn get_jwks(key_path: &str) -> Option> None } +/// Returns the fingerprint of the RSA keypair at `key_path` +/// +/// # Panics +/// +/// Panics if the key at `key_path` is not a valid RSA keypair or does not exist. +#[must_use] pub fn get_fingerprint(key_path: &str) -> Option { use ring::signature::KeyPair; let keypair = biscuit::jws::Secret::rsa_keypair_from_file(key_path) @@ -132,6 +145,11 @@ pub fn get_fingerprint(key_path: &str) -> Option { } } +/// Creates a JWT token with the given `issuer`, `audience` and `private_claims` +/// +/// # Panics +/// +/// Panics if the `ENV_SHARED_SECRET` is not set pub fn create_token< T: std::fmt::Display + Clone + serde::Serialize + for<'de> serde::Deserialize<'de>, >( @@ -139,15 +157,8 @@ pub fn create_token< audience: &str, private_claims: &T, ) -> String { - let signing_secret = match env::var(ENV_SHARED_SECRET) { - Ok(secret) => biscuit::jws::Secret::Bytes(secret.to_string().into_bytes()), - Err(_) => { - panic!( - "Shared Secret not configured. Please configure environment variable {}", - ENV_SHARED_SECRET - ); - } - }; + let secret = env::var(ENV_SHARED_SECRET).unwrap_or_else(|_| panic!("Shared Secret not configured. Please configure environment variable {ENV_SHARED_SECRET}")); + let signing_secret = biscuit::jws::Secret::Bytes(secret.to_string().into_bytes()); let expiration_date = chrono::Utc::now() + chrono::Duration::minutes(5); let claims = biscuit::ClaimsSet:: { @@ -171,11 +182,16 @@ pub fn create_token< ); jwt.into_encoded(&signing_secret) - .unwrap() + .expect("Encoded JWT with the signing secret") .unwrap_encoded() .to_string() } +/// Decodes the given `token` and validates it against the given `audience` +/// +/// # Errors +/// +/// Returns an error if the token is invalid or the audience is not as expected. pub fn decode_token serde::Deserialize<'de>>( token: &str, audience: &str, @@ -226,7 +242,7 @@ pub fn decode_token serde::Deserialize<'d mod test { #[test] fn get_fingerprint() { - let fingerprint = super::get_fingerprint("keys/private_key.der").unwrap(); + let fingerprint = super::get_fingerprint("keys/private_key.der").expect("Fingerprint can be generated"); assert_eq!(fingerprint, "Qra//29Frxbj5hh5Azef+G36SeiOm9q7s8+w8uGLD28"); } } diff --git a/clearing-house-app/src/model/document.rs b/clearing-house-app/src/model/document.rs index 2617090..0c655b1 100644 --- a/clearing-house-app/src/model/document.rs +++ b/clearing-house-app/src/model/document.rs @@ -17,6 +17,7 @@ pub struct Document { /// Documents should have a globally unique id, setting the id manually is discouraged. impl Document { + #[must_use] pub fn new(pid: String, content: IdsMessage) -> Self { Self { id: uuid::Uuid::new_v4(), diff --git a/clearing-house-app/src/model/ids/message.rs b/clearing-house-app/src/model/ids/message.rs index 46ad6c4..8b8e16c 100644 --- a/clearing-house-app/src/model/ids/message.rs +++ b/clearing-house-app/src/model/ids/message.rs @@ -107,10 +107,10 @@ impl Default for IdsMessage { type_message: MessageType::Message, id: Some(autogen("MessageProcessedNotification")), pid: None, - model_version: "".to_string(), + model_version: String::new(), correlation_message: None, issued: InfoModelDateTime::default(), - issuer_connector: InfoModelId::new("".to_string()), + issuer_connector: InfoModelId::new(String::new()), sender_agent: "https://w3id.org/idsa/core/ClearingHouse".to_string(), recipient_connector: None, recipient_agent: None, @@ -124,55 +124,55 @@ impl Default for IdsMessage { } } -/// Conversion from Document to IdsMessage +/// Conversion from `Document` to `IdsMessage` /// -/// note: Documents are converted into LogMessages. The LogMessage contains -/// the payload and payload type, which is the data that was stored previously. -/// All other fields of the LogMessage are meta data about the logging, e.g. +/// note: Documents are converted into `LogMessage`'s. The `LogMessage` contains +/// the `payload` and `payload_type`, which is the data that was stored previously. +/// All other fields of the `LogMessage` are `metadata` about the logging, e.g. /// when the message was logged, etc. /// -/// meta data that we also need to store -/// - message_id -/// - pid -/// - model_version -/// - correlation_message -/// - issued -/// - issuer_connector -/// - sender_agent -/// - transfer_contract -/// - content_version -/// - security_token -/// - authorization_token -/// - payload -/// - payload_type +/// metadata that we also need to store +/// - `message_id` +/// - `pid` +/// - `model_version` +/// - `correlation_message` +/// - `issued` +/// - `issuer_connector` +/// - `sender_agent` +/// - `transfer_contract` +/// - `content_version` +/// - `security_token` +/// - `authorization_token` +/// - `payload` +/// - `payload_type` impl From for IdsMessage { fn from(doc: Document) -> Self { doc.content.clone() } } -/// Conversion from IdsMessage to Document +/// Conversion from `IdsMessage` to `Document` /// /// most important part to store: -/// payload and payload type +/// `payload` and `payload_type` /// -/// meta data that we also need to store -/// - message_id -/// - pid -/// - model_version -/// - correlation_message -/// - issued -/// - issuer_connector -/// - sender_agent -/// - transfer_contract -/// - content_version -/// - security_token -/// - authorization_token -/// - payload -/// - payload_type -impl Into for IdsMessage { - fn into(self) -> Document { - let mut m = self.clone(); +/// metadata that we also need to store +/// - `message_id` +/// - `pid` +/// - `model_version` +/// - `correlation_message` +/// - `issued` +/// - `issuer_connector` +/// - `sender_agent` +/// - `transfer_contract` +/// - `content_version` +/// - `security_token` +/// - `authorization_token` +/// - `payload` +/// - `payload_type` +impl From for Document { + fn from(value: IdsMessage) -> Self { + let mut m = value.clone(); m.id = Some(m.id.unwrap_or_else(|| autogen("Message"))); diff --git a/clearing-house-app/src/model/ids/mod.rs b/clearing-house-app/src/model/ids/mod.rs index f72ed4c..0cb8162 100644 --- a/clearing-house-app/src/model/ids/mod.rs +++ b/clearing-house-app/src/model/ids/mod.rs @@ -29,6 +29,7 @@ impl std::fmt::Display for InfoModelComplexId { } impl InfoModelComplexId { + #[must_use] pub fn new(id: String) -> InfoModelComplexId { InfoModelComplexId { id: Some(id) } } @@ -48,6 +49,7 @@ pub enum InfoModelId { } impl InfoModelId { + #[must_use] pub fn new(id: String) -> InfoModelId { InfoModelId::SimpleId(id) } @@ -96,9 +98,9 @@ impl std::fmt::Display for InfoModelDateTime { pub struct InfoModelTimeStamp { //IDS name #[serde( - rename = "@type", - alias = "type", - skip_serializing_if = "Option::is_none" + rename = "@type", + alias = "type", + skip_serializing_if = "Option::is_none" )] pub format: Option, //IDS name @@ -118,7 +120,7 @@ impl Default for InfoModelTimeStamp { impl std::fmt::Display for InfoModelTimeStamp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match serde_json::to_string(&self) { - Ok(result) => write!(f, "{}", result), + Ok(result) => write!(f, "{result}"), Err(e) => { error!("could not convert DateTimeStamp to json: {}", e); write!(f, "") @@ -362,6 +364,12 @@ pub struct IdsQueryResult { } impl IdsQueryResult { + /// Create a new `IdsQueryResult` + /// + /// # Panics + /// + /// Panics if the `date_from` or `date_to` seconds are out of reach for `chrono::NaiveDateTime::from_timestamp_opt` + #[must_use] pub fn new( date_from: i64, date_to: i64, diff --git a/clearing-house-app/src/model/mod.rs b/clearing-house-app/src/model/mod.rs index 36e06d8..4942808 100644 --- a/clearing-house-app/src/model/mod.rs +++ b/clearing-house-app/src/model/mod.rs @@ -14,13 +14,31 @@ pub enum SortingOrder { Descending, } +/// Time 00:00:00; idiomatic way to create a `chrono::NaiveTime` object +const fn start_of_day() -> chrono::NaiveTime { + if let Some(time) = chrono::NaiveTime::from_hms_opt(0, 0, 0) { + time + } else { + panic!("00:00:00 is a valid time") + } +} + +/// Time 23:59:59; idiomatic way to create a `chrono::NaiveTime` object +const fn end_of_day() -> chrono::NaiveTime { + if let Some(time) = chrono::NaiveTime::from_hms_opt(23, 59, 59) { + time + } else { + panic!("23:59:59 is a valid time") + } +} + /// Parses a date string into a `chrono::NaiveDateTime` object. If `to_date` is true, the time will be set to 23:59:59, otherwise it is 00:00:00. pub fn parse_date(date: Option, to_date: bool) -> Option { // If it is a to_date, we want to set the time to 23:59:59, otherwise it is 00:00:00 let time: chrono::NaiveTime = if to_date { - chrono::NaiveTime::from_hms_opt(23, 59, 59).expect("23:59:59 is a valid time") + end_of_day() } else { - chrono::NaiveTime::from_hms_opt(0, 0, 0).expect("00:00:00 is a valid time") + start_of_day() }; match date { @@ -39,6 +57,10 @@ pub fn parse_date(date: Option, to_date: bool) -> Option, date_to: Option, @@ -53,8 +75,7 @@ pub fn validate_and_sanitize_dates( let default_to_date = now.add(chrono::Duration::seconds(1)); let default_from_date = default_to_date .date() - .and_hms_opt(0, 0, 0) - .expect("00:00:00 is a valid time") + .and_time(start_of_day()) - chrono::Duration::weeks(2); match (date_from, date_to) { @@ -68,6 +89,7 @@ pub fn validate_and_sanitize_dates( #[cfg(test)] mod test { use std::ops::Add; + use crate::model::{end_of_day, start_of_day}; #[test] fn validate_and_sanitize_dates() { @@ -75,8 +97,7 @@ mod test { let date_now = chrono::Local::now().naive_local(); let date_now_midnight = date_now .date() - .and_hms_opt(0, 0, 0) - .expect("00:00:00 is a valid time"); + .and_time(start_of_day()); let date_from = date_now_midnight - chrono::Duration::weeks(2); let date_to = date_now_midnight - chrono::Duration::weeks(1); @@ -134,17 +155,15 @@ mod test { let wrong_date = Some("2020-13-01".to_string()); let valid_date = Some("2020-01-01".to_string()); let valid_date_parsed = chrono::NaiveDate::from_ymd_opt(2020, 1, 1).expect("This is valid"); - let day_start_time = chrono::NaiveTime::from_hms_opt(0, 0, 0).expect("This is valid"); - let day_end_time = chrono::NaiveTime::from_hms_opt(23, 59, 59).expect("This is valid"); assert!(super::parse_date(wrong_date, false).is_none()); assert_eq!( super::parse_date(valid_date.clone(), false), - Some(valid_date_parsed.and_time(day_start_time)) + Some(valid_date_parsed.and_time(start_of_day())) ); assert_eq!( super::parse_date(valid_date, true), - Some(valid_date_parsed.and_time(day_end_time)) + Some(valid_date_parsed.and_time(end_of_day())) ); } } diff --git a/clearing-house-app/src/model/process.rs b/clearing-house-app/src/model/process.rs index db4d6e1..1f936cb 100644 --- a/clearing-house-app/src/model/process.rs +++ b/clearing-house-app/src/model/process.rs @@ -5,10 +5,12 @@ pub struct Process { } impl Process { + #[must_use] pub fn new(id: String, owners: Vec) -> Self { Self { id, owners } } + #[must_use] pub fn is_authorized(&self, owner: &str) -> bool { self.owners.contains(&owner.to_string()) } @@ -42,6 +44,11 @@ pub struct DataTransaction { impl biscuit::CompactJson for DataTransaction {} impl DataTransaction { + /// Signs a `DataTransaction` with a given key on the `key_path` and returns a `Receipt`. + /// + /// # Panics + /// + /// Panics if the key at `key_path` is not a valid RSA keypair or does not exist pub fn sign(&self, key_path: &str) -> Receipt { let jws = biscuit::jws::Compact::new_decoded( biscuit::jws::Header::from_registered_header(biscuit::jws::RegisteredHeader { diff --git a/clearing-house-app/src/services/document_service.rs b/clearing-house-app/src/services/document_service.rs index 4620224..0fc72fc 100644 --- a/clearing-house-app/src/services/document_service.rs +++ b/clearing-house-app/src/services/document_service.rs @@ -6,7 +6,7 @@ use crate::model::{parse_date, validate_and_sanitize_dates, SortingOrder}; use crate::services::{DocumentReceipt, QueryResult}; use std::convert::TryFrom; -/// Error type for DocumentService +/// Error type for `DocumentService` #[derive(thiserror::Error, Debug)] pub enum DocumentServiceError { #[error("Document already exists!")] @@ -28,19 +28,15 @@ impl axum::response::IntoResponse for DocumentServiceError { fn into_response(self) -> axum::response::Response { use axum::http::StatusCode; match self { - Self::DocumentAlreadyExists => { - (StatusCode::BAD_REQUEST, self.to_string()).into_response() - } - Self::MissingPayload => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), + Self::DocumentAlreadyExists | Self::MissingPayload | Self::InvalidDates => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::DatabaseError { source, description, } => ( StatusCode::INTERNAL_SERVER_ERROR, - format!("{}: {}", description, source), + format!("{description}: {source}"), ) .into_response(), - Self::InvalidDates => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(), } } @@ -69,26 +65,23 @@ impl DocumentService { } // check if doc id already exists - match self.db.exists_document(&doc.id).await { - Ok(true) => { - warn!("Document exists already!"); - Err(DocumentServiceError::DocumentAlreadyExists) - } - _ => { - // prepare the success result message - let receipt = DocumentReceipt::new(doc.ts, &doc.pid, &doc.id.to_string()); - - trace!("storing document ...."); - // store document - match self.db.add_document(doc).await { - Ok(_b) => Ok(receipt), - Err(e) => { - error!("Error while adding: {:?}", e); - Err(DocumentServiceError::DatabaseError { - source: e, - description: "Error while adding document".to_string(), - }) - } + if let Ok(true) = self.db.exists_document(&doc.id).await { + warn!("Document exists already!"); + Err(DocumentServiceError::DocumentAlreadyExists) + } else { + // prepare the success result message + let receipt = DocumentReceipt::new(doc.ts, &doc.pid, &doc.id.to_string()); + + trace!("storing document ...."); + // store document + match self.db.add_document(doc).await { + Ok(_b) => Ok(receipt), + Err(e) => { + error!("Error while adding: {:?}", e); + Err(DocumentServiceError::DatabaseError { + source: e, + description: "Error while adding document".to_string(), + }) } } } diff --git a/clearing-house-app/src/services/logging_service.rs b/clearing-house-app/src/services/logging_service.rs index de6b005..40fa1b3 100644 --- a/clearing-house-app/src/services/logging_service.rs +++ b/clearing-house-app/src/services/logging_service.rs @@ -12,7 +12,7 @@ use crate::model::{ }; use crate::services::document_service::DocumentService; -/// Error type for LoggingService +/// Error type for `LoggingService` #[derive(Debug, thiserror::Error)] pub enum LoggingServiceError { #[error("Received empty payload, which cannot be logged!")] @@ -42,10 +42,7 @@ impl axum::response::IntoResponse for LoggingServiceError { fn into_response(self) -> axum::response::Response { use axum::http::StatusCode; match self { - Self::EmptyPayloadReceived => { - (StatusCode::BAD_REQUEST, self.to_string()).into_response() - } - Self::AttemptedAccessToDefaultPid => { + Self::EmptyPayloadReceived | Self::AttemptedAccessToDefaultPid | Self::InvalidRequest | Self::ProcessAlreadyExists | Self::ParsingError(_) => { (StatusCode::BAD_REQUEST, self.to_string()).into_response() } Self::DatabaseError { @@ -53,18 +50,13 @@ impl axum::response::IntoResponse for LoggingServiceError { description, } => ( StatusCode::INTERNAL_SERVER_ERROR, - format!("{}: {}", description, source), + format!("{description}: {source}"), ) .into_response(), Self::UserNotAuthorized => (StatusCode::FORBIDDEN, self.to_string()).into_response(), - Self::InvalidRequest => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), - Self::ProcessAlreadyExists => { - (StatusCode::BAD_REQUEST, self.to_string()).into_response() - } Self::ProcessDoesNotExist(_) => { (StatusCode::NOT_FOUND, self.to_string()).into_response() } - Self::ParsingError(_) => (StatusCode::BAD_REQUEST, self.to_string()).into_response(), Self::DocumentServiceError(e) => e.into_response(), } } @@ -204,10 +196,10 @@ impl LoggingService { match self.db.get_process(&pid).await { Ok(Some(p)) => { warn!("Requested pid '{}' already exists.", &p.id); - if !p.owners.contains(user) { - Err(LoggingServiceError::UserNotAuthorized) // Forbidden - } else { + if p.owners.contains(user) { Err(LoggingServiceError::ProcessAlreadyExists) // BadRequest + } else { + Err(LoggingServiceError::UserNotAuthorized) // Forbidden } } Ok(None) => { @@ -218,7 +210,7 @@ impl LoggingService { let new_process = Process::new(pid.clone(), owners); match self.db.store_process(new_process).await { - Ok(_) => Ok(pid.clone()), + Ok(()) => Ok(pid.clone()), Err(e) => { error!("Error while creating process '{}': {}", &pid, e); Err(LoggingServiceError::DatabaseError { @@ -289,6 +281,10 @@ impl LoggingService { } } + /// Query a single message by its `id` and `pid` + /// + /// `_message` is required because the `ClearingHouseMessage` as request body is required by the route + #[allow(clippy::no_effect_underscore_binding)] pub(crate) async fn query_id( &self, ch_claims: ChClaims, diff --git a/clearing-house-app/src/util.rs b/clearing-house-app/src/util.rs index 5af5d60..a43e2ba 100644 --- a/clearing-house-app/src/util.rs +++ b/clearing-house-app/src/util.rs @@ -5,8 +5,8 @@ pub struct ServiceConfig { pub service_id: String, } -pub(super) fn init_service_config(service_id: String) -> anyhow::Result { - match std::env::var(&service_id) { +pub(super) fn init_service_config(service_id: &str) -> anyhow::Result { + match std::env::var(service_id) { Ok(id) => Ok(ServiceConfig { service_id: id }), Err(_e) => { anyhow::bail!( @@ -29,6 +29,10 @@ pub(super) fn init_signing_key(signing_key_path: Option<&str>) -> anyhow::Result } /// Signal handler to catch a Ctrl+C and initiate a graceful shutdown +/// +/// # Panics +/// +/// May panic if the signal handler cannot be installed pub async fn shutdown_signal() { let ctrl_c = async { tokio::signal::ctrl_c() @@ -37,7 +41,7 @@ pub async fn shutdown_signal() { }; #[cfg(unix)] - let terminate = async { + let terminate = async { tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .expect("failed to install signal handler") .recv() @@ -45,17 +49,18 @@ pub async fn shutdown_signal() { }; #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); + let terminate = std::future::pending::<()>(); tokio::select! { - _ = ctrl_c => {}, - _ = terminate => {}, + () = ctrl_c => {}, + () = terminate => {}, } info!("signal received, starting graceful shutdown"); } /// Returns a new UUID as a string with hyphens. +#[must_use] pub fn new_uuid() -> String { use uuid::Uuid; Uuid::new_v4().hyphenated().to_string() From bc7763e9eab18b98cf949a1456b481d5f2164665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 14 Mar 2024 10:05:46 +0100 Subject: [PATCH 177/183] chore: update npm dependencies --- package-lock.json | 2336 +++++++++++++++++++++++++++++---------------- 1 file changed, 1515 insertions(+), 821 deletions(-) diff --git a/package-lock.json b/package-lock.json index c542bc7..adfd6db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,34 +16,106 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", - "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.5" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", - "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -176,16 +248,16 @@ } }, "node_modules/@octokit/core": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.0.tgz", - "integrity": "sha512-YbAtMWIrbZ9FCXbLwT9wWB8TyLjq9mxpKdgB3dUNxQcIVTf9hJ70gRPwAcqGZdY6WdJPZ0I7jLaaNDCiloGN2A==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", + "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", "dev": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.0.0", "@octokit/request": "^8.0.2", "@octokit/request-error": "^5.0.0", - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" }, @@ -194,13 +266,12 @@ } }, "node_modules/@octokit/endpoint": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.0.tgz", - "integrity": "sha512-szrQhiqJ88gghWY2Htt8MqUDO6++E/EIXqJ2ZEp5ma3uGS46o7LZAzSLt49myB7rT+Hfw5Y6gO3LmOxGzHijAQ==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", + "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", "dev": true, "dependencies": { - "@octokit/types": "^11.0.0", - "is-plain-object": "^5.0.0", + "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" }, "engines": { @@ -208,13 +279,13 @@ } }, "node_modules/@octokit/graphql": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.1.tgz", - "integrity": "sha512-T5S3oZ1JOE58gom6MIcrgwZXzTaxRnxBso58xhozxHpOqSTgDS6YNeEUvZ/kRvXgPrRz/KHnZhtb7jUMRi9E6w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", "dev": true, "dependencies": { "@octokit/request": "^8.0.1", - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" }, "engines": { @@ -222,34 +293,34 @@ } }, "node_modules/@octokit/openapi-types": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz", - "integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==", + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-8.0.0.tgz", - "integrity": "sha512-2xZ+baZWUg+qudVXnnvXz7qfrTmDeYPCzangBVq/1gXxii/OiS//4shJp9dnCCvj1x+JAm9ji1Egwm1BA47lPQ==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", + "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", "dev": true, "dependencies": { - "@octokit/types": "^11.0.0" + "@octokit/types": "^12.6.0" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": "5" } }, "node_modules/@octokit/plugin-retry": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.0.tgz", - "integrity": "sha512-a1/A4A+PB1QoAHQfLJxGHhLfSAT03bR1jJz3GgQJZvty2ozawFWs93MiBQXO7SL2YbO7CIq0Goj4qLOBj8JeMQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", "dev": true, "dependencies": { "@octokit/request-error": "^5.0.0", - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "bottleneck": "^2.15.3" }, "engines": { @@ -260,12 +331,12 @@ } }, "node_modules/@octokit/plugin-throttling": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-7.0.0.tgz", - "integrity": "sha512-KL2k/d0uANc8XqP5S64YcNFCudR3F5AaKO39XWdUtlJIjT9Ni79ekWJ6Kj5xvAw87udkOMEPcVf9xEge2+ahew==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", "dev": true, "dependencies": { - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.2.0", "bottleneck": "^2.15.3" }, "engines": { @@ -276,15 +347,14 @@ } }, "node_modules/@octokit/request": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.1.tgz", - "integrity": "sha512-8N+tdUz4aCqQmXl8FpHYfKG9GelDFd7XGVzyN8rc6WxVlYcfpHECnuRkgquzz+WzvHTK62co5di8gSXnzASZPQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.2.0.tgz", + "integrity": "sha512-exPif6x5uwLqv1N1irkLG1zZNJkOtj8bZxuVHd71U5Ftuxf2wGNvAJyNBcPbPC+EBzwYEbBDdSFb8EPcjpYxPQ==", "dev": true, "dependencies": { "@octokit/endpoint": "^9.0.0", "@octokit/request-error": "^5.0.0", - "@octokit/types": "^11.1.0", - "is-plain-object": "^5.0.0", + "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" }, "engines": { @@ -292,12 +362,12 @@ } }, "node_modules/@octokit/request-error": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.0.tgz", - "integrity": "sha512-1ue0DH0Lif5iEqT52+Rf/hf0RmGO9NWFjrzmrkArpG9trFfDM/efx00BJHdLGuro4BR/gECxCU2Twf5OKrRFsQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", "dev": true, "dependencies": { - "@octokit/types": "^11.0.0", + "@octokit/types": "^12.0.0", "deprecation": "^2.0.0", "once": "^1.4.0" }, @@ -306,12 +376,12 @@ } }, "node_modules/@octokit/types": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-11.1.0.tgz", - "integrity": "sha512-Fz0+7GyLm/bHt8fwEqgvRBWwIV1S6wRRyq+V6exRKLVWaKGsuy6H9QFYeBVDV7rK6fO3XwHgQOPxv+cLj2zpXQ==", + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", "dev": true, "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "@octokit/openapi-types": "^20.0.0" } }, "node_modules/@pnpm/config.env-replace": { @@ -373,55 +443,15 @@ "semantic-release": ">=18.0.0" } }, - "node_modules/@semantic-release/changelog/node_modules/@semantic-release/error": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", - "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", - "dev": true, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/@semantic-release/changelog/node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@semantic-release/changelog/node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@semantic-release/changelog/node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@semantic-release/commit-analyzer": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-10.0.1.tgz", - "integrity": "sha512-9ejHzTAijYs9z246sY/dKBatmOPcd0GQ7lH4MgLCkv1q4GCiDZRkjHJkaQZXZVaK7mJybS+sH3Ng6G8i3pYMGQ==", + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-10.0.4.tgz", + "integrity": "sha512-pFGn99fn8w4/MHE0otb2A/l5kxgOuxaaauIh4u30ncoTJuqWj4hXTgEJ03REqjS+w1R2vPftSsO26WC61yOcpw==", "dev": true, "dependencies": { "conventional-changelog-angular": "^6.0.0", "conventional-commits-filter": "^3.0.0", - "conventional-commits-parser": "^4.0.0", + "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", "import-from": "^4.0.0", "lodash-es": "^4.17.21", @@ -435,12 +465,12 @@ } }, "node_modules/@semantic-release/error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", - "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", + "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", "dev": true, "engines": { - "node": ">=18" + "node": ">=14.17" } }, "node_modules/@semantic-release/git": { @@ -465,211 +495,311 @@ "semantic-release": ">=18.0.0" } }, - "node_modules/@semantic-release/git/node_modules/@semantic-release/error": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", - "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", + "node_modules/@semantic-release/github": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-9.2.6.tgz", + "integrity": "sha512-shi+Lrf6exeNZF+sBhK+P011LSbhmIAoUEgEY6SsxF8irJ+J2stwI5jkyDQ+4gzYyDImzV6LCKdYB9FXnQRWKA==", "dev": true, + "dependencies": { + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "debug": "^4.3.4", + "dir-glob": "^3.0.1", + "globby": "^14.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "issue-parser": "^6.0.0", + "lodash-es": "^4.17.21", + "mime": "^4.0.0", + "p-filter": "^4.0.0", + "url-join": "^5.0.0" + }, "engines": { - "node": ">=14.17" + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" } }, - "node_modules/@semantic-release/git/node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "node_modules/@semantic-release/github/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/github/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", "dev": true, "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/git/node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "node_modules/@semantic-release/github/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, "engines": { - "node": ">=6" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/git/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "node_modules/@semantic-release/github/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-10.0.6.tgz", + "integrity": "sha512-DyqHrGE8aUyapA277BB+4kV0C4iMHh3sHzUWdf0jTgp5NNJxVUz76W1f57FB64Ue03him3CBXxFqQD2xGabxow==", + "dev": true, + "dependencies": { + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "execa": "^8.0.0", + "fs-extra": "^11.0.0", + "lodash-es": "^4.17.21", + "nerf-dart": "^1.0.0", + "normalize-url": "^8.0.0", + "npm": "^9.5.0", + "rc": "^1.2.8", + "read-pkg": "^8.0.0", + "registry-auth-token": "^5.0.0", + "semver": "^7.1.2", + "tempy": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/npm/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/npm/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" }, "engines": { - "node": ">=10" + "node": ">=16.17" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/@semantic-release/git/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/@semantic-release/npm/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, "engines": { - "node": ">=10.17.0" + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/git/node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "node_modules/@semantic-release/npm/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=16.17.0" } }, - "node_modules/@semantic-release/git/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "node_modules/@semantic-release/npm/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/git/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "node_modules/@semantic-release/npm/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/git/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/@semantic-release/npm/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "dependencies": { - "path-key": "^3.0.0" + "path-key": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/git/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/@semantic-release/npm/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, "dependencies": { - "mimic-fn": "^2.1.0" + "mimic-fn": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/git/node_modules/p-reduce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", - "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@semantic-release/git/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "node_modules/@semantic-release/npm/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@semantic-release/github": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-9.0.4.tgz", - "integrity": "sha512-kQCGFAsBErvCR6hzNuzu63cj4erQN2krm9zQlg8vl4j5X0mL0d/Ras0wmL5Gkr1TuSS2lweME7M4J5zvtDDDSA==", + "node_modules/@semantic-release/npm/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "dependencies": { - "@octokit/core": "^5.0.0", - "@octokit/plugin-paginate-rest": "^8.0.0", - "@octokit/plugin-retry": "^6.0.0", - "@octokit/plugin-throttling": "^7.0.0", - "@semantic-release/error": "^4.0.0", - "aggregate-error": "^4.0.1", - "debug": "^4.3.4", - "dir-glob": "^3.0.1", - "globby": "^13.1.4", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "issue-parser": "^6.0.0", - "lodash-es": "^4.17.21", - "mime": "^3.0.0", - "p-filter": "^3.0.0", - "url-join": "^5.0.0" - }, "engines": { - "node": ">=18" + "node": ">=14" }, - "peerDependencies": { - "semantic-release": ">=20.1.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@semantic-release/npm": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-10.0.4.tgz", - "integrity": "sha512-6R3timIQ7VoL2QWRkc9DG8v74RQtRp7UOe/2KbNaqwJ815qOibAv65bH3RtTEhs4axEaHoZf7HDgFs5opaZ9Jw==", + "node_modules/@semantic-release/npm/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, - "dependencies": { - "@semantic-release/error": "^4.0.0", - "aggregate-error": "^4.0.1", - "execa": "^7.0.0", - "fs-extra": "^11.0.0", - "lodash-es": "^4.17.21", - "nerf-dart": "^1.0.0", - "normalize-url": "^8.0.0", - "npm": "^9.5.0", - "rc": "^1.2.8", - "read-pkg": "^8.0.0", - "registry-auth-token": "^5.0.0", - "semver": "^7.1.2", - "tempy": "^3.0.0" - }, "engines": { - "node": ">=18" + "node": ">=12" }, - "peerDependencies": { - "semantic-release": ">=20.1.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@semantic-release/release-notes-generator": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-11.0.4.tgz", - "integrity": "sha512-j0Znnwq9IdWTCGzqSlkLv4MpALTsVDZxcVESzJCNN8pK2BYQlYaKsdZ1Ea/+7RlppI3vjhEi33ZKmjSGY1FLKw==", + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-11.0.7.tgz", + "integrity": "sha512-T09QB9ImmNx7Q6hY6YnnEbw/rEJ6a+22LBxfZq+pSAXg/OL/k0siwEm5cK4k1f9dE2Z2mPIjJKKohzUm0jbxcQ==", "dev": true, "dependencies": { "conventional-changelog-angular": "^6.0.0", "conventional-changelog-writer": "^6.0.0", - "conventional-commits-filter": "^3.0.0", - "conventional-commits-parser": "^4.0.0", + "conventional-commits-filter": "^4.0.0", + "conventional-commits-parser": "^5.0.0", "debug": "^4.0.0", "get-stream": "^7.0.0", "import-from": "^4.0.0", @@ -684,6 +814,15 @@ "semantic-release": ">=20.1.0" } }, + "node_modules/@semantic-release/release-notes-generator/node_modules/conventional-commits-filter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-4.0.0.tgz", + "integrity": "sha512-rnpnibcSOdFcdclpFwWa+pPlZJhXE7l+XK04zxhbWrhgpR96h33QLz8hITTXbcYICxVr3HZFtbtUAQ+4LdBo9A==", + "dev": true, + "engines": { + "node": ">=16" + } + }, "node_modules/@semantic-release/release-notes-generator/node_modules/get-stream": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", @@ -696,16 +835,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", "dev": true }, "node_modules/@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, "node_modules/agent-base": { @@ -721,19 +872,16 @@ } }, "node_modules/aggregate-error": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-4.0.1.tgz", - "integrity": "sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "dependencies": { - "clean-stack": "^4.0.0", - "indent-string": "^5.0.0" + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/ansi-escapes": { @@ -899,18 +1047,12 @@ } }, "node_modules/clean-stack": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-4.2.0.tgz", - "integrity": "sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, - "dependencies": { - "escape-string-regexp": "5.0.0" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/cli-table3": { @@ -1027,21 +1169,33 @@ } }, "node_modules/conventional-commits-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-4.0.0.tgz", - "integrity": "sha512-WRv5j1FsVM5FISJkoYMR6tPk07fkKT0UodruX4je86V4owk451yjXAKzKAPOs9l7y59E2viHUS9eQ+dfUA9NSg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", "dev": true, "dependencies": { - "is-text-path": "^1.0.1", + "is-text-path": "^2.0.0", "JSONStream": "^1.3.5", - "meow": "^8.1.2", - "split2": "^3.2.2" + "meow": "^12.0.1", + "split2": "^4.0.0" }, "bin": { - "conventional-commits-parser": "cli.js" + "conventional-commits-parser": "cli.mjs" }, "engines": { - "node": ">=14" + "node": ">=16" + } + }, + "node_modules/conventional-commits-parser/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/core-util-is": { @@ -1051,14 +1205,14 @@ "dev": true }, "node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, "dependencies": { - "import-fresh": "^3.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", + "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "engines": { @@ -1066,6 +1220,14 @@ }, "funding": { "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/cross-spawn": { @@ -1193,56 +1355,166 @@ "path-type": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/env-ci": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-9.1.1.tgz", + "integrity": "sha512-Im2yEWeF4b2RAMAaWvGioXk6m0UNaIjD8hj28j2ij5ldnIFrDQT0+pzDvpbRkcjurhXhf/AsBKv8P2rtmGi9Aw==", + "dev": true, + "dependencies": { + "execa": "^7.0.0", + "java-properties": "^1.0.2" + }, + "engines": { + "node": "^16.14 || >=18" + } + }, + "node_modules/env-ci/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/env-ci/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/env-ci/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/dot-prop": { + "node_modules/env-ci/node_modules/npm-run-path": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "dependencies": { - "is-obj": "^2.0.0" + "path-key": "^4.0.0" }, "engines": { - "node": ">=8" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "node_modules/env-ci/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, "dependencies": { - "readable-stream": "^2.0.2" + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "node_modules/env-ci/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/env-ci": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-9.1.1.tgz", - "integrity": "sha512-Im2yEWeF4b2RAMAaWvGioXk6m0UNaIjD8hj28j2ij5ldnIFrDQT0+pzDvpbRkcjurhXhf/AsBKv8P2rtmGi9Aw==", + "node_modules/env-ci/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, - "dependencies": { - "execa": "^7.0.0", - "java-properties": "^1.0.2" - }, "engines": { - "node": "^16.14 || >=18" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/error-ex": { @@ -1255,9 +1527,9 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, "engines": { "node": ">=6" @@ -1289,32 +1561,32 @@ } }, "node_modules/execa": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", - "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" }, "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -1328,9 +1600,9 @@ } }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -1406,9 +1678,9 @@ } }, "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -1420,10 +1692,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/get-caller-file": { "version": "2.0.5", @@ -1482,19 +1757,32 @@ } }, "node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", + "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", "dev": true, "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1507,13 +1795,13 @@ "dev": true }, "node_modules/handlebars": { - "version": "4.7.7", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", - "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "dependencies": { "minimist": "^1.2.5", - "neo-async": "^2.6.0", + "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, @@ -1536,18 +1824,6 @@ "node": ">=6" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1557,6 +1833,18 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hook-std": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-3.0.0.tgz", @@ -1570,21 +1858,21 @@ } }, "node_modules/hosted-git-info": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", - "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", + "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", "dev": true, "dependencies": { - "lru-cache": "^7.5.1" + "lru-cache": "^10.0.1" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/http-proxy-agent": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, "dependencies": { "agent-base": "^7.1.0", @@ -1595,9 +1883,9 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz", - "integrity": "sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", "dev": true, "dependencies": { "agent-base": "^7.0.2", @@ -1608,18 +1896,18 @@ } }, "node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "engines": { - "node": ">=14.18.0" + "node": ">=10.17.0" } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -1663,15 +1951,12 @@ } }, "node_modules/indent-string": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", - "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/inherits": { @@ -1709,12 +1994,12 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1777,37 +2062,28 @@ "node": ">=0.10.0" } }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-text-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", "dev": true, "dependencies": { - "text-extensions": "^1.0.0" + "text-extensions": "^2.0.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/is-unicode-supported": { @@ -2112,12 +2388,12 @@ "dev": true }, "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", + "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, "engines": { - "node": ">=12" + "node": "14 || >=16.14" } }, "node_modules/map-obj": { @@ -2368,27 +2644,27 @@ } }, "node_modules/mime": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", - "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz", + "integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==", "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], "bin": { - "mime": "cli.js" + "mime": "bin/cli.js" }, "engines": { - "node": ">=10.0.0" + "node": ">=16" } }, "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/min-indent": { @@ -2499,9 +2775,9 @@ } }, "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", "dev": true, "engines": { "node": ">=14.16" @@ -2511,9 +2787,9 @@ } }, "node_modules/npm": { - "version": "9.8.1", - "resolved": "https://registry.npmjs.org/npm/-/npm-9.8.1.tgz", - "integrity": "sha512-AfDvThQzsIXhYgk9zhbk5R+lh811lKkLAeQMMhSypf1BM7zUafeIIBzMzespeuVEJ0+LvY36oRQYf7IKLzU3rw==", + "version": "9.9.3", + "resolved": "https://registry.npmjs.org/npm/-/npm-9.9.3.tgz", + "integrity": "sha512-Z1l+rcQ5kYb17F3hHtO601arEpvdRYnCLtg8xo3AGtyj3IthwaraEOexI9903uANkifFbqHC8hT53KIrozWg8A==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -2558,6 +2834,7 @@ "ms", "node-gyp", "nopt", + "normalize-package-data", "npm-audit-report", "npm-install-checks", "npm-package-arg", @@ -2574,6 +2851,7 @@ "read", "semver", "sigstore", + "spdx-expression-parse", "ssri", "supports-color", "tar", @@ -2585,10 +2863,17 @@ "write-file-atomic" ], "dev": true, + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^6.3.0", - "@npmcli/config": "^6.2.1", + "@npmcli/arborist": "^6.5.0", + "@npmcli/config": "^6.4.0", "@npmcli/fs": "^3.1.0", "@npmcli/map-workspaces": "^3.0.4", "@npmcli/package-json": "^4.0.1", @@ -2596,43 +2881,44 @@ "@npmcli/run-script": "^6.0.2", "abbrev": "^2.0.0", "archy": "~1.0.0", - "cacache": "^17.1.3", + "cacache": "^17.1.4", "chalk": "^5.3.0", - "ci-info": "^3.8.0", + "ci-info": "^4.0.0", "cli-columns": "^4.0.0", "cli-table3": "^0.6.3", "columnify": "^1.6.0", "fastest-levenshtein": "^1.0.16", - "fs-minipass": "^3.0.2", - "glob": "^10.2.7", + "fs-minipass": "^3.0.3", + "glob": "^10.3.10", "graceful-fs": "^4.2.11", "hosted-git-info": "^6.1.1", "ini": "^4.1.1", "init-package-json": "^5.0.0", "is-cidr": "^4.0.2", - "json-parse-even-better-errors": "^3.0.0", + "json-parse-even-better-errors": "^3.0.1", "libnpmaccess": "^7.0.2", - "libnpmdiff": "^5.0.19", - "libnpmexec": "^6.0.3", - "libnpmfund": "^4.0.19", + "libnpmdiff": "^5.0.20", + "libnpmexec": "^6.0.4", + "libnpmfund": "^4.2.1", "libnpmhook": "^9.0.3", "libnpmorg": "^5.0.4", - "libnpmpack": "^5.0.19", - "libnpmpublish": "^7.5.0", + "libnpmpack": "^5.0.20", + "libnpmpublish": "^7.5.1", "libnpmsearch": "^6.0.2", "libnpmteam": "^5.0.3", "libnpmversion": "^4.0.2", "make-fetch-happen": "^11.1.1", "minimatch": "^9.0.3", - "minipass": "^5.0.0", + "minipass": "^7.0.4", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^9.4.0", + "node-gyp": "^9.4.1", "nopt": "^7.2.0", + "normalize-package-data": "^5.0.0", "npm-audit-report": "^5.0.0", - "npm-install-checks": "^6.1.1", + "npm-install-checks": "^6.3.0", "npm-package-arg": "^10.1.0", - "npm-pick-manifest": "^8.0.1", + "npm-pick-manifest": "^8.0.2", "npm-profile": "^7.0.1", "npm-registry-fetch": "^14.0.5", "npm-user-validate": "^2.0.0", @@ -2643,11 +2929,12 @@ "proc-log": "^3.0.0", "qrcode-terminal": "^0.12.0", "read": "^2.1.0", - "semver": "^7.5.4", - "sigstore": "^1.7.0", - "ssri": "^10.0.4", + "semver": "^7.6.0", + "sigstore": "^1.9.0", + "spdx-expression-parse": "^3.0.1", + "ssri": "^10.0.5", "supports-color": "^9.4.0", - "tar": "^6.1.15", + "tar": "^6.2.0", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "treeverse": "^3.0.0", @@ -2664,30 +2951,15 @@ } }, "node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "path-key": "^3.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/npm/node_modules/@colors/colors": { @@ -2700,6 +2972,12 @@ "node": ">=0.1.90" } }, + "node_modules/npm/node_modules/@gar/promisify": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, "node_modules/npm/node_modules/@isaacs/cliui": { "version": "8.0.2", "dev": true, @@ -2774,7 +3052,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "6.3.0", + "version": "6.5.1", "dev": true, "inBundle": true, "license": "ISC", @@ -2787,7 +3065,7 @@ "@npmcli/name-from-folder": "^2.0.0", "@npmcli/node-gyp": "^3.0.0", "@npmcli/package-json": "^4.0.0", - "@npmcli/query": "^3.0.0", + "@npmcli/query": "^3.1.0", "@npmcli/run-script": "^6.0.0", "bin-links": "^4.0.1", "cacache": "^17.0.4", @@ -2797,7 +3075,7 @@ "json-stringify-nice": "^1.1.4", "minimatch": "^9.0.0", "nopt": "^7.0.0", - "npm-install-checks": "^6.0.0", + "npm-install-checks": "^6.2.0", "npm-package-arg": "^10.1.0", "npm-pick-manifest": "^8.0.1", "npm-registry-fetch": "^14.0.3", @@ -2821,13 +3099,13 @@ } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "6.2.1", + "version": "6.4.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/map-workspaces": "^3.0.2", - "ci-info": "^3.8.0", + "ci-info": "^4.0.0", "ini": "^4.1.0", "nopt": "^7.0.0", "proc-log": "^3.0.0", @@ -2928,6 +3206,19 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm/node_modules/@npmcli/move-file": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm/node_modules/@npmcli/name-from-folder": { "version": "2.0.0", "dev": true, @@ -2977,7 +3268,7 @@ } }, "node_modules/npm/node_modules/@npmcli/query": { - "version": "3.0.0", + "version": "3.1.0", "dev": true, "inBundle": true, "license": "ISC", @@ -3011,25 +3302,51 @@ "license": "MIT", "optional": true, "engines": { - "node": ">=14" + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.2.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.2.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@sigstore/protobuf-specs": { - "version": "0.1.0", + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "1.0.0", "dev": true, "inBundle": true, "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^1.1.0", + "@sigstore/protobuf-specs": "^0.2.0", + "make-fetch-happen": "^11.0.1" + }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "1.0.2", + "version": "1.0.3", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.1.0", + "@sigstore/protobuf-specs": "^0.2.0", "tuf-js": "^1.1.7" }, "engines": { @@ -3076,18 +3393,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/abort-controller": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, "node_modules/npm/node_modules/agent-base": { "version": "6.0.2", "dev": true, @@ -3101,13 +3406,11 @@ } }, "node_modules/npm/node_modules/agentkeepalive": { - "version": "4.3.0", + "version": "4.5.0", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "debug": "^4.1.0", - "depd": "^2.0.0", "humanize-ms": "^1.2.1" }, "engines": { @@ -3164,14 +3467,10 @@ "license": "MIT" }, "node_modules/npm/node_modules/are-we-there-yet": { - "version": "4.0.0", + "version": "4.0.2", "dev": true, "inBundle": true, "license": "ISC", - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^4.1.0" - }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -3182,28 +3481,8 @@ "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/base64-js": { - "version": "1.5.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/bin-links": { - "version": "4.0.2", + "version": "4.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -3235,30 +3514,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/npm/node_modules/buffer": { - "version": "6.0.3", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "inBundle": true, - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/npm/node_modules/builtins": { "version": "5.0.1", "dev": true, @@ -3269,7 +3524,7 @@ } }, "node_modules/npm/node_modules/cacache": { - "version": "17.1.3", + "version": "17.1.4", "dev": true, "inBundle": true, "license": "ISC", @@ -3278,7 +3533,7 @@ "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^7.7.1", - "minipass": "^5.0.0", + "minipass": "^7.0.3", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", @@ -3313,7 +3568,7 @@ } }, "node_modules/npm/node_modules/ci-info": { - "version": "3.8.0", + "version": "4.0.0", "dev": true, "funding": [ { @@ -3386,7 +3641,7 @@ } }, "node_modules/npm/node_modules/cmd-shim": { - "version": "6.0.1", + "version": "6.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -3534,17 +3789,8 @@ "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/depd": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/npm/node_modules/diff": { - "version": "5.1.0", + "version": "5.2.0", "dev": true, "inBundle": true, "license": "BSD-3-Clause", @@ -3589,24 +3835,6 @@ "inBundle": true, "license": "MIT" }, - "node_modules/npm/node_modules/event-target-shim": { - "version": "5.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/npm/node_modules/events": { - "version": "3.3.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, "node_modules/npm/node_modules/exponential-backoff": { "version": "3.1.1", "dev": true, @@ -3639,12 +3867,12 @@ } }, "node_modules/npm/node_modules/fs-minipass": { - "version": "3.0.2", + "version": "3.0.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "minipass": "^5.0.0" + "minipass": "^7.0.3" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -3657,10 +3885,13 @@ "license": "ISC" }, "node_modules/npm/node_modules/function-bind": { - "version": "1.1.1", + "version": "1.1.2", "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/npm/node_modules/gauge": { "version": "5.0.1", @@ -3682,19 +3913,19 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "10.2.7", + "version": "10.3.10", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", + "jackspeak": "^2.3.5", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2", - "path-scurry": "^1.7.0" + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", + "path-scurry": "^1.10.1" }, "bin": { - "glob": "dist/cjs/src/bin.js" + "glob": "dist/esm/bin.mjs" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3709,24 +3940,24 @@ "inBundle": true, "license": "ISC" }, - "node_modules/npm/node_modules/has": { - "version": "1.0.3", + "node_modules/npm/node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hasown": { + "version": "2.0.1", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.1" + "function-bind": "^1.1.2" }, "engines": { - "node": ">= 0.4.0" + "node": ">= 0.4" } }, - "node_modules/npm/node_modules/has-unicode": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "ISC" - }, "node_modules/npm/node_modules/hosted-git-info": { "version": "6.1.1", "dev": true, @@ -3794,28 +4025,8 @@ "node": ">=0.10.0" } }, - "node_modules/npm/node_modules/ieee754": { - "version": "1.2.1", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "inBundle": true, - "license": "BSD-3-Clause" - }, "node_modules/npm/node_modules/ignore-walk": { - "version": "6.0.3", + "version": "6.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -3844,6 +4055,12 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/infer-owner": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "ISC" + }, "node_modules/npm/node_modules/inflight": { "version": "1.0.6", "dev": true, @@ -3887,11 +4104,24 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/ip": { - "version": "2.0.0", + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause" }, "node_modules/npm/node_modules/ip-regex": { "version": "4.3.0", @@ -3915,12 +4145,12 @@ } }, "node_modules/npm/node_modules/is-core-module": { - "version": "2.12.1", + "version": "2.13.1", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3948,7 +4178,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/jackspeak": { - "version": "2.2.1", + "version": "2.3.6", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", @@ -3965,8 +4195,14 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/npm/node_modules/jsbn": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "MIT", @@ -4005,7 +4241,7 @@ "license": "MIT" }, "node_modules/npm/node_modules/libnpmaccess": { - "version": "7.0.2", + "version": "7.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -4018,12 +4254,12 @@ } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "5.0.19", + "version": "5.0.21", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.3.0", + "@npmcli/arborist": "^6.5.0", "@npmcli/disparity-colors": "^3.0.0", "@npmcli/installed-package-contents": "^2.0.2", "binary-extensions": "^2.2.0", @@ -4038,14 +4274,14 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "6.0.3", + "version": "6.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.3.0", + "@npmcli/arborist": "^6.5.0", "@npmcli/run-script": "^6.0.0", - "ci-info": "^3.7.1", + "ci-info": "^4.0.0", "npm-package-arg": "^10.1.0", "npmlog": "^7.0.1", "pacote": "^15.0.8", @@ -4060,19 +4296,19 @@ } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "4.0.19", + "version": "4.2.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.3.0" + "@npmcli/arborist": "^6.5.0" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmhook": { - "version": "9.0.3", + "version": "9.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -4085,7 +4321,7 @@ } }, "node_modules/npm/node_modules/libnpmorg": { - "version": "5.0.4", + "version": "5.0.5", "dev": true, "inBundle": true, "license": "ISC", @@ -4098,12 +4334,12 @@ } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "5.0.19", + "version": "5.0.21", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^6.3.0", + "@npmcli/arborist": "^6.5.0", "@npmcli/run-script": "^6.0.0", "npm-package-arg": "^10.1.0", "pacote": "^15.0.8" @@ -4113,12 +4349,12 @@ } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "7.5.0", + "version": "7.5.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "ci-info": "^3.6.1", + "ci-info": "^4.0.0", "normalize-package-data": "^5.0.0", "npm-package-arg": "^10.1.0", "npm-registry-fetch": "^14.0.3", @@ -4132,7 +4368,7 @@ } }, "node_modules/npm/node_modules/libnpmsearch": { - "version": "6.0.2", + "version": "6.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -4144,7 +4380,7 @@ } }, "node_modules/npm/node_modules/libnpmteam": { - "version": "5.0.3", + "version": "5.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -4157,7 +4393,7 @@ } }, "node_modules/npm/node_modules/libnpmversion": { - "version": "4.0.2", + "version": "4.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -4207,6 +4443,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm/node_modules/make-fetch-happen/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/minimatch": { "version": "9.0.3", "dev": true, @@ -4223,12 +4468,12 @@ } }, "node_modules/npm/node_modules/minipass": { - "version": "5.0.0", + "version": "7.0.4", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/npm/node_modules/minipass-collect": { @@ -4256,12 +4501,12 @@ } }, "node_modules/npm/node_modules/minipass-fetch": { - "version": "3.0.3", + "version": "3.0.4", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^5.0.0", + "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, @@ -4428,7 +4673,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "9.4.0", + "version": "9.4.1", "dev": true, "inBundle": true, "license": "MIT", @@ -4437,7 +4682,7 @@ "exponential-backoff": "^3.1.1", "glob": "^7.1.4", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^11.0.3", + "make-fetch-happen": "^10.0.3", "nopt": "^6.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", @@ -4452,33 +4697,127 @@ "node": "^12.13 || ^14.13 || >=16" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { - "version": "1.1.1", + "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/fs": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/are-we-there-yet": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { + "version": "16.1.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/glob": { + "version": "8.1.0", "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, - "node_modules/npm/node_modules/node-gyp/node_modules/are-we-there-yet": { - "version": "3.0.1", + "node_modules/npm/node_modules/node-gyp/node_modules/cacache/node_modules/minimatch": { + "version": "5.1.6", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + "node": ">=10" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/brace-expansion": { - "version": "1.1.11", + "node_modules/npm/node_modules/node-gyp/node_modules/fs-minipass": { + "version": "2.1.0", "dev": true, "inBundle": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" } }, "node_modules/npm/node_modules/node-gyp/node_modules/gauge": { @@ -4520,6 +4859,33 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { + "version": "10.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm/node_modules/node-gyp/node_modules/minimatch": { "version": "3.1.2", "dev": true, @@ -4532,6 +4898,35 @@ "node": "*" } }, + "node_modules/npm/node_modules/node-gyp/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/minipass-fetch": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { "version": "6.0.0", "dev": true, @@ -4562,25 +4957,47 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/readable-stream": { - "version": "3.6.2", + "node_modules/npm/node_modules/node-gyp/node_modules/signal-exit": { + "version": "3.0.7", "dev": true, "inBundle": true, - "license": "MIT", + "license": "ISC" + }, + "node_modules/npm/node_modules/node-gyp/node_modules/ssri": { + "version": "9.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" + "minipass": "^3.1.1" }, "engines": { - "node": ">= 6" + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/signal-exit": { - "version": "3.0.7", + "node_modules/npm/node_modules/node-gyp/node_modules/unique-filename": { + "version": "2.0.1", "dev": true, "inBundle": true, - "license": "ISC" + "license": "ISC", + "dependencies": { + "unique-slug": "^3.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/unique-slug": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } }, "node_modules/npm/node_modules/node-gyp/node_modules/which": { "version": "2.0.2", @@ -4649,7 +5066,7 @@ } }, "node_modules/npm/node_modules/npm-install-checks": { - "version": "6.1.1", + "version": "6.3.0", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -4697,7 +5114,7 @@ } }, "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "8.0.1", + "version": "8.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -4742,6 +5159,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/npm-user-validate": { "version": "2.0.0", "dev": true, @@ -4822,6 +5248,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/npm/node_modules/pacote/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/parse-conflict-json": { "version": "3.0.1", "dev": true, @@ -4855,13 +5290,13 @@ } }, "node_modules/npm/node_modules/path-scurry": { - "version": "1.9.2", + "version": "1.10.1", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1", - "minipass": "^5.0.0 || ^6.0.2" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -4871,7 +5306,7 @@ } }, "node_modules/npm/node_modules/path-scurry/node_modules/lru-cache": { - "version": "9.1.1", + "version": "10.2.0", "dev": true, "inBundle": true, "license": "ISC", @@ -4880,7 +5315,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.0.13", + "version": "6.0.15", "dev": true, "inBundle": true, "license": "MIT", @@ -4901,15 +5336,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/process": { - "version": "0.11.10", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/npm/node_modules/promise-all-reject-late": { "version": "1.0.1", "dev": true, @@ -5017,18 +5443,17 @@ } }, "node_modules/npm/node_modules/readable-stream": { - "version": "4.4.0", + "version": "3.6.2", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">= 6" } }, "node_modules/npm/node_modules/retry": { @@ -5125,7 +5550,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.5.4", + "version": "7.6.0", "dev": true, "inBundle": true, "license": "ISC", @@ -5179,7 +5604,7 @@ } }, "node_modules/npm/node_modules/signal-exit": { - "version": "4.0.2", + "version": "4.1.0", "dev": true, "inBundle": true, "license": "ISC", @@ -5191,13 +5616,15 @@ } }, "node_modules/npm/node_modules/sigstore": { - "version": "1.7.0", + "version": "1.9.0", "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.1.0", - "@sigstore/tuf": "^1.0.1", + "@sigstore/bundle": "^1.1.0", + "@sigstore/protobuf-specs": "^0.2.0", + "@sigstore/sign": "^1.0.0", + "@sigstore/tuf": "^1.0.3", "make-fetch-happen": "^11.0.1" }, "bin": { @@ -5218,16 +5645,16 @@ } }, "node_modules/npm/node_modules/socks": { - "version": "2.7.1", + "version": "2.8.1", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { - "ip": "^2.0.0", + "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" }, "engines": { - "node": ">= 10.13.0", + "node": ">= 10.0.0", "npm": ">= 3.0.0" } }, @@ -5256,7 +5683,7 @@ } }, "node_modules/npm/node_modules/spdx-exceptions": { - "version": "2.3.0", + "version": "2.5.0", "dev": true, "inBundle": true, "license": "CC-BY-3.0" @@ -5272,18 +5699,18 @@ } }, "node_modules/npm/node_modules/spdx-license-ids": { - "version": "3.0.13", + "version": "3.0.17", "dev": true, "inBundle": true, "license": "CC0-1.0" }, "node_modules/npm/node_modules/ssri": { - "version": "10.0.4", + "version": "10.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "minipass": "^5.0.0" + "minipass": "^7.0.3" }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -5365,7 +5792,7 @@ } }, "node_modules/npm/node_modules/tar": { - "version": "6.1.15", + "version": "6.2.0", "dev": true, "inBundle": true, "license": "ISC", @@ -5405,6 +5832,15 @@ "node": ">=8" } }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/npm/node_modules/text-table": { "version": "0.2.0", "dev": true, @@ -5663,15 +6099,15 @@ } }, "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "dependencies": { - "mimic-fn": "^4.0.0" + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">=12" + "node": ">=6" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5690,15 +6126,15 @@ } }, "node_modules/p-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-3.0.0.tgz", - "integrity": "sha512-QtoWLjXAW++uTX67HZQz1dbTpqBfiidsB6VtQUC9iR85S120+s0T5sO6s+B5MLzFcZkrEd/DGMmCjR+f2Qpxwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", + "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", "dev": true, "dependencies": { - "p-map": "^5.1.0" + "p-map": "^7.0.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5744,30 +6180,24 @@ } }, "node_modules/p-map": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-5.5.0.tgz", - "integrity": "sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.1.tgz", + "integrity": "sha512-2wnaR0XL/FDOj+TgpDuRb2KTjLnu3Fma6b1ZUwGY7LcqenMcvP/YFpjpbPKY6WVGsbuJZRuoUz8iPrt8ORnAFw==", "dev": true, - "dependencies": { - "aggregate-error": "^4.0.0" - }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-reduce": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", - "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", + "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/p-try": { @@ -6000,15 +6430,15 @@ } }, "node_modules/read-pkg": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.0.0.tgz", - "integrity": "sha512-Ajb9oSjxXBw0YyOiwtQ2dKbAA/vMnUPnY63XcCk+mXo0BwIdQEMgZLZiMWGttQHcUhUgbK0mH85ethMPKXxziw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-8.1.0.tgz", + "integrity": "sha512-PORM8AgzXeskHO/WEv312k9U03B8K9JSiWF/8N9sUuFjBa+9SF2u6K7VClzXwDXab51jCd8Nd36CNM+zR97ScQ==", "dev": true, "dependencies": { "@types/normalize-package-data": "^2.4.1", - "normalize-package-data": "^5.0.0", + "normalize-package-data": "^6.0.0", "parse-json": "^7.0.0", - "type-fest": "^3.8.0" + "type-fest": "^4.2.0" }, "engines": { "node": ">=16" @@ -6018,15 +6448,27 @@ } }, "node_modules/read-pkg-up": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.0.0.tgz", - "integrity": "sha512-jgmKiS//w2Zs+YbX039CorlkOp8FIVbSAN8r8GJHDsGlmNPXo+VeHkqAwCiQVTTx5/LwLZTcEw59z3DvcLbr0g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-10.1.0.tgz", + "integrity": "sha512-aNtBq4jR8NawpKJQldrQcSW9y/d+KWH4v24HWkHljOZ7H0av+YTGANBzRh9A5pw7v/bLVsLVPpOhJ7gHNVy8lA==", "dev": true, "dependencies": { "find-up": "^6.3.0", - "read-pkg": "^8.0.0", - "type-fest": "^3.12.0" + "read-pkg": "^8.1.0", + "type-fest": "^4.2.0" + }, + "engines": { + "node": ">=16" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.12.0.tgz", + "integrity": "sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==", + "dev": true, "engines": { "node": ">=16" }, @@ -6035,42 +6477,42 @@ } }, "node_modules/read-pkg/node_modules/json-parse-even-better-errors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", - "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", + "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/read-pkg/node_modules/lines-and-columns": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", - "integrity": "sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", + "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", "dev": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", - "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", + "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", "dev": true, "dependencies": { - "hosted-git-info": "^6.0.0", + "hosted-git-info": "^7.0.0", "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/read-pkg/node_modules/parse-json": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.0.0.tgz", - "integrity": "sha512-kP+TQYAzAiVnzOlWOe0diD6L35s9bJh0SCn95PIbZFKrOYuIRQsQkeWEYxzVDuHTt9V9YqvYCJ2Qo4z9wdfZPw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-7.1.1.tgz", + "integrity": "sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.21.4", @@ -6086,6 +6528,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/read-pkg/node_modules/parse-json/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/type-fest": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.12.0.tgz", + "integrity": "sha512-5Y2/pp2wtJk8o08G0CMkuFPCO354FGwk/vbidxrdhRGZfd0tFnb4Qb8anp9XxXriwBgVPjdWbKpGl4J9lJY2jQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -6101,24 +6567,21 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", "dev": true, - "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/redent/node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, "engines": { "node": ">=8" } @@ -6154,12 +6617,12 @@ } }, "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { - "is-core-module": "^2.11.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -6213,15 +6676,29 @@ } }, "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] }, "node_modules/semantic-release": { - "version": "21.0.7", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-21.0.7.tgz", - "integrity": "sha512-peRDSXN+hF8EFSKzze90ff/EnAmgITHQ/a3SZpRV3479ny0BIZWEJ33uX6/GlOSKdaSxo9hVRDyv2/u2MuF+Bw==", + "version": "21.1.2", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-21.1.2.tgz", + "integrity": "sha512-kz76azHrT8+VEkQjoCBHE06JNQgTgsC4bT8XfCzb7DHcsk9vG3fqeMVik8h5rcWCYi2Fd+M3bwA7BG8Z8cRwtA==", "dev": true, "dependencies": { "@semantic-release/commit-analyzer": "^10.0.0", @@ -6229,17 +6706,17 @@ "@semantic-release/github": "^9.0.0", "@semantic-release/npm": "^10.0.2", "@semantic-release/release-notes-generator": "^11.0.0", - "aggregate-error": "^4.0.1", + "aggregate-error": "^5.0.0", "cosmiconfig": "^8.0.0", "debug": "^4.0.0", "env-ci": "^9.0.0", - "execa": "^7.0.0", + "execa": "^8.0.0", "figures": "^5.0.0", "find-versions": "^5.1.0", "get-stream": "^6.0.0", "git-log-parser": "^1.2.0", "hook-std": "^3.0.0", - "hosted-git-info": "^6.0.0", + "hosted-git-info": "^7.0.0", "lodash-es": "^4.17.21", "marked": "^5.0.0", "marked-terminal": "^5.1.1", @@ -6260,10 +6737,208 @@ "node": ">=18" } }, + "node_modules/semantic-release/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/semantic-release/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/clean-stack": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", + "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", + "dev": true, + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/semantic-release/node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/semantic-release/node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/p-reduce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", + "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/semantic-release/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -6439,12 +7114,12 @@ } }, "node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "engines": { - "node": ">=12" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6476,9 +7151,9 @@ } }, "node_modules/spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "dev": true }, "node_modules/spdx-expression-parse": { @@ -6492,9 +7167,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.13", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", - "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "version": "3.0.17", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", + "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", "dev": true }, "node_modules/split": { @@ -6510,26 +7185,12 @@ } }, "node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "dependencies": { - "readable-stream": "^3.0.0" - } - }, - "node_modules/split2/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, "engines": { - "node": ">= 6" + "node": ">= 10.x" } }, "node_modules/stream-combiner2": { @@ -6551,6 +7212,12 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -6587,15 +7254,12 @@ } }, "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/strip-indent": { @@ -6683,6 +7347,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tempy/node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -6696,12 +7372,15 @@ } }, "node_modules/text-extensions": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", - "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", "dev": true, "engines": { - "node": ">=0.10" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/through": { @@ -6733,10 +7412,13 @@ } }, "node_modules/traverse": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz", - "integrity": "sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg==", + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", + "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6775,6 +7457,18 @@ "node": ">=0.8.0" } }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -6791,15 +7485,15 @@ } }, "node_modules/universal-user-agent": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", - "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", "dev": true }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "engines": { "node": ">= 10.0.0" From b472344d7bb9e9f63dc4c97bbf3545e7d761d8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 14 Mar 2024 12:30:41 +0100 Subject: [PATCH 178/183] feat: uses now `referringConnector` instead of `SKI:AKI` --- .../de/truzzt/clearinghouse/edc/handler/Handler.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java index 88cc093..b82eb89 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java @@ -50,10 +50,10 @@ default Date convertLocalDateTime(LocalDateTime localDateTime) { var tokenValue = securityToken.getTokenValue(); var decodedToken = JWT.decode(tokenValue); - - var subject = decodedToken.getSubject(); - if (subject == null) { - throw new EdcException("JWT Token subject is missing"); + + var referringConnector = decodedToken.getClaim("referringConnector").asString(); + if (referringConnector == null) { + throw new EdcException("JWT Token referringConnector is missing"); } var issuedAt = LocalDateTime.now(); @@ -63,7 +63,7 @@ default Date convertLocalDateTime(LocalDateTime localDateTime) { var jwtToken = JWT.create() .withAudience(context.getSetting(JWT_AUDIENCE_SETTING, JWT_AUDIENCE_DEFAULT_VALUE)) .withIssuer(context.getSetting(JWT_ISSUER_SETTING, JWT_ISSUER_DEFAULT_VALUE)) - .withClaim("client_id", subject) + .withClaim("client_id", referringConnector) .withIssuedAt(convertLocalDateTime(issuedAt)) .withExpiresAt(convertLocalDateTime(expiresAt)); From 25cd379c969c8747f618fc5166e97957631504e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Sch=C3=B6nenberg?= Date: Thu, 14 Mar 2024 12:39:09 +0100 Subject: [PATCH 179/183] fix: change tests for `referringConnector` test --- .../java/de/truzzt/clearinghouse/edc/handler/Handler.java | 2 +- .../clearinghouse/edc/handler/LogMessageHandlerTest.java | 4 ++-- .../clearinghouse/edc/handler/RequestMessageHandlerTest.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java index b82eb89..648da13 100644 --- a/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java +++ b/clearing-house-edc/core/src/main/java/de/truzzt/clearinghouse/edc/handler/Handler.java @@ -50,7 +50,7 @@ default Date convertLocalDateTime(LocalDateTime localDateTime) { var tokenValue = securityToken.getTokenValue(); var decodedToken = JWT.decode(tokenValue); - + var referringConnector = decodedToken.getClaim("referringConnector").asString(); if (referringConnector == null) { throw new EdcException("JWT Token referringConnector is missing"); diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index 5d9e2b9..9c71f4f 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -90,13 +90,13 @@ public void successfulHandleRequest(){ } @Test - public void missingSubjectBuildJwtToken() { + public void missingReferringConnectorBuildJwtToken() { EdcException exception = assertThrows(EdcException.class, () -> logMessageHandler.buildJWTToken( TestUtils.getInvalidTokenHandlerRequest(mapper) .getHeader() .getSecurityToken(), context)); - assertEquals("JWT Token subject is missing",exception.getMessage()); + assertEquals("JWT Token referringConnector is missing",exception.getMessage()); } @Test public void successfulBuildJwtToken() { diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandlerTest.java index 5c9032b..eac9ab1 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/RequestMessageHandlerTest.java @@ -93,13 +93,13 @@ public void successfulHandleRequest(){ } @Test - public void missingSubjectBuildJwtToken() { + public void missingReferringConnectorBuildJwtToken() { EdcException exception = assertThrows(EdcException.class, () -> requestMessageHandler.buildJWTToken( TestUtils.getInvalidTokenHandlerRequest(mapper) .getHeader() .getSecurityToken(), context)); - assertEquals("JWT Token subject is missing",exception.getMessage()); + assertEquals("JWT Token referringConnector is missing",exception.getMessage()); } @Test public void successfulBuildJwtToken() { From ebbbbf5be4c84541c14493883bda1e0c63455e74 Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Fri, 15 Mar 2024 09:59:06 -0300 Subject: [PATCH 180/183] bugfix: Change the invalid token --- .../clearinghouse/edc/handler/LogMessageHandlerTest.java | 3 ++- .../core/src/test/resources/headers/invalid-token.json | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index 9c71f4f..5d734ac 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -91,12 +91,13 @@ public void successfulHandleRequest(){ @Test public void missingReferringConnectorBuildJwtToken() { + doReturn("1").when(context).getSetting(anyString(), anyString()); EdcException exception = assertThrows(EdcException.class, () -> logMessageHandler.buildJWTToken( TestUtils.getInvalidTokenHandlerRequest(mapper) .getHeader() .getSecurityToken(), context)); - assertEquals("JWT Token referringConnector is missing",exception.getMessage()); + assertEquals("JWT Token referringConnector is missing", exception.getMessage()); } @Test public void successfulBuildJwtToken() { diff --git a/clearing-house-edc/core/src/test/resources/headers/invalid-token.json b/clearing-house-edc/core/src/test/resources/headers/invalid-token.json index ea2bbf7..c3542b3 100644 --- a/clearing-house-edc/core/src/test/resources/headers/invalid-token.json +++ b/clearing-house-edc/core/src/test/resources/headers/invalid-token.json @@ -8,7 +8,7 @@ "ids:securityToken": { "@type" : "ids:DynamicAttributeToken", "@id" : "https://w3id.org/idsa/autogen/dynamicAttributeToken/7bbbd2c1-2d75-4e3d-bd10-c52d0381cab0", - "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJyZWZlcnJpbmdDb25uZWN0b3IiOiJodHRwOi8vYnJva2VyLmlkcy5pc3N0LmZyYXVuaG9mZXIuZGUuZGVtbyIsIkB0eXBlIjoiaWRzOkRhdFBheWxvYWQiLCJAY29udGV4dCI6Imh0dHBzOi8vdzNpZC5vcmcvaWRzYS9jb250ZXh0cy9jb250ZXh0Lmpzb25sZCIsInRyYW5zcG9ydENlcnRzU2hhMjU2IjoiOTc0ZTYzMjRmMTJmMTA5MTZmNDZiZmRlYjE4YjhkZDZkYTc4Y2M2YTZhMDU2NjAzMWZhNWYxYTM5ZWM4ZTYwMCJ9.hekZoPDjEWaXreQl3l0PUIjBOPQhAl0w2mH4_PdNWuA", + "ids:tokenValue" : "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzY29wZXMiOlsiaWRzYzpJRFNfQ09OTkVDVE9SX0FUVFJJQlVURVNfQUxMIl0sImF1ZCI6Imlkc2M6SURTX0NPTk5FQ1RPUlNfQUxMIiwiaXNzIjoiaHR0cHM6Ly9kYXBzLmFpc2VjLmZyYXVuaG9mZXIuZGUiLCJuYmYiOjE2MzQ2NTA3MzksImlhdCI6MTYzNDY1MDczOSwianRpIjoiTVRneE9EUXdPVFF6TXpZd05qWXlOVFExTUE9PSIsImV4cCI6MTYzNDY1NDMzOSwic2VjdXJpdHlQcm9maWxlIjoiaWRzYzpCQVNFX1NFQ1VSSVRZX1BST0ZJTEUiLCJAdHlwZSI6ImlkczpEYXRQYXlsb2FkIiwiQGNvbnRleHQiOiJodHRwczovL3czaWQub3JnL2lkc2EvY29udGV4dHMvY29udGV4dC5qc29ubGQiLCJ0cmFuc3BvcnRDZXJ0c1NoYTI1NiI6Ijk3NGU2MzI0ZjEyZjEwOTE2ZjQ2YmZkZWIxOGI4ZGQ2ZGE3OGNjNmE2YTA1NjYwMzFmYTVmMWEzOWVjOGU2MDAifQ.bz-XdCsjNwk8ce-9oHFta2wyojw7m4yplSGUoX1yAWY", "ids:tokenFormat" : { "@id" : "idsc:JWT" } From 9bbdb0a4ff56ff2278dd9c9f331cb6d4a1fe607c Mon Sep 17 00:00:00 2001 From: Augusto Leal Date: Fri, 15 Mar 2024 10:01:21 -0300 Subject: [PATCH 181/183] bugfix: Change the invalid token --- .../clearinghouse/edc/handler/LogMessageHandlerTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java index 5d734ac..9c71f4f 100644 --- a/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java +++ b/clearing-house-edc/core/src/test/java/de/truzzt/clearinghouse/edc/handler/LogMessageHandlerTest.java @@ -91,13 +91,12 @@ public void successfulHandleRequest(){ @Test public void missingReferringConnectorBuildJwtToken() { - doReturn("1").when(context).getSetting(anyString(), anyString()); EdcException exception = assertThrows(EdcException.class, () -> logMessageHandler.buildJWTToken( TestUtils.getInvalidTokenHandlerRequest(mapper) .getHeader() .getSecurityToken(), context)); - assertEquals("JWT Token referringConnector is missing", exception.getMessage()); + assertEquals("JWT Token referringConnector is missing",exception.getMessage()); } @Test public void successfulBuildJwtToken() { From 4ca18d4587d74b29d11e59a0e6de2c5f3b36ccd1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 15 Mar 2024 13:17:41 +0000 Subject: [PATCH 182/183] chore(release): 1.0.0-beta.3 [skip ci] # [1.0.0-beta.3](https://github.com/ids-basecamp/clearinghouse/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2024-03-15) ### Bug Fixes * change tests for `referringConnector` test ([25cd379](https://github.com/ids-basecamp/clearinghouse/commit/25cd379c969c8747f618fc5166e97957631504e8)) ### Features * **ch-app:** Add create_process test and fix an issue ([8cfb5e1](https://github.com/ids-basecamp/clearinghouse/commit/8cfb5e18feea759aeb4425cb900453f86f07c15f)) * **ch-app:** Add testcontainers for Integration tests with database ([679b06b](https://github.com/ids-basecamp/clearinghouse/commit/679b06b95d8e7ac58019fd21e678c6725c79083e)) * enable pedantic linter and fix clippy findings where appropiate ([df0a5d4](https://github.com/ids-basecamp/clearinghouse/commit/df0a5d40ed50ea45f90383c21666b51fb89bdddd)) * uses now `referringConnector` instead of `SKI:AKI` ([b472344](https://github.com/ids-basecamp/clearinghouse/commit/b472344d7bb9e9f63dc4c97bbf3545e7d761d8f6)) --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b29dd0..99379d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +# [1.0.0-beta.3](https://github.com/ids-basecamp/clearinghouse/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2024-03-15) + + +### Bug Fixes + +* change tests for `referringConnector` test ([25cd379](https://github.com/ids-basecamp/clearinghouse/commit/25cd379c969c8747f618fc5166e97957631504e8)) + + +### Features + +* **ch-app:** Add create_process test and fix an issue ([8cfb5e1](https://github.com/ids-basecamp/clearinghouse/commit/8cfb5e18feea759aeb4425cb900453f86f07c15f)) +* **ch-app:** Add testcontainers for Integration tests with database ([679b06b](https://github.com/ids-basecamp/clearinghouse/commit/679b06b95d8e7ac58019fd21e678c6725c79083e)) +* enable pedantic linter and fix clippy findings where appropiate ([df0a5d4](https://github.com/ids-basecamp/clearinghouse/commit/df0a5d40ed50ea45f90383c21666b51fb89bdddd)) +* uses now `referringConnector` instead of `SKI:AKI` ([b472344](https://github.com/ids-basecamp/clearinghouse/commit/b472344d7bb9e9f63dc4c97bbf3545e7d761d8f6)) + # [1.0.0-beta.2](https://github.com/ids-basecamp/clearinghouse/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2024-02-19) From 09bd3ef1f2eea52961acf2c10f6c54507656c155 Mon Sep 17 00:00:00 2001 From: dhommen Date: Mon, 30 Sep 2024 13:35:35 +0200 Subject: [PATCH 183/183] chore: update semantic-release to not comment or relable on release --- .releaserc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.releaserc b/.releaserc index 35403cd..e4ce0ea 100644 --- a/.releaserc +++ b/.releaserc @@ -15,6 +15,12 @@ "@semantic-release/release-notes-generator", "@semantic-release/changelog", "@semantic-release/git", - "@semantic-release/github" + [ + "@semantic-release/github", + { + "successComment": false, + "releasedLabels": false + } + ] ] }