mirror of
https://github.com/Infisical/infisical.git
synced 2025-08-31 15:32:32 +00:00
Compare commits
124 Commits
misc/optim
...
feat/azure
Author | SHA1 | Date | |
---|---|---|---|
|
8ba61e8293 | ||
|
f5434b5cba | ||
|
1159b74bdb | ||
|
bc4885b098 | ||
|
97be78a107 | ||
|
4b42f7b1b5 | ||
|
3de7fec650 | ||
|
7bc6697801 | ||
|
34c6d254a0 | ||
|
a0da2f2d4c | ||
|
c7987772e3 | ||
|
5eee99e9ac | ||
|
4485d7f757 | ||
|
d3c3f3a17e | ||
|
999588b06e | ||
|
37153cd8cf | ||
|
4547ed7aeb | ||
|
aae6a3f9af | ||
|
13b20806ba | ||
|
fc6778dd89 | ||
|
2f68ff1629 | ||
|
cde7673a23 | ||
|
1165b05e8a | ||
|
8884c0e6bd | ||
|
0762de93d6 | ||
|
af2f21fe93 | ||
|
dcd588007c | ||
|
0c92764409 | ||
|
976317e71b | ||
|
7b52d60036 | ||
|
83479a091e | ||
|
4e2592960d | ||
|
bceaac844f | ||
|
2f375d6b65 | ||
|
8f00bab61c | ||
|
ec12acfcdf | ||
|
34a8301617 | ||
|
8ffff7e779 | ||
|
a349dda4bc | ||
|
f63ee39f3d | ||
|
f550a2ae3f | ||
|
725e55f7e5 | ||
|
f59efc1948 | ||
|
f52e90a5c1 | ||
|
2fda307b67 | ||
|
ff7b530252 | ||
|
10cfbe0c74 | ||
|
4da24bfa39 | ||
|
8123be4c14 | ||
|
9a98192b9b | ||
|
991ee20ec7 | ||
|
dc48281e6a | ||
|
b3002d784e | ||
|
c782493704 | ||
|
6c7062fa16 | ||
|
5c632db282 | ||
|
817daecc6c | ||
|
461deef0d5 | ||
|
7748e03612 | ||
|
2389c64e69 | ||
|
de5ad47f77 | ||
|
e0161cd06f | ||
|
7c12fa3a4c | ||
|
0af53e82da | ||
|
f0c080187e | ||
|
47118bcf19 | ||
|
bb1975491f | ||
|
52bbe25fc5 | ||
|
5c21ac3182 | ||
|
6204b181e7 | ||
|
06de9d06c9 | ||
|
3cceec86c8 | ||
|
ff043f990f | ||
|
bb14231d71 | ||
|
9e177c1e45 | ||
|
5aeb823c9e | ||
|
d587e779f5 | ||
|
d985b84577 | ||
|
f9a9565630 | ||
|
05ba0abadd | ||
|
fff9a96204 | ||
|
f78556c85f | ||
|
13aa380cac | ||
|
f2a9a57c95 | ||
|
6384fa6dba | ||
|
c34ec8de09 | ||
|
ef8a7f1233 | ||
|
09db98db50 | ||
|
a37f1eb1f8 | ||
|
2113abcfdc | ||
|
ea2707651c | ||
|
b986ff9a21 | ||
|
106833328b | ||
|
8a72023e80 | ||
|
41a3ac6bd4 | ||
|
2fb5cc1712 | ||
|
b352428032 | ||
|
914bb3d389 | ||
|
be70bfa33f | ||
|
7758e5dbfa | ||
|
22fca374f2 | ||
|
94039ca509 | ||
|
c8f124e4c5 | ||
|
2501c57030 | ||
|
97dac1da94 | ||
|
f9f989c8af | ||
|
60b3f5c7c6 | ||
|
c2cea8cffc | ||
|
02ee418763 | ||
|
faca20c00c | ||
|
69c3687add | ||
|
1645534b54 | ||
|
dca0b0c614 | ||
|
d3d0d44778 | ||
|
67abcbfe7a | ||
|
fc772e6b89 | ||
|
c8108ff49a | ||
|
806165b9e9 | ||
|
9fde0a5787 | ||
|
9ee2581659 | ||
|
2deff0ef55 | ||
|
4312378589 | ||
|
d749a9621f | ||
|
9686d14e7f |
290
backend/package-lock.json
generated
290
backend/package-lock.json
generated
@@ -63,6 +63,7 @@
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1553.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios-ntlm": "^1.4.4",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"botbuilder": "^4.23.2",
|
||||
@@ -12956,216 +12957,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.107.tgz",
|
||||
"integrity": "sha512-zKhqDyFcTsyLIYK1iEmavljZnf4CCor5pF52UzLAz4B6Nu/4GLU+2LQVAf+oRHjusG39PTPjd2AlRT3f3QWfsQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@swc/counter": "^0.1.1",
|
||||
"@swc/types": "^0.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/swc"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-darwin-arm64": "1.3.107",
|
||||
"@swc/core-darwin-x64": "1.3.107",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.107",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.107",
|
||||
"@swc/core-linux-arm64-musl": "1.3.107",
|
||||
"@swc/core-linux-x64-gnu": "1.3.107",
|
||||
"@swc/core-linux-x64-musl": "1.3.107",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.107",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.107",
|
||||
"@swc/core-win32-x64-msvc": "1.3.107"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/helpers": "^0.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/helpers": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.107.tgz",
|
||||
"integrity": "sha512-47tD/5vSXWxPd0j/ZllyQUg4bqalbQTsmqSw0J4dDdS82MWqCAwUErUrAZPRjBkjNQ6Kmrf5rpCWaGTtPw+ngw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-x64": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.107.tgz",
|
||||
"integrity": "sha512-hwiLJ2ulNkBGAh1m1eTfeY1417OAYbRGcb/iGsJ+LuVLvKAhU/itzsl535CvcwAlt2LayeCFfcI8gdeOLeZa9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm-gnueabihf": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.107.tgz",
|
||||
"integrity": "sha512-I2wzcC0KXqh0OwymCmYwNRgZ9nxX7DWnOOStJXV3pS0uB83TXAkmqd7wvMBuIl9qu4Hfomi9aDM7IlEEn9tumQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-gnu": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.107.tgz",
|
||||
"integrity": "sha512-HWgnn7JORYlOYnGsdunpSF8A+BCZKPLzLtEUA27/M/ZuANcMZabKL9Zurt7XQXq888uJFAt98Gy+59PU90aHKg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-arm64-musl": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.107.tgz",
|
||||
"integrity": "sha512-vfPF74cWfAm8hyhS8yvYI94ucMHIo8xIYU+oFOW9uvDlGQRgnUf/6DEVbLyt/3yfX5723Ln57U8uiMALbX5Pyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-gnu": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.107.tgz",
|
||||
"integrity": "sha512-uBVNhIg0ip8rH9OnOsCARUFZ3Mq3tbPHxtmWk9uAa5u8jQwGWeBx5+nTHpDOVd3YxKb6+5xDEI/edeeLpha/9g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-linux-x64-musl": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.107.tgz",
|
||||
"integrity": "sha512-mvACkUvzSIB12q1H5JtabWATbk3AG+pQgXEN95AmEX2ZA5gbP9+B+mijsg7Sd/3tboHr7ZHLz/q3SHTvdFJrEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-arm64-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-J3P14Ngy/1qtapzbguEH41kY109t6DFxfbK4Ntz9dOWNuVY3o9/RTB841ctnJk0ZHEG+BjfCJjsD2n8H5HcaOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-ia32-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-ZBUtgyjTHlz8TPJh7kfwwwFma+ktr6OccB1oXC8fMSopD0AxVnQasgun3l3099wIsAB9eEsJDQ/3lDkOLs1gBA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-win32-x64-msvc": {
|
||||
"version": "1.3.107",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.107.tgz",
|
||||
"integrity": "sha512-Eyzo2XRqWOxqhE1gk9h7LWmUf4Bp4Xn2Ttb0ayAXFp6YSTxQIThXcT9kipXZqcpxcmDwoq8iWbbf2P8XL743EA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -13183,14 +12974,6 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/types": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz",
|
||||
"integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@techteamer/ocsp": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@techteamer/ocsp/-/ocsp-1.0.1.tgz",
|
||||
@@ -15195,6 +14978,18 @@
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-ntlm": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/axios-ntlm/-/axios-ntlm-1.4.4.tgz",
|
||||
"integrity": "sha512-kpCRdzMfL8gi0Z0o96P3QPAK4XuC8iciGgxGXe+PeQ4oyjI2LZN8WSOKbu0Y9Jo3T/A7pB81n6jYVPIpglEuRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.8.4",
|
||||
"des.js": "^1.1.0",
|
||||
"dev-null": "^0.1.1",
|
||||
"js-md4": "^0.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/axios-retry": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.0.0.tgz",
|
||||
@@ -16954,6 +16749,16 @@
|
||||
"resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
|
||||
"integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ=="
|
||||
},
|
||||
"node_modules/des.js": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
|
||||
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.1",
|
||||
"minimalistic-assert": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
@@ -16981,6 +16786,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dev-null": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz",
|
||||
"integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
@@ -19029,49 +18840,6 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
|
||||
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"gaxios": "^5.0.0",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata/node_modules/gaxios": {
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz",
|
||||
"integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^5.0.0",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.6.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata/node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
|
@@ -183,6 +183,7 @@
|
||||
"argon2": "^0.31.2",
|
||||
"aws-sdk": "^2.1553.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios-ntlm": "^1.4.4",
|
||||
"axios-retry": "^4.0.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"botbuilder": "^4.23.2",
|
||||
|
1
backend/src/@types/fastify.d.ts
vendored
1
backend/src/@types/fastify.d.ts
vendored
@@ -148,6 +148,7 @@ declare module "fastify" {
|
||||
interface Session {
|
||||
callbackPort: string;
|
||||
isAdminLogin: boolean;
|
||||
orgSlug?: string;
|
||||
}
|
||||
|
||||
interface FastifyRequest {
|
||||
|
@@ -0,0 +1,49 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const BATCH_SIZE = 1000;
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified"))) {
|
||||
// Add the column
|
||||
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||
t.boolean("isEmailVerified").defaultTo(false);
|
||||
});
|
||||
|
||||
const aliasesToUpdate: { aliasId: string; isEmailVerified: boolean }[] = await knex(TableName.UserAliases)
|
||||
.join(TableName.Users, `${TableName.UserAliases}.userId`, `${TableName.Users}.id`)
|
||||
.select([`${TableName.UserAliases}.id as aliasId`, `${TableName.Users}.isEmailVerified`]);
|
||||
|
||||
for (let i = 0; i < aliasesToUpdate.length; i += BATCH_SIZE) {
|
||||
const batch = aliasesToUpdate.slice(i, i + BATCH_SIZE);
|
||||
|
||||
const trueIds = batch.filter((row) => row.isEmailVerified).map((row) => row.aliasId);
|
||||
|
||||
if (trueIds.length > 0) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.UserAliases).whereIn("id", trueIds).update({ isEmailVerified: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!(await knex.schema.hasColumn(TableName.AuthTokens, "aliasId"))) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.string("aliasId").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.UserAliases, "isEmailVerified")) {
|
||||
await knex.schema.alterTable(TableName.UserAliases, (t) => {
|
||||
t.dropColumn("isEmailVerified");
|
||||
});
|
||||
}
|
||||
|
||||
if (await knex.schema.hasColumn(TableName.AuthTokens, "aliasId")) {
|
||||
await knex.schema.alterTable(TableName.AuthTokens, (t) => {
|
||||
t.dropColumn("aliasId");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME = "googleSsoAuthEnforced";
|
||||
const GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME = "googleSsoAuthLastUsed";
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||
);
|
||||
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||
if (!hasGoogleSsoAuthEnforcedColumn)
|
||||
table.boolean(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME).defaultTo(false).notNullable();
|
||||
if (!hasGoogleSsoAuthLastUsedColumn) table.timestamp(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME).nullable();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasGoogleSsoAuthEnforcedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME
|
||||
);
|
||||
|
||||
const hasGoogleSsoAuthLastUsedColumn = await knex.schema.hasColumn(
|
||||
TableName.Organization,
|
||||
GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME
|
||||
);
|
||||
|
||||
await knex.schema.alterTable(TableName.Organization, (table) => {
|
||||
if (hasGoogleSsoAuthEnforcedColumn) table.dropColumn(GOOGLE_SSO_AUTH_ENFORCED_COLUMN_NAME);
|
||||
if (hasGoogleSsoAuthLastUsedColumn) table.dropColumn(GOOGLE_SSO_AUTH_LAST_USED_COLUMN_NAME);
|
||||
});
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (!(await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission"))) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.boolean("shouldCheckSecretPermission").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) {
|
||||
await knex.schema.alterTable(TableName.SecretApprovalPolicy, (t) => {
|
||||
t.dropColumn("shouldCheckSecretPermission");
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { selectAllTableCols } from "@app/lib/knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
const BATCH_SIZE = 100;
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
if (await knex.schema.hasColumn(TableName.SecretApprovalPolicy, "shouldCheckSecretPermission")) {
|
||||
// find all existing SecretApprovalPolicy rows to backfill shouldCheckSecretPermission flag
|
||||
const rows = await knex(TableName.SecretApprovalPolicy).select(selectAllTableCols(TableName.SecretApprovalPolicy));
|
||||
|
||||
if (rows.length > 0) {
|
||||
for (let i = 0; i < rows.length; i += BATCH_SIZE) {
|
||||
const batch = rows.slice(i, i + BATCH_SIZE);
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await knex(TableName.SecretApprovalPolicy)
|
||||
.whereIn(
|
||||
"id",
|
||||
batch.map((row) => row.id)
|
||||
)
|
||||
.update({ shouldCheckSecretPermission: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {}
|
@@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasPropertiesCol = await knex.schema.hasColumn(TableName.PkiSubscriber, "properties");
|
||||
|
||||
if (!hasPropertiesCol) {
|
||||
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
|
||||
t.jsonb("properties").nullable();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasPropertiesCol = await knex.schema.hasColumn(TableName.PkiSubscriber, "properties");
|
||||
|
||||
if (hasPropertiesCol) {
|
||||
await knex.schema.alterTable(TableName.PkiSubscriber, (t) => {
|
||||
t.dropColumn("properties");
|
||||
});
|
||||
}
|
||||
}
|
@@ -17,7 +17,8 @@ export const AuthTokensSchema = z.object({
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
userId: z.string().uuid().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional()
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
aliasId: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TAuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||
|
@@ -36,7 +36,9 @@ export const OrganizationsSchema = z.object({
|
||||
scannerProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
shareSecretsProductEnabled: z.boolean().default(true).nullable().optional(),
|
||||
maxSharedSecretLifetime: z.number().default(2592000).nullable().optional(),
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional()
|
||||
maxSharedSecretViewLimit: z.number().nullable().optional(),
|
||||
googleSsoAuthEnforced: z.boolean().default(false),
|
||||
googleSsoAuthLastUsed: z.date().nullable().optional()
|
||||
});
|
||||
|
||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||
|
@@ -25,7 +25,8 @@ export const PkiSubscribersSchema = z.object({
|
||||
lastAutoRenewAt: z.date().nullable().optional(),
|
||||
lastOperationStatus: z.string().nullable().optional(),
|
||||
lastOperationMessage: z.string().nullable().optional(),
|
||||
lastOperationAt: z.date().nullable().optional()
|
||||
lastOperationAt: z.date().nullable().optional(),
|
||||
properties: z.unknown().nullable().optional()
|
||||
});
|
||||
|
||||
export type TPkiSubscribers = z.infer<typeof PkiSubscribersSchema>;
|
||||
|
@@ -17,7 +17,8 @@ export const SecretApprovalPoliciesSchema = z.object({
|
||||
updatedAt: z.date(),
|
||||
enforcementLevel: z.string().default("hard"),
|
||||
deletedAt: z.date().nullable().optional(),
|
||||
allowedSelfApprovals: z.boolean().default(true)
|
||||
allowedSelfApprovals: z.boolean().default(true),
|
||||
shouldCheckSecretPermission: z.boolean().nullable().optional()
|
||||
});
|
||||
|
||||
export type TSecretApprovalPolicies = z.infer<typeof SecretApprovalPoliciesSchema>;
|
||||
|
@@ -16,7 +16,8 @@ export const UserAliasesSchema = z.object({
|
||||
emails: z.string().array().nullable().optional(),
|
||||
orgId: z.string().uuid().nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
updatedAt: z.date(),
|
||||
isEmailVerified: z.boolean().default(false).nullable().optional()
|
||||
});
|
||||
|
||||
export type TUserAliases = z.infer<typeof UserAliasesSchema>;
|
||||
|
@@ -133,6 +133,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
approvals: z.number(),
|
||||
approvers: z
|
||||
.object({
|
||||
isOrgMembershipActive: z.boolean().nullable().optional(),
|
||||
userId: z.string().nullable().optional(),
|
||||
sequence: z.number().nullable().optional(),
|
||||
approvalsRequired: z.number().nullable().optional(),
|
||||
@@ -150,6 +151,7 @@ export const registerAccessApprovalRequestRouter = async (server: FastifyZodProv
|
||||
}),
|
||||
reviewers: z
|
||||
.object({
|
||||
isOrgMembershipActive: z.boolean().nullable().optional(),
|
||||
userId: z.string(),
|
||||
status: z.string()
|
||||
})
|
||||
|
@@ -294,22 +294,30 @@ export const registerSecretApprovalRequestRouter = async (server: FastifyZodProv
|
||||
200: z.object({
|
||||
approval: SecretApprovalRequestsSchema.merge(
|
||||
z.object({
|
||||
// secretPath: z.string(),
|
||||
policy: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
approvals: z.number(),
|
||||
approvers: approvalRequestUser.array(),
|
||||
approvers: approvalRequestUser
|
||||
.extend({ isOrgMembershipActive: z.boolean().nullable().optional() })
|
||||
.array(),
|
||||
bypassers: approvalRequestUser.array(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
enforcementLevel: z.string(),
|
||||
deletedAt: z.date().nullish(),
|
||||
allowedSelfApprovals: z.boolean()
|
||||
allowedSelfApprovals: z.boolean(),
|
||||
shouldCheckSecretPermission: z.boolean().nullable().optional()
|
||||
}),
|
||||
environment: z.string(),
|
||||
statusChangedByUser: approvalRequestUser.optional(),
|
||||
committerUser: approvalRequestUser.nullish(),
|
||||
reviewers: approvalRequestUser.extend({ status: z.string(), comment: z.string().optional() }).array(),
|
||||
reviewers: approvalRequestUser
|
||||
.extend({
|
||||
status: z.string(),
|
||||
comment: z.string().optional(),
|
||||
isOrgMembershipActive: z.boolean().nullable().optional()
|
||||
})
|
||||
.array(),
|
||||
secretPath: z.string(),
|
||||
commits: secretRawSchema
|
||||
.omit({ _id: true, environment: true, workspace: true, type: true, version: true, secretValue: true })
|
||||
|
@@ -5,6 +5,7 @@ import {
|
||||
AccessApprovalRequestsSchema,
|
||||
TableName,
|
||||
TAccessApprovalRequests,
|
||||
TOrgMemberships,
|
||||
TUserGroupMembership,
|
||||
TUsers
|
||||
} from "@app/db/schemas";
|
||||
@@ -144,6 +145,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
@@ -151,6 +153,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
)[];
|
||||
bypassers: string[];
|
||||
@@ -202,6 +205,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
reviewers: {
|
||||
userId: string;
|
||||
status: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}[];
|
||||
approvers: (
|
||||
| {
|
||||
@@ -210,6 +214,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
@@ -217,6 +222,7 @@ export interface TAccessApprovalRequestDALFactory extends Omit<TOrmify<TableName
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
)[];
|
||||
bypassers: string[];
|
||||
@@ -288,6 +294,24 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
`requestedByUser.id`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("approverOrgMembership"),
|
||||
`${TableName.AccessApprovalPolicyApprover}.approverUserId`,
|
||||
`approverOrgMembership.userId`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("approverGroupOrgMembership"),
|
||||
`${TableName.Users}.id`,
|
||||
`approverGroupOrgMembership.userId`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("reviewerOrgMembership"),
|
||||
`${TableName.AccessApprovalRequestReviewer}.reviewerUserId`,
|
||||
`reviewerOrgMembership.userId`
|
||||
)
|
||||
|
||||
.leftJoin(TableName.Environment, `${TableName.AccessApprovalPolicy}.envId`, `${TableName.Environment}.id`)
|
||||
|
||||
.select(selectAllTableCols(TableName.AccessApprovalRequest))
|
||||
@@ -300,6 +324,10 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
db.ref("allowedSelfApprovals").withSchema(TableName.AccessApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||
db.ref("envId").withSchema(TableName.AccessApprovalPolicy).as("policyEnvId"),
|
||||
db.ref("deletedAt").withSchema(TableName.AccessApprovalPolicy).as("policyDeletedAt"),
|
||||
|
||||
db.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"),
|
||||
db.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"),
|
||||
db.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"),
|
||||
db.ref("maxTimePeriod").withSchema(TableName.AccessApprovalPolicy).as("policyMaxTimePeriod")
|
||||
)
|
||||
.select(db.ref("approverUserId").withSchema(TableName.AccessApprovalPolicyApprover))
|
||||
@@ -396,17 +424,26 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
{
|
||||
key: "reviewerUserId",
|
||||
label: "reviewers" as const,
|
||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status }) => (userId ? { userId, status } : undefined)
|
||||
mapper: ({ reviewerUserId: userId, reviewerStatus: status, reviewerIsOrgMembershipActive }) =>
|
||||
userId ? { userId, status, isOrgMembershipActive: reviewerIsOrgMembershipActive } : undefined
|
||||
},
|
||||
{
|
||||
key: "approverUserId",
|
||||
label: "approvers" as const,
|
||||
mapper: ({ approverUserId, approverSequence, approvalsRequired, approverUsername, approverEmail }) => ({
|
||||
mapper: ({
|
||||
approverUserId,
|
||||
approverSequence,
|
||||
approvalsRequired,
|
||||
approverUsername,
|
||||
approverEmail,
|
||||
approverIsOrgMembershipActive
|
||||
}) => ({
|
||||
userId: approverUserId,
|
||||
sequence: approverSequence,
|
||||
approvalsRequired,
|
||||
email: approverEmail,
|
||||
username: approverUsername
|
||||
username: approverUsername,
|
||||
isOrgMembershipActive: approverIsOrgMembershipActive
|
||||
})
|
||||
},
|
||||
{
|
||||
@@ -417,13 +454,15 @@ export const accessApprovalRequestDALFactory = (db: TDbClient): TAccessApprovalR
|
||||
approverSequence,
|
||||
approvalsRequired,
|
||||
approverGroupEmail,
|
||||
approverGroupUsername
|
||||
approverGroupUsername,
|
||||
approverGroupIsOrgMembershipActive
|
||||
}) => ({
|
||||
userId: approverGroupUserId,
|
||||
sequence: approverSequence,
|
||||
approvalsRequired,
|
||||
email: approverGroupEmail,
|
||||
username: approverGroupUsername
|
||||
username: approverGroupUsername,
|
||||
isOrgMembershipActive: approverGroupIsOrgMembershipActive
|
||||
})
|
||||
},
|
||||
{ key: "bypasserUserId", label: "bypassers" as const, mapper: ({ bypasserUserId }) => bypasserUserId },
|
||||
|
@@ -87,6 +87,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
@@ -94,6 +95,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
)[];
|
||||
bypassers: string[];
|
||||
@@ -145,6 +147,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
reviewers: {
|
||||
userId: string;
|
||||
status: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}[];
|
||||
approvers: (
|
||||
| {
|
||||
@@ -153,6 +156,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
| {
|
||||
userId: string;
|
||||
@@ -160,6 +164,7 @@ export interface TAccessApprovalRequestServiceFactory {
|
||||
approvalsRequired: number | null | undefined;
|
||||
email: string | null | undefined;
|
||||
username: string;
|
||||
isOrgMembershipActive: boolean;
|
||||
}
|
||||
)[];
|
||||
bypassers: string[];
|
||||
|
@@ -14,7 +14,7 @@ import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { EventType, filterableSecretEvents } from "./audit-log-types";
|
||||
|
||||
export interface TAuditLogDALFactory extends Omit<TOrmify<TableName.AuditLog>, "find"> {
|
||||
pruneAuditLog: (tx?: knex.Knex) => Promise<void>;
|
||||
pruneAuditLog: () => Promise<void>;
|
||||
find: (
|
||||
arg: Omit<TFindQuery, "actor" | "eventType"> & {
|
||||
actorId?: string | undefined;
|
||||
@@ -41,6 +41,10 @@ type TFindQuery = {
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
|
||||
export const auditLogDALFactory = (db: TDbClient) => {
|
||||
const auditLogOrm = ormify(db, TableName.AuditLog);
|
||||
|
||||
@@ -151,20 +155,20 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
};
|
||||
|
||||
// delete all audit log that have expired
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
|
||||
const runPrune = async (dbClient: knex.Knex) => {
|
||||
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
|
||||
const MAX_RETRY_ON_FAILURE = 3;
|
||||
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async () => {
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
|
||||
const today = new Date();
|
||||
let deletedAuditLogIds: { id: string }[] = [];
|
||||
let numberOfRetryOnFailure = 0;
|
||||
let isRetrying = false;
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
|
||||
do {
|
||||
try {
|
||||
const findExpiredLogSubQuery = dbClient(TableName.AuditLog)
|
||||
const findExpiredLogSubQuery = trx(TableName.AuditLog)
|
||||
.where("expiresAt", "<", today)
|
||||
.where("createdAt", "<", today) // to use audit log partition
|
||||
.orderBy(`${TableName.AuditLog}.createdAt`, "desc")
|
||||
@@ -172,34 +176,25 @@ export const auditLogDALFactory = (db: TDbClient) => {
|
||||
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
|
||||
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
deletedAuditLogIds = await dbClient(TableName.AuditLog)
|
||||
.whereIn("id", findExpiredLogSubQuery)
|
||||
.del()
|
||||
.returning("id");
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
};
|
||||
const results = await trx(TableName.AuditLog).whereIn("id", findExpiredLogSubQuery).del().returning("id");
|
||||
|
||||
if (tx) {
|
||||
await runPrune(tx);
|
||||
} else {
|
||||
const QUERY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
||||
await db.transaction(async (trx) => {
|
||||
await trx.raw(`SET statement_timeout = ${QUERY_TIMEOUT_MS}`);
|
||||
await runPrune(trx);
|
||||
});
|
||||
}
|
||||
return results;
|
||||
});
|
||||
|
||||
numberOfRetryOnFailure = 0; // reset
|
||||
} catch (error) {
|
||||
numberOfRetryOnFailure += 1;
|
||||
deletedAuditLogIds = [];
|
||||
logger.error(error, "Failed to delete audit log on pruning");
|
||||
} finally {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 10); // time to breathe for db
|
||||
});
|
||||
}
|
||||
isRetrying = numberOfRetryOnFailure > 0;
|
||||
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
|
||||
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
|
||||
};
|
||||
|
||||
const create: TAuditLogDALFactory["create"] = async (tx) => {
|
||||
|
@@ -281,6 +281,7 @@ export enum EventType {
|
||||
UPDATE_SSH_CERTIFICATE_TEMPLATE = "update-ssh-certificate-template",
|
||||
DELETE_SSH_CERTIFICATE_TEMPLATE = "delete-ssh-certificate-template",
|
||||
GET_SSH_CERTIFICATE_TEMPLATE = "get-ssh-certificate-template",
|
||||
GET_AZURE_AD_TEMPLATES = "get-azure-ad-templates",
|
||||
GET_SSH_HOST = "get-ssh-host",
|
||||
CREATE_SSH_HOST = "create-ssh-host",
|
||||
UPDATE_SSH_HOST = "update-ssh-host",
|
||||
@@ -2497,6 +2498,14 @@ interface CreateCertificateTemplateEstConfig {
|
||||
};
|
||||
}
|
||||
|
||||
interface GetAzureAdCsTemplatesEvent {
|
||||
type: EventType.GET_AZURE_AD_TEMPLATES;
|
||||
metadata: {
|
||||
caId: string;
|
||||
amount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateCertificateTemplateEstConfig {
|
||||
type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
|
||||
metadata: {
|
||||
@@ -3636,6 +3645,7 @@ export type Event =
|
||||
| CreateCertificateTemplateEstConfig
|
||||
| UpdateCertificateTemplateEstConfig
|
||||
| GetCertificateTemplateEstConfig
|
||||
| GetAzureAdCsTemplatesEvent
|
||||
| AttemptCreateSlackIntegration
|
||||
| AttemptReinstallSlackIntegration
|
||||
| UpdateSlackIntegration
|
||||
|
@@ -123,7 +123,7 @@ export function createEventStreamClient(redis: Redis, options: IEventStreamClien
|
||||
|
||||
await redis.set(key, "1", "EX", 60);
|
||||
|
||||
stream.push("1");
|
||||
send({ type: "ping" });
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
|
@@ -400,15 +400,13 @@ export const ldapConfigServiceFactory = ({
|
||||
|
||||
userAlias = await userDAL.transaction(async (tx) => {
|
||||
let newUser: TUsers | undefined;
|
||||
if (serverCfg.trustLdapEmails) {
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email: email.toLowerCase(),
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email: email.toLowerCase(),
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!newUser) {
|
||||
const uniqueUsername = await normalizeUsername(username, userDAL);
|
||||
@@ -433,7 +431,8 @@ export const ldapConfigServiceFactory = ({
|
||||
aliasType: UserAliasType.LDAP,
|
||||
externalId,
|
||||
emails: [email],
|
||||
orgId
|
||||
orgId,
|
||||
isEmailVerified: serverCfg.trustLdapEmails
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -556,15 +555,14 @@ export const ldapConfigServiceFactory = ({
|
||||
return newUser;
|
||||
});
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted) && userAlias.isEmailVerified;
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
hasExchangedPrivateKey: true,
|
||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
||||
...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: organization.name,
|
||||
@@ -572,6 +570,7 @@ export const ldapConfigServiceFactory = ({
|
||||
organizationSlug: organization.slug,
|
||||
authMethod: AuthMethod.LDAP,
|
||||
authType: UserAliasType.LDAP,
|
||||
aliasId: userAlias.id,
|
||||
isUserCompleted,
|
||||
...(relayState
|
||||
? {
|
||||
@@ -585,10 +584,11 @@ export const ldapConfigServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
if (user.email && !user.isEmailVerified) {
|
||||
if (user.email && !userAlias.isEmailVerified) {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
aliasId: userAlias.id
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
|
@@ -32,6 +32,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
auditLogStreams: false,
|
||||
auditLogStreamLimit: 3,
|
||||
samlSSO: false,
|
||||
enforceGoogleSSO: false,
|
||||
hsm: false,
|
||||
oidcSSO: false,
|
||||
scim: false,
|
||||
|
@@ -47,6 +47,7 @@ export type TFeatureSet = {
|
||||
auditLogStreamLimit: 3;
|
||||
githubOrgSync: false;
|
||||
samlSSO: false;
|
||||
enforceGoogleSSO: false;
|
||||
hsm: false;
|
||||
oidcSSO: false;
|
||||
secretAccessInsights: false;
|
||||
|
@@ -180,7 +180,7 @@ export const oidcConfigServiceFactory = ({
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
const userAlias = await userAliasDAL.findOne({
|
||||
let userAlias = await userAliasDAL.findOne({
|
||||
externalId,
|
||||
orgId,
|
||||
aliasType: UserAliasType.OIDC
|
||||
@@ -231,32 +231,29 @@ export const oidcConfigServiceFactory = ({
|
||||
} else {
|
||||
user = await userDAL.transaction(async (tx) => {
|
||||
let newUser: TUsers | undefined;
|
||||
// we prioritize getting the most complete user to create the new alias under
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (serverCfg.trustOidcEmails) {
|
||||
// we prioritize getting the most complete user to create the new alias under
|
||||
if (!newUser) {
|
||||
// this fetches user entries created via invites
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
isEmailVerified: true
|
||||
username: email
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!newUser) {
|
||||
// this fetches user entries created via invites
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
username: email
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (newUser && !newUser.isEmailVerified) {
|
||||
// we automatically mark it as email-verified because we've configured trust for OIDC emails
|
||||
newUser = await userDAL.updateById(newUser.id, {
|
||||
isEmailVerified: true
|
||||
});
|
||||
}
|
||||
if (newUser && !newUser.isEmailVerified) {
|
||||
// we automatically mark it as email-verified because we've configured trust for OIDC emails
|
||||
newUser = await userDAL.updateById(newUser.id, {
|
||||
isEmailVerified: serverCfg.trustOidcEmails
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,13 +273,14 @@ export const oidcConfigServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
await userAliasDAL.create(
|
||||
userAlias = await userAliasDAL.create(
|
||||
{
|
||||
userId: newUser.id,
|
||||
aliasType: UserAliasType.OIDC,
|
||||
externalId,
|
||||
emails: email ? [email] : [],
|
||||
orgId
|
||||
orgId,
|
||||
isEmailVerified: serverCfg.trustOidcEmails
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -404,19 +402,20 @@ export const oidcConfigServiceFactory = ({
|
||||
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted);
|
||||
const isUserCompleted = Boolean(user.isAccepted) && userAlias.isEmailVerified;
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
||||
...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: organization.name,
|
||||
organizationId: organization.id,
|
||||
organizationSlug: organization.slug,
|
||||
hasExchangedPrivateKey: true,
|
||||
aliasId: userAlias.id,
|
||||
authMethod: AuthMethod.OIDC,
|
||||
authType: UserAliasType.OIDC,
|
||||
isUserCompleted,
|
||||
@@ -430,10 +429,11 @@ export const oidcConfigServiceFactory = ({
|
||||
|
||||
await oidcConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||
|
||||
if (user.email && !user.isEmailVerified) {
|
||||
if (user.email && !userAlias.isEmailVerified) {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
aliasId: userAlias.id
|
||||
});
|
||||
|
||||
await smtpService
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
ProjectPermissionPkiSubscriberActions,
|
||||
ProjectPermissionPkiTemplateActions,
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretEventActions,
|
||||
ProjectPermissionSecretRotationActions,
|
||||
ProjectPermissionSecretScanningConfigActions,
|
||||
ProjectPermissionSecretScanningDataSourceActions,
|
||||
@@ -252,6 +253,16 @@ const buildAdminPermissionRules = () => {
|
||||
ProjectPermissionSub.SecretScanningConfigs
|
||||
);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||
],
|
||||
ProjectPermissionSub.SecretEvents
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -455,6 +466,16 @@ const buildMemberPermissionRules = () => {
|
||||
|
||||
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||
],
|
||||
ProjectPermissionSub.SecretEvents
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
@@ -505,6 +526,16 @@ const buildViewerPermissionRules = () => {
|
||||
|
||||
can([ProjectPermissionSecretScanningConfigActions.Read], ProjectPermissionSub.SecretScanningConfigs);
|
||||
|
||||
can(
|
||||
[
|
||||
ProjectPermissionSecretEventActions.SubscribeCreated,
|
||||
ProjectPermissionSecretEventActions.SubscribeDeleted,
|
||||
ProjectPermissionSecretEventActions.SubscribeUpdated,
|
||||
ProjectPermissionSecretEventActions.SubscribeImportMutations
|
||||
],
|
||||
ProjectPermissionSub.SecretEvents
|
||||
);
|
||||
|
||||
return rules;
|
||||
};
|
||||
|
||||
|
@@ -35,6 +35,7 @@ export interface TPermissionDALFactory {
|
||||
projectFavorites?: string[] | null | undefined;
|
||||
customRoleSlug?: string | null | undefined;
|
||||
orgAuthEnforced?: boolean | null | undefined;
|
||||
orgGoogleSsoAuthEnforced: boolean;
|
||||
} & {
|
||||
groups: {
|
||||
id: string;
|
||||
@@ -87,6 +88,7 @@ export interface TPermissionDALFactory {
|
||||
}[];
|
||||
orgId: string;
|
||||
orgAuthEnforced: boolean | null | undefined;
|
||||
orgGoogleSsoAuthEnforced: boolean;
|
||||
orgRole: OrgMembershipRole;
|
||||
userId: string;
|
||||
projectId: string;
|
||||
@@ -350,6 +352,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
db.ref("slug").withSchema(TableName.OrgRoles).withSchema(TableName.OrgRoles).as("customRoleSlug"),
|
||||
db.ref("permissions").withSchema(TableName.OrgRoles),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||
db.ref("groupId").withSchema("userGroups"),
|
||||
db.ref("groupOrgId").withSchema("userGroups"),
|
||||
@@ -369,6 +372,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
OrgMembershipsSchema.extend({
|
||||
permissions: z.unknown(),
|
||||
orgAuthEnforced: z.boolean().optional().nullable(),
|
||||
orgGoogleSsoAuthEnforced: z.boolean(),
|
||||
bypassOrgAuthEnabled: z.boolean(),
|
||||
customRoleSlug: z.string().optional().nullable(),
|
||||
shouldUseNewPrivilegeSystem: z.boolean()
|
||||
@@ -988,6 +992,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
db.ref("key").withSchema(TableName.IdentityMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.IdentityMetadata).as("metadataValue"),
|
||||
db.ref("authEnforced").withSchema(TableName.Organization).as("orgAuthEnforced"),
|
||||
db.ref("googleSsoAuthEnforced").withSchema(TableName.Organization).as("orgGoogleSsoAuthEnforced"),
|
||||
db.ref("bypassOrgAuthEnabled").withSchema(TableName.Organization).as("bypassOrgAuthEnabled"),
|
||||
db.ref("role").withSchema(TableName.OrgMembership).as("orgRole"),
|
||||
db.ref("orgId").withSchema(TableName.Project),
|
||||
@@ -1003,6 +1008,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
orgId,
|
||||
username,
|
||||
orgAuthEnforced,
|
||||
orgGoogleSsoAuthEnforced,
|
||||
orgRole,
|
||||
membershipId,
|
||||
groupMembershipId,
|
||||
@@ -1016,6 +1022,7 @@ export const permissionDALFactory = (db: TDbClient): TPermissionDALFactory => {
|
||||
}) => ({
|
||||
orgId,
|
||||
orgAuthEnforced,
|
||||
orgGoogleSsoAuthEnforced,
|
||||
orgRole: orgRole as OrgMembershipRole,
|
||||
userId,
|
||||
projectId,
|
||||
|
@@ -121,6 +121,7 @@ function isAuthMethodSaml(actorAuthMethod: ActorAuthMethod) {
|
||||
function validateOrgSSO(
|
||||
actorAuthMethod: ActorAuthMethod,
|
||||
isOrgSsoEnforced: TOrganizations["authEnforced"],
|
||||
isOrgGoogleSsoEnforced: TOrganizations["googleSsoAuthEnforced"],
|
||||
isOrgSsoBypassEnabled: TOrganizations["bypassOrgAuthEnabled"],
|
||||
orgRole: OrgMembershipRole
|
||||
) {
|
||||
@@ -128,10 +129,16 @@ function validateOrgSSO(
|
||||
throw new UnauthorizedError({ name: "No auth method defined" });
|
||||
}
|
||||
|
||||
if (isOrgSsoEnforced && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||
if ((isOrgSsoEnforced || isOrgGoogleSsoEnforced) && isOrgSsoBypassEnabled && orgRole === OrgMembershipRole.Admin) {
|
||||
return;
|
||||
}
|
||||
|
||||
// case: google sso is enforced, but the actor is not using google sso
|
||||
if (isOrgGoogleSsoEnforced && actorAuthMethod !== null && actorAuthMethod !== AuthMethod.GOOGLE) {
|
||||
throw new ForbiddenRequestError({ name: "Org auth enforced. Cannot access org-scoped resource" });
|
||||
}
|
||||
|
||||
// case: SAML SSO is enforced, but the actor is not using SAML SSO
|
||||
if (
|
||||
isOrgSsoEnforced &&
|
||||
actorAuthMethod !== null &&
|
||||
|
@@ -146,6 +146,7 @@ export const permissionServiceFactory = ({
|
||||
validateOrgSSO(
|
||||
authMethod,
|
||||
membership.orgAuthEnforced,
|
||||
membership.orgGoogleSsoAuthEnforced,
|
||||
membership.bypassOrgAuthEnabled,
|
||||
membership.role as OrgMembershipRole
|
||||
);
|
||||
@@ -238,6 +239,7 @@ export const permissionServiceFactory = ({
|
||||
validateOrgSSO(
|
||||
authMethod,
|
||||
userProjectPermission.orgAuthEnforced,
|
||||
userProjectPermission.orgGoogleSsoAuthEnforced,
|
||||
userProjectPermission.bypassOrgAuthEnabled,
|
||||
userProjectPermission.orgRole
|
||||
);
|
||||
|
@@ -246,7 +246,7 @@ export const samlConfigServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
const userAlias = await userAliasDAL.findOne({
|
||||
let userAlias = await userAliasDAL.findOne({
|
||||
externalId,
|
||||
orgId,
|
||||
aliasType: UserAliasType.SAML
|
||||
@@ -320,15 +320,13 @@ export const samlConfigServiceFactory = ({
|
||||
|
||||
user = await userDAL.transaction(async (tx) => {
|
||||
let newUser: TUsers | undefined;
|
||||
if (serverCfg.trustSamlEmails) {
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
}
|
||||
newUser = await userDAL.findOne(
|
||||
{
|
||||
email,
|
||||
isEmailVerified: true
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
if (!newUser) {
|
||||
const uniqueUsername = await normalizeUsername(`${firstName ?? ""}-${lastName ?? ""}`, userDAL);
|
||||
@@ -346,13 +344,14 @@ export const samlConfigServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
await userAliasDAL.create(
|
||||
userAlias = await userAliasDAL.create(
|
||||
{
|
||||
userId: newUser.id,
|
||||
aliasType: UserAliasType.SAML,
|
||||
externalId,
|
||||
emails: email ? [email] : [],
|
||||
orgId
|
||||
orgId,
|
||||
isEmailVerified: serverCfg.trustSamlEmails
|
||||
},
|
||||
tx
|
||||
);
|
||||
@@ -410,13 +409,13 @@ export const samlConfigServiceFactory = ({
|
||||
}
|
||||
await licenseService.updateSubscriptionOrgMemberCount(organization.id);
|
||||
|
||||
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified);
|
||||
const isUserCompleted = Boolean(user.isAccepted && user.isEmailVerified && userAlias.isEmailVerified);
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
username: user.username,
|
||||
...(user.email && { email: user.email, isEmailVerified: user.isEmailVerified }),
|
||||
...(user.email && { email: user.email, isEmailVerified: userAlias.isEmailVerified }),
|
||||
firstName,
|
||||
lastName,
|
||||
organizationName: organization.name,
|
||||
@@ -424,6 +423,7 @@ export const samlConfigServiceFactory = ({
|
||||
organizationSlug: organization.slug,
|
||||
authMethod: authProvider,
|
||||
hasExchangedPrivateKey: true,
|
||||
aliasId: userAlias.id,
|
||||
authType: UserAliasType.SAML,
|
||||
isUserCompleted,
|
||||
...(relayState
|
||||
@@ -440,10 +440,11 @@ export const samlConfigServiceFactory = ({
|
||||
|
||||
await samlConfigDAL.update({ orgId }, { lastUsed: new Date() });
|
||||
|
||||
if (user.email && !user.isEmailVerified) {
|
||||
if (user.email && !userAlias.isEmailVerified) {
|
||||
const token = await tokenService.createTokenForUser({
|
||||
type: TokenType.TOKEN_EMAIL_VERIFICATION,
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
aliasId: userAlias.id
|
||||
});
|
||||
|
||||
await smtpService.sendMail({
|
||||
|
@@ -4,6 +4,7 @@ import { TDbClient } from "@app/db";
|
||||
import {
|
||||
SecretApprovalRequestsSchema,
|
||||
TableName,
|
||||
TOrgMemberships,
|
||||
TSecretApprovalRequests,
|
||||
TSecretApprovalRequestsSecrets,
|
||||
TUserGroupMembership,
|
||||
@@ -107,11 +108,32 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
||||
`secretApprovalReviewerUser.id`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("approverOrgMembership"),
|
||||
`${TableName.SecretApprovalPolicyApprover}.approverUserId`,
|
||||
`approverOrgMembership.userId`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("approverGroupOrgMembership"),
|
||||
`secretApprovalPolicyGroupApproverUser.id`,
|
||||
`approverGroupOrgMembership.userId`
|
||||
)
|
||||
|
||||
.leftJoin<TOrgMemberships>(
|
||||
db(TableName.OrgMembership).as("reviewerOrgMembership"),
|
||||
`${TableName.SecretApprovalRequestReviewer}.reviewerUserId`,
|
||||
`reviewerOrgMembership.userId`
|
||||
)
|
||||
|
||||
.select(selectAllTableCols(TableName.SecretApprovalRequest))
|
||||
.select(
|
||||
tx.ref("approverUserId").withSchema(TableName.SecretApprovalPolicyApprover),
|
||||
tx.ref("userId").withSchema("approverUserGroupMembership").as("approverGroupUserId"),
|
||||
tx.ref("email").withSchema("secretApprovalPolicyApproverUser").as("approverEmail"),
|
||||
tx.ref("isActive").withSchema("approverOrgMembership").as("approverIsOrgMembershipActive"),
|
||||
tx.ref("isActive").withSchema("approverGroupOrgMembership").as("approverGroupIsOrgMembershipActive"),
|
||||
tx.ref("email").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupEmail"),
|
||||
tx.ref("username").withSchema("secretApprovalPolicyApproverUser").as("approverUsername"),
|
||||
tx.ref("username").withSchema("secretApprovalPolicyGroupApproverUser").as("approverGroupUsername"),
|
||||
@@ -148,6 +170,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
tx.ref("username").withSchema("secretApprovalReviewerUser").as("reviewerUsername"),
|
||||
tx.ref("firstName").withSchema("secretApprovalReviewerUser").as("reviewerFirstName"),
|
||||
tx.ref("lastName").withSchema("secretApprovalReviewerUser").as("reviewerLastName"),
|
||||
tx.ref("isActive").withSchema("reviewerOrgMembership").as("reviewerIsOrgMembershipActive"),
|
||||
tx.ref("id").withSchema(TableName.SecretApprovalPolicy).as("policyId"),
|
||||
tx.ref("name").withSchema(TableName.SecretApprovalPolicy).as("policyName"),
|
||||
tx.ref("projectId").withSchema(TableName.Environment),
|
||||
@@ -157,7 +180,11 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
tx.ref("enforcementLevel").withSchema(TableName.SecretApprovalPolicy).as("policyEnforcementLevel"),
|
||||
tx.ref("allowedSelfApprovals").withSchema(TableName.SecretApprovalPolicy).as("policyAllowedSelfApprovals"),
|
||||
tx.ref("approvals").withSchema(TableName.SecretApprovalPolicy).as("policyApprovals"),
|
||||
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt")
|
||||
tx.ref("deletedAt").withSchema(TableName.SecretApprovalPolicy).as("policyDeletedAt"),
|
||||
tx
|
||||
.ref("shouldCheckSecretPermission")
|
||||
.withSchema(TableName.SecretApprovalPolicy)
|
||||
.as("policySecretReadAccessCompat")
|
||||
);
|
||||
|
||||
const findById = async (id: string, tx?: Knex) => {
|
||||
@@ -197,7 +224,8 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
enforcementLevel: el.policyEnforcementLevel,
|
||||
envId: el.policyEnvId,
|
||||
deletedAt: el.policyDeletedAt,
|
||||
allowedSelfApprovals: el.policyAllowedSelfApprovals
|
||||
allowedSelfApprovals: el.policyAllowedSelfApprovals,
|
||||
shouldCheckSecretPermission: el.policySecretReadAccessCompat
|
||||
}
|
||||
}),
|
||||
childrenMapper: [
|
||||
@@ -211,9 +239,21 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
reviewerLastName: lastName,
|
||||
reviewerUsername: username,
|
||||
reviewerFirstName: firstName,
|
||||
reviewerComment: comment
|
||||
reviewerComment: comment,
|
||||
reviewerIsOrgMembershipActive: isOrgMembershipActive
|
||||
}) =>
|
||||
userId ? { userId, status, email, firstName, lastName, username, comment: comment ?? "" } : undefined
|
||||
userId
|
||||
? {
|
||||
userId,
|
||||
status,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username,
|
||||
comment: comment ?? "",
|
||||
isOrgMembershipActive
|
||||
}
|
||||
: undefined
|
||||
},
|
||||
{
|
||||
key: "approverUserId",
|
||||
@@ -223,13 +263,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
approverEmail: email,
|
||||
approverUsername: username,
|
||||
approverLastName: lastName,
|
||||
approverFirstName: firstName
|
||||
approverFirstName: firstName,
|
||||
approverIsOrgMembershipActive: isOrgMembershipActive
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username
|
||||
username,
|
||||
isOrgMembershipActive
|
||||
})
|
||||
},
|
||||
{
|
||||
@@ -240,13 +282,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
approverGroupEmail: email,
|
||||
approverGroupUsername: username,
|
||||
approverGroupLastName: lastName,
|
||||
approverGroupFirstName: firstName
|
||||
approverGroupFirstName: firstName,
|
||||
approverGroupIsOrgMembershipActive: isOrgMembershipActive
|
||||
}) => ({
|
||||
userId,
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
username
|
||||
username,
|
||||
isOrgMembershipActive
|
||||
})
|
||||
},
|
||||
{
|
||||
@@ -653,14 +697,15 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
db.ref("firstName").withSchema("committerUser").as("committerUserFirstName"),
|
||||
db.ref("lastName").withSchema("committerUser").as("committerUserLastName")
|
||||
)
|
||||
.distinctOn(`${TableName.SecretApprovalRequest}.id`)
|
||||
.as("inner");
|
||||
|
||||
const query = (tx || db)
|
||||
.select("*")
|
||||
const countQuery = (await (tx || db)
|
||||
.select(db.raw("count(*) OVER() as total_count"))
|
||||
.from(innerQuery)
|
||||
.orderBy("createdAt", "desc") as typeof innerQuery;
|
||||
.from(innerQuery.clone().distinctOn(`${TableName.SecretApprovalRequest}.id`))) as Array<{
|
||||
total_count: number;
|
||||
}>;
|
||||
|
||||
const query = (tx || db).select("*").from(innerQuery).orderBy("createdAt", "desc") as typeof innerQuery;
|
||||
|
||||
if (search) {
|
||||
void query.where((qb) => {
|
||||
@@ -686,8 +731,7 @@ export const secretApprovalRequestDALFactory = (db: TDbClient) => {
|
||||
.where("w.rank", ">=", rankOffset)
|
||||
.andWhere("w.rank", "<", rankOffset + limit);
|
||||
|
||||
// @ts-expect-error knex does not infer
|
||||
const totalCount = Number(docs[0]?.total_count || 0);
|
||||
const totalCount = Number(countQuery[0]?.total_count || 0);
|
||||
|
||||
const formattedDoc = sqlNestRelationships({
|
||||
data: docs,
|
||||
|
@@ -258,6 +258,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
if (actor === ActorType.SERVICE) throw new BadRequestError({ message: "Cannot use service token" });
|
||||
|
||||
const secretApprovalRequest = await secretApprovalRequestDAL.findById(id);
|
||||
|
||||
if (!secretApprovalRequest)
|
||||
throw new NotFoundError({ message: `Secret approval request with ID '${id}' not found` });
|
||||
|
||||
@@ -280,13 +281,22 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
) {
|
||||
throw new ForbiddenRequestError({ message: "User has insufficient privileges" });
|
||||
}
|
||||
const getHasSecretReadAccess = (environment: string, tags: { slug: string }[], secretPath?: string) => {
|
||||
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
environment,
|
||||
secretPath: secretPath || "/",
|
||||
secretTags: tags.map((i) => i.slug)
|
||||
});
|
||||
return canRead;
|
||||
const getHasSecretReadAccess = (
|
||||
shouldCheckSecretPermission: boolean | null | undefined,
|
||||
environment: string,
|
||||
tags: { slug: string }[],
|
||||
secretPath?: string
|
||||
) => {
|
||||
if (shouldCheckSecretPermission) {
|
||||
const canRead = hasSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.ReadValue, {
|
||||
environment,
|
||||
secretPath: secretPath || "/",
|
||||
secretTags: tags.map((i) => i.slug)
|
||||
});
|
||||
return canRead;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
let secrets;
|
||||
@@ -308,8 +318,18 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
version: el.version,
|
||||
secretMetadata: el.secretMetadata as ResourceMetadataDTO,
|
||||
isRotatedSecret: el.secret?.isRotatedSecret ?? false,
|
||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValue: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secret && el.secret.isRotatedSecret
|
||||
? undefined
|
||||
@@ -325,11 +345,17 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
id: el.secret.id,
|
||||
version: el.secret.version,
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
secretValue: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secret.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secret.encryptedValue }).toString()
|
||||
@@ -345,11 +371,17 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
id: el.secretVersion.id,
|
||||
version: el.secretVersion.version,
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
secretValue: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path)
|
||||
secretValue: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
)
|
||||
? INFISICAL_SECRET_VALUE_HIDDEN_MASK
|
||||
: el.secretVersion.encryptedValue
|
||||
? secretManagerDecryptor({ cipherTextBlob: el.secretVersion.encryptedValue }).toString()
|
||||
@@ -367,7 +399,12 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
const encryptedSecrets = await secretApprovalRequestSecretDAL.findByRequestId(secretApprovalRequest.id);
|
||||
secrets = encryptedSecrets.map((el) => ({
|
||||
...el,
|
||||
secretValueHidden: !getHasSecretReadAccess(secretApprovalRequest.environment, el.tags, secretPath?.[0]?.path),
|
||||
secretValueHidden: !getHasSecretReadAccess(
|
||||
secretApprovalRequest.policy.shouldCheckSecretPermission,
|
||||
secretApprovalRequest.environment,
|
||||
el.tags,
|
||||
secretPath?.[0]?.path
|
||||
),
|
||||
...decryptSecretWithBot(el, botKey),
|
||||
secret: el.secret
|
||||
? {
|
||||
@@ -1447,6 +1484,7 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
|
||||
const commits: Omit<TSecretApprovalRequestsSecretsV2Insert, "requestId">[] = [];
|
||||
const commitTagIds: Record<string, string[]> = {};
|
||||
const existingTagIds: Record<string, string[]> = {};
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
@@ -1512,6 +1550,11 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
type: SecretType.Shared
|
||||
}))
|
||||
);
|
||||
|
||||
secretsToUpdateStoredInDB.forEach((el) => {
|
||||
if (el.tags?.length) existingTagIds[el.key] = el.tags.map((i) => i.id);
|
||||
});
|
||||
|
||||
if (secretsToUpdateStoredInDB.length !== secretsToUpdate.length)
|
||||
throw new NotFoundError({
|
||||
message: `Secret does not exist: ${secretsToUpdateStoredInDB.map((el) => el.key).join(",")}`
|
||||
@@ -1555,7 +1598,10 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretMetadata
|
||||
}) => {
|
||||
const secretId = updatingSecretsGroupByKey[secretKey][0].id;
|
||||
if (tagIds?.length) commitTagIds[newSecretName ?? secretKey] = tagIds;
|
||||
if (tagIds?.length || existingTagIds[secretKey]?.length) {
|
||||
commitTagIds[newSecretName ?? secretKey] = tagIds || existingTagIds[secretKey];
|
||||
}
|
||||
|
||||
return {
|
||||
...latestSecretVersions[secretId],
|
||||
secretMetadata,
|
||||
|
@@ -2312,6 +2312,15 @@ export const AppConnections = {
|
||||
OKTA: {
|
||||
instanceUrl: "The URL used to access your Okta organization.",
|
||||
apiToken: "The API token used to authenticate with Okta."
|
||||
},
|
||||
AZURE_ADCS: {
|
||||
adcsUrl:
|
||||
"The HTTPS URL of the Azure ADCS instance to connect with (e.g., 'https://adcs.yourdomain.com/certsrv').",
|
||||
username: "The username used to access Azure ADCS (format: 'DOMAIN\\username' or 'username@domain.com').",
|
||||
password: "The password used to access Azure ADCS.",
|
||||
sslRejectUnauthorized:
|
||||
"Whether or not to reject unauthorized SSL certificates (true/false). Set to false only in test environments with self-signed certificates.",
|
||||
sslCertificate: "The SSL certificate (PEM format) to use for secure connection."
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2491,6 +2500,7 @@ export const SecretSyncs = {
|
||||
},
|
||||
RENDER: {
|
||||
serviceId: "The ID of the Render service to sync secrets to.",
|
||||
environmentGroupId: "The ID of the Render environment group to sync secrets to.",
|
||||
scope: "The Render scope that secrets should be synced to.",
|
||||
type: "The Render resource type to sync secrets to."
|
||||
},
|
||||
|
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Safely retrieves a value from a nested object using dot notation path
|
||||
*/
|
||||
export const getStringValueByDot = (
|
||||
export const getValueByDot = (
|
||||
obj: Record<string, unknown> | null | undefined,
|
||||
path: string,
|
||||
defaultValue?: string
|
||||
): string | undefined => {
|
||||
defaultValue?: string | number | boolean
|
||||
): string | number | boolean | undefined => {
|
||||
// Handle null or undefined input
|
||||
if (!obj) {
|
||||
return defaultValue;
|
||||
@@ -26,7 +26,7 @@ export const getStringValueByDot = (
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
|
||||
if (typeof current !== "string") {
|
||||
if (typeof current !== "string" && typeof current !== "number" && typeof current !== "boolean") {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
@@ -726,7 +726,8 @@ export const registerRoutes = async (
|
||||
permissionService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
projectMembershipDAL
|
||||
projectMembershipDAL,
|
||||
userAliasDAL
|
||||
});
|
||||
|
||||
const totpService = totpServiceFactory({
|
||||
|
@@ -15,6 +15,10 @@ import {
|
||||
} from "@app/services/app-connection/1password";
|
||||
import { Auth0ConnectionListItemSchema, SanitizedAuth0ConnectionSchema } from "@app/services/app-connection/auth0";
|
||||
import { AwsConnectionListItemSchema, SanitizedAwsConnectionSchema } from "@app/services/app-connection/aws";
|
||||
import {
|
||||
AzureADCSConnectionListItemSchema,
|
||||
SanitizedAzureADCSConnectionSchema
|
||||
} from "@app/services/app-connection/azure-adcs/azure-adcs-connection-schemas";
|
||||
import {
|
||||
AzureAppConfigurationConnectionListItemSchema,
|
||||
SanitizedAzureAppConfigurationConnectionSchema
|
||||
@@ -150,7 +154,8 @@ const SanitizedAppConnectionSchema = z.union([
|
||||
...SanitizedSupabaseConnectionSchema.options,
|
||||
...SanitizedDigitalOceanConnectionSchema.options,
|
||||
...SanitizedNetlifyConnectionSchema.options,
|
||||
...SanitizedOktaConnectionSchema.options
|
||||
...SanitizedOktaConnectionSchema.options,
|
||||
...SanitizedAzureADCSConnectionSchema.options
|
||||
]);
|
||||
|
||||
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
@@ -190,7 +195,8 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
|
||||
SupabaseConnectionListItemSchema,
|
||||
DigitalOceanConnectionListItemSchema,
|
||||
NetlifyConnectionListItemSchema,
|
||||
OktaConnectionListItemSchema
|
||||
OktaConnectionListItemSchema,
|
||||
AzureADCSConnectionListItemSchema
|
||||
]);
|
||||
|
||||
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
|
@@ -0,0 +1,18 @@
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
CreateAzureADCSConnectionSchema,
|
||||
SanitizedAzureADCSConnectionSchema,
|
||||
UpdateAzureADCSConnectionSchema
|
||||
} from "@app/services/app-connection/azure-adcs";
|
||||
|
||||
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
|
||||
|
||||
export const registerAzureADCSConnectionRouter = async (server: FastifyZodProvider) => {
|
||||
registerAppConnectionEndpoints({
|
||||
app: AppConnection.AzureADCS,
|
||||
server,
|
||||
sanitizedResponseSchema: SanitizedAzureADCSConnectionSchema,
|
||||
createSchema: CreateAzureADCSConnectionSchema,
|
||||
updateSchema: UpdateAzureADCSConnectionSchema
|
||||
});
|
||||
};
|
@@ -53,4 +53,36 @@ export const registerChecklyConnectionRouter = async (server: FastifyZodProvider
|
||||
return { accounts };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/accounts/:accountId/groups`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid(),
|
||||
accountId: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
groups: z
|
||||
.object({
|
||||
name: z.string(),
|
||||
id: z.string()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId, accountId } = req.params;
|
||||
|
||||
const groups = await server.services.appConnection.checkly.listGroups(connectionId, accountId, req.permission);
|
||||
|
||||
return { groups };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -5,6 +5,7 @@ import { AppConnection } from "@app/services/app-connection/app-connection-enums
|
||||
import { registerOnePassConnectionRouter } from "./1password-connection-router";
|
||||
import { registerAuth0ConnectionRouter } from "./auth0-connection-router";
|
||||
import { registerAwsConnectionRouter } from "./aws-connection-router";
|
||||
import { registerAzureADCSConnectionRouter } from "./azure-adcs-connection-router";
|
||||
import { registerAzureAppConfigurationConnectionRouter } from "./azure-app-configuration-connection-router";
|
||||
import { registerAzureClientSecretsConnectionRouter } from "./azure-client-secrets-connection-router";
|
||||
import { registerAzureDevOpsConnectionRouter } from "./azure-devops-connection-router";
|
||||
@@ -50,6 +51,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
|
||||
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
|
||||
[AppConnection.AzureClientSecrets]: registerAzureClientSecretsConnectionRouter,
|
||||
[AppConnection.AzureDevOps]: registerAzureDevOpsConnectionRouter,
|
||||
[AppConnection.AzureADCS]: registerAzureADCSConnectionRouter,
|
||||
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
|
||||
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
|
||||
[AppConnection.TerraformCloud]: registerTerraformCloudConnectionRouter,
|
||||
|
@@ -49,4 +49,32 @@ export const registerRenderConnectionRouter = async (server: FastifyZodProvider)
|
||||
return services;
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: `/:connectionId/environment-groups`,
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
connectionId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
})
|
||||
.array()
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { connectionId } = req.params;
|
||||
const groups = await server.services.appConnection.render.listEnvironmentGroups(connectionId, req.permission);
|
||||
|
||||
return groups;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@@ -67,7 +67,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
|
||||
handler: () => ({ message: "Authenticated" as const })
|
||||
});
|
||||
|
||||
|
@@ -0,0 +1,78 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import {
|
||||
AzureAdCsCertificateAuthoritySchema,
|
||||
CreateAzureAdCsCertificateAuthoritySchema,
|
||||
UpdateAzureAdCsCertificateAuthoritySchema
|
||||
} from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
|
||||
import { registerCertificateAuthorityEndpoints } from "./certificate-authority-endpoints";
|
||||
|
||||
export const registerAzureAdCsCertificateAuthorityRouter = async (server: FastifyZodProvider) => {
|
||||
registerCertificateAuthorityEndpoints({
|
||||
caType: CaType.AZURE_AD_CS,
|
||||
server,
|
||||
responseSchema: AzureAdCsCertificateAuthoritySchema,
|
||||
createSchema: CreateAzureAdCsCertificateAuthoritySchema,
|
||||
updateSchema: UpdateAzureAdCsCertificateAuthoritySchema
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/:caId/templates",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
hide: false,
|
||||
description: "Get available certificate templates from Azure AD CS CA",
|
||||
params: z.object({
|
||||
caId: z.string().describe("Azure AD CS CA ID")
|
||||
}),
|
||||
querystring: z.object({
|
||||
projectId: z.string().describe("Project ID")
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
templates: z.array(
|
||||
z.object({
|
||||
id: z.string().describe("Template identifier"),
|
||||
name: z.string().describe("Template display name"),
|
||||
description: z.string().optional().describe("Template description")
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const templates = await server.services.certificateAuthority.getAzureAdcsTemplates({
|
||||
caId: req.params.caId,
|
||||
projectId: req.query.projectId,
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_AZURE_AD_TEMPLATES,
|
||||
metadata: {
|
||||
caId: req.params.caId,
|
||||
amount: templates.length
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { templates };
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,6 +1,7 @@
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
|
||||
import { registerAcmeCertificateAuthorityRouter } from "./acme-certificate-authority-router";
|
||||
import { registerAzureAdCsCertificateAuthorityRouter } from "./azure-ad-cs-certificate-authority-router";
|
||||
import { registerInternalCertificateAuthorityRouter } from "./internal-certificate-authority-router";
|
||||
|
||||
export * from "./internal-certificate-authority-router";
|
||||
@@ -8,5 +9,6 @@ export * from "./internal-certificate-authority-router";
|
||||
export const CERTIFICATE_AUTHORITY_REGISTER_ROUTER_MAP: Record<CaType, (server: FastifyZodProvider) => Promise<void>> =
|
||||
{
|
||||
[CaType.INTERNAL]: registerInternalCertificateAuthorityRouter,
|
||||
[CaType.ACME]: registerAcmeCertificateAuthorityRouter
|
||||
[CaType.ACME]: registerAcmeCertificateAuthorityRouter,
|
||||
[CaType.AZURE_AD_CS]: registerAzureAdCsCertificateAuthorityRouter
|
||||
};
|
||||
|
@@ -279,6 +279,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
||||
name: GenericResourceNameSchema.optional(),
|
||||
slug: slugSchema({ max: 64 }).optional(),
|
||||
authEnforced: z.boolean().optional(),
|
||||
googleSsoAuthEnforced: z.boolean().optional(),
|
||||
scimEnabled: z.boolean().optional(),
|
||||
defaultMembershipRoleSlug: slugSchema({ max: 64, field: "Default Membership Role" }).optional(),
|
||||
enforceMfa: z.boolean().optional(),
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import RE2 from "re2";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CertificatesSchema } from "@app/db/schemas";
|
||||
@@ -112,7 +113,88 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
.transform((arr) => Array.from(new Set(arr)))
|
||||
.describe(PKI_SUBSCRIBERS.CREATE.extendedKeyUsages),
|
||||
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.CREATE.enableAutoRenewal),
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays)
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.CREATE.autoRenewalPeriodInDays),
|
||||
properties: z
|
||||
.object({
|
||||
azureTemplateType: z.string().optional().describe("Azure ADCS Certificate Template Type"),
|
||||
organization: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Organization cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Organization cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Organization (O) - Maximum 64 characters, no special DN characters"),
|
||||
organizationalUnit: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Organizational Unit cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Organizational Unit (OU) - Maximum 64 characters, no special DN characters"),
|
||||
country: z
|
||||
.string()
|
||||
.trim()
|
||||
.length(2, "Country must be exactly 2 characters")
|
||||
.regex(new RE2("^[A-Z]{2}$"), "Country must be exactly 2 uppercase letters")
|
||||
.optional()
|
||||
.describe("Country (C) - Two uppercase letter country code (e.g., US, CA, GB)"),
|
||||
state: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "State cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"State cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("State/Province (ST) - Maximum 64 characters, no special DN characters"),
|
||||
locality: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Locality cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Locality cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Locality (L) - Maximum 64 characters, no special DN characters"),
|
||||
emailAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.email("Email Address must be a valid email format")
|
||||
.min(6, "Email Address must be at least 6 characters")
|
||||
.max(64, "Email Address cannot exceed 64 characters")
|
||||
.optional()
|
||||
.describe("Email Address - Valid email format between 6 and 64 characters")
|
||||
})
|
||||
.optional()
|
||||
.describe("Additional subscriber properties and subject fields")
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
@@ -199,7 +281,88 @@ export const registerPkiSubscriberRouter = async (server: FastifyZodProvider) =>
|
||||
.optional()
|
||||
.describe(PKI_SUBSCRIBERS.UPDATE.extendedKeyUsages),
|
||||
enableAutoRenewal: z.boolean().optional().describe(PKI_SUBSCRIBERS.UPDATE.enableAutoRenewal),
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays)
|
||||
autoRenewalPeriodInDays: z.number().min(1).optional().describe(PKI_SUBSCRIBERS.UPDATE.autoRenewalPeriodInDays),
|
||||
properties: z
|
||||
.object({
|
||||
azureTemplateType: z.string().optional().describe("Azure ADCS Certificate Template Type"),
|
||||
organization: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Organization cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Organization contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Organization cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Organization (O) - Maximum 64 characters, no special DN characters"),
|
||||
organizationalUnit: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Organizational Unit cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Organizational Unit contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Organizational Unit cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Organizational Unit (OU) - Maximum 64 characters, no special DN characters"),
|
||||
country: z
|
||||
.string()
|
||||
.trim()
|
||||
.length(2, "Country must be exactly 2 characters")
|
||||
.regex(new RE2("^[A-Z]{2}$"), "Country must be exactly 2 uppercase letters")
|
||||
.optional()
|
||||
.describe("Country (C) - Two uppercase letter country code (e.g., US, CA, GB)"),
|
||||
state: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "State cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'State contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"State cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("State/Province (ST) - Maximum 64 characters, no special DN characters"),
|
||||
locality: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(64, "Locality cannot exceed 64 characters")
|
||||
.regex(
|
||||
new RE2('^[^,=+<>#;\\\\"/\\r\\n\\t]*$'),
|
||||
'Locality contains invalid characters: , = + < > # ; \\ " / \\r \\n \\t'
|
||||
)
|
||||
.regex(
|
||||
new RE2("^[^\\\\s\\\\-_.]+.*[^\\\\s\\\\-_.]+$|^[^\\\\s\\\\-_.]{1}$"),
|
||||
"Locality cannot start or end with spaces, hyphens, underscores, or periods"
|
||||
)
|
||||
.optional()
|
||||
.describe("Locality (L) - Maximum 64 characters, no special DN characters"),
|
||||
emailAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.email("Email Address must be a valid email format")
|
||||
.min(6, "Email Address must be at least 6 characters")
|
||||
.max(64, "Email Address cannot exceed 64 characters")
|
||||
.optional()
|
||||
.describe("Email Address - Valid email format between 6 and 64 characters")
|
||||
})
|
||||
.optional()
|
||||
.describe("Additional subscriber properties and subject fields")
|
||||
}),
|
||||
response: {
|
||||
200: sanitizedPkiSubscriber
|
||||
|
@@ -108,7 +108,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
id: true
|
||||
}).merge(UserEncryptionKeysSchema.pick({ publicKey: true })),
|
||||
})
|
||||
.merge(UserEncryptionKeysSchema.pick({ publicKey: true }))
|
||||
.extend({
|
||||
isOrgMembershipActive: z.boolean()
|
||||
}),
|
||||
project: SanitizedProjectSchema.pick({ name: true, id: true }),
|
||||
roles: z.array(
|
||||
z.object({
|
||||
|
@@ -54,6 +54,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
try {
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const callbackPort = req.session.get("callbackPort");
|
||||
// @ts-expect-error this is because this is express type and not fastify
|
||||
const orgSlug = req.session.get("orgSlug");
|
||||
|
||||
const email = profile?.emails?.[0]?.value;
|
||||
if (!email)
|
||||
@@ -67,7 +69,8 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
||||
firstName: profile?.name?.givenName || "",
|
||||
lastName: profile?.name?.familyName || "",
|
||||
authMethod: AuthMethod.GOOGLE,
|
||||
callbackPort
|
||||
callbackPort,
|
||||
orgSlug
|
||||
});
|
||||
cb(null, { isUserCompleted, providerAuthToken });
|
||||
} catch (error) {
|
||||
@@ -215,6 +218,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
callback_port: z.string().optional(),
|
||||
org_slug: z.string().optional(),
|
||||
is_admin_login: z
|
||||
.string()
|
||||
.optional()
|
||||
@@ -223,12 +227,15 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
preValidation: [
|
||||
async (req, res) => {
|
||||
const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
|
||||
const { callback_port: callbackPort, is_admin_login: isAdminLogin, org_slug: orgSlug } = req.query;
|
||||
// ensure fresh session state per login attempt
|
||||
await req.session.regenerate();
|
||||
if (callbackPort) {
|
||||
req.session.set("callbackPort", callbackPort);
|
||||
}
|
||||
if (orgSlug) {
|
||||
req.session.set("orgSlug", orgSlug);
|
||||
}
|
||||
if (isAdminLogin) {
|
||||
req.session.set("isAdminLogin", isAdminLogin);
|
||||
}
|
||||
|
@@ -6,12 +6,14 @@ import { readLimit } from "@app/server/config/rateLimiter";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { AcmeCertificateAuthoritySchema } from "@app/services/certificate-authority/acme/acme-certificate-authority-schemas";
|
||||
import { AzureAdCsCertificateAuthoritySchema } from "@app/services/certificate-authority/azure-ad-cs/azure-ad-cs-certificate-authority-schemas";
|
||||
import { CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { InternalCertificateAuthoritySchema } from "@app/services/certificate-authority/internal/internal-certificate-authority-schemas";
|
||||
|
||||
const CertificateAuthoritySchema = z.discriminatedUnion("type", [
|
||||
InternalCertificateAuthoritySchema,
|
||||
AcmeCertificateAuthoritySchema
|
||||
AcmeCertificateAuthoritySchema,
|
||||
AzureAdCsCertificateAuthoritySchema
|
||||
]);
|
||||
|
||||
export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
@@ -52,19 +54,31 @@ export const registerCaRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission
|
||||
);
|
||||
|
||||
const azureAdCsCas = await server.services.certificateAuthority.listCertificateAuthoritiesByProjectId(
|
||||
{
|
||||
projectId: req.query.projectId,
|
||||
type: CaType.AZURE_AD_CS
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
projectId: req.query.projectId,
|
||||
event: {
|
||||
type: EventType.GET_CAS,
|
||||
metadata: {
|
||||
caIds: [...(internalCas ?? []).map((ca) => ca.id), ...(acmeCas ?? []).map((ca) => ca.id)]
|
||||
caIds: [
|
||||
...(internalCas ?? []).map((ca) => ca.id),
|
||||
...(acmeCas ?? []).map((ca) => ca.id),
|
||||
...(azureAdCsCas ?? []).map((ca) => ca.id)
|
||||
]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? [])]
|
||||
certificateAuthorities: [...(internalCas ?? []), ...(acmeCas ?? []), ...(azureAdCsCas ?? [])]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@@ -18,14 +18,14 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
username: z.string().trim()
|
||||
token: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({})
|
||||
}
|
||||
},
|
||||
handler: async (req) => {
|
||||
await server.services.user.sendEmailVerificationCode(req.body.username);
|
||||
await server.services.user.sendEmailVerificationCode(req.body.token);
|
||||
return {};
|
||||
}
|
||||
});
|
||||
|
@@ -8,6 +8,7 @@ export enum AppConnection {
|
||||
AzureAppConfiguration = "azure-app-configuration",
|
||||
AzureClientSecrets = "azure-client-secrets",
|
||||
AzureDevOps = "azure-devops",
|
||||
AzureADCS = "azure-adcs",
|
||||
Humanitec = "humanitec",
|
||||
TerraformCloud = "terraform-cloud",
|
||||
Vercel = "vercel",
|
||||
|
@@ -31,6 +31,11 @@ import {
|
||||
} from "./app-connection-types";
|
||||
import { Auth0ConnectionMethod, getAuth0ConnectionListItem, validateAuth0ConnectionCredentials } from "./auth0";
|
||||
import { AwsConnectionMethod, getAwsConnectionListItem, validateAwsConnectionCredentials } from "./aws";
|
||||
import { AzureADCSConnectionMethod } from "./azure-adcs";
|
||||
import {
|
||||
getAzureADCSConnectionListItem,
|
||||
validateAzureADCSConnectionCredentials
|
||||
} from "./azure-adcs/azure-adcs-connection-fns";
|
||||
import {
|
||||
AzureAppConfigurationConnectionMethod,
|
||||
getAzureAppConfigurationConnectionListItem,
|
||||
@@ -136,6 +141,7 @@ export const listAppConnectionOptions = () => {
|
||||
getAzureKeyVaultConnectionListItem(),
|
||||
getAzureAppConfigurationConnectionListItem(),
|
||||
getAzureDevopsConnectionListItem(),
|
||||
getAzureADCSConnectionListItem(),
|
||||
getDatabricksConnectionListItem(),
|
||||
getHumanitecConnectionListItem(),
|
||||
getTerraformCloudConnectionListItem(),
|
||||
@@ -227,6 +233,7 @@ export const validateAppConnectionCredentials = async (
|
||||
[AppConnection.AzureClientSecrets]:
|
||||
validateAzureClientSecretsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureDevOps]: validateAzureDevOpsConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.AzureADCS]: validateAzureADCSConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Humanitec]: validateHumanitecConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.Postgres]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
[AppConnection.MsSql]: validateSqlConnectionCredentials as TAppConnectionCredentialsValidator,
|
||||
@@ -300,6 +307,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) =>
|
||||
case MsSqlConnectionMethod.UsernameAndPassword:
|
||||
case MySqlConnectionMethod.UsernameAndPassword:
|
||||
case OracleDBConnectionMethod.UsernameAndPassword:
|
||||
case AzureADCSConnectionMethod.UsernamePassword:
|
||||
return "Username & Password";
|
||||
case WindmillConnectionMethod.AccessToken:
|
||||
case HCVaultConnectionMethod.AccessToken:
|
||||
@@ -357,6 +365,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record<
|
||||
[AppConnection.AzureKeyVault]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureAppConfiguration]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureDevOps]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.AzureADCS]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Humanitec]: platformManagedCredentialsNotSupported,
|
||||
[AppConnection.Postgres]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
[AppConnection.MsSql]: transferSqlConnectionCredentialsToPlatform as TAppConnectionTransitionCredentialsToPlatform,
|
||||
|
@@ -9,6 +9,7 @@ export const APP_CONNECTION_NAME_MAP: Record<AppConnection, string> = {
|
||||
[AppConnection.AzureAppConfiguration]: "Azure App Configuration",
|
||||
[AppConnection.AzureClientSecrets]: "Azure Client Secrets",
|
||||
[AppConnection.AzureDevOps]: "Azure DevOps",
|
||||
[AppConnection.AzureADCS]: "Azure ADCS",
|
||||
[AppConnection.Databricks]: "Databricks",
|
||||
[AppConnection.Humanitec]: "Humanitec",
|
||||
[AppConnection.TerraformCloud]: "Terraform Cloud",
|
||||
@@ -49,6 +50,7 @@ export const APP_CONNECTION_PLAN_MAP: Record<AppConnection, AppConnectionPlanTyp
|
||||
[AppConnection.AzureAppConfiguration]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureClientSecrets]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureDevOps]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.AzureADCS]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Databricks]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.Humanitec]: AppConnectionPlanType.Regular,
|
||||
[AppConnection.TerraformCloud]: AppConnectionPlanType.Regular,
|
||||
|
@@ -45,6 +45,7 @@ import {
|
||||
import { ValidateAuth0ConnectionCredentialsSchema } from "./auth0";
|
||||
import { ValidateAwsConnectionCredentialsSchema } from "./aws";
|
||||
import { awsConnectionService } from "./aws/aws-connection-service";
|
||||
import { ValidateAzureADCSConnectionCredentialsSchema } from "./azure-adcs/azure-adcs-connection-schemas";
|
||||
import { ValidateAzureAppConfigurationConnectionCredentialsSchema } from "./azure-app-configuration";
|
||||
import { ValidateAzureClientSecretsConnectionCredentialsSchema } from "./azure-client-secrets";
|
||||
import { azureClientSecretsConnectionService } from "./azure-client-secrets/azure-client-secrets-service";
|
||||
@@ -122,6 +123,7 @@ const VALIDATE_APP_CONNECTION_CREDENTIALS_MAP: Record<AppConnection, TValidateAp
|
||||
[AppConnection.AzureKeyVault]: ValidateAzureKeyVaultConnectionCredentialsSchema,
|
||||
[AppConnection.AzureAppConfiguration]: ValidateAzureAppConfigurationConnectionCredentialsSchema,
|
||||
[AppConnection.AzureDevOps]: ValidateAzureDevOpsConnectionCredentialsSchema,
|
||||
[AppConnection.AzureADCS]: ValidateAzureADCSConnectionCredentialsSchema,
|
||||
[AppConnection.Databricks]: ValidateDatabricksConnectionCredentialsSchema,
|
||||
[AppConnection.Humanitec]: ValidateHumanitecConnectionCredentialsSchema,
|
||||
[AppConnection.TerraformCloud]: ValidateTerraformCloudConnectionCredentialsSchema,
|
||||
|
@@ -33,6 +33,12 @@ import {
|
||||
TAwsConnectionInput,
|
||||
TValidateAwsConnectionCredentialsSchema
|
||||
} from "./aws";
|
||||
import {
|
||||
TAzureADCSConnection,
|
||||
TAzureADCSConnectionConfig,
|
||||
TAzureADCSConnectionInput,
|
||||
TValidateAzureADCSConnectionCredentialsSchema
|
||||
} from "./azure-adcs/azure-adcs-connection-types";
|
||||
import {
|
||||
TAzureAppConfigurationConnection,
|
||||
TAzureAppConfigurationConnectionConfig,
|
||||
@@ -223,6 +229,7 @@ export type TAppConnection = { id: string } & (
|
||||
| TAzureKeyVaultConnection
|
||||
| TAzureAppConfigurationConnection
|
||||
| TAzureDevOpsConnection
|
||||
| TAzureADCSConnection
|
||||
| TDatabricksConnection
|
||||
| THumanitecConnection
|
||||
| TTerraformCloudConnection
|
||||
@@ -267,6 +274,7 @@ export type TAppConnectionInput = { id: string } & (
|
||||
| TAzureKeyVaultConnectionInput
|
||||
| TAzureAppConfigurationConnectionInput
|
||||
| TAzureDevOpsConnectionInput
|
||||
| TAzureADCSConnectionInput
|
||||
| TDatabricksConnectionInput
|
||||
| THumanitecConnectionInput
|
||||
| TTerraformCloudConnectionInput
|
||||
@@ -322,6 +330,7 @@ export type TAppConnectionConfig =
|
||||
| TAzureKeyVaultConnectionConfig
|
||||
| TAzureAppConfigurationConnectionConfig
|
||||
| TAzureDevOpsConnectionConfig
|
||||
| TAzureADCSConnectionConfig
|
||||
| TAzureClientSecretsConnectionConfig
|
||||
| TDatabricksConnectionConfig
|
||||
| THumanitecConnectionConfig
|
||||
@@ -359,6 +368,7 @@ export type TValidateAppConnectionCredentialsSchema =
|
||||
| TValidateAzureAppConfigurationConnectionCredentialsSchema
|
||||
| TValidateAzureClientSecretsConnectionCredentialsSchema
|
||||
| TValidateAzureDevOpsConnectionCredentialsSchema
|
||||
| TValidateAzureADCSConnectionCredentialsSchema
|
||||
| TValidateDatabricksConnectionCredentialsSchema
|
||||
| TValidateHumanitecConnectionCredentialsSchema
|
||||
| TValidatePostgresConnectionCredentialsSchema
|
||||
|
@@ -0,0 +1,3 @@
|
||||
export enum AzureADCSConnectionMethod {
|
||||
UsernamePassword = "username-password"
|
||||
}
|
@@ -0,0 +1,455 @@
|
||||
/* eslint-disable no-case-declarations, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires, no-await-in-loop, no-continue */
|
||||
import { NtlmClient } from "axios-ntlm";
|
||||
import https from "https";
|
||||
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator/validate-url";
|
||||
import { decryptAppConnectionCredentials } from "@app/services/app-connection/app-connection-fns";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection-dal";
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
|
||||
import { TAzureADCSConnectionConfig } from "./azure-adcs-connection-types";
|
||||
|
||||
// Type definitions for axios-ntlm
|
||||
interface AxiosNtlmConfig {
|
||||
ntlm: {
|
||||
domain: string;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
httpsAgent?: https.Agent;
|
||||
url: string;
|
||||
method?: string;
|
||||
data?: string;
|
||||
headers?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface AxiosNtlmResponse {
|
||||
status: number;
|
||||
data: string;
|
||||
headers: unknown;
|
||||
}
|
||||
|
||||
// Types for credential parsing
|
||||
interface ParsedCredentials {
|
||||
domain: string;
|
||||
username: string;
|
||||
fullUsername: string; // domain\username format
|
||||
}
|
||||
|
||||
// Helper function to parse and normalize credentials for Windows authentication
|
||||
const parseCredentials = (inputUsername: string): ParsedCredentials => {
|
||||
// Ensure inputUsername is a string
|
||||
if (typeof inputUsername !== "string" || !inputUsername.trim()) {
|
||||
throw new BadRequestError({
|
||||
message: "Username must be a non-empty string"
|
||||
});
|
||||
}
|
||||
|
||||
let domain = "";
|
||||
let username = "";
|
||||
let fullUsername = "";
|
||||
|
||||
if (inputUsername.includes("\\")) {
|
||||
// Already in domain\username format
|
||||
const parts = inputUsername.split("\\");
|
||||
if (parts.length === 2) {
|
||||
[domain, username] = parts;
|
||||
fullUsername = inputUsername;
|
||||
} else {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid domain\\username format. Expected format: DOMAIN\\username"
|
||||
});
|
||||
}
|
||||
} else if (inputUsername.includes("@")) {
|
||||
// UPN format: user@domain.com
|
||||
const [user, domainPart] = inputUsername.split("@");
|
||||
if (!user || !domainPart) {
|
||||
throw new BadRequestError({
|
||||
message: "Invalid UPN format. Expected format: user@domain.com"
|
||||
});
|
||||
}
|
||||
|
||||
username = user;
|
||||
// Extract NetBIOS name from FQDN
|
||||
domain = domainPart.split(".")[0].toUpperCase();
|
||||
fullUsername = `${domain}\\${username}`;
|
||||
} else {
|
||||
// Plain username - assume local account or current domain
|
||||
username = inputUsername;
|
||||
domain = "";
|
||||
fullUsername = inputUsername;
|
||||
}
|
||||
|
||||
return { domain, username, fullUsername };
|
||||
};
|
||||
|
||||
// Helper to normalize URL
|
||||
const normalizeAdcsUrl = (url: string): string => {
|
||||
let normalizedUrl = url.trim();
|
||||
|
||||
// Remove trailing slash
|
||||
normalizedUrl = normalizedUrl.replace(/\/$/, "");
|
||||
|
||||
// Ensure HTTPS protocol
|
||||
if (normalizedUrl.startsWith("http://")) {
|
||||
normalizedUrl = normalizedUrl.replace("http://", "https://");
|
||||
} else if (!normalizedUrl.startsWith("https://")) {
|
||||
normalizedUrl = `https://${normalizedUrl}`;
|
||||
}
|
||||
|
||||
return normalizedUrl;
|
||||
};
|
||||
|
||||
// NTLM request wrapper
|
||||
const createHttpsAgent = (sslRejectUnauthorized: boolean, sslCertificate?: string): https.Agent => {
|
||||
const agentOptions: https.AgentOptions = {
|
||||
rejectUnauthorized: sslRejectUnauthorized,
|
||||
keepAlive: true, // axios-ntlm needs keepAlive for NTLM handshake
|
||||
ca: sslCertificate ? [sslCertificate.trim()] : undefined,
|
||||
// Disable hostname verification as Microsoft servers by default use local IPs for certificates
|
||||
// which may not match the hostname used to connect
|
||||
checkServerIdentity: () => undefined
|
||||
};
|
||||
|
||||
return new https.Agent(agentOptions);
|
||||
};
|
||||
|
||||
const axiosNtlmRequest = async (config: AxiosNtlmConfig): Promise<AxiosNtlmResponse> => {
|
||||
const method = config.method || "GET";
|
||||
|
||||
const credentials = {
|
||||
username: config.ntlm.username,
|
||||
password: config.ntlm.password,
|
||||
domain: config.ntlm.domain || "",
|
||||
workstation: ""
|
||||
};
|
||||
|
||||
const axiosConfig = {
|
||||
httpsAgent: config.httpsAgent,
|
||||
timeout: 30000
|
||||
};
|
||||
|
||||
const client = NtlmClient(credentials, axiosConfig);
|
||||
|
||||
const requestOptions: { url: string; method: string; data?: string; headers?: Record<string, string> } = {
|
||||
url: config.url,
|
||||
method
|
||||
};
|
||||
|
||||
if (config.data) {
|
||||
requestOptions.data = config.data;
|
||||
}
|
||||
|
||||
if (config.headers) {
|
||||
requestOptions.headers = config.headers;
|
||||
}
|
||||
|
||||
const response = await client(requestOptions);
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
data: response.data,
|
||||
headers: response.headers
|
||||
};
|
||||
};
|
||||
|
||||
// Test ADCS connectivity and authentication using NTLM
|
||||
const testAdcsConnection = async (
|
||||
credentials: ParsedCredentials,
|
||||
password: string,
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
): Promise<boolean> => {
|
||||
// Test endpoints in order of preference
|
||||
const testEndpoints = [
|
||||
"/certsrv/certrqus.asp", // Certificate request status (most reliable)
|
||||
"/certsrv/certfnsh.asp", // Certificate finalization
|
||||
"/certsrv/default.asp", // Main ADCS page
|
||||
"/certsrv/" // Root certsrv
|
||||
];
|
||||
|
||||
for (const endpoint of testEndpoints) {
|
||||
try {
|
||||
const testUrl = `${baseUrl}${endpoint}`;
|
||||
|
||||
const shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
const response = await axiosNtlmRequest({
|
||||
url: testUrl,
|
||||
method: "GET",
|
||||
httpsAgent,
|
||||
ntlm: {
|
||||
domain: credentials.domain,
|
||||
username: credentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
|
||||
// Check if we got a successful response
|
||||
if (response.status === 200) {
|
||||
const responseText = response.data;
|
||||
|
||||
// Verify this is actually an ADCS server by checking content
|
||||
const adcsIndicators = [
|
||||
"Microsoft Active Directory Certificate Services",
|
||||
"Certificate Services",
|
||||
"Request a certificate",
|
||||
"certsrv",
|
||||
"Certificate Template",
|
||||
"Web Enrollment"
|
||||
];
|
||||
|
||||
const isAdcsServer = adcsIndicators.some((indicator) =>
|
||||
responseText.toLowerCase().includes(indicator.toLowerCase())
|
||||
);
|
||||
|
||||
if (isAdcsServer) {
|
||||
// Successfully authenticated and confirmed ADCS
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
throw new BadRequestError({
|
||||
message: "Authentication failed. Please verify your credentials are correct."
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
throw new BadRequestError({
|
||||
message: "Access denied. Your account may not have permission to access ADCS web enrollment."
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle network and connection errors
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("ENOTFOUND")) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot resolve ADCS server hostname. Please verify the URL is correct."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("ECONNREFUSED")) {
|
||||
throw new BadRequestError({
|
||||
message: "Connection refused by ADCS server. Please verify the server is running and accessible."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("ETIMEDOUT") || error.message.includes("timeout")) {
|
||||
throw new BadRequestError({
|
||||
message: "Connection timeout. Please verify the server is accessible and not blocked by firewall."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("certificate") || error.message.includes("SSL") || error.message.includes("TLS")) {
|
||||
throw new BadRequestError({
|
||||
message: `SSL/TLS certificate error: ${error.message}. This may indicate a certificate verification failure.`
|
||||
});
|
||||
}
|
||||
if (error.message.includes("DEPTH_ZERO_SELF_SIGNED_CERT")) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Self-signed certificate detected. Either provide the server's certificate or set 'sslRejectUnauthorized' to false."
|
||||
});
|
||||
}
|
||||
if (error.message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE")) {
|
||||
throw new BadRequestError({
|
||||
message: "Unable to verify certificate signature. Please provide the correct CA certificate."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Continue to next endpoint for other errors
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we get here, no endpoint worked
|
||||
throw new BadRequestError({
|
||||
message: "Could not connect to ADCS server. Please verify the server URL and that Web Enrollment is enabled."
|
||||
});
|
||||
};
|
||||
|
||||
// Create authenticated NTLM client for ADCS operations
|
||||
const createNtlmClient = (
|
||||
username: string,
|
||||
password: string,
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
) => {
|
||||
const parsedCredentials = parseCredentials(username);
|
||||
const normalizedUrl = normalizeAdcsUrl(baseUrl);
|
||||
|
||||
return {
|
||||
get: async (endpoint: string, additionalHeaders: Record<string, string> = {}) => {
|
||||
const shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
return axiosNtlmRequest({
|
||||
url: `${normalizedUrl}${endpoint}`,
|
||||
method: "GET",
|
||||
httpsAgent,
|
||||
headers: additionalHeaders,
|
||||
ntlm: {
|
||||
domain: parsedCredentials.domain,
|
||||
username: parsedCredentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
},
|
||||
post: async (endpoint: string, body: string, additionalHeaders: Record<string, string> = {}) => {
|
||||
const shouldRejectUnauthorized = sslRejectUnauthorized;
|
||||
|
||||
const httpsAgent = createHttpsAgent(shouldRejectUnauthorized, sslCertificate);
|
||||
|
||||
return axiosNtlmRequest({
|
||||
url: `${normalizedUrl}${endpoint}`,
|
||||
method: "POST",
|
||||
httpsAgent,
|
||||
data: body,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
...additionalHeaders
|
||||
},
|
||||
ntlm: {
|
||||
domain: parsedCredentials.domain,
|
||||
username: parsedCredentials.username,
|
||||
password
|
||||
}
|
||||
});
|
||||
},
|
||||
baseUrl: normalizedUrl,
|
||||
credentials: parsedCredentials
|
||||
};
|
||||
};
|
||||
|
||||
export const getAzureADCSConnectionCredentials = async (
|
||||
connectionId: string,
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById">,
|
||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
|
||||
) => {
|
||||
const appConnection = await appConnectionDAL.findById(connectionId);
|
||||
|
||||
if (!appConnection) {
|
||||
throw new NotFoundError({ message: `Connection with ID '${connectionId}' not found` });
|
||||
}
|
||||
|
||||
if (appConnection.app !== AppConnection.AzureADCS) {
|
||||
throw new BadRequestError({ message: `Connection with ID '${connectionId}' is not an Azure ADCS connection` });
|
||||
}
|
||||
|
||||
switch (appConnection.method) {
|
||||
case AzureADCSConnectionMethod.UsernamePassword:
|
||||
const credentials = (await decryptAppConnectionCredentials({
|
||||
orgId: appConnection.orgId,
|
||||
kmsService,
|
||||
encryptedCredentials: appConnection.encryptedCredentials
|
||||
})) as {
|
||||
username: string;
|
||||
password: string;
|
||||
adcsUrl: string;
|
||||
sslRejectUnauthorized?: boolean;
|
||||
sslCertificate?: string;
|
||||
};
|
||||
|
||||
return {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
adcsUrl: credentials.adcsUrl,
|
||||
sslRejectUnauthorized: credentials.sslRejectUnauthorized ?? true,
|
||||
sslCertificate: credentials.sslCertificate
|
||||
};
|
||||
|
||||
default:
|
||||
throw new BadRequestError({
|
||||
message: `Unsupported Azure ADCS connection method: ${appConnection.method}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const validateAzureADCSConnectionCredentials = async (appConnection: TAzureADCSConnectionConfig) => {
|
||||
const { credentials } = appConnection;
|
||||
|
||||
try {
|
||||
// Parse and validate credentials
|
||||
const parsedCredentials = parseCredentials(credentials.username);
|
||||
const normalizedUrl = normalizeAdcsUrl(credentials.adcsUrl);
|
||||
|
||||
// Validate URL to prevent DNS manipulation attacks and SSRF
|
||||
await blockLocalAndPrivateIpAddresses(normalizedUrl);
|
||||
|
||||
// Test the connection using NTLM
|
||||
await testAdcsConnection(
|
||||
parsedCredentials,
|
||||
credentials.password,
|
||||
normalizedUrl,
|
||||
credentials.sslRejectUnauthorized ?? true,
|
||||
credentials.sslCertificate
|
||||
);
|
||||
|
||||
// If we get here, authentication was successful
|
||||
return {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
adcsUrl: credentials.adcsUrl,
|
||||
sslRejectUnauthorized: credentials.sslRejectUnauthorized ?? true,
|
||||
sslCertificate: credentials.sslCertificate
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Handle unexpected errors
|
||||
let errorMessage = "Unable to validate ADCS connection.";
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes("401") || error.message.includes("Unauthorized")) {
|
||||
errorMessage = "NTLM authentication failed. Please verify your username, password, and domain are correct.";
|
||||
} else if (error.message.includes("ENOTFOUND") || error.message.includes("ECONNREFUSED")) {
|
||||
errorMessage = "Cannot connect to the ADCS server. Please verify the server URL is correct and accessible.";
|
||||
} else if (error.message.includes("timeout")) {
|
||||
errorMessage = "Connection to ADCS server timed out. Please verify the server is accessible.";
|
||||
} else if (
|
||||
error.message.includes("certificate") ||
|
||||
error.message.includes("SSL") ||
|
||||
error.message.includes("TLS") ||
|
||||
error.message.includes("DEPTH_ZERO_SELF_SIGNED_CERT") ||
|
||||
error.message.includes("UNABLE_TO_VERIFY_LEAF_SIGNATURE")
|
||||
) {
|
||||
errorMessage = `SSL/TLS certificate error: ${error.message}. The server certificate may be self-signed or the CA certificate may be incorrect.`;
|
||||
}
|
||||
}
|
||||
|
||||
throw new BadRequestError({
|
||||
message: `Failed to validate Azure ADCS connection: ${errorMessage} Details: ${
|
||||
error instanceof Error ? error.message : "Unknown error"
|
||||
}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getAzureADCSConnectionListItem = () => ({
|
||||
name: "Azure ADCS" as const,
|
||||
app: AppConnection.AzureADCS as const,
|
||||
methods: [AzureADCSConnectionMethod.UsernamePassword] as [AzureADCSConnectionMethod.UsernamePassword]
|
||||
});
|
||||
|
||||
// Export helper functions for use in certificate ordering
|
||||
export const createAdcsHttpClient = (
|
||||
username: string,
|
||||
password: string,
|
||||
baseUrl: string,
|
||||
sslRejectUnauthorized: boolean = true,
|
||||
sslCertificate?: string
|
||||
) => {
|
||||
return createNtlmClient(username, password, baseUrl, sslRejectUnauthorized, sslCertificate);
|
||||
};
|
@@ -0,0 +1,88 @@
|
||||
import z from "zod";
|
||||
|
||||
import { AppConnections } from "@app/lib/api-docs";
|
||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||
import {
|
||||
BaseAppConnectionSchema,
|
||||
GenericCreateAppConnectionFieldsSchema,
|
||||
GenericUpdateAppConnectionFieldsSchema
|
||||
} from "@app/services/app-connection/app-connection-schemas";
|
||||
|
||||
import { AzureADCSConnectionMethod } from "./azure-adcs-connection-enums";
|
||||
|
||||
export const AzureADCSUsernamePasswordCredentialsSchema = z.object({
|
||||
adcsUrl: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "ADCS URL required")
|
||||
.max(255)
|
||||
.refine((value) => value.startsWith("https://"), "ADCS URL must use HTTPS")
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.adcsUrl),
|
||||
username: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Username required")
|
||||
.max(255)
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.username),
|
||||
password: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Password required")
|
||||
.max(255)
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.password),
|
||||
sslRejectUnauthorized: z.boolean().optional().describe(AppConnections.CREDENTIALS.AZURE_ADCS.sslRejectUnauthorized),
|
||||
sslCertificate: z
|
||||
.string()
|
||||
.trim()
|
||||
.transform((value) => value || undefined)
|
||||
.optional()
|
||||
.describe(AppConnections.CREDENTIALS.AZURE_ADCS.sslCertificate)
|
||||
});
|
||||
|
||||
const BaseAzureADCSConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.AzureADCS) });
|
||||
|
||||
export const AzureADCSConnectionSchema = BaseAzureADCSConnectionSchema.extend({
|
||||
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema
|
||||
});
|
||||
|
||||
export const SanitizedAzureADCSConnectionSchema = z.discriminatedUnion("method", [
|
||||
BaseAzureADCSConnectionSchema.extend({
|
||||
method: z.literal(AzureADCSConnectionMethod.UsernamePassword),
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.pick({
|
||||
username: true,
|
||||
adcsUrl: true,
|
||||
sslRejectUnauthorized: true,
|
||||
sslCertificate: true
|
||||
})
|
||||
})
|
||||
]);
|
||||
|
||||
export const ValidateAzureADCSConnectionCredentialsSchema = z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z
|
||||
.literal(AzureADCSConnectionMethod.UsernamePassword)
|
||||
.describe(AppConnections.CREATE(AppConnection.AzureADCS).method),
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.describe(
|
||||
AppConnections.CREATE(AppConnection.AzureADCS).credentials
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
export const CreateAzureADCSConnectionSchema = ValidateAzureADCSConnectionCredentialsSchema.and(
|
||||
GenericCreateAppConnectionFieldsSchema(AppConnection.AzureADCS)
|
||||
);
|
||||
|
||||
export const UpdateAzureADCSConnectionSchema = z
|
||||
.object({
|
||||
credentials: AzureADCSUsernamePasswordCredentialsSchema.optional().describe(
|
||||
AppConnections.UPDATE(AppConnection.AzureADCS).credentials
|
||||
)
|
||||
})
|
||||
.and(GenericUpdateAppConnectionFieldsSchema(AppConnection.AzureADCS));
|
||||
|
||||
export const AzureADCSConnectionListItemSchema = z.object({
|
||||
name: z.literal("Azure ADCS"),
|
||||
app: z.literal(AppConnection.AzureADCS),
|
||||
methods: z.nativeEnum(AzureADCSConnectionMethod).array()
|
||||
});
|
@@ -0,0 +1,23 @@
|
||||
import z from "zod";
|
||||
|
||||
import { DiscriminativePick } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import {
|
||||
AzureADCSConnectionSchema,
|
||||
CreateAzureADCSConnectionSchema,
|
||||
ValidateAzureADCSConnectionCredentialsSchema
|
||||
} from "./azure-adcs-connection-schemas";
|
||||
|
||||
export type TAzureADCSConnection = z.infer<typeof AzureADCSConnectionSchema>;
|
||||
|
||||
export type TAzureADCSConnectionInput = z.infer<typeof CreateAzureADCSConnectionSchema> & {
|
||||
app: AppConnection.AzureADCS;
|
||||
};
|
||||
|
||||
export type TValidateAzureADCSConnectionCredentialsSchema = typeof ValidateAzureADCSConnectionCredentialsSchema;
|
||||
|
||||
export type TAzureADCSConnectionConfig = DiscriminativePick<
|
||||
TAzureADCSConnectionInput,
|
||||
"method" | "app" | "credentials"
|
||||
>;
|
4
backend/src/services/app-connection/azure-adcs/index.ts
Normal file
4
backend/src/services/app-connection/azure-adcs/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./azure-adcs-connection-enums";
|
||||
export * from "./azure-adcs-connection-fns";
|
||||
export * from "./azure-adcs-connection-schemas";
|
||||
export * from "./azure-adcs-connection-types";
|
@@ -4,6 +4,7 @@ import { AxiosInstance, AxiosRequestConfig, AxiosResponse, HttpStatusCode, isAxi
|
||||
|
||||
import { createRequestClient } from "@app/lib/config/request";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { SecretSyncError } from "@app/services/secret-sync/secret-sync-errors";
|
||||
|
||||
import { ChecklyConnectionMethod } from "./checkly-connection-constants";
|
||||
import { TChecklyAccount, TChecklyConnectionConfig, TChecklyVariable } from "./checkly-connection-types";
|
||||
@@ -181,6 +182,122 @@ class ChecklyPublicClient {
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
async getCheckGroups(connection: TChecklyConnectionConfig, accountId: string, limit = 50, page = 1) {
|
||||
const res = await this.send<{ id: number; name: string }[]>(connection, {
|
||||
accountId,
|
||||
method: "GET",
|
||||
url: `/v1/check-groups`,
|
||||
params: { limit, page }
|
||||
});
|
||||
|
||||
return res?.map((group) => ({
|
||||
id: group.id.toString(),
|
||||
name: group.name
|
||||
}));
|
||||
}
|
||||
|
||||
async getCheckGroup(connection: TChecklyConnectionConfig, accountId: string, groupId: string) {
|
||||
try {
|
||||
type ChecklyGroupResponse = {
|
||||
id: number;
|
||||
name: string;
|
||||
environmentVariables: Array<{
|
||||
key: string;
|
||||
value: string;
|
||||
locked: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
const res = await this.send<ChecklyGroupResponse>(connection, {
|
||||
accountId,
|
||||
method: "GET",
|
||||
url: `/v1/check-groups/${groupId}`
|
||||
});
|
||||
|
||||
if (!res) return null;
|
||||
|
||||
return {
|
||||
id: res.id.toString(),
|
||||
name: res.name,
|
||||
environmentVariables: res.environmentVariables
|
||||
};
|
||||
} catch (error) {
|
||||
if (isAxiosError(error) && error.response?.status === HttpStatusCode.NotFound) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async updateCheckGroupEnvironmentVariables(
|
||||
connection: TChecklyConnectionConfig,
|
||||
accountId: string,
|
||||
groupId: string,
|
||||
environmentVariables: Array<{ key: string; value: string; locked?: boolean }>
|
||||
) {
|
||||
if (environmentVariables.length > 50) {
|
||||
throw new SecretSyncError({
|
||||
message: "Checkly does not support syncing more than 50 variables to Check Group",
|
||||
shouldRetry: false
|
||||
});
|
||||
}
|
||||
|
||||
const apiVariables = environmentVariables.map((v) => ({
|
||||
key: v.key,
|
||||
value: v.value,
|
||||
locked: v.locked ?? false,
|
||||
secret: true
|
||||
}));
|
||||
|
||||
const group = await this.getCheckGroup(connection, accountId, groupId);
|
||||
|
||||
await this.send(connection, {
|
||||
accountId,
|
||||
method: "PUT",
|
||||
url: `/v2/check-groups/${groupId}`,
|
||||
data: { name: group?.name, environmentVariables: apiVariables }
|
||||
});
|
||||
|
||||
return this.getCheckGroup(connection, accountId, groupId);
|
||||
}
|
||||
|
||||
async getCheckGroupEnvironmentVariables(connection: TChecklyConnectionConfig, accountId: string, groupId: string) {
|
||||
const group = await this.getCheckGroup(connection, accountId, groupId);
|
||||
return group?.environmentVariables || [];
|
||||
}
|
||||
|
||||
async upsertCheckGroupEnvironmentVariables(
|
||||
connection: TChecklyConnectionConfig,
|
||||
accountId: string,
|
||||
groupId: string,
|
||||
variables: Array<{ key: string; value: string; locked?: boolean }>
|
||||
) {
|
||||
const existingVars = await this.getCheckGroupEnvironmentVariables(connection, accountId, groupId);
|
||||
const varMap = new Map(existingVars.map((v) => [v.key, v]));
|
||||
|
||||
for (const newVar of variables) {
|
||||
varMap.set(newVar.key, {
|
||||
key: newVar.key,
|
||||
value: newVar.value,
|
||||
locked: newVar.locked ?? false
|
||||
});
|
||||
}
|
||||
|
||||
return this.updateCheckGroupEnvironmentVariables(connection, accountId, groupId, Array.from(varMap.values()));
|
||||
}
|
||||
|
||||
async deleteCheckGroupEnvironmentVariable(
|
||||
connection: TChecklyConnectionConfig,
|
||||
accountId: string,
|
||||
groupId: string,
|
||||
variableKey: string
|
||||
) {
|
||||
const existingVars = await this.getCheckGroupEnvironmentVariables(connection, accountId, groupId);
|
||||
const filteredVars = existingVars.filter((v) => v.key !== variableKey);
|
||||
|
||||
return this.updateCheckGroupEnvironmentVariables(connection, accountId, groupId, filteredVars);
|
||||
}
|
||||
}
|
||||
|
||||
export const ChecklyPublicAPI = new ChecklyPublicClient();
|
||||
|
@@ -24,7 +24,19 @@ export const checklyConnectionService = (getAppConnection: TGetAppConnectionFunc
|
||||
}
|
||||
};
|
||||
|
||||
const listGroups = async (connectionId: string, accountId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Checkly, connectionId, actor);
|
||||
try {
|
||||
const groups = await ChecklyPublicAPI.getCheckGroups(appConnection, accountId);
|
||||
return groups!;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list accounts on Checkly");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listAccounts
|
||||
listAccounts,
|
||||
listGroups
|
||||
};
|
||||
};
|
||||
|
@@ -33,3 +33,15 @@ export type TChecklyAccount = {
|
||||
name: string;
|
||||
runtimeId: string;
|
||||
};
|
||||
|
||||
export type TChecklyGroupEnvironmentVariable = {
|
||||
key: string;
|
||||
value: string;
|
||||
locked: boolean;
|
||||
};
|
||||
|
||||
export type TChecklyGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
environmentVariables?: TChecklyGroupEnvironmentVariable[];
|
||||
};
|
||||
|
@@ -1,5 +1,3 @@
|
||||
import { createAppAuth } from "@octokit/auth-app";
|
||||
import { request } from "@octokit/request";
|
||||
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||
import https from "https";
|
||||
import RE2 from "re2";
|
||||
@@ -8,6 +6,7 @@ import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic
|
||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request as httpRequest } from "@app/lib/config/request";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, ForbiddenRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||
import { logger } from "@app/lib/logger";
|
||||
@@ -114,10 +113,13 @@ export const requestWithGitHubGateway = async <T>(
|
||||
);
|
||||
};
|
||||
|
||||
export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) => {
|
||||
export const getGitHubAppAuthToken = async (
|
||||
appConnection: TGitHubConnection,
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||
) => {
|
||||
const appCfg = getConfig();
|
||||
const appId = appCfg.INF_APP_CONNECTION_GITHUB_APP_ID;
|
||||
const appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
||||
let appPrivateKey = appCfg.INF_APP_CONNECTION_GITHUB_APP_PRIVATE_KEY;
|
||||
|
||||
if (!appId || !appPrivateKey) {
|
||||
throw new InternalServerError({
|
||||
@@ -125,21 +127,42 @@ export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) =>
|
||||
});
|
||||
}
|
||||
|
||||
appPrivateKey = appPrivateKey
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.join("\n");
|
||||
|
||||
if (appConnection.method !== GitHubConnectionMethod.App) {
|
||||
throw new InternalServerError({ message: "Cannot generate GitHub App token for non-app connection" });
|
||||
}
|
||||
|
||||
const appAuth = createAppAuth({
|
||||
appId,
|
||||
privateKey: appPrivateKey,
|
||||
installationId: appConnection.credentials.installationId,
|
||||
request: request.defaults({
|
||||
baseUrl: `https://${await getGitHubInstanceApiUrl(appConnection)}`
|
||||
})
|
||||
});
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const payload = {
|
||||
iat: now,
|
||||
exp: now + 5 * 60,
|
||||
iss: appId
|
||||
};
|
||||
|
||||
const { token } = await appAuth({ type: "installation" });
|
||||
return token;
|
||||
const appJwt = crypto.jwt().sign(payload, appPrivateKey, { algorithm: "RS256" });
|
||||
|
||||
const apiBaseUrl = await getGitHubInstanceApiUrl(appConnection);
|
||||
const { installationId } = appConnection.credentials;
|
||||
|
||||
const response = await requestWithGitHubGateway<{ token: string; expires_at: string }>(
|
||||
appConnection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `https://${apiBaseUrl}/app/installations/${installationId}/access_tokens`,
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${appJwt}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return response.data.token;
|
||||
};
|
||||
|
||||
const parseGitHubLinkHeader = (linkHeader: string | undefined): Record<string, string> => {
|
||||
@@ -174,7 +197,9 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
|
||||
const { credentials, method } = appConnection;
|
||||
|
||||
const token =
|
||||
method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection);
|
||||
method === GitHubConnectionMethod.OAuth
|
||||
? credentials.accessToken
|
||||
: await getGitHubAppAuthToken(appConnection, gatewayService);
|
||||
|
||||
const baseUrl = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
|
||||
const initialUrlObj = new URL(baseUrl);
|
||||
|
@@ -8,9 +8,11 @@ import { IntegrationUrls } from "@app/services/integration-auth/integration-list
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { RenderConnectionMethod } from "./render-connection-enums";
|
||||
import {
|
||||
TRawRenderEnvironmentGroup,
|
||||
TRawRenderService,
|
||||
TRenderConnection,
|
||||
TRenderConnectionConfig,
|
||||
TRenderEnvironmentGroup,
|
||||
TRenderService
|
||||
} from "./render-connection-types";
|
||||
|
||||
@@ -32,7 +34,11 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
||||
const perPage = 100;
|
||||
let cursor;
|
||||
|
||||
let maxIterations = 10;
|
||||
|
||||
while (hasMorePages) {
|
||||
if (maxIterations <= 0) break;
|
||||
|
||||
const res: TRawRenderService[] = (
|
||||
await request.get<TRawRenderService[]>(`${IntegrationUrls.RENDER_API_URL}/v1/services`, {
|
||||
params: new URLSearchParams({
|
||||
@@ -59,6 +65,8 @@ export const listRenderServices = async (appConnection: TRenderConnection): Prom
|
||||
} else {
|
||||
cursor = res[res.length - 1].cursor;
|
||||
}
|
||||
|
||||
maxIterations -= 1;
|
||||
}
|
||||
|
||||
return services;
|
||||
@@ -86,3 +94,52 @@ export const validateRenderConnectionCredentials = async (config: TRenderConnect
|
||||
|
||||
return inputCredentials;
|
||||
};
|
||||
|
||||
export const listRenderEnvironmentGroups = async (
|
||||
appConnection: TRenderConnection
|
||||
): Promise<TRenderEnvironmentGroup[]> => {
|
||||
const {
|
||||
credentials: { apiKey }
|
||||
} = appConnection;
|
||||
|
||||
const groups: TRenderEnvironmentGroup[] = [];
|
||||
let hasMorePages = true;
|
||||
const perPage = 100;
|
||||
let cursor;
|
||||
let maxIterations = 10;
|
||||
|
||||
while (hasMorePages) {
|
||||
if (maxIterations <= 0) break;
|
||||
|
||||
const res: TRawRenderEnvironmentGroup[] = (
|
||||
await request.get<TRawRenderEnvironmentGroup[]>(`${IntegrationUrls.RENDER_API_URL}/v1/env-groups`, {
|
||||
params: new URLSearchParams({
|
||||
...(cursor ? { cursor: String(cursor) } : {}),
|
||||
limit: String(perPage)
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json",
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
})
|
||||
).data;
|
||||
|
||||
res.forEach((item) => {
|
||||
groups.push({
|
||||
name: item.envGroup.name,
|
||||
id: item.envGroup.id
|
||||
});
|
||||
});
|
||||
|
||||
if (res.length < perPage) {
|
||||
hasMorePages = false;
|
||||
} else {
|
||||
cursor = res[res.length - 1].cursor;
|
||||
}
|
||||
|
||||
maxIterations -= 1;
|
||||
}
|
||||
|
||||
return groups;
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@ import { logger } from "@app/lib/logger";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
|
||||
import { AppConnection } from "../app-connection-enums";
|
||||
import { listRenderServices } from "./render-connection-fns";
|
||||
import { listRenderEnvironmentGroups, listRenderServices } from "./render-connection-fns";
|
||||
import { TRenderConnection } from "./render-connection-types";
|
||||
|
||||
type TGetAppConnectionFunc = (
|
||||
@@ -24,7 +24,20 @@ export const renderConnectionService = (getAppConnection: TGetAppConnectionFunc)
|
||||
}
|
||||
};
|
||||
|
||||
const listEnvironmentGroups = async (connectionId: string, actor: OrgServiceActor) => {
|
||||
const appConnection = await getAppConnection(AppConnection.Render, connectionId, actor);
|
||||
try {
|
||||
const groups = await listRenderEnvironmentGroups(appConnection);
|
||||
|
||||
return groups;
|
||||
} catch (error) {
|
||||
logger.error(error, "Failed to list environment groups for Render connection");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listServices
|
||||
listServices,
|
||||
listEnvironmentGroups
|
||||
};
|
||||
};
|
||||
|
@@ -33,3 +33,16 @@ export type TRawRenderService = {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type TRenderEnvironmentGroup = {
|
||||
name: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TRawRenderEnvironmentGroup = {
|
||||
cursor: string;
|
||||
envGroup: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
@@ -75,7 +75,7 @@ export const getTokenConfig = (tokenType: TokenType) => {
|
||||
};
|
||||
|
||||
export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAuthTokenServiceFactoryDep) => {
|
||||
const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
|
||||
const createTokenForUser = async ({ type, userId, orgId, aliasId }: TCreateTokenForUserDTO) => {
|
||||
const { token, ...tkCfg } = getTokenConfig(type);
|
||||
const appCfg = getConfig();
|
||||
const tokenHash = await crypto.hashing().createHash(token, appCfg.SALT_ROUNDS);
|
||||
@@ -88,7 +88,8 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, orgMembershipDAL }: TAu
|
||||
type,
|
||||
userId,
|
||||
orgId,
|
||||
triesLeft: tkCfg?.triesLeft
|
||||
triesLeft: tkCfg?.triesLeft,
|
||||
aliasId
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
@@ -14,6 +14,7 @@ export type TCreateTokenForUserDTO = {
|
||||
type: TokenType;
|
||||
userId: string;
|
||||
orgId?: string;
|
||||
aliasId?: string;
|
||||
};
|
||||
|
||||
export type TCreateOrgInviteTokenDTO = {
|
||||
|
@@ -448,15 +448,41 @@ export const authLoginServiceFactory = ({
|
||||
|
||||
// Check if the user actually has access to the specified organization.
|
||||
const userOrgs = await orgDAL.findAllOrgsByUserId(user.id);
|
||||
const hasOrganizationMembership = userOrgs.some((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
|
||||
const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited");
|
||||
|
||||
const selectedOrg = await orgDAL.findById(organizationId);
|
||||
|
||||
if (!hasOrganizationMembership) {
|
||||
// Check if authEnforced is true, if that's the case, throw an error
|
||||
if (selectedOrg.authEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "Authentication is required by your organization before you can log in."
|
||||
});
|
||||
}
|
||||
|
||||
if (!selectedOrgMembership) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: `User does not have access to the organization named ${selectedOrg?.name}`
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) {
|
||||
const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin;
|
||||
|
||||
if (!canBypass) {
|
||||
throw new ForbiddenRequestError({
|
||||
message: "Google SSO is enforced for this organization. Please use Google SSO to login.",
|
||||
error: "GoogleSsoEnforced"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (decodedToken.authMethod === AuthMethod.GOOGLE) {
|
||||
await orgDAL.updateById(selectedOrg.id, {
|
||||
googleSsoAuthLastUsed: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled;
|
||||
const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||
const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined;
|
||||
@@ -502,7 +528,8 @@ export const authLoginServiceFactory = ({
|
||||
selectedOrg.authEnforced &&
|
||||
selectedOrg.bypassOrgAuthEnabled &&
|
||||
!isAuthMethodSaml(decodedToken.authMethod) &&
|
||||
decodedToken.authMethod !== AuthMethod.OIDC
|
||||
decodedToken.authMethod !== AuthMethod.OIDC &&
|
||||
decodedToken.authMethod !== AuthMethod.GOOGLE
|
||||
) {
|
||||
await auditLogService.createAuditLog({
|
||||
orgId: organizationId,
|
||||
@@ -705,7 +732,7 @@ export const authLoginServiceFactory = ({
|
||||
/*
|
||||
* OAuth2 login for google,github, and other oauth2 provider
|
||||
* */
|
||||
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort }: TOauthLoginDTO) => {
|
||||
const oauth2Login = async ({ email, firstName, lastName, authMethod, callbackPort, orgSlug }: TOauthLoginDTO) => {
|
||||
// akhilmhdh: case sensitive email resolution
|
||||
const usersByUsername = await userDAL.findUserByUsername(email);
|
||||
let user = usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
|
||||
@@ -759,6 +786,8 @@ export const authLoginServiceFactory = ({
|
||||
|
||||
const appCfg = getConfig();
|
||||
|
||||
let orgId = "";
|
||||
let orgName: undefined | string;
|
||||
if (!user) {
|
||||
// Create a new user based on oAuth
|
||||
if (!serverCfg?.allowSignUp) throw new BadRequestError({ message: "Sign up disabled", name: "Oauth 2 login" });
|
||||
@@ -784,7 +813,6 @@ export const authLoginServiceFactory = ({
|
||||
});
|
||||
|
||||
if (authMethod === AuthMethod.GITHUB && serverCfg.defaultAuthOrgId && !appCfg.isCloud) {
|
||||
let orgId = "";
|
||||
const defaultOrg = await orgDAL.findOrgById(serverCfg.defaultAuthOrgId);
|
||||
if (!defaultOrg) {
|
||||
throw new BadRequestError({
|
||||
@@ -824,11 +852,39 @@ export const authLoginServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!orgId && orgSlug) {
|
||||
const org = await orgDAL.findOrgBySlug(orgSlug);
|
||||
|
||||
if (org) {
|
||||
// checks for the membership and only sets the orgId / orgName if the user is a member of the specified org
|
||||
const orgMembership = await orgDAL.findMembership({
|
||||
[`${TableName.OrgMembership}.userId` as "userId"]: user.id,
|
||||
[`${TableName.OrgMembership}.orgId` as "orgId"]: org.id,
|
||||
[`${TableName.OrgMembership}.isActive` as "isActive"]: true,
|
||||
[`${TableName.OrgMembership}.status` as "status"]: OrgMembershipStatus.Accepted
|
||||
});
|
||||
|
||||
if (orgMembership) {
|
||||
orgId = org.id;
|
||||
orgName = org.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const isUserCompleted = user.isAccepted;
|
||||
const providerAuthToken = crypto.jwt().sign(
|
||||
{
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||
userId: user.id,
|
||||
|
||||
...(orgId && orgSlug && orgName !== undefined
|
||||
? {
|
||||
organizationId: orgId,
|
||||
organizationName: orgName,
|
||||
organizationSlug: orgSlug
|
||||
}
|
||||
: {}),
|
||||
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
isEmailVerified: user.isEmailVerified,
|
||||
|
@@ -32,6 +32,7 @@ export type TOauthLoginDTO = {
|
||||
lastName?: string;
|
||||
authMethod: AuthMethod;
|
||||
callbackPort?: string;
|
||||
orgSlug?: string;
|
||||
};
|
||||
|
||||
export type TOauthTokenExchangeDTO = {
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CaType } from "../certificate-authority-enums";
|
||||
import {
|
||||
BaseCertificateAuthoritySchema,
|
||||
GenericCreateCertificateAuthorityFieldsSchema,
|
||||
GenericUpdateCertificateAuthorityFieldsSchema
|
||||
} from "../certificate-authority-schemas";
|
||||
|
||||
export const AzureAdCsCertificateAuthorityConfigurationSchema = z.object({
|
||||
azureAdcsConnectionId: z.string().uuid().trim().describe("Azure ADCS Connection ID")
|
||||
});
|
||||
|
||||
export const AzureAdCsCertificateAuthoritySchema = BaseCertificateAuthoritySchema.extend({
|
||||
type: z.literal(CaType.AZURE_AD_CS),
|
||||
configuration: AzureAdCsCertificateAuthorityConfigurationSchema
|
||||
});
|
||||
|
||||
export const CreateAzureAdCsCertificateAuthoritySchema = GenericCreateCertificateAuthorityFieldsSchema(
|
||||
CaType.AZURE_AD_CS
|
||||
).extend({
|
||||
configuration: AzureAdCsCertificateAuthorityConfigurationSchema
|
||||
});
|
||||
|
||||
export const UpdateAzureAdCsCertificateAuthoritySchema = GenericUpdateCertificateAuthorityFieldsSchema(
|
||||
CaType.AZURE_AD_CS
|
||||
).extend({
|
||||
configuration: AzureAdCsCertificateAuthorityConfigurationSchema.optional()
|
||||
});
|
@@ -0,0 +1,13 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
AzureAdCsCertificateAuthoritySchema,
|
||||
CreateAzureAdCsCertificateAuthoritySchema,
|
||||
UpdateAzureAdCsCertificateAuthoritySchema
|
||||
} from "./azure-ad-cs-certificate-authority-schemas";
|
||||
|
||||
export type TAzureAdCsCertificateAuthority = z.infer<typeof AzureAdCsCertificateAuthoritySchema>;
|
||||
|
||||
export type TCreateAzureAdCsCertificateAuthorityDTO = z.infer<typeof CreateAzureAdCsCertificateAuthoritySchema>;
|
||||
|
||||
export type TUpdateAzureAdCsCertificateAuthorityDTO = z.infer<typeof UpdateAzureAdCsCertificateAuthoritySchema>;
|
@@ -1,6 +1,7 @@
|
||||
export enum CaType {
|
||||
INTERNAL = "internal",
|
||||
ACME = "acme"
|
||||
ACME = "acme",
|
||||
AZURE_AD_CS = "azure-ad-cs"
|
||||
}
|
||||
|
||||
export enum InternalCaType {
|
||||
@@ -17,3 +18,9 @@ export enum CaStatus {
|
||||
export enum CaRenewalType {
|
||||
EXISTING = "existing"
|
||||
}
|
||||
|
||||
export enum CaCapability {
|
||||
ISSUE_CERTIFICATES = "issue-certificates",
|
||||
REVOKE_CERTIFICATES = "revoke-certificates",
|
||||
RENEW_CERTIFICATES = "renew-certificates"
|
||||
}
|
||||
|
@@ -1,6 +1,29 @@
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import { CaCapability, CaType } from "./certificate-authority-enums";
|
||||
|
||||
export const CERTIFICATE_AUTHORITIES_TYPE_MAP: Record<CaType, string> = {
|
||||
[CaType.INTERNAL]: "Internal",
|
||||
[CaType.ACME]: "ACME"
|
||||
[CaType.ACME]: "ACME",
|
||||
[CaType.AZURE_AD_CS]: "Azure AD Certificate Service"
|
||||
};
|
||||
|
||||
export const CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP: Record<CaType, CaCapability[]> = {
|
||||
[CaType.INTERNAL]: [
|
||||
CaCapability.ISSUE_CERTIFICATES,
|
||||
CaCapability.REVOKE_CERTIFICATES,
|
||||
CaCapability.RENEW_CERTIFICATES
|
||||
],
|
||||
[CaType.ACME]: [CaCapability.ISSUE_CERTIFICATES, CaCapability.REVOKE_CERTIFICATES, CaCapability.RENEW_CERTIFICATES],
|
||||
[CaType.AZURE_AD_CS]: [
|
||||
CaCapability.ISSUE_CERTIFICATES,
|
||||
CaCapability.RENEW_CERTIFICATES
|
||||
// Note: REVOKE_CERTIFICATES intentionally omitted - not supported by ADCS connector
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a certificate authority type supports a specific capability
|
||||
*/
|
||||
export const caSupportsCapability = (caType: CaType, capability: CaCapability): boolean => {
|
||||
const capabilities = CERTIFICATE_AUTHORITIES_CAPABILITIES_MAP[caType] || [];
|
||||
return capabilities.includes(capability);
|
||||
};
|
||||
|
@@ -21,6 +21,7 @@ import { TCertificateSecretDALFactory } from "../certificate/certificate-secret-
|
||||
import { TPkiSubscriberDALFactory } from "../pki-subscriber/pki-subscriber-dal";
|
||||
import { SubscriberOperationStatus } from "../pki-subscriber/pki-subscriber-types";
|
||||
import { AcmeCertificateAuthorityFns } from "./acme/acme-certificate-authority-fns";
|
||||
import { AzureAdCsCertificateAuthorityFns } from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import { keyAlgorithmToAlgCfg } from "./certificate-authority-fns";
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
|
||||
type TCertificateAuthorityQueueFactoryDep = {
|
||||
certificateAuthorityDAL: TCertificateAuthorityDALFactory;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
externalCertificateAuthorityDAL: Pick<TExternalCertificateAuthorityDALFactory, "create" | "update">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem">;
|
||||
@@ -82,6 +83,19 @@ export const certificateAuthorityQueueFactory = ({
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
// TODO 1: auto-periodic rotation
|
||||
// TODO 2: manual rotation
|
||||
|
||||
@@ -158,6 +172,13 @@ export const certificateAuthorityQueueFactory = ({
|
||||
lastOperationMessage: "Certificate ordered successfully",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
} else if (caType === CaType.AZURE_AD_CS) {
|
||||
await azureAdCsFns.orderSubscriberCertificate(subscriberId);
|
||||
await pkiSubscriberDAL.updateById(subscriberId, {
|
||||
lastOperationStatus: SubscriberOperationStatus.SUCCESS,
|
||||
lastOperationMessage: "Certificate ordered successfully",
|
||||
lastOperationAt: new Date()
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
|
@@ -22,6 +22,14 @@ import {
|
||||
TCreateAcmeCertificateAuthorityDTO,
|
||||
TUpdateAcmeCertificateAuthorityDTO
|
||||
} from "./acme/acme-certificate-authority-types";
|
||||
import {
|
||||
AzureAdCsCertificateAuthorityFns,
|
||||
castDbEntryToAzureAdCsCertificateAuthority
|
||||
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-fns";
|
||||
import {
|
||||
TCreateAzureAdCsCertificateAuthorityDTO,
|
||||
TUpdateAzureAdCsCertificateAuthorityDTO
|
||||
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-types";
|
||||
import { TCertificateAuthorityDALFactory } from "./certificate-authority-dal";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import {
|
||||
@@ -34,7 +42,7 @@ import { TInternalCertificateAuthorityServiceFactory } from "./internal/internal
|
||||
import { TCreateInternalCertificateAuthorityDTO } from "./internal/internal-certificate-authority-types";
|
||||
|
||||
type TCertificateAuthorityServiceFactoryDep = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update">;
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "findById" | "update" | "updateById">;
|
||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
@@ -91,6 +99,19 @@ export const certificateAuthorityServiceFactory = ({
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const azureAdCsFns = AzureAdCsCertificateAuthorityFns({
|
||||
appConnectionDAL,
|
||||
appConnectionService,
|
||||
certificateAuthorityDAL,
|
||||
externalCertificateAuthorityDAL,
|
||||
certificateDAL,
|
||||
certificateBodyDAL,
|
||||
certificateSecretDAL,
|
||||
kmsService,
|
||||
pkiSubscriberDAL,
|
||||
projectDAL
|
||||
});
|
||||
|
||||
const createCertificateAuthority = async (
|
||||
{ type, projectId, name, enableDirectIssuance, configuration, status }: TCreateCertificateAuthorityDTO,
|
||||
actor: OrgServiceActor
|
||||
@@ -146,6 +167,17 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.createCertificateAuthority({
|
||||
name,
|
||||
projectId,
|
||||
configuration: configuration as TCreateAzureAdCsCertificateAuthorityDTO["configuration"],
|
||||
enableDirectIssuance,
|
||||
status,
|
||||
actor
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -205,6 +237,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -249,6 +285,10 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return acmeFns.listCertificateAuthorities({ projectId });
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.listCertificateAuthorities({ projectId });
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -323,6 +363,17 @@ export const certificateAuthorityServiceFactory = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return azureAdCsFns.updateCertificateAuthority({
|
||||
id: certificateAuthority.id,
|
||||
configuration: configuration as TUpdateAzureAdCsCertificateAuthorityDTO["configuration"],
|
||||
enableDirectIssuance,
|
||||
actor,
|
||||
status,
|
||||
name
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
@@ -384,14 +435,54 @@ export const certificateAuthorityServiceFactory = ({
|
||||
return castDbEntryToAcmeCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
if (type === CaType.AZURE_AD_CS) {
|
||||
return castDbEntryToAzureAdCsCertificateAuthority(certificateAuthority);
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid certificate authority type" });
|
||||
};
|
||||
|
||||
const getAzureAdcsTemplates = async ({
|
||||
caId,
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: {
|
||||
caId: string;
|
||||
projectId: string;
|
||||
actor: OrgServiceActor["type"];
|
||||
actorId: string;
|
||||
actorAuthMethod: OrgServiceActor["authMethod"];
|
||||
actorOrgId?: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor,
|
||||
actorId,
|
||||
projectId,
|
||||
actorAuthMethod,
|
||||
actorOrgId,
|
||||
actionProjectType: ActionProjectType.CertificateManager
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.CertificateAuthorities
|
||||
);
|
||||
|
||||
return azureAdCsFns.getTemplates({
|
||||
caId,
|
||||
projectId
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
createCertificateAuthority,
|
||||
findCertificateAuthorityByNameAndProjectId,
|
||||
listCertificateAuthoritiesByProjectId,
|
||||
updateCertificateAuthority,
|
||||
deleteCertificateAuthority
|
||||
deleteCertificateAuthority,
|
||||
getAzureAdcsTemplates
|
||||
};
|
||||
};
|
||||
|
@@ -1,13 +1,23 @@
|
||||
import { TAcmeCertificateAuthority, TAcmeCertificateAuthorityInput } from "./acme/acme-certificate-authority-types";
|
||||
import {
|
||||
TAzureAdCsCertificateAuthority,
|
||||
TCreateAzureAdCsCertificateAuthorityDTO
|
||||
} from "./azure-ad-cs/azure-ad-cs-certificate-authority-types";
|
||||
import { CaType } from "./certificate-authority-enums";
|
||||
import {
|
||||
TInternalCertificateAuthority,
|
||||
TInternalCertificateAuthorityInput
|
||||
} from "./internal/internal-certificate-authority-types";
|
||||
|
||||
export type TCertificateAuthority = TInternalCertificateAuthority | TAcmeCertificateAuthority;
|
||||
export type TCertificateAuthority =
|
||||
| TInternalCertificateAuthority
|
||||
| TAcmeCertificateAuthority
|
||||
| TAzureAdCsCertificateAuthority;
|
||||
|
||||
export type TCertificateAuthorityInput = TInternalCertificateAuthorityInput | TAcmeCertificateAuthorityInput;
|
||||
export type TCertificateAuthorityInput =
|
||||
| TInternalCertificateAuthorityInput
|
||||
| TAcmeCertificateAuthorityInput
|
||||
| TCreateAzureAdCsCertificateAuthorityDTO;
|
||||
|
||||
export type TCreateCertificateAuthorityDTO = Omit<TCertificateAuthority, "id">;
|
||||
|
||||
|
@@ -36,12 +36,18 @@ import { validateAndMapAltNameType } from "../certificate-authority-validators";
|
||||
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
|
||||
|
||||
type TInternalCertificateAuthorityFnsDeps = {
|
||||
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
|
||||
certificateAuthorityDAL: Pick<
|
||||
TCertificateAuthorityDALFactory,
|
||||
"findByIdWithAssociatedCa" | "findById" | "create" | "transaction" | "updateById" | "findWithAssociatedCa"
|
||||
>;
|
||||
certificateAuthorityCertDAL: Pick<TCertificateAuthorityCertDALFactory, "findById">;
|
||||
certificateAuthoritySecretDAL: Pick<TCertificateAuthoritySecretDALFactory, "findOne">;
|
||||
certificateAuthorityCrlDAL: Pick<TCertificateAuthorityCrlDALFactory, "findOne">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById" | "transaction" | "findOne" | "updateById">;
|
||||
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "encryptWithKmsKey" | "generateKmsKey">;
|
||||
kmsService: Pick<
|
||||
TKmsServiceFactory,
|
||||
"decryptWithKmsKey" | "encryptWithKmsKey" | "generateKmsKey" | "createCipherPairWithDataKey"
|
||||
>;
|
||||
certificateDAL: Pick<TCertificateDALFactory, "create" | "transaction">;
|
||||
certificateBodyDAL: Pick<TCertificateBodyDALFactory, "create">;
|
||||
certificateSecretDAL: Pick<TCertificateSecretDALFactory, "create">;
|
||||
|
@@ -14,6 +14,8 @@ import { TCertificateBodyDALFactory } from "@app/services/certificate/certificat
|
||||
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
|
||||
import { TCertificateAuthorityCertDALFactory } from "@app/services/certificate-authority/certificate-authority-cert-dal";
|
||||
import { TCertificateAuthorityDALFactory } from "@app/services/certificate-authority/certificate-authority-dal";
|
||||
import { CaCapability, CaType } from "@app/services/certificate-authority/certificate-authority-enums";
|
||||
import { caSupportsCapability } from "@app/services/certificate-authority/certificate-authority-maps";
|
||||
import { TCertificateAuthoritySecretDALFactory } from "@app/services/certificate-authority/certificate-authority-secret-dal";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TPkiCollectionDALFactory } from "@app/services/pki-collection/pki-collection-dal";
|
||||
@@ -184,9 +186,11 @@ export const certificateServiceFactory = ({
|
||||
|
||||
const ca = await certificateAuthorityDAL.findByIdWithAssociatedCa(cert.caId);
|
||||
|
||||
if (ca.externalCa?.id) {
|
||||
// Check if the CA type supports revocation
|
||||
const caType = (ca.externalCa?.type as CaType) ?? CaType.INTERNAL;
|
||||
if (!caSupportsCapability(caType, CaCapability.REVOKE_CERTIFICATES)) {
|
||||
throw new BadRequestError({
|
||||
message: "Cannot revoke external certificates"
|
||||
message: "Certificate revocation is not supported by this certificate authority type"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,18 +222,37 @@ export const certificateServiceFactory = ({
|
||||
}
|
||||
);
|
||||
|
||||
// rebuild CRL (TODO: move to interval-based cron job)
|
||||
await rebuildCaCrl({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
certificateDAL,
|
||||
kmsService
|
||||
});
|
||||
// Note: External CA revocation handling would go here for supported CA types
|
||||
// Currently, only internal CAs and ACME CAs support revocation
|
||||
|
||||
return { revokedAt, cert, ca: expandInternalCa(ca) };
|
||||
// rebuild CRL (TODO: move to interval-based cron job)
|
||||
// Only rebuild CRL for internal CAs - external CAs manage their own CRLs
|
||||
if (!ca.externalCa?.id) {
|
||||
await rebuildCaCrl({
|
||||
caId: ca.id,
|
||||
certificateAuthorityDAL,
|
||||
certificateAuthorityCrlDAL,
|
||||
certificateAuthoritySecretDAL,
|
||||
projectDAL,
|
||||
certificateDAL,
|
||||
kmsService
|
||||
});
|
||||
}
|
||||
|
||||
// Return appropriate CA format based on CA type
|
||||
const caResult = ca.externalCa?.id
|
||||
? {
|
||||
id: ca.id,
|
||||
name: ca.name,
|
||||
projectId: ca.projectId,
|
||||
status: ca.status,
|
||||
enableDirectIssuance: ca.enableDirectIssuance,
|
||||
type: ca.externalCa.type,
|
||||
externalCa: ca.externalCa
|
||||
}
|
||||
: expandInternalCa(ca);
|
||||
|
||||
return { revokedAt, cert, ca: caResult };
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -156,6 +156,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
`${TableName.GroupProjectMembershipRole}.customRoleId`,
|
||||
`${TableName.ProjectRoles}.id`
|
||||
)
|
||||
.join(TableName.OrgMembership, `${TableName.Users}.id`, `${TableName.OrgMembership}.userId`)
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.UserGroupMembership),
|
||||
db.ref("createdAt").withSchema(TableName.UserGroupMembership),
|
||||
@@ -176,7 +177,8 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
db.ref("temporaryRange").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("temporaryAccessStartTime").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.GroupProjectMembershipRole),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("isActive").withSchema(TableName.OrgMembership)
|
||||
)
|
||||
.where({ isGhost: false });
|
||||
|
||||
@@ -192,7 +194,8 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
userId,
|
||||
projectName,
|
||||
createdAt
|
||||
createdAt,
|
||||
isActive
|
||||
}) => ({
|
||||
isGroupMember: true,
|
||||
id,
|
||||
@@ -202,7 +205,7 @@ export const groupProjectDALFactory = (db: TDbClient) => {
|
||||
id: projectId,
|
||||
name: projectName
|
||||
},
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost, isOrgMembershipActive: isActive },
|
||||
createdAt
|
||||
}),
|
||||
key: "id",
|
||||
|
@@ -21,7 +21,7 @@ import {
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
import { getValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@@ -189,7 +189,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
if (identityJwtAuth.boundClaims) {
|
||||
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
const value = getValueByDot(tokenData, claimKey);
|
||||
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
@@ -198,9 +198,7 @@ export const identityJwtAuthServiceFactory = ({
|
||||
}
|
||||
|
||||
// handle both single and multi-valued claims
|
||||
if (
|
||||
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry))
|
||||
) {
|
||||
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(value, claimEntry))) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: claim mismatch for field ${claimKey}`
|
||||
});
|
||||
|
@@ -1,7 +1,16 @@
|
||||
import picomatch from "picomatch";
|
||||
|
||||
export const doesFieldValueMatchOidcPolicy = (fieldValue: string, policyValue: string) =>
|
||||
policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||
export const doesFieldValueMatchOidcPolicy = (fieldValue: string | number | boolean, policyValue: string) => {
|
||||
if (typeof fieldValue === "boolean") {
|
||||
return fieldValue === (policyValue === "true");
|
||||
}
|
||||
|
||||
if (typeof fieldValue === "number") {
|
||||
return fieldValue === parseInt(policyValue, 10);
|
||||
}
|
||||
|
||||
return policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||
};
|
||||
|
||||
export const doesAudValueMatchOidcPolicy = (fieldValue: string | string[], policyValue: string) => {
|
||||
if (Array.isArray(fieldValue)) {
|
||||
|
@@ -22,7 +22,7 @@ import {
|
||||
UnauthorizedError
|
||||
} from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { getStringValueByDot } from "@app/lib/template/dot-access";
|
||||
import { getValueByDot } from "@app/lib/template/dot-access";
|
||||
|
||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||
@@ -146,7 +146,7 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (identityOidcAuth.boundClaims) {
|
||||
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
|
||||
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
const value = getValueByDot(tokenData, claimKey);
|
||||
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
@@ -167,13 +167,13 @@ export const identityOidcAuthServiceFactory = ({
|
||||
if (identityOidcAuth.claimMetadataMapping) {
|
||||
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
|
||||
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
|
||||
const value = getStringValueByDot(tokenData, claimKey) || "";
|
||||
const value = getValueByDot(tokenData, claimKey);
|
||||
if (!value) {
|
||||
throw new UnauthorizedError({
|
||||
message: `Access denied: token has no ${claimKey} field`
|
||||
});
|
||||
}
|
||||
filteredClaims[permissionKey] = value;
|
||||
filteredClaims[permissionKey] = value.toString();
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -124,12 +124,12 @@ export const orgMembershipDALFactory = (db: TDbClient) => {
|
||||
void qb
|
||||
.whereNull(`${TableName.OrgMembership}.lastInvitedAt`)
|
||||
.whereBetween(`${TableName.OrgMembership}.createdAt`, [twelveMonthsAgo, oneWeekAgo]);
|
||||
})
|
||||
.orWhere((qb) => {
|
||||
// lastInvitedAt is older than 1 week ago AND createdAt is younger than 1 month ago
|
||||
void qb
|
||||
.where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo)
|
||||
.where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo);
|
||||
void qb.orWhere((qbInner) => {
|
||||
void qbInner
|
||||
.where(`${TableName.OrgMembership}.lastInvitedAt`, "<", oneWeekAgo)
|
||||
.where(`${TableName.OrgMembership}.createdAt`, ">", oneMonthAgo);
|
||||
});
|
||||
});
|
||||
|
||||
return memberships;
|
||||
|
@@ -8,6 +8,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
authEnforced: true,
|
||||
googleSsoAuthEnforced: true,
|
||||
scimEnabled: true,
|
||||
kmsDefaultKeyId: true,
|
||||
defaultMembershipRole: true,
|
||||
|
@@ -364,6 +364,7 @@ export const orgServiceFactory = ({
|
||||
name,
|
||||
slug,
|
||||
authEnforced,
|
||||
googleSsoAuthEnforced,
|
||||
scimEnabled,
|
||||
defaultMembershipRoleSlug,
|
||||
enforceMfa,
|
||||
@@ -430,6 +431,21 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (googleSsoAuthEnforced !== undefined) {
|
||||
if (!plan.enforceGoogleSSO) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to enforce Google SSO due to plan restriction. Upgrade plan to enforce Google SSO."
|
||||
});
|
||||
}
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Sso);
|
||||
}
|
||||
|
||||
if (authEnforced && googleSsoAuthEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "SAML/OIDC auth enforcement and Google SSO auth enforcement cannot be enabled at the same time."
|
||||
});
|
||||
}
|
||||
|
||||
if (authEnforced) {
|
||||
const samlCfg = await samlConfigDAL.findOne({
|
||||
orgId,
|
||||
@@ -460,6 +476,21 @@ export const orgServiceFactory = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (googleSsoAuthEnforced) {
|
||||
if (googleSsoAuthEnforced && currentOrg.authEnforced) {
|
||||
throw new BadRequestError({
|
||||
message: "Google SSO auth enforcement cannot be enabled when SAML/OIDC auth enforcement is enabled."
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentOrg.googleSsoAuthLastUsed) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Google SSO auth enforcement cannot be enabled because Google SSO has not been used yet. Please log in via Google SSO at least once before enforcing it for your organization."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let defaultMembershipRole: string | undefined;
|
||||
if (defaultMembershipRoleSlug) {
|
||||
defaultMembershipRole = await getDefaultOrgMembershipRoleForUpdateOrg({
|
||||
@@ -474,6 +505,7 @@ export const orgServiceFactory = ({
|
||||
name,
|
||||
slug: slug ? slugify(slug) : undefined,
|
||||
authEnforced,
|
||||
googleSsoAuthEnforced,
|
||||
scimEnabled,
|
||||
defaultMembershipRole,
|
||||
enforceMfa,
|
||||
|
@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
|
||||
name: string;
|
||||
slug: string;
|
||||
authEnforced: boolean;
|
||||
googleSsoAuthEnforced: boolean;
|
||||
scimEnabled: boolean;
|
||||
defaultMembershipRoleSlug: string;
|
||||
enforceMfa: boolean;
|
||||
|
@@ -18,7 +18,8 @@ export const sanitizedPkiSubscriber = PkiSubscribersSchema.pick({
|
||||
lastOperationAt: true,
|
||||
enableAutoRenewal: true,
|
||||
autoRenewalPeriodInDays: true,
|
||||
lastAutoRenewAt: true
|
||||
lastAutoRenewAt: true,
|
||||
properties: true
|
||||
}).extend({
|
||||
supportsImmediateCertIssuance: z.boolean().optional()
|
||||
});
|
||||
|
@@ -109,6 +109,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays,
|
||||
properties,
|
||||
projectId,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
@@ -157,7 +158,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
autoRenewalPeriodInDays,
|
||||
properties
|
||||
});
|
||||
|
||||
return newSubscriber;
|
||||
@@ -221,6 +223,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays,
|
||||
properties,
|
||||
actorId,
|
||||
actorAuthMethod,
|
||||
actor,
|
||||
@@ -275,7 +278,8 @@ export const pkiSubscriberServiceFactory = ({
|
||||
keyUsages,
|
||||
extendedKeyUsages,
|
||||
enableAutoRenewal,
|
||||
autoRenewalPeriodInDays
|
||||
autoRenewalPeriodInDays,
|
||||
properties
|
||||
});
|
||||
|
||||
return updatedSubscriber;
|
||||
@@ -360,7 +364,7 @@ export const pkiSubscriberServiceFactory = ({
|
||||
throw new BadRequestError({ message: "CA is disabled" });
|
||||
}
|
||||
|
||||
if (ca.externalCa?.id && ca.externalCa.type === CaType.ACME) {
|
||||
if (ca.externalCa?.id && (ca.externalCa.type === CaType.ACME || ca.externalCa.type === CaType.AZURE_AD_CS)) {
|
||||
await certificateAuthorityQueue.orderCertificateForSubscriber({
|
||||
subscriberId: subscriber.id,
|
||||
caType: ca.externalCa.type
|
||||
|
@@ -18,6 +18,7 @@ export type TCreatePkiSubscriberDTO = {
|
||||
extendedKeyUsages: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
properties?: TPkiSubscriberProperties;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TGetPkiSubscriberDTO = {
|
||||
@@ -36,6 +37,7 @@ export type TUpdatePkiSubscriberDTO = {
|
||||
extendedKeyUsages?: CertExtendedKeyUsage[];
|
||||
enableAutoRenewal?: boolean;
|
||||
autoRenewalPeriodInDays?: number;
|
||||
properties?: TPkiSubscriberProperties;
|
||||
} & TProjectPermission;
|
||||
|
||||
export type TDeletePkiSubscriberDTO = {
|
||||
@@ -69,3 +71,13 @@ export enum SubscriberOperationStatus {
|
||||
SUCCESS = "success",
|
||||
FAILED = "failed"
|
||||
}
|
||||
|
||||
export type TPkiSubscriberProperties = {
|
||||
azureTemplateType?: string;
|
||||
organization?: string;
|
||||
organizationalUnit?: string;
|
||||
country?: string;
|
||||
state?: string;
|
||||
locality?: string;
|
||||
emailAddress?: string;
|
||||
};
|
||||
|
@@ -21,6 +21,14 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
.where({ [`${TableName.ProjectMembership}.projectId` as "projectId"]: projectId })
|
||||
.join(TableName.Project, `${TableName.ProjectMembership}.projectId`, `${TableName.Project}.id`)
|
||||
.join(TableName.Users, `${TableName.ProjectMembership}.userId`, `${TableName.Users}.id`)
|
||||
.join(TableName.OrgMembership, (qb) => {
|
||||
qb.on(`${TableName.Users}.id`, "=", `${TableName.OrgMembership}.userId`).andOn(
|
||||
`${TableName.OrgMembership}.orgId`,
|
||||
"=",
|
||||
`${TableName.Project}.orgId`
|
||||
);
|
||||
})
|
||||
|
||||
.where((qb) => {
|
||||
if (filter.usernames) {
|
||||
void qb.whereIn("username", filter.usernames);
|
||||
@@ -90,7 +98,8 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
db.ref("temporaryRange").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("temporaryAccessStartTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("temporaryAccessEndTime").withSchema(TableName.ProjectUserMembershipRole),
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project)
|
||||
db.ref("name").as("projectName").withSchema(TableName.Project),
|
||||
db.ref("isActive").withSchema(TableName.OrgMembership)
|
||||
)
|
||||
.where({ isGhost: false })
|
||||
.orderBy(`${TableName.Users}.username` as "username");
|
||||
@@ -107,12 +116,22 @@ export const projectMembershipDALFactory = (db: TDbClient) => {
|
||||
id,
|
||||
userId,
|
||||
projectName,
|
||||
createdAt
|
||||
createdAt,
|
||||
isActive
|
||||
}) => ({
|
||||
id,
|
||||
userId,
|
||||
projectId,
|
||||
user: { email, username, firstName, lastName, id: userId, publicKey, isGhost },
|
||||
user: {
|
||||
email,
|
||||
username,
|
||||
firstName,
|
||||
lastName,
|
||||
id: userId,
|
||||
publicKey,
|
||||
isGhost,
|
||||
isOrgMembershipActive: isActive
|
||||
},
|
||||
project: {
|
||||
id: projectId,
|
||||
name: projectName
|
||||
|
@@ -97,7 +97,6 @@ export const projectMembershipServiceFactory = ({
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId, { roles });
|
||||
|
||||
// projectMembers[0].project
|
||||
if (includeGroupMembers) {
|
||||
const groupMembers = await groupProjectDAL.findAllProjectGroupMembers(projectId);
|
||||
const allMembers = [
|
||||
|
@@ -23,56 +23,120 @@ export const ChecklySyncFns = {
|
||||
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||
if (config.groupId) {
|
||||
// Handle group environment variables
|
||||
const groupVars = await ChecklyPublicAPI.getCheckGroupEnvironmentVariables(
|
||||
secretSync.connection,
|
||||
config.accountId,
|
||||
config.groupId
|
||||
);
|
||||
|
||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||
const checklyGroupSecrets = Object.fromEntries(groupVars.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
try {
|
||||
// Prepare all variables to update at once
|
||||
const updatedVariables = { ...checklyGroupSecrets };
|
||||
|
||||
for (const key of Object.keys(secretMap)) {
|
||||
const entry = secretMap[key];
|
||||
|
||||
// If value is empty, we skip the upsert - checkly does not allow empty values
|
||||
// If value is empty, we skip adding it - checkly does not allow empty values
|
||||
if (entry.value.trim() === "") {
|
||||
// Delete the secret from Checkly if its empty
|
||||
// Delete the secret from the group if it's empty
|
||||
if (!disableSecretDeletion) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key
|
||||
});
|
||||
delete updatedVariables[key];
|
||||
}
|
||||
continue; // Skip empty values
|
||||
}
|
||||
|
||||
await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, {
|
||||
// Add or update the variable
|
||||
updatedVariables[key] = {
|
||||
key,
|
||||
value: entry.value,
|
||||
secret: true,
|
||||
locked: true
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// Remove secrets that are not in the secretMap if deletion is enabled
|
||||
if (!disableSecretDeletion) {
|
||||
for (const key of Object.keys(checklyGroupSecrets)) {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
|
||||
if (!secretMap[key]) {
|
||||
delete updatedVariables[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update all group environment variables at once
|
||||
try {
|
||||
await ChecklyPublicAPI.updateCheckGroupEnvironmentVariables(
|
||||
secretSync.connection,
|
||||
config.accountId,
|
||||
config.groupId,
|
||||
Object.values(updatedVariables)
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof SecretSyncError) throw error;
|
||||
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
secretKey: "group_update"
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Handle global variables (existing logic)
|
||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const key of Object.keys(checklySecrets)) {
|
||||
try {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
for await (const key of Object.keys(secretMap)) {
|
||||
try {
|
||||
const entry = secretMap[key];
|
||||
|
||||
if (!secretMap[key]) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key
|
||||
// If value is empty, we skip the upsert - checkly does not allow empty values
|
||||
if (entry.value.trim() === "") {
|
||||
// Delete the secret from Checkly if its empty
|
||||
if (!disableSecretDeletion) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key
|
||||
});
|
||||
}
|
||||
continue; // Skip empty values
|
||||
}
|
||||
|
||||
await ChecklyPublicAPI.upsertVariable(secretSync.connection, config.accountId, {
|
||||
key,
|
||||
value: entry.value,
|
||||
secret: true,
|
||||
locked: true
|
||||
});
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (disableSecretDeletion) return;
|
||||
|
||||
for await (const key of Object.keys(checklySecrets)) {
|
||||
try {
|
||||
// eslint-disable-next-line no-continue
|
||||
if (!matchesSchema(key, environment?.slug || "", keySchema)) continue;
|
||||
|
||||
if (!secretMap[key]) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: key
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -80,23 +144,54 @@ export const ChecklySyncFns = {
|
||||
async removeSecrets(secretSync: TChecklySyncWithCredentials, secretMap: TSecretMap) {
|
||||
const config = secretSync.destinationConfig;
|
||||
|
||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||
if (config.groupId) {
|
||||
// Handle group environment variables
|
||||
const groupVars = await ChecklyPublicAPI.getCheckGroupEnvironmentVariables(
|
||||
secretSync.connection,
|
||||
config.accountId,
|
||||
config.groupId
|
||||
);
|
||||
|
||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||
const checklyGroupSecrets = Object.fromEntries(groupVars.map((variable) => [variable.key, variable]));
|
||||
|
||||
// Filter out the secrets to remove
|
||||
const remainingVariables = Object.keys(checklyGroupSecrets)
|
||||
.filter((key) => !(key in secretMap))
|
||||
.map((key) => checklyGroupSecrets[key]);
|
||||
|
||||
for await (const secret of Object.keys(checklySecrets)) {
|
||||
try {
|
||||
if (secret in secretMap) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key: secret
|
||||
});
|
||||
}
|
||||
await ChecklyPublicAPI.updateCheckGroupEnvironmentVariables(
|
||||
secretSync.connection,
|
||||
config.accountId,
|
||||
config.groupId,
|
||||
remainingVariables
|
||||
);
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: secret
|
||||
secretKey: "group_remove"
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Handle global variables (existing logic)
|
||||
const variables = await ChecklyPublicAPI.getVariables(secretSync.connection, config.accountId);
|
||||
|
||||
const checklySecrets = Object.fromEntries(variables!.map((variable) => [variable.key, variable]));
|
||||
|
||||
for await (const secret of Object.keys(checklySecrets)) {
|
||||
try {
|
||||
if (secret in secretMap) {
|
||||
await ChecklyPublicAPI.deleteVariable(secretSync.connection, config.accountId, {
|
||||
key: secret
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
throw new SecretSyncError({
|
||||
error,
|
||||
secretKey: secret
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -11,7 +11,17 @@ import { TSyncOptionsConfig } from "@app/services/secret-sync/secret-sync-types"
|
||||
|
||||
const ChecklySyncDestinationConfigSchema = z.object({
|
||||
accountId: z.string().min(1, "Account ID is required").max(255, "Account ID must be less than 255 characters"),
|
||||
accountName: z.string().min(1, "Account Name is required").max(255, "Account ID must be less than 255 characters")
|
||||
accountName: z
|
||||
.string()
|
||||
.min(1, "Account Name is required")
|
||||
.max(255, "Account ID must be less than 255 characters")
|
||||
.optional(),
|
||||
groupId: z.string().min(1, "Group ID is required").max(255, "Group ID must be less than 255 characters").optional(),
|
||||
groupName: z
|
||||
.string()
|
||||
.min(1, "Group Name is required")
|
||||
.max(255, "Group Name must be less than 255 characters")
|
||||
.optional()
|
||||
});
|
||||
|
||||
const ChecklySyncOptionsConfig: TSyncOptionsConfig = { canImportSecrets: false };
|
||||
|
@@ -207,7 +207,7 @@ export const GithubSyncFns = {
|
||||
const token =
|
||||
connection.method === GitHubConnectionMethod.OAuth
|
||||
? connection.credentials.accessToken
|
||||
: await getGitHubAppAuthToken(connection);
|
||||
: await getGitHubAppAuthToken(connection, gatewayService);
|
||||
|
||||
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
|
||||
const publicKey = await getPublicKey(secretSync, gatewayService, token);
|
||||
@@ -264,7 +264,7 @@ export const GithubSyncFns = {
|
||||
const token =
|
||||
connection.method === GitHubConnectionMethod.OAuth
|
||||
? connection.credentials.accessToken
|
||||
: await getGitHubAppAuthToken(connection);
|
||||
: await getGitHubAppAuthToken(connection, gatewayService);
|
||||
|
||||
const encryptedSecrets = await getEncryptedSecrets(secretSync, gatewayService);
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export enum RenderSyncScope {
|
||||
Service = "service"
|
||||
Service = "service",
|
||||
EnvironmentGroup = "environment-group"
|
||||
}
|
||||
|
||||
export enum RenderSyncType {
|
||||
|
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable no-await-in-loop */
|
||||
import { isAxiosError } from "axios";
|
||||
import { AxiosRequestConfig, isAxiosError } from "axios";
|
||||
|
||||
import { request } from "@app/lib/config/request";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
|
||||
import { matchesSchema } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import { TSecretMap } from "@app/services/secret-sync/secret-sync-types";
|
||||
|
||||
import { RenderSyncScope } from "./render-sync-enums";
|
||||
import { TRenderSecret, TRenderSyncWithCredentials } from "./render-sync-types";
|
||||
|
||||
const MAX_RETRIES = 5;
|
||||
@@ -27,6 +29,80 @@ const makeRequestWithRetry = async <T>(requestFn: () => Promise<T>, attempt = 0)
|
||||
}
|
||||
};
|
||||
|
||||
async function getSecrets(input: { destination: TRenderSyncWithCredentials["destinationConfig"]; token: string }) {
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${input.token}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (input.destination.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
req.url = `/services/${input.destination.serviceId}/env-vars`;
|
||||
|
||||
const allSecrets: TRenderSecret[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
||||
do {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
const { data } = await makeRequestWithRetry(() =>
|
||||
request.request<
|
||||
{
|
||||
envVar: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
cursor: string;
|
||||
}[]
|
||||
>({
|
||||
...req,
|
||||
params: {
|
||||
cursor
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const secrets = data.map((item) => ({
|
||||
key: item.envVar.key,
|
||||
value: item.envVar.value
|
||||
}));
|
||||
|
||||
allSecrets.push(...secrets);
|
||||
|
||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||
cursor = data[data.length - 1].cursor;
|
||||
} else {
|
||||
cursor = undefined;
|
||||
}
|
||||
} while (cursor);
|
||||
|
||||
return allSecrets;
|
||||
}
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
req.url = `/env-groups/${input.destination.environmentGroupId}`;
|
||||
|
||||
const res = await makeRequestWithRetry(() =>
|
||||
request.request<{
|
||||
envVars: {
|
||||
key: string;
|
||||
value: string;
|
||||
}[];
|
||||
}>(req)
|
||||
);
|
||||
|
||||
return res.data.envVars.map((item) => ({
|
||||
key: item.key,
|
||||
value: item.value
|
||||
}));
|
||||
}
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
}
|
||||
|
||||
const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredentials): Promise<TRenderSecret[]> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
@@ -35,45 +111,12 @@ const getRenderEnvironmentSecrets = async (secretSync: TRenderSyncWithCredential
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const baseUrl = `${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`;
|
||||
const allSecrets: TRenderSecret[] = [];
|
||||
let cursor: string | undefined;
|
||||
const secrets = await getSecrets({
|
||||
destination: destinationConfig,
|
||||
token: apiKey
|
||||
});
|
||||
|
||||
do {
|
||||
const url = cursor ? `${baseUrl}?cursor=${cursor}` : baseUrl;
|
||||
|
||||
const { data } = await makeRequestWithRetry(() =>
|
||||
request.get<
|
||||
{
|
||||
envVar: {
|
||||
key: string;
|
||||
value: string;
|
||||
};
|
||||
cursor: string;
|
||||
}[]
|
||||
>(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const secrets = data.map((item) => ({
|
||||
key: item.envVar.key,
|
||||
value: item.envVar.value
|
||||
}));
|
||||
|
||||
allSecrets.push(...secrets);
|
||||
|
||||
if (data.length > 0 && data[data.length - 1]?.cursor) {
|
||||
cursor = data[data.length - 1].cursor;
|
||||
} else {
|
||||
cursor = undefined;
|
||||
}
|
||||
} while (cursor);
|
||||
|
||||
return allSecrets;
|
||||
return secrets;
|
||||
};
|
||||
|
||||
const batchUpdateEnvironmentSecrets = async (
|
||||
@@ -87,14 +130,91 @@ const batchUpdateEnvironmentSecrets = async (
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.put(`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/env-vars`, envVars, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "PUT",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${destinationConfig.serviceId}/env-vars`,
|
||||
data: envVars
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
for await (const variable of envVars) {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${variable.key}`,
|
||||
data: {
|
||||
value: variable.value
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteEnvironmentSecret = async (
|
||||
secretSync: TRenderSyncWithCredentials,
|
||||
envVar: { key: string; value: string }
|
||||
): Promise<void> => {
|
||||
const {
|
||||
destinationConfig,
|
||||
connection: {
|
||||
credentials: { apiKey }
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${destinationConfig.serviceId}/env-vars/${envVar.key}`
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}/env-vars/${envVar.key}`
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
@@ -105,18 +225,50 @@ const redeployService = async (secretSync: TRenderSyncWithCredentials) => {
|
||||
}
|
||||
} = secretSync;
|
||||
|
||||
await makeRequestWithRetry(() =>
|
||||
request.post(
|
||||
`${IntegrationUrls.RENDER_API_URL}/v1/services/${destinationConfig.serviceId}/deploys`,
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
const req: AxiosRequestConfig = {
|
||||
baseURL: `${IntegrationUrls.RENDER_API_URL}/v1`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
};
|
||||
|
||||
switch (destinationConfig.scope) {
|
||||
case RenderSyncScope.Service: {
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
method: "POST",
|
||||
url: `/services/${destinationConfig.serviceId}/deploys`,
|
||||
data: {}
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case RenderSyncScope.EnvironmentGroup: {
|
||||
const { data } = await request.request<{ serviceLinks: { id: string }[] }>({
|
||||
...req,
|
||||
method: "GET",
|
||||
url: `/env-groups/${destinationConfig.environmentGroupId}`
|
||||
});
|
||||
|
||||
for await (const link of data.serviceLinks) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
await makeRequestWithRetry(() =>
|
||||
request.request({
|
||||
...req,
|
||||
url: `/services/${link.id}/deploys`,
|
||||
data: {}
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new BadRequestError({ message: "Unknown render sync destination scope" });
|
||||
}
|
||||
};
|
||||
|
||||
export const RenderSyncFns = {
|
||||
@@ -169,14 +321,15 @@ export const RenderSyncFns = {
|
||||
const finalEnvVars: Array<{ key: string; value: string }> = [];
|
||||
|
||||
for (const renderSecret of renderSecrets) {
|
||||
if (!(renderSecret.key in secretMap)) {
|
||||
if (renderSecret.key in secretMap) {
|
||||
finalEnvVars.push({
|
||||
key: renderSecret.key,
|
||||
value: renderSecret.value
|
||||
});
|
||||
}
|
||||
}
|
||||
await batchUpdateEnvironmentSecrets(secretSync, finalEnvVars);
|
||||
|
||||
await Promise.all(finalEnvVars.map((el) => deleteEnvironmentSecret(secretSync, el)));
|
||||
|
||||
if (secretSync.syncOptions.autoRedeployServices) {
|
||||
await redeployService(secretSync);
|
||||
|
@@ -17,6 +17,14 @@ const RenderSyncDestinationConfigSchema = z.discriminatedUnion("scope", [
|
||||
scope: z.literal(RenderSyncScope.Service).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||
serviceId: z.string().min(1, "Service ID is required").describe(SecretSyncs.DESTINATION_CONFIG.RENDER.serviceId),
|
||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||
}),
|
||||
z.object({
|
||||
scope: z.literal(RenderSyncScope.EnvironmentGroup).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.scope),
|
||||
environmentGroupId: z
|
||||
.string()
|
||||
.min(1, "Environment Group ID is required")
|
||||
.describe(SecretSyncs.DESTINATION_CONFIG.RENDER.environmentGroupId),
|
||||
type: z.nativeEnum(RenderSyncType).describe(SecretSyncs.DESTINATION_CONFIG.RENDER.type)
|
||||
})
|
||||
]);
|
||||
|
||||
|
@@ -684,9 +684,9 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
throw new BadRequestError({ message: "Missing personal user id" });
|
||||
}
|
||||
void bd.orWhere({
|
||||
key: el.key,
|
||||
type: el.type,
|
||||
userId: el.type === SecretType.Personal ? el.userId : null
|
||||
[`${TableName.SecretV2}.key` as "key"]: el.key,
|
||||
[`${TableName.SecretV2}.type` as "type"]: el.type,
|
||||
[`${TableName.SecretV2}.userId` as "userId"]: el.type === SecretType.Personal ? el.userId : null
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -695,12 +695,60 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretRotationV2SecretMapping}.secretId`
|
||||
)
|
||||
|
||||
.leftJoin(
|
||||
TableName.SecretV2JnTag,
|
||||
`${TableName.SecretV2}.id`,
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretV2}Id`
|
||||
)
|
||||
.leftJoin(
|
||||
TableName.SecretTag,
|
||||
`${TableName.SecretV2JnTag}.${TableName.SecretTag}Id`,
|
||||
`${TableName.SecretTag}.id`
|
||||
)
|
||||
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
|
||||
.select(db.ref("id").withSchema(TableName.SecretTag).as("tagId"))
|
||||
.select(db.ref("color").withSchema(TableName.SecretTag).as("tagColor"))
|
||||
.select(db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"))
|
||||
.select(
|
||||
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
|
||||
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
|
||||
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue")
|
||||
)
|
||||
.select(selectAllTableCols(TableName.SecretV2))
|
||||
.select(db.ref("rotationId").withSchema(TableName.SecretRotationV2SecretMapping));
|
||||
return secrets.map((secret) => ({
|
||||
...secret,
|
||||
isRotatedSecret: Boolean(secret.rotationId)
|
||||
}));
|
||||
|
||||
const docs = sqlNestRelationships({
|
||||
data: secrets,
|
||||
key: "id",
|
||||
parentMapper: (secret) => ({
|
||||
...secret,
|
||||
isRotatedSecret: Boolean(secret.rotationId)
|
||||
}),
|
||||
childrenMapper: [
|
||||
{
|
||||
key: "tagId",
|
||||
label: "tags" as const,
|
||||
mapper: ({ tagId: id, tagColor: color, tagSlug: slug }) => ({
|
||||
id,
|
||||
color,
|
||||
slug,
|
||||
name: slug
|
||||
})
|
||||
},
|
||||
{
|
||||
key: "metadataId",
|
||||
label: "secretMetadata" as const,
|
||||
mapper: ({ metadataKey, metadataValue, metadataId }) => ({
|
||||
id: metadataId,
|
||||
key: metadataKey,
|
||||
value: metadataValue
|
||||
})
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return docs;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "find by secret keys" });
|
||||
}
|
||||
|
@@ -1074,12 +1074,22 @@ export const secretV2BridgeServiceFactory = ({
|
||||
currentPath: path
|
||||
});
|
||||
|
||||
if (!deepPaths) return { secrets: [], imports: [] };
|
||||
if (!deepPaths?.length) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder with path '${path}' in environment '${environment}' was not found. Please ensure the environment slug and secret path is correct.`,
|
||||
name: "SecretPathNotFound"
|
||||
});
|
||||
}
|
||||
|
||||
paths = deepPaths.map(({ folderId, path: p }) => ({ folderId, path: p }));
|
||||
} else {
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) return { secrets: [], imports: [] };
|
||||
if (!folder) {
|
||||
throw new NotFoundError({
|
||||
message: `Folder with path '${path}' in environment '${environment}' was not found. Please ensure the environment slug and secret path is correct.`,
|
||||
name: "SecretPathNotFound"
|
||||
});
|
||||
}
|
||||
|
||||
paths = [{ folderId: folder.id, path }];
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user