diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a0cc4493..02067e1c 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -25,7 +25,7 @@ jobs: needs: build steps: - name: Add hosts for integration tests - run: sudo echo "127.0.0.1 localhost auth.example.com matrix.example.com matrix1.example.com matrix2.example.com matrix3.example.com federation.example.com" | sudo tee -a /etc/hosts + run: sudo echo "127.0.0.1 localhost auth.example.com matrix.example.com matrix1.example.com matrix2.example.com matrix3.example.com federation.example.com opensearch.example.com" | sudo tee -a /etc/hosts - uses: actions/checkout@v3 - name: Set up Node LTS uses: actions/setup-node@v3 diff --git a/package-lock.json b/package-lock.json index e0fbaf7b..80b41e88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "supertest": "^6.3.3", "swagger-jsdoc": "^6.2.8", "swagger-ui-dist": "^4.18.3", - "testcontainers": "^9.8.0", + "testcontainers": "^10.6.0", "toad-cache": "^3.3.0", "ts-jest": "^29.1.0", "typescript": "^4.9.5" @@ -4446,6 +4446,27 @@ "@octokit/openapi-types": "^18.0.0" } }, + "node_modules/@opensearch-project/opensearch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.7.0.tgz", + "integrity": "sha512-ee4XEU0CSwbThGgKcROmQPwG48QjMaMJzJdgUaGqeIeni7YMJqlZ6g4pbPD7iDE19Y1e2/OEzeW54DE/Fyky2g==", + "dependencies": { + "aws4": "^1.11.0", + "debug": "^4.3.1", + "hpagent": "^1.2.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0" + }, + "engines": { + "node": ">=10", + "yarn": "^1.22.10" + } + }, + "node_modules/@opensearch-project/opensearch/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/@parcel/watcher": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.0.4.tgz", @@ -4959,9 +4980,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.2.tgz", - "integrity": "sha512-VGodkwtEuZ+ENPz/CpDSl091koMv8ao5jHVMbG1vNK+sbx/48/wVzP84M5xSfDAC69mAKKoEkSo+ym9bXYRK9w==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.3.tgz", + "integrity": "sha512-1ACInKIT0pXmTYuPoJAL8sOT0lV3PEACFSVxnD03hGIojJ1CmbzZmLJyk2xew+yxqTlmx7xydkiJcBzdp0V+AQ==", "cpu": [ "arm" ], @@ -4972,9 +4993,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.2.tgz", - "integrity": "sha512-5/W1xyIdc7jw6c/f1KEtg1vYDBWnWCsLiipK41NiaWGLG93eH2edgE6EgQJ3AGiPERhiOLUqlDSfjRK08C9xFg==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.3.tgz", + "integrity": "sha512-vGl+Bny8cawCM7ExugzqEB8ke3t7Pm9/mo+ciA9kJh6pMuNyM+31qhewMwHwseDZ/LtdW0SCocW1CsMxcq1Lsg==", "cpu": [ "arm64" ], @@ -4985,9 +5006,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.2.tgz", - "integrity": "sha512-vOAKMqZSTbPfyPVu1jBiy+YniIQd3MG7LUnqV0dA6Q5tyhdqYtxacTHP1+S/ksKl6qCtMG1qQ0grcIgk/19JEA==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.3.tgz", + "integrity": "sha512-Lj8J9WzQRvfWO4GfI+bBkIThUFV1PtI+es/YH/3cwUQ+edXu8Mre0JRJfRrAeRjPiHDPFFZaX51zfgHHEhgRAg==", "cpu": [ "arm64" ], @@ -4998,9 +5019,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.2.tgz", - "integrity": "sha512-aIJVRUS3Dnj6MqocBMrcXlatKm64O3ITeQAdAxVSE9swyhNyV1dwnRgw7IGKIkDQofatd8UqMSyUxuFEa42EcA==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.3.tgz", + "integrity": "sha512-NPPOXMTIWJk50lgZmRReEYJFvLG5rgMDzaVauWNB2MgFQYm9HuNXQdVVg3iEZ3A5StIzxhMlPjVyS5fsv4PJmg==", "cpu": [ "x64" ], @@ -5011,9 +5032,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.2.tgz", - "integrity": "sha512-/bjfUiXwy3P5vYr6/ezv//Yle2Y0ak3a+Av/BKoi76nFryjWCkki8AuVoPR7ZU/ckcvAWFo77OnFK14B9B5JsA==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.3.tgz", + "integrity": "sha512-ij4tv1XtWcDScaTgoMnvDEYZ2Wjl2ZhDFEyftjBKu6sNNLHIkKuXBol/bVSh+md5zSJ6em9hUXyPO3cVPCsl4Q==", "cpu": [ "arm" ], @@ -5024,9 +5045,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.2.tgz", - "integrity": "sha512-S24b+tJHwpq2TNRz9T+r71FjMvyBBApY8EkYxz8Cwi/rhH6h+lu/iDUxyc9PuHf9UvyeBFYkWWcrDahai/NCGw==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.3.tgz", + "integrity": "sha512-MTMAl30dzcfYB+smHe1sJuS2P1/hB8pqylkCe0/8/Lo8CADjy/eM8x43nBoR5eqcYgpOtCh7IgHpvqSMAE38xw==", "cpu": [ "arm" ], @@ -5037,9 +5058,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.2.tgz", - "integrity": "sha512-UN7VAXLyeyGbCQWiOtQN7BqmjTDw1ON2Oos4lfk0YR7yNhFEJWZiwGtvj9Ay4lsT/ueT04sh80Sg2MlWVVZ+Ug==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.3.tgz", + "integrity": "sha512-vY3fAg6JLDoNh781HHHMPvt8K6RWG3OmEj3xI9BOFSQTD5PNaGKvCB815MyGlDnFYUw7lH+WvvQqoBwLtRDR1A==", "cpu": [ "arm64" ], @@ -5050,9 +5071,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.2.tgz", - "integrity": "sha512-ZBKvz3+rIhQjusKMccuJiPsStCrPOtejCHxTe+yWp3tNnuPWtyCh9QLGPKz6bFNFbwbw28E2T6zDgzJZ05F1JQ==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.3.tgz", + "integrity": "sha512-61SpQGBSb8QkfV/hUYWezlEig4ro55t8NcE5wWmy1bqRsRVHCEDkF534d+Lln/YeLUoSWtJHvvG3bx9lH/S6uA==", "cpu": [ "arm64" ], @@ -5063,9 +5084,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.2.tgz", - "integrity": "sha512-LjMMFiVBRL3wOe095vHAekL4b7nQqf4KZEpdMWd3/W+nIy5o9q/8tlVKiqMbfieDypNXLsxM9fexOxd9Qcklyg==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.3.tgz", + "integrity": "sha512-4XGexJthsNhEEgv/zK4/NnAOjYKoeCsIoT+GkqTY2u3rse0lbJ8ft1bpDCdlkvifsLDL2uwe4fn8PLR4IMTKQQ==", "cpu": [ "ppc64" ], @@ -5076,9 +5097,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.2.tgz", - "integrity": "sha512-ohkPt0lKoCU0s4B6twro2aft+QROPdUiWwOjPNTzwTsBK5w+2+iT9kySdtOdq0gzWJAdiqsV4NFtXOwGZmIsHA==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.3.tgz", + "integrity": "sha512-/pArXjqnEdhbQ1qe4CTTlJ6/GjWGdWNRucKAp4fqKnKf7QC0BES3QEV34ACumHHQ4uEGt4GctF2ISCMRhkli0A==", "cpu": [ "riscv64" ], @@ -5089,9 +5110,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.2.tgz", - "integrity": "sha512-jm2lvLc+/gqXfndlpDw05jKvsl/HKYxUEAt1h5UXcMFVpO4vGpoWmJVUfKDtTqSaHcCNw1his1XjkgR9aort3w==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.3.tgz", + "integrity": "sha512-vu4f3Y8iwjtRfSZdmtP8nC1jmRx1IrRVo2cLQlQfpFZ0e2AE9YbPgfIzpuK+i3C4zFETaLLNGezbBns2NuS/uA==", "cpu": [ "s390x" ], @@ -5102,9 +5123,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.2.tgz", - "integrity": "sha512-oc5/SlITI/Vj/qL4UM+lXN7MERpiy1HEOnrE+SegXwzf7WP9bzmZd6+MDljCEZTdSY84CpvUv9Rq7bCaftn1+g==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.3.tgz", + "integrity": "sha512-n4HEgIJulNSmAKT3SYF/1wuzf9od14woSBseNkzur7a+KJIbh2Jb+J9KIsdGt3jJnsLW0BT1Sj6MiwL4Zzku6Q==", "cpu": [ "x64" ], @@ -5115,9 +5136,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.2.tgz", - "integrity": "sha512-/2VWEBG6mKbS2itm7hzPwhIPaxfZh/KLWrYg20pCRLHhNFtF+epLgcBtwy3m07bl/k86Q3PFRAf2cX+VbZbwzQ==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.3.tgz", + "integrity": "sha512-guO/4N1884ig2AzTKPc6qA7OTnFMUEg/X2wiesywRO1eRD7FzHiaiTQQOLFmnUXWj2pgQXIT1g5g3e2RpezXcQ==", "cpu": [ "x64" ], @@ -5128,9 +5149,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.2.tgz", - "integrity": "sha512-Wg7ANh7+hSilF0lG3e/0Oy8GtfTIfEk1327Bw8juZOMOoKmJLs3R+a4JDa/4cHJp2Gs7QfCDTepXXcyFD0ubBg==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.3.tgz", + "integrity": "sha512-+rxD3memdkhGz0NhNqbYHXBoA33MoHBK4uubZjF1IeQv1Psi6tqgsCcC6vwQjxBM1qoCqOQQBy0cgNbbZKnGUg==", "cpu": [ "arm64" ], @@ -5141,9 +5162,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.2.tgz", - "integrity": "sha512-J/jCDKVMWp0Y2ELnTjpQFYUCUWv1Jr+LdFrJVZtdqGyjDo0PHPa7pCamjHvJel6zBFM3doFFqAr7cmXYWBAbfw==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.3.tgz", + "integrity": "sha512-0NxVbLhBXmwANWWbgZY/RdSkeuHEgF+u8Dc0qBowUVBYsR2y2vwVGjKgUcj1wtu3jpjs057io5g9HAPr3Icqjg==", "cpu": [ "ia32" ], @@ -5154,9 +5175,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.2.tgz", - "integrity": "sha512-3nIf+SJMs2ZzrCh+SKNqgLVV9hS/UY0UjT1YU8XQYFGLiUfmHYJ/5trOU1XSvmHjV5gTF/K3DjrWxtyzKKcAHA==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.3.tgz", + "integrity": "sha512-hutnZavtOx/G4uVdgoZz5279By9NVbgmxOmGGgnzUjZYuwp2+NzGq6KXQmHXBWz7W/vottXn38QmKYAdQLa/vQ==", "cpu": [ "x64" ], @@ -5738,15 +5759,6 @@ "@types/estree": "*" } }, - "node_modules/@types/archiver": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/@types/archiver/-/archiver-5.3.4.tgz", - "integrity": "sha512-Lj7fLBIMwYFgViVVZHEdExZC3lVYsl+QL0VmdNdIzGZH544jHveYWij6qdnBgJQDnR7pMKliN9z2cPZFEbhyPw==", - "dev": true, - "dependencies": { - "@types/readdir-glob": "*" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -6195,15 +6207,6 @@ "@types/react": "*" } }, - "node_modules/@types/readdir-glob": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.5.tgz", - "integrity": "sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -6945,9 +6948,9 @@ } }, "node_modules/@vanilla-extract/integration/node_modules/rollup": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.2.tgz", - "integrity": "sha512-sxDP0+pya/Yi5ZtptF4p3avI+uWCIf/OdrfdH2Gbv1kWddLKk0U7WE3PmQokhi5JrektxsK3sK8s4hzAmjqahw==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.3.tgz", + "integrity": "sha512-Ygm4fFO4usWcAG3Ud36Lmif5nudoi0X6QPLC+kRgrRjulAbmFkaTawP7fTIkRDnCNSf/4IAQzXM1T8e691kRtw==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -6960,22 +6963,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.16.2", - "@rollup/rollup-android-arm64": "4.16.2", - "@rollup/rollup-darwin-arm64": "4.16.2", - "@rollup/rollup-darwin-x64": "4.16.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.16.2", - "@rollup/rollup-linux-arm-musleabihf": "4.16.2", - "@rollup/rollup-linux-arm64-gnu": "4.16.2", - "@rollup/rollup-linux-arm64-musl": "4.16.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.16.2", - "@rollup/rollup-linux-riscv64-gnu": "4.16.2", - "@rollup/rollup-linux-s390x-gnu": "4.16.2", - "@rollup/rollup-linux-x64-gnu": "4.16.2", - "@rollup/rollup-linux-x64-musl": "4.16.2", - "@rollup/rollup-win32-arm64-msvc": "4.16.2", - "@rollup/rollup-win32-ia32-msvc": "4.16.2", - "@rollup/rollup-win32-x64-msvc": "4.16.2", + "@rollup/rollup-android-arm-eabi": "4.16.3", + "@rollup/rollup-android-arm64": "4.16.3", + "@rollup/rollup-darwin-arm64": "4.16.3", + "@rollup/rollup-darwin-x64": "4.16.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.16.3", + "@rollup/rollup-linux-arm-musleabihf": "4.16.3", + "@rollup/rollup-linux-arm64-gnu": "4.16.3", + "@rollup/rollup-linux-arm64-musl": "4.16.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.16.3", + "@rollup/rollup-linux-riscv64-gnu": "4.16.3", + "@rollup/rollup-linux-s390x-gnu": "4.16.3", + "@rollup/rollup-linux-x64-gnu": "4.16.3", + "@rollup/rollup-linux-x64-musl": "4.16.3", + "@rollup/rollup-win32-arm64-msvc": "4.16.3", + "@rollup/rollup-win32-ia32-msvc": "4.16.3", + "@rollup/rollup-win32-x64-msvc": "4.16.3", "fsevents": "~2.3.2" } }, @@ -7814,6 +7817,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws4": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + }, "node_modules/axe-core": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz", @@ -7843,6 +7851,12 @@ "dequal": "^2.0.3" } }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -8035,6 +8049,42 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "devOptional": true }, + "node_modules/bare-events": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz", + "integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.2.3.tgz", + "integrity": "sha512-amG72llr9pstfXOBOHve1WjiuKKAMnebcmMbPWDZ7BCevAoJLpugjuAPRsDINEyjT0a6tbaVx3DctkXIRbLuJw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-path": "^2.0.0", + "streamx": "^2.13.0" + } + }, + "node_modules/bare-os": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.1.tgz", + "integrity": "sha512-OwPyHgBBMkhC29Hl3O4/YfxW9n7mdTr2+SsO29XBWKKJsbgj3mnorDB80r5TiCQgQstgE5ga1qNYrpes6NvX2w==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.1.tgz", + "integrity": "sha512-OHM+iwRDRMDBsSW7kl3dO62JyHdBKO3B25FB9vNQBPcGHMo4+eA8Yj41Lfbk3pS/seDY+siNge0LdRTulAau/A==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -9595,7 +9645,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "devOptional": true, "dependencies": { "ms": "2.1.2" }, @@ -11971,6 +12020,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, "node_modules/fast-glob": { "version": "3.2.11", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", @@ -13227,6 +13282,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "engines": { + "node": ">=14" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -17935,9 +17998,9 @@ "dev": true }, "node_modules/node-abi": { - "version": "3.60.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.60.0.tgz", - "integrity": "sha512-zcGgwoXbzw9NczqbGzAWL/ToDYAxv1V8gL1D67ClbdkIfeeDBbY0GelZtC25ayLvVjr2q2cloHeQV1R0QAWqRQ==", + "version": "3.61.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.61.0.tgz", + "integrity": "sha512-dYDO1rxzvMXjEMi37PBeFuYgwh3QZpsw/jt+qOmnRSwiV4z4c+OLoRlTa3V8ID4TrkSQpzCVc9OI2sstFaINfQ==", "optional": true, "dependencies": { "semver": "^7.3.5" @@ -21291,6 +21354,12 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -22563,6 +22632,11 @@ "loose-envify": "^1.1.0" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -23738,6 +23812,19 @@ "resolved": "https://registry.npmjs.org/stream-slice/-/stream-slice-0.1.2.tgz", "integrity": "sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA==" }, + "node_modules/streamx": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz", + "integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==", + "dev": true, + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -24655,30 +24742,26 @@ } }, "node_modules/testcontainers": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-9.12.0.tgz", - "integrity": "sha512-zmjLTAUqCiDvhDq7TCwcyhI3m/cXXKGnhyLLJ9pgh53VgG9O+P+opX1pIx28aYTUQ7Yu6b5sJf0xoIuxoiclWg==", + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.8.2.tgz", + "integrity": "sha512-9Ink7NUyYZwOjQhk0C6R6basWy2WADNly+md3D9YDap0pcDr3C+vrO8Ah1bkYco/9Zg8VoYTHO+blkLeebBYkA==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", - "@types/archiver": "^5.3.2", - "@types/dockerode": "^3.3.19", - "archiver": "^5.3.1", - "async-lock": "^1.4.0", + "@types/dockerode": "^3.3.24", + "archiver": "^5.3.2", + "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.4", - "docker-compose": "^0.24.1", + "docker-compose": "^0.24.6", "dockerode": "^3.3.5", "get-port": "^5.1.1", - "node-fetch": "^2.6.12", + "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", - "properties-reader": "^2.2.0", + "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^2.1.1", + "tar-fs": "^3.0.5", "tmp": "^0.2.1" - }, - "engines": { - "node": ">= 10.16" } }, "node_modules/testcontainers/node_modules/node-fetch": { @@ -24701,6 +24784,41 @@ } } }, + "node_modules/testcontainers/node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/testcontainers/node_modules/tar-fs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", + "dev": true, + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" + } + }, + "node_modules/testcontainers/node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/testcontainers/node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -26457,9 +26575,9 @@ } }, "node_modules/vite-node/node_modules/rollup": { - "version": "4.16.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.2.tgz", - "integrity": "sha512-sxDP0+pya/Yi5ZtptF4p3avI+uWCIf/OdrfdH2Gbv1kWddLKk0U7WE3PmQokhi5JrektxsK3sK8s4hzAmjqahw==", + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.3.tgz", + "integrity": "sha512-Ygm4fFO4usWcAG3Ud36Lmif5nudoi0X6QPLC+kRgrRjulAbmFkaTawP7fTIkRDnCNSf/4IAQzXM1T8e691kRtw==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -26472,22 +26590,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.16.2", - "@rollup/rollup-android-arm64": "4.16.2", - "@rollup/rollup-darwin-arm64": "4.16.2", - "@rollup/rollup-darwin-x64": "4.16.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.16.2", - "@rollup/rollup-linux-arm-musleabihf": "4.16.2", - "@rollup/rollup-linux-arm64-gnu": "4.16.2", - "@rollup/rollup-linux-arm64-musl": "4.16.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.16.2", - "@rollup/rollup-linux-riscv64-gnu": "4.16.2", - "@rollup/rollup-linux-s390x-gnu": "4.16.2", - "@rollup/rollup-linux-x64-gnu": "4.16.2", - "@rollup/rollup-linux-x64-musl": "4.16.2", - "@rollup/rollup-win32-arm64-msvc": "4.16.2", - "@rollup/rollup-win32-ia32-msvc": "4.16.2", - "@rollup/rollup-win32-x64-msvc": "4.16.2", + "@rollup/rollup-android-arm-eabi": "4.16.3", + "@rollup/rollup-android-arm64": "4.16.3", + "@rollup/rollup-darwin-arm64": "4.16.3", + "@rollup/rollup-darwin-x64": "4.16.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.16.3", + "@rollup/rollup-linux-arm-musleabihf": "4.16.3", + "@rollup/rollup-linux-arm64-gnu": "4.16.3", + "@rollup/rollup-linux-arm64-musl": "4.16.3", + "@rollup/rollup-linux-powerpc64le-gnu": "4.16.3", + "@rollup/rollup-linux-riscv64-gnu": "4.16.3", + "@rollup/rollup-linux-s390x-gnu": "4.16.3", + "@rollup/rollup-linux-x64-gnu": "4.16.3", + "@rollup/rollup-linux-x64-musl": "4.16.3", + "@rollup/rollup-win32-arm64-msvc": "4.16.3", + "@rollup/rollup-win32-ia32-msvc": "4.16.3", + "@rollup/rollup-win32-x64-msvc": "4.16.3", "fsevents": "~2.3.2" } }, @@ -27869,6 +27987,7 @@ "version": "0.0.1", "license": "AGPL-3.0-or-later", "dependencies": { + "@opensearch-project/opensearch": "^2.5.0", "@twake/matrix-application-server": "*", "@twake/matrix-identity-server": "*", "lodash": "^4.17.21", diff --git a/package.json b/package.json index 3de4ed81..befb7867 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "supertest": "^6.3.3", "swagger-jsdoc": "^6.2.8", "swagger-ui-dist": "^4.18.3", - "testcontainers": "^9.8.0", + "testcontainers": "^10.6.0", "toad-cache": "^3.3.0", "ts-jest": "^29.1.0", "typescript": "^4.9.5" diff --git a/packages/federation-server/src/__testData__/docker-compose.yml b/packages/federation-server/src/__testData__/docker-compose.yml index ddb51bf3..df75879e 100644 --- a/packages/federation-server/src/__testData__/docker-compose.yml +++ b/packages/federation-server/src/__testData__/docker-compose.yml @@ -3,6 +3,7 @@ version: '3.8' services: postgresql: image: postgres:13-bullseye + container_name: postgresql volumes: - ./synapse-data/matrix.example.com.log.config:/data/matrix.example.com.log.config - ./db/init-synapse-db.sh:/docker-entrypoint-initdb.d/init-synapse-db.sh @@ -23,6 +24,7 @@ services: synapse-federation: &synapse_template image: matrixdotorg/synapse:v1.89.0 + container_name: synapse-federation volumes: - ./synapse-data:/data - ./nginx/ssl/ca.pem:/etc/ssl/certs/ca.pem @@ -44,6 +46,7 @@ services: synapse-1: <<: *synapse_template + container_name: synapse-1 environment: - UID=${MYUID} - VIRTUAL_PORT=8008 @@ -52,6 +55,7 @@ services: synapse-2: <<: *synapse_template + container_name: synapse-2 environment: - UID=${MYUID} - VIRTUAL_PORT=8008 @@ -60,6 +64,7 @@ services: synapse-3: <<: *synapse_template + container_name: synapse-3 environment: - UID=${MYUID} - VIRTUAL_PORT=8008 @@ -93,6 +98,7 @@ services: federation-server: image: federation-server + container_name: federation-server build: context: ../../../.. dockerfile: ./packages/federation-server/Dockerfile @@ -117,6 +123,7 @@ services: identity-server-1: &identity-server-template image: identity-server + container_name: identity-server-1 build: context: ../../../.. dockerfile: ./packages/federation-server/src/__testData__/identity-server/Dockerfile @@ -139,6 +146,7 @@ services: identity-server-2: <<: *identity-server-template + container_name: identity-server-2 depends_on: annuaire: condition: service_started @@ -156,6 +164,7 @@ services: identity-server-3: <<: *identity-server-template + container_name: identity-server-3 depends_on: annuaire: condition: service_started @@ -173,6 +182,7 @@ services: nginx-proxy: image: nginxproxy/nginx-proxy + container_name: nginx-proxy ports: - 443:443 volumes: diff --git a/packages/federation-server/src/index.test.ts b/packages/federation-server/src/index.test.ts index d374d670..1e3bd8fd 100644 --- a/packages/federation-server/src/index.test.ts +++ b/packages/federation-server/src/index.test.ts @@ -1,5 +1,4 @@ import { Hash } from '@twake/crypto' -import dockerComposeV1, { v2 as dockerComposeV2 } from 'docker-compose' import express from 'express' import fs from 'fs' import type * as http from 'http' @@ -57,7 +56,6 @@ describe('Federation server', () => { }) describe('Integration tests', () => { - let containerNameSuffix: string let startedCompose: StartedDockerComposeEnvironment let identity1IPAddress: string let identity2IPAddress: string @@ -172,43 +170,18 @@ describe('Federation server', () => { syswideCas.addCAs( path.join(pathToTestDataFolder, 'nginx', 'ssl', 'ca.pem') ) - Promise.allSettled([dockerComposeV1.version(), dockerComposeV2.version()]) - // eslint-disable-next-line @typescript-eslint/promise-function-async - .then((results) => { - const promiseSucceededIndex = results.findIndex( - (res) => res.status === 'fulfilled' - ) - if (promiseSucceededIndex === -1) { - throw new Error('Docker compose is not installed') - } - containerNameSuffix = promiseSucceededIndex === 0 ? '_' : '-' - return new DockerComposeEnvironment( - path.join(pathToTestDataFolder), - 'docker-compose.yml' - ) - .withEnvironment({ MYUID: os.userInfo().uid.toString() }) - .withWaitStrategy( - `postgresql${containerNameSuffix}1`, - Wait.forHealthCheck() - ) - .withWaitStrategy( - `synapse-federation${containerNameSuffix}1`, - Wait.forHealthCheck() - ) - .withWaitStrategy( - `synapse-1${containerNameSuffix}1`, - Wait.forHealthCheck() - ) - .withWaitStrategy( - `synapse-2${containerNameSuffix}1`, - Wait.forHealthCheck() - ) - .withWaitStrategy( - `synapse-3${containerNameSuffix}1`, - Wait.forHealthCheck() - ) - .up() - }) + + new DockerComposeEnvironment( + path.join(pathToTestDataFolder), + 'docker-compose.yml' + ) + .withEnvironment({ MYUID: os.userInfo().uid.toString() }) + .withWaitStrategy('postgresql', Wait.forHealthCheck()) + .withWaitStrategy('synapse-federation', Wait.forHealthCheck()) + .withWaitStrategy('synapse-1', Wait.forHealthCheck()) + .withWaitStrategy('synapse-2', Wait.forHealthCheck()) + .withWaitStrategy('synapse-3', Wait.forHealthCheck()) + .up() // eslint-disable-next-line @typescript-eslint/promise-function-async .then((upResult) => { startedCompose = upResult @@ -275,10 +248,10 @@ describe('Federation server', () => { beforeAll((done) => { identity1IPAddress = startedCompose - .getContainer(`identity-server-1${containerNameSuffix}1`) + .getContainer(`identity-server-1`) .getIpAddress('test') identity2IPAddress = startedCompose - .getContainer(`identity-server-2${containerNameSuffix}1`) + .getContainer(`identity-server-2`) .getIpAddress('test') confOriginalContent = fs.readFileSync( @@ -295,9 +268,8 @@ describe('Federation server', () => { 'utf-8' ) - federationServerContainer = startedCompose.getContainer( - `federation-server${containerNameSuffix}1` - ) + federationServerContainer = + startedCompose.getContainer('federation-server') federationServerContainer .restart() @@ -746,25 +718,19 @@ describe('Federation server', () => { 'Certificates files for federation server has not been created' ) return Promise.all([ - startedCompose - .getContainer(`identity-server-1${containerNameSuffix}1`) - .restart(), - startedCompose - .getContainer(`identity-server-2${containerNameSuffix}1`) - .restart(), - startedCompose - .getContainer(`identity-server-3${containerNameSuffix}1`) - .restart() + startedCompose.getContainer('identity-server-1').restart(), + startedCompose.getContainer('identity-server-2').restart(), + startedCompose.getContainer('identity-server-3').restart() ]) }) // eslint-disable-next-line @typescript-eslint/promise-function-async .then(() => { identity1IPAddress = startedCompose - .getContainer(`identity-server-1${containerNameSuffix}1`) + .getContainer(`identity-server-1`) .getIpAddress('test') identity2IPAddress = startedCompose - .getContainer(`identity-server-2${containerNameSuffix}1`) + .getContainer(`identity-server-2`) .getIpAddress('test') const testConfig: Config = { @@ -776,16 +742,16 @@ describe('Federation server', () => { database_user: 'twake', database_password: 'twake!1', database_host: `${startedCompose - .getContainer(`postgresql${containerNameSuffix}1`) + .getContainer(`postgresql`) .getHost()}:5432`, database_name: 'federation', ldap_base: 'dc=example,dc=com', ldap_uri: `ldap://${startedCompose - .getContainer(`postgresql${containerNameSuffix}1`) + .getContainer(`postgresql`) .getHost()}:389`, matrix_database_engine: 'pg', matrix_database_host: `${startedCompose - .getContainer(`postgresql${containerNameSuffix}1`) + .getContainer(`postgresql`) .getHost()}:5432`, matrix_database_name: 'synapsefederation', matrix_database_user: 'synapse', diff --git a/packages/matrix-application-server/src/index.ts b/packages/matrix-application-server/src/index.ts index abb83ecb..0937011d 100644 --- a/packages/matrix-application-server/src/index.ts +++ b/packages/matrix-application-server/src/index.ts @@ -26,7 +26,10 @@ export { EHttpMethod } from './routes' export { AppServerAPIError, validationErrorHandler, - type expressAppHandler + type expressAppHandler, + errorMiddleware, + allowCors, + methodNotAllowed } from './utils' export declare interface AppService { diff --git a/packages/matrix-application-server/src/interfaces.ts b/packages/matrix-application-server/src/interfaces.ts index dc9fcea2..49926c3d 100644 --- a/packages/matrix-application-server/src/interfaces.ts +++ b/packages/matrix-application-server/src/interfaces.ts @@ -3,7 +3,7 @@ export interface TransactionRequestBody { } export interface ClientEvent { - content: Record + content: Record> event_id: string origin_server_ts: number room_id: string @@ -11,6 +11,7 @@ export interface ClientEvent { state_key?: string type: string unsigned?: UnsignedData + redacts?: string } interface UnsignedData { diff --git a/packages/matrix-identity-server/src/matrixDb/index.ts b/packages/matrix-identity-server/src/matrixDb/index.ts index dc8d4766..5122c2ce 100644 --- a/packages/matrix-identity-server/src/matrixDb/index.ts +++ b/packages/matrix-identity-server/src/matrixDb/index.ts @@ -9,6 +9,8 @@ type Collections = | 'room_stats_state' | 'local_media_repository' | 'room_aliases' + | 'room_stats_state' + | 'event_json' type Get = ( table: Collections, diff --git a/packages/tom-server/jest.config.js b/packages/tom-server/jest.config.js index 69342229..c0f2dfe7 100644 --- a/packages/tom-server/jest.config.js +++ b/packages/tom-server/jest.config.js @@ -2,7 +2,7 @@ import jestConfigBase from '../../jest-base.config.js' export default { ...jestConfigBase, - testTimeout: 120000, + testTimeout: 420000, setupFilesAfterEnv: ['/jest.setup.ts'], moduleNameMapper: { ...jestConfigBase.moduleNameMapper, diff --git a/packages/tom-server/package.json b/packages/tom-server/package.json index b4bdd36e..25923d91 100644 --- a/packages/tom-server/package.json +++ b/packages/tom-server/package.json @@ -40,6 +40,7 @@ "test": "jest" }, "dependencies": { + "@opensearch-project/opensearch": "^2.5.0", "@twake/matrix-application-server": "*", "@twake/matrix-identity-server": "*", "lodash": "^4.17.21", diff --git a/packages/tom-server/src/application-server/__testData__/config.json b/packages/tom-server/src/application-server/__testData__/config.json index ac6e4d47..9d78d7f7 100644 --- a/packages/tom-server/src/application-server/__testData__/config.json +++ b/packages/tom-server/src/application-server/__testData__/config.json @@ -15,7 +15,7 @@ "template_dir": "./templates", "ldap_base": "dc=example,dc=com", "ldap_uri": "ldap://localhost:21389/", - "matrix_server": "matrix.example.com", + "matrix_server": "matrix.example.com:444", "registration_file_path": "./src/application-server/__testData__/synapse-data/registration.yaml", "matrix_database_engine": "sqlite", "matrix_database_host": "./src/application-server/__testData__/synapse-data/homeserver.db", @@ -23,5 +23,6 @@ "aliases": [{ "exclusive": false, "regex": "#_twake_.*" }], "users": [{ "exclusive": false, "regex": "@.*" }] }, + "opensearch_is_activated": false, "push_ephemeral": true } diff --git a/packages/tom-server/src/application-server/__testData__/docker-compose.yml b/packages/tom-server/src/application-server/__testData__/docker-compose.yml index 2fc27899..378cd76a 100644 --- a/packages/tom-server/src/application-server/__testData__/docker-compose.yml +++ b/packages/tom-server/src/application-server/__testData__/docker-compose.yml @@ -3,6 +3,7 @@ version: '3.8' services: synapse: image: matrixdotorg/synapse:v1.89.0 + container_name: synapse-tom-1 volumes: - ./synapse-data:/data - ./nginx/ssl/auth.example.com.crt:/etc/ssl/certs/ca-certificates.crt @@ -37,7 +38,9 @@ services: nginx-proxy: image: nginxproxy/nginx-proxy ports: - - 443:443 + - 444:444 + environment: + - HTTPS_PORT=444 volumes: - /var/run/docker.sock:/tmp/docker.sock:ro - ./nginx/ssl:/etc/nginx/certs \ No newline at end of file diff --git a/packages/tom-server/src/application-server/__testData__/ldap/ldif/base_ldap_users.ldif b/packages/tom-server/src/application-server/__testData__/ldap/ldif/base_ldap_users.ldif index 04025bd6..eb4e7f3f 100644 --- a/packages/tom-server/src/application-server/__testData__/ldap/ldif/base_ldap_users.ldif +++ b/packages/tom-server/src/application-server/__testData__/ldap/ldif/base_ldap_users.ldif @@ -135,7 +135,7 @@ userPassword: jfett dn: uid=lskywalker,ou=users,dc=example,dc=com objectClass: inetOrgPerson uid: lskywalker -cn: Luc Skywalker +cn: Luke Skywalker sn: Lskywalker mail: lskywalker@example.com userPassword: lskywalker diff --git a/packages/tom-server/src/application-server/__testData__/llng/lmConf-1.json b/packages/tom-server/src/application-server/__testData__/llng/lmConf-1.json index 1aa95886..3248f5ac 100644 --- a/packages/tom-server/src/application-server/__testData__/llng/lmConf-1.json +++ b/packages/tom-server/src/application-server/__testData__/llng/lmConf-1.json @@ -250,7 +250,7 @@ "oidcRPMetaDataOptionsLogoutSessionRequired": 1, "oidcRPMetaDataOptionsLogoutType": "back", "oidcRPMetaDataOptionsPublic": 0, - "oidcRPMetaDataOptionsRedirectUris": "https://matrix.example.com/_synapse/client/oidc/callback", + "oidcRPMetaDataOptionsRedirectUris": "https://matrix.example.com:444/_synapse/client/oidc/callback", "oidcRPMetaDataOptionsRefreshToken": 0, "oidcRPMetaDataOptionsRequirePKCE": 0 } diff --git a/packages/tom-server/src/application-server/__testData__/synapse-data/homeserver.yaml b/packages/tom-server/src/application-server/__testData__/synapse-data/homeserver.yaml index fb19c796..7e180d96 100644 --- a/packages/tom-server/src/application-server/__testData__/synapse-data/homeserver.yaml +++ b/packages/tom-server/src/application-server/__testData__/synapse-data/homeserver.yaml @@ -10,7 +10,7 @@ # each option, go to docs/usage/configuration/config_documentation.md or # https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html server_name: "example.com" -public_baseurl: "https://matrix.example.com/" +public_baseurl: "https://matrix.example.com:444/" pid_file: /data/homeserver.pid listeners: - port: 8008 diff --git a/packages/tom-server/src/application-server/index.test.ts b/packages/tom-server/src/application-server/index.test.ts index 07c204a7..99a78fb8 100644 --- a/packages/tom-server/src/application-server/index.test.ts +++ b/packages/tom-server/src/application-server/index.test.ts @@ -1,7 +1,6 @@ import { type TwakeLogger } from '@twake/logger' import { type AppServiceOutput } from '@twake/matrix-application-server/src/utils' import { type DbGetResult } from '@twake/matrix-identity-server' -import dockerComposeV1, { v2 as dockerComposeV2 } from 'docker-compose' import express from 'express' import fs from 'fs' import type * as http from 'http' @@ -81,7 +80,10 @@ describe('ApplicationServer', () => { redirect: 'manual' } ) - let location = response.headers.get('location') as string + let location = (response.headers.get('location') as string).replace( + 'auth.example.com', + 'auth.example.com:444' + ) const matrixCookies = response.headers.get('set-cookie') response = await fetch.default(location) body = await response.text() @@ -215,7 +217,6 @@ describe('ApplicationServer', () => { let appServiceToken: string let newRoomId: string let rSkywalkerMatrixToken: string - let containerNameSuffix: string beforeAll((done) => { syswideCas.addCAs( @@ -238,30 +239,13 @@ describe('ApplicationServer', () => { ).as_token deleteUserDB(testConfig) // eslint-disable-next-line @typescript-eslint/promise-function-async - .then((_) => - Promise.allSettled([ - dockerComposeV1.version(), - dockerComposeV2.version() - ]) - ) - // eslint-disable-next-line @typescript-eslint/promise-function-async - .then((results) => { - const promiseSucceededIndex = results.findIndex( - (res) => res.status === 'fulfilled' - ) - if (promiseSucceededIndex === -1) { - throw new Error('Docker compose is not installed') - } - containerNameSuffix = promiseSucceededIndex === 0 ? '_' : '-' + .then((_) => { return new DockerComposeEnvironment( path.join(pathToTestDataFolder), 'docker-compose.yml' ) .withEnvironment({ MYUID: os.userInfo().uid.toString() }) - .withWaitStrategy( - `synapse${containerNameSuffix}1`, - Wait.forHealthCheck() - ) + .withWaitStrategy('synapse-tom-1', Wait.forHealthCheck()) .up() }) // eslint-disable-next-line @typescript-eslint/promise-function-async diff --git a/packages/tom-server/src/application-server/index.ts b/packages/tom-server/src/application-server/index.ts index 14ef984b..584e9167 100644 --- a/packages/tom-server/src/application-server/index.ts +++ b/packages/tom-server/src/application-server/index.ts @@ -27,11 +27,7 @@ export default class TwakeApplicationServer extendRoutes(this, parent) this.on('ephemeral_type: m.presence', (event: ClientEvent) => { - if ( - event.type === 'm.presence' && - 'presence' in event.content && - event.content.presence === 'online' - ) { + if (event.content.presence === 'online') { const matrixUserId = event.sender let ldapUid: string | null = null if (matrixUserId != null) { @@ -114,11 +110,7 @@ export default class TwakeApplicationServer }) this.on('state event | type: m.room.member', (event: ClientEvent) => { - if ( - event.type === 'm.room.member' && - 'membership' in event.content && - event.content.membership === 'leave' - ) { + if (event.content.membership === 'leave') { const matrixUserId = event.sender const targetUserId = event.state_key if ( diff --git a/packages/tom-server/src/config.json b/packages/tom-server/src/config.json index b687fd75..f2074d8c 100644 --- a/packages/tom-server/src/config.json +++ b/packages/tom-server/src/config.json @@ -30,9 +30,11 @@ "ldapjs_opts": {}, "logging": { "log_level": "info", - "log_transports": [{ - "type":"Console" - }], + "log_transports": [ + { + "type": "Console" + } + ], "silent": false, "exit_on_error": false, "default_meta": null, @@ -48,6 +50,16 @@ "matrix_database_ssl": false, "matrix_database_user": null, "oidc_issuer": "", + "opensearch_ca_cert_path": "", + "opensearch_host": "localhost", + "opensearch_is_activated": true, + "opensearch_max_retries": 7, + "opensearch_number_of_shards": 5, + "opensearch_number_of_replicas": 1, + "opensearch_password": "admin", + "opensearch_ssl": false, + "opensearch_user": "admin", + "opensearch_wait_for_active_shards": "1", "pepperCron": "0 0 * * *", "policies": null, "rate_limiting_window": 600000, @@ -78,4 +90,4 @@ "sms_api_login": "", "sms_api_key": "", "trust_x_forwarded_for": false -} \ No newline at end of file +} diff --git a/packages/tom-server/src/identity-server/__testData__/registerConf.json b/packages/tom-server/src/identity-server/__testData__/registerConf.json index 00b91d79..e48a5c77 100644 --- a/packages/tom-server/src/identity-server/__testData__/registerConf.json +++ b/packages/tom-server/src/identity-server/__testData__/registerConf.json @@ -16,6 +16,7 @@ "keys_depth": 5, "mail_link_delay": 7200, "matrix_server": "localhost", + "opensearch_is_activated": false, "server_name": "example.com", "smtp_sender": "yadd@debian.org", "smtp_server": "localhost", diff --git a/packages/tom-server/src/identity-server/__testData__/termsConf.json b/packages/tom-server/src/identity-server/__testData__/termsConf.json index 04afd6a7..7bab7bb2 100644 --- a/packages/tom-server/src/identity-server/__testData__/termsConf.json +++ b/packages/tom-server/src/identity-server/__testData__/termsConf.json @@ -15,6 +15,7 @@ "key_delay": 3600, "keys_depth": 5, "mail_link_delay": 7200, + "opensearch_is_activated": false, "server_name": "example.com", "smtp_sender": "yadd@debian.org", "smtp_server": "localhost", diff --git a/packages/tom-server/src/index.ts b/packages/tom-server/src/index.ts index ee9dd5b5..b4cbe0cf 100644 --- a/packages/tom-server/src/index.ts +++ b/packages/tom-server/src/index.ts @@ -14,6 +14,8 @@ import IdServer from './identity-server' import mutualRoomsAPIRouter from './mutual-rooms-api' import privateNoteApiRouter from './private-note-api' import roomTagsAPIRouter from './room-tags-api' +import TwakeSearchEngine from './search-engine-api' +import { type IOpenSearchRepository } from './search-engine-api/repositories/interfaces/opensearch-repository.interface' import smsApiRouter from './sms-api' import type { Config, ConfigurationFile, TwakeIdentityServer } from './types' import userInfoAPIRouter from './user-info-api' @@ -26,6 +28,7 @@ export default class TwakeServer { endpoints: Router db?: TwakeDB matrixDb: MatrixDB + private _openSearchClient: IOpenSearchRepository | undefined ready!: Promise idServer!: TwakeIdentityServer @@ -72,6 +75,9 @@ export default class TwakeServer { cleanJobs(): void { this.idServer.cleanJobs() this.matrixDb.close() + if (this._openSearchClient != null) { + this._openSearchClient.close() + } } private _getConfigurationFile( @@ -96,71 +102,78 @@ export default class TwakeServer { } private async _initServer(confDesc?: ConfigDescription): Promise { - try { - await this.idServer.ready - await this.matrixDb.ready - await initializeDb(this) + await this.idServer.ready + await this.matrixDb.ready + await initializeDb(this) - const vaultServer = new VaultServer( - this.idServer.db, - this.idServer.authenticate - ) - const wellKnown = new WellKnown(this.conf) - const privateNoteApi = privateNoteApiRouter( - this.idServer.db, - this.conf, - this.idServer.authenticate, - this.logger - ) - const mutualRoolsApi = mutualRoomsAPIRouter( - this.conf, - this.matrixDb.db, - this.idServer.authenticate, - this.logger - ) - const roomTagsApi = roomTagsAPIRouter( + const vaultServer = new VaultServer( + this.idServer.db, + this.idServer.authenticate + ) + const wellKnown = new WellKnown(this.conf) + const privateNoteApi = privateNoteApiRouter( + this.idServer.db, + this.conf, + this.idServer.authenticate, + this.logger + ) + const mutualRoolsApi = mutualRoomsAPIRouter( + this.conf, + this.matrixDb.db, + this.idServer.authenticate, + this.logger + ) + const roomTagsApi = roomTagsAPIRouter( + this.idServer.db, + this.matrixDb.db, + this.conf, + this.idServer.authenticate, + this.logger + ) + const userInfoApi = userInfoAPIRouter(this.idServer, this.conf, this.logger) + + const smsApi = smsApiRouter( + this.conf, + this.idServer.authenticate, + this.logger + ) + + this.endpoints.use(privateNoteApi) + this.endpoints.use(mutualRoolsApi) + this.endpoints.use(vaultServer.endpoints) + this.endpoints.use(roomTagsApi) + this.endpoints.use(userInfoApi) + this.endpoints.use(smsApi) + + if ( + this.conf.opensearch_is_activated != null && + this.conf.opensearch_is_activated + ) { + const searchEngineApi = new TwakeSearchEngine( this.idServer.db, - this.matrixDb.db, - this.conf, + this.idServer.userDB, this.idServer.authenticate, - this.logger - ) - const userInfoApi = userInfoAPIRouter( - this.idServer, + this.matrixDb, this.conf, - this.logger + this.logger, + confDesc ) + await searchEngineApi.ready + this._openSearchClient = searchEngineApi.openSearchRepository + this.endpoints.use(searchEngineApi.router.routes) + } - const smsApi = smsApiRouter( - this.conf, - this.idServer.authenticate, - this.logger - ) - - this.endpoints.use(privateNoteApi) - this.endpoints.use(mutualRoolsApi) - this.endpoints.use(vaultServer.endpoints) - this.endpoints.use(roomTagsApi) - this.endpoints.use(userInfoApi) - this.endpoints.use(smsApi) - - Object.keys(this.idServer.api.get).forEach((k) => { - this.endpoints.get(k, this.idServer.api.get[k]) - }) - Object.keys(this.idServer.api.post).forEach((k) => { - this.endpoints.post(k, this.idServer.api.post[k]) - }) - this.endpoints.use(vaultServer.endpoints) - Object.keys(wellKnown.api.get).forEach((k) => { - this.endpoints.get(k, wellKnown.api.get[k]) - }) + Object.keys(this.idServer.api.get).forEach((k) => { + this.endpoints.get(k, this.idServer.api.get[k]) + }) + Object.keys(this.idServer.api.post).forEach((k) => { + this.endpoints.post(k, this.idServer.api.post[k]) + }) + this.endpoints.use(vaultServer.endpoints) + Object.keys(wellKnown.api.get).forEach((k) => { + this.endpoints.get(k, wellKnown.api.get[k]) + }) - return true - } catch (error) { - /* istanbul ignore next */ - this.logger.error(`Unable to initialize server`, { error }) - /* istanbul ignore next */ - throw Error('Unable to initialize server', { cause: error }) - } + return true } } diff --git a/packages/tom-server/src/search-engine-api/__testData__/config.json b/packages/tom-server/src/search-engine-api/__testData__/config.json new file mode 100644 index 00000000..1fdee6f8 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/config.json @@ -0,0 +1,26 @@ +{ + "additional_features": true, + "base_url": "http://localhost:3000/", + "database_engine": "pg", + "database_host": "localhost:5433", + "database_name": "identity", + "database_user": "twake", + "database_password": "twake!1", + "ldap_base": "dc=example,dc=com", + "ldap_filter": "(ObjectClass=inetOrgPerson)", + "ldap_uri": "ldap://localhost:21390/", + "matrix_database_engine": "pg", + "matrix_server": "matrix.example.com:445", + "matrix_database_host": "localhost:5433", + "matrix_database_name": "synapse", + "matrix_database_password": "synapse!1", + "matrix_database_user": "synapse", + "opensearch_ca_cert_path": "./src/search-engine-api/__testData__/nginx/ssl/ca.pem", + "opensearch_host": "opensearch.example.com:445", + "opensearch_password": "admin", + "opensearch_ssl": true, + "opensearch_user": "admin", + "registration_file_path": "./src/search-engine-api/__testData__/synapse-data/registration.yaml", + "server_name": "example.com", + "userdb_engine": "ldap" +} \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/__testData__/db/init-id-db.sh b/packages/tom-server/src/search-engine-api/__testData__/db/init-id-db.sh new file mode 100644 index 00000000..b3ff0da5 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/db/init-id-db.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +psql -U postgres <<-EOSQL + CREATE USER twake PASSWORD 'twake!1'; + CREATE DATABASE identity TEMPLATE='template0' LOCALE='C' ENCODING='UTF8' OWNER='twake'; +EOSQL \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/__testData__/db/init-llng-db.sh b/packages/tom-server/src/search-engine-api/__testData__/db/init-llng-db.sh new file mode 100644 index 00000000..80d7a094 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/db/init-llng-db.sh @@ -0,0 +1,95 @@ +#!/bin/sh +set -e + +DATABASE=${PG_DATABASE:-lemonldapng} +USER=${PG_USER:-lemonldap} +PASSWORD=${PG_PASSWORD:-lemonldap} +TABLE=${PG_TABLE:-lmConfig} +PTABLE=${PG_PERSISTENT_SESSIONS_TABLE:-psessions} +STABLE=${PG_SESSIONS_TABLE:-sessions} +SAMLTABLE=${PG_SAML_TABLE:-samlsessions} +OIDCTABLE=${PG_OIDC_TABLE:-oidcsessions} +CASTABLE=${PG_CAS_TABLE:-cassessions} + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE USER $USER PASSWORD '$PASSWORD'; + CREATE DATABASE $DATABASE; +EOSQL +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$DATABASE" <<-EOSQL + CREATE TABLE $TABLE ( + cfgNum integer not null primary key, + data text + ); + GRANT ALL PRIVILEGES ON TABLE $TABLE TO $USER; + + CREATE TABLE $PTABLE ( + id varchar(64) not null primary key, + a_session jsonb + ); + CREATE INDEX i_p__session_kind ON psessions ((a_session ->> '_session_kind')); + CREATE INDEX i_p__httpSessionType ON psessions ((a_session ->> '_httpSessionType')); + CREATE INDEX i_p__session_uid ON psessions ((a_session ->> '_session_uid')); + CREATE INDEX i_p_ipAddr ON psessions ((a_session ->> 'ipAddr')); + CREATE INDEX i_p__whatToTrace ON psessions ((a_session ->> '_whatToTrace')); + GRANT ALL PRIVILEGES ON TABLE $PTABLE TO $USER; + + CREATE UNLOGGED TABLE $STABLE ( + id varchar(64) not null primary key, + a_session jsonb + ); + CREATE INDEX i_s__whatToTrace ON sessions ((a_session ->> '_whatToTrace')); + CREATE INDEX i_s__session_kind ON sessions ((a_session ->> '_session_kind')); + CREATE INDEX i_s__utime ON sessions ((cast (a_session ->> '_utime' as bigint))); + CREATE INDEX i_s_ipAddr ON sessions ((a_session ->> 'ipAddr')); + CREATE INDEX i_s__httpSessionType ON sessions ((a_session ->> '_httpSessionType')); + CREATE INDEX i_s_user ON sessions ((a_session ->> 'user')); + GRANT ALL PRIVILEGES ON TABLE $STABLE TO $USER; + + CREATE UNLOGGED TABLE $SAMLTABLE ( + id varchar(64) not null primary key, + a_session jsonb + ); + CREATE INDEX i_a__session_kind ON $SAMLTABLE ((a_session ->> '_session_kind')); + CREATE INDEX i_a__utime ON $SAMLTABLE ((cast(a_session ->> '_utime' as bigint))); + CREATE INDEX i_a_ProxyID ON $SAMLTABLE ((a_session ->> 'ProxyID')); + CREATE INDEX i_a__nameID ON $SAMLTABLE ((a_session ->> '_nameID')); + CREATE INDEX i_a__assert_id ON $SAMLTABLE ((a_session ->> '_assert_id')); + CREATE INDEX i_a__art_id ON $SAMLTABLE ((a_session ->> '_art_id')); + CREATE INDEX i_a__saml_id ON $SAMLTABLE ((a_session ->> '_saml_id')); + GRANT ALL PRIVILEGES ON TABLE $SAMLTABLE TO $USER; + + CREATE UNLOGGED TABLE $OIDCTABLE ( + id varchar(64) not null primary key, + a_session jsonb + ); + CREATE INDEX i_o__session_kind ON $OIDCTABLE ((a_session ->> '_session_kind')); + CREATE INDEX i_o__utime ON $OIDCTABLE ((cast(a_session ->> '_utime' as bigint ))); + GRANT ALL PRIVILEGES ON TABLE $OIDCTABLE TO $USER; + + CREATE UNLOGGED TABLE $CASTABLE ( + id varchar(64) not null primary key, + a_session jsonb + ); + CREATE INDEX i_c__session_kind ON $CASTABLE ((a_session ->> '_session_kind')); + CREATE INDEX i_c__utime ON $CASTABLE ((cast(a_session ->> '_utime' as bigint))); + CREATE INDEX i_c__cas_id ON $CASTABLE ((a_session ->> '_cas_id')); + CREATE INDEX i_c_pgtIou ON $CASTABLE ((a_session ->> 'pgtIou')); + GRANT ALL PRIVILEGES ON TABLE $CASTABLE TO $USER; +EOSQL + +if test -e /llng-conf/conf.json; then + SERIALIZED=`perl -MJSON -e '$/=undef; + open F, "/llng-conf/conf.json" or die $!; + $a=JSON::from_json(); + $a->{cfgNum}=1; + $a=JSON::to_json($a); + $a=~s/'\''/'\'\''/g; + $a =~ s/\\\\/\\\\\\\\/g; + print $a;'` + echo "set val '$SERIALIZED'" >&2 + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$DATABASE" <<-EOSQL + \\set val '$SERIALIZED' + INSERT INTO $TABLE (cfgNum, data) VALUES (1, :'val'); + \\unset val +EOSQL +fi diff --git a/packages/tom-server/src/search-engine-api/__testData__/db/init-synapse-db.sh b/packages/tom-server/src/search-engine-api/__testData__/db/init-synapse-db.sh new file mode 100644 index 00000000..2a6f1859 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/db/init-synapse-db.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +psql -U postgres <<-EOSQL + CREATE USER synapse PASSWORD 'synapse!1'; + CREATE DATABASE synapse TEMPLATE='template0' LOCALE='C' ENCODING='UTF8' OWNER='synapse'; +EOSQL \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/__testData__/docker-compose.yml b/packages/tom-server/src/search-engine-api/__testData__/docker-compose.yml new file mode 100644 index 00000000..0aee22da --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/docker-compose.yml @@ -0,0 +1,100 @@ +version: '3.8' + +networks: + twake_chat: + +services: + postgresql: + image: postgres:13-bullseye + container_name: postgresql-tom + volumes: + - ./synapse-data/matrix.example.com.log.config:/data/matrix.example.com.log.config + - ./db/init-synapse-db.sh:/docker-entrypoint-initdb.d/init-synapse-db.sh + - ./db/init-llng-db.sh:/docker-entrypoint-initdb.d/init-llng-db.sh + - ./db/init-id-db.sh:/docker-entrypoint-initdb.d/init-id-db.sh + - ./llng/lmConf-1.json:/llng-conf/conf.json + environment: + - POSTGRES_PASSWORD=synapse!! + healthcheck: + test: ['CMD-SHELL', 'pg_isready'] + interval: 10s + timeout: 5s + retries: 5 + ports: + - 5433:5432 + networks: + - twake_chat + + synapse: + image: matrixdotorg/synapse:v1.89.0 + container_name: synapse-tom + volumes: + - ./synapse-data:/data + - ./nginx/ssl/ca.pem:/etc/ssl/certs/ca.pem + - ./nginx/ssl/9da13359.0:/etc/ssl/certs/9da13359.0 + depends_on: + - auth + environment: + - UID=${MYUID} + - VIRTUAL_PORT=8008 + - VIRTUAL_HOST=matrix.example.com + healthcheck: + test: ["CMD", "curl", "-fSs", "http://localhost:8008/health"] + interval: 10s + timeout: 10s + retries: 3 + networks: + - twake_chat + extra_hosts: + - "host.docker.internal:host-gateway" + + auth: + image: yadd/lemonldap-ng-portal:2.16.1-bullseye + volumes: + - ./llng/lmConf-1.json:/var/lib/lemonldap-ng/conf/lmConf-1.json + - ./llng/ssl.conf:/etc/nginx/sites-enabled/0000default.conf + - ./nginx/ssl/auth.example.com.crt:/etc/nginx/ssl/auth.example.com.crt + - ./nginx/ssl/auth.example.com.key:/etc/nginx/ssl/auth.example.com.key + environment: + - PORTAL=https://auth.example.com + - VIRTUAL_HOST=auth.example.com + - PG_SERVER=postgresql + depends_on: + postgresql: + condition: service_healthy + networks: + - twake_chat + + annuaire: + image: ldap + build: ./ldap + ports: + - 21390:389 + networks: + - twake_chat + + # opensearchdashboard: + # image: opensearchproject/opensearch-dashboards + # ports: + # - 5601:5601 + # expose: + # - "5601" + # environment: + # - OPENSEARCH_HOSTS=http://opensearch:9200 + # - DISABLE_SECURITY_DASHBOARDS_PLUGIN=true + # networks: + # - twake_chat + + nginx-proxy: + image: nginxproxy/nginx-proxy + container_name: nginx-proxy-tom + ports: + - 445:443 + volumes: + - /var/run/docker.sock:/tmp/docker.sock:ro + - ./nginx/ssl:/etc/nginx/certs + networks: + twake_chat: + aliases: + - matrix.example.com + - auth.example.com diff --git a/packages/tom-server/src/search-engine-api/__testData__/generate-self-signed-certificate.sh b/packages/tom-server/src/search-engine-api/__testData__/generate-self-signed-certificate.sh new file mode 100755 index 00000000..ea5f1a5c --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/generate-self-signed-certificate.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +# tu use this cript, execute this file with domain as first argument or "-ip" as first argument and host IP as second parameter if you want to set subjectAltName property +SCRIPT_PARENT_PATH=$( cd "$(dirname "$0")" ; pwd -P ) +ADDITIONAL_PARAMS="" +COMMON_NAME=$1 +if [ "$1" = "-ip" ]; then + COMMON_NAME=$2 + echo "subjectAltName = IP:$COMMON_NAME" > $SCRIPT_PARENT_PATH/openssl-ext.cnf + ADDITIONAL_PARAMS="-extfile $SCRIPT_PARENT_PATH/openssl-ext.cnf" +fi + +CERTIFICATE_KEY=$COMMON_NAME.key +CERTIFICATE_CRT=$COMMON_NAME.crt +CA_CRT_PATH=$SCRIPT_PARENT_PATH/nginx/ssl/ca.pem +CA_KEY_PATH=$SCRIPT_PARENT_PATH/nginx/ssl/ca.key + +cd $SCRIPT_PARENT_PATH +openssl genrsa -out $CERTIFICATE_KEY 4096 +openssl req \ + -new \ + -key $CERTIFICATE_KEY \ + -nodes \ + -out server.csr \ + -subj "/C=FR/ST=Centre/L=Paris/O=Linagora/OU=IT/CN=$COMMON_NAME" +if [ ! -f "$CA_CRT_PATH" ]; then + openssl genrsa -out ca.key 4096 + openssl req \ + -new \ + -x509 \ + -nodes \ + -days 36500 \ + -key ca.key \ + -out ca.pem \ + -subj "/C=AU/ST=Some-State/O=Internet Widgits Pty Ltd" + mv ca.pem ca.key $SCRIPT_PARENT_PATH/nginx/ssl +fi +openssl x509 \ + -req \ + -in server.csr \ + -CAkey $CA_KEY_PATH \ + -CA $CA_CRT_PATH \ + -set_serial -01 \ + -out $CERTIFICATE_CRT \ + -days 36500 \ + -sha256 $ADDITIONAL_PARAMS +openssl verify -CAfile $CA_CRT_PATH $CERTIFICATE_CRT +mv $CERTIFICATE_KEY $CERTIFICATE_CRT $SCRIPT_PARENT_PATH/nginx/ssl +rm server.csr +if [ -f "openssl-ext.cnf" ]; then + rm openssl-ext.cnf +fi \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/__testData__/images/anakin-at-the-office.jpg b/packages/tom-server/src/search-engine-api/__testData__/images/anakin-at-the-office.jpg new file mode 100644 index 00000000..f3b29041 Binary files /dev/null and b/packages/tom-server/src/search-engine-api/__testData__/images/anakin-at-the-office.jpg differ diff --git a/packages/tom-server/src/search-engine-api/__testData__/ldap/Dockerfile b/packages/tom-server/src/search-engine-api/__testData__/ldap/Dockerfile new file mode 100644 index 00000000..83adb618 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/ldap/Dockerfile @@ -0,0 +1,44 @@ +FROM debian:bullseye-slim +LABEL maintainer Linagora + +ENV DEBIAN_FRONTEND=noninteractive + +# Update system and install dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + apt-transport-https \ + ca-certificates \ + curl \ + gpg \ + wget && \ + curl https://ltb-project.org/documentation/_static/RPM-GPG-KEY-LTB-project | gpg --dearmor > /usr/share/keyrings/ltb-project-openldap-archive-keyring.gpg && \ + echo "deb [arch=amd64 signed-by=/usr/share/keyrings/ltb-project-openldap-archive-keyring.gpg] https://ltb-project.org/debian/openldap25/bullseye bullseye main" > /etc/apt/sources.list.d/ltb-project.list && \ + apt-get update && \ + apt-get install -y openldap-ltb openldap-ltb-contrib-overlays openldap-ltb-mdb-utils ldap-utils && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Copy configuration files +COPY ./ldif/config-20230322180123.ldif /var/backups/openldap/ +COPY ./ldif/base_ldap_users.ldif /tmp + +# Configure LDAP +RUN rm -rf /usr/local/openldap/var/lib/ldap /usr/local/openldap/etc/openldap/slapd.d && \ + mkdir -p /usr/local/openldap/var/lib/ldap && \ + chown -R ldap:ldap /usr/local/openldap/var/lib/ldap && \ + mkdir -p /usr/local/openldap/etc/openldap/slapd.d && \ + chown -R ldap:ldap /usr/local/openldap/etc/openldap/slapd.d && \ + usr/local/openldap/sbin/slapd-cli restoreconfig -b /var/backups/openldap/config-20230322180123.ldif && \ + mkdir -p /usr/local/openldap/var/lib/ldap/data && \ + chown -R ldap:ldap /usr/local/openldap/var/lib/ldap/data && \ + /usr/local/openldap/sbin/slapadd -F /usr/local/openldap/etc/openldap/slapd.d/ -b "dc=example,dc=com" -l /tmp/base_ldap_users.ldif + +# Expose LDAP port +EXPOSE 389 + +# Define LDAP data volume +VOLUME /usr/local/openldap/var/openldap-data + +# Set the entrypoint script +CMD ["/usr/local/openldap/libexec/slapd", "-h", "ldap://*", "-u", "ldap", "-g", "ldap", "-d", "256"] + diff --git a/packages/tom-server/src/search-engine-api/__testData__/ldap/ldif/base_ldap_users.ldif b/packages/tom-server/src/search-engine-api/__testData__/ldap/ldif/base_ldap_users.ldif new file mode 100644 index 00000000..eb4e7f3f --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/ldap/ldif/base_ldap_users.ldif @@ -0,0 +1,174 @@ +dn: dc=example,dc=com +objectClass: top +objectClass: organization +objectClass: dcObject +dc: example +o: Example + +dn: ou=users,dc=example,dc=com +objectClass: top +objectClass: organizationalUnit +ou: users + +dn: uid=dwho,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: dwho +cn: Dr Who +sn: Dwho +mail: dwho@example.com +mobile: 33671298765 +userPassword: dwho + +dn: uid=rtyler,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: rtyler +cn: Rose Tyler +sn: Rtyler +mail: rtyler@example.com +mobile: 33671298767 +userPassword: rtyler + +dn: uid=msmith,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: msmith +cn: Mr Smith +sn: Msmith +mail: msmith@example.com +userPassword: msmith + +dn: uid=okenobi,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: okenobi +cn: Obi-Wan Kenobi +sn: Okenobi +mail: okenobi@example.com +userPassword: okenobi + +dn: uid=qjinn,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: qjinn +cn: Qui-Gon Jinn +sn: Qgonjinn +mail: qjinn@example.com +userPassword: qjinn + +dn: uid=chewbacca,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: chewbacca +cn: Chewbacca +sn: Chewbacca +mail: chewbacca@example.com +userPassword: chewbacca + +dn: uid=lorgana,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: lorgana +cn: Leia Organa +sn: Lorgana +mail: lorgana@example.com +userPassword: lorgana + +dn: uid=pamidala,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: pamidala +cn: Padme Amidala +sn: Pamidala +mail: pamidala@example.com +userPassword: pamidala + +dn: uid=cdooku,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: cdooku +cn: Comte Dooku +sn: Cdooku +mail: cdooku@example.com +userPassword: cdooku + +dn: uid=kren,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: kren +cn: Kylo Ren +sn: Kren +mail: kren@example.com +userPassword: kren + +dn: uid=dmaul,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: dmaul +cn: Dark Maul +sn: Dmaul +mail: dmaul@example.com +userPassword: dmaul + +dn: uid=askywalker,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: askywalker +cn: Anakin Skywalker +sn: Askywalker +mail: askywalker@example.com +userPassword: askywalker + +dn: uid=jbinks,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: jbinks +cn: Jar Jar Binks +sn: Jbinks +mail: jbinks@example.com +userPassword: jbinks + +dn: uid=bfett,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: bfett +cn: Boba Fett +sn: Bfett +mail: bfett@example.com +userPassword: bfett + +dn: uid=jfett,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: jfett +cn: Jango Ffett +sn: Jfett +mail: jfett@example.com +userPassword: jfett + +dn: uid=lskywalker,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: lskywalker +cn: Luke Skywalker +sn: Lskywalker +mail: lskywalker@example.com +userPassword: lskywalker + +dn: uid=myoda,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: myoda +cn: Master Yoda +sn: Myoda +mail: myoda@example.com +userPassword: myoda + +dn: uid=hsolo,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: hsolo +cn: Han Solo +sn: Hsolo +mail: hsolo@example.com +userPassword: hsolo + +dn: uid=r2d2,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: r2d2 +cn: R2D2 +sn: R2D2 +mail: r2d2@example.com +userPassword: r2d2 + +dn: uid=c3po,ou=users,dc=example,dc=com +objectClass: inetOrgPerson +uid: c3po +cn: C3PO +sn: C3po +mail: c3po@example.com +userPassword: c3po + diff --git a/packages/tom-server/src/search-engine-api/__testData__/ldap/ldif/config-20230322180123.ldif b/packages/tom-server/src/search-engine-api/__testData__/ldap/ldif/config-20230322180123.ldif new file mode 100644 index 00000000..5c566113 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/ldap/ldif/config-20230322180123.ldif @@ -0,0 +1,260 @@ +dn: cn=config +objectClass: olcGlobal +cn: config +olcPidFile: /usr/local/openldap/var/run/slapd.pid + +dn: cn=schema,cn=config +objectClass: olcSchemaConfig +cn: schema +structuralObjectClass: olcSchemaConfig +entryUUID: 713c4cc8-df8a-103c-9770-230a0a7123cd +creatorsName: cn=config +createTimestamp: 20221013213337Z +entryCSN: 20221013213337.589745Z#000000#000#000000 +modifiersName: cn=config +modifyTimestamp: 20221013213337Z + +dn: cn={0}core,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: {0}core +olcAttributeTypes: {0}( 2.5.4.2 NAME 'knowledgeInformation' DESC 'RFC2256: knowledge information' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} ) +olcAttributeTypes: {1}( 2.5.4.4 NAME ( 'sn' 'surname' ) DESC 'RFC2256: last (family) name(s) for which the entity is known by' SUP name ) +olcAttributeTypes: {2}( 2.5.4.5 NAME 'serialNumber' DESC 'RFC2256: serial number of the entity' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{64} ) +olcAttributeTypes: {3}( 2.5.4.6 NAME ( 'c' 'countryName' ) DESC 'RFC4519: two-letter ISO-3166 country code' SUP name SYNTAX 1.3.6.1.4.1.1466.115.121.1.11 SINGLE-VALUE ) +olcAttributeTypes: {4}( 2.5.4.7 NAME ( 'l' 'localityName' ) DESC 'RFC2256: locality which this object resides in' SUP name ) +olcAttributeTypes: {5}( 2.5.4.8 NAME ( 'st' 'stateOrProvinceName' ) DESC 'RFC2256: state or province which this object resides in' SUP name ) +olcAttributeTypes: {6}( 2.5.4.9 NAME ( 'street' 'streetAddress' ) DESC 'RFC2256: street address of this object' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) +olcAttributeTypes: {7}( 2.5.4.10 NAME ( 'o' 'organizationName' ) DESC 'RFC2256: organization this object belongs to' SUP name ) +olcAttributeTypes: {8}( 2.5.4.11 NAME ( 'ou' 'organizationalUnitName' ) DESC 'RFC2256: organizational unit this object belongs to' SUP name ) +olcAttributeTypes: {9}( 2.5.4.12 NAME 'title' DESC 'RFC2256: title associated with the entity' SUP name ) +olcAttributeTypes: {10}( 2.5.4.14 NAME 'searchGuide' DESC 'RFC2256: search guide, deprecated by enhancedSearchGuide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.25 ) +olcAttributeTypes: {11}( 2.5.4.15 NAME 'businessCategory' DESC 'RFC2256: business category' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) +olcAttributeTypes: {12}( 2.5.4.16 NAME 'postalAddress' DESC 'RFC2256: postal address' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 ) +olcAttributeTypes: {13}( 2.5.4.17 NAME 'postalCode' DESC 'RFC2256: postal code' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} ) +olcAttributeTypes: {14}( 2.5.4.18 NAME 'postOfficeBox' DESC 'RFC2256: Post Office Box' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{40} ) +olcAttributeTypes: {15}( 2.5.4.19 NAME 'physicalDeliveryOfficeName' DESC 'RFC2256: Physical Delivery Office Name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{128} ) +olcAttributeTypes: {16}( 2.5.4.20 NAME 'telephoneNumber' DESC 'RFC2256: Telephone Number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50{32} ) +olcAttributeTypes: {17}( 2.5.4.21 NAME 'telexNumber' DESC 'RFC2256: Telex Number' SYNTAX 1.3.6.1.4.1.1466.115.121.1.52 ) +olcAttributeTypes: {18}( 2.5.4.22 NAME 'teletexTerminalIdentifier' DESC 'RFC2256: Teletex Terminal Identifier' SYNTAX 1.3.6.1.4.1.1466.115.121.1.51 ) +olcAttributeTypes: {19}( 2.5.4.23 NAME ( 'facsimileTelephoneNumber' 'fax' ) DESC 'RFC2256: Facsimile (Fax) Telephone Number' SYNTAX 1.3.6.1.4.1.1466.115.121.1.22 ) +olcAttributeTypes: {20}( 2.5.4.24 NAME 'x121Address' DESC 'RFC2256: X.121 Address' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{15} ) +olcAttributeTypes: {21}( 2.5.4.25 NAME 'internationaliSDNNumber' DESC 'RFC2256: international ISDN number' EQUALITY numericStringMatch SUBSTR numericStringSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.36{16} ) +olcAttributeTypes: {22}( 2.5.4.26 NAME 'registeredAddress' DESC 'RFC2256: registered postal address' SUP postalAddress SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 ) +olcAttributeTypes: {23}( 2.5.4.27 NAME 'destinationIndicator' DESC 'RFC2256: destination indicator' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44{128} ) +olcAttributeTypes: {24}( 2.5.4.28 NAME 'preferredDeliveryMethod' DESC 'RFC2256: preferred delivery method' SYNTAX 1.3.6.1.4.1.1466.115.121.1.14 SINGLE-VALUE ) +olcAttributeTypes: {25}( 2.5.4.29 NAME 'presentationAddress' DESC 'RFC2256: presentation address' EQUALITY presentationAddressMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.43 SINGLE-VALUE ) +olcAttributeTypes: {26}( 2.5.4.30 NAME 'supportedApplicationContext' DESC 'RFC2256: supported application context' EQUALITY objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 ) +olcAttributeTypes: {27}( 2.5.4.31 NAME 'member' DESC 'RFC2256: member of a group' SUP distinguishedName ) +olcAttributeTypes: {28}( 2.5.4.32 NAME 'owner' DESC 'RFC2256: owner (of the object)' SUP distinguishedName ) +olcAttributeTypes: {29}( 2.5.4.33 NAME 'roleOccupant' DESC 'RFC2256: occupant of role' SUP distinguishedName ) +olcAttributeTypes: {30}( 2.5.4.36 NAME 'userCertificate' DESC 'RFC2256: X.509 user certificate, use ;binary' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 ) +olcAttributeTypes: {31}( 2.5.4.37 NAME 'cACertificate' DESC 'RFC2256: X.509 CA certificate, use ;binary' EQUALITY certificateExactMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.8 ) +olcAttributeTypes: {32}( 2.5.4.38 NAME 'authorityRevocationList' DESC 'RFC2256: X.509 authority revocation list, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 ) +olcAttributeTypes: {33}( 2.5.4.39 NAME 'certificateRevocationList' DESC 'RFC2256: X.509 certificate revocation list, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 ) +olcAttributeTypes: {34}( 2.5.4.40 NAME 'crossCertificatePair' DESC 'RFC2256: X.509 cross certificate pair, use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.10 ) +olcAttributeTypes: {35}( 2.5.4.42 NAME ( 'givenName' 'gn' ) DESC 'RFC2256: first name(s) for which the entity is known by' SUP name ) +olcAttributeTypes: {36}( 2.5.4.43 NAME 'initials' DESC 'RFC2256: initials of some or all of names, but not the surname(s).' SUP name ) +olcAttributeTypes: {37}( 2.5.4.44 NAME 'generationQualifier' DESC 'RFC2256: name qualifier indicating a generation' SUP name ) +olcAttributeTypes: {38}( 2.5.4.45 NAME 'x500UniqueIdentifier' DESC 'RFC2256: X.500 unique identifier' EQUALITY bitStringMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.6 ) +olcAttributeTypes: {39}( 2.5.4.46 NAME 'dnQualifier' DESC 'RFC2256: DN qualifier' EQUALITY caseIgnoreMatch ORDERING caseIgnoreOrderingMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.44 ) +olcAttributeTypes: {40}( 2.5.4.47 NAME 'enhancedSearchGuide' DESC 'RFC2256: enhanced search guide' SYNTAX 1.3.6.1.4.1.1466.115.121.1.21 ) +olcAttributeTypes: {41}( 2.5.4.48 NAME 'protocolInformation' DESC 'RFC2256: protocol information' EQUALITY protocolInformationMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.42 ) +olcAttributeTypes: {42}( 2.5.4.50 NAME 'uniqueMember' DESC 'RFC2256: unique member of a group' EQUALITY uniqueMemberMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.34 ) +olcAttributeTypes: {43}( 2.5.4.51 NAME 'houseIdentifier' DESC 'RFC2256: house identifier' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{32768} ) +olcAttributeTypes: {44}( 2.5.4.52 NAME 'supportedAlgorithms' DESC 'RFC2256: supported algorithms' SYNTAX 1.3.6.1.4.1.1466.115.121.1.49 ) +olcAttributeTypes: {45}( 2.5.4.53 NAME 'deltaRevocationList' DESC 'RFC2256: delta revocation list; use ;binary' SYNTAX 1.3.6.1.4.1.1466.115.121.1.9 ) +olcAttributeTypes: {46}( 2.5.4.54 NAME 'dmdName' DESC 'RFC2256: name of DMD' SUP name ) +olcAttributeTypes: {47}( 2.5.4.65 NAME 'pseudonym' DESC 'X.520(4th): pseudonym for the object' SUP name ) +olcAttributeTypes: {48}( 0.9.2342.19200300.100.1.3 NAME ( 'mail' 'rfc822Mailbox' ) DESC 'RFC1274: RFC822 Mailbox' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} ) +olcAttributeTypes: {49}( 0.9.2342.19200300.100.1.25 NAME ( 'dc' 'domainComponent' ) DESC 'RFC1274/2247: domain component' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) +olcAttributeTypes: {50}( 0.9.2342.19200300.100.1.37 NAME 'associatedDomain' DESC 'RFC1274: domain associated with object' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {51}( 1.2.840.113549.1.9.1 NAME ( 'email' 'emailAddress' 'pkcs9email' ) DESC 'RFC3280: legacy attribute for email addresses in DNs' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} ) +olcObjectClasses: {0}( 2.5.6.2 NAME 'country' DESC 'RFC2256: a country' SUP top STRUCTURAL MUST c MAY ( searchGuide $ description ) ) +olcObjectClasses: {1}( 2.5.6.3 NAME 'locality' DESC 'RFC2256: a locality' SUP top STRUCTURAL MAY ( street $ seeAlso $ searchGuide $ st $ l $ description ) ) +olcObjectClasses: {2}( 2.5.6.4 NAME 'organization' DESC 'RFC2256: an organization' SUP top STRUCTURAL MUST o MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) ) +olcObjectClasses: {3}( 2.5.6.5 NAME 'organizationalUnit' DESC 'RFC2256: an organizational unit' SUP top STRUCTURAL MUST ou MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) ) +olcObjectClasses: {4}( 2.5.6.6 NAME 'person' DESC 'RFC2256: a person' SUP top STRUCTURAL MUST ( sn $ cn ) MAY ( userPassword $ telephoneNumber $ seeAlso $ description ) ) +olcObjectClasses: {5}( 2.5.6.7 NAME 'organizationalPerson' DESC 'RFC2256: an organizational person' SUP person STRUCTURAL MAY ( title $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l ) ) +olcObjectClasses: {6}( 2.5.6.8 NAME 'organizationalRole' DESC 'RFC2256: an organizational role' SUP top STRUCTURAL MUST cn MAY ( x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ seeAlso $ roleOccupant $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ ou $ st $ l $ description ) ) +olcObjectClasses: {7}( 2.5.6.9 NAME 'groupOfNames' DESC 'RFC2256: a group of names (DNs)' SUP top STRUCTURAL MUST ( member $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) ) +olcObjectClasses: {8}( 2.5.6.10 NAME 'residentialPerson' DESC 'RFC2256: an residential person' SUP person STRUCTURAL MUST l MAY ( businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ preferredDeliveryMethod $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l ) ) +olcObjectClasses: {9}( 2.5.6.11 NAME 'applicationProcess' DESC 'RFC2256: an application process' SUP top STRUCTURAL MUST cn MAY ( seeAlso $ ou $ l $ description ) ) +olcObjectClasses: {10}( 2.5.6.12 NAME 'applicationEntity' DESC 'RFC2256: an application entity' SUP top STRUCTURAL MUST ( presentationAddress $ cn ) MAY ( supportedApplicationContext $ seeAlso $ ou $ o $ l $ description ) ) +olcObjectClasses: {11}( 2.5.6.13 NAME 'dSA' DESC 'RFC2256: a directory system agent (a server)' SUP applicationEntity STRUCTURAL MAY knowledgeInformation ) +olcObjectClasses: {12}( 2.5.6.14 NAME 'device' DESC 'RFC2256: a device' SUP top STRUCTURAL MUST cn MAY ( serialNumber $ seeAlso $ owner $ ou $ o $ l $ description ) ) +olcObjectClasses: {13}( 2.5.6.15 NAME 'strongAuthenticationUser' DESC 'RFC2256: a strong authentication user' SUP top AUXILIARY MUST userCertificate ) +olcObjectClasses: {14}( 2.5.6.16 NAME 'certificationAuthority' DESC 'RFC2256: a certificate authority' SUP top AUXILIARY MUST ( authorityRevocationList $ certificateRevocationList $ cACertificate ) MAY crossCertificatePair ) +olcObjectClasses: {15}( 2.5.6.17 NAME 'groupOfUniqueNames' DESC 'RFC2256: a group of unique names (DN and Unique Identifier)' SUP top STRUCTURAL MUST ( uniqueMember $ cn ) MAY ( businessCategory $ seeAlso $ owner $ ou $ o $ description ) ) +olcObjectClasses: {16}( 2.5.6.18 NAME 'userSecurityInformation' DESC 'RFC2256: a user security information' SUP top AUXILIARY MAY ( supportedAlgorithms ) ) +olcObjectClasses: {17}( 2.5.6.16.2 NAME 'certificationAuthority-V2' SUP certificationAuthority AUXILIARY MAY ( deltaRevocationList ) ) +olcObjectClasses: {18}( 2.5.6.19 NAME 'cRLDistributionPoint' SUP top STRUCTURAL MUST ( cn ) MAY ( certificateRevocationList $ authorityRevocationList $ deltaRevocationList ) ) +olcObjectClasses: {19}( 2.5.6.20 NAME 'dmd' SUP top STRUCTURAL MUST ( dmdName ) MAY ( userPassword $ searchGuide $ seeAlso $ businessCategory $ x121Address $ registeredAddress $ destinationIndicator $ preferredDeliveryMethod $ telexNumber $ teletexTerminalIdentifier $ telephoneNumber $ internationaliSDNNumber $ facsimileTelephoneNumber $ street $ postOfficeBox $ postalCode $ postalAddress $ physicalDeliveryOfficeName $ st $ l $ description ) ) +olcObjectClasses: {20}( 2.5.6.21 NAME 'pkiUser' DESC 'RFC2587: a PKI user' SUP top AUXILIARY MAY userCertificate ) +olcObjectClasses: {21}( 2.5.6.22 NAME 'pkiCA' DESC 'RFC2587: PKI certificate authority' SUP top AUXILIARY MAY ( authorityRevocationList $ certificateRevocationList $ cACertificate $ crossCertificatePair ) ) +olcObjectClasses: {22}( 2.5.6.23 NAME 'deltaCRL' DESC 'RFC4523: X.509 delta CRL' SUP top AUXILIARY MAY deltaRevocationList ) +olcObjectClasses: {23}( 1.3.6.1.4.1.250.3.15 NAME 'labeledURIObject' DESC 'RFC2079: object that contains the URI attribute type' MAY ( labeledURI ) SUP top AUXILIARY ) +olcObjectClasses: {24}( 0.9.2342.19200300.100.4.19 NAME 'simpleSecurityObject' DESC 'RFC1274: simple security object' SUP top AUXILIARY MUST userPassword ) +olcObjectClasses: {25}( 1.3.6.1.4.1.1466.344 NAME 'dcObject' DESC 'RFC2247: domain component object' SUP top AUXILIARY MUST dc ) +olcObjectClasses: {26}( 1.3.6.1.1.3.1 NAME 'uidObject' DESC 'RFC2377: uid object' SUP top AUXILIARY MUST uid ) +structuralObjectClass: olcSchemaConfig +entryUUID: 713c5a24-df8a-103c-9771-230a0a7123cd +creatorsName: cn=config +createTimestamp: 20221013213337Z +entryCSN: 20221013213337.590086Z#000000#000#000000 +modifiersName: cn=config +modifyTimestamp: 20221013213337Z + +dn: cn={1}cosine,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: {1}cosine +olcAttributeTypes: {0}( 0.9.2342.19200300.100.1.2 NAME 'textEncodedORAddress' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {1}( 0.9.2342.19200300.100.1.4 NAME 'info' DESC 'RFC1274: general information' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{2048} ) +olcAttributeTypes: {2}( 0.9.2342.19200300.100.1.5 NAME ( 'drink' 'favouriteDrink' ) DESC 'RFC1274: favorite drink' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {3}( 0.9.2342.19200300.100.1.6 NAME 'roomNumber' DESC 'RFC1274: room number' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {4}( 0.9.2342.19200300.100.1.7 NAME 'photo' DESC 'RFC1274: photo (G3 fax)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.23{25000} ) +olcAttributeTypes: {5}( 0.9.2342.19200300.100.1.8 NAME 'userClass' DESC 'RFC1274: category of user' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {6}( 0.9.2342.19200300.100.1.9 NAME 'host' DESC 'RFC1274: host computer' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {7}( 0.9.2342.19200300.100.1.10 NAME 'manager' DESC 'RFC1274: DN of manager' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) +olcAttributeTypes: {8}( 0.9.2342.19200300.100.1.11 NAME 'documentIdentifier' DESC 'RFC1274: unique identifier of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {9}( 0.9.2342.19200300.100.1.12 NAME 'documentTitle' DESC 'RFC1274: title of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {10}( 0.9.2342.19200300.100.1.13 NAME 'documentVersion' DESC 'RFC1274: version of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {11}( 0.9.2342.19200300.100.1.14 NAME 'documentAuthor' DESC 'RFC1274: DN of author of document' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) +olcAttributeTypes: {12}( 0.9.2342.19200300.100.1.15 NAME 'documentLocation' DESC 'RFC1274: location of document original' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {13}( 0.9.2342.19200300.100.1.20 NAME ( 'homePhone' 'homeTelephoneNumber' ) DESC 'RFC1274: home telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 ) +olcAttributeTypes: {14}( 0.9.2342.19200300.100.1.21 NAME 'secretary' DESC 'RFC1274: DN of secretary' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) +olcAttributeTypes: {15}( 0.9.2342.19200300.100.1.22 NAME 'otherMailbox' SYNTAX 1.3.6.1.4.1.1466.115.121.1.39 ) +olcAttributeTypes: {16}( 0.9.2342.19200300.100.1.26 NAME 'aRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {17}( 0.9.2342.19200300.100.1.27 NAME 'mDRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {18}( 0.9.2342.19200300.100.1.28 NAME 'mXRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {19}( 0.9.2342.19200300.100.1.29 NAME 'nSRecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {20}( 0.9.2342.19200300.100.1.30 NAME 'sOARecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {21}( 0.9.2342.19200300.100.1.31 NAME 'cNAMERecord' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {22}( 0.9.2342.19200300.100.1.38 NAME 'associatedName' DESC 'RFC1274: DN of entry associated with domain' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) +olcAttributeTypes: {23}( 0.9.2342.19200300.100.1.39 NAME 'homePostalAddress' DESC 'RFC1274: home postal address' EQUALITY caseIgnoreListMatch SUBSTR caseIgnoreListSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.41 ) +olcAttributeTypes: {24}( 0.9.2342.19200300.100.1.40 NAME 'personalTitle' DESC 'RFC1274: personal title' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {25}( 0.9.2342.19200300.100.1.41 NAME ( 'mobile' 'mobileTelephoneNumber' ) DESC 'RFC1274: mobile telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 ) +olcAttributeTypes: {26}( 0.9.2342.19200300.100.1.42 NAME ( 'pager' 'pagerTelephoneNumber' ) DESC 'RFC1274: pager telephone number' EQUALITY telephoneNumberMatch SUBSTR telephoneNumberSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.50 ) +olcAttributeTypes: {27}( 0.9.2342.19200300.100.1.43 NAME ( 'co' 'friendlyCountryName' ) DESC 'RFC1274: friendly country name' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: {28}( 0.9.2342.19200300.100.1.44 NAME 'uniqueIdentifier' DESC 'RFC1274: unique identifer' EQUALITY caseIgnoreMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {29}( 0.9.2342.19200300.100.1.45 NAME 'organizationalStatus' DESC 'RFC1274: organizational status' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {30}( 0.9.2342.19200300.100.1.46 NAME 'janetMailbox' DESC 'RFC1274: Janet mailbox' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{256} ) +olcAttributeTypes: {31}( 0.9.2342.19200300.100.1.47 NAME 'mailPreferenceOption' DESC 'RFC1274: mail preference option' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 ) +olcAttributeTypes: {32}( 0.9.2342.19200300.100.1.48 NAME 'buildingName' DESC 'RFC1274: name of building' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15{256} ) +olcAttributeTypes: {33}( 0.9.2342.19200300.100.1.49 NAME 'dSAQuality' DESC 'RFC1274: DSA Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.19 SINGLE-VALUE ) +olcAttributeTypes: {34}( 0.9.2342.19200300.100.1.50 NAME 'singleLevelQuality' DESC 'RFC1274: Single Level Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE ) +olcAttributeTypes: {35}( 0.9.2342.19200300.100.1.51 NAME 'subtreeMinimumQuality' DESC 'RFC1274: Subtree Minimum Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE ) +olcAttributeTypes: {36}( 0.9.2342.19200300.100.1.52 NAME 'subtreeMaximumQuality' DESC 'RFC1274: Subtree Maximum Quality' SYNTAX 1.3.6.1.4.1.1466.115.121.1.13 SINGLE-VALUE ) +olcAttributeTypes: {37}( 0.9.2342.19200300.100.1.53 NAME 'personalSignature' DESC 'RFC1274: Personal Signature (G3 fax)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.23 ) +olcAttributeTypes: {38}( 0.9.2342.19200300.100.1.54 NAME 'dITRedirect' DESC 'RFC1274: DIT Redirect' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 ) +olcAttributeTypes: {39}( 0.9.2342.19200300.100.1.55 NAME 'audio' DESC 'RFC1274: audio (u-law)' SYNTAX 1.3.6.1.4.1.1466.115.121.1.4{25000} ) +olcAttributeTypes: {40}( 0.9.2342.19200300.100.1.56 NAME 'documentPublisher' DESC 'RFC1274: publisher of document' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcObjectClasses: {0}( 0.9.2342.19200300.100.4.4 NAME ( 'pilotPerson' 'newPilotPerson' ) SUP person STRUCTURAL MAY ( userid $ textEncodedORAddress $ rfc822Mailbox $ favouriteDrink $ roomNumber $ userClass $ homeTelephoneNumber $ homePostalAddress $ secretary $ personalTitle $ preferredDeliveryMethod $ businessCategory $ janetMailbox $ otherMailbox $ mobileTelephoneNumber $ pagerTelephoneNumber $ organizationalStatus $ mailPreferenceOption $ personalSignature ) ) +olcObjectClasses: {1}( 0.9.2342.19200300.100.4.5 NAME 'account' SUP top STRUCTURAL MUST userid MAY ( description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ host ) ) +olcObjectClasses: {2}( 0.9.2342.19200300.100.4.6 NAME 'document' SUP top STRUCTURAL MUST documentIdentifier MAY ( commonName $ description $ seeAlso $ localityName $ organizationName $ organizationalUnitName $ documentTitle $ documentVersion $ documentAuthor $ documentLocation $ documentPublisher ) ) +olcObjectClasses: {3}( 0.9.2342.19200300.100.4.7 NAME 'room' SUP top STRUCTURAL MUST commonName MAY ( roomNumber $ description $ seeAlso $ telephoneNumber ) ) +olcObjectClasses: {4}( 0.9.2342.19200300.100.4.9 NAME 'documentSeries' SUP top STRUCTURAL MUST commonName MAY ( description $ seeAlso $ telephonenumber $ localityName $ organizationName $ organizationalUnitName ) ) +olcObjectClasses: {5}( 0.9.2342.19200300.100.4.13 NAME 'domain' SUP top STRUCTURAL MUST domainComponent MAY ( associatedName $ organizationName $ description $ businessCategory $ seeAlso $ searchGuide $ userPassword $ localityName $ stateOrProvinceName $ streetAddress $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address ) ) +olcObjectClasses: {6}( 0.9.2342.19200300.100.4.14 NAME 'RFC822localPart' SUP domain STRUCTURAL MAY ( commonName $ surname $ description $ seeAlso $ telephoneNumber $ physicalDeliveryOfficeName $ postalAddress $ postalCode $ postOfficeBox $ streetAddress $ facsimileTelephoneNumber $ internationalISDNNumber $ telephoneNumber $ teletexTerminalIdentifier $ telexNumber $ preferredDeliveryMethod $ destinationIndicator $ registeredAddress $ x121Address ) ) +olcObjectClasses: {7}( 0.9.2342.19200300.100.4.15 NAME 'dNSDomain' SUP domain STRUCTURAL MAY ( ARecord $ MDRecord $ MXRecord $ NSRecord $ SOARecord $ CNAMERecord ) ) +olcObjectClasses: {8}( 0.9.2342.19200300.100.4.17 NAME 'domainRelatedObject' DESC 'RFC1274: an object related to an domain' SUP top AUXILIARY MUST associatedDomain ) +olcObjectClasses: {9}( 0.9.2342.19200300.100.4.18 NAME 'friendlyCountry' SUP country STRUCTURAL MUST friendlyCountryName ) +olcObjectClasses: {10}( 0.9.2342.19200300.100.4.20 NAME 'pilotOrganization' SUP ( organization $ organizationalUnit ) STRUCTURAL MAY buildingName ) +olcObjectClasses: {11}( 0.9.2342.19200300.100.4.21 NAME 'pilotDSA' SUP dsa STRUCTURAL MAY dSAQuality ) +olcObjectClasses: {12}( 0.9.2342.19200300.100.4.22 NAME 'qualityLabelledData' SUP top AUXILIARY MUST dsaQuality MAY ( subtreeMinimumQuality $ subtreeMaximumQuality ) ) +structuralObjectClass: olcSchemaConfig +entryUUID: 713ca97a-df8a-103c-9772-230a0a7123cd +creatorsName: cn=config +createTimestamp: 20221013213337Z +entryCSN: 20221013213337.592117Z#000000#000#000000 +modifiersName: cn=config +modifyTimestamp: 20221013213337Z + +dn: cn={2}inetorgperson,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: {2}inetorgperson +olcAttributeTypes: {0}( 2.16.840.1.113730.3.1.1 NAME 'carLicense' DESC 'RFC2798: vehicle license or registration plate' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: {1}( 2.16.840.1.113730.3.1.2 NAME 'departmentNumber' DESC 'RFC2798: identifies a department within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: {2}( 2.16.840.1.113730.3.1.241 NAME 'displayName' DESC 'RFC2798: preferred name to be used when displaying entries' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +olcAttributeTypes: {3}( 2.16.840.1.113730.3.1.3 NAME 'employeeNumber' DESC 'RFC2798: numerically identifies an employee within an organization' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +olcAttributeTypes: {4}( 2.16.840.1.113730.3.1.4 NAME 'employeeType' DESC 'RFC2798: type of employment for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 ) +olcAttributeTypes: {5}( 0.9.2342.19200300.100.1.60 NAME 'jpegPhoto' DESC 'RFC2798: a JPEG image' SYNTAX 1.3.6.1.4.1.1466.115.121.1.28 ) +olcAttributeTypes: {6}( 2.16.840.1.113730.3.1.39 NAME 'preferredLanguage' DESC 'RFC2798: preferred written or spoken language for a person' EQUALITY caseIgnoreMatch SUBSTR caseIgnoreSubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 SINGLE-VALUE ) +olcAttributeTypes: {7}( 2.16.840.1.113730.3.1.40 NAME 'userSMIMECertificate' DESC 'RFC2798: PKCS#7 SignedData used to support S/MIME' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 ) +olcAttributeTypes: {8}( 2.16.840.1.113730.3.1.216 NAME 'userPKCS12' DESC 'RFC2798: personal identity information, a PKCS #12 PFX' SYNTAX 1.3.6.1.4.1.1466.115.121.1.5 ) +olcObjectClasses: {0}( 2.16.840.1.113730.3.2.2 NAME 'inetOrgPerson' DESC 'RFC2798: Internet Organizational Person' SUP organizationalPerson STRUCTURAL MAY ( audio $ businessCategory $ carLicense $ departmentNumber $ displayName $ employeeNumber $ employeeType $ givenName $ homePhone $ homePostalAddress $ initials $ jpegPhoto $ labeledURI $ mail $ manager $ mobile $ o $ pager $ photo $ roomNumber $ secretary $ uid $ userCertificate $ x500uniqueIdentifier $ preferredLanguage $ userSMIMECertificate $ userPKCS12 ) ) +structuralObjectClass: olcSchemaConfig +entryUUID: 713cc81a-df8a-103c-9773-230a0a7123cd +creatorsName: cn=config +createTimestamp: 20221013213337Z +entryCSN: 20221013213337.592900Z#000000#000#000000 +modifiersName: cn=config +modifyTimestamp: 20221013213337Z + +dn: cn={3}nis,cn=schema,cn=config +objectClass: olcSchemaConfig +cn: {3}nis +olcAttributeTypes: {0}( 1.3.6.1.1.1.1.2 NAME 'gecos' DESC 'The GECOS field; the common name' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) +olcAttributeTypes: {1}( 1.3.6.1.1.1.1.3 NAME 'homeDirectory' DESC 'The absolute path to the home directory' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) +olcAttributeTypes: {2}( 1.3.6.1.1.1.1.4 NAME 'loginShell' DESC 'The path to the login shell' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE ) +olcAttributeTypes: {3}( 1.3.6.1.1.1.1.5 NAME 'shadowLastChange' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {4}( 1.3.6.1.1.1.1.6 NAME 'shadowMin' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {5}( 1.3.6.1.1.1.1.7 NAME 'shadowMax' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {6}( 1.3.6.1.1.1.1.8 NAME 'shadowWarning' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {7}( 1.3.6.1.1.1.1.9 NAME 'shadowInactive' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {8}( 1.3.6.1.1.1.1.10 NAME 'shadowExpire' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {9}( 1.3.6.1.1.1.1.11 NAME 'shadowFlag' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {10}( 1.3.6.1.1.1.1.12 NAME 'memberUid' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {11}( 1.3.6.1.1.1.1.13 NAME 'memberNisNetgroup' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {12}( 1.3.6.1.1.1.1.14 NAME 'nisNetgroupTriple' DESC 'Netgroup triple' SYNTAX 1.3.6.1.1.1.0.0 ) +olcAttributeTypes: {13}( 1.3.6.1.1.1.1.15 NAME 'ipServicePort' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {14}( 1.3.6.1.1.1.1.16 NAME 'ipServiceProtocol' SUP name ) +olcAttributeTypes: {15}( 1.3.6.1.1.1.1.17 NAME 'ipProtocolNumber' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {16}( 1.3.6.1.1.1.1.18 NAME 'oncRpcNumber' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE ) +olcAttributeTypes: {17}( 1.3.6.1.1.1.1.19 NAME 'ipHostNumber' DESC 'IP address' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} ) +olcAttributeTypes: {18}( 1.3.6.1.1.1.1.20 NAME 'ipNetworkNumber' DESC 'IP network' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE ) +olcAttributeTypes: {19}( 1.3.6.1.1.1.1.21 NAME 'ipNetmaskNumber' DESC 'IP netmask' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} SINGLE-VALUE ) +olcAttributeTypes: {20}( 1.3.6.1.1.1.1.22 NAME 'macAddress' DESC 'MAC address' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{128} ) +olcAttributeTypes: {21}( 1.3.6.1.1.1.1.23 NAME 'bootParameter' DESC 'rpc.bootparamd parameter' SYNTAX 1.3.6.1.1.1.0.1 ) +olcAttributeTypes: {22}( 1.3.6.1.1.1.1.24 NAME 'bootFile' DESC 'Boot image name' EQUALITY caseExactIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 ) +olcAttributeTypes: {23}( 1.3.6.1.1.1.1.26 NAME 'nisMapName' SUP name ) +olcAttributeTypes: {24}( 1.3.6.1.1.1.1.27 NAME 'nisMapEntry' EQUALITY caseExactIA5Match SUBSTR caseExactIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26{1024} SINGLE-VALUE ) +olcObjectClasses: {0}( 1.3.6.1.1.1.2.0 NAME 'posixAccount' DESC 'Abstraction of an account with POSIX attributes' SUP top AUXILIARY MUST ( cn $ uid $ uidNumber $ gidNumber $ homeDirectory ) MAY ( userPassword $ loginShell $ gecos $ description ) ) +olcObjectClasses: {1}( 1.3.6.1.1.1.2.1 NAME 'shadowAccount' DESC 'Additional attributes for shadow passwords' SUP top AUXILIARY MUST uid MAY ( userPassword $ shadowLastChange $ shadowMin $ shadowMax $ shadowWarning $ shadowInactive $ shadowExpire $ shadowFlag $ description ) ) +olcObjectClasses: {2}( 1.3.6.1.1.1.2.2 NAME 'posixGroup' DESC 'Abstraction of a group of accounts' SUP top STRUCTURAL MUST ( cn $ gidNumber ) MAY ( userPassword $ memberUid $ description ) ) +olcObjectClasses: {3}( 1.3.6.1.1.1.2.3 NAME 'ipService' DESC 'Abstraction an Internet Protocol service' SUP top STRUCTURAL MUST ( cn $ ipServicePort $ ipServiceProtocol ) MAY description ) +olcObjectClasses: {4}( 1.3.6.1.1.1.2.4 NAME 'ipProtocol' DESC 'Abstraction of an IP protocol' SUP top STRUCTURAL MUST ( cn $ ipProtocolNumber $ description ) MAY description ) +olcObjectClasses: {5}( 1.3.6.1.1.1.2.5 NAME 'oncRpc' DESC 'Abstraction of an ONC/RPC binding' SUP top STRUCTURAL MUST ( cn $ oncRpcNumber $ description ) MAY description ) +olcObjectClasses: {6}( 1.3.6.1.1.1.2.6 NAME 'ipHost' DESC 'Abstraction of a host, an IP device' SUP top AUXILIARY MUST ( cn $ ipHostNumber ) MAY ( l $ description $ manager ) ) +olcObjectClasses: {7}( 1.3.6.1.1.1.2.7 NAME 'ipNetwork' DESC 'Abstraction of an IP network' SUP top STRUCTURAL MUST ( cn $ ipNetworkNumber ) MAY ( ipNetmaskNumber $ l $ description $ manager ) ) +olcObjectClasses: {8}( 1.3.6.1.1.1.2.8 NAME 'nisNetgroup' DESC 'Abstraction of a netgroup' SUP top STRUCTURAL MUST cn MAY ( nisNetgroupTriple $ memberNisNetgroup $ description ) ) +olcObjectClasses: {9}( 1.3.6.1.1.1.2.9 NAME 'nisMap' DESC 'A generic abstraction of a NIS map' SUP top STRUCTURAL MUST nisMapName MAY description ) +olcObjectClasses: {10}( 1.3.6.1.1.1.2.10 NAME 'nisObject' DESC 'An entry in a NIS map' SUP top STRUCTURAL MUST ( cn $ nisMapEntry $ nisMapName ) MAY description ) +olcObjectClasses: {11}( 1.3.6.1.1.1.2.11 NAME 'ieee802Device' DESC 'A device with a MAC address' SUP top AUXILIARY MAY macAddress ) +olcObjectClasses: {12}( 1.3.6.1.1.1.2.12 NAME 'bootableDevice' DESC 'A device with boot parameters' SUP top AUXILIARY MAY ( bootFile $ bootParameter ) ) +structuralObjectClass: olcSchemaConfig +entryUUID: 713cd51c-df8a-103c-9774-230a0a7123cd +creatorsName: cn=config +createTimestamp: 20221013213337Z +entryCSN: 20221013213337.593234Z#000000#000#000000 +modifiersName: cn=config +modifyTimestamp: 20221013213337Z + +dn: olcDatabase=config,cn=config +objectClass: olcDatabaseConfig +olcDatabase: config + +dn: cn=module,cn=config +objectClass: olcModuleList +cn: module +olcModuleLoad: back_mdb + +dn: olcDatabase={1}mdb,cn=config +objectClass: olcDatabaseConfig +objectClass: olcMdbConfig +olcDatabase: {1}mdb +olcSuffix: dc=example,dc=com +olcDbDirectory: /usr/local/openldap/var/openldap-data +olcRootDN: cn=admin,dc=example,dc=com +olcRootPW: admin +olcAccess: to * by * write + diff --git a/packages/tom-server/src/search-engine-api/__testData__/llng/lmConf-1.json b/packages/tom-server/src/search-engine-api/__testData__/llng/lmConf-1.json new file mode 100644 index 00000000..1b56b4b3 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/llng/lmConf-1.json @@ -0,0 +1,481 @@ +{ + "ADPwdExpireWarning": 0, + "ADPwdMaxAge": 0, + "SMTPServer": "", + "SMTPTLS": "", + "SSLAuthnLevel": 5, + "SSLIssuerVar": "SSL_CLIENT_I_DN", + "SSLVar": "SSL_CLIENT_S_DN_Email", + "SSLVarIf": {}, + "activeTimer": 1, + "apacheAuthnLevel": 3, + "applicationList": {}, + "authChoiceParam": "lmAuth", + "authentication": "LDAP", + "available2F": "UTOTP,TOTP,U2F,REST,Mail2F,Ext2F,WebAuthn,Yubikey,Radius,Password", + "available2FSelfRegistration": "Password,TOTP,U2F,WebAuthn,Yubikey", + "bruteForceProtectionLockTimes": "15, 30, 60, 300, 600", + "bruteForceProtectionMaxAge": 300, + "bruteForceProtectionMaxFailed": 3, + "bruteForceProtectionMaxLockTime": 900, + "bruteForceProtectionTempo": 30, + "captcha_mail_enabled": 1, + "captcha_register_enabled": 1, + "captcha_size": 6, + "casAccessControlPolicy": "none", + "casAuthnLevel": 1, + "casTicketExpiration": 0, + "certificateResetByMailCeaAttribute": "description", + "certificateResetByMailCertificateAttribute": "userCertificate;binary", + "certificateResetByMailURL": "https://auth.example.com/certificateReset", + "certificateResetByMailValidityDelay": 0, + "cfgAuthor": "The LemonLDAP::NG team", + "cfgDate": "1627287638", + "cfgNum": "1", + "cfgVersion": "2.0.16", + "checkDevOpsCheckSessionAttributes": 1, + "checkDevOpsDisplayNormalizedHeaders": 1, + "checkDevOpsDownload": 1, + "checkHIBPRequired": 1, + "checkHIBPURL": "https://api.pwnedpasswords.com/range/", + "checkTime": 600, + "checkUserDisplayComputedSession": 1, + "checkUserDisplayEmptyHeaders": 0, + "checkUserDisplayEmptyValues": 0, + "checkUserDisplayHiddenAttributes": 0, + "checkUserDisplayHistory": 0, + "checkUserDisplayNormalizedHeaders": 0, + "checkUserDisplayPersistentInfo": 0, + "checkUserHiddenAttributes": "_loginHistory, _session_id, hGroups", + "checkUserIdRule": 1, + "checkXSS": 1, + "confirmFormMethod": "post", + "contextSwitchingIdRule": 1, + "contextSwitchingPrefix": "switching", + "contextSwitchingRule": 0, + "contextSwitchingStopWithLogout": 1, + "cookieName": "lemonldap", + "corsAllow_Credentials": "true", + "corsAllow_Headers": "*", + "corsAllow_Methods": "POST,GET", + "corsAllow_Origin": "*", + "corsEnabled": 1, + "corsExpose_Headers": "*", + "corsMax_Age": "86400", + "crowdsecAction": "reject", + "cspConnect": "'self'", + "cspDefault": "'self'", + "cspFont": "'self'", + "cspFormAction": "*", + "cspFrameAncestors": "", + "cspImg": "'self' data:", + "cspScript": "'self'", + "cspStyle": "'self'", + "dbiAuthnLevel": 2, + "dbiExportedVars": {}, + "decryptValueRule": 0, + "demoExportedVars": { + "cn": "cn", + "mail": "mail", + "uid": "uid" + }, + "displaySessionId": 1, + "domain": "example.com", + "exportedHeaders": {}, + "exportedVars": {}, + "ext2fActivation": 0, + "ext2fCodeActivation": "\\d{6}", + "facebookAuthnLevel": 1, + "facebookExportedVars": {}, + "facebookUserField": "id", + "failedLoginNumber": 5, + "findUserControl": "^[*\\w]+$", + "findUserWildcard": "*", + "formTimeout": 120, + "githubAuthnLevel": 1, + "githubScope": "user:email", + "githubUserField": "login", + "globalLogoutRule": 0, + "globalLogoutTimer": 1, + "globalStorage": "Apache::Session::File", + "globalStorageOptions": { + "Directory": "/var/lib/lemonldap-ng/sessions", + "LockDirectory": "/var/lib/lemonldap-ng/sessions/lock", + "generateModule": "Lemonldap::NG::Common::Apache::Session::Generate::SHA256" + }, + "gpgAuthnLevel": 5, + "gpgDb": "", + "grantSessionRules": {}, + "groups": {}, + "handlerInternalCache": 15, + "handlerServiceTokenTTL": 30, + "hiddenAttributes": "_password, _2fDevices", + "httpOnly": 1, + "https": -1, + "impersonationHiddenAttributes": "_2fDevices, _loginHistory", + "impersonationIdRule": 1, + "impersonationMergeSSOgroups": 0, + "impersonationPrefix": "real_", + "impersonationRule": 0, + "impersonationSkipEmptyValues": 1, + "infoFormMethod": "get", + "issuerDBCASPath": "^/cas/", + "issuerDBCASRule": 1, + "issuerDBGetParameters": {}, + "issuerDBGetPath": "^/get/", + "issuerDBGetRule": 1, + "issuerDBOpenIDConnectActivation": 1, + "issuerDBOpenIDConnectPath": "^/oauth2/", + "issuerDBOpenIDConnectRule": 1, + "issuerDBOpenIDPath": "^/openidserver/", + "issuerDBOpenIDRule": 1, + "issuerDBSAMLPath": "^/saml/", + "issuerDBSAMLRule": 1, + "issuersTimeout": 120, + "jsRedirect": 0, + "key": "^vmTGvh{+]5!ToB?", + "krbAuthnLevel": 3, + "krbRemoveDomain": 1, + "ldapServer": "annuaire", + "ldapAuthnLevel": 2, + "ldapBase": "dc=example,dc=com", + "ldapExportedVars": { + "cn": "cn", + "mail": "mail", + "uid": "uid" + }, + "ldapGroupAttributeName": "member", + "ldapGroupAttributeNameGroup": "dn", + "ldapGroupAttributeNameSearch": "cn", + "ldapGroupAttributeNameUser": "dn", + "ldapGroupObjectClass": "groupOfNames", + "ldapIOTimeout": 10, + "ldapPasswordResetAttribute": "pwdReset", + "ldapPasswordResetAttributeValue": "TRUE", + "ldapPwdEnc": "utf-8", + "ldapSearchDeref": "find", + "ldapTimeout": 10, + "ldapUsePasswordResetAttribute": 1, + "ldapVerify": "require", + "ldapVersion": 3, + "linkedInAuthnLevel": 1, + "linkedInFields": "id,first-name,last-name,email-address", + "linkedInScope": "r_liteprofile r_emailaddress", + "linkedInUserField": "emailAddress", + "localSessionStorage": "Cache::FileCache", + "localSessionStorageOptions": { + "cache_depth": 3, + "cache_root": "/var/lib/lemonldap-ng/cache", + "default_expires_in": 600, + "directory_umask": "007", + "namespace": "lemonldap-ng-sessions" + }, + "locationDetectGeoIpLanguages": "en, fr", + "locationRules": { + "auth.example.com": { + "(?#checkUser)^/checkuser": "inGroup(\"timelords\")", + "(?#errors)^/lmerror/": "accept", + "default": "accept" + } + }, + "loginHistoryEnabled": 1, + "logoutServices": {}, + "macros": { + "UA": "$ENV{HTTP_USER_AGENT}", + "_whatToTrace": "$_auth eq 'SAML' ? lc($_user.'@'.$_idpConfKey) : $_auth eq 'OpenIDConnect' ? lc($_user.'@'.$_oidc_OP) : lc($_user)" + }, + "mail2fActivation": 0, + "mail2fCodeRegex": "\\d{6}", + "mailCharset": "utf-8", + "mailFrom": "noreply@example.com", + "mailSessionKey": "mail", + "mailTimeout": 0, + "mailUrl": "https://auth.example.com/resetpwd", + "managerDn": "", + "managerPassword": "", + "max2FDevices": 10, + "max2FDevicesNameLength": 20, + "multiValuesSeparator": "; ", + "mySessionAuthorizedRWKeys": [ + "_appsListOrder", + "_oidcConnectedRP", + "_oidcConsents" + ], + "newLocationWarningLocationAttribute": "ipAddr", + "newLocationWarningLocationDisplayAttribute": "", + "newLocationWarningMaxValues": "0", + "notification": 0, + "notificationDefaultCond": "", + "notificationServerPOST": 1, + "notificationServerSentAttributes": "uid reference date title subtitle text check", + "notificationStorage": "File", + "notificationStorageOptions": { + "dirName": "/var/lib/lemonldap-ng/notifications" + }, + "notificationWildcard": "allusers", + "notificationsMaxRetrieve": 3, + "notifyDeleted": 1, + "nullAuthnLevel": 0, + "oidcAuthnLevel": 1, + "oidcOPMetaDataExportedVars": {}, + "oidcOPMetaDataJSON": {}, + "oidcOPMetaDataJWKS": {}, + "oidcOPMetaDataOptions": {}, + "oidcRPCallbackGetParam": "openidconnectcallback", + "oidcRPMetaDataExportedVars": { + "matrix0": { + "email": "mail", + "family_name": "cn", + "given_name": "cn", + "name": "cn", + "nickname": "uid", + "preferred_username": "uid" + }, + "matrix1": { + "email": "mail", + "family_name": "cn", + "given_name": "cn", + "name": "cn", + "nickname": "uid", + "preferred_username": "uid" + }, + "matrix2": { + "email": "mail", + "family_name": "cn", + "given_name": "cn", + "name": "cn", + "nickname": "uid", + "preferred_username": "uid" + }, + "matrix3": { + "email": "mail", + "family_name": "cn", + "given_name": "cn", + "name": "cn", + "nickname": "uid", + "preferred_username": "uid" + } + }, + "oidcRPMetaDataMacros": null, + "oidcRPMetaDataOptions": { + "matrix1": { + "oidcRPMetaDataOptionsAccessTokenClaims": 0, + "oidcRPMetaDataOptionsAccessTokenJWT": 0, + "oidcRPMetaDataOptionsAccessTokenSignAlg": "RS256", + "oidcRPMetaDataOptionsAllowClientCredentialsGrant": 0, + "oidcRPMetaDataOptionsAllowOffline": 0, + "oidcRPMetaDataOptionsAllowPasswordGrant": 0, + "oidcRPMetaDataOptionsBypassConsent": 1, + "oidcRPMetaDataOptionsClientID": "matrix1", + "oidcRPMetaDataOptionsClientSecret": "matrix1*", + "oidcRPMetaDataOptionsIDTokenForceClaims": 0, + "oidcRPMetaDataOptionsIDTokenSignAlg": "RS256", + "oidcRPMetaDataOptionsLogoutBypassConfirm": 0, + "oidcRPMetaDataOptionsLogoutSessionRequired": 1, + "oidcRPMetaDataOptionsLogoutType": "back", + "oidcRPMetaDataOptionsPublic": 0, + "oidcRPMetaDataOptionsRedirectUris": "https://matrix.example.com/_synapse/client/oidc/callback", + "oidcRPMetaDataOptionsRefreshToken": 0, + "oidcRPMetaDataOptionsRequirePKCE": 0 + } + }, + "oidcRPMetaDataOptionsExtraClaims": null, + "oidcRPMetaDataScopeRules": null, + "oidcRPStateTimeout": 600, + "oidcServiceAccessTokenExpiration": 3600, + "oidcServiceAllowAuthorizationCodeFlow": 1, + "oidcServiceAllowImplicitFlow": 0, + "oidcServiceAuthorizationCodeExpiration": 60, + "oidcServiceDynamicRegistrationExportedVars": {}, + "oidcServiceDynamicRegistrationExtraClaims": {}, + "oidcServiceIDTokenExpiration": 3600, + "oidcServiceIgnoreScopeForClaims": 1, + "oidcServiceKeyIdSig": "oMGHInscAW3Nsa0FcnCnDA", + "oidcServiceMetaDataAuthnContext": { + "loa-1": 1, + "loa-2": 2, + "loa-3": 3, + "loa-4": 4, + "loa-5": 5 + }, + "oidcServiceMetaDataAuthorizeURI": "authorize", + "oidcServiceMetaDataBackChannelURI": "blogout", + "oidcServiceMetaDataCheckSessionURI": "checksession.html", + "oidcServiceMetaDataEndSessionURI": "logout", + "oidcServiceMetaDataFrontChannelURI": "flogout", + "oidcServiceMetaDataIntrospectionURI": "introspect", + "oidcServiceMetaDataJWKSURI": "jwks", + "oidcServiceMetaDataRegistrationURI": "register", + "oidcServiceMetaDataTokenURI": "token", + "oidcServiceMetaDataUserInfoURI": "userinfo", + "oidcServiceOfflineSessionExpiration": 2592000, + "oidcServicePrivateKeySig": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDywteBzIOlhKc4\nO+vhMStDYOpPYrWDOodkUZ7OsxlWVNZ/b/lqIFS56+MHPkKNQuT4zZCyO8bEKmmR\nZ6kPFJoGbO1zJCPQ/RKjimX4J/5gDb1BAlo+6agJi55e3Bw0zKNJDU0mRyedcIzW\n7ywTgyj6B35pl/Sfloi4Q1XEizHar+26h66SOEtnppMxGvwsxO8gFWz26CPmalvY\n5GNYR0txbXUZn7I4kDa4mMWgNfeocWc78Qbt4RV5EuQdbRh1sou4tL9Nn4EuGhg0\nmfsSI0xVAj7f82Wn3kW6qEbhuejrY7aqmZjN7yrMKtCBuV7o4hVrjYLuM2j0mInY\nMy5nRNOVAgMBAAECggEAJ145nK8R2lG83H27LvXOUkrxNJaJYRKoyjgCTPr2bO2t\nK1V5WSCNHOmIE7ChEk962m5bvMu83CsUm6P34p4wrEIV78o4lLe1whe7mZbCxcj0\nnApJoFI8EfA2aqO/X0CgakRh8ocvgXSzIlf/CdsHViTI907ROOAso9Unn4wDNbdp\nMrhi3H2SnA+ewzj85WygBVTNQmVBjJSSLXTQRkfHye0ztvQm59gqqaJaM2rkBjvA\nlPWAVsgakOk4pgClKElCsIjWPJwdYtcd8VJrwnro5J9KhMwB//AArGgqOaXUHnLH\nv5aZZp6FjV/M3BxbSp4cG6hXmK1hrDFLecRddYP1gQKBgQD+Y4/ee57Z0E2V8833\nYfrK3F23sfxmZ7zUwEbgFXUfRy3RVW7Hbc7PAJzxzrk+LYk/zaZrrfEJguqG2O6m\nVNYkqxKu69Nn964CMdV15JGxVzpzsN5adKlcvKVVv9gx2rF3SMUOHiRutj2BlUtO\niCq0G3jFsXWIRzePig9PbWP6CQKBgQD0TG2DeDDUgKbeJYIzXfmCvGxlm5MZqCc/\nK7d8P9U0svG//jJRTsa9hcLjk7N24CzhLNHyJmT7dh1Xy1oLyHNPZ4nQRmCe+HUf\nu0SK10WZ2K55ekUmqS+xSuDFWJtWa5SE46cKg0fKu7YkiDKI1s6I3qrF4lew2aDE\n2p8GJRrgLQKBgCh2PZPtpb6PW0fWl5QZiYJqup1VOggvx+EvFBbgUti+wZLiO9SM\nqrBSMKRldSFmrMXxN984s3YH1LXOG2dpZwY+D6Ky79VBl/PRaVpvGJ1Uen+cSkGo\n/Kc7ejDBaunDFycZ8/3i3Xiek/ngfTHohqJPHE6Vg1RBv5ydIQJJK/XBAoGAU1XO\n9c4GOjc4tQbuhz9DYgmMoIyVfWcTHEV5bfUIcdWpCelYmMval8QNWzyDN8X5CUcU\nxxm50N3V3KENsn9KdofHRzj6tL/klFJ5azNMFtMHkYDYHfwQvNXiHu++7Zf9LefK\nj5eA4fNuir+7HVrJUX9DmgVADJ/wa7Z4EMyPgnECgYA/NLUs4920h10ie5lFffpM\nqq6CRcBjsQ7eGK9UI1Z2KZUh94eqIENSJ7whBjXKvJJvhAlH4//lVFMMRs7oJePY\nThg+8In7PB64yMOIJZLc5Fekn9aGG6YtErPzePQkXSYCKZxWl5EpjQZGgPRVkNtD\n2nflyJLjiCbTjeNgWIOZlw==\n-----END PRIVATE KEY-----\n", + "oidcServicePublicKeySig": "-----BEGIN CERTIFICATE-----\nMIICuDCCAaCgAwIBAgIEFU77HjANBgkqhkiG9w0BAQsFADAeMRwwGgYDVQQDDBNt\nYXRyaXgubGluYWdvcmEuY29tMB4XDTIzMDIxNTAzMTk0NloXDTQzMDIxMDAzMTk0\nNlowHjEcMBoGA1UEAwwTbWF0cml4LmxpbmFnb3JhLmNvbTCCASIwDQYJKoZIhvcN\nAQEBBQADggEPADCCAQoCggEBAPLC14HMg6WEpzg76+ExK0Ng6k9itYM6h2RRns6z\nGVZU1n9v+WogVLnr4wc+Qo1C5PjNkLI7xsQqaZFnqQ8UmgZs7XMkI9D9EqOKZfgn\n/mANvUECWj7pqAmLnl7cHDTMo0kNTSZHJ51wjNbvLBODKPoHfmmX9J+WiLhDVcSL\nMdqv7bqHrpI4S2emkzEa/CzE7yAVbPboI+ZqW9jkY1hHS3FtdRmfsjiQNriYxaA1\n96hxZzvxBu3hFXkS5B1tGHWyi7i0v02fgS4aGDSZ+xIjTFUCPt/zZafeRbqoRuG5\n6OtjtqqZmM3vKswq0IG5XujiFWuNgu4zaPSYidgzLmdE05UCAwEAATANBgkqhkiG\n9w0BAQsFAAOCAQEArNmGxZVvmvdOLctv+zQ+npzQtOTaJcf+r/1xYuM4FZVe4yLc\ny9ElDskoDWjvQU7jKeJeaDOYgMJQNrek8Doj8uHPWNe6jYFa62Csg9aPz6e8qbtq\nWI+sXds5GJd6xZ8mi2L4MdT/tf8dBgcgybuoRyhBtJwG1rLNAYkeXMxkBzOFcU7K\nR/SZ0q9ToLAWFDhn42MTjPN3t6GwKDzGNsM/SI/3WvUwpQbtK91hjPnNDwKiAtGG\nfUteuigfXY+0hEcQwJdR0St/FQ8UYYcAB5YT9IkT1wCcU5LfPHCBf3OXNpbnQsHh\netQMKLibM6wWdXNwmsd1szO66ft3QZ4h4EG3Vw==\n-----END CERTIFICATE-----\n", + "oidcStorageOptions": {}, + "openIdAuthnLevel": 1, + "openIdExportedVars": {}, + "openIdIDPList": "0;", + "openIdSPList": "0;", + "openIdSreg_email": "mail", + "openIdSreg_fullname": "cn", + "openIdSreg_nickname": "uid", + "openIdSreg_timezone": "_timezone", + "pamAuthnLevel": 2, + "pamService": "login", + "password2fActivation": 0, + "password2fSelfRegistration": 0, + "password2fUserCanRemoveKey": 1, + "passwordDB": "Demo", + "passwordPolicyActivation": 1, + "passwordPolicyMinDigit": 0, + "passwordPolicyMinLower": 0, + "passwordPolicyMinSize": 0, + "passwordPolicyMinSpeChar": 0, + "passwordPolicyMinUpper": 0, + "passwordPolicySpecialChar": "__ALL__", + "passwordResetAllowedRetries": 3, + "persistentSessionAttributes": "_loginHistory _2fDevices notification_", + "persistentStorage": "Apache::Session::File", + "persistentStorageOptions": { + "Directory": "/var/lib/lemonldap-ng/psessions", + "LockDirectory": "/var/lib/lemonldap-ng/psessions/lock" + }, + "port": -1, + "portal": "https://auth.example.com", + "portalAntiFrame": 1, + "portalCheckLogins": 1, + "portalDisplayAppslist": 1, + "portalDisplayChangePassword": "$_auth =~ /^(LDAP|DBI|Demo)$/", + "portalDisplayGeneratePassword": 1, + "portalDisplayLoginHistory": 1, + "portalDisplayLogout": 1, + "portalDisplayOidcConsents": "$_oidcConsents && $_oidcConsents =~ /\\w+/", + "portalDisplayOrder": "Appslist ChangePassword LoginHistory OidcConsents Logout", + "portalDisplayRefreshMyRights": 1, + "portalDisplayRegister": 1, + "portalErrorOnExpiredSession": 1, + "portalFavicon": "common/favicon.ico", + "portalForceAuthnInterval": 5, + "portalMainLogo": "common/logos/logo_llng_400px.png", + "portalPingInterval": 60000, + "portalRequireOldPassword": 1, + "portalSkin": "bootstrap", + "portalSkinBackground": "1280px-Cedar_Breaks_National_Monument_partially.jpg", + "portalUserAttr": "_user", + "proxyAuthServiceChoiceParam": "lmAuth", + "proxyAuthnLevel": 2, + "radius2fActivation": 0, + "radius2fTimeout": 20, + "radiusAuthnLevel": 3, + "radiusExportedVars": {}, + "randomPasswordRegexp": "[A-Z]{3}[a-z]{5}.\\d{2}", + "redirectFormMethod": "get", + "registerDB": "Null", + "registerTimeout": 0, + "registerUrl": "https://auth.example.com/register", + "reloadTimeout": 5, + "reloadUrls": { + "localhost": "https://reload.example.com/reload" + }, + "rememberAuthChoiceRule": 0, + "rememberCookieName": "llngrememberauthchoice", + "rememberCookieTimeout": 31536000, + "rememberTimer": 5, + "remoteGlobalStorage": "Lemonldap::NG::Common::Apache::Session::SOAP", + "remoteGlobalStorageOptions": { + "ns": "https://auth.example.com/Lemonldap/NG/Common/PSGI/SOAPService", + "proxy": "https://auth.example.com/sessions" + }, + "requireToken": 1, + "rest2fActivation": 0, + "restAuthnLevel": 2, + "restClockTolerance": 15, + "sameSite": "", + "samlAttributeAuthorityDescriptorAttributeServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/AA/SOAP;", + "samlAuthnContextMapKerberos": 4, + "samlAuthnContextMapPassword": 2, + "samlAuthnContextMapPasswordProtectedTransport": 3, + "samlAuthnContextMapTLSClient": 5, + "samlEntityID": "#PORTAL#/saml/metadata", + "samlIDPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", + "samlIDPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", + "samlIDPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleLogout;#PORTAL#/saml/singleLogoutReturn", + "samlIDPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/singleLogoutSOAP;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPArtifact": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/singleSignOnArtifact;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/singleSignOn;", + "samlIDPSSODescriptorSingleSignOnServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/singleSignOn;", + "samlIDPSSODescriptorWantAuthnRequestsSigned": 1, + "samlMetadataForceUTF8": 1, + "samlNameIDFormatMapEmail": "mail", + "samlNameIDFormatMapKerberos": "uid", + "samlNameIDFormatMapWindows": "uid", + "samlNameIDFormatMapX509": "mail", + "samlOrganizationDisplayName": "Example", + "samlOrganizationName": "Example", + "samlOrganizationURL": "https://www.example.com", + "samlOverrideIDPEntityID": "", + "samlRelayStateTimeout": 600, + "samlSPSSODescriptorArtifactResolutionServiceArtifact": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/artifact", + "samlSPSSODescriptorAssertionConsumerServiceHTTPArtifact": "0;1;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact;#PORTAL#/saml/proxySingleSignOnArtifact", + "samlSPSSODescriptorAssertionConsumerServiceHTTPPost": "1;0;urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleSignOnPost", + "samlSPSSODescriptorAuthnRequestsSigned": 1, + "samlSPSSODescriptorSingleLogoutServiceHTTPPost": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", + "samlSPSSODescriptorSingleLogoutServiceHTTPRedirect": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect;#PORTAL#/saml/proxySingleLogout;#PORTAL#/saml/proxySingleLogoutReturn", + "samlSPSSODescriptorSingleLogoutServiceSOAP": "urn:oasis:names:tc:SAML:2.0:bindings:SOAP;#PORTAL#/saml/proxySingleLogoutSOAP;", + "samlSPSSODescriptorWantAssertionsSigned": 1, + "samlServiceSignatureMethod": "RSA_SHA256", + "scrollTop": 400, + "securedCookie": 0, + "sessionDataToRemember": {}, + "sfEngine": "::2F::Engines::Default", + "sfManagerRule": 1, + "sfRemovedMsgRule": 0, + "sfRemovedNotifMsg": "_removedSF_ expired second factor(s) has/have been removed (_nameSF_)!", + "sfRemovedNotifRef": "RemoveSF", + "sfRemovedNotifTitle": "Second factor notification", + "sfRequired": 0, + "showLanguages": 1, + "singleIP": 0, + "singleSession": 0, + "singleUserByIP": 0, + "slaveAuthnLevel": 2, + "slaveExportedVars": {}, + "soapProxyUrn": "urn:Lemonldap/NG/Common/PSGI/SOAPService", + "stayConnected": 0, + "stayConnectedCookieName": "llngconnection", + "stayConnectedTimeout": 2592000, + "successLoginNumber": 5, + "timeout": 72000, + "timeoutActivity": 0, + "timeoutActivityInterval": 60, + "totp2fActivation": 0, + "totp2fDigits": 6, + "totp2fInterval": 30, + "totp2fRange": 1, + "totp2fSelfRegistration": 0, + "totp2fUserCanRemoveKey": 1, + "twitterAuthnLevel": 1, + "twitterUserField": "screen_name", + "u2fActivation": 0, + "u2fSelfRegistration": 0, + "u2fUserCanRemoveKey": 1, + "upgradeSession": 1, + "useRedirectOnError": 1, + "useSafeJail": 1, + "userControl": "^[\\w\\.\\-@]+$", + "userDB": "Same", + "utotp2fActivation": 0, + "viewerHiddenKeys": "samlIDPMetaDataNodes, samlSPMetaDataNodes", + "webIDAuthnLevel": 1, + "webIDExportedVars": {}, + "webauthn2fActivation": 0, + "webauthn2fSelfRegistration": 0, + "webauthn2fUserCanRemoveKey": 1, + "webauthn2fUserVerification": "preferred", + "whatToTrace": "_whatToTrace", + "yubikey2fActivation": 0, + "yubikey2fPublicIDSize": 12, + "yubikey2fSelfRegistration": 0, + "yubikey2fUserCanRemoveKey": 1 + } + \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/__testData__/llng/ssl.conf b/packages/tom-server/src/search-engine-api/__testData__/llng/ssl.conf new file mode 100644 index 00000000..85afa7cd --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/llng/ssl.conf @@ -0,0 +1,12 @@ +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + ssl_certificate /etc/nginx/ssl/auth.example.com.crt; + ssl_certificate_key /etc/nginx/ssl/auth.example.com.key; + server_name _; + location / { + proxy_pass http://auth.example.com:80/; + proxy_redirect off; + proxy_set_header Host $host; + } +} \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/9da13359.0 b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/9da13359.0 new file mode 120000 index 00000000..e375f5ab --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/9da13359.0 @@ -0,0 +1 @@ +ca.pem \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/auth.example.com.crt b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/auth.example.com.crt new file mode 100644 index 00000000..66306eae --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/auth.example.com.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFJDCCAwwCAf8wDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDAgFw0yNDAxMjUxMDU1NTdaGA8yMTI0MDEwMTEwNTU1N1owaTELMAkGA1UEBhMC +RlIxDzANBgNVBAgMBkNlbnRyZTEOMAwGA1UEBwwFUGFyaXMxETAPBgNVBAoMCExp +bmFnb3JhMQswCQYDVQQLDAJJVDEZMBcGA1UEAwwQYXV0aC5leGFtcGxlLmNvbTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM8xy/VQsmnrzLGPqvf69XLf +HO3BwnIvwhegN+rXEF8bl3ahL0dusJ1E+i38X3RZtRgC5/59RmfgwRSAAr8BhfcF +UJoReKJ5t5pJMf8ef9V1PVmJRxpwN6RsgDO0Aq/8NIvfrclGXsV1FeyFxjBezVYV +pKp+b+JZ4DFjNtZvL7Z1MRQZYr4nQ0Sk/JP5A1XbgyKBnOP60aZwqKp6vdNfrwrF +6EoF1jVJfZvGCO6rkF4P+1/CkHSOElrtpou+zFvEZHX4uJInW5ArckHfhSQLXGqO +sJGOuyvL2ZI0Lgk0JRWrUbVuS3LWKalDgHwn1AdMfPQc3pz+YEHgd6vNNrMBpLZI +6Hjcblq0Z2bVbn7RO44iwYr92Pfy9JYsk0O9Ks8UWfuHIrH85Pgtfq5leL+05jDv +fiZpLdS1DMy7g1DKt0YvjBW8C6lkkhYt6bsoK/+1kCMgZ7WpWJI1Jgmjjgymh/0B +HNAKlhcz3lIrEBixrPTvsiac7Yp8TNbj3IdbUUEsG82A0zCwAJ2frTdj/dCZWr9N +e2Xk6J8heTAbfJLeAk76Q/TNVzhACcdw5Iy2SJYGxyOUWQ11R/Kjtd9SVwz2iU58 +mJNBQYjLcJcAYBi4IpxTkZyGZcnCEZ2gmxKvTKR9ulQEm8AKjP8/F2AKor8vSUKD +u3WywWl4fmH8NOpto4E3AgMBAAEwDQYJKoZIhvcNAQELBQADggIBAJdSoTQLTfOB +nUS5QpBlBQMvNfUi6Y2dqiQjb5kmNxurBe7tyIcHB3CvTw0Iof0xVEpls/MEZQGK +rcZXtG617m4E82mdIBKPpswSNZ2u+MNt2s1k8Cz7kY++J55nIVzPALV0n+z41LOJ +1CyQcKSf+Olr6oas8OlWkrJip4ZK6JB0WeKIaW45gq/GUXsEXlG7nBaCCaVptsha +Zgosrt8b2ivJOpWL2YZ4Z/yD9Q7H0AEA8c6ks7mobdBOsFxLqG8S+hw+kyGUtKTM +yHhf0enQRYCtnBHls29/TZLUET9zMDuMxFQa4BlwU3jlqjieOAuF2cZhPuWU1ae+ +sjy4fLLJ6NBEN+DPQu6avY0kpao8ltLRHheewqUG/fxtiMzmptyiXTlloS/xbFfS +Y1pbXlNm+znzcSGH1YPOXkfShyhv64DPphz4AkdLCEtSKhYKEuPlgjcElfX89/bm +8fEjZ833w2CHPORWtQbgNFZYpBLkDrnugJY0f4Wr/TCFBSe/h4qgLJv5WQ+u5+sJ +lFDdiE90SEb0x+ZuAe895O4s93g34HRRTssouue392kIybjPkOq10G9eZY4pyN72 +zqz8UZxmSTrMoFve0sTE2yXGa7V7EpHvR5qMtlcn7dy7AU75Px4onIy1l4THuqdd +XErmy1mkvltLyuGv2nnJ3pXfPF2kapB8 +-----END CERTIFICATE----- diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/auth.example.com.key b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/auth.example.com.key new file mode 100644 index 00000000..dc2af169 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/auth.example.com.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDPMcv1ULJp68yx +j6r3+vVy3xztwcJyL8IXoDfq1xBfG5d2oS9HbrCdRPot/F90WbUYAuf+fUZn4MEU +gAK/AYX3BVCaEXiiebeaSTH/Hn/VdT1ZiUcacDekbIAztAKv/DSL363JRl7FdRXs +hcYwXs1WFaSqfm/iWeAxYzbWby+2dTEUGWK+J0NEpPyT+QNV24MigZzj+tGmcKiq +er3TX68KxehKBdY1SX2bxgjuq5BeD/tfwpB0jhJa7aaLvsxbxGR1+LiSJ1uQK3JB +34UkC1xqjrCRjrsry9mSNC4JNCUVq1G1bkty1impQ4B8J9QHTHz0HN6c/mBB4Her +zTazAaS2SOh43G5atGdm1W5+0TuOIsGK/dj38vSWLJNDvSrPFFn7hyKx/OT4LX6u +ZXi/tOYw734maS3UtQzMu4NQyrdGL4wVvAupZJIWLem7KCv/tZAjIGe1qViSNSYJ +o44Mpof9ARzQCpYXM95SKxAYsaz077ImnO2KfEzW49yHW1FBLBvNgNMwsACdn603 +Y/3QmVq/TXtl5OifIXkwG3yS3gJO+kP0zVc4QAnHcOSMtkiWBscjlFkNdUfyo7Xf +UlcM9olOfJiTQUGIy3CXAGAYuCKcU5GchmXJwhGdoJsSr0ykfbpUBJvACoz/Pxdg +CqK/L0lCg7t1ssFpeH5h/DTqbaOBNwIDAQABAoICACYEVBEiCmqG+psF6m/v20OF +jrBNYhlDjBB7tGbhqT5aOLNqpdssgzmII4N2kCkwIJtURS8b22RKCANz7Y0QgX0u +u3hZhlIBlV+42HSgKwKGrYgVOTevqXYA9pEGEYwq8ZVMqH2K7O68KhapARF1A6Ys ++HbUFkFpDkrhknlME2weGrA+bDDJ0Xzx7OpVwXfqfChDsf7e0cMBXuFQ/i2fm+WV +JKcYZRKH9oUzlAX+8tFfi1cpwwmv28xVWL7BdovMAEbpKSygDhvo7OELW0me0Ak5 +P0ql7s/9amF6M4w6xicwtSBeKXfbte851IRzZmMkdLTx6yLRReYwgqTCVawIvCjl +g7CMXOaina1Gjc57e2HihaOle304M73qht3Q4uqzkHih0OgH9ZW3yj9vq33li52t +tTdLV11jKZOLmXCMFAi4x0QwgDwUkjPGCxibZjX/nhYigyojftdsuat1E9P67Ivw +smB/gA2MxIPot4iBXxUFydRn3uVrwDFSMXVHxd9i+COpXwml4JPueoAF9fYcoIp/ +2aq/cHMIkomkC0Q7i3uZM6doJeL5D4oIopElVakA1VuFdA/8LiTiKr/sM0Ym691u +zUmIzGyW4rReFXb/x2ilMW/l9OOltBUSO/8I3R4olFPcALnPQFbVJBnVBEMQUARM +Ai2FpzFFQVIumYk+dN7BAoIBAQDWo0Zqx8XuBLeo9zUI0nfF6QfxFr0lnsnLySwP +gPCccjQ0eahF9Bi7kALXzuP8HNYtOaP6jtrtAWFm7X1xC/dkUQDp4Vao3CC4/beS +4jN0EBHEwgLAoJLF6JxOMfiiTO+DH8Bw6tchmbA5L3uu0XeWEE1IVjtq2DdZsKX4 +MwW5jQ0N2fw9kK918hsk9zRZ/gH1CAmEjwwiJFKZOwLzFhm+ykbNzLb5pmAXahK3 +5jbD188qmO9/ubRQ/T38DN/IN2n2AtMvpJT2EDDtgmZj0olXuItDhuDLR+tiPB6w +oJUTuE97gi8YZvk+WArcmEV5ugn/msDDVQoJRYoD4FtTN5VXAoIBAQD3H1KW1T8j +wJefh/peamDP8bF1DPBnVyiISP0i4uk1hc1ysFJwzUjWvIIrWj8pb9nTbcR63pJZ +LCAXZqrY/Hc0UD5GiwxFVojFcIub9KIr04agUbMxa8jxp3wieRKS+73+cIHGk7Qf +FSpA1C0DCVZ1OdOsh4cLYAkEtyoFMBccTgUS7Ima0U9o7b8DDho+lPuAQepD4eCj +SX4O1EequJ8m10Y9a/7LwLuLMlqojRz6daJmDhbX8J5UbqC4g08BRaTbgz40Rb6a +vY3Nri2GA41yTE2AKo7mJUlIP2A1rfnKeqygVcEPOeJgUKbia6L58B1/91+iRzQv +3+55Y959bychAoIBAQCVd6Ij3fZhp/tVuMC/4gDyWzLimtkhB5CzTuZV7Y6hA46D +NG0QOcm3Y7P3IOX2vQYQ/GDKrQybmyh/CsceIB0pSJeARyGX+aL38AcUTF1UZ5RY +FlrgVXGgTDn84iOosjbgcw4KFB+4EFR9nildNhU29Sc8RoCeCO+Sj8ckLjPAYQ9E +JBbZsJXfZresaFGWkaI/RleKbise43h5qHSHX06SZD4mNnb9JvUnmQBr++8LNo/X +tCSkJ2gANjoh+b0kqiIp5RG3zb7GE8RewT4YKZbm9WZVoemM5gpuoDsm+MyXrPP8 +qE2vipXq6li2AXvwJrOrwdKWs/OHPVu9E1HFg6GFAoIBAQCbfG7Hjob6pMwByVnD +nCUr0UO5hRmhu9o53cq/74uSbIy207AbX16sFdHFGzRQixrAB/mu4Wmth7DtaGCo +xDjwhmiYlBZ1bhwCNmzxBHwhHSdAqgcYWlwFiD73pbwFFTYW6I0O95JGWFfMkHN9 +zJtEiMzhaiiTBKrH98MNnpN78K8KmB+AdKAFQkmDz5S9uZmAunh+m5luw+f3xqMN +DLq+goakUNXxN2QJEfauxJLuF6PFmKnQ1omYUD75uUy1XS98GljCJPvnesrFFgl4 +n7WYq9+7e4uLzPwN5CpRvBRFzOfevfYJ8X644SYPom/Z2LWG9YuLnEd+s+PlJuwv +egdhAoIBAAPLI8w3EhS3phQFnNg2gX/fegy1jTkNzyvAb0X95jYeXyGPtzx/d6Ue +lHSNtZQAMPkOYja/VcpjnMIFVopWtW45hU9FZLG8NWQJowYsxUSj9UmXXr1lhBnL +OaamMomO6DLlvv0ggOctmXlQWU51r2jIePTc3OUswswDOLf9yIj1+f6NsHRG0M9g +eC1A5LKhxXT8w8UC/W37iQ8kzOvgq46MzYq3A7+A+7f5RoQmc+T8gpeibEQIAbDf +4RRE1TulF3L8zGjRtPIT4DDNsFEaCLfKSbpvMZuWNGM28ssL797S3w/dpPanH5d/ +8MC5ePCOo8Jid8ttrGvS2XXoxriOVUw= +-----END PRIVATE KEY----- diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/ca.key b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/ca.key new file mode 100644 index 00000000..40710292 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/ca.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQDmNUbLZTioK/Ai +JwLwGmApw84B7hTHUgbu0XLNsMt+S7NKVDqAuWltk5uyMzEUacG3zNCqU1ZEC2dB +gIq31UgXPoi8woZynrCt0UENl0LYPRPUR71EdC/9uKBynFXNA9u/afopYC/PyZLW +hOfUcxIWh4nZaUYD9F3QwadTOFxuvt10O2uW47AtIv/VhJSm/0IW6gzGLaiBlUQE +B9rB3g8O9Lk/CGpsDm9YBZ/F/5AoUhcsMqAOpEi7TRk5yRlw1X57lYn3TfqbpIld +jv8DShLritKQ6o8NGM/9xuB8kWzb4gBGNNoTx8F7LHkJ7LU2ToR9BnysMQ6bC26J +1vhwulvkJQ4GDhmA6g+uKB8jBRwj59oP+XRLIwTqaxC+qjU95ISz4na/a91Nlpmb +GtqWODHr2qJfEQrPJJru95ixrcDVVj7W3u/9rpAyHU6hEFntcL2NAaPm24Bm2cbr +1XuSfpJZ4Nrx/7e/XK49jOy8yQo6ZLEtkM774KMHiJkXa2SXutwU69oMRwgOQ72P +1JNumYvpYVVn+0CX8G+GNwKASUDYMt+v0f8mi4+hOPU9FBMDiBVClAns8zbidto3 +BiU3jxbbFBRDLNpenW54LUuKVB4VuVlCe5prcpKH7AVtHk/jFbF4IHBuWtVF+38N +wnxCBIcW/YC7m05iO/bpvH3RVyCOpQIDAQABAoICACeBfZJynrg3gBhwTvAC6r1I +Ga6Zm62//SoXPgcf/6//EDfhf/+usfHIyseYQuQwqPqglq+gKRX4ygHC7CtTmfFJ +PUB9doKtin7twebx7hn7U0+S9x9L/B9jw38fppbOAnRVHMXkeJgFTOJtAPbjv3cn +z+eESixsD3x/ezZMHgqwTQNBHjvQ+58nWjWbcMI+3GnfxQzucXQ5eChj663o7Hch +1reDO3YrPP7jSjG5o5TTz5+5WV/h4AxqlPlmciv7q17MgRZ0Zp02rY8ldsxq9he/ +ZVbStfVmrGzt6ADgmQib/nWN5N3Poju/3E6wdUGqVFC7YAJR0eKYIeJcRprb1GBd +ivfQu1xLmlyoAxrqi8324FR358m4aKHkWIaYzM+oqP6jHMsT+a407UVeKL+zDSqA +86JoVwS7V2Ae5+V1F/ZJaFme/+10CN54VVHD8xcYbBsMPwrofS5XiRJYGs0rFmWg +wkGt6HfK2BTBCvkcywRkMmcsPMDebVwiVFXKbSFTTZWSx5VtxXGCNPhYvfiCgBdJ +czCc7EZez2+viW3DXvY00tQlIl/6ylQvqvFqXyhC/p0lxSjfiOKQA0HTy8QmmZh9 +/KpRXlRwd98Vmr+cd+U7kt9nMMPyiVfAhACDmmJLcgRJI/2vbInxVcs0xWyfFa7C +JP9nv62E6Ljwj1TuVZqBAoIBAQD1akRU+vkDP425PBSOnvG/uCeRRPAO2oYjnyAp ++V7WgwcywS8FVCPFeKt4UPlB9s7Yk3Jyc5AoXmeOhNqCfG1w6KNR47QIQzfZ+Vh8 +3CPgH5Lc0ox6kt+Kk8gvGxUUhg4ino8CijAGjiRvr4QR6vyJa6mo/NfhJPU9rFPl +fTMIJs1rlyH6kPKMGZXomMsFajkmgfFD5kmJyROpvDi3W9FlvUPVN5pjQ+f5yMA/ +638Echk78GbLjQgg3UJdqAbW4aw0lNJy+KcjKgybhFPwHsg763m4TiDkDBA4g3dl +rxKORxvqz7BM65FxXK8xnKI8Mw0F7YIKoDdeeYKca8Fy6MUlAoIBAQDwIxpKrdio +I7tWh/yhhvdhffSrn5Nu7tVcoa1Px/TCUadnMkSLmRkvzXR3BPbfAVYJL3XUiHXc +v49dd+rV8fhF+GGC2XX/LfJvFjUXYODg9QgVU3sKo+iFgajzyxWdowzdZ9Lsd5sz +JJMGfp6KlcEQX+TS0ZvmLP89/bC7j8yHcHpWcCZ05Mroii4K0ISCTQVda1e2LWgu +zsVK9cwXA4sK9KLyirC6COIWelOWhL+PuJ95/zUvhCgVEDAJpDBRjmoMCLY5S4la +0HaHq36h948aPY63z4jYGOU/R0dFFBtqIW5F+efXhIfU+firdOxk20W3RxKxdiwU +u9ZM7D+qWCuBAoIBAE5ClAXRfsUVaDlwulF8yDTOIfgGVtM1xl7nqJcaCa84W3xI +9JirazjWsT+N+t6ZOP8BjhaHWao16KofHZtM2I2P8jzz8v5LiSz+gcRXYy1ehDPd +BKU13wlO9SBob4F6+lj53Tr/HC+K9n2TJ/eayut7pL/Z2XHXmkkPgjWFhleMICe2 +K0S/IkmhAxgIWX2hkRYBjBGOB1dkAtw2xJNcOVtLTq1YrOgIyJnz9bKsg3XEeN2P +XQh+MeBhDn/VTFEL6CFgb/fv6USibSDOwwGon0vUXJ10dLKkUivjaJjJio5KiNGJ +Z3wwBtJyrv+QJoAx+24vfi+rRdzfvNHq3uao6e0CggEAd2odUvGsgcBzEo7BNFn3 +fsWx+/54xHuEInJLyxa2QkN0qb63k2vouHrE5cLUOQVjEWJGiA/r/IBN/L77SrTv +L2xaoUUehm0E1/UFJcEJUxTGlkRTNXFY2bsml0VwVFmWtitBGlJIHWCctGgW0vex +cEEfey69BfNuYhdb4YmavedTDtTqasqzlHvSdZJHsrw2ZMRSc8eUvWIZfjNI8FDU +vff1aANL6tcsBt2B36HX2NKIi5Q7kIt5my/Xk5PQa14UojNa2pcTkNOFfeXsLQL8 +aKIf7IwJktyec58wc8uR7m79dVLW1beUDHbaD/ku7OCVhJSVWSZYuV7HLK1243DB +AQKCAQAMZBmfFe954w5cBeVua95VHhiL6PNWY2f49KLxlQHP4WtMnzdEJiC3bkLp +Kc2yp5V1qyrAEDWrfNKnozhieDvOV+QNfVW4yxMTpbQ3t8k8yfNqwJafwzgkImqm +wolsIb77RKIABbkj0usVRBxvvn/2+7kuAL/r+dsNoiTsDXq58LmhjOy5s5A6VWvd +7VezYrMTNGtXzDxKa4WCPufYxv0xeRNcNJBIwYek84CrJXDP1LblfE0+HQcBjtWK +QOX2nSquLE2lNiPL9w6OnO8xXaeYp6FPyyE0D5zzpPvfvLwWrg35FOEkLMkIRmDL +ihycX8ry3MlIyX2sgTBP8dmsxMRU +-----END PRIVATE KEY----- diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/ca.pem b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/ca.pem new file mode 100644 index 00000000..7e4d2778 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/ca.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFbTCCA1WgAwIBAgIUGhSA3BDdpHF/Fcq4Ukxpc2sXoAgwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAgFw0yNDAxMjUxMDU1NDlaGA8yMTI0 +MDEwMTEwNTU0OVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUx +ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAOY1RstlOKgr8CInAvAaYCnDzgHuFMdSBu7Rcs2w +y35Ls0pUOoC5aW2Tm7IzMRRpwbfM0KpTVkQLZ0GAirfVSBc+iLzChnKesK3RQQ2X +Qtg9E9RHvUR0L/24oHKcVc0D279p+ilgL8/JktaE59RzEhaHidlpRgP0XdDBp1M4 +XG6+3XQ7a5bjsC0i/9WElKb/QhbqDMYtqIGVRAQH2sHeDw70uT8IamwOb1gFn8X/ +kChSFywyoA6kSLtNGTnJGXDVfnuVifdN+pukiV2O/wNKEuuK0pDqjw0Yz/3G4HyR +bNviAEY02hPHwXsseQnstTZOhH0GfKwxDpsLbonW+HC6W+QlDgYOGYDqD64oHyMF +HCPn2g/5dEsjBOprEL6qNT3khLPidr9r3U2WmZsa2pY4Mevaol8RCs8kmu73mLGt +wNVWPtbe7/2ukDIdTqEQWe1wvY0Bo+bbgGbZxuvVe5J+klng2vH/t79crj2M7LzJ +CjpksS2QzvvgoweImRdrZJe63BTr2gxHCA5DvY/Uk26Zi+lhVWf7QJfwb4Y3AoBJ +QNgy36/R/yaLj6E49T0UEwOIFUKUCezzNuJ22jcGJTePFtsUFEMs2l6dbngtS4pU +HhW5WUJ7mmtykofsBW0eT+MVsXggcG5a1UX7fw3CfEIEhxb9gLubTmI79um8fdFX +II6lAgMBAAGjUzBRMB0GA1UdDgQWBBRsZ19BHWcC/szX/TM7jRYUqtrhYzAfBgNV +HSMEGDAWgBRsZ19BHWcC/szX/TM7jRYUqtrhYzAPBgNVHRMBAf8EBTADAQH/MA0G +CSqGSIb3DQEBCwUAA4ICAQDRY1b3E1lW/hAw9z1Ok+ukB/1ObB84MIxOP8Ie0JsX +xGzMyysdREsv9wqZRIx78JX64pXJ9lsdnir6lM6yO7Ur3BkJm08qpbrtEOv2O3x4 +AHv+fyTvfKxOcqFNONw3D0zqwUGACZfAuf65onK04yQCXfa3a1HnLxW3NAPzbAxX +I4h0w0c1sayCHh+g3m8yxe97Hi3X/Wzst4ZzXkt7sy6TenD2+fKLZbKc6FKHugbT +PBSnys4AtyfnxyUZHOrBboKozo9I35NdypYKVPFQT/CbWWfqY3/js8BkBXvnSCy9 +Eo0zokI/bvmj5ooJTigx6uBM5eV1wUNVKREyac1JnthTPCzVzIFm5HkPFvQ7oYrW +PxzQjW+kPI+G2QHMGka+HBI3ITdc0xG7DJquvB+DF3QepT+S7JlnjfQjQeE7SR/7 +HFmHWsEUGGeaqa7NJ8Sysw0VdNDwEgGJLEgRJVyVWLMnz9BTWm04X8o5o4t3mNSJ +bPUnx3M7QGeNTPdYO1TV5Y2lGm7m4piPS5QBl22np/UpAoDk5e4FfDC9JOd2qbZH +giDobUWZYThyvvqCww9xLD0cEbXrIbuDl2PCkloWg1zLxjFOBN+FTzbOLQ4OiV3r +04qrprxl98Gg6vIjO+S0s9EJmgQxxPlhyDUxhFw+v17SqFWlLh6yUObOfu3tnK5m +lA== +-----END CERTIFICATE----- diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/matrix.example.com.crt b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/matrix.example.com.crt new file mode 100644 index 00000000..ac592f62 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/matrix.example.com.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFJjCCAw4CAf8wDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDAgFw0yNDAxMjUxMDU1NDlaGA8yMTI0MDEwMTEwNTU0OVowazELMAkGA1UEBhMC +RlIxDzANBgNVBAgMBkNlbnRyZTEOMAwGA1UEBwwFUGFyaXMxETAPBgNVBAoMCExp +bmFnb3JhMQswCQYDVQQLDAJJVDEbMBkGA1UEAwwSbWF0cml4LmV4YW1wbGUuY29t +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA67bFDFK2lQ/UxB5sG0QU +PrRUhNU6oDm06kmNcPC2++aZ32kbg1oySYkpzGozwWssRvcJlNzniql+vQL/diO3 +H3upTqcBfYv6oidY/BY12ZgIsJ5FWRpLQlY62LUSAzNWzqaI1TyalTUN9r1fMTnm +Qh4ir+DECs0YPXUCsxd4dLbLSgt2JkgDl9/Vwvb0nHmwi5ALJHIT3lYLQG0akJ0p +7vv8CyN5G7l7rFM0Q/fsJ+CQaTweZWhFlqQKWcA6nZpM5xmCqGD1KkhYRvuowJvL +KlmutPHvgzx9ZIoeO6bz/9lH4ql3Qu2qWFQhGVQ0EJUR+b2i5NB5/EAc46BVmNBU +bQa9heztZEMTEtsa/U7d45frT7dv602RHvJv3rttJFicoy0rJCikC4M20Ju6dj+p +XS1fMTY8neWLvOeRru2K3ijxD5kvL1t6tApfV3cpkodp6LaGXcsfVAZntM00sSmp +KbIDJnipXtMJIMmXtthDF0Ov8J5hpXzmUca6Gm7Y/b1yQfyS2zzZK5+D1+W1shiR +k1Lp0MuDBrlXweGe4+4ukik4vjO3crLXSzxIeifYDLG+U45WZgAOpQBVF5g3HIa9 +fsd3KOSW1i1zhDJDseSy8AMszIA9CmTAcXvI80bJ0e9g1T8ndF6DhepE/h1mDHEz +Ah9PgQz/ZRexz7DeNdR3G/kCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAkzp0fU2A +G0+QElVk10D1NIsxgyV3TDtRl3gQ7nlM9gRc3y+1YDimmgeCDpFrfyqCPHhqxa0+ +nhe5bmBLtK0kfIXTgfVhQAyosxXAzSXu2DwoQC4KeWU9aKP022T1WqPUcPWQUH9N +F7WIg5XMwrghqi2n0XQNLMn5U3VYWkVoHTIULQdOjfhT/5D/0lcr6yKzAwDuEaiN +7tpdmcmDugdyVoYPsh3fhSfjqSYC7pH6x0o1sYcXVEkALOMrbL7HcP7NW0dXxIkp +qu2E2doPzi2ixClWCHMYQQOQgPNJ5Trwjhwe+0Vm1L47DSu4vE4IPX+vRblW9a90 +l2Yj0DOUuj3nGPzs264h2buEMJKAj0iwS+S9N06QSawRRDy0UTb2irPIy/D2cHZx +powB7Ik2DX2WQvTQCcrrk2/T5xYJ4Zn+g8vAd3DpEEWMt4LRdQFIpFTF63R5KYz5 +2xSYwZJ4rCBXgB70IyeuYqJrKnCHSS3xvjw8jmqh4VKyphs7jx4DjeZzhSdQfHqW +TjJKP5VeeEkeZnN1oE5m6L1rqFdqX9egRl7M3+nKe5KSduM9ghSb9Z/PHYO4cofY +zOwTSX1GNtRXwGQQ54NCB4fZCssswNIR0CKbiE5MlsGosY+O3ktqeU5CxumwJRkS +Sw8AqpIlLxEHKRS/JgX0FpqEoNHoqrAIMbY= +-----END CERTIFICATE----- diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/matrix.example.com.key b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/matrix.example.com.key new file mode 100644 index 00000000..a4f11057 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/matrix.example.com.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDrtsUMUraVD9TE +HmwbRBQ+tFSE1TqgObTqSY1w8Lb75pnfaRuDWjJJiSnMajPBayxG9wmU3OeKqX69 +Av92I7cfe6lOpwF9i/qiJ1j8FjXZmAiwnkVZGktCVjrYtRIDM1bOpojVPJqVNQ32 +vV8xOeZCHiKv4MQKzRg9dQKzF3h0tstKC3YmSAOX39XC9vScebCLkAskchPeVgtA +bRqQnSnu+/wLI3kbuXusUzRD9+wn4JBpPB5laEWWpApZwDqdmkznGYKoYPUqSFhG ++6jAm8sqWa608e+DPH1kih47pvP/2UfiqXdC7apYVCEZVDQQlRH5vaLk0Hn8QBzj +oFWY0FRtBr2F7O1kQxMS2xr9Tt3jl+tPt2/rTZEe8m/eu20kWJyjLSskKKQLgzbQ +m7p2P6ldLV8xNjyd5Yu855Gu7YreKPEPmS8vW3q0Cl9XdymSh2notoZdyx9UBme0 +zTSxKakpsgMmeKle0wkgyZe22EMXQ6/wnmGlfOZRxroabtj9vXJB/JLbPNkrn4PX +5bWyGJGTUunQy4MGuVfB4Z7j7i6SKTi+M7dystdLPEh6J9gMsb5TjlZmAA6lAFUX +mDcchr1+x3co5JbWLXOEMkOx5LLwAyzMgD0KZMBxe8jzRsnR72DVPyd0XoOF6kT+ +HWYMcTMCH0+BDP9lF7HPsN411Hcb+QIDAQABAoICAFOHzveJfkN/uzYO09+rtgLs +k8EI8UAjgw29p/58h1PoSeImlMXtGkH99g6HGjUyXhv94mrbB8CXRR8FJ3N9v6DM +CVkijMApcVWyXPHkiwvDuVyhkdC8JSxqc2sla68vq9UKphXu5pb2mK62ODwxGPyY +QlGSdNahDLSGuUCvEhRGTO899Y4mWgOholZ3foLPCvXCQ3iUZp8VXeJkZ5QU5e3X +ZV+rH/lnt0B/sddeTdVp2rM4R0tHctWp5zMcEImWSydgXnF9/pOP1Jy/BPPQoeQt +qOBdljOrJYKSAZnBFdm2baeQx38zyviHQ71+nf68XQTkI4wzpu7x52rxADRpq2jT +Ximc6QzZ0p0D3fuyNv1hLfrVvVXUv8aTXWBZrt+VfAsRcO6ejzmT4mDIaAaueI8N +F4pzD1VBQc07/cdcPGHboj0Ye6mSXEgiNFpKre6RHaxYGFatnvq/ZcCUZcyHNWz2 +gNNg/aIFkPkab0GUNlcA1zSiP47Hih0b7B8BNAK/J0PBO++Tv/zzkP3HKx4Dsa9g +2NNeO7P49GImxyYEwHg9POiNLzvitfC2dKGu2omIQTeIKXWPljxuEooVT0hkrjGq +xnoQj7sOdt3JCCfxjDxhoxhvga7X1wHhx/v73NMC81kNYjUFYBChX66EiujNSW2R +V4/uuC4frmUFGmgBiHrbAoIBAQD8Y/8bjky2J0HV1L0GKOAvDtbooSaH2p04NawC +0uhbZEVXYh4Xc6Kdw5ixgbb/RD7KxkdDQtgBbi0SzLgVk3YczwhARfVxc4n39RaF +bOdBn6fiD3+pD4hrjA4XgxP+0IqjtTx70YqhRNntXn9jd71qrzzJRNu40yNNW5NC +4j3kgTasuAm2bAaS9Ah63rcwxlj+kfI9+xSPhyXaCo709vGsCE1uQUZtUitXEDSw +8okc7nzxrB4cBY7+ochtOtzpF3NkfnSF7yVXkdICP7zpf2m65NFdmMypcKCs9ilw +cwDIL8KLLEbfnyqOPL4RtKY8OjfD+Y3t7tVzuWOL6sDF3dKfAoIBAQDvFbhGy8Y7 +GVe1fE/ba0fN8p9FBpqkX4cFupQsY7OOM8T524T06WYqvBEIwzcNp3nYc9leofYq +JJma6975NtNIbqxq3Umy2WiZYmfMqMDo8Sc+Muznc5YpeiB6OK15v4l1GD1VWdI1 +RaOV3cKMXhnoXcTDmb4m++v00/uTK6pEtjdeyHkXoSjHmn46EgLLW7V+KstpOf15 +/yK/vupYWwbgvvOfWj2DC8IohYBZP4iFlinlegU2jJlsTuUbl+qWBRRgRltTqXmk +Vx6gOogEqZMPKTti50c426vI1X3aXVU4YiQmUA9nA/4L2qkwqhTwWLq3yGs78Xvi +MxyBG7j1WeJnAoIBAQCaSocp0VQUBuu4TNVBbrueCPRYQivL4Vk7g5QkJcrmE+ZQ +BStgKtC+oVQ3L5UveAjq7UujUrm6JiBn3b6rcfpCok3o/NuO/5LYgnvCFVFKTM/U +4qSoNVawaG408WzH2bTnX2QaTX7yF6Uh9yLpK8of7gC7Cd1In8p1AAaGXMh5aISE +Ef3eByv9qjGE66IRry+4cIAmY9et5nC9WrcKCeyzvl+Xh1AGhLT6BG4xvhMUHLdF +BnNhrgQ8paphHBrwY+WnCacyOYAaiIpZ1Z0nIT0Bg+B5129GJhQTqGis1aEkwA2u +BuNM0YCyc2++YzE8oFp285hQXDEhDbRNVLWEQJcBAoIBAF5J3JDfEGCCUBrc2cmY +94p7IuDgB+DHY8KYoJMZBtkQBaDcOAU2fvpfjQA9rNqPr/fzSEiP6zsXkBSO7TKv +soegThMfDk+geiXzrygBbYLwiB95igCFjzTwWxqYe6HGLfmmA5pDgClOO4OBH5ao +DeOcB1t0qI9LTvURHOgfklji29dfjJILFsARZ7KTI9L7agpF6k6ndhXEzvl724PY +8k90PzQbLKMf4gSFEecgrUCxxfggNSocLO2P9774HKXpfu2xEZdfAQAU85kRPE9K +aRrTkf4hY+9Cgu8Dc0zI/jDsU4FglZJ0+p3GMG9mxDc9ZvXP7qqHQ+ojahxoyHrK +ZgUCggEBANGK4AeN2ijihqPXSxrcqyef0f6z8jz+YE5AtaamySDQOUgarq2v5Tau +GL5EMTQqirzqBzia271jjQ/GCwH8NaNkPpRbSbhERCkGd9Wk1eIBfEozDevddLI4 +gpJW0N20e4IkrtQa1XtQTY0RmGalANS8PJY5gMyOvfagNueTDfR4Ma96FmaT0r0r +a4ULegcUMDYFhyQNRJxkvmzl0iu3qJjjfdSnE3DKf9kf43JgMCuseNOYo6dicDGy +W+/3m1LneLp6DdgcFHrQ79bh5/+8UulX69nzsuVPbnIlPrjpNc92HPYHlZ6EEQuW +VQtGdt2o2wkAwabZieczo+3cIUj3di4= +-----END PRIVATE KEY----- diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/opensearch.example.com.crt b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/opensearch.example.com.crt new file mode 100644 index 00000000..ad86671e --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/opensearch.example.com.crt @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFKjCCAxICAf8wDQYJKoZIhvcNAQELBQAwRTELMAkGA1UEBhMCQVUxEzARBgNV +BAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0 +ZDAgFw0yNDAyMDcxMzMzMzlaGA8yMTI0MDExNDEzMzMzOVowbzELMAkGA1UEBhMC +RlIxDzANBgNVBAgMBkNlbnRyZTEOMAwGA1UEBwwFUGFyaXMxETAPBgNVBAoMCExp +bmFnb3JhMQswCQYDVQQLDAJJVDEfMB0GA1UEAwwWb3BlbnNlYXJjaC5leGFtcGxl +LmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK45xyC8GVxUaQkd +MIBqHpKelZjYw8xw8s39KE2/bTvHdl5xgNDJj7lbgmYmRQkLFUb5kkB0tFw9jiHk +kpOvKDoonMsM3yJJRgNUXxUFwgn01U/XgyQYkbLzuOGkL5d7o/fep6x1+/GkAn/a +DfCBlVfISOZ6UzDb5SuRFlUXqktfhLqTC24/E25+fBBtcgJBgD/O/GF80y9jD00D +JyHCLH2eEYvzOZ7v6JGfrfeJGM9O4gd8Lq0klXggMuXlsDoRAKRiONICooXBuYG8 +lE0uumLb5ekDeiVm4RdWq4rS2o2UoqQyHs3FE3jmwh9xejXL4rZQhcoLD9AGAsLR +6oUU+JkGUEUPqFsa5YF/jKV0bYM/5JblLxDEq2MRoXdb+N+qWpgPE9phUHQ9lYNf +/+GFjaJrsYswN0uTpDx5qoRE7U2bQmA8Q3NDcy6cbwY5ppB3Tfp3y8xZw2wSjG0P +Mg5pRruu1nSP1veUi6RLRfMuiGo2kUWCCi5C4wgzIuuGMLBiWbzhhf0GeOuj/0tp +QhPeAGYYmSXcGlvSnSypjmCm2Kb1MvWZqKayYaZharTE9FZ0Jp3vA9jqPo1/vep8 +dfRXM7GL3oThvc8K0xiZBORaA8RNgcUUMp8jynT1Ug72AeCnZr3VdcBI3Ww0zrBU +4PPeVpO8OEcGKiVEdxj1p/cBtU1lAgMBAAEwDQYJKoZIhvcNAQELBQADggIBADB2 +Iv4LjwMIuV4shH+VV9WRNLJAK/nVJbKYi02SG013UnQ3sczIVX3YVpkGQfK5pviD +qCTUBa+Nki0AcNxQgFvBfSzzuNRF8IHnGAT8ZGrst/D8ULhQ/FdFt04uHBFDeH0h +LdQ7sjHiN0roTwYdAmmSUUVKtQz8tXh4oKxkLFRID8zjOridKw19x6QTQPElcF16 +OC+yJLFSHANciid4ozuyTtmSkJScuvimwkQwaNsqyAO3MxP21DoKa8a/sPZAyAUz +1Jr61bgEdgs8syQol7SzKnPXbhY/y5Jx2Bh+a/SzaBd/qc0hiZIcdqEwHsXywFv9 +z6W4Wv1gn3xraPClTAUStWnC0uxoPjhKLb0x6zj3R44GRnsdhnz2bCBfGiRU02Fk +7vZuUHXA3mPaPvFTRs+hr6Jpeb1dbPm7N+hKrKm2l0vdzR9pVg8AdMI4xSMwEGdw +Q7inKudqRkpOdVzVK8daVL61ZRtRbJehPxZIrEkk4VvZaGej4+41odGqizQ573U5 +d15pMYhOo23y1SV72taHCJO166GkCBUTOdioJiLYbcrJ/SofbjK0S6O1UEelV4kJ +epnpevDrGII7Uhsqxxc4SqBHYM/BXOd/S3kb8QLPjPDTTHbthbYzdhKLoXadefhP +tkyLHyKvmCtEbzoK7suKKhAcmDytAsDxjDmcXqMO +-----END CERTIFICATE----- diff --git a/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/opensearch.example.com.key b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/opensearch.example.com.key new file mode 100644 index 00000000..02015b88 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/nginx/ssl/opensearch.example.com.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCuOccgvBlcVGkJ +HTCAah6SnpWY2MPMcPLN/ShNv207x3ZecYDQyY+5W4JmJkUJCxVG+ZJAdLRcPY4h +5JKTryg6KJzLDN8iSUYDVF8VBcIJ9NVP14MkGJGy87jhpC+Xe6P33qesdfvxpAJ/ +2g3wgZVXyEjmelMw2+UrkRZVF6pLX4S6kwtuPxNufnwQbXICQYA/zvxhfNMvYw9N +Aychwix9nhGL8zme7+iRn633iRjPTuIHfC6tJJV4IDLl5bA6EQCkYjjSAqKFwbmB +vJRNLrpi2+XpA3olZuEXVquK0tqNlKKkMh7NxRN45sIfcXo1y+K2UIXKCw/QBgLC +0eqFFPiZBlBFD6hbGuWBf4yldG2DP+SW5S8QxKtjEaF3W/jfqlqYDxPaYVB0PZWD +X//hhY2ia7GLMDdLk6Q8eaqERO1Nm0JgPENzQ3MunG8GOaaQd036d8vMWcNsEoxt +DzIOaUa7rtZ0j9b3lIukS0XzLohqNpFFggouQuMIMyLrhjCwYlm84YX9Bnjro/9L +aUIT3gBmGJkl3Bpb0p0sqY5gptim9TL1maimsmGmYWq0xPRWdCad7wPY6j6Nf73q +fHX0VzOxi96E4b3PCtMYmQTkWgPETYHFFDKfI8p09VIO9gHgp2a91XXASN1sNM6w +VODz3laTvDhHBiolRHcY9af3AbVNZQIDAQABAoICAApXX5xvzcmPMRTbaK+WnO3y +/8osw6J06dSUPDoxLJipxDri3dSGwkMsTVcm2l4pDEBEPAwbYUFAXhlg6dpeQTMC +ihv7TZtJYiB8d5BV4SiaIbc1gZE47B0FHmo2RqTlL9xcmPNBpYy4QXW5Sa6G4oht +WPZlOF7kDnxBhmPSnccPil9QrxMCJ3Mditumw2ei36vp600WDar4ZEYb88yrK9zg +7wWxkDAA6XsLUVYqCxDzC7OKCXM5gq24q4y9z3IC5Fjdg6XjhiYOU6aBvQO/zExl +5QWpsSxbKO0rtc7tqQ9STT0VxIJOOlOozsjzAWAEFBbiPK67bVrZoHxT3Wm8zuyd +PQIwS0KIB/UhAhM2Gb44cbEg+V8UrJS1T//mNFgxigrZvsiRB0DfpnXHs0RWoe0L +cmUTFn6p05olWnwReyd3j38ESOlWT3kt0q+v33iZwFJficobQ7nov5cngTyXa25q +KT4KYdfQ3aF94XnN0GK0n1P6M7bW+klg6p5pYpe2afz5V3fQuEkbCLmjfPRJML0+ +Q0zPvjp/gp4w5Mn4zTd0pKN/Ewx+HYHYcoC8JUnlgMZwx5W4K6xIqsHe3Uh5QJ1H +lwHZWXKhkkRmYvBoa1foxaC8be6OH0yg0FNyfctMnxJPnbDkr+t/Pwqg3Mw9/7ys +e8QXMIzV22gt6C4YzH0BAoIBAQDclXcC0Ge1O8t7NtcX+lLY0zGdFd2vSTavNexr +FTlnJco4vGTmkq/jm9q2yB5pZVCDoOnWn+/KLXrdVrbDTt37BDK+PRSGQQBpJw8p +QjgxUdVhMHA5/oQnsj8dkW1+Qx5O2I5jGn38xrAwu/m/A5TznkLcwo0OSKa+6HCb +XBL2wAD4MlepS8n52jVP5vTgK9uOPQe5Dnyc82RyeFVlJghKrZmu4FnLUb+LNBcH +H2QaonfVbWRsjv0YphwrcDFFl4nkJQ0Ci0AL6nnAuT+AtRplq+DeeJETXbfFA6Ma +sm9L6mJDo6otVKYOiQ72JkZ8NkdiXUwietvM/EHwARA90yRFAoIBAQDKMuEuT6uU +Ww7L7DuYAsPN3lr4su10K/ydE2jWjJ9tC7aN+Zf55ckpc5N1r4iLwONjKoZm6cTM +V5B0JFN/TQhjom8NoGPKsxHRGg/nQB5HxFR7BZmnFcZSu6ZvbhSRgubYcjzOHASJ +U95rKHcMIjdAcj84tseXxSIyjOsOkKCDT83bFS6heu4dUrbK3yZ7/GSzIEqv8UTW +TBaxS9HEDfJipoSbP5OBc6wBfkVh1AXG+r3i9sSIH6yMAnK79AlzMzgYG9MxPiRy +zm4aR8ZSi5T4czWMBnYKP4tMJZoiCjmF+XdtflfCxOl6eRoSS8frAwjQz33Jtqfx +V/VW+BxFGGahAoIBAQCL2GJsMU4ekzss8ZaqR/RwLGy+51b1QxhdOnWZagpLf6TW +FXJuz76dMXkW+oZ1UVsbKFA31owChJTpcIlMB1sqQf4dp8G0X89v2uh8wtO3SOdb +x4bO7bJBLHthNorRSqITYK3c3LXVJO4c53+tfwrW7JX9OYaN8LduPxTtGhGXyCCV +Oe1jkn4JXjMAZi8HVCbM5ZpY03tjUddzzyBskREerzLIsMmc4kXqberPhDJFxIzu +jXzmajzBfMZNL8K9GRa9wlOeMkQ3ib8I1SkSYz7KCI723D81pOvWBrlIOqne2kjU +ExXXyVvByVjn61oyc4MMNJQJJBTnv2HaVAJE//B1AoIBAFXkx0OlFH4xMFfwQmCQ +zBzoGD0NxVFUXjtbw21gz1jDYQluveCqfInfTwTvTFIR3oaByhZtt+wWRocP52hs +kOPCXOqs97dj2m25ZIgX9MUH4dtgxaT02wrKLCmp2ZL2yJmp7aqgvEyaFCHxTqEY +59+4qKKvApq2Y5CVzESjq4wcmpY2qVhvoDdUq9ICeZax4RU24oNbOqLOL9WhH7rp +Mc42bp6Eo2SafrcjrNWh+9JLMd74dQRecC4J3DN7t4f4ehvDtjN08obSqnL/ioAG +S4I/br/M/tfbppDyaEeNkGIZV2JsCVvzyjr8ttaO2p4668PIYOcPcMhVVSNcwqWX +eAECggEAHIHkDoEZewnOHcuge3+FzZXLkzK5ucT57Upz5LhRop9T++eHRQIV9c1H +EKIpti7CtR2tFKeSIfWyDcINjwR3uYVkQ+gUmqBIvGRi+QfNjjfUdIoXysaWySak +/HcbpMVpnSUgakp7mZJre5OlaBVx9IGiQJifGeHsa8OhlHbRKsK9O1hyfnJf6auQ +0lOPK+I6PJ5WpjjM5dImvEHBFYqH9ViRbCgK9loRSP1370WhUt1bJaTmxc4ETowQ +10t+rid4qh/SN1J+SA8QCf0i3y0pCrV3PpcIkJYTHTAOVb0XCV7Q8ddP2DMzA/GK +YF3wBZPszCouEMHLb0UKcIPkQ2RdMg== +-----END PRIVATE KEY----- diff --git a/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-data.json b/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-data.json new file mode 100644 index 00000000..a8563511 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-data.json @@ -0,0 +1,301 @@ +[ + { + "_index": "mailbox_v2", + "_source": { + "attachments": [{ + "contentDisposition": "attachment", + "fileExtension": "txt", + "fileName": "message1.txt", + "mediaType": "text/plain", + "textContent": "May the Force be with you!" + }], + "bcc": [{ + "address": "hsolo@example.com", + "domain": "example.com", + "name": "Han Solo" + }], + "cc": [{ + "address": "c3po@example.com", + "domain": "example.com", + "name": "C-3PO" + }], + "date": "2024-02-22T12:30:00Z", + "from": [{ + "address": "lskywalker@example.com", + "domain": "example.com", + "name": "Luke Skywalker" + }], + "hasAttachment": true, + "headers": [ + { "name": "Header1", "value": "Value1" }, + { "name": "Header2", "value": "Value2" } + ], + "htmlBody": "

May the Force be with you!

", + "isAnswered": false, + "isDeleted": false, + "isDraft": false, + "isFlagged": true, + "isRecent": true, + "isUnread": false, + "mailboxId": "mailbox1", + "mediaType": "text/plain", + "messageId": "message1", + "mimeMessageID": "mimeMessageID1", + "modSeq": 12345, + "saveDate": "2024-02-22T12:30:00Z", + "sentDate": "2024-02-22T12:30:00Z", + "size": 1024, + "subject": ["Star Wars Message 1"], + "subtype": "subtype1", + "textBody": "May the Force be with you!", + "threadId": "thread1", + "to": [{ + "address": "jbinks@example.com", + "domain": "example.com", + "name": "Jar Jar Binks" + }], + "uid": 123456, + "userFlags": ["Flag1", "Flag2"] + } + }, + { + "_index": "mailbox_v2", + "_source": { + "attachments": [{ + "contentDisposition": "attachment", + "fileExtension": "pdf", + "fileName": "attachment2.pdf", + "mediaType": "application/pdf", + "textContent": "The plans are in the droid." + }], + "bcc": [{ + "address": "myoda@example.com", + "domain": "example.com", + "name": "Master Yoda" + }], + "cc": [{ + "address": "lorgana@example.com", + "domain": "example.com", + "name": "Leia Organa" + }], + "date": "2024-02-23T14:45:00Z", + "from": [{ + "address": "okenobi@example.com", + "domain": "example.com", + "name": "Obi-Wan Kenobi" + }], + "hasAttachment": true, + "headers": [ + { "name": "Header3", "value": "Value3" }, + { "name": "Header4", "value": "Value4" } + ], + "htmlBody": "

The plans are in the droid.

", + "isAnswered": false, + "isDeleted": false, + "isDraft": true, + "isFlagged": false, + "isRecent": false, + "isUnread": true, + "mailboxId": "mailbox2", + "mediaType": "application/pdf", + "messageId": "message2", + "mimeMessageID": "mimeMessageID2", + "modSeq": 54321, + "saveDate": "2024-02-23T14:45:00Z", + "sentDate": "2024-02-23T14:45:00Z", + "size": 2048, + "subject": ["Star Wars Message 2"], + "subtype": "subtype2", + "textBody": "The plans are in the droid.", + "threadId": "thread2", + "to": [{ + "address": "lskywalker@example.com", + "domain": "example.com", + "name": "Luke Skywalker" + }], + "uid": 654321, + "userFlags": ["Flag3"] + } + }, + { + "_index": "mailbox_v2", + "_source": { + "attachments": [{ + "contentDisposition": "attachment", + "fileExtension": "jpg", + "fileName": "image1.jpg", + "mediaType": "image/jpeg", + "textContent": "A beautiful galaxy far, far away." + }], + "bcc": [{ + "address": "okenobi@example.com", + "domain": "example.com", + "name": "Obi-Wan Kenobi" + }], + "cc": [{ + "address": "pamidala@example.com", + "domain": "example.com", + "name": "Padme Amidala" + }], + "date": "2024-02-24T10:15:00Z", + "from": [{ + "address": "dmaul@example.com", + "domain": "example.com", + "name": "Dark Maul" + }], + "hasAttachment": true, + "headers": [ + { "name": "Header5", "value": "Value5" }, + { "name": "Header6", "value": "Value6" } + ], + "htmlBody": "

A beautiful galaxy far, far away.

", + "isAnswered": true, + "isDeleted": false, + "isDraft": false, + "isFlagged": true, + "isRecent": true, + "isUnread": false, + "mailboxId": "mailbox3", + "mediaType": "image/jpeg", + "messageId": "message3", + "mimeMessageID": "mimeMessageID3", + "modSeq": 98765, + "saveDate": "2024-02-24T10:15:00Z", + "sentDate": "2024-02-24T10:15:00Z", + "size": 4096, + "subject": ["Star Wars Message 3"], + "subtype": "subtype3", + "textBody": "A beautiful galaxy far, far away.", + "threadId": "thread3", + "to": [{ + "address": "kren@example.com", + "domain": "example.com", + "name": "Kylo Ren" + }], + "uid": 987654, + "userFlags": ["Flag4", "Flag5"] + } + }, + { + "_index": "mailbox_v2", + "_source": { + "attachments": [{ + "contentDisposition": "inline", + "fileExtension": "png", + "fileName": "image2.png", + "mediaType": "image/png", + "textContent": "May the pixels be with you." + }], + "bcc": [{ + "address": "chewbacca@example.com", + "domain": "example.com", + "name": "Chewbacca" + }, { + "address": "jbinks@example.com", + "domain": "example.com", + "name": "Jar Jar Binks" + }], + "cc": [{ + "address": "qjinn@example.com", + "domain": "example.com", + "name": "Qui-Gon Jinn" + }], + "date": "2024-02-25T08:45:00Z", + "from": [{ + "address": "lskywalker@example.com", + "domain": "example.com", + "name": "Luke Skywalker" + }], + "hasAttachment": true, + "headers": [ + { "name": "Header7", "value": "Value7" }, + { "name": "Header8", "value": "Value8" } + ], + "htmlBody": "

May the pixels be with you.

", + "isAnswered": false, + "isDeleted": true, + "isDraft": false, + "isFlagged": false, + "isRecent": true, + "isUnread": false, + "mailboxId": "mailbox4", + "mediaType": "image/png", + "messageId": "message4", + "mimeMessageID": "mimeMessageID4", + "modSeq": 13579, + "saveDate": "2024-02-25T08:45:00Z", + "sentDate": "2024-02-25T08:45:00Z", + "size": 8192, + "subject": ["Star Wars Message 4"], + "subtype": "subtype4", + "textBody": "May the pixels be with you.", + "threadId": "thread4", + "to": [{ + "address": "hsolo@example.com", + "domain": "example.com", + "name": "Han Solo" + }], + "uid": 1234567, + "userFlags": ["Flag6"] + } + }, + { + "_index": "mailbox_v2", + "_source": { + "attachments": [{ + "contentDisposition": "attachment", + "fileExtension": "docx", + "fileName": "document1.docx", + "mediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "textContent": "A long time ago in a galaxy far, far away." + }], + "bcc": [{ + "address": "lskywalker@example.com", + "domain": "example.com", + "name": "Luke Skywalker" + }], + "cc": [{ + "address": "myoda@example.com", + "domain": "example.com", + "name": "Master Yoda" + }], + "date": "2024-02-26T16:30:00Z", + "from": [{ + "address": "hsolo@example.com", + "domain": "example.com", + "name": "Han Solo" + }], + "hasAttachment": true, + "headers": [ + { "name": "Header9", "value": "Value9" }, + { "name": "Header10", "value": "Value10" } + ], + "htmlBody": "

A long time ago in a galaxy far, far away.

", + "isAnswered": true, + "isDeleted": false, + "isDraft": false, + "isFlagged": true, + "isRecent": false, + "isUnread": true, + "mailboxId": "mailbox5", + "mediaType": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "messageId": "message5", + "mimeMessageID": "mimeMessageID5", + "modSeq": 24680, + "saveDate": "2024-02-26T16:30:00Z", + "sentDate": "2024-02-26T16:30:00Z", + "size": 16384, + "subject": ["Star Wars Message 5"], + "subtype": "subtype5", + "textBody": "A long time ago in a galaxy far, far away.", + "threadId": "thread5", + "to": [{ + "address": "chewbacca@example.com", + "domain": "example.com", + "name": "Chewbacca" + }], + "uid": 2345678, + "userFlags": ["Flag7", "Flag8"] + } + } +] diff --git a/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-mapping.json b/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-mapping.json new file mode 100644 index 00000000..ce356979 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/opensearch/tmail-mapping.json @@ -0,0 +1,210 @@ +{ + "mailbox_v2": { + "mappings": { + "dynamic": "strict", + "properties": { + "attachments": { + "properties": { + "contentDisposition": { + "type": "keyword" + }, + "fileExtension": { + "type": "keyword" + }, + "fileName": { + "type": "text", + "analyzer": "standard" + }, + "mediaType": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "textContent": { + "type": "text", + "analyzer": "standard" + } + } + }, + "bcc": { + "properties": { + "address": { + "type": "text", + "fields": { + "raw": { + "type": "keyword" + } + }, + "analyzer": "standard" + }, + "domain": { + "type": "text", + "analyzer": "simple", + "search_analyzer": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "cc": { + "properties": { + "address": { + "type": "text", + "fields": { + "raw": { + "type": "keyword" + } + }, + "analyzer": "standard" + }, + "domain": { + "type": "text", + "analyzer": "simple", + "search_analyzer": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "date": { + "type": "date", + "format": "uuuu-MM-dd'T'HH:mm:ssX||uuuu-MM-dd'T'HH:mm:ssXXX||uuuu-MM-dd'T'HH:mm:ssXXXXX" + }, + "from": { + "properties": { + "address": { + "type": "text", + "fields": { + "raw": { + "type": "keyword" + } + }, + "analyzer": "standard" + }, + "domain": { + "type": "text", + "analyzer": "simple", + "search_analyzer": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "hasAttachment": { + "type": "boolean" + }, + "headers": { + "type": "nested", + "properties": { + "name": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "htmlBody": { + "type": "text", + "analyzer": "standard" + }, + "isAnswered": { + "type": "boolean" + }, + "isDeleted": { + "type": "boolean" + }, + "isDraft": { + "type": "boolean" + }, + "isFlagged": { + "type": "boolean" + }, + "isRecent": { + "type": "boolean" + }, + "isUnread": { + "type": "boolean" + }, + "mailboxId": { + "type": "keyword", + "store": true + }, + "mediaType": { + "type": "keyword" + }, + "messageId": { + "type": "keyword", + "store": true + }, + "mimeMessageID": { + "type": "keyword" + }, + "modSeq": { + "type": "long" + }, + "saveDate": { + "type": "date", + "format": "uuuu-MM-dd'T'HH:mm:ssX||uuuu-MM-dd'T'HH:mm:ssXXX||uuuu-MM-dd'T'HH:mm:ssXXXXX" + }, + "sentDate": { + "type": "date", + "format": "uuuu-MM-dd'T'HH:mm:ssX||uuuu-MM-dd'T'HH:mm:ssXXX||uuuu-MM-dd'T'HH:mm:ssXXXXX" + }, + "size": { + "type": "long" + }, + "subject": { + "type": "text", + "fields": { + "raw": { + "type": "keyword" + } + } + }, + "subtype": { + "type": "keyword" + }, + "textBody": { + "type": "text", + "analyzer": "standard" + }, + "threadId": { + "type": "keyword" + }, + "to": { + "properties": { + "address": { + "type": "text", + "fields": { + "raw": { + "type": "keyword" + } + }, + "analyzer": "standard" + }, + "domain": { + "type": "text", + "analyzer": "simple", + "search_analyzer": "keyword" + }, + "name": { + "type": "text" + } + } + }, + "uid": { + "type": "long", + "store": true + }, + "userFlags": { + "type": "keyword" + } + } + } + } +} diff --git a/packages/tom-server/src/search-engine-api/__testData__/synapse-data/homeserver.yaml b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/homeserver.yaml new file mode 100644 index 00000000..d9cc5250 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/homeserver.yaml @@ -0,0 +1,65 @@ +# Configuration file for Synapse. +# +# This is a YAML file: see [1] for a quick introduction. Note in particular +# that *indentation is important*: all the elements of a list or dictionary +# should have the same indentation. +# +# [1] https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html +# +# For more information on how to configure Synapse, including a complete accounting of +# each option, go to docs/usage/configuration/config_documentation.md or +# https://matrix-org.github.io/synapse/latest/usage/configuration/config_documentation.html +server_name: "example.com" +public_baseurl: "https://matrix.example.com/" +pid_file: /data/homeserve.pid +listeners: + - port: 8008 + tls: false + type: http + x_forwarded: true + resources: + - names: [client, federation] + compress: false +database: + name: psycopg2 + args: + user: synapse + password: 'synapse!1' + database: synapse + host: postgresql + cp_min: 2 + cp_max: 4 + keepalives_idle: 10 + keepalives_interval: 10 + keepalives_count: 3 +log_config: "/data/matrix.example.com.log.config" +media_store_path: /data/media_store +registration_shared_secret: "u+Q^i6&*Y9azZ*~pID^.a=qrvd+mUIBX9SAreEPGJ=xzP&c+Sk" +report_stats: false +macaroon_secret_key: "=0ws-1~ztzXm&xh+As;7YL5.-U~r-T,F4zR3mW#E;6Y::Rb7&G" +form_secret: "&YFO.XSc*2^2ZsW#hmoR+t:wf03~u#fin#O.R&erFcl9_mEayv" +signing_key_path: "/data/matrix.example.com.signing.key" +trusted_key_servers: + - server_name: "matrix.org" + accept_keys_insecurely: true +accept_keys_insecurely: true +app_service_config_files: + - /data/registration.yaml +oidc_config: + idp_id: lemonldap + idp_name: lemonldap + enabled: true + issuer: "https://auth.example.com/" + client_id: "matrix1" + client_secret: "matrix1*" + scopes: ["openid", "profile"] + discover: true + user_profile_method: "userinfo_endpoint" + user_mapping_provider: + config: + subject_claim: "sub" + localpart_template: "{{ user.preferred_username }}" + display_name_template: "{{ user.name }}" +rc_message: + per_second: 0.5 + burst_count: 20 \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/__testData__/synapse-data/matrix.example.com.log.config b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/matrix.example.com.log.config new file mode 100644 index 00000000..3e1efccc --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/matrix.example.com.log.config @@ -0,0 +1,77 @@ +# Log configuration for Synapse. +# +# This is a YAML file containing a standard Python logging configuration +# dictionary. See [1] for details on the valid settings. +# +# Synapse also supports structured logging for machine readable logs which can +# be ingested by ELK stacks. See [2] for details. +# +# [1]: https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema +# [2]: https://matrix-org.github.io/synapse/latest/structured_logging.html + +version: 1 + +formatters: + precise: + format: '%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(request)s - %(message)s' + +handlers: + file: + class: logging.handlers.TimedRotatingFileHandler + formatter: precise + filename: /data/homeserver.log + when: midnight + backupCount: 3 # Does not include the current log file. + encoding: utf8 + + # Default to buffering writes to log file for efficiency. + # WARNING/ERROR logs will still be flushed immediately, but there will be a + # delay (of up to `period` seconds, or until the buffer is full with + # `capacity` messages) before INFO/DEBUG logs get written. + buffer: + class: synapse.logging.handlers.PeriodicallyFlushingMemoryHandler + target: file + + # The capacity is the maximum number of log lines that are buffered + # before being written to disk. Increasing this will lead to better + # performance, at the expensive of it taking longer for log lines to + # be written to disk. + # This parameter is required. + capacity: 10 + + # Logs with a level at or above the flush level will cause the buffer to + # be flushed immediately. + # Default value: 40 (ERROR) + # Other values: 50 (CRITICAL), 30 (WARNING), 20 (INFO), 10 (DEBUG) + flushLevel: 30 # Flush immediately for WARNING logs and higher + + # The period of time, in seconds, between forced flushes. + # Messages will not be delayed for longer than this time. + # Default value: 5 seconds + period: 5 + + # A handler that writes logs to stderr. Unused by default, but can be used + # instead of "buffer" and "file" in the logger handlers. + console: + class: logging.StreamHandler + formatter: precise + +loggers: + synapse.storage.SQL: + # beware: increasing this to DEBUG will make synapse log sensitive + # information such as access tokens. + level: INFO + +root: + level: INFO + + # Write logs to the `buffer` handler, which will buffer them together in memory, + # then write them to a file. + # + # Replace "buffer" with "console" to log to stderr instead. (Note that you'll + # also need to update the configuration for the `twisted` logger above, in + # this case.) + # + handlers: [buffer] + +disable_existing_loggers: false diff --git a/packages/tom-server/src/search-engine-api/__testData__/synapse-data/registration.yaml b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/registration.yaml new file mode 100644 index 00000000..7335edd5 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/__testData__/synapse-data/registration.yaml @@ -0,0 +1,10 @@ +id: 'test' +hs_token: 'hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL' +as_token: 'asTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL' +url: 'http://host.docker.internal:3002/' +sender_localpart: 'sender_localpart_test' +namespaces: + rooms: + - exclusive: false + regex: '!.*' +de.sorunome.msc2409.push_ephemeral: true \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/conf/opensearch-configuration.ts b/packages/tom-server/src/search-engine-api/conf/opensearch-configuration.ts new file mode 100644 index 00000000..df617978 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/conf/opensearch-configuration.ts @@ -0,0 +1,108 @@ +import { type ClientOptions } from '@opensearch-project/opensearch' +import { Utils } from '@twake/matrix-identity-server' +import fs from 'fs' +import { type Config } from '../../types' + +export class OpenSearchConfiguration { + private _host!: string + private _protocol!: string + private _username: string | undefined + private _password: string | undefined + private _caCertPath: string | undefined + private _maxRetries!: number + + constructor(config: Config) { + this._setUsername(config.opensearch_user) + this._setPassword(config.opensearch_password) + if (this._username == null && this._password != null) { + throw new Error('opensearch_user is missing') + } + if (this._username != null && this._password == null) { + throw new Error('opensearch_password is missing') + } + this._setHost(config.opensearch_host) + this._setProtocol(config.opensearch_ssl) + this._setCaCertPath(config.opensearch_ca_cert_path) + this._setMaxRetries(config.opensearch_max_retries) + } + + private _setHost(host: string | null | undefined): void { + if (host == null) { + throw new Error('opensearch_host is required when using OpenSearch') + } + if (typeof host !== 'string') { + throw new Error('opensearch_host must be a string') + } + if (host.match(Utils.hostnameRe) == null) { + throw new Error('opensearch_host is invalid') + } + this._host = host + } + + private _setProtocol(ssl: boolean | undefined): void { + if (ssl != null && typeof ssl !== 'boolean') { + throw new Error('opensearch_ssl must be a boolean') + } + this._protocol = ssl != null && ssl ? 'https' : 'http' + } + + private _setUsername(username: string | null | undefined): void { + if (username != null) { + if (typeof username !== 'string') { + throw new Error('opensearch_user must be a string') + } + this._username = username + } + } + + private _setPassword(password: string | null | undefined): void { + if (password != null) { + if (typeof password !== 'string') { + throw new Error('opensearch_password must be a string') + } + this._password = password + } + } + + private _setCaCertPath(caCertPath: string | null | undefined): void { + if (caCertPath != null) { + if (typeof caCertPath !== 'string') { + throw new Error('opensearch_ca_cert_path must be a string') + } + this._caCertPath = caCertPath + } + } + + private _setMaxRetries(maxRetries: number | undefined | null): void { + if (maxRetries != null && typeof maxRetries !== 'number') { + throw new Error('opensearch_max_retries must be a number') + } + this._maxRetries = maxRetries ?? 3 + } + + getMaxRetries(): number { + return this._maxRetries + } + + getClientOptions(): ClientOptions { + let auth = '' + if (this._username != null && this._password != null) { + auth = `${this._username}:${this._password}@` + } + + let options: ClientOptions = { + node: `${this._protocol}://${auth}${this._host}`, + maxRetries: this._maxRetries + } + + if (this._protocol === 'https' && this._caCertPath != null) { + options = { + ...options, + ssl: { + ca: fs.readFileSync(this._caCertPath, 'utf-8') + } + } + } + return options + } +} diff --git a/packages/tom-server/src/search-engine-api/conf/tchat-mapping.json b/packages/tom-server/src/search-engine-api/conf/tchat-mapping.json new file mode 100644 index 00000000..b2667b4c --- /dev/null +++ b/packages/tom-server/src/search-engine-api/conf/tchat-mapping.json @@ -0,0 +1,34 @@ +{ + "rooms": { + "mappings": { + "dynamic": "strict", + "properties": { + "name": { + "type": "text", + "analyzer": "standard" + } + } + } + }, + "messages": { + "mappings": { + "dynamic": "strict", + "properties": { + "room_id": { + "type": "keyword" + }, + "sender": { + "type": "text" + }, + "content": { + "type": "text", + "analyzer": "standard" + }, + "display_name": { + "type": "text", + "analyzer": "standard" + } + } + } + } +} diff --git a/packages/tom-server/src/search-engine-api/controllers/opensearch.controller.ts b/packages/tom-server/src/search-engine-api/controllers/opensearch.controller.ts new file mode 100644 index 00000000..06a3c40f --- /dev/null +++ b/packages/tom-server/src/search-engine-api/controllers/opensearch.controller.ts @@ -0,0 +1,36 @@ +import { type TwakeLogger } from '@twake/logger' +import { AppServerAPIError } from '@twake/matrix-application-server' +import { type NextFunction, type Request, type Response } from 'express' +import { type IOpenSearchService } from '../services/interfaces/opensearch-service.interface' +import { logError } from '../utils/error' + +export class OpenSearchController { + restoreRoute = '/_twake/app/v1/opensearch/restore' + + constructor( + private readonly _opensearchService: IOpenSearchService, + private readonly _logger: TwakeLogger + ) {} + + postRestore = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + await this._opensearchService.createTomIndexes(true) + res.sendStatus(204) + } catch (e: any) { + logError(this._logger, e, { + httpMethod: 'POST', + endpointPath: this.restoreRoute + }) + + next( + new AppServerAPIError({ + status: 500 + }) + ) + } + } +} diff --git a/packages/tom-server/src/search-engine-api/controllers/search-engine.controller.ts b/packages/tom-server/src/search-engine-api/controllers/search-engine.controller.ts new file mode 100644 index 00000000..ac4ae2b4 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/controllers/search-engine.controller.ts @@ -0,0 +1,63 @@ +import { type TwakeLogger } from '@twake/logger' +import { + AppServerAPIError, + EHttpMethod, + validationErrorHandler +} from '@twake/matrix-application-server' +import { type UserDB } from '@twake/matrix-identity-server' +import { type NextFunction, type Response } from 'express' +import { type AuthRequest } from '../../types' +import { type ISearchEngineService } from '../services/interfaces/search-engine-service.interface' +import { logError } from '../utils/error' + +export class SearchEngineController { + searchRoute = '/_twake/app/v1/search' + + constructor( + private readonly _searchEngineService: ISearchEngineService, + private readonly _userDB: UserDB, + private readonly _logger: TwakeLogger + ) {} + + postSearch = async ( + req: AuthRequest, + res: Response, + next: NextFunction + ): Promise => { + const userId = req.userId as string + try { + validationErrorHandler(req) + const match = userId.match(/^@(.*):/) + if (match == null) { + throw new Error(`Cannot extract user uid from matrix user id ${userId}`) + } + const results = await this._userDB.get('users', ['mail'], { + uid: match[1] + }) + if (results.length === 0) { + throw new Error(`User with user id ${match[1]} not found`) + } + const responseBody = + await this._searchEngineService.getMailsMessagesRoomsContainingSearchValue( + req.body.searchValue as string, + userId, + results[0].mail as string + ) + res.json(responseBody) + } catch (e: any) { + logError(this._logger, e, { + httpMethod: EHttpMethod.POST, + endpointPath: this.searchRoute, + matrixUserId: userId + }) + + next( + e instanceof AppServerAPIError + ? e + : new AppServerAPIError({ + status: 500 + }) + ) + } + } +} diff --git a/packages/tom-server/src/search-engine-api/index.ts b/packages/tom-server/src/search-engine-api/index.ts new file mode 100644 index 00000000..d96e3821 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/index.ts @@ -0,0 +1,124 @@ +import { type ConfigDescription } from '@twake/config-parser' +import { type TwakeLogger } from '@twake/logger' +import MatrixApplicationServer, { + type AppService, + type ClientEvent +} from '@twake/matrix-application-server' +import { type MatrixDB, type UserDB } from '@twake/matrix-identity-server' +import { Router } from 'express' +import { + type AuthenticationFunction, + type Config, + type IdentityServerDb +} from '../types' +import { type IMatrixDBRoomsRepository } from './repositories/interfaces/matrix-db-rooms-repository.interface' +import { type IOpenSearchRepository } from './repositories/interfaces/opensearch-repository.interface' +import { MatrixDBRoomsRepository } from './repositories/matrix-db-rooms.repository' +import { OpenSearchRepository } from './repositories/opensearch.repository' +import { extendRoutes } from './routes' +import { type IOpenSearchService } from './services/interfaces/opensearch-service.interface' +import { OpenSearchService } from './services/opensearch.service' +import { formatErrorMessageForLog, logError } from './utils/error' + +export default class TwakeSearchEngine + extends MatrixApplicationServer + implements AppService +{ + routes = Router() + declare conf: Config + ready!: Promise + public readonly openSearchService: IOpenSearchService + public readonly openSearchRepository: IOpenSearchRepository + public readonly matrixDBRoomsRepository: IMatrixDBRoomsRepository + + constructor( + public readonly idDb: IdentityServerDb, + public readonly userDB: UserDB, + public readonly authenticate: AuthenticationFunction, + matrixDb: MatrixDB, + conf: Config, + logger: TwakeLogger, + confDesc?: ConfigDescription + ) { + super(conf, confDesc, logger) + this.openSearchRepository = new OpenSearchRepository(this.conf, this.logger) + this.matrixDBRoomsRepository = new MatrixDBRoomsRepository(matrixDb) + this.openSearchService = new OpenSearchService( + this.openSearchRepository, + this.matrixDBRoomsRepository + ) + + this.ready = new Promise((resolve, reject) => { + this.openSearchService + .createTomIndexes() + .then(() => { + extendRoutes(this) + this.on('state event | type: m.room.name', (event: ClientEvent) => { + this.openSearchService.updateRoomName(event).catch((e: any) => { + logError(this.logger, e) + }) + }) + + this.on( + 'state event | type: m.room.encryption', + (event: ClientEvent) => { + if (event.content.algorithm != null) { + this.openSearchService.deindexRoom(event).catch((e: any) => { + logError(this.logger, e) + }) + } + } + ) + + this.on('type: m.room.message', (event: ClientEvent) => { + if ( + event.content['m.new_content'] != null && + (event.content['m.relates_to'] as Record) + ?.event_id != null && + (event.content['m.relates_to'] as Record) + ?.rel_type === 'm.replace' + ) { + this.openSearchService.updateMessage(event).catch((e: any) => { + logError(this.logger, e) + }) + } else { + this.openSearchService.indexMessage(event).catch((e: any) => { + logError(this.logger, e) + }) + } + }) + + this.on('type: m.room.redaction', (event: ClientEvent) => { + if (event.redacts?.match(/^\$.{1,255}$/g) != null) { + this.openSearchService.deindexMessage(event).catch((e: any) => { + logError(this.logger, e) + }) + } + }) + + this.on('state event | type: m.room.member', (event: ClientEvent) => { + if ( + event.unsigned?.prev_content?.displayname != null && + event.content.displayname != null && + event.content.displayname !== + event.unsigned?.prev_content?.displayname + ) { + this.openSearchService + .updateDisplayName(event) + .catch((e: any) => { + logError(this.logger, e) + }) + } + }) + resolve() + }) + .catch((e) => { + reject(formatErrorMessageForLog(e)) + }) + }) + } + + close(): void { + this.openSearchRepository.close() + } +} diff --git a/packages/tom-server/src/search-engine-api/repositories/interfaces/matrix-db-rooms-repository.interface.ts b/packages/tom-server/src/search-engine-api/repositories/interfaces/matrix-db-rooms-repository.interface.ts new file mode 100644 index 00000000..4530ab1c --- /dev/null +++ b/packages/tom-server/src/search-engine-api/repositories/interfaces/matrix-db-rooms-repository.interface.ts @@ -0,0 +1,40 @@ +import { type ClientEvent } from '@twake/matrix-application-server' + +export interface IRoomDetail { + room_id: string + name: string | null + canonical_alias: string | null + join_rules: string | null + history_visibility: string | null + encryption: string | null + avatar: string | null + guest_access: string | null + is_federatable: boolean | null + topic: string | null + room_type: string | null +} + +export interface IMatrixDBRoomsRepository { + getAllClearRoomsIds: () => Promise + isEncryptedRoom: (roomId: string) => Promise + getRoomsDetails: (roomsIds: string[]) => Promise> + getRoomDetail: (roomId: string) => Promise + getUserDisplayName: (roomId: string, userId: string) => Promise + getAllClearRoomsNames: () => Promise> + getMembersDisplayNames: ( + roomsIds: string[] + ) => Promise> + getAllClearRoomsMessages: () => Promise< + Array<{ + room_id: string + event_id: string + json: ClientEvent & { display_name: string | null } + }> + > + getUserRoomsIds: (userId: string) => Promise + getDirectRoomsIds: (roomsIds: string[]) => Promise + getDirectRoomsAvatarUrl: ( + roomsIds: string[], + userId: string + ) => Promise> +} diff --git a/packages/tom-server/src/search-engine-api/repositories/interfaces/opensearch-repository.interface.ts b/packages/tom-server/src/search-engine-api/repositories/interfaces/opensearch-repository.interface.ts new file mode 100644 index 00000000..8a850d6e --- /dev/null +++ b/packages/tom-server/src/search-engine-api/repositories/interfaces/opensearch-repository.interface.ts @@ -0,0 +1,146 @@ +import { type ApiResponse } from '@opensearch-project/opensearch' + +export enum EOpenSearchIndexingAction { + CREATE = 'create', + INDEX = 'index' +} + +export interface Document { + id: string + [key: string]: string +} + +export interface DocumentWithIndexingAction extends Document { + action: EOpenSearchIndexingAction.CREATE | EOpenSearchIndexingAction.INDEX +} + +export interface IOpenSearchClientError { + name: string + meta: { + body: T + statusCode: number + headers: { + server: string + date: string + 'content-type': string + 'content-length': string + connection: string + 'strict-transport-security': string + } + meta: { + context: string | null + request: { + params: { + method: string + path: string + body: string + querystring: string + headers: { + 'user-agent': string + 'content-type': string + 'content-length': string + } + timeout: number + } + options: { + maxRetries: number + } + id: number + } + name: string + connection: { + url: string + id: string + headers: Record + deadCount: number + resurrectTimeout: number + _openRequests: number + status: string + roles: { + data: boolean + ingest: boolean + } + } + attempts: number + aborted: boolean + } + } +} + +export interface IErrorOnMultipleDocuments { + took: number + timed_out: boolean + total: number + updated: number + deleted: number + batches: number + version_conflicts: number + noops: number + retries: { + bulk: number + search: number + } + throttled_millis: number + requests_per_second: number + throttled_until_millis: number + failures: Array<{ + index: string + id: string + cause: { + type: string + reason: string + index: string + shard: string + index_uuid: string + } + status: number + }> +} +export interface IQuery { + operator: string + field: string +} + +export type searchRequestBody = Record + +export interface IErrorOnSingleDocument { + _index: string + _id: string + _version: number + result: string + _shards: { + total: number + successful: number + failed: number + } + _seq_no: number + _primary_term: number +} + +export interface IOpenSearchRepository { + createIndex: (index: string, mappings: Record) => Promise + indexDocument: (index: string, document: Document) => Promise + indexDocuments: ( + documentsByIndex: Record< + string, + DocumentWithIndexingAction | DocumentWithIndexingAction[] + > + ) => Promise + updateDocument: (index: string, document: Document) => Promise + updateDocuments: ( + index: string, + script: string, + query: Record + ) => Promise + indexExists: (index: string) => Promise + deleteDocument: (index: string, id: string) => Promise + deleteDocuments: ( + index: string, + query: Record + ) => Promise + searchOnMultipleIndexes: ( + searchValue: string, + elements: searchRequestBody + ) => Promise, unknown>> + close: () => void +} diff --git a/packages/tom-server/src/search-engine-api/repositories/matrix-db-rooms.repository.ts b/packages/tom-server/src/search-engine-api/repositories/matrix-db-rooms.repository.ts new file mode 100644 index 00000000..f01af1dc --- /dev/null +++ b/packages/tom-server/src/search-engine-api/repositories/matrix-db-rooms.repository.ts @@ -0,0 +1,220 @@ +import { type ClientEvent } from '@twake/matrix-application-server' +import { type MatrixDB } from '@twake/matrix-identity-server' +import lodash from 'lodash' +import { + type IMatrixDBRoomsRepository, + type IRoomDetail +} from './interfaces/matrix-db-rooms-repository.interface' +const { groupBy, mapValues } = lodash + +export class MatrixDBRoomsRepository implements IMatrixDBRoomsRepository { + constructor(private readonly _matrixDb: MatrixDB) {} + + private _checkRoomStatsStateResult( + roomId: string, + results: Array> | IRoomDetail[] + ): void { + if (results.length === 0) { + throw new Error(`No room stats state found with id ${roomId}`) + } + if (results.length > 1) { + throw new Error(`More than one room found with id ${roomId}`) + } + } + + async getAllClearRoomsIds(): Promise { + return ( + (await this._matrixDb.getAll('room_stats_state', [ + 'room_id', + 'encryption' + ])) as Array<{ room_id: string; encryption: string | null }> + ) + .filter((room) => room.encryption == null) + .map((room) => room.room_id) + } + + async isEncryptedRoom(roomId: string): Promise { + const results = (await this._matrixDb.get( + 'room_stats_state', + ['encryption'], + { + room_id: roomId + } + )) as Array<{ encryption: string | null }> + this._checkRoomStatsStateResult(roomId, results) + return results[0].encryption != null + } + + async getRoomsDetails( + roomsIds: string[] + ): Promise> { + const results = (await this._matrixDb.get('room_stats_state', ['*'], { + room_id: roomsIds + })) as unknown as IRoomDetail[] + return mapValues( + groupBy(results, 'room_id'), + (res) => res.pop() as IRoomDetail + ) + } + + async getRoomDetail(roomId: string): Promise { + const result = (await this._matrixDb.get('room_stats_state', ['*'], { + room_id: roomId + })) as unknown as IRoomDetail[] + this._checkRoomStatsStateResult(roomId, result) + return result[0] + } + + async getUserDisplayName( + roomId: string, + userId: string + ): Promise { + const memberships = (await this._matrixDb.get( + 'room_memberships', + ['display_name', 'membership'], + { + room_id: roomId, + user_id: userId + } + )) as Array<{ display_name: string | null; membership: string }> + if (memberships.length === 0) { + throw new Error( + `No memberships found for user ${userId} in room ${roomId}` + ) + } + const lastItem = memberships.pop() + if (lastItem?.membership !== 'join') { + throw new Error( + `User ${userId} is not allowed to participate in room ${roomId}` + ) + } + return lastItem.display_name + } + + async getAllClearRoomsNames(): Promise< + Array<{ room_id: string; name: string }> + > { + return ( + (await this._matrixDb.getAll('room_stats_state', [ + 'room_id', + 'encryption', + 'name' + ])) as Array<{ + room_id: string + encryption: string | null + name: string | null + }> + ) + .filter((room) => room.encryption == null && room.name != null) + .map((room) => ({ room_id: room.room_id, name: room.name as string })) + } + + async getMembersDisplayNames( + roomsIds: string[] + ): Promise> { + const results = (await this._matrixDb.get( + 'room_memberships', + ['user_id', 'display_name'], + { + room_id: roomsIds + } + )) as Array<{ user_id: string; display_name: string | null }> + + return mapValues( + groupBy(results, 'user_id'), + (res) => res.map((r) => r.display_name).pop() as string | null + ) + } + + async getAllClearRoomsMessages(): Promise< + Array<{ + room_id: string + event_id: string + json: ClientEvent & { display_name: string | null } + }> + > { + const clearRoomsIds = await this.getAllClearRoomsIds() + const displayNameByUserId = await this.getMembersDisplayNames(clearRoomsIds) + return ( + (await this._matrixDb.get('event_json', [ + 'room_id', + 'event_id', + 'json' + ])) as Array<{ room_id: string; event_id: string; json: string }> + ) + .map((event) => ({ + event_id: event.event_id, + room_id: event.room_id, + json: JSON.parse(event.json) as ClientEvent + })) + .filter( + (event) => + clearRoomsIds.includes(event.room_id) && + event.json.type === 'm.room.message' + ) + .map((event) => ({ + ...event, + json: { + ...event.json, + display_name: displayNameByUserId[event.json.sender] ?? null + } + })) + } + + async getUserRoomsIds(userId: string): Promise { + const allUserRooms = (await this._matrixDb.get( + 'room_memberships', + ['room_id', 'membership'], + { + user_id: userId + } + )) as Array<{ room_id: string; membership: string }> + const roomMembershipsById = mapValues( + groupBy(allUserRooms, 'room_id'), + (membershipDetails) => membershipDetails.map((m) => m.membership) + ) + return Object.keys(roomMembershipsById).filter( + (roomId: string) => roomMembershipsById[roomId].pop() === 'join' + ) + } + + async getDirectRoomsIds(roomsIds: string[]): Promise { + return ( + (await this._matrixDb.get('event_json', ['room_id', 'json'], { + room_id: roomsIds + })) as Array<{ room_id: string; json: string }> + ) + .map((event) => ({ + room_id: event.room_id, + json: JSON.parse(event.json) as ClientEvent + })) + .filter( + (event) => + event.json.type === 'm.room.member' && event.json.content.is_direct + ) + .map((event) => event.room_id) + } + + async getDirectRoomsAvatarUrl( + roomsIds: string[], + userId: string + ): Promise> { + const results = ( + (await this._matrixDb.get( + 'room_memberships', + ['room_id', 'user_id', 'avatar_url'], + { + room_id: roomsIds + } + )) as Array<{ + room_id: string + user_id: string + avatar_url: string | null + }> + ).filter((membership) => membership.user_id !== userId) + return mapValues( + groupBy(results, 'room_id'), + (res) => res.map((r) => r.avatar_url).pop() as string | null + ) + } +} diff --git a/packages/tom-server/src/search-engine-api/repositories/opensearch.repository.ts b/packages/tom-server/src/search-engine-api/repositories/opensearch.repository.ts new file mode 100644 index 00000000..156c096f --- /dev/null +++ b/packages/tom-server/src/search-engine-api/repositories/opensearch.repository.ts @@ -0,0 +1,366 @@ +import { Client, type ApiResponse } from '@opensearch-project/opensearch' +import { type TwakeLogger } from '@twake/logger' +import { type Config } from '../../types' +import { OpenSearchConfiguration } from '../conf/opensearch-configuration' +import { OpenSearchClientException } from '../utils/error' +import { + type Document, + type DocumentWithIndexingAction, + type IErrorOnMultipleDocuments, + type IErrorOnSingleDocument, + type IOpenSearchClientError, + type IOpenSearchRepository, + type IQuery, + type searchRequestBody +} from './interfaces/opensearch-repository.interface' + +export class OpenSearchRepository implements IOpenSearchRepository { + private _numberOfShards!: number + private _numberOfReplicas!: number + private _waitForActiveShards!: string + private readonly _openSearchClient: Client + private readonly _requestOptions: Record + + constructor(config: Config, private readonly logger: TwakeLogger) { + this._setNumberOfShards(config.opensearch_number_of_shards) + this._setNumberOfReplicas(config.opensearch_number_of_replicas) + this._setWaitForActiveShards(config.opensearch_wait_for_active_shards) + const openSearchConfiguration = new OpenSearchConfiguration(config) + this._openSearchClient = new Client( + openSearchConfiguration.getClientOptions() + ) + this._requestOptions = { + maxRetries: openSearchConfiguration.getMaxRetries() + } + } + + private _setNumberOfShards(numberOfShards: number | undefined | null): void { + if (numberOfShards != null && typeof numberOfShards !== 'number') { + throw new Error('opensearch_number_of_shards must be a number') + } + this._numberOfShards = numberOfShards ?? 1 + } + + private _setNumberOfReplicas( + numberOfReplicas: number | undefined | null + ): void { + if (numberOfReplicas != null && typeof numberOfReplicas !== 'number') { + throw new Error('opensearch_number_of_replicas must be a number') + } + this._numberOfReplicas = numberOfReplicas ?? 1 + } + + private _setWaitForActiveShards( + waitForActiveShards: string | undefined | null + ): void { + if (waitForActiveShards != null) { + if (typeof waitForActiveShards !== 'string') { + throw new Error('opensearch_wait_for_active_shards must be a string') + } else if (waitForActiveShards?.match(/all|\d+/g) == null) { + throw new Error( + 'opensearch_wait_for_active_shards must be a string equal to a number or "all"' + ) + } + } + this._waitForActiveShards = waitForActiveShards ?? '1' + } + + private _checkOpenSearchApiResponse( + response: ApiResponse>, + additionalsValidStatusCode: number[] = [] + ): void { + if ( + response.statusCode != null && + String(response.statusCode).match(/^20[0-8]$/g) == null && + !additionalsValidStatusCode.includes(response.statusCode) + ) { + throw new OpenSearchClientException( + JSON.stringify(response.body, null, 2), + response.statusCode ?? 500 + ) + } + } + + private _checkException( + e: + | IOpenSearchClientError< + IErrorOnMultipleDocuments | IErrorOnSingleDocument + > + | Error + ): never { + if ('meta' in e) { + throw new OpenSearchClientException( + JSON.stringify( + 'failures' in e.meta.body ? e.meta.body.failures : e.meta.body, + null, + 2 + ), + e.meta.statusCode ?? 500 + ) + } + throw e + } + + async createIndex( + index: string, + mappings: Record + ): Promise { + try { + const response = await this._openSearchClient.indices.create( + { + index, + wait_for_active_shards: this._waitForActiveShards, + body: { + mappings, + settings: { + index: { + number_of_shards: this._numberOfShards, + number_of_replicas: this._numberOfReplicas + } + } + } + }, + this._requestOptions + ) + this._checkOpenSearchApiResponse(response) + this.logger.info(`Index ${index} created`) + } catch (e) { + this._checkException(e as Error) + } + } + + async indexDocument(index: string, document: Document): Promise { + try { + const { id, ...body } = document + const response = await this._openSearchClient.index( + { + id, + index, + wait_for_active_shards: this._waitForActiveShards, + body, + refresh: true + }, + this._requestOptions + ) + this._checkOpenSearchApiResponse(response) + } catch (e) { + this._checkException(e as IOpenSearchClientError) + } + } + + async indexDocuments( + documentsByIndex: Record< + string, + DocumentWithIndexingAction | DocumentWithIndexingAction[] + > + ): Promise { + try { + const response = await this._openSearchClient.bulk( + { + wait_for_active_shards: this._waitForActiveShards, + refresh: true, + body: Object.keys(documentsByIndex).reduce< + Array> + >((acc, index) => { + let documentDetails: any + if (Array.isArray(documentsByIndex[index])) { + documentDetails = ( + documentsByIndex[index] as DocumentWithIndexingAction[] + ).reduce>>((acc, doc) => { + const { action, id, ...properties } = doc + return [ + ...acc, + { + [action]: { + _id: id, + _index: index + } + }, + properties + ] + }, []) + } else { + const { action, id, ...properties } = documentsByIndex[ + index + ] as DocumentWithIndexingAction + documentDetails = [ + { + [action]: { + _id: id, + _index: index + } + }, + properties + ] + } + return [...acc, ...documentDetails] + }, []) + }, + this._requestOptions + ) + this._checkOpenSearchApiResponse(response) + } catch (e) { + this._checkException( + e as IOpenSearchClientError + ) + } + } + + async updateDocument(index: string, document: Document): Promise { + try { + const { id, ...updatedFields } = document + + const response = await this._openSearchClient.update( + { + id, + index, + wait_for_active_shards: this._waitForActiveShards, + body: { doc: updatedFields }, + refresh: true + }, + this._requestOptions + ) + this._checkOpenSearchApiResponse(response) + } catch (e) { + this._checkException(e as IOpenSearchClientError) + } + } + + async updateDocuments( + index: string, + script: string, + query: Record + ): Promise { + try { + const response = await this._openSearchClient.update_by_query( + { + index, + refresh: true, + conflicts: 'proceed', + wait_for_active_shards: this._waitForActiveShards, + body: { script: { source: script }, query } + }, + this._requestOptions + ) + this._checkOpenSearchApiResponse(response) + } catch (e) { + this._checkException( + e as IOpenSearchClientError + ) + } + } + + async deleteDocument(index: string, id: string): Promise { + try { + const documentExists = ( + await this._openSearchClient.exists({ + index, + id + }) + )?.body + if (documentExists) { + const response = await this._openSearchClient.delete({ + index, + id, + refresh: true, + wait_for_active_shards: this._waitForActiveShards + }) + this._checkOpenSearchApiResponse(response) + } + } catch (e) { + this._checkException(e as IOpenSearchClientError) + } + } + + async deleteDocuments( + index: string, + query: Record + ): Promise { + try { + const response = await this._openSearchClient.delete_by_query( + { + index, + refresh: true, + wait_for_active_shards: this._waitForActiveShards, + conflicts: 'proceed', + body: { + query + } + }, + this._requestOptions + ) + this._checkOpenSearchApiResponse(response) + } catch (e) { + this._checkException(e as IOpenSearchClientError) + } + } + + async indexExists(index: string): Promise { + try { + const roomsIndexExists = await this._openSearchClient.indices.exists( + { + index + }, + this._requestOptions + ) + this._checkOpenSearchApiResponse(roomsIndexExists, [404]) + return roomsIndexExists?.body + } catch (e) { + this._checkException(e as Error) + } + } + + async searchOnMultipleIndexes( + searchValue: string, + elements: searchRequestBody + ): Promise, unknown>> { + try { + const response = await this._openSearchClient.msearch( + { + body: Object.keys(elements).reduce>>( + (acc, index) => [ + ...acc, + { + index + }, + { + size: 10000, + query: Array.isArray(elements[index]) + ? { + bool: { + should: (elements[index] as IQuery[]).map((elt) => ({ + [elt.operator]: { + [elt.field]: { + value: searchValue, + case_insensitive: true + } + } + })) + } + } + : { + [(elements[index] as IQuery).operator]: { + [(elements[index] as IQuery).field]: { + value: searchValue, + case_insensitive: true + } + } + } + } + ], + [] + ) + }, + this._requestOptions + ) + this._checkOpenSearchApiResponse(response) + return response + } catch (e) { + this._checkException(e as Error) + } + } + + close(): void { + void this._openSearchClient.close() + } +} diff --git a/packages/tom-server/src/search-engine-api/routes/index.ts b/packages/tom-server/src/search-engine-api/routes/index.ts new file mode 100644 index 00000000..9f8d61d1 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/routes/index.ts @@ -0,0 +1,274 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { + EHttpMethod, + allowCors, + errorMiddleware, + methodNotAllowed +} from '@twake/matrix-application-server' +import { body } from 'express-validator' +import type TwakeSearchEngine from '..' +import authMiddleware from '../../utils/middlewares/auth.middleware' +import { OpenSearchController } from '../controllers/opensearch.controller' +import { SearchEngineController } from '../controllers/search-engine.controller' +import { SearchEngineService } from '../services/search-engine.service' + +export const extendRoutes = (server: TwakeSearchEngine): void => { + const searchEngineService = new SearchEngineService( + server.openSearchRepository, + server.matrixDBRoomsRepository + ) + const searchEngineController = new SearchEngineController( + searchEngineService, + server.userDB, + server.logger + ) + + const openSearchController = new OpenSearchController( + server.openSearchService, + server.logger + ) + + /** + * @openapi + * '/_twake/app/v1/search': + * post: + * tags: + * - Search Engine + * description: Search performs with OpenSearch on Tchat messages and rooms + * requestBody: + * description: Object containing search query details + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * searchValue: + * type: string + * description: Value used to perform the search on rooms and messages data + * required: + * - searchValue + * example: + * searchValue: "hello" + * responses: + * 200: + * description: Success + * content: + * application/json: + * schema: + * type: object + * properties: + * rooms: + * type: array + * description: List of rooms whose name contains the search value + * items: + * type: object + * properties: + * room_id: + * type: string + * name: + * type: string + * avatar_url: + * type: string + * description: Url of the room's avatar + * messages: + * type: array + * description: List of messages whose content or/and sender display name contain the search value + * items: + * type: object + * properties: + * room_id: + * type: string + * event_id: + * type: string + * description: Id of the message + * content: + * type: string + * display_name: + * type: string + * description: Sender display name + * avatar_url: + * type: string + * description: Sender's avatar url if it is a direct chat, otherwise it is the room's avatar url + * room_name: + * type: string + * description: Room's name in case of the message is not part of a direct chat + * mails: + * type: array + * description: List of mails from Tmail whose meta or content contain the search value + * items: + * type: object + * properties: + * attachments: + * type: array + * items: + * type: object + * properties: + * contentDisposition: + * type: string + * fileExtension: + * type: string + * fileName: + * type: string + * mediaType: + * type: string + * subtype: + * type: string + * textContent: + * type: string + * bcc: + * type: array + * items: + * type: object + * properties: + * address: + * type: string + * domain: + * type: string + * name: + * type: string + * cc: + * type: array + * items: + * type: object + * properties: + * address: + * type: string + * domain: + * type: string + * name: + * type: string + * date: + * type: string + * from: + * type: array + * items: + * type: object + * properties: + * address: + * type: string + * domain: + * type: string + * name: + * type: string + * hasAttachment: + * type: boolean + * headers: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * value: + * type: string + * htmlBody: + * type: string + * isAnswered: + * type: boolean + * isDeleted: + * type: boolean + * isDraft: + * type: boolean + * isFlagged: + * type: boolean + * isRecent: + * type: boolean + * isUnread: + * type: boolean + * mailboxId: + * type: string + * mediaType: + * type: string + * messageId: + * type: string + * mimeMessageID: + * type: string + * modSeq: + * type: number + * saveDate: + * type: string + * sentDate: + * type: string + * size: + * type: number + * subject: + * type: array + * items: + * type: string + * subtype: + * type: string + * textBody: + * type: string + * threadId: + * type: string + * to: + * type: array + * items: + * type: object + * properties: + * address: + * type: string + * domain: + * type: string + * name: + * type: string + * uid: + * type: number + * userFlags: + * type: array + * items: + * type: string + * example: + * rooms: [{"room_id": "!dYqMpBXVQgKWETVAtJ:example.com", "name": "Hello world room", "avatar_url": "mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR"}, {"room_id": "!dugSgNYwppGGoeJwYB:example.com", "name": "Worldwide room", "avatar_url": null}] + * messages: + * [{"room_id": "!dYqMpBXVQgKWETVAtJ:example.com", "event_id": "$c0hW6db_GUjk0NRBUuO12IyMpi48LE_tQK6sH3dkd1U", "content": "Hello world", "display_name": "Anakin Skywalker", "avatar_url": "mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR", "room_name": "Hello world room"}, {"room_id": "!ftGqINYwppGGoeJwYB:example.com", "event_id": "$IUzFofxHCvvoHJ-k2nfx7OlWOO8AuPvlHHqkeJLzxJ8", "content": "Hello world my friends in direct chat", "display_name": "Luke Skywalker", "avatar_url": "mxc://matrix.org/wefh34uihSDRGhw34"}] + * mails: [{"id": "message1","attachments": [{"contentDisposition": "attachment","fileExtension": "jpg","fileName": "image1.jpg","mediaType": "image/jpeg","textContent": "A beautiful galaxy far, far away."}],"bcc": [{"address": "okenobi@example.com","domain": "example.com","name": "Obi-Wan Kenobi"}],"cc": [{"address": "pamidala@example.com","domain": "example.com","name": "Padme Amidala"}],"date": "2024-02-24T10:15:00Z","from": [{"address": "dmaul@example.com","domain": "example.com","name": "Dark Maul"}],"hasAttachment": true,"headers": [{ "name": "Header5", "value": "Value5" },{ "name": "Header6", "value": "Value6" }],"htmlBody": "

A beautiful galaxy far, far away.

","isAnswered": true,"isDeleted": false,"isDraft": false,"isFlagged": true,"isRecent": true,"isUnread": false,"mailboxId": "mailbox3","mediaType": "image/jpeg","messageId": "message3","mimeMessageID": "mimeMessageID3","modSeq": 98765,"saveDate": "2024-02-24T10:15:00Z","sentDate": "2024-02-24T10:15:00Z","size": 4096,"subject": ["Star Wars Message 3"],"subtype": "subtype3","textBody": "A beautiful galaxy far, far away.","threadId": "thread3","to": [{"address": "kren@example.com","domain": "example.com","name": "Kylo Ren"}],"uid": 987654,"userFlags": ["Flag4", "Flag5"]}] + * 400: + * $ref: '#/components/responses/BadRequest' + * 401: + * $ref: '#/components/responses/Unauthorized' + * 404: + * $ref: '#/components/responses/NotFound' + * 405: + * $ref: '#/components/responses/Unrecognized' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ + server.router.addRoute( + searchEngineController.searchRoute, + EHttpMethod.POST, + searchEngineController.postSearch, + [body('searchValue').exists().isString()], + authMiddleware(server.authenticate, server.logger) + ) + + /** + * @openapi + * '/_twake/app/v1/opensearch/restore': + * post: + * tags: + * - Search Engine + * description: Restore OpenSearch indexes using Matrix homeserver database + * requestBody: + * content: + * application/json: + * schema: + * type: object + * responses: + * 204: + * description: Success + * content: + * application/json: + * schema: + * type: object + * 405: + * $ref: '#/components/responses/Unrecognized' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ + server.router.routes + .route(openSearchController.restoreRoute) + .post(allowCors, openSearchController.postRestore, errorMiddleware) + .all(allowCors, methodNotAllowed, errorMiddleware) +} diff --git a/packages/tom-server/src/search-engine-api/services/interfaces/opensearch-service.interface.ts b/packages/tom-server/src/search-engine-api/services/interfaces/opensearch-service.interface.ts new file mode 100644 index 00000000..a0d643f7 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/services/interfaces/opensearch-service.interface.ts @@ -0,0 +1,11 @@ +import { type ClientEvent } from '@twake/matrix-application-server' + +export interface IOpenSearchService { + updateRoomName: (event: ClientEvent) => Promise + updateDisplayName: (event: ClientEvent) => Promise + indexMessage: (event: ClientEvent) => Promise + updateMessage: (event: ClientEvent) => Promise + deindexRoom: (event: ClientEvent) => Promise + createTomIndexes: (forceRestore?: boolean) => Promise + deindexMessage: (event: ClientEvent) => Promise +} diff --git a/packages/tom-server/src/search-engine-api/services/interfaces/search-engine-service.interface.ts b/packages/tom-server/src/search-engine-api/services/interfaces/search-engine-service.interface.ts new file mode 100644 index 00000000..4e0e2576 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/services/interfaces/search-engine-service.interface.ts @@ -0,0 +1,117 @@ +export interface ISearchEngineService { + getMailsMessagesRoomsContainingSearchValue: ( + searchValue: string, + userId: string, + userEmail: string + ) => Promise +} + +export interface IOpenSearchResponse { + took: number + timed_out: boolean + _shards: { + total: number + successful: number + skipped: number + failed: number + } + hits: { + total: { value: number; relation: string } + max_score: 1 + hits: Array> + } + status: number +} + +interface IOpenSearchResult { + _index: string + _id: string + _score: number + _source: T +} + +export interface IOpenSearchRoomResult { + name: string +} + +export interface IOpenSearchMessageResult { + room_id: string + content: string + sender: string + display_name: string | null +} + +export interface IOpenSearchMailResult { + attachments: Array<{ + contentDisposition: string + fileExtension: string + fileName: string + mediaType: string + subtype: string + textContent: string + }> + bcc: Array<{ + address: string + domain: string + name: string + }> + cc: Array<{ + address: string + domain: string + name: string + }> + date: string + from: Array<{ + address: string + domain: string + name: string + }> + hasAttachment: boolean + headers: Array<{ + name: string + value: string + }> + htmlBody: string + isAnswered: boolean + isDeleted: boolean + isDraft: boolean + isFlagged: boolean + isRecent: boolean + isUnread: boolean + mailboxId: string + mediaType: string + messageId: string + mimeMessageID: string + modSeq: number + saveDate: string + sentDate: string + size: number + subject: string[] + subtype: string + textBody: string + threadId: string + to: Array<{ + address: string + domain: string + name: string + }> + uid: number + userFlags: string[] +} + +export interface IResponseBody { + rooms: Array<{ + room_id: string + name: string + avatar_url: string | null + }> + messages: Array<{ + room_id: string + event_id: string + content: string + display_name: string | null + avatar_url: string | null + room_name: string | null + }> + mails: Array<{ id: string } & IOpenSearchMailResult> +} diff --git a/packages/tom-server/src/search-engine-api/services/opensearch.service.ts b/packages/tom-server/src/search-engine-api/services/opensearch.service.ts new file mode 100644 index 00000000..3e17e08a --- /dev/null +++ b/packages/tom-server/src/search-engine-api/services/opensearch.service.ts @@ -0,0 +1,168 @@ +import { type ClientEvent } from '@twake/matrix-application-server' +import tchatMapping from '../conf/tchat-mapping.json' +import { type IMatrixDBRoomsRepository } from '../repositories/interfaces/matrix-db-rooms-repository.interface' +import { + EOpenSearchIndexingAction, + type DocumentWithIndexingAction, + type IOpenSearchRepository +} from '../repositories/interfaces/opensearch-repository.interface' +import { tomMessagesIndex, tomRoomsIndex } from '../utils/constantes' +import { type IOpenSearchService } from './interfaces/opensearch-service.interface' + +export class OpenSearchService implements IOpenSearchService { + constructor( + private readonly _openSearchRepository: IOpenSearchRepository, + private readonly _matrixDBRoomsRepository: IMatrixDBRoomsRepository + ) {} + + async updateRoomName(event: ClientEvent): Promise { + const isEncryptedRoom = await this._matrixDBRoomsRepository.isEncryptedRoom( + event.room_id + ) + + if (!isEncryptedRoom) { + await this._openSearchRepository.indexDocument(tomRoomsIndex, { + id: event.room_id, + name: event.content.name as string + }) + } + } + + async updateDisplayName(event: ClientEvent): Promise { + await this._openSearchRepository.updateDocuments( + tomMessagesIndex, + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `ctx._source.display_name = "${event.content.displayname as string}"`, + { match: { sender: event.sender } } + ) + } + + async deindexRoom(event: ClientEvent): Promise { + await this._openSearchRepository.deleteDocument( + tomRoomsIndex, + event.room_id + ) + await this._openSearchRepository.deleteDocuments(tomMessagesIndex, { + match: { room_id: event.room_id } + }) + } + + async deindexMessage(event: ClientEvent): Promise { + await this._openSearchRepository.deleteDocument( + tomMessagesIndex, + event.redacts as string + ) + } + + async indexMessage(event: ClientEvent): Promise { + const roomDetail = await this._matrixDBRoomsRepository.getRoomDetail( + event.room_id + ) + if (roomDetail.encryption == null) { + const displayName = + await this._matrixDBRoomsRepository.getUserDisplayName( + event.room_id, + event.sender + ) + + let body: any = { + [tomMessagesIndex]: { + id: event.event_id, + action: EOpenSearchIndexingAction.CREATE, + room_id: event.room_id, + content: event.content.body, + sender: event.sender + } + } + if (displayName != null) { + body[tomMessagesIndex] = { + ...body[tomMessagesIndex], + display_name: displayName + } + } + if (roomDetail.name != null) { + body = { + ...body, + [tomRoomsIndex]: { + id: event.room_id, + action: EOpenSearchIndexingAction.INDEX, + name: roomDetail.name + } + } + } + await this._openSearchRepository.indexDocuments(body) + } + } + + async updateMessage(event: ClientEvent): Promise { + const isEncryptedRoom = await this._matrixDBRoomsRepository.isEncryptedRoom( + event.room_id + ) + + if (!isEncryptedRoom) { + await this._openSearchRepository.updateDocument(tomMessagesIndex, { + id: (event.content['m.relates_to'] as Record).event_id, + content: (event.content['m.new_content'] as Record).body + }) + } + } + + async createTomIndexes(forceRestore = false): Promise { + const [roomsIndexExists, messagesIndexExists] = await Promise.all([ + this._openSearchRepository.indexExists(tomRoomsIndex), + this._openSearchRepository.indexExists(tomMessagesIndex) + ]) + if (!roomsIndexExists) { + await this._openSearchRepository.createIndex( + tomRoomsIndex, + tchatMapping.rooms.mappings + ) + } + + const clearRoomsNames = + await this._matrixDBRoomsRepository.getAllClearRoomsNames() + if (clearRoomsNames.length > 0 && (!roomsIndexExists || forceRestore)) { + await this._openSearchRepository.indexDocuments({ + [tomRoomsIndex]: clearRoomsNames.map((room) => ({ + id: room.room_id, + action: EOpenSearchIndexingAction.CREATE, + name: room.name + })) + }) + } + + if (!messagesIndexExists) { + await this._openSearchRepository.createIndex( + tomMessagesIndex, + tchatMapping.messages.mappings + ) + } + + const clearRoomsMessages = + await this._matrixDBRoomsRepository.getAllClearRoomsMessages() + + if ( + clearRoomsMessages.length > 0 && + (!messagesIndexExists || forceRestore) + ) { + await this._openSearchRepository.indexDocuments({ + [tomMessagesIndex]: clearRoomsMessages.map((event) => { + let document: DocumentWithIndexingAction = { + id: event.event_id, + action: EOpenSearchIndexingAction.CREATE, + room_id: event.room_id, + content: event.json.content.body as string, + sender: event.json.sender + } + if (event.json.display_name != null) { + document = { + ...document, + display_name: event.json.display_name + } + } + return document + }) + }) + } + } +} diff --git a/packages/tom-server/src/search-engine-api/services/search-engine.service.ts b/packages/tom-server/src/search-engine-api/services/search-engine.service.ts new file mode 100644 index 00000000..ca6e5683 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/services/search-engine.service.ts @@ -0,0 +1,133 @@ +import { type IMatrixDBRoomsRepository } from '../repositories/interfaces/matrix-db-rooms-repository.interface' +import { type IOpenSearchRepository } from '../repositories/interfaces/opensearch-repository.interface' +import { + tmailMailsIndex, + tomMessagesIndex, + tomRoomsIndex +} from '../utils/constantes' +import { + type IOpenSearchMailResult, + type IOpenSearchMessageResult, + type IOpenSearchResponse, + type IOpenSearchRoomResult, + type IResponseBody, + type ISearchEngineService +} from './interfaces/search-engine-service.interface' + +export class SearchEngineService implements ISearchEngineService { + constructor( + private readonly _openSearchRepository: IOpenSearchRepository, + private readonly _matrixDBRoomsRepository: IMatrixDBRoomsRepository + ) {} + + async getMailsMessagesRoomsContainingSearchValue( + searchValue: string, + userId: string, + userEmail: string + ): Promise { + const regexp = `.*${searchValue}.*` + const operator = 'regexp' + const response = await this._openSearchRepository.searchOnMultipleIndexes( + regexp, + { + [tomRoomsIndex]: { operator, field: 'name' }, + [tomMessagesIndex]: [ + { operator, field: 'display_name' }, + { operator, field: 'content' } + ], + [tmailMailsIndex]: [ + { operator, field: 'attachments.fileName' }, + { operator, field: 'attachments.textContent' }, + { operator, field: 'bcc.address' }, + { operator, field: 'bcc.name' }, + { operator, field: 'cc.address' }, + { operator, field: 'cc.name' }, + { operator, field: 'from.address' }, + { operator, field: 'from.name' }, + { operator, field: 'to.address' }, + { operator, field: 'to.name' }, + { operator, field: 'subject' }, + { operator, field: 'textBody' }, + { operator, field: 'userFlags' } + ] + } + ) + + const userRoomsIds = await this._matrixDBRoomsRepository.getUserRoomsIds( + userId + ) + + const openSearchRoomsResult = ( + response.body.responses as Array< + IOpenSearchResponse + > + )[0].hits.hits.filter((osResult) => userRoomsIds.includes(osResult._id)) + + const openSearchMessagesResult = ( + response.body.responses as Array< + IOpenSearchResponse + > + )[1].hits.hits.filter((osResult) => + userRoomsIds.includes(osResult._source.room_id) + ) + + const openSearchMailsResult = ( + response.body.responses as Array< + IOpenSearchResponse + > + )[2].hits.hits.filter((osResult) => + osResult._source.bcc + .concat(osResult._source.cc, osResult._source.from, osResult._source.to) + .some((userDetail) => userDetail.address === userEmail) + ) + + const responseBody: IResponseBody = { + rooms: [], + messages: [], + mails: openSearchMailsResult.map((result) => ({ + id: result._id ?? result._source.messageId, + ...result._source + })) + } + + const roomsDetails = await this._matrixDBRoomsRepository.getRoomsDetails( + userRoomsIds + ) + + openSearchRoomsResult.forEach((osResult) => { + responseBody.rooms.push({ + room_id: osResult._id, + name: osResult._source.name, + avatar_url: roomsDetails[osResult._id]?.avatar + }) + }) + + const directRoomsIds = + await this._matrixDBRoomsRepository.getDirectRoomsIds(userRoomsIds) + + const directRoomsAvatarsUrls = + await this._matrixDBRoomsRepository.getDirectRoomsAvatarUrl( + directRoomsIds, + userId + ) + + openSearchMessagesResult.forEach((osResult) => { + const roomId = osResult._source.room_id + const message = { + room_id: roomId, + event_id: osResult._id, + content: osResult._source.content, + display_name: osResult._source.display_name + } + const isDirectRoom = directRoomsIds.includes(roomId) + responseBody.messages.push({ + ...message, + avatar_url: isDirectRoom + ? directRoomsAvatarsUrls[roomId] + : roomsDetails[roomId]?.avatar, + room_name: roomsDetails[roomId]?.name + }) + }) + return responseBody + } +} diff --git a/packages/tom-server/src/search-engine-api/tests/events-listener.test.ts b/packages/tom-server/src/search-engine-api/tests/events-listener.test.ts new file mode 100644 index 00000000..fd5f7ec8 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/tests/events-listener.test.ts @@ -0,0 +1,1681 @@ +import { type DbGetResult } from '@twake/matrix-identity-server' +import express, { Router } from 'express' +import supertest from 'supertest' +import TwakeServer from '../..' +import { type Config } from '../../types' +import defaultConfig from '../__testData__/config.json' +import { OpenSearchClientException } from '../utils/error' + +let testServer: TwakeServer +const homeServerToken = + 'hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL' + +const mockIndex = jest.fn().mockResolvedValue({ statusCode: 200 }) +const mockBulk = jest.fn().mockResolvedValue({ statusCode: 200 }) +const mockDelete = jest.fn().mockResolvedValue({ statusCode: 200 }) +const mockDeleteByQuery = jest.fn().mockResolvedValue({ statusCode: 200 }) +const mockUpdateByQuery = jest.fn().mockResolvedValue({ statusCode: 200 }) +const mockExists = jest.fn().mockResolvedValue({ statusCode: 200, body: true }) +const mockUpdate = jest.fn().mockResolvedValue({ statusCode: 200 }) + +jest.mock('@opensearch-project/opensearch', () => ({ + Client: jest.fn().mockImplementation(() => ({ + indices: { + exists: jest.fn().mockResolvedValue({ statusCode: 200, body: true }) + }, + index: mockIndex, + bulk: mockBulk, + delete: mockDelete, + delete_by_query: mockDeleteByQuery, + update_by_query: mockUpdateByQuery, + update: mockUpdate, + exists: mockExists, + close: jest.fn() + })) +})) + +jest.mock('@twake/matrix-identity-server', () => ({ + default: jest.fn().mockImplementation(() => ({ + ready: Promise.resolve(true), + db: {}, + userDB: {}, + api: { get: {}, post: {} }, + cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) + })), + MatrixDB: jest.fn().mockImplementation(() => ({ + ready: Promise.resolve(true), + get: jest.fn().mockResolvedValue([]), + close: jest.fn(), + getAll: jest.fn().mockResolvedValue([]) + })), + Utils: { + hostnameRe: + /^((([a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*([a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9]|[a-zA-Z])(:(\d+))?$/ + } +})) + +jest.mock('../../identity-server/index.ts', () => { + return function () { + return { + ready: Promise.resolve(true), + db: {}, + userDB: {}, + api: { get: {}, post: {} }, + cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) + } + } +}) + +jest.mock('../../application-server/index.ts', () => { + return function () { + return { + router: { + routes: Router() + } + } + } +}) + +jest.mock('../../db/index.ts', () => jest.fn()) + +describe('Search engine API - Opensearch service', () => { + let app: express.Application + let loggerErrorSpyOn: jest.SpyInstance + let transactionId = 1 + + beforeAll((done) => { + testServer = new TwakeServer(defaultConfig as Config) + loggerErrorSpyOn = jest.spyOn(testServer.logger, 'error') + testServer.ready + .then(() => { + app = express() + app.use(testServer.endpoints) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + if (testServer != null) testServer.cleanJobs() + }) + + afterEach(() => { + jest.clearAllMocks() + transactionId++ + }) + + describe('Update room name', () => { + afterEach(() => { + mockIndex.mockResolvedValue({ statusCode: 200 }) + }) + + it('should log an error when matrix db client does not find the involved room on update room name event', async () => { + jest.spyOn(testServer.matrixDb, 'get').mockResolvedValue([]) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room1', + type: 'm.room.name', + state_key: '@test:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new Error('No room stats state found with id room1').message, + {} + ) + }) + + it('should log an error when matrix db client finds multiple rooms on update room name event', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValue([ + { encryption: null }, + { encryption: 'test' } + ] as unknown as DbGetResult) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room1', + type: 'm.room.name', + state_key: '@test:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new Error('More than one room found with id room1').message, + {} + ) + }) + + it('should work if matrix db client finds the involved rooms on update room name event', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValue([{ encryption: null }] as unknown as DbGetResult) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room1', + type: 'm.room.name', + state_key: '@test:example.com', + content: { + name: 'new_name' + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + }) + + it('should call logger when opensearch client throws an error', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValue([{ encryption: null }] as unknown as DbGetResult) + + const error = new Error('An error occured in opensearch index API') + mockIndex.mockRejectedValue(error) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room1', + type: 'm.room.name', + state_key: '@test:example.com', + content: { + name: 'new_name' + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + }) + + it('should call logger when opensearch client response status code is not 20X', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValue([{ encryption: null }] as unknown as DbGetResult) + + const error = new Error('An error occured in opensearch index API') + mockIndex.mockResolvedValue({ statusCode: 502, body: error }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room1', + type: 'm.room.name', + state_key: '@test:example.com', + content: { + name: 'new_name' + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 502) + .message, + { status: '502' } + ) + }) + }) + + describe('Index message', () => { + afterEach(() => { + mockBulk.mockResolvedValue({ statusCode: 200 }) + }) + + it('should log an error when matrix db client does not find the involved room on received message event', async () => { + jest.spyOn(testServer.matrixDb, 'get').mockResolvedValue([]) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: {}, + room_id: 'room2', + type: 'm.room.message' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new Error('No room stats state found with id room2').message, + {} + ) + }) + + it('should log an error when matrix db client finds multiple rooms on received message event', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValue([ + { encryption: null }, + { encryption: 'test' } + ] as unknown as DbGetResult) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room2', + type: 'm.room.message', + content: {} + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new Error('More than one room found with id room2').message, + {} + ) + }) + + it('should log an error when matrix db client does not found membership for the sender in the room', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValueOnce([{ encryption: null }] as unknown as DbGetResult) + .mockResolvedValueOnce([]) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room2', + type: 'm.room.message', + sender: '@toto:example.com', + content: {} + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new Error( + 'No memberships found for user @toto:example.com in room room2' + ).message, + {} + ) + }) + + it('should log an error when last membership in the room for the sender is not "join"', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValueOnce([{ encryption: null }] as unknown as DbGetResult) + .mockResolvedValueOnce([ + { display_name: 'Toto', membership: 'invite' }, + { display_name: 'Toto', membership: 'join' }, + { display_name: 'Toto', membership: 'leave' } + ]) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room2', + type: 'm.room.message', + sender: '@toto:example.com', + content: {} + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new Error( + 'User @toto:example.com is not allowed to participate in room room2' + ).message, + {} + ) + }) + + it('should not call logger error method if no problem occurs', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValueOnce([ + { encryption: null, name: 'Room 2' } + ] as unknown as DbGetResult) + .mockResolvedValueOnce([ + { display_name: 'Toto', membership: 'invite' }, + { display_name: 'Toto', membership: 'join' } + ]) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room2', + type: 'm.room.message', + sender: '@toto:example.com', + content: { + body: 'Hello world', + type: 'text' + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + }) + + it('should call logger when opensearch client throws an error', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValueOnce([ + { encryption: null, name: 'Room 2' } + ] as unknown as DbGetResult) + .mockResolvedValueOnce([ + { display_name: 'Toto', membership: 'invite' }, + { display_name: 'Toto', membership: 'join' } + ]) + + const error = new Error('An error occured in opensearch bulk API') + mockBulk.mockRejectedValue(error) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room2', + type: 'm.room.message', + sender: '@toto:example.com', + content: { + body: 'Hello world', + type: 'text' + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + }) + + it('should call logger when opensearch client response status code is not 20X', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValueOnce([ + { encryption: null, name: 'Room 2' } + ] as unknown as DbGetResult) + .mockResolvedValueOnce([ + { display_name: 'Toto', membership: 'invite' }, + { display_name: 'Toto', membership: 'join' } + ]) + + const error = new Error('An error occured in opensearch bulk API') + mockBulk.mockResolvedValue({ statusCode: 503, body: error }) + + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room2', + type: 'm.room.message', + sender: '@toto:example.com', + content: { + body: 'Hello world', + type: 'text' + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 503) + .message, + { status: '503' } + ) + }) + }) + + describe('Deindex room', () => { + afterEach(() => { + mockExists.mockResolvedValue({ statusCode: 200, body: true }) + mockDelete.mockResolvedValue({ statusCode: 200 }) + mockDeleteByQuery.mockResolvedValue({ statusCode: 200 }) + }) + + it('should call logger when opensearch exists throws an error', async () => { + const error = new Error('An error occured in opensearch exists API') + mockExists.mockRejectedValue(error) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: { algorithm: 'm.megolm.v1.aes-sha2' } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + expect(mockExists).toHaveBeenCalledTimes(1) + }) + + it('should not call logger when opensearch exists response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch exists API') + mockExists.mockResolvedValue({ statusCode: 404, body: error }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: { algorithm: 'm.megolm.v1.aes-sha2' } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(1) + }) + + it('should call logger when opensearch delete throws an error', async () => { + const error = new Error('An error occured in opensearch delete API') + mockDelete.mockRejectedValue(error) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: { algorithm: 'm.megolm.v1.aes-sha2' } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(1) + }) + + it('should call logger when opensearch delete response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch delete API') + mockDelete.mockResolvedValue({ statusCode: 505, body: error }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: { algorithm: 'm.megolm.v1.aes-sha2' } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 505) + .message, + { status: '505' } + ) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(1) + }) + + it('should call logger when opensearch delete_by_query throws an error', async () => { + const error = new Error( + 'An error occured in opensearch delete_by_query API' + ) + mockDeleteByQuery.mockRejectedValue(error) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: { algorithm: 'm.megolm.v1.aes-sha2' } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(1) + expect(mockDeleteByQuery).toHaveBeenCalledTimes(1) + }) + + it('should call logger when opensearch delete_by_query response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch delete API') + mockDeleteByQuery.mockResolvedValue({ statusCode: 505, body: error }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: { algorithm: 'm.megolm.v1.aes-sha2' } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 505) + .message, + { status: '505' } + ) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(1) + expect(mockDeleteByQuery).toHaveBeenCalledTimes(1) + }) + + it('should not call opensearch exists method if event.content.algorithm is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: {} + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(0) + expect(mockDelete).toHaveBeenCalledTimes(0) + expect(mockDeleteByQuery).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch delete methods if document does not exist', async () => { + mockExists.mockResolvedValue({ statusCode: 404, body: false }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: { algorithm: 'm.megolm.v1.aes-sha2' } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(0) + expect(mockDeleteByQuery).toHaveBeenCalledTimes(1) + }) + + it('should not call logger error method if no problem occurs', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + room_id: 'room3', + state_key: '@test:example.com', + type: 'm.room.encryption', + sender: '@toto:example.com', + content: { algorithm: 'm.megolm.v1.aes-sha2' } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(1) + expect(mockDeleteByQuery).toHaveBeenCalledTimes(1) + }) + }) + + describe('Deindex message', () => { + afterEach(() => { + mockExists.mockResolvedValue({ statusCode: 200, body: true }) + mockDelete.mockResolvedValue({ statusCode: 200 }) + }) + + it('should call logger when opensearch exists throws an error', async () => { + const error = new Error('An error occured in opensearch exists API') + mockExists.mockRejectedValue(error) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + redacts: '$N1iUgYSegBr2JWThSAuEEsGznZtnbhRPmCNXKIwe6sE', + room_id: 'room4', + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + expect(mockExists).toHaveBeenCalledTimes(1) + }) + + it('should not call logger when opensearch exists response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch exists API') + mockExists.mockResolvedValue({ statusCode: 404, body: error }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + redacts: '$N1iUgYSegBr2JWThSAuEEsGznZtnbhRPmCNXKIwe6sE', + room_id: 'room4', + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(1) + }) + + it('should call logger when opensearch delete throws an error', async () => { + const error = new Error('An error occured in opensearch delete API') + mockDelete.mockRejectedValue(error) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + redacts: '$N1iUgYSegBr2JWThSAuEEsGznZtnbhRPmCNXKIwe6sE', + room_id: 'room4', + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(1) + }) + + it('should call logger when opensearch delete response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch delete API') + mockDelete.mockResolvedValue({ statusCode: 505, body: error }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + redacts: '$N1iUgYSegBr2JWThSAuEEsGznZtnbhRPmCNXKIwe6sE', + room_id: 'room4', + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 505) + .message, + { status: '505' } + ) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(1) + }) + + it('should not call opensearch delete methods if event.redacts is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + room_id: 'room4', + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(0) + expect(mockDelete).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch delete methods if event.redacts is null', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + room_id: 'room4', + redacts: null, + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(0) + expect(mockDelete).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch delete methods if event.redacts does not match event id regex', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + room_id: 'room4', + redacts: 'falsy_event_id', + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(0) + expect(mockDelete).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch delete methods if document does not exist', async () => { + mockExists.mockResolvedValue({ statusCode: 404, body: false }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + room_id: 'room4', + redacts: '$POT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(0) + }) + + it('should not call logger error method if no problem occurs', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + reason: 'Message content is invalid' + }, + event_id: '$NTT-aFQYu0MblYL81AOsvC5RB6i9uHk8TuAdH1tOg6w', + room_id: 'room4', + redacts: '$N1iUgYSegBr2JWThSAuEEsGznZtnbhRPmCNXKIwe6sE', + sender: '@lskywalker:example.com', + type: 'm.room.redaction', + user_id: '@lskywalker:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockExists).toHaveBeenCalledTimes(1) + expect(mockDelete).toHaveBeenCalledTimes(1) + }) + }) + + describe('Update display name', () => { + afterEach(() => { + mockUpdateByQuery.mockResolvedValue({ statusCode: 200 }) + }) + + it('should call logger when opensearch client throws an error', async () => { + const error = new Error( + 'An error occured in opensearch update_by_query API' + ) + mockUpdateByQuery.mockRejectedValue(error) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'new display name', + membership: 'join' + }, + room_id: 'room5', + sender: '@askywalker:example.com', + state_key: '@askywalker:example.com', + type: 'm.room.member', + unsigned: { + prev_content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'old display name', + membership: 'join' + } + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + expect(mockUpdateByQuery).toHaveBeenCalledTimes(1) + }) + + it('should call logger when opensearch client response status code is not 20X', async () => { + const error = new Error( + 'An error occured in opensearch update_by_query API' + ) + mockUpdateByQuery.mockResolvedValue({ statusCode: 506, body: error }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'new display name', + membership: 'join' + }, + room_id: 'room5', + sender: '@askywalker:example.com', + state_key: '@askywalker:example.com', + type: 'm.room.member', + unsigned: { + prev_content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'old display name', + membership: 'join' + } + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 506) + .message, + { status: '506' } + ) + expect(mockUpdateByQuery).toHaveBeenCalledTimes(1) + }) + + it('should not call opensearch update_by_query method if event.content.display_name is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: {}, + room_id: 'room5', + sender: '@askywalker:example.com', + state_key: '@askywalker:example.com', + type: 'm.room.member', + unsigned: { + prev_content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'old display name', + membership: 'join' + } + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockUpdateByQuery).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update_by_query method if event.unsigned.prev_content.display_name is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'new display name', + membership: 'join' + }, + room_id: 'room5', + sender: '@askywalker:example.com', + state_key: '@askywalker:example.com', + type: 'm.room.member', + unsigned: { + prev_content: {} + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockUpdateByQuery).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update_by_query method if event.unsigned.prev_content is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'new display name', + membership: 'join' + }, + room_id: 'room5', + sender: '@askywalker:example.com', + state_key: '@askywalker:example.com', + type: 'm.room.member', + unsigned: {} + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockUpdateByQuery).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update_by_query method if event.unsigned is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'new display name', + membership: 'join' + }, + room_id: 'room5', + sender: '@askywalker:example.com', + state_key: '@askywalker:example.com', + type: 'm.room.member' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockUpdateByQuery).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update_by_query method if display name has not changed', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'old display name', + membership: 'join' + }, + room_id: 'room5', + sender: '@askywalker:example.com', + state_key: '@askywalker:example.com', + type: 'm.room.member', + unsigned: { + prev_content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'old display name', + membership: 'join' + } + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockUpdateByQuery).toHaveBeenCalledTimes(0) + }) + + it('should not call logger error method if no problem occurs', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'new display name', + membership: 'join' + }, + room_id: 'room5', + sender: '@askywalker:example.com', + state_key: '@askywalker:example.com', + type: 'm.room.member', + unsigned: { + prev_content: { + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + displayname: 'old display name', + membership: 'join' + } + } + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockUpdateByQuery).toHaveBeenCalledTimes(1) + }) + }) + + describe('Update message content', () => { + beforeEach(() => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValue([{ encryption: null }] as unknown as DbGetResult) + }) + + afterEach(() => { + mockUpdate.mockResolvedValue({ statusCode: 200 }) + }) + + it('should call logger when opensearch client throws an error', async () => { + const error = new Error('An error occured in opensearch update API') + mockUpdate.mockRejectedValue(error) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ', + rel_type: 'm.replace' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, {}) + expect(mockUpdate).toHaveBeenCalledTimes(1) + }) + + it('should call logger when opensearch client response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch update API') + mockUpdate.mockResolvedValue({ statusCode: 506, body: error }) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ', + rel_type: 'm.replace' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 506) + .message, + { status: '506' } + ) + expect(mockUpdate).toHaveBeenCalledTimes(1) + }) + + it('should not call opensearch update method if event.content["m.new_content"] is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ', + rel_type: 'm.replace' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if event.content["m.new_content"] is null', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': null, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ', + rel_type: 'm.replace' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if event.content["m.relates_to"] is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if event.content["m.relates_to"] is null', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': null, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if event.content["m.relates_to"].event_id is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + rel_type: 'm.replace' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if event.content["m.relates_to"].event_id is null', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + event_id: null, + rel_type: 'm.replace' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if event.content["m.relates_to"].rel_type is undefined', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if event.content["m.relates_to"].rel_type is null', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ', + rel_type: null + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if event.content["m.relates_to"].rel_type is not equal to "m.replace"', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ', + rel_type: 'falsy' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call opensearch update method if it is an encrypted room', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValue([{ encryption: 'test' }] as unknown as DbGetResult) + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ', + rel_type: 'm.replace' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(mockUpdate).toHaveBeenCalledTimes(0) + }) + + it('should not call logger error method if no problem occurs', async () => { + await supertest(app) + .put(`/_matrix/app/v1/transactions/${transactionId}`) + .auth(homeServerToken, { + type: 'bearer' + }) + .send({ + events: [ + { + content: { + body: ' * But this room does not have avatar unless you add one', + 'm.mentions': {}, + 'm.new_content': { + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {}, + msgtype: 'm.text' + }, + 'm.relates_to': { + event_id: '$GGTg4DaUAHGVL_pHMAJtMz6F2cAK4cUXug-vkRG-yZQ', + rel_type: 'm.replace' + }, + msgtype: 'm.text' + }, + event_id: '$lDH6ZUNilncsYuFpkV-6xmQH9QZM3_KZVVBI95dY6XA', + room_id: 'room6', + sender: '@toto:example.com', + type: 'm.room.message', + user_id: '@toto:example.com' + } + ] + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(mockUpdate).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/packages/tom-server/src/search-engine-api/tests/index.test.ts b/packages/tom-server/src/search-engine-api/tests/index.test.ts new file mode 100644 index 00000000..1e15a2d3 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/tests/index.test.ts @@ -0,0 +1,1557 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { type TwakeLogger } from '@twake/logger' +import express from 'express' +import fs from 'fs' +import type * as http from 'http' +import * as fetch from 'node-fetch' +import os from 'os' +import path from 'path' +import supertest, { type Response } from 'supertest' +import { + DockerComposeEnvironment, + GenericContainer, + Wait, + type StartedDockerComposeEnvironment, + type StartedTestContainer +} from 'testcontainers' +import TwakeServer from '../..' +import JEST_PROCESS_ROOT_PATH from '../../../jest.globals' +import { type Config } from '../../types' +import defaultConfig from '../__testData__/config.json' +import tmailData from '../__testData__/opensearch/tmail-data.json' +import tmailMapping from '../__testData__/opensearch/tmail-mapping.json' +import { + EOpenSearchIndexingAction, + type DocumentWithIndexingAction, + type IOpenSearchRepository +} from '../repositories/interfaces/opensearch-repository.interface' +import { OpenSearchRepository } from '../repositories/opensearch.repository' +import { tmailMailsIndex } from '../utils/constantes' + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const syswideCas = require('@small-tech/syswide-cas') + +const pathToTestDataFolder = path.join( + JEST_PROCESS_ROOT_PATH, + 'src', + 'search-engine-api', + '__testData__' +) +const pathToSynapseDataFolder = path.join(pathToTestDataFolder, 'synapse-data') + +jest.unmock('node-fetch') + +describe('Search engine API - Integration tests', () => { + const matrixServer = defaultConfig.matrix_server + let openSearchContainer: GenericContainer + let openSearchStartedContainer: StartedTestContainer + let startedCompose: StartedDockerComposeEnvironment + let tokens: Record = { + askywalker: '', + lskywalker: '', + okenobi: '', + chewbacca: '' + } + + let twakeServer: TwakeServer + let app: express.Application + let expressTwakeServer: http.Server + let openSearchRepository: IOpenSearchRepository + const { + opensearch_ca_cert_path: osCaCertPath, + opensearch_host: osHost, + opensearch_password: osPwd, + opensearch_user: osUser, + ...testConfig + } = { ...defaultConfig, rate_limiting_window: 600000 } + process.env.OPENSEARCH_CA_CERT_PATH = osCaCertPath + process.env.OPENSEARCH_HOST = osHost + process.env.OPENSEARCH_PASSWORD = osPwd + process.env.OPENSEARCH_USER = osUser + + const addIndexedMailsInOpenSearch = async ( + conf: Config, + logger: TwakeLogger + ): Promise => { + openSearchRepository = new OpenSearchRepository(conf, logger) + await openSearchRepository.createIndex( + tmailMailsIndex, + tmailMapping.mailbox_v2.mappings + ) + await openSearchRepository.indexDocuments({ + [tmailMailsIndex]: tmailData.map((data) => ({ + ...data._source, + action: EOpenSearchIndexingAction.CREATE, + id: data._source.messageId + })) as unknown as DocumentWithIndexingAction[] + }) + } + + const simulationConnection = async ( + username: string, + password: string + ): Promise => { + let response = await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/login` + ) + ) + let body = (await response.json()) as any + const providerId = body.flows[0].identity_providers[0].id + response = await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/r0/login/sso/redirect/${providerId}?redirectUrl=http://localhost:9876` + ), + { + redirect: 'manual' + } + ) + let location = (response.headers.get('location') as string).replace( + 'auth.example.com', + 'auth.example.com:445' + ) + const matrixCookies = response.headers.get('set-cookie') + response = await fetch.default(location) + body = await response.text() + const hiddenInputFieldsWithValue = [ + ...(body as string).matchAll(/ `${matchElt[1]}=${matchElt[2]}&`) + .join('') + const formWithToken = `${hiddenInputFieldsWithValue}user=${username}&password=${password}` + response = await fetch.default(location, { + method: 'POST', + body: new URLSearchParams(formWithToken), + redirect: 'manual' + }) + location = (response.headers.get('location') as string).replace( + 'matrix.example.com', + 'matrix.example.com:445' + ) + response = await fetch.default(location, { + headers: { + cookie: matrixCookies as string + } + }) + body = await response.text() + const loginTokenValue = [ + ...(body as string).matchAll(/loginToken=(\S+?)"/g) + ][0][1] + response = await fetch.default( + encodeURI(`https://${matrixServer}/_matrix/client/v3/login`), + { + method: 'POST', + body: JSON.stringify({ + initial_device_display_name: 'Jest Test Client', + token: loginTokenValue, + type: 'm.login.token' + }) + } + ) + body = (await response.json()) as any + return body.access_token as string + } + + const connectMultipleUsers = async ( + usersCredentials: Array<{ username: string; password: string }> + ): Promise => { + const tokens: string[] = [] + for (let i = 0; i < usersCredentials.length; i++) { + const token = await simulationConnection( + usersCredentials[i].username, + usersCredentials[i].password + ) + tokens.push(token as string) + } + return tokens + } + + const createRoom = async ( + token: string, + invitations: string[] = [], + name?: string, + isDirect?: boolean, + initialState?: Array> + ): Promise => { + if ((isDirect == null || !isDirect) && name == null) { + throw Error('Name must be defined for an undirect room') + } + let requestBody = {} + requestBody = name != null ? { ...requestBody, name } : requestBody + requestBody = + isDirect != null ? { ...requestBody, is_direct: isDirect } : requestBody + requestBody = + invitations != null + ? { ...requestBody, invite: invitations } + : requestBody + requestBody = + initialState != null + ? { ...requestBody, initial_state: initialState } + : requestBody + + const response = await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/createRoom` + ), + { + method: 'POST', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(requestBody) + } + ) + const responseBody = (await response.json()) as Record + return responseBody.room_id + } + + const joinRoom = async (roomId: string, token: string): Promise => { + await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/join/${roomId}` + ), + { + method: 'POST', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${token}` + } + } + ) + } + + const addAvatar = async (userId: string, token: string): Promise => { + await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/profile/${userId}/avatar_url` + ), + { + method: 'PUT', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34' + }) + } + ) + } + + interface IFileInfo { + h: number + mimetype: string + size: number + w: number + 'xyz.amorgan.blurhash': string + } + + interface IFile extends IFileInfo { + thumbnail_url?: string + thumbnail_info: IFileInfo + } + + interface IMessage { + body: string + filename?: string + msgtype: string + url?: string + info?: IFile + } + + const sendMessage = async ( + token: string, + roomId: string, + message: IMessage, + filePath?: string + ): Promise> => { + if (message.msgtype === 'm.image') { + const file = fs.readFileSync(filePath as string) + await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/media/v3/upload?filename=${message.filename}` + ), + { + method: 'POST', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${token}`, + 'content-type': 'image/jpeg' + }, + body: Buffer.from(file).toString('base64') + } + ) + } + + const response = await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/rooms/${roomId}/send/m.room.message/${Math.random()}` + ), + { + method: 'PUT', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${token}` + }, + body: JSON.stringify(message) + } + ) + const body = (await response.json()) as Record + return body + } + + beforeAll((done) => { + syswideCas.addCAs(path.join(pathToTestDataFolder, 'nginx', 'ssl', 'ca.pem')) + new DockerComposeEnvironment( + path.join(pathToTestDataFolder), + 'docker-compose.yml' + ) + .withEnvironment({ MYUID: os.userInfo().uid.toString() }) + .withWaitStrategy('postgresql-tom', Wait.forHealthCheck()) + .withWaitStrategy('synapse-tom', Wait.forHealthCheck()) + .up() + // eslint-disable-next-line @typescript-eslint/promise-function-async + .then((upResult) => { + startedCompose = upResult + + openSearchContainer = new GenericContainer( + 'opensearchproject/opensearch' + ) + .withHealthCheck({ + test: [ + 'CMD', + 'curl', + 'http://localhost:9200', + '-ku', + "'admin:admin'" + ], + interval: 10000, + timeout: 10000, + retries: 3 + }) + .withNetworkMode( + startedCompose.getContainer('nginx-proxy-tom').getNetworkNames()[0] + ) + .withEnvironment({ + 'discovery.type': 'single-node', + DISABLE_INSTALL_DEMO_CONFIG: 'true', + DISABLE_SECURITY_PLUGIN: 'true', + VIRTUAL_PORT: '9200', + VIRTUAL_HOST: 'opensearch.example.com' + }) + .withWaitStrategy(Wait.forHealthCheck()) + + return openSearchContainer.start() + }) + // eslint-disable-next-line @typescript-eslint/promise-function-async + .then((startedContainer) => { + openSearchStartedContainer = startedContainer + return startedCompose.getContainer('nginx-proxy-tom').restart() + }) + // eslint-disable-next-line @typescript-eslint/promise-function-async + .then(() => { + twakeServer = new TwakeServer(testConfig as Config) + app = express() + return twakeServer.ready + }) + // eslint-disable-next-line @typescript-eslint/promise-function-async + .then(() => { + app.use(twakeServer.endpoints) + return new Promise((resolve, reject) => { + expressTwakeServer = app.listen(3002, () => { + resolve() + }) + }) + }) + // eslint-disable-next-line @typescript-eslint/promise-function-async + .then(() => { + return addIndexedMailsInOpenSearch(twakeServer.conf, twakeServer.logger) + }) + // eslint-disable-next-line @typescript-eslint/promise-function-async + .then(() => { + return connectMultipleUsers([ + { username: 'askywalker', password: 'askywalker' }, + { username: 'lskywalker', password: 'lskywalker' }, + { username: 'okenobi', password: 'okenobi' }, + { username: 'chewbacca', password: 'chewbacca' } + ]) + }) + .then((usersTokens) => { + if (usersTokens == null || usersTokens.some((t) => t == null)) { + throw new Error('Error during user authentication') + } + const usersTokensChecked = usersTokens + tokens = { + askywalker: usersTokensChecked[0], + lskywalker: usersTokensChecked[1], + okenobi: usersTokensChecked[2], + chewbacca: usersTokensChecked[3] + } + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll((done) => { + const filesToDelete: string[] = [ + path.join(pathToSynapseDataFolder, 'matrix.example.com.signing.key'), + path.join(pathToSynapseDataFolder, 'homeserver.log'), + path.join(pathToSynapseDataFolder, 'media_store') + ] + filesToDelete.forEach((path: string) => { + if (fs.existsSync(path)) { + const isDir = fs.statSync(path).isDirectory() + isDir + ? fs.rmSync(path, { recursive: true, force: true }) + : fs.unlinkSync(path) + } + }) + if (openSearchRepository != null) openSearchRepository.close() + if (twakeServer != null) twakeServer.cleanJobs() + if (expressTwakeServer != null) { + expressTwakeServer.close((err) => { + if (startedCompose != null) { + startedCompose + .down() + .then(() => { + err != null ? done(err) : done() + }) + .catch((e) => { + done(e) + }) + } else if (err != null) { + done(err) + } else { + done() + } + }) + } else { + done() + } + }) + + let roomGroupId1: string + let roomGroupId2: string + let roomGroupId3: string + let roomGroupId4: string + let roomIdDirect: string + let roomIdEncrypted: string + + it('should find rooms matching search', async () => { + await addAvatar('@lskywalker:example.com', tokens.lskywalker) + roomGroupId1 = await createRoom( + tokens.askywalker, + ['@lskywalker:example.com', '@okenobi:example.com'], + 'test skywalkers room', + false, + [ + { + content: { url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR' }, + state_key: '', + type: 'm.room.avatar' + } + ] + ) + await Promise.all( + // eslint-disable-next-line @typescript-eslint/promise-function-async + ['lskywalker', 'okenobi'].map>((uid) => + joinRoom(roomGroupId1, tokens[uid]) + ) + ) + + roomGroupId2 = await createRoom( + tokens.okenobi, + ['@askywalker:example.com', '@lskywalker:example.com'], + 'test okenobi room' + ) + await Promise.all( + // eslint-disable-next-line @typescript-eslint/promise-function-async + ['askywalker', 'lskywalker'].map>((uid) => + joinRoom(roomGroupId2, tokens[uid]) + ) + ) + + roomGroupId3 = await createRoom( + tokens.okenobi, + ['@lskywalker:example.com'], + 'test skywalkers room without Anakin' + ) + await joinRoom(roomGroupId3, tokens.lskywalker) + + roomGroupId4 = await createRoom( + tokens.askywalker, + ['@lskywalker:example.com'], + 'test skywalkers room without avatar' + ) + await joinRoom(roomGroupId4, tokens.lskywalker) + + roomIdDirect = await createRoom( + tokens.lskywalker, + ['@askywalker:example.com'], + undefined, + true + ) + await joinRoom(roomIdDirect, tokens.askywalker) + + roomIdEncrypted = await createRoom( + tokens.askywalker, + ['@lskywalker:example.com', '@okenobi:example.com'], + 'test encrypted skywalkers room', + false, + [ + { + content: { algorithm: 'm.megolm.v1.aes-sha2' }, + type: 'm.room.encryption' + } + ] + ) + await Promise.all( + // eslint-disable-next-line @typescript-eslint/promise-function-async + ['lskywalker', 'okenobi'].map>((uid) => + joinRoom(roomIdEncrypted, tokens[uid]) + ) + ) + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.askywalker}`) + .send({ + searchValue: 'sky' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(2) + expect(response.body.messages).toHaveLength(0) + expect(response.body.mails).toHaveLength(0) + expect(response.body.rooms).toEqual( + expect.arrayContaining([ + { + room_id: roomGroupId1, + name: 'test skywalkers room', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR' + }, + { + room_id: roomGroupId4, + name: 'test skywalkers room without avatar', + avatar_url: null + } + ]) + ) + }) + + let msg1EventId: string + let msg2EventId: string + let msg3EventId: string + let msg4EventId: string + let msg5EventId: string + let msg6EventId: string + let msg7EventId: string + let msg8EventId: string + let msg9EventId: string + let msg10EventId: string + let msg11EventId: string + let msg12EventId: string + let msg13EventId: string + let msg14EventId: string + let msg15EventId: string + + it('should find messages matching search', async () => { + msg1EventId = ( + await sendMessage(tokens.askywalker, roomGroupId1, { + body: 'Hello members', + msgtype: 'm.text' + }) + ).event_id + msg2EventId = ( + await sendMessage(tokens.lskywalker, roomGroupId1, { + body: 'Hello others', + msgtype: 'm.text' + }) + ).event_id + msg3EventId = ( + await sendMessage(tokens.okenobi, roomGroupId1, { + body: 'Hello Anakin Skywalker', + msgtype: 'm.text' + }) + ).event_id + + msg4EventId = ( + await sendMessage(tokens.lskywalker, roomIdDirect, { + body: 'Hello Anakin this is Luke, it is a direct message', + msgtype: 'm.text' + }) + ).event_id + msg5EventId = ( + await sendMessage(tokens.askywalker, roomIdDirect, { + body: 'Hey Luke', + msgtype: 'm.text' + }) + ).event_id + msg6EventId = ( + await sendMessage(tokens.lskywalker, roomIdDirect, { + body: 'How are you dad?', + msgtype: 'm.text' + }) + ).event_id + + msg7EventId = ( + await sendMessage(tokens.okenobi, roomGroupId2, { + body: 'Hello this is Obi-Wan, admin of this room', + msgtype: 'm.text' + }) + ).event_id + msg8EventId = ( + await sendMessage(tokens.askywalker, roomGroupId2, { + body: 'Hello master, I will be late', + msgtype: 'm.text' + }) + ).event_id + msg9EventId = ( + await sendMessage(tokens.lskywalker, roomGroupId2, { + body: 'Hye this is Luke', + msgtype: 'm.text' + }) + ).event_id + + msg10EventId = ( + await sendMessage(tokens.okenobi, roomGroupId3, { + body: 'Hello this is Obi-Wan, Anakin is not a member of this room', + msgtype: 'm.text' + }) + ).event_id + msg11EventId = ( + await sendMessage(tokens.lskywalker, roomGroupId3, { + body: 'Hello Obi-Wan, we should invite him', + msgtype: 'm.text' + }) + ).event_id + + msg12EventId = ( + await sendMessage(tokens.lskywalker, roomGroupId4, { + body: 'Hello this is Luke, anakin is a member of this room', + msgtype: 'm.text' + }) + ).event_id + msg13EventId = ( + await sendMessage(tokens.lskywalker, roomGroupId4, { + body: 'But this room does not have avatar', + msgtype: 'm.text' + }) + ).event_id + + msg14EventId = ( + await sendMessage( + tokens.askywalker, + roomGroupId1, + { + body: 'anakin-at-the-office.jpg', + msgtype: 'm.image', + filename: 'anakin-at-the-office.jpg' + }, + path.join(pathToTestDataFolder, 'images', 'anakin-at-the-office.jpg') + ) + ).event_id + msg15EventId = ( + await sendMessage(tokens.askywalker, roomIdEncrypted, { + body: 'Hey this is Anakin, we are in an encrypted room', + msgtype: 'm.text' + }) + ).event_id + await new Promise((resolve, _reject) => { + setTimeout(() => { + resolve() + }, 3000) + }) + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.askywalker}`) + .send({ + searchValue: 'anak' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(0) + expect(response.body.messages).toHaveLength(7) + expect(response.body.mails).toHaveLength(0) + expect(response.body.messages).toEqual( + expect.arrayContaining([ + { + room_id: roomGroupId1, + event_id: msg1EventId, + content: 'Hello members', + display_name: 'Anakin Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'test skywalkers room' + }, + { + room_id: roomGroupId1, + event_id: msg3EventId, + content: 'Hello Anakin Skywalker', + display_name: 'Obi-Wan Kenobi', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'test skywalkers room' + }, + { + room_id: roomIdDirect, + event_id: msg4EventId, + content: 'Hello Anakin this is Luke, it is a direct message', + display_name: 'Luke Skywalker', + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + room_name: null + }, + { + room_id: roomIdDirect, + event_id: msg5EventId, + content: 'Hey Luke', + display_name: 'Anakin Skywalker', + avatar_url: 'mxc://matrix.org/wefh34uihSDRGhw34', + room_name: null + }, + { + room_id: roomGroupId2, + event_id: msg8EventId, + content: 'Hello master, I will be late', + display_name: 'Anakin Skywalker', + avatar_url: null, + room_name: 'test okenobi room' + }, + { + room_id: roomGroupId4, + event_id: msg12EventId, + content: 'Hello this is Luke, anakin is a member of this room', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + }, + { + room_id: roomGroupId1, + event_id: msg14EventId, + content: 'anakin-at-the-office.jpg', + display_name: 'Anakin Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'test skywalkers room' + } + ]) + ) + }) + + it('should find mails matching search', async () => { + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'ay' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(0) + expect(response.body.messages).toHaveLength(0) + expect(response.body.mails).toHaveLength(3) + expect(response.body.mails).toEqual( + expect.arrayContaining([ + { + ...tmailData[0]._source, + id: tmailData[0]._source.messageId + }, + { + ...tmailData[3]._source, + id: tmailData[3]._source.messageId + }, + { + ...tmailData[4]._source, + id: tmailData[4]._source.messageId + } + ]) + ) + }) + + let expectedRooms: Array<{ + room_id: string + name: string + avatar_url: string | null + }> + + let expectedMessages: Array<{ + room_id: string + event_id: string + content: string + display_name: string | null + avatar_url: string | null + room_name: string | null + }> + + const expectedMails = [ + { + ...tmailData[0]._source, + id: tmailData[0]._source.messageId + }, + { + ...tmailData[1]._source, + id: tmailData[1]._source.messageId + }, + { + ...tmailData[3]._source, + id: tmailData[3]._source.messageId + }, + { + ...tmailData[4]._source, + id: tmailData[4]._source.messageId + } + ] + it('should find rooms, messages and mails matching search', async () => { + expectedRooms = [ + { + room_id: roomGroupId1, + name: 'test skywalkers room', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR' + }, + { + room_id: roomGroupId4, + name: 'test skywalkers room without avatar', + avatar_url: null + }, + { + room_id: roomGroupId3, + name: 'test skywalkers room without Anakin', + avatar_url: null + } + ] + + expectedMessages = [ + { + room_id: roomGroupId1, + event_id: msg1EventId, + content: 'Hello members', + display_name: 'Anakin Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'test skywalkers room' + }, + { + room_id: roomGroupId1, + event_id: msg2EventId, + content: 'Hello others', + display_name: 'Luke Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'test skywalkers room' + }, + { + room_id: roomGroupId1, + event_id: msg3EventId, + content: 'Hello Anakin Skywalker', + display_name: 'Obi-Wan Kenobi', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'test skywalkers room' + }, + { + room_id: roomIdDirect, + event_id: msg4EventId, + content: 'Hello Anakin this is Luke, it is a direct message', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomIdDirect, + event_id: msg5EventId, + content: 'Hey Luke', + display_name: 'Anakin Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomIdDirect, + event_id: msg6EventId, + content: 'How are you dad?', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomGroupId2, + event_id: msg8EventId, + content: 'Hello master, I will be late', + display_name: 'Anakin Skywalker', + avatar_url: null, + room_name: 'test okenobi room' + }, + { + room_id: roomGroupId2, + event_id: msg9EventId, + content: 'Hye this is Luke', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test okenobi room' + }, + { + room_id: roomGroupId3, + event_id: msg11EventId, + content: 'Hello Obi-Wan, we should invite him', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without Anakin' + }, + { + room_id: roomGroupId4, + event_id: msg12EventId, + content: 'Hello this is Luke, anakin is a member of this room', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + }, + { + room_id: roomGroupId4, + event_id: msg13EventId, + content: 'But this room does not have avatar', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + }, + { + room_id: roomGroupId1, + event_id: msg14EventId, + content: 'anakin-at-the-office.jpg', + display_name: 'Anakin Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'test skywalkers room' + } + ] + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'skywalker' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(3) + expect(response.body.messages).toHaveLength(12) + expect(response.body.mails).toHaveLength(4) + expect(response.body.rooms).toEqual(expect.arrayContaining(expectedRooms)) + expect(response.body.messages).toEqual( + expect.arrayContaining(expectedMessages) + ) + expect(response.body.mails).toEqual(expect.arrayContaining(expectedMails)) + }) + + it('should find rooms, messages and mails matching search after opensearch container and ToM server restart', async () => { + if (openSearchRepository != null) openSearchRepository.close() + if (twakeServer != null) twakeServer.cleanJobs() + await new Promise((resolve, reject) => { + expressTwakeServer.close((err) => { + if (err != null) { + reject(err) + } + resolve() + }) + }) + + await openSearchStartedContainer.stop() + await openSearchContainer.start() + + console.info('Server closed. Restarting.') + + await startedCompose.getContainer('nginx-proxy-tom').restart() + + twakeServer = new TwakeServer(testConfig as Config) + app = express() + await twakeServer.ready + app.use(twakeServer.endpoints) + await new Promise((resolve, reject) => { + expressTwakeServer = app.listen(3002, () => { + resolve() + }) + }) + await addIndexedMailsInOpenSearch(twakeServer.conf, twakeServer.logger) + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve() + }, 3000) + }) + console.info('Server is listening to port 3002.') + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'skywalker' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(3) + expect(response.body.messages).toHaveLength(12) + expect(response.body.mails).toHaveLength(4) + expect(response.body.rooms).toEqual(expect.arrayContaining(expectedRooms)) + expect(response.body.messages).toEqual( + expect.arrayContaining(expectedMessages) + ) + expect(response.body.mails).toEqual(expect.arrayContaining(expectedMails)) + }) + + it('should find rooms, messages and mails matching search after manual restore', async () => { + let response = await supertest(app).post( + '/_twake/app/v1/opensearch/restore' + ) + expect(response.statusCode).toBe(204) + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve() + }, 3000) + }) + + response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'skywalker' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(3) + expect(response.body.messages).toHaveLength(12) + expect(response.body.mails).toHaveLength(4) + expect(response.body.rooms).toEqual(expect.arrayContaining(expectedRooms)) + expect(response.body.messages).toEqual( + expect.arrayContaining(expectedMessages) + ) + expect(response.body.mails).toEqual(expect.arrayContaining(expectedMails)) + }) + + it('should find rooms matching search after update room name', async () => { + await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/rooms/${roomGroupId1}/state/m.room.name/` + ), + { + method: 'PUT', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${tokens.askywalker}` + }, + body: JSON.stringify({ name: 'Skywalkers room updated' }) + } + ) + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve() + }, 3000) + }) + + expectedRooms = [ + { + room_id: roomGroupId1, + name: 'Skywalkers room updated', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR' + }, + { + room_id: roomGroupId4, + name: 'test skywalkers room without avatar', + avatar_url: null + }, + { + room_id: roomGroupId3, + name: 'test skywalkers room without Anakin', + avatar_url: null + } + ] + + expectedMessages = [ + { + room_id: roomGroupId1, + event_id: msg1EventId, + content: 'Hello members', + display_name: 'Anakin Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'Skywalkers room updated' + }, + { + room_id: roomGroupId1, + event_id: msg2EventId, + content: 'Hello others', + display_name: 'Luke Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'Skywalkers room updated' + }, + { + room_id: roomGroupId1, + event_id: msg3EventId, + content: 'Hello Anakin Skywalker', + display_name: 'Obi-Wan Kenobi', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'Skywalkers room updated' + }, + { + room_id: roomIdDirect, + event_id: msg4EventId, + content: 'Hello Anakin this is Luke, it is a direct message', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomIdDirect, + event_id: msg5EventId, + content: 'Hey Luke', + display_name: 'Anakin Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomIdDirect, + event_id: msg6EventId, + content: 'How are you dad?', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomGroupId2, + event_id: msg8EventId, + content: 'Hello master, I will be late', + display_name: 'Anakin Skywalker', + avatar_url: null, + room_name: 'test okenobi room' + }, + { + room_id: roomGroupId2, + event_id: msg9EventId, + content: 'Hye this is Luke', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test okenobi room' + }, + { + room_id: roomGroupId3, + event_id: msg11EventId, + content: 'Hello Obi-Wan, we should invite him', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without Anakin' + }, + { + room_id: roomGroupId4, + event_id: msg12EventId, + content: 'Hello this is Luke, anakin is a member of this room', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + }, + { + room_id: roomGroupId4, + event_id: msg13EventId, + content: 'But this room does not have avatar', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + }, + { + room_id: roomGroupId1, + event_id: msg14EventId, + content: 'anakin-at-the-office.jpg', + display_name: 'Anakin Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'Skywalkers room updated' + } + ] + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'skywalker' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(3) + expect(response.body.messages).toHaveLength(12) + expect(response.body.mails).toHaveLength(4) + expect(response.body.rooms).toEqual(expect.arrayContaining(expectedRooms)) + expect(response.body.messages).toEqual( + expect.arrayContaining(expectedMessages) + ) + expect(response.body.mails).toEqual(expect.arrayContaining(expectedMails)) + }) + + it('should find messages matching search after display name update', async () => { + await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/profile/@askywalker:example.com/displayname` + ), + { + method: 'PUT', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${tokens.askywalker}` + }, + body: JSON.stringify({ displayname: 'Dark Vador' }) + } + ) + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve() + }, 3000) + }) + + expectedMessages = [ + { + room_id: roomGroupId1, + event_id: msg2EventId, + content: 'Hello others', + display_name: 'Luke Skywalker', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'Skywalkers room updated' + }, + { + room_id: roomGroupId1, + event_id: msg3EventId, + content: 'Hello Anakin Skywalker', + display_name: 'Obi-Wan Kenobi', + avatar_url: 'mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR', + room_name: 'Skywalkers room updated' + }, + { + room_id: roomIdDirect, + event_id: msg4EventId, + content: 'Hello Anakin this is Luke, it is a direct message', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomIdDirect, + event_id: msg6EventId, + content: 'How are you dad?', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomGroupId2, + event_id: msg9EventId, + content: 'Hye this is Luke', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test okenobi room' + }, + { + room_id: roomGroupId3, + event_id: msg11EventId, + content: 'Hello Obi-Wan, we should invite him', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without Anakin' + }, + { + room_id: roomGroupId4, + event_id: msg12EventId, + content: 'Hello this is Luke, anakin is a member of this room', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + }, + { + room_id: roomGroupId4, + event_id: msg13EventId, + content: 'But this room does not have avatar', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + } + ] + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'skywalker' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(3) + expect(response.body.messages).toHaveLength(8) + expect(response.body.mails).toHaveLength(4) + expect(response.body.rooms).toEqual(expect.arrayContaining(expectedRooms)) + expect(response.body.messages).toEqual( + expect.arrayContaining(expectedMessages) + ) + expect(response.body.mails).toEqual(expect.arrayContaining(expectedMails)) + }) + + it("should remove room and room's messages from results after room encryption", async () => { + await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/rooms/${roomGroupId1}/state/m.room.encryption` + ), + { + method: 'PUT', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${tokens.askywalker}` + }, + body: JSON.stringify({ algorithm: 'm.megolm.v1.aes-sha2' }) + } + ) + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve() + }, 3000) + }) + + expectedRooms = [ + { + room_id: roomGroupId4, + name: 'test skywalkers room without avatar', + avatar_url: null + }, + { + room_id: roomGroupId3, + name: 'test skywalkers room without Anakin', + avatar_url: null + } + ] + + expectedMessages = [ + { + room_id: roomIdDirect, + event_id: msg4EventId, + content: 'Hello Anakin this is Luke, it is a direct message', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomIdDirect, + event_id: msg6EventId, + content: 'How are you dad?', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomGroupId2, + event_id: msg9EventId, + content: 'Hye this is Luke', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test okenobi room' + }, + { + room_id: roomGroupId3, + event_id: msg11EventId, + content: 'Hello Obi-Wan, we should invite him', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without Anakin' + }, + { + room_id: roomGroupId4, + event_id: msg12EventId, + content: 'Hello this is Luke, anakin is a member of this room', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + }, + { + room_id: roomGroupId4, + event_id: msg13EventId, + content: 'But this room does not have avatar', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + } + ] + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'skywalker' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(2) + expect(response.body.messages).toHaveLength(6) + expect(response.body.mails).toHaveLength(4) + expect(response.body.rooms).toEqual(expect.arrayContaining(expectedRooms)) + expect(response.body.messages).toEqual( + expect.arrayContaining(expectedMessages) + ) + expect(response.body.mails).toEqual(expect.arrayContaining(expectedMails)) + }) + + it('should remove message from results after deleting message', async () => { + await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/rooms/${roomGroupId4}/redact/${msg12EventId}/123` + ), + { + method: 'PUT', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${tokens.lskywalker}` + }, + body: JSON.stringify({ reason: 'Message content is invalid' }) + } + ) + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve() + }, 3000) + }) + + expectedMessages = [ + { + room_id: roomIdDirect, + event_id: msg4EventId, + content: 'Hello Anakin this is Luke, it is a direct message', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomIdDirect, + event_id: msg6EventId, + content: 'How are you dad?', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: null + }, + { + room_id: roomGroupId2, + event_id: msg9EventId, + content: 'Hye this is Luke', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test okenobi room' + }, + { + room_id: roomGroupId3, + event_id: msg11EventId, + content: 'Hello Obi-Wan, we should invite him', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without Anakin' + }, + { + room_id: roomGroupId4, + event_id: msg13EventId, + content: 'But this room does not have avatar', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + } + ] + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'skywalker' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(2) + expect(response.body.messages).toHaveLength(5) + expect(response.body.mails).toHaveLength(4) + expect(response.body.rooms).toEqual(expect.arrayContaining(expectedRooms)) + expect(response.body.messages).toEqual( + expect.arrayContaining(expectedMessages) + ) + expect(response.body.mails).toEqual(expect.arrayContaining(expectedMails)) + }) + + it('results should contain message with correct content after updating message content', async () => { + await fetch.default( + encodeURI( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `https://${matrixServer}/_matrix/client/v3/rooms/${roomGroupId4}/send/m.room.message/124` + ), + { + method: 'PUT', + headers: { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + Authorization: `Bearer ${tokens.lskywalker}` + }, + body: JSON.stringify({ + msgtype: 'm.text', + body: ' * But this room does not have avatar unless you add one', + 'm.new_content': { + msgtype: 'm.text', + body: 'But this room does not have avatar unless you add one', + 'm.mentions': {} + }, + 'm.mentions': {}, + 'm.relates_to': { + rel_type: 'm.replace', + event_id: msg13EventId + } + }) + } + ) + await new Promise((resolve, reject) => { + setTimeout(() => { + resolve() + }, 3000) + }) + + expectedMessages[expectedMessages.length - 1] = { + room_id: roomGroupId4, + event_id: msg13EventId, + content: 'But this room does not have avatar unless you add one', + display_name: 'Luke Skywalker', + avatar_url: null, + room_name: 'test skywalkers room without avatar' + } + + const response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${tokens.lskywalker}`) + .send({ + searchValue: 'skywalker' + }) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(2) + expect(response.body.messages).toHaveLength(5) + expect(response.body.mails).toHaveLength(4) + expect(response.body.rooms).toEqual(expect.arrayContaining(expectedRooms)) + expect(response.body.messages).toEqual( + expect.arrayContaining(expectedMessages) + ) + expect(response.body.mails).toEqual(expect.arrayContaining(expectedMails)) + }) + + it('should reject if more than 100 requests are done in less than 10 seconds', async () => { + let response + let token + // eslint-disable-next-line @typescript-eslint/no-for-in-array, @typescript-eslint/no-unused-vars + for (const i in [...Array(101).keys()]) { + token = + Number(i) % 2 === 0 ? `Bearer ${tokens.lskywalker}` : 'falsy_token' + response = await supertest(app) + .post('/_twake/app/v1/search') + .set('Accept', 'application/json') + .set('Authorization', token) + .send({ + searchValue: 'skywalker' + }) + } + expect((response as Response).statusCode).toEqual(429) + await new Promise((resolve) => setTimeout(resolve, 11000)) + }) +}) diff --git a/packages/tom-server/src/search-engine-api/tests/opensearch-controller.test.ts b/packages/tom-server/src/search-engine-api/tests/opensearch-controller.test.ts new file mode 100644 index 00000000..5f52ff60 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/tests/opensearch-controller.test.ts @@ -0,0 +1,243 @@ +import express, { Router } from 'express' +import supertest from 'supertest' +import TwakeServer from '../..' +import { type Config } from '../../types' +import defaultConfig from '../__testData__/config.json' +import { OpenSearchClientException } from '../utils/error' + +let testServer: TwakeServer +const mockOpenSearchExists = jest + .fn() + .mockResolvedValue({ statusCode: 200, body: true }) + +const mockOpenSearchCreate = jest.fn().mockResolvedValue({ statusCode: 200 }) + +const mockOpenSearchBulk = jest.fn().mockResolvedValue({ statusCode: 200 }) + +jest.mock('@opensearch-project/opensearch', () => ({ + Client: jest.fn().mockImplementation(() => ({ + indices: { + exists: mockOpenSearchExists, + create: mockOpenSearchCreate + }, + bulk: mockOpenSearchBulk, + close: jest.fn() + })) +})) + +jest.mock('@twake/matrix-identity-server', () => ({ + default: jest.fn().mockImplementation(() => ({ + ready: Promise.resolve(true), + db: {}, + userDB: {}, + api: { get: {}, post: {} }, + cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) + })), + MatrixDB: jest.fn().mockImplementation(() => ({ + ready: Promise.resolve(true), + get: jest + .fn() + .mockResolvedValueOnce([ + { user_id: '@toto:example.com', display_name: 'Toto' } + ]) + .mockResolvedValueOnce([ + { + event_id: 'event1', + room_id: 'room1', + json: '{"type":"m.room.message","content":{"body":"Hello world","type":"text"}}' + } + ]) + .mockResolvedValueOnce([ + { user_id: '@toto:example.com', display_name: 'Toto' } + ]) + .mockResolvedValueOnce([ + { + event_id: 'event1', + room_id: 'room1', + json: '{"type":"m.room.message","content":{"body":"Hello world","type":"text"}}' + } + ]), + getAll: jest.fn().mockResolvedValue([ + { + room_id: 'room1', + encryption: null, + name: 'Room1' + } + ]), + close: jest.fn() + })), + Utils: { + hostnameRe: + /^((([a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*([a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9]|[a-zA-Z])(:(\d+))?$/ + } +})) + +jest.mock('../../identity-server/index.ts', () => { + return function () { + return { + ready: Promise.resolve(true), + db: {}, + userDB: {}, + api: { get: {}, post: {} }, + cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) + } + } +}) + +jest.mock('../../application-server/index.ts', () => { + return function () { + return { + router: { + routes: Router() + } + } + } +}) + +jest.mock('../../db/index.ts', () => jest.fn()) + +describe('Search engine API - Opensearch controller', () => { + let app: express.Application + let loggerErrorSpyOn: jest.SpyInstance + const restoreRoute = '/_twake/app/v1/opensearch/restore' + + beforeAll((done) => { + testServer = new TwakeServer(defaultConfig as Config) + loggerErrorSpyOn = jest.spyOn(testServer.logger, 'error') + testServer.ready + .then(() => { + app = express() + app.use(testServer.endpoints) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + if (testServer != null) testServer.cleanJobs() + }) + + afterEach(() => { + jest.clearAllMocks() + mockOpenSearchExists.mockResolvedValue({ statusCode: 200, body: true }) + mockOpenSearchCreate.mockResolvedValue({ statusCode: 200 }) + mockOpenSearchBulk.mockResolvedValue({ statusCode: 200 }) + }) + + it('should log an error when an error occured with opensearch exists API', async () => { + const error = new Error('An error occured in opensearch exists API') + mockOpenSearchExists.mockRejectedValue(error) + const response = await supertest(app).post(restoreRoute).send({}) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, { + httpMethod: 'POST', + endpointPath: restoreRoute + }) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ + error: 'Internal server error' + }) + }) + + it('should call logger when opensearch client response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch exists API') + mockOpenSearchExists.mockResolvedValue({ statusCode: 501, body: error }) + const response = await supertest(app).post(restoreRoute).send({}) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 501) + .message, + { + httpMethod: 'POST', + endpointPath: restoreRoute, + status: '501' + } + ) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ + error: 'Internal server error' + }) + }) + + it('should log an error when an error occured with opensearch create index API', async () => { + const error = new Error('An error occured in opensearch create index API') + mockOpenSearchExists.mockResolvedValue({ statusCode: 404, body: false }) + mockOpenSearchCreate.mockRejectedValue(error) + const response = await supertest(app).post(restoreRoute).send({}) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, { + httpMethod: 'POST', + endpointPath: restoreRoute + }) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ + error: 'Internal server error' + }) + }) + + it('should call logger when opensearch client response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch create index API') + mockOpenSearchExists.mockResolvedValue({ statusCode: 404, body: false }) + mockOpenSearchCreate.mockResolvedValue({ statusCode: 502, body: error }) + const response = await supertest(app).post(restoreRoute).send({}) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 502) + .message, + { + httpMethod: 'POST', + endpointPath: restoreRoute, + status: '502' + } + ) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ + error: 'Internal server error' + }) + }) + + it('should log an error when an error occured with opensearch bulk API', async () => { + const error = new Error('An error occured in opensearch bulk API') + mockOpenSearchExists.mockResolvedValue({ statusCode: 404, body: false }) + mockOpenSearchBulk.mockRejectedValue(error) + const response = await supertest(app).post(restoreRoute).send({}) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith(error.message, { + httpMethod: 'POST', + endpointPath: restoreRoute + }) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ + error: 'Internal server error' + }) + }) + + it('should call logger when opensearch client response status code is not 20X', async () => { + const error = new Error('An error occured in opensearch bulk API') + mockOpenSearchBulk.mockResolvedValue({ statusCode: 504, body: error }) + const response = await supertest(app).post(restoreRoute).send({}) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException(JSON.stringify(error, null, 2), 504) + .message, + { + httpMethod: 'POST', + endpointPath: restoreRoute, + status: '504' + } + ) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ + error: 'Internal server error' + }) + }) + + it('should send a response with status 204 if no error occurs', async () => { + const response = await supertest(app).post(restoreRoute).send({}) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(response.statusCode).toBe(204) + expect(response.body).toEqual({}) + }) +}) diff --git a/packages/tom-server/src/search-engine-api/tests/search-engine-controller.test.ts b/packages/tom-server/src/search-engine-api/tests/search-engine-controller.test.ts new file mode 100644 index 00000000..44ed5c42 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/tests/search-engine-controller.test.ts @@ -0,0 +1,394 @@ +import { AppServerAPIError } from '@twake/matrix-application-server' +import { type DbGetResult } from '@twake/matrix-identity-server' +import express, { Router, type NextFunction } from 'express' +import supertest from 'supertest' +import TwakeServer from '../..' +import { type AuthRequest, type Config } from '../../types' +import defaultConfig from '../__testData__/config.json' +import { OpenSearchClientException } from '../utils/error' + +const initialUserId = '@toto:example.com' +let userId = initialUserId +const userMail = 'toto@example.com' +let testServer: TwakeServer + +const mockUserDBGet = jest.fn().mockResolvedValue([{ mail: userMail }]) +const msearchResponseBody = { + responses: [ + { + hits: { + hits: [ + { + _id: 'room1', + _source: { + name: 'Room1 is not member' + } + }, + { + _id: 'room2', + _source: { + name: 'Room2 is member' + } + } + ] + } + }, + { + hits: { + hits: [ + { + _id: 'message1', + _source: { + room_id: 'room1', + sender: '@john:example.com', + display_name: 'John test', + content: 'Hello world' + } + }, + { + _id: 'message2', + _source: { + display_name: 'Rose', + room_id: 'room2', + sender: '@rose:example.com', + content: 'See you tomorrow world' + } + }, + { + _id: 'message3', + _source: { + display_name: 'Toto', + room_id: 'room2', + sender: initialUserId, + content: 'Goodbye world' + } + } + ] + } + }, + { + hits: { + hits: [ + { + _id: 'mail1', + _source: { + bcc: [{ address: userMail }], + cc: [{ address: userMail }], + from: [{ address: userMail }], + to: [{ address: userMail }] + } + }, + { + _id: 'mail2', + _source: { + bcc: [{ address: 'other@example.com' }], + cc: [{ address: 'other@example.com' }], + from: [{ address: 'other@example.com' }, { address: userMail }], + to: [{ address: 'other@example.com' }] + } + }, + { + _id: 'mail3', + _source: { + bcc: [{ address: 'other@example.com' }], + cc: [{ address: 'other@example.com' }], + from: [{ address: 'other@example.com' }], + to: [{ address: 'other@example.com' }] + } + } + ] + } + } + ] +} + +const mockMSearchMock = jest.fn().mockResolvedValue({ + statusCode: 200, + body: msearchResponseBody +}) + +jest.mock('@opensearch-project/opensearch', () => ({ + Client: jest.fn().mockImplementation(() => ({ + indices: { + exists: jest.fn().mockResolvedValue({ statusCode: 200, body: true }) + }, + msearch: mockMSearchMock, + close: jest.fn() + })) +})) + +jest.mock('@twake/matrix-identity-server', () => ({ + default: jest.fn().mockImplementation(() => ({ + ready: Promise.resolve(true), + db: {}, + userDB: { get: mockUserDBGet }, + api: { get: {}, post: {} }, + cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) + })), + MatrixDB: jest.fn().mockImplementation(() => ({ + ready: Promise.resolve(true), + get: jest.fn().mockResolvedValue([]), + getAll: jest.fn().mockResolvedValue([]), + close: jest.fn() + })), + Utils: { + hostnameRe: + /^((([a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*([a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9]|[a-zA-Z])(:(\d+))?$/ + } +})) + +jest.mock('../../identity-server/index.ts', () => { + return function () { + return { + ready: Promise.resolve(true), + db: {}, + userDB: { get: mockUserDBGet }, + api: { get: {}, post: {} }, + cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) + } + } +}) + +jest.mock('../../application-server/index.ts', () => { + return function () { + return { + router: { + routes: Router() + } + } + } +}) + +jest.mock('../../db/index.ts', () => jest.fn()) + +jest.mock('../../utils/middlewares/auth.middleware.ts', () => + jest + .fn() + .mockReturnValue((req: AuthRequest, res: Response, next: NextFunction) => { + req.userId = userId + next() + }) +) + +describe('Search engine API - Search engine controller', () => { + let app: express.Application + let loggerErrorSpyOn: jest.SpyInstance + const searchRoute = '/_twake/app/v1/search' + + beforeAll((done) => { + testServer = new TwakeServer(defaultConfig as Config) + loggerErrorSpyOn = jest.spyOn(testServer.logger, 'error') + testServer.ready + .then(() => { + app = express() + app.use(testServer.endpoints) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + afterAll(() => { + if (testServer != null) testServer.cleanJobs() + }) + + it('should log an error when request body does not content searchValue property', async () => { + const response = await supertest(app).post(searchRoute).send({}) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new AppServerAPIError({ + status: 400, + message: 'Error field: Invalid value (property: searchValue)' + }).message, + { + httpMethod: 'POST', + endpointPath: searchRoute, + matrixUserId: userId, + status: '400' + } + ) + expect(response.statusCode).toBe(400) + expect(response.body).toEqual({ + error: 'Error field: Invalid value (property: searchValue)' + }) + }) + + it('should log an error when searchValue property in request body is not a string', async () => { + const response = await supertest(app) + .post(searchRoute) + .send({ searchValue: 123 }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new AppServerAPIError({ + status: 400, + message: 'Error field: Invalid value (property: searchValue)' + }).message, + { + httpMethod: 'POST', + endpointPath: searchRoute, + matrixUserId: userId, + status: '400' + } + ) + expect(response.statusCode).toBe(400) + expect(response.body).toEqual({ + error: 'Error field: Invalid value (property: searchValue)' + }) + }) + + it('should log an error when auth middleware set a wrong userId in request', async () => { + userId = 'falsy_userId' + const response = await supertest(app).post(searchRoute).send({ + searchValue: 'test' + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new Error('Cannot extract user uid from matrix user id falsy_userId') + .message, + { + httpMethod: 'POST', + endpointPath: searchRoute, + matrixUserId: userId + } + ) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ error: 'Internal server error' }) + userId = initialUserId + }) + + it('should log an error when userDB client does not find user with uid matching req.userId', async () => { + mockUserDBGet.mockResolvedValue([]) + const response = await supertest(app).post(searchRoute).send({ + searchValue: 'test' + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new Error('User with user id toto not found').message, + { + httpMethod: 'POST', + endpointPath: searchRoute, + matrixUserId: userId + } + ) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ error: 'Internal server error' }) + mockUserDBGet.mockResolvedValue([{ mail: userMail }]) + }) + + it('should log an error when opensearch client returns a reponse with status code not equal to 200', async () => { + const errorMessage = 'An error occured in opensearch msearch API' + mockMSearchMock.mockResolvedValue({ + body: { + text: errorMessage + }, + statusCode: 502 + }) + const response = await supertest(app).post(searchRoute).send({ + searchValue: 'test' + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + new OpenSearchClientException( + JSON.stringify( + { + text: errorMessage + }, + null, + 2 + ), + response.statusCode + ).message, + { + httpMethod: 'POST', + endpointPath: searchRoute, + matrixUserId: userId, + status: '502' + } + ) + expect(response.statusCode).toBe(500) + expect(response.body).toEqual({ error: 'Internal server error' }) + mockMSearchMock.mockResolvedValue({ + body: msearchResponseBody, + statusCode: 200 + }) + }) + + it('should return rooms, mails and messages matching search', async () => { + jest + .spyOn(testServer.matrixDb, 'get') + .mockResolvedValueOnce([ + { room_id: 'room1', membership: 'invite' }, + { room_id: 'room1', membership: 'join' }, + { room_id: 'room1', membership: 'leave' }, + { room_id: 'room2', membership: 'invite' }, + { room_id: 'room2', membership: 'join' } + ]) + .mockResolvedValueOnce([ + { name: 'Room1 is not member', avatar_url: null }, + { name: 'Room2 is member', avatar_url: 'avatar_room2' } + ] as DbGetResult) + .mockResolvedValueOnce([ + { + room_id: 'room1', + json: '{"type":"m.room.member","content":{"is_direct":false}}' + }, + { + room_id: 'room2', + json: '{"type":"m.room.member","content":{"is_direct":true}}' + } + ]) + .mockResolvedValueOnce([ + { room_id: 'room2', user_id: userId, avatar_url: 'toto_avatar' }, + { + room_id: 'room2', + user_id: '@rose:example.com', + avatar_url: 'rose_avatar' + } + ]) + const response = await supertest(app).post(searchRoute).send({ + searchValue: 'test' + }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(0) + expect(response.statusCode).toBe(200) + expect(response.body.rooms).toHaveLength(1) + expect(response.body.rooms[0]).toEqual({ + room_id: 'room2', + name: 'Room2 is member' + }) + expect(response.body.messages).toHaveLength(2) + expect(response.body.messages[0]).toEqual({ + room_id: 'room2', + event_id: 'message2', + content: 'See you tomorrow world', + display_name: 'Rose', + avatar_url: 'rose_avatar' + }) + expect(response.body.messages[1]).toEqual({ + room_id: 'room2', + event_id: 'message3', + content: 'Goodbye world', + display_name: 'Toto', + avatar_url: 'rose_avatar' + }) + expect(response.body.mails).toHaveLength(2) + expect(response.body.mails[0]).toEqual({ + id: 'mail1', + bcc: [{ address: userMail }], + cc: [{ address: userMail }], + from: [{ address: userMail }], + to: [{ address: userMail }] + }) + expect(response.body.mails[1]).toEqual({ + id: 'mail2', + bcc: [{ address: 'other@example.com' }], + cc: [{ address: 'other@example.com' }], + from: [{ address: 'other@example.com' }, { address: userMail }], + to: [{ address: 'other@example.com' }] + }) + }) +}) diff --git a/packages/tom-server/src/search-engine-api/tests/server-instanciation.test.ts b/packages/tom-server/src/search-engine-api/tests/server-instanciation.test.ts new file mode 100644 index 00000000..2448926b --- /dev/null +++ b/packages/tom-server/src/search-engine-api/tests/server-instanciation.test.ts @@ -0,0 +1,287 @@ +import { Router } from 'express' +import TwakeServer from '../..' +import defaultConfDesc from '../../config.json' +import { type Config } from '../../types' +import defaultConfig from '../__testData__/config.json' + +const mockExists = jest.fn().mockResolvedValue({ statusCode: 200, body: true }) +let testServer: TwakeServer + +jest.mock('@opensearch-project/opensearch', () => ({ + Client: jest.fn().mockImplementation(() => ({ + indices: { + exists: mockExists + }, + close: jest.fn() + })) +})) + +jest.mock('@twake/matrix-identity-server', () => ({ + default: jest.fn().mockImplementation(() => ({ + ready: Promise.resolve(true), + db: {}, + userDB: {}, + api: { get: {}, post: {} }, + cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) + })), + MatrixDB: jest.fn().mockImplementation(() => ({ + ready: Promise.resolve(true), + close: jest.fn(), + get: jest.fn().mockResolvedValue([]), + getAll: jest.fn().mockResolvedValue([]) + })), + Utils: { + hostnameRe: + /^((([a-zA-Z0-9][-a-zA-Z0-9]*)?[a-zA-Z0-9])[.])*([a-zA-Z][-a-zA-Z0-9]*[a-zA-Z0-9]|[a-zA-Z])(:(\d+))?$/ + } +})) + +jest.mock('../../identity-server/index.ts', () => { + return function () { + return { + ready: Promise.resolve(true), + db: {}, + userDB: {}, + api: { get: {}, post: {} }, + cleanJobs: jest.fn().mockImplementation(() => testServer.logger.close()) + } + } +}) + +jest.mock('../../application-server/index.ts', () => { + return function () { + return { + router: { + routes: Router() + } + } + } +}) + +jest.mock('../../db/index.ts', () => jest.fn()) + +describe('Search engine API - Opensearch configuration', () => { + afterEach(() => { + if (testServer != null) testServer.cleanJobs() + }) + + it('should throw error if opensearch_user is defined and is not a string', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_user: 123 + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_user must be a string') + }) + ) + }) + + it('should throw error if opensearch_password is defined and is not a string', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_password: 123 + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_password must be a string') + }) + ) + }) + + it('should throw error if opensearch_password is defined and opensearch_user is not', async () => { + await expect(async () => { + const { opensearch_user: unusedVar, ...testConfig } = + defaultConfig as Config + const { opensearch_user: unusedVar2, ...testConfDesc } = defaultConfDesc + testServer = new TwakeServer(testConfig, testConfDesc) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_user is missing') + }) + ) + }) + + it('should throw error if opensearch_user is defined and opensearch_password is not', async () => { + await expect(async () => { + const { opensearch_password: unusedVar, ...testConfig } = + defaultConfig as Config + const { opensearch_password: unusedVar2, ...testConfDesc } = + defaultConfDesc + testServer = new TwakeServer(testConfig, testConfDesc) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_password is missing') + }) + ) + }) + + it('should throw error if opensearch_host not is defined', async () => { + await expect(async () => { + const { opensearch_host: unusedVar, ...testConfig } = + defaultConfig as Config + const { opensearch_host: unusedVar2, ...testConfDesc } = defaultConfDesc + testServer = new TwakeServer(testConfig, testConfDesc) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_host is required when using OpenSearch') + }) + ) + }) + + it('should throw error if opensearch_host is not a string', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_host: 123 + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_host must be a string') + }) + ) + }) + + it('should throw error if opensearch_host does not match hostname regular expression', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...(defaultConfig as Config), + opensearch_host: 'falsy_host' + }) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_host is invalid') + }) + ) + }) + + it('should throw error if opensearch_ssl is defined and is not a boolean', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_ssl: 123 + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_ssl must be a boolean') + }) + ) + }) + + it('should throw error if opensearch_ca_cert_path is defined and is not a string', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_ca_cert_path: 123 + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_ca_cert_path must be a string') + }) + ) + }) + + it('should throw error if opensearch_max_retries is defined and is not a number', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_max_retries: 'falsy' + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_max_retries must be a number') + }) + ) + }) + + it('should throw error if opensearch_number_of_shards is defined and is not a number', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_number_of_shards: 'falsy' + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_number_of_shards must be a number') + }) + ) + }) + + it('should throw error if opensearch_number_of_replicas is defined and is not a number', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_number_of_replicas: 'falsy' + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_number_of_replicas must be a number') + }) + ) + }) + + it('should throw error if opensearch_wait_for_active_shards is defined and is not a string', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_wait_for_active_shards: 123 + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error('opensearch_wait_for_active_shards must be a string') + }) + ) + }) + + it('should throw error if opensearch_wait_for_active_shards is defined and is not a string representing a number or equal to "all"', async () => { + await expect(async () => { + testServer = new TwakeServer({ + ...defaultConfig, + opensearch_wait_for_active_shards: 'falsy' + } as unknown as Config) + await testServer.ready + }).rejects.toThrow( + Error('Unable to initialize server', { + cause: new Error( + 'opensearch_wait_for_active_shards must be a string equal to a number or "all"' + ) + }) + ) + }) + + it('should log an error if opensearch API throws an error on create tom indexes', async () => { + const error = new Error('An error occured in opensearch exists API') + mockExists.mockRejectedValue(error) + testServer = new TwakeServer(defaultConfig as Config) + const loggerErrorSpyOn = jest.spyOn(testServer.logger, 'error') + await expect(testServer.ready).rejects.toStrictEqual( + new Error('Unable to initialize server', { cause: error }) + ) + mockExists.mockResolvedValue({ statusCode: 200, body: true }) + expect(loggerErrorSpyOn).toHaveBeenCalledTimes(1) + expect(loggerErrorSpyOn).toHaveBeenCalledWith( + `Unable to initialize server`, + { error: error.message } + ) + }) + + it('should initialize server if config is correct', async () => { + testServer = new TwakeServer(defaultConfig as Config) + await expect(testServer.ready).resolves.toEqual(true) + }) +}) diff --git a/packages/tom-server/src/search-engine-api/tests/tsconfig.json b/packages/tom-server/src/search-engine-api/tests/tsconfig.json new file mode 100644 index 00000000..1364af34 --- /dev/null +++ b/packages/tom-server/src/search-engine-api/tests/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../../../tsconfig-test.json", + "include": ["**/*.test.ts"] +} \ No newline at end of file diff --git a/packages/tom-server/src/search-engine-api/utils/constantes.ts b/packages/tom-server/src/search-engine-api/utils/constantes.ts new file mode 100644 index 00000000..dac9d7bf --- /dev/null +++ b/packages/tom-server/src/search-engine-api/utils/constantes.ts @@ -0,0 +1,3 @@ +export const tomRoomsIndex = 'tom_rooms' +export const tomMessagesIndex = 'tom_messages' +export const tmailMailsIndex = 'mailbox_v2' diff --git a/packages/tom-server/src/search-engine-api/utils/error.ts b/packages/tom-server/src/search-engine-api/utils/error.ts new file mode 100644 index 00000000..d980bd5d --- /dev/null +++ b/packages/tom-server/src/search-engine-api/utils/error.ts @@ -0,0 +1,39 @@ +import { type TwakeLogger } from '@twake/logger' +import { AppServerAPIError } from '@twake/matrix-application-server' + +export class OpenSearchClientException extends Error { + constructor( + message?: string, + public readonly statusCode = 500, + options?: ErrorOptions + ) { + super(message, options) + } +} + +export const formatErrorMessageForLog = ( + error: OpenSearchClientException | AppServerAPIError | Error | string +): string => { + return error instanceof OpenSearchClientException || + error instanceof AppServerAPIError || + error instanceof Error + ? error.message + : JSON.stringify(error, null, 2) +} + +export const logError = ( + logger: TwakeLogger, + error: OpenSearchClientException | AppServerAPIError | Error | string, + additionnalDetails?: Record +): void => { + const errorDetail = additionnalDetails ?? {} + if ( + (error instanceof OpenSearchClientException || + error instanceof AppServerAPIError) && + error.statusCode != null + ) { + errorDetail.status = error.statusCode.toString() + } + + logger.error(formatErrorMessageForLog(error), errorDetail) +} diff --git a/packages/tom-server/src/types.ts b/packages/tom-server/src/types.ts index c6c3cc2e..4942f807 100644 --- a/packages/tom-server/src/types.ts +++ b/packages/tom-server/src/types.ts @@ -6,9 +6,9 @@ import { type IdentityServerDb as MIdentityServerDb, type Utils as MUtils } from '@twake/matrix-identity-server' +import { type Request } from 'express' import type { PathOrFileDescriptor } from 'fs' import type AugmentedIdentityServer from './identity-server' -import { type Request } from 'express' export type expressAppHandler = MUtils.expressAppHandler export type AuthenticationFunction = MUtils.AuthenticationFunction @@ -24,6 +24,16 @@ export type Config = MConfig & matrix_server: string matrix_database_host: string oidc_issuer?: string + opensearch_ca_cert_path?: string + opensearch_host?: string + opensearch_is_activated?: boolean + opensearch_max_retries?: number + opensearch_number_of_shards?: number + opensearch_number_of_replicas?: number + opensearch_password?: string + opensearch_ssl?: boolean + opensearch_user?: string + opensearch_wait_for_active_shards?: string sms_api_key?: string sms_api_login?: string sms_api_url?: string diff --git a/packages/tom-server/src/vault-api/__testData__/config.json b/packages/tom-server/src/vault-api/__testData__/config.json index 861c1497..8aeedfa2 100644 --- a/packages/tom-server/src/vault-api/__testData__/config.json +++ b/packages/tom-server/src/vault-api/__testData__/config.json @@ -1,8 +1,9 @@ { + "base_url": "http://example.com", "database_engine": "sqlite", "database_host": "./server.db", - "server_name": "matrix.org", - "base_url": "http://example.com", + "opensearch_is_activated": false, "registration_file_path": "registration.yaml", - "sender_localpart": "twake" + "sender_localpart": "twake", + "server_name": "matrix.org" } \ No newline at end of file diff --git a/packages/tom-server/tsconfig.json b/packages/tom-server/tsconfig.json index 304c5bc3..3b8bf1c3 100644 --- a/packages/tom-server/tsconfig.json +++ b/packages/tom-server/tsconfig.json @@ -3,5 +3,5 @@ "compilerOptions": { "outDir": "dist" }, - "include": ["src/**/*", "test/**/*", "singleton.ts"] + "include": ["src/**/*", "singleton.ts"] } diff --git a/server.mjs b/server.mjs index eb5b8124..4165394c 100644 --- a/server.mjs +++ b/server.mjs @@ -54,6 +54,18 @@ let conf = { ? JSON.parse(process.env.MATRIX_DATABASE_SSL) : false, oidc_issuer: process.env.OIDC_ISSUER, + opensearch_ca_cert_path: process.env.OPENSEARCH_CA_CERT_PATH, + opensearch_host: process.env.OPENSEARCH_HOST, + opensearch_is_activated: process.env.OPENSEARCH_IS_ACTIVATED || false, + opensearch_max_retries: +process.env.OPENSEARCH_MAX_RETRIES || null, + opensearch_number_of_shards: +process.env.OPENSEARCH_NUMBER_OF_SHARDS || null, + opensearch_number_of_replicas: + +process.env.OPENSEARCH_NUMBER_OF_REPLICAS || null, + opensearch_password: process.env.OPENSEARCH_PASSWORD, + opensearch_ssl: process.env.OPENSEARCH_SSL || false, + opensearch_user: process.env.OPENSEARCH_USER, + opensearch_wait_for_active_shards: + process.env.OPENSEARCH_WAIT_FOR_ACTIVE_SHARDS, pepperCron: process.env.PEPPER_CRON || '9 1 * * *', rate_limiting_window: process.env.RATE_LIMITING_WINDOW || 600000, rate_limiting_nb_requests: process.env.RATE_LIMITING_NB_REQUESTS || 100, diff --git a/tsconfig-test.json b/tsconfig-test.json index ba89be65..b548b0bd 100644 --- a/tsconfig-test.json +++ b/tsconfig-test.json @@ -2,7 +2,7 @@ "compilerOptions": { "strict": true, "module": "esnext", - "target": "es2015", + "target": "esnext", "esModuleInterop": true, "moduleResolution": "node", "lib": ["esnext"],